init
This commit is contained in:
commit
90525cdfd8
22 files changed
+9590
No files matched your search
+34
@@ -0,0 +1,34 @@
|
||||
# prod
|
||||
dist/
|
||||
|
||||
# dev
|
||||
.yarn/
|
||||
!.yarn/releases
|
||||
.vscode/*
|
||||
!.vscode/launch.json
|
||||
!.vscode/*.code-snippets
|
||||
.idea/workspace.xml
|
||||
.idea/usage.statistics.xml
|
||||
.idea/shelf
|
||||
|
||||
# deps
|
||||
node_modules/
|
||||
.wrangler
|
||||
|
||||
# env
|
||||
.env
|
||||
.env.production
|
||||
.dev.vars
|
||||
|
||||
# logs
|
||||
logs/
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
wrangler.toml
|
||||
@@ -0,0 +1,342 @@
|
||||
# AWSL One API
|
||||
|
||||
一个基于 Cloudflare Workers 的 OpenAI API 代理服务,支持多渠道管理、Token 管理和使用量统计。
|
||||
|
||||
## ✨ 特性
|
||||
|
||||
- 🚀 **基于 Cloudflare Workers**:无服务器架构,全球边缘部署
|
||||
- 🔐 **多渠道支持**:支持 Azure OpenAI 等多种 AI 服务提供商
|
||||
- 🎫 **Token 管理**:完整的 API Token 生成、管理和配额控制
|
||||
- 📊 **使用量统计**:实时统计 API 使用量和费用
|
||||
- 💰 **定价管理**:灵活的模型定价配置
|
||||
- 🎨 **Web 管理界面**:直观的 Web 界面进行配置管理
|
||||
- 🧪 **API 测试工具**:内置 API 测试功能,支持实时调试
|
||||
- 📚 **OpenAPI 文档**:自动生成的 API 文档
|
||||
|
||||
## 🏗️ 项目结构
|
||||
|
||||
```text
|
||||
awsl-one-api/
|
||||
├── src/ # 源代码目录
|
||||
│ ├── admin/ # 管理接口
|
||||
│ │ ├── channel_api.ts # 频道管理 API
|
||||
│ │ ├── token_api.ts # Token 管理 API
|
||||
│ │ ├── pricing_api.ts # 定价管理 API
|
||||
│ │ ├── db_api.ts # 数据库管理 API
|
||||
│ │ └── index.ts # 管理接口路由
|
||||
│ ├── providers/ # AI 服务提供商
|
||||
│ │ ├── azure-openai-proxy.ts # Azure OpenAI 代理
|
||||
│ │ └── index.ts # 提供商路由
|
||||
│ ├── db/ # 数据库相关
|
||||
│ ├── model/ # 数据模型
|
||||
│ ├── constants.ts # 常量定义
|
||||
│ ├── utils.ts # 工具函数
|
||||
│ └── index.ts # 主入口文件
|
||||
├── public/ # 静态文件
|
||||
│ └── index.html # Web 管理界面
|
||||
├── type.d.ts # 类型定义
|
||||
├── wrangler.toml # Cloudflare Workers 配置
|
||||
└── package.json # 项目配置
|
||||
```
|
||||
|
||||
## 🏗️ 系统架构
|
||||
|
||||
```text
|
||||
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
||||
│ 客户端应用 │───▶│ AWSL One API │───▶│ AI 服务商 │
|
||||
│ │ │ (CF Workers) │ │ (Azure OpenAI) │
|
||||
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────┐
|
||||
│ Cloudflare D1 │
|
||||
│ 数据库 │
|
||||
└─────────────────┘
|
||||
```
|
||||
|
||||
## 🚀 快速开始
|
||||
|
||||
### 环境要求
|
||||
|
||||
- Node.js 18+
|
||||
- pnpm
|
||||
- Cloudflare Workers 账户
|
||||
|
||||
### 安装依赖
|
||||
|
||||
```bash
|
||||
pnpm install
|
||||
```
|
||||
|
||||
### 配置环境
|
||||
|
||||
1. 复制 `wrangler.toml.template` 为 `wrangler.toml` 并修改配置:
|
||||
|
||||
```toml
|
||||
name = "awsl-one-api"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-04-28"
|
||||
routes = [
|
||||
{ pattern = "your-domain.com", custom_domain = true },
|
||||
]
|
||||
|
||||
[vars]
|
||||
ADMIN_TOKEN = "your-secure-admin-token-here"
|
||||
|
||||
[assets]
|
||||
directory = "public"
|
||||
binding = "ASSETS"
|
||||
run_worker_first = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "awsl-one-api"
|
||||
database_id = "your-database-id"
|
||||
```
|
||||
|
||||
1. 创建 Cloudflare D1 数据库:
|
||||
|
||||
```bash
|
||||
wrangler d1 create awsl-one-api
|
||||
```
|
||||
|
||||
### 本地开发
|
||||
|
||||
```bash
|
||||
pnpm dev
|
||||
```
|
||||
|
||||
### 部署到生产环境
|
||||
|
||||
```bash
|
||||
pnpm deploy
|
||||
```
|
||||
|
||||
## 📖 使用指南
|
||||
|
||||
### 初始化数据库
|
||||
|
||||
首次部署后,需要初始化数据库。可以通过以下方式之一:
|
||||
|
||||
#### 方式一:Web 界面(推荐)
|
||||
|
||||
1. 访问 `https://your-domain.com`
|
||||
2. 使用管理员 Token 登录
|
||||
3. 切换到 **📊 数据库** 标签
|
||||
4. 点击 **🔄 初始化数据库** 按钮
|
||||
|
||||
#### 方式二:API 调用
|
||||
|
||||
```bash
|
||||
curl -X POST https://your-domain.com/api/admin/db_initialize \
|
||||
-H "x-admin-token: your-admin-token"
|
||||
```
|
||||
|
||||
### 频道配置
|
||||
|
||||
1. 在 Web 界面切换到 **🔗 频道管理** 标签
|
||||
2. 点击 **➕ 添加频道** 按钮
|
||||
3. 填写频道标识和配置信息
|
||||
4. 点击 **💾 保存频道** 按钮
|
||||
|
||||
### Token 创建和使用
|
||||
|
||||
1. 在 Web 界面切换到 **🔑 令牌管理** 标签
|
||||
2. 点击 **➕ 添加令牌** 按钮
|
||||
3. 填写令牌名称,系统会自动生成 `sk-` 开头的 Token
|
||||
4. 配置允许访问的频道和配额
|
||||
5. 点击 **💾 保存令牌** 按钮
|
||||
6. 使用 **📋 复制** 按钮获取 Token 用于 API 调用
|
||||
|
||||
### OpenAI 兼容 API
|
||||
|
||||
本项目提供完全兼容的 OpenAI API 接口:
|
||||
|
||||
```bash
|
||||
curl https://your-domain.com/v1/chat/completions \
|
||||
-H "Content-Type: application/json" \
|
||||
-H "Authorization: Bearer YOUR_API_TOKEN" \
|
||||
-d '{
|
||||
"model": "gpt-4",
|
||||
"messages": [
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Hello, world!"
|
||||
}
|
||||
]
|
||||
}'
|
||||
```
|
||||
|
||||
### API 测试工具
|
||||
|
||||
管理界面内置了强大的 API 测试工具,无需额外工具即可进行 API 调试:
|
||||
|
||||
#### 功能特性
|
||||
|
||||
- **🚀 一键测试**:直接在 Web 界面中测试 API 调用
|
||||
- **📝 JSON 编辑器**:支持 JSON 格式验证和语法高亮
|
||||
- **⚡ 实时响应**:显示响应时间、状态码和完整响应内容
|
||||
- **🔍 错误诊断**:自动区分 HTTP 错误和 JSON 响应,便于排查问题
|
||||
- **📋 一键复制**:支持复制 Token 和响应内容
|
||||
|
||||
#### 使用步骤
|
||||
|
||||
1. 访问管理界面,切换到 **🧪 API 测试** 标签
|
||||
2. 输入你的 API Token(可从令牌管理页面复制)
|
||||
3. 编辑请求 JSON(预填充标准格式)
|
||||
4. 点击 **🚀 发送请求** 按钮
|
||||
5. 查看响应结果和状态信息
|
||||
|
||||
## 🛠️ 管理功能
|
||||
|
||||
### Web 管理界面
|
||||
|
||||
访问 `https://your-domain.com` 即可使用 Web 管理界面,功能包括:
|
||||
|
||||
- **📊 数据库管理**:一键初始化数据库表结构
|
||||
- **🔗 频道配置管理**:添加、编辑、删除 AI 服务提供商频道
|
||||
- **🔑 API Token 管理**:生成、管理和监控 API Token 使用情况
|
||||
- **💰 定价配置**:灵活配置不同模型的定价策略
|
||||
- **🧪 API 测试工具**:内置 API 测试界面,支持实时调试和错误排查
|
||||
|
||||
#### 管理界面特性
|
||||
|
||||
- **现代化 UI**:响应式设计,支持桌面和移动设备
|
||||
- **实时反馈**:操作结果即时显示,支持悬浮提示
|
||||
- **智能表单**:自动生成 Token、JSON 格式验证、一键复制功能
|
||||
- **安全认证**:管理员 Token 认证,数据安全保护
|
||||
|
||||
### API 管理接口
|
||||
|
||||
所有管理 API 需要在请求头中包含 `x-admin-token`。
|
||||
|
||||
#### 数据库管理
|
||||
|
||||
```bash
|
||||
# 初始化数据库
|
||||
POST /api/admin/db_initialize
|
||||
```
|
||||
|
||||
#### 频道管理
|
||||
|
||||
```bash
|
||||
# 获取频道列表
|
||||
GET /api/admin/channel
|
||||
|
||||
# 创建/更新频道
|
||||
POST /api/admin/channel/{key}
|
||||
|
||||
# 删除频道
|
||||
DELETE /api/admin/channel/{key}
|
||||
```
|
||||
|
||||
#### Token 管理
|
||||
|
||||
```bash
|
||||
# 获取 Token 列表
|
||||
GET /api/admin/token
|
||||
|
||||
# 创建/更新 Token
|
||||
POST /api/admin/token/{key}
|
||||
|
||||
# 删除 Token
|
||||
DELETE /api/admin/token/{key}
|
||||
```
|
||||
|
||||
#### 定价管理
|
||||
|
||||
```bash
|
||||
# 获取定价配置
|
||||
GET /api/admin/pricing
|
||||
|
||||
# 更新定价配置
|
||||
POST /api/admin/pricing
|
||||
```
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 渠道配置
|
||||
|
||||
支持多种 AI 服务提供商的配置,以 Azure OpenAI 为例:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Azure OpenAI",
|
||||
"type": "azure-openai",
|
||||
"endpoint": "https://your-resource.openai.azure.com/",
|
||||
"api_key": "your-api-key",
|
||||
"api_version": "2024-02-15-preview",
|
||||
"deployment_mapper": {
|
||||
"gpt-4": "gpt-4-deployment-name",
|
||||
"gpt-3.5-turbo": "gpt-35-turbo-deployment-name"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**配置说明**:
|
||||
|
||||
- `name`: 频道显示名称
|
||||
- `type`: 服务提供商类型(如 `azure-openai`)
|
||||
- `endpoint`: API 端点地址
|
||||
- `api_key`: API 密钥
|
||||
- `api_version`: API 版本
|
||||
- `deployment_mapper`: 模型名称映射关系
|
||||
|
||||
### Token 配置
|
||||
|
||||
支持详细的 Token 配置,包括名称、访问权限和配额管理:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "用户令牌1",
|
||||
"channel_keys": ["azure-openai-1", "azure-openai-2"],
|
||||
"total_quota": 1000000
|
||||
}
|
||||
```
|
||||
|
||||
**配置说明**:
|
||||
|
||||
- `name`: Token 名称,便于管理识别
|
||||
- `channel_keys`: 允许访问的频道列表,空数组表示允许所有频道
|
||||
- `total_quota`: 总配额(基础单位:1百万 token = $1.00)
|
||||
|
||||
## 📊 监控与统计
|
||||
|
||||
- **使用量统计**:自动记录每次 API 调用的 Token 使用量
|
||||
- **费用计算**:基于模型定价自动计算费用
|
||||
- **配额管理**:支持 Token 级别的配额限制
|
||||
- **实时监控**:Web 界面实时显示使用情况和剩余配额
|
||||
|
||||
## 🎯 核心优势
|
||||
|
||||
- **零配置部署**:基于 Cloudflare Workers,无需服务器维护
|
||||
- **全球加速**:利用 Cloudflare 全球边缘网络,低延迟访问
|
||||
- **成本优化**:按需计费,无固定服务器成本
|
||||
- **高可用性**:Cloudflare 基础设施保证 99.9% 可用性
|
||||
- **安全可靠**:内置 Token 认证和配额管理机制
|
||||
|
||||
## 🔒 安全性
|
||||
|
||||
- **Token 认证**:所有 API 调用需要有效的 Bearer Token
|
||||
- **管理员认证**:管理接口使用独立的管理员 Token
|
||||
- **CORS 支持**:配置跨域访问策略
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
部署后可访问以下地址查看完整 API 文档:
|
||||
|
||||
- Swagger UI: `https://your-domain.com/api/docs`
|
||||
- ReDoc: `https://your-domain.com/api/redocs`
|
||||
- OpenAPI JSON: `https://your-domain.com/api/openapi.json`
|
||||
|
||||
## 🤝 贡献
|
||||
|
||||
欢迎提交 Issue 和 Pull Request!
|
||||
|
||||
## 📄 许可证
|
||||
|
||||
MIT License
|
||||
|
||||
## 🙋♂️ 支持
|
||||
|
||||
如有问题或建议,请创建 Issue 或联系维护者。
|
||||
@@ -0,0 +1,17 @@
|
||||
{
|
||||
"name": "awsl-ai-proxy",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy --minify",
|
||||
"cf-typegen": "wrangler types --include-env=false"
|
||||
},
|
||||
"dependencies": {
|
||||
"chanfana": "^2.8.0",
|
||||
"hono": "^4.8.2",
|
||||
"zod": "^3.24.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"wrangler": "^4.14.2"
|
||||
},
|
||||
"packageManager": "pnpm@10.10.0+sha512.d615db246fe70f25dcfea6d8d73dee782ce23e2245e3c4f6f888249fb568149318637dca73c2c5c8ef2a4ca0d5657fb9567188bfab47f566d1ee6ce987815c39"
|
||||
}
|
||||
Generated
+972
@@ -0,0 +1,972 @@
|
||||
lockfileVersion: '9.0'
|
||||
|
||||
settings:
|
||||
autoInstallPeers: true
|
||||
excludeLinksFromLockfile: false
|
||||
|
||||
importers:
|
||||
|
||||
.:
|
||||
dependencies:
|
||||
chanfana:
|
||||
specifier: ^2.8.0
|
||||
version: 2.8.0
|
||||
hono:
|
||||
specifier: ^4.8.2
|
||||
version: 4.8.2
|
||||
zod:
|
||||
specifier: ^3.24.4
|
||||
version: 3.24.4
|
||||
devDependencies:
|
||||
wrangler:
|
||||
specifier: ^4.14.2
|
||||
version: 4.14.2
|
||||
|
||||
packages:
|
||||
|
||||
'@asteasolutions/zod-to-openapi@7.3.0':
|
||||
resolution: {integrity: sha512-7tE/r1gXwMIvGnXVUdIqUhCU1RevEFC4Jk6Bussa0fk1ecbnnINkZzj1EOAJyE/M3AI25DnHT/zKQL1/FPFi8Q==}
|
||||
peerDependencies:
|
||||
zod: ^3.20.2
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.4.0':
|
||||
resolution: {integrity: sha512-+tv3z+SPp+gqTIcImN9o0hqE9xyfQjI1XD9pL6NuKjua9B1y7mNYv0S9cP+QEbA4ppVgGZEmKOvHX5G5Ei1CVA==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
|
||||
'@cloudflare/unenv-preset@2.3.1':
|
||||
resolution: {integrity: sha512-Xq57Qd+ADpt6hibcVBO0uLG9zzRgyRhfCUgBT9s+g3+3Ivg5zDyVgLFy40ES1VdNcu8rPNSivm9A+kGP5IVaPg==}
|
||||
peerDependencies:
|
||||
unenv: 2.0.0-rc.15
|
||||
workerd: ^1.20250320.0
|
||||
peerDependenciesMeta:
|
||||
workerd:
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20250428.0':
|
||||
resolution: {integrity: sha512-6nVe9oV4Hdec6ctzMtW80TiDvNTd2oFPi3VsKqSDVaJSJbL+4b6seyJ7G/UEPI+si6JhHBSLV2/9lNXNGLjClA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20250428.0':
|
||||
resolution: {integrity: sha512-/TB7bh7SIJ5f+6r4PHsAz7+9Qal/TK1cJuKFkUno1kqGlZbdrMwH0ATYwlWC/nBFeu2FB3NUolsTntEuy23hnQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20250428.0':
|
||||
resolution: {integrity: sha512-9eCbj+R3CKqpiXP6DfAA20DxKge+OTj7Hyw3ZewiEhWH9INIHiJwJQYybu4iq9kJEGjnGvxgguLFjSCWm26hgg==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20250428.0':
|
||||
resolution: {integrity: sha512-D9NRBnW46nl1EQsP13qfkYb5lbt4C6nxl38SBKY/NOcZAUoHzNB5K0GaK8LxvpkM7X/97ySojlMfR5jh5DNXYQ==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20250428.0':
|
||||
resolution: {integrity: sha512-RQCRj28eitjKD0tmei6iFOuWqMuHMHdNGEigRmbkmuTlpbWHNAoHikgCzZQ/dkKDdatA76TmcpbyECNf31oaTA==}
|
||||
engines: {node: '>=16'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
resolution: {integrity: sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==}
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.2':
|
||||
resolution: {integrity: sha512-wCIboOL2yXZym2cgm6mlA742s9QeJ8DjGVaL39dLN4rRwrOgOyYSnOaFPhKZGLb2ngj4EyfAFjsNJwPXZvseag==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [aix]
|
||||
|
||||
'@esbuild/android-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-5ZAX5xOmTligeBaeNEPnPaeEuah53Id2tX4c2CVP3JaROTH+j4fnfHCkr1PjXMd78hMst+TlkfKcW/DlTq0i4w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-arm@0.25.2':
|
||||
resolution: {integrity: sha512-NQhH7jFstVY5x8CKbcfa166GoV0EFkaPkCKBQkdPJFvo5u+nGXLEH/ooniLb3QI8Fk58YAx7nsPLozUWfCBOJA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/android-x64@0.25.2':
|
||||
resolution: {integrity: sha512-Ffcx+nnma8Sge4jzddPHCZVRvIfQ0kMsUsCMcJRHkGJ1cDmhe4SsrYIjLUKn1xpHZybmOqCWwB0zQvsjdEHtkg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [android]
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-MpM6LUVTXAzOvN4KbjzU/q5smzryuoNjlriAIx+06RpecwCkL9JpenNzpKd2YMzLJFOdPqBpuub6eVRP5IgiSA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/darwin-x64@0.25.2':
|
||||
resolution: {integrity: sha512-5eRPrTX7wFyuWe8FqEFPG2cU0+butQQVNcT4sVipqjLYQjjh8a8+vUTfgBKM88ObB85ahsnTwF7PSIt6PG+QkA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-mLwm4vXKiQ2UTSX4+ImyiPdiHjiZhIaE9QvC7sw0tZ6HoNMjYAqQpGyui5VRIi5sGd+uWq940gdCbY3VLvsO1w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.2':
|
||||
resolution: {integrity: sha512-6qyyn6TjayJSwGpm8J9QYYGQcRgc90nmfdUb0O7pp1s4lTY+9D0H9O02v5JqGApUyiHOtkz6+1hZNvNtEhbwRQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [freebsd]
|
||||
|
||||
'@esbuild/linux-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-gq/sjLsOyMT19I8obBISvhoYiZIAaGF8JpeXu1u8yPv8BE5HlWYobmlsfijFIZ9hIVGYkbdFhEqC0NvM4kNO0g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-arm@0.25.2':
|
||||
resolution: {integrity: sha512-UHBRgJcmjJv5oeQF8EpTRZs/1knq6loLxTsjc3nxO9eXAPDLcWW55flrMVc97qFPbmZP31ta1AZVUKQzKTzb0g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ia32@0.25.2':
|
||||
resolution: {integrity: sha512-bBYCv9obgW2cBP+2ZWfjYTU+f5cxRoGGQ5SeDbYdFCAZpYWrfjjfYwvUpP8MlKbP0nwZ5gyOU/0aUzZ5HWPuvQ==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-loong64@0.25.2':
|
||||
resolution: {integrity: sha512-SHNGiKtvnU2dBlM5D8CXRFdd+6etgZ9dXfaPCeJtz+37PIUlixvlIhI23L5khKXs3DIzAn9V8v+qb1TRKrgT5w==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.2':
|
||||
resolution: {integrity: sha512-hDDRlzE6rPeoj+5fsADqdUZl1OzqDYow4TB4Y/3PlKBD0ph1e6uPHzIQcv2Z65u2K0kpeByIyAjCmjn1hJgG0Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [mips64el]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.2':
|
||||
resolution: {integrity: sha512-tsHu2RRSWzipmUi9UBDEzc0nLc4HtpZEI5Ba+Omms5456x5WaNuiG3u7xh5AO6sipnJ9r4cRWQB2tUjPyIkc6g==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.2':
|
||||
resolution: {integrity: sha512-k4LtpgV7NJQOml/10uPU0s4SAXGnowi5qBSjaLWMojNCUICNu7TshqHLAEbkBdAszL5TabfvQ48kK84hyFzjnw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-s390x@0.25.2':
|
||||
resolution: {integrity: sha512-GRa4IshOdvKY7M/rDpRR3gkiTNp34M0eLTaC1a08gNrh4u488aPhuZOCpkF6+2wl3zAN7L7XIpOFBhnaE3/Q8Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/linux-x64@0.25.2':
|
||||
resolution: {integrity: sha512-QInHERlqpTTZ4FRB0fROQWXcYRD64lAoiegezDunLpalZMjcUcld3YzZmVJ2H/Cp0wJRZ8Xtjtj0cEHhYc/uUg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-talAIBoY5M8vHc6EeI2WW9d/CkiO9MQJ0IOWX8hrLhxGbro/vBXJvaQXefW2cP0z0nQVTdQ/eNyGFV1GSKrxfw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.2':
|
||||
resolution: {integrity: sha512-voZT9Z+tpOxrvfKFyfDYPc4DO4rk06qamv1a/fkuzHpiVBMOhpjK+vBmWM8J1eiB3OLSMFYNaOaBNLXGChf5tg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [netbsd]
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-dcXYOC6NXOqcykeDlwId9kB6OkPUxOEqU+rkrYVqJbK2hagWOMrsTGsMr8+rW02M+d5Op5NNlgMmjzecaRf7Tg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.2':
|
||||
resolution: {integrity: sha512-t/TkWwahkH0Tsgoq1Ju7QfgGhArkGLkF1uYz8nQS/PPFlXbP5YgRpqQR3ARRiC2iXoLTWFxc6DJMSK10dVXluw==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [openbsd]
|
||||
|
||||
'@esbuild/sunos-x64@0.25.2':
|
||||
resolution: {integrity: sha512-cfZH1co2+imVdWCjd+D1gf9NjkchVhhdpgb1q5y6Hcv9TP6Zi9ZG/beI3ig8TvwT9lH9dlxLq5MQBBgwuj4xvA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [sunos]
|
||||
|
||||
'@esbuild/win32-arm64@0.25.2':
|
||||
resolution: {integrity: sha512-7Loyjh+D/Nx/sOTzV8vfbB3GJuHdOQyrOryFdZvPHLf42Tk9ivBU5Aedi7iyX+x6rbn2Mh68T4qq1SDqJBQO5Q==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [arm64]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-ia32@0.25.2':
|
||||
resolution: {integrity: sha512-WRJgsz9un0nqZJ4MfhabxaD9Ft8KioqU3JMinOTvobbX6MOSUigSBlogP8QB3uxpJDsFS6yN+3FDBdqE5lg9kg==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@esbuild/win32-x64@0.25.2':
|
||||
resolution: {integrity: sha512-kM3HKb16VIXZyIeVrM1ygYmZBKybX8N4p754bw390wGO3Tf2j4L2/WYL+4suWujpgf6GBYs3jv7TyUivdd05JA==}
|
||||
engines: {node: '>=18'}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@fastify/busboy@2.1.1':
|
||||
resolution: {integrity: sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA==}
|
||||
engines: {node: '>=14'}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-darwin-x64@0.33.5':
|
||||
resolution: {integrity: sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg==}
|
||||
cpu: [arm64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.0.4':
|
||||
resolution: {integrity: sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ==}
|
||||
cpu: [x64]
|
||||
os: [darwin]
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-s390x@0.33.5':
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
|
||||
'@img/sharp-wasm32@0.33.5':
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [wasm32]
|
||||
|
||||
'@img/sharp-win32-ia32@0.33.5':
|
||||
resolution: {integrity: sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ia32]
|
||||
os: [win32]
|
||||
|
||||
'@img/sharp-win32-x64@0.33.5':
|
||||
resolution: {integrity: sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2':
|
||||
resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==}
|
||||
engines: {node: '>=6.0.0'}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0':
|
||||
resolution: {integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==}
|
||||
|
||||
acorn-walk@8.3.2:
|
||||
resolution: {integrity: sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
acorn@8.14.0:
|
||||
resolution: {integrity: sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
hasBin: true
|
||||
|
||||
argparse@2.0.1:
|
||||
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
|
||||
|
||||
as-table@1.0.55:
|
||||
resolution: {integrity: sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ==}
|
||||
|
||||
blake3-wasm@2.1.5:
|
||||
resolution: {integrity: sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g==}
|
||||
|
||||
chanfana@2.8.0:
|
||||
resolution: {integrity: sha512-R0lY+c4lBVO8jnop9J5hY6W2Vq5YS3sb9NwI4nY2oJU65E38rC6LCNNhEVEITVmcn+EhuKIQK4FLJLW93gFG4g==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
|
||||
color-name@1.1.4:
|
||||
resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==}
|
||||
|
||||
color-string@1.9.1:
|
||||
resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==}
|
||||
|
||||
color@4.2.3:
|
||||
resolution: {integrity: sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==}
|
||||
engines: {node: '>=12.5.0'}
|
||||
|
||||
cookie@0.7.2:
|
||||
resolution: {integrity: sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
||||
data-uri-to-buffer@2.0.2:
|
||||
resolution: {integrity: sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA==}
|
||||
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
detect-libc@2.0.4:
|
||||
resolution: {integrity: sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
esbuild@0.25.2:
|
||||
resolution: {integrity: sha512-16854zccKPnC+toMywC+uKNeYSv+/eXkevRAfwRD/G9Cleq66m8XFIrigkbvauLLlCfDL45Q2cWegSg53gGBnQ==}
|
||||
engines: {node: '>=18'}
|
||||
hasBin: true
|
||||
|
||||
exit-hook@2.2.1:
|
||||
resolution: {integrity: sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
exsolve@1.0.5:
|
||||
resolution: {integrity: sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg==}
|
||||
|
||||
fsevents@2.3.3:
|
||||
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||
os: [darwin]
|
||||
|
||||
get-source@2.0.12:
|
||||
resolution: {integrity: sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w==}
|
||||
|
||||
glob-to-regexp@0.4.1:
|
||||
resolution: {integrity: sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==}
|
||||
|
||||
hono@4.8.2:
|
||||
resolution: {integrity: sha512-hM+1RIn9PK1I6SiTNS6/y7O1mvg88awYLFEuEtoiMtRyT3SD2iu9pSFgbBXT3b1Ua4IwzvSTLvwO0SEhDxCi4w==}
|
||||
engines: {node: '>=16.9.0'}
|
||||
|
||||
is-arrayish@0.3.2:
|
||||
resolution: {integrity: sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==}
|
||||
|
||||
js-yaml@4.1.0:
|
||||
resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==}
|
||||
hasBin: true
|
||||
|
||||
mime@3.0.0:
|
||||
resolution: {integrity: sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==}
|
||||
engines: {node: '>=10.0.0'}
|
||||
hasBin: true
|
||||
|
||||
miniflare@4.20250428.1:
|
||||
resolution: {integrity: sha512-M3qcJXjeAEimHrEeWXEhrJiC3YHB5M3QSqqK67pOTI+lHn0QyVG/2iFUjVJ/nv+i10uxeAEva8GRGeu+tKRCmQ==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
|
||||
mustache@4.2.0:
|
||||
resolution: {integrity: sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ==}
|
||||
hasBin: true
|
||||
|
||||
ohash@2.0.11:
|
||||
resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==}
|
||||
|
||||
openapi3-ts@4.4.0:
|
||||
resolution: {integrity: sha512-9asTNB9IkKEzWMcHmVZE7Ts3kC9G7AFHfs8i7caD8HbI76gEjdkId4z/AkP83xdZsH7PLAnnbl47qZkXuxpArw==}
|
||||
|
||||
path-to-regexp@6.3.0:
|
||||
resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==}
|
||||
|
||||
pathe@2.0.3:
|
||||
resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==}
|
||||
|
||||
printable-characters@1.0.42:
|
||||
resolution: {integrity: sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ==}
|
||||
|
||||
semver@7.7.1:
|
||||
resolution: {integrity: sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==}
|
||||
engines: {node: '>=10'}
|
||||
hasBin: true
|
||||
|
||||
sharp@0.33.5:
|
||||
resolution: {integrity: sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
resolution: {integrity: sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==}
|
||||
|
||||
source-map@0.6.1:
|
||||
resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
stacktracey@2.1.8:
|
||||
resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==}
|
||||
|
||||
stoppable@1.1.0:
|
||||
resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==}
|
||||
engines: {node: '>=4', npm: '>=6'}
|
||||
|
||||
tslib@2.8.1:
|
||||
resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==}
|
||||
|
||||
ufo@1.6.1:
|
||||
resolution: {integrity: sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA==}
|
||||
|
||||
undici@5.29.0:
|
||||
resolution: {integrity: sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==}
|
||||
engines: {node: '>=14.0'}
|
||||
|
||||
unenv@2.0.0-rc.15:
|
||||
resolution: {integrity: sha512-J/rEIZU8w6FOfLNz/hNKsnY+fFHWnu9MH4yRbSZF3xbbGHovcetXPs7sD+9p8L6CeNC//I9bhRYAOsBt2u7/OA==}
|
||||
|
||||
workerd@1.20250428.0:
|
||||
resolution: {integrity: sha512-JJNWkHkwPQKQdvtM9UORijgYdcdJsihA4SfYjwh02IUQsdMyZ9jizV1sX9yWi9B9ptlohTW8UNHJEATuphGgdg==}
|
||||
engines: {node: '>=16'}
|
||||
hasBin: true
|
||||
|
||||
wrangler@4.14.2:
|
||||
resolution: {integrity: sha512-kD0ilA7WXwhDt1lfyPwppZ2/lT9uwDLp93tfqnqrUENmP/uj+5tErzfZY51emtgXZoU1HFK6HytBEYLRCvCiPg==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
hasBin: true
|
||||
peerDependencies:
|
||||
'@cloudflare/workers-types': ^4.20250428.0
|
||||
peerDependenciesMeta:
|
||||
'@cloudflare/workers-types':
|
||||
optional: true
|
||||
|
||||
ws@8.18.0:
|
||||
resolution: {integrity: sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==}
|
||||
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
|
||||
|
||||
yaml@2.7.1:
|
||||
resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==}
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
youch@3.3.4:
|
||||
resolution: {integrity: sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg==}
|
||||
|
||||
zod@3.22.3:
|
||||
resolution: {integrity: sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug==}
|
||||
|
||||
zod@3.24.4:
|
||||
resolution: {integrity: sha512-OdqJE9UDRPwWsrHjLN2F8bPxvwJBK22EHLWtanu0LSYr5YqzsaaW3RMgmjwr8Rypg5k+meEJdSPXJZXE/yqOMg==}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@asteasolutions/zod-to-openapi@7.3.0(zod@3.24.4)':
|
||||
dependencies:
|
||||
openapi3-ts: 4.4.0
|
||||
zod: 3.24.4
|
||||
|
||||
'@cloudflare/kv-asset-handler@0.4.0':
|
||||
dependencies:
|
||||
mime: 3.0.0
|
||||
|
||||
'@cloudflare/unenv-preset@2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250428.0)':
|
||||
dependencies:
|
||||
unenv: 2.0.0-rc.15
|
||||
optionalDependencies:
|
||||
workerd: 1.20250428.0
|
||||
|
||||
'@cloudflare/workerd-darwin-64@1.20250428.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-darwin-arm64@1.20250428.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-64@1.20250428.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-linux-arm64@1.20250428.0':
|
||||
optional: true
|
||||
|
||||
'@cloudflare/workerd-windows-64@1.20250428.0':
|
||||
optional: true
|
||||
|
||||
'@cspotcode/source-map-support@0.8.1':
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.9
|
||||
|
||||
'@emnapi/runtime@1.4.3':
|
||||
dependencies:
|
||||
tslib: 2.8.1
|
||||
optional: true
|
||||
|
||||
'@esbuild/aix-ppc64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-arm@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/android-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/darwin-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/freebsd-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-arm@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ia32@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-loong64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-mips64el@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-ppc64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-riscv64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-s390x@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/linux-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/netbsd-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/openbsd-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/sunos-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-arm64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-ia32@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@esbuild/win32-x64@0.25.2':
|
||||
optional: true
|
||||
|
||||
'@fastify/busboy@2.1.1': {}
|
||||
|
||||
'@img/sharp-darwin-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-darwin-x64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-arm64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-darwin-x64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-arm@1.0.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-s390x@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linux-x64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-arm64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-libvips-linuxmusl-x64@1.0.4':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-arm@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-s390x@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-s390x': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linux-x64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-arm64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-linuxmusl-x64@0.33.5':
|
||||
optionalDependencies:
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||
optional: true
|
||||
|
||||
'@img/sharp-wasm32@0.33.5':
|
||||
dependencies:
|
||||
'@emnapi/runtime': 1.4.3
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-ia32@0.33.5':
|
||||
optional: true
|
||||
|
||||
'@img/sharp-win32-x64@0.33.5':
|
||||
optional: true
|
||||
|
||||
'@jridgewell/resolve-uri@3.1.2': {}
|
||||
|
||||
'@jridgewell/sourcemap-codec@1.5.0': {}
|
||||
|
||||
'@jridgewell/trace-mapping@0.3.9':
|
||||
dependencies:
|
||||
'@jridgewell/resolve-uri': 3.1.2
|
||||
'@jridgewell/sourcemap-codec': 1.5.0
|
||||
|
||||
acorn-walk@8.3.2: {}
|
||||
|
||||
acorn@8.14.0: {}
|
||||
|
||||
argparse@2.0.1: {}
|
||||
|
||||
as-table@1.0.55:
|
||||
dependencies:
|
||||
printable-characters: 1.0.42
|
||||
|
||||
blake3-wasm@2.1.5: {}
|
||||
|
||||
chanfana@2.8.0:
|
||||
dependencies:
|
||||
'@asteasolutions/zod-to-openapi': 7.3.0(zod@3.24.4)
|
||||
js-yaml: 4.1.0
|
||||
openapi3-ts: 4.4.0
|
||||
zod: 3.24.4
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
optional: true
|
||||
|
||||
color-name@1.1.4:
|
||||
optional: true
|
||||
|
||||
color-string@1.9.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
simple-swizzle: 0.2.2
|
||||
optional: true
|
||||
|
||||
color@4.2.3:
|
||||
dependencies:
|
||||
color-convert: 2.0.1
|
||||
color-string: 1.9.1
|
||||
optional: true
|
||||
|
||||
cookie@0.7.2: {}
|
||||
|
||||
data-uri-to-buffer@2.0.2: {}
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
detect-libc@2.0.4:
|
||||
optional: true
|
||||
|
||||
esbuild@0.25.2:
|
||||
optionalDependencies:
|
||||
'@esbuild/aix-ppc64': 0.25.2
|
||||
'@esbuild/android-arm': 0.25.2
|
||||
'@esbuild/android-arm64': 0.25.2
|
||||
'@esbuild/android-x64': 0.25.2
|
||||
'@esbuild/darwin-arm64': 0.25.2
|
||||
'@esbuild/darwin-x64': 0.25.2
|
||||
'@esbuild/freebsd-arm64': 0.25.2
|
||||
'@esbuild/freebsd-x64': 0.25.2
|
||||
'@esbuild/linux-arm': 0.25.2
|
||||
'@esbuild/linux-arm64': 0.25.2
|
||||
'@esbuild/linux-ia32': 0.25.2
|
||||
'@esbuild/linux-loong64': 0.25.2
|
||||
'@esbuild/linux-mips64el': 0.25.2
|
||||
'@esbuild/linux-ppc64': 0.25.2
|
||||
'@esbuild/linux-riscv64': 0.25.2
|
||||
'@esbuild/linux-s390x': 0.25.2
|
||||
'@esbuild/linux-x64': 0.25.2
|
||||
'@esbuild/netbsd-arm64': 0.25.2
|
||||
'@esbuild/netbsd-x64': 0.25.2
|
||||
'@esbuild/openbsd-arm64': 0.25.2
|
||||
'@esbuild/openbsd-x64': 0.25.2
|
||||
'@esbuild/sunos-x64': 0.25.2
|
||||
'@esbuild/win32-arm64': 0.25.2
|
||||
'@esbuild/win32-ia32': 0.25.2
|
||||
'@esbuild/win32-x64': 0.25.2
|
||||
|
||||
exit-hook@2.2.1: {}
|
||||
|
||||
exsolve@1.0.5: {}
|
||||
|
||||
fsevents@2.3.3:
|
||||
optional: true
|
||||
|
||||
get-source@2.0.12:
|
||||
dependencies:
|
||||
data-uri-to-buffer: 2.0.2
|
||||
source-map: 0.6.1
|
||||
|
||||
glob-to-regexp@0.4.1: {}
|
||||
|
||||
hono@4.8.2: {}
|
||||
|
||||
is-arrayish@0.3.2:
|
||||
optional: true
|
||||
|
||||
js-yaml@4.1.0:
|
||||
dependencies:
|
||||
argparse: 2.0.1
|
||||
|
||||
mime@3.0.0: {}
|
||||
|
||||
miniflare@4.20250428.1:
|
||||
dependencies:
|
||||
'@cspotcode/source-map-support': 0.8.1
|
||||
acorn: 8.14.0
|
||||
acorn-walk: 8.3.2
|
||||
exit-hook: 2.2.1
|
||||
glob-to-regexp: 0.4.1
|
||||
stoppable: 1.1.0
|
||||
undici: 5.29.0
|
||||
workerd: 1.20250428.0
|
||||
ws: 8.18.0
|
||||
youch: 3.3.4
|
||||
zod: 3.22.3
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
mustache@4.2.0: {}
|
||||
|
||||
ohash@2.0.11: {}
|
||||
|
||||
openapi3-ts@4.4.0:
|
||||
dependencies:
|
||||
yaml: 2.7.1
|
||||
|
||||
path-to-regexp@6.3.0: {}
|
||||
|
||||
pathe@2.0.3: {}
|
||||
|
||||
printable-characters@1.0.42: {}
|
||||
|
||||
semver@7.7.1:
|
||||
optional: true
|
||||
|
||||
sharp@0.33.5:
|
||||
dependencies:
|
||||
color: 4.2.3
|
||||
detect-libc: 2.0.4
|
||||
semver: 7.7.1
|
||||
optionalDependencies:
|
||||
'@img/sharp-darwin-arm64': 0.33.5
|
||||
'@img/sharp-darwin-x64': 0.33.5
|
||||
'@img/sharp-libvips-darwin-arm64': 1.0.4
|
||||
'@img/sharp-libvips-darwin-x64': 1.0.4
|
||||
'@img/sharp-libvips-linux-arm': 1.0.5
|
||||
'@img/sharp-libvips-linux-arm64': 1.0.4
|
||||
'@img/sharp-libvips-linux-s390x': 1.0.4
|
||||
'@img/sharp-libvips-linux-x64': 1.0.4
|
||||
'@img/sharp-libvips-linuxmusl-arm64': 1.0.4
|
||||
'@img/sharp-libvips-linuxmusl-x64': 1.0.4
|
||||
'@img/sharp-linux-arm': 0.33.5
|
||||
'@img/sharp-linux-arm64': 0.33.5
|
||||
'@img/sharp-linux-s390x': 0.33.5
|
||||
'@img/sharp-linux-x64': 0.33.5
|
||||
'@img/sharp-linuxmusl-arm64': 0.33.5
|
||||
'@img/sharp-linuxmusl-x64': 0.33.5
|
||||
'@img/sharp-wasm32': 0.33.5
|
||||
'@img/sharp-win32-ia32': 0.33.5
|
||||
'@img/sharp-win32-x64': 0.33.5
|
||||
optional: true
|
||||
|
||||
simple-swizzle@0.2.2:
|
||||
dependencies:
|
||||
is-arrayish: 0.3.2
|
||||
optional: true
|
||||
|
||||
source-map@0.6.1: {}
|
||||
|
||||
stacktracey@2.1.8:
|
||||
dependencies:
|
||||
as-table: 1.0.55
|
||||
get-source: 2.0.12
|
||||
|
||||
stoppable@1.1.0: {}
|
||||
|
||||
tslib@2.8.1:
|
||||
optional: true
|
||||
|
||||
ufo@1.6.1: {}
|
||||
|
||||
undici@5.29.0:
|
||||
dependencies:
|
||||
'@fastify/busboy': 2.1.1
|
||||
|
||||
unenv@2.0.0-rc.15:
|
||||
dependencies:
|
||||
defu: 6.1.4
|
||||
exsolve: 1.0.5
|
||||
ohash: 2.0.11
|
||||
pathe: 2.0.3
|
||||
ufo: 1.6.1
|
||||
|
||||
workerd@1.20250428.0:
|
||||
optionalDependencies:
|
||||
'@cloudflare/workerd-darwin-64': 1.20250428.0
|
||||
'@cloudflare/workerd-darwin-arm64': 1.20250428.0
|
||||
'@cloudflare/workerd-linux-64': 1.20250428.0
|
||||
'@cloudflare/workerd-linux-arm64': 1.20250428.0
|
||||
'@cloudflare/workerd-windows-64': 1.20250428.0
|
||||
|
||||
wrangler@4.14.2:
|
||||
dependencies:
|
||||
'@cloudflare/kv-asset-handler': 0.4.0
|
||||
'@cloudflare/unenv-preset': 2.3.1(unenv@2.0.0-rc.15)(workerd@1.20250428.0)
|
||||
blake3-wasm: 2.1.5
|
||||
esbuild: 0.25.2
|
||||
miniflare: 4.20250428.1
|
||||
path-to-regexp: 6.3.0
|
||||
unenv: 2.0.0-rc.15
|
||||
workerd: 1.20250428.0
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
sharp: 0.33.5
|
||||
transitivePeerDependencies:
|
||||
- bufferutil
|
||||
- utf-8-validate
|
||||
|
||||
ws@8.18.0: {}
|
||||
|
||||
yaml@2.7.1: {}
|
||||
|
||||
youch@3.3.4:
|
||||
dependencies:
|
||||
cookie: 0.7.2
|
||||
mustache: 4.2.0
|
||||
stacktracey: 2.1.8
|
||||
|
||||
zod@3.22.3: {}
|
||||
|
||||
zod@3.24.4: {}
|
||||
+1443
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,125 @@
|
||||
import { Context } from "hono"
|
||||
import { contentJson, OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
import { ChannelConfigRow } from "../db/db_model";
|
||||
|
||||
// 获取所有 Channel 配置
|
||||
export class ChannelGetEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Get all channel configurations',
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.array(z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
}))),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const result = await c.env.DB.prepare(
|
||||
"SELECT * FROM channel_config"
|
||||
).all<ChannelConfigRow>();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.results
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// 创建或更新 Channel 配置
|
||||
export class ChannelUpsertEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Create or update channel configuration',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Channel key'),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
name: z.string().describe('Channel name'),
|
||||
type: z.string().describe('Channel type'),
|
||||
endpoint: z.string().describe('API endpoint'),
|
||||
api_key: z.string().describe('API key'),
|
||||
api_version: z.string().describe('API version'),
|
||||
deployment_mapper: z.record(z.string()).describe('Model deployment mapping'),
|
||||
model_pricing: z.record(z.object({
|
||||
input: z.number().describe('Input token price per 1000 tokens'),
|
||||
output: z.number().describe('Output token price per 1000 tokens'),
|
||||
})).optional().describe('Custom model pricing for this channel'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const { key } = c.req.param();
|
||||
const config = await c.req.json<ChannelConfig>();
|
||||
|
||||
// Upsert channel config directly using SQL
|
||||
// excluded.value 指的是 INSERT 语句中要插入的新值
|
||||
// 当发生冲突时,用新值更新现有记录
|
||||
const result = await c.env.DB.prepare(
|
||||
`INSERT INTO channel_config (key, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = datetime('now')`
|
||||
).bind(key, JSON.stringify(config)).run();
|
||||
|
||||
if (!result.success) {
|
||||
return c.text('Failed to upsert channel config', 500);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: true
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除 Channel 配置
|
||||
export class ChannelDeleteEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Delete channel configuration',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Channel key'),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const { key } = c.req.param();
|
||||
|
||||
// Delete channel config directly using SQL
|
||||
const result = await c.env.DB.prepare(
|
||||
`DELETE FROM channel_config WHERE key = ?`
|
||||
).bind(key).run();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.success
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { Context } from "hono"
|
||||
import { OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import db from "../db"
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
|
||||
export class DBInitializeEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.string()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
await db.initialize(c);
|
||||
await db.migrate(c);
|
||||
return {
|
||||
success: true,
|
||||
data: await db.getVersion(c)
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import { Context, Hono, Next } from "hono"
|
||||
import { fromHono } from 'chanfana';
|
||||
import { DBInitializeEndpoint } from "./db_api"
|
||||
import {
|
||||
ChannelGetEndpoint, ChannelUpsertEndpoint, ChannelDeleteEndpoint
|
||||
} from "./channel_api"
|
||||
import {
|
||||
TokenListEndpoint, TokenUpsertEndpoint, TokenDeleteEndpoint
|
||||
} from "./token_api"
|
||||
import {
|
||||
PricingGetEndpoint, PricingUpdateEndpoint
|
||||
} from "./pricing_api"
|
||||
|
||||
const app = new Hono<HonoCustomType>()
|
||||
export const api = fromHono(app)
|
||||
|
||||
// Authentication Middleware - using environment variable
|
||||
app.use('/api/admin/*', async (c, next) => {
|
||||
const token = c.req.header('x-admin-token');
|
||||
const adminToken = c.env.ADMIN_TOKEN;
|
||||
|
||||
if (!token || !adminToken || token !== adminToken) {
|
||||
return c.text("Unauthorized", 401);
|
||||
}
|
||||
await next();
|
||||
});
|
||||
|
||||
api.post("/api/admin/db_initialize", DBInitializeEndpoint)
|
||||
|
||||
api.get("/api/admin/channel", ChannelGetEndpoint)
|
||||
api.post("/api/admin/channel/:key", ChannelUpsertEndpoint)
|
||||
api.delete("/api/admin/channel/:key", ChannelDeleteEndpoint)
|
||||
|
||||
// Token management routes
|
||||
api.get("/api/admin/token", TokenListEndpoint)
|
||||
api.post("/api/admin/token/:key", TokenUpsertEndpoint)
|
||||
api.delete("/api/admin/token/:key", TokenDeleteEndpoint)
|
||||
|
||||
// Pricing management routes
|
||||
api.get("/api/admin/pricing", PricingGetEndpoint)
|
||||
api.post("/api/admin/pricing", PricingUpdateEndpoint)
|
||||
@@ -0,0 +1,72 @@
|
||||
import { Context } from "hono";
|
||||
import { OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
import { getJsonSetting, saveSetting } from "../utils";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
|
||||
// Pricing 获取配置 API
|
||||
export class PricingGetEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Get global pricing configuration',
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.record(z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
}))),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const pricingConfig = await getJsonSetting<Record<string, ModelPricing>>(
|
||||
c,
|
||||
CONSTANTS.MODEL_PRICING_KEY
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: pricingConfig || {}
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Pricing 更新配置 API
|
||||
export class PricingUpdateEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Update global pricing configuration',
|
||||
request: {
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.record(z.object({
|
||||
input: z.number(),
|
||||
output: z.number(),
|
||||
})),
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const body = await c.req.json<Record<string, ModelPricing>>();
|
||||
|
||||
await saveSetting(
|
||||
c,
|
||||
CONSTANTS.MODEL_PRICING_KEY,
|
||||
JSON.stringify(body)
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: "Pricing config updated successfully"
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
import { Context } from "hono"
|
||||
import { OpenAPIRoute } from 'chanfana';
|
||||
import { z } from 'zod';
|
||||
|
||||
import { CommonErrorResponse, CommonSuccessfulResponse } from "../model";
|
||||
|
||||
// Token 列表 API
|
||||
export class TokenListEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Get all tokens',
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.array(z.object({
|
||||
key: z.string(),
|
||||
value: z.string(),
|
||||
usage: z.number(),
|
||||
created_at: z.string(),
|
||||
updated_at: z.string(),
|
||||
}))),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const result = await c.env.DB.prepare(
|
||||
`SELECT * FROM api_token ORDER BY created_at DESC`
|
||||
).all<ApiTokenRow>();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.results
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Token 创建/更新 API (Upsert)
|
||||
export class TokenUpsertEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Create or update a token',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Token key'),
|
||||
}),
|
||||
body: {
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
name: z.string().describe('Token name'),
|
||||
channel_keys: z.array(z.string()).describe('Channel keys to bind (empty array means access to all channels)'),
|
||||
total_quota: z.number().describe('Total quota amount'),
|
||||
}),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const body = await c.req.json<ApiTokenData>();
|
||||
const { key } = c.req.param();
|
||||
|
||||
// Validate channels exist using batch query (if channel_keys is not empty)
|
||||
if (body.channel_keys && body.channel_keys.length > 0) {
|
||||
const channelQuery = body.channel_keys.map(() => '?').join(',');
|
||||
const existingChannels = await c.env.DB.prepare(
|
||||
`SELECT key FROM channel_config WHERE key IN (${channelQuery})`
|
||||
).bind(...body.channel_keys).all();
|
||||
|
||||
if (!existingChannels.results || existingChannels.results.length !== body.channel_keys.length) {
|
||||
const existingKeys = existingChannels.results?.map((row: any) => row.key) || [];
|
||||
const missingKeys = body.channel_keys.filter(key => !existingKeys.includes(key));
|
||||
return c.text(`Channels not found: ${missingKeys.join(', ')}`, 400);
|
||||
}
|
||||
}
|
||||
|
||||
// Upsert token directly using SQL with ON CONFLICT
|
||||
const result = await c.env.DB.prepare(
|
||||
`INSERT INTO api_token (key, value)
|
||||
VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET
|
||||
value = excluded.value,
|
||||
updated_at = datetime('now')`
|
||||
).bind(key, JSON.stringify(body)).run();
|
||||
|
||||
if (!result.success) {
|
||||
return c.text('Failed to upsert token', 500);
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: true
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
|
||||
// Token 删除 API
|
||||
export class TokenDeleteEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['Admin API'],
|
||||
summary: 'Delete token',
|
||||
request: {
|
||||
params: z.object({
|
||||
key: z.string().describe('Token key'),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
...CommonSuccessfulResponse(z.boolean()),
|
||||
...CommonErrorResponse,
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const { key } = c.req.param();
|
||||
|
||||
// Delete token directly using SQL
|
||||
const result = await c.env.DB.prepare(
|
||||
`DELETE FROM api_token WHERE key = ?`
|
||||
).bind(key).run();
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.success
|
||||
} as CommonResponse;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
// Token 工具类
|
||||
export class TokenUtils {
|
||||
static async updateUsage(c: Context<HonoCustomType>, key: string, usageAmount: number): Promise<boolean> {
|
||||
try {
|
||||
const result = await c.env.DB.prepare(
|
||||
`UPDATE api_token SET usage = usage + ?, updated_at = datetime('now') WHERE key = ?`
|
||||
).bind(usageAmount, key).run();
|
||||
|
||||
return result.success;
|
||||
} catch (error) {
|
||||
console.error('Error updating token usage:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
export const CONSTANTS = {
|
||||
VERSION: "v0.0.1",
|
||||
DB_VERSION_KEY: "db_version",
|
||||
DB_VERSION: "v0.0.1",
|
||||
MODEL_PRICING_KEY: "MODEL_PRICING_CONFIG",
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import { Context } from "hono";
|
||||
import { CONSTANTS } from "../constants";
|
||||
import utils from "../utils";
|
||||
|
||||
const DB_INIT_QUERIES = `
|
||||
CREATE TABLE IF NOT EXISTS channel_config (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS api_token (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
usage REAL DEFAULT 0,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
`
|
||||
|
||||
const dbOperations = {
|
||||
initialize: async (c: Context<HonoCustomType>) => {
|
||||
// remove all \r and \n characters from the query string
|
||||
// split by ; and join with a ;\n
|
||||
const query = DB_INIT_QUERIES.replace(/[\r\n]/g, "")
|
||||
.split(";")
|
||||
.map((query) => query.trim())
|
||||
.join(";\n");
|
||||
await c.env.DB.exec(query);
|
||||
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version) {
|
||||
return c.json({ message: "Database already initialized" });
|
||||
}
|
||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
},
|
||||
migrate: async (c: Context<HonoCustomType>) => {
|
||||
const version = await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
if (version != CONSTANTS.DB_VERSION) {
|
||||
// TODO: Perform migration logic here
|
||||
|
||||
// Update the version in the settings table
|
||||
await utils.saveSetting(c, CONSTANTS.DB_VERSION_KEY, CONSTANTS.DB_VERSION);
|
||||
}
|
||||
},
|
||||
getVersion: async (c: Context<HonoCustomType>): Promise<string | null> => {
|
||||
return await utils.getSetting(c, CONSTANTS.DB_VERSION_KEY);
|
||||
}
|
||||
}
|
||||
|
||||
export default dbOperations;
|
||||
@@ -0,0 +1,48 @@
|
||||
import { Context, Hono, Next } from 'hono'
|
||||
import { cors } from 'hono/cors';
|
||||
import { api as providerApi } from './providers'
|
||||
import { api as adminApi } from './admin'
|
||||
import { fromHono } from 'chanfana';
|
||||
|
||||
const API_PATHS = [
|
||||
"/api",
|
||||
"/v1",
|
||||
];
|
||||
|
||||
const app = new Hono<HonoCustomType>()
|
||||
const openapi = fromHono(app, {
|
||||
schema: {
|
||||
info: {
|
||||
title: 'Awsl One API',
|
||||
version: '1.0.0',
|
||||
}
|
||||
},
|
||||
docs_url: '/api/docs',
|
||||
redoc_url: '/api/redocs',
|
||||
openapi_url: '/api/openapi.json'
|
||||
});
|
||||
|
||||
// cors
|
||||
openapi.use('/*', cors());
|
||||
// global middlewares
|
||||
app.use('/*', async (c, next) => {
|
||||
// check if the request is for static files
|
||||
if (c.env.ASSETS && !API_PATHS.some(path => c.req.path.startsWith(path))) {
|
||||
const url = new URL(c.req.raw.url);
|
||||
if (!url.pathname.includes('.')) {
|
||||
url.pathname = ""
|
||||
}
|
||||
return c.env.ASSETS.fetch(url);
|
||||
}
|
||||
await next()
|
||||
});
|
||||
// global error handler
|
||||
openapi.onError((err, c) => {
|
||||
console.error(err)
|
||||
return c.text(`${err.name} ${err.message}`, 500)
|
||||
})
|
||||
|
||||
openapi.route('/', providerApi)
|
||||
openapi.route('/', adminApi)
|
||||
|
||||
export default app
|
||||
@@ -0,0 +1,47 @@
|
||||
import { z, ZodType } from 'zod';
|
||||
|
||||
export const CommonSuccessfulResponse = (
|
||||
dataType: ZodType | null = null
|
||||
) => {
|
||||
return {
|
||||
200: {
|
||||
description: 'Successful Request',
|
||||
content: {
|
||||
'application/json': {
|
||||
schema: z.object({
|
||||
success: z.boolean(),
|
||||
message: z.nullable(z.string()),
|
||||
data: z.nullable(dataType || z.any()),
|
||||
}),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const CommonErrorResponse = {
|
||||
400: {
|
||||
description: 'Bad Request',
|
||||
content: {
|
||||
'text/plain': {
|
||||
schema: z.string(),
|
||||
}
|
||||
},
|
||||
},
|
||||
401: {
|
||||
description: 'Unauthorized',
|
||||
content: {
|
||||
'text/plain': {
|
||||
schema: z.string(),
|
||||
}
|
||||
},
|
||||
},
|
||||
500: {
|
||||
description: 'Internal Server Error',
|
||||
content: {
|
||||
'text/plain': {
|
||||
schema: z.string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
import { Context } from "hono"
|
||||
import { streamText } from 'hono/streaming'
|
||||
|
||||
// 辅助函数
|
||||
const buildProxyRequest = (
|
||||
request: Request,
|
||||
reqJson: any,
|
||||
config: ChannelConfig,
|
||||
deploymentName: string
|
||||
): Request => {
|
||||
const url = new URL(request.url)
|
||||
const targetUrl = new URL(config.endpoint)
|
||||
targetUrl.pathname = `/openai/deployments/${deploymentName}/${url.pathname.replace('/v1/', '')}`
|
||||
targetUrl.searchParams.set('api-version', config.api_version)
|
||||
|
||||
const targetHeaders = new Headers(request.headers)
|
||||
targetHeaders.delete("Authorization")
|
||||
targetHeaders.set("api-key", config.api_key)
|
||||
|
||||
return new Request(targetUrl, {
|
||||
method: request.method,
|
||||
headers: targetHeaders,
|
||||
body: JSON.stringify(reqJson),
|
||||
})
|
||||
}
|
||||
|
||||
const checkoutUsageData = async (
|
||||
saveUsage: (usage: Usage) => Promise<void>,
|
||||
response: Response | OpenAIResponse
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const res = response instanceof Response
|
||||
? await response.clone().json<OpenAIResponse>()
|
||||
: response
|
||||
if (!res.usage) return;
|
||||
await saveUsage(res.usage)
|
||||
} catch (error) {
|
||||
console.error("Error logging usage data:", error)
|
||||
}
|
||||
}
|
||||
|
||||
const processStreamData = (
|
||||
lines: string[],
|
||||
saveUsage: (usage: Usage) => Promise<void>
|
||||
): void => {
|
||||
lines
|
||||
.map(line => line.trim())
|
||||
.filter(line => line.length > 0 && line.startsWith('data:'))
|
||||
.map(line => line.replace('data: ', '').trim())
|
||||
.filter(line => line !== '[DONE]')
|
||||
.forEach(jsonContent => {
|
||||
try {
|
||||
const res = JSON.parse(jsonContent) as OpenAIResponse
|
||||
checkoutUsageData(saveUsage, res)
|
||||
} catch (e) {
|
||||
console.error("Error parsing stream data:", e)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const handleStreamResponse = async (
|
||||
c: Context<HonoCustomType>,
|
||||
response: Response,
|
||||
saveUsage: (usage: Usage) => Promise<void>
|
||||
): Promise<Response> => {
|
||||
return streamText(c, async (stream) => {
|
||||
const reader = response.body?.getReader()
|
||||
if (!reader) {
|
||||
throw new Error("No reader found in response body")
|
||||
}
|
||||
|
||||
const decoder = new TextDecoder('utf-8')
|
||||
let buffer = ""
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read()
|
||||
if (done) break
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true })
|
||||
await stream.write(chunk)
|
||||
buffer += chunk
|
||||
|
||||
if (!chunk.includes('\n')) continue
|
||||
|
||||
const lines = buffer.split('\n')
|
||||
buffer = lines.pop() || ""
|
||||
|
||||
processStreamData(lines, saveUsage)
|
||||
}
|
||||
|
||||
// 处理最后剩余的数据
|
||||
if (buffer.trim()) {
|
||||
processStreamData([buffer], saveUsage)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export default {
|
||||
async fetch(
|
||||
c: Context<HonoCustomType>,
|
||||
config: ChannelConfig,
|
||||
saveUsage: (usage: Usage) => Promise<void>,
|
||||
): Promise<Response> {
|
||||
// 准备请求数据
|
||||
const reqJson = await c.req.json()
|
||||
// 强制包含使用数据
|
||||
const { model: modelName, stream } = reqJson;
|
||||
if (stream) {
|
||||
reqJson.stream_options = { "include_usage": true }
|
||||
}
|
||||
|
||||
// 获取部署名称
|
||||
const deploymentName = config.deployment_mapper[modelName]
|
||||
if (!deploymentName) {
|
||||
throw new Error(`Model ${modelName} not supported`)
|
||||
}
|
||||
|
||||
// 构建目标请求
|
||||
const proxyRequest = buildProxyRequest(c.req.raw, reqJson, config, deploymentName)
|
||||
|
||||
// 发送请求并获取响应
|
||||
const response = await fetch(proxyRequest)
|
||||
|
||||
// 处理流式响应
|
||||
if (stream) {
|
||||
return handleStreamResponse(c, response, saveUsage)
|
||||
}
|
||||
|
||||
// 处理常规响应
|
||||
if (response.ok) {
|
||||
await checkoutUsageData(saveUsage, response)
|
||||
}
|
||||
|
||||
return response
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
import { Context, Hono } from "hono"
|
||||
import { contentJson, fromHono, OpenAPIRoute } from 'chanfana';
|
||||
|
||||
import azureOpenaiProxy from "./azure-openai-proxy"
|
||||
import utils from "../utils"
|
||||
import { TokenUtils } from "../admin/token_utils"
|
||||
import { ChannelConfigRow } from "../db/db_model"
|
||||
import { CONSTANTS } from "../constants"
|
||||
import { z } from "zod";
|
||||
|
||||
export const api = fromHono(new Hono<HonoCustomType>())
|
||||
|
||||
const providerMap: Record<
|
||||
string,
|
||||
(
|
||||
c: Context<HonoCustomType>,
|
||||
config: ChannelConfig,
|
||||
saveUsage: (usage: Usage) => Promise<void>
|
||||
) => Promise<Response>
|
||||
> = {
|
||||
"azure-openai": azureOpenaiProxy.fetch,
|
||||
};
|
||||
|
||||
class ProxyEndpoint extends OpenAPIRoute {
|
||||
schema = {
|
||||
tags: ['OpenAI Proxy'],
|
||||
request: {
|
||||
headers: z.object({
|
||||
'Authorization': z.string().describe("Token for authentication"),
|
||||
}),
|
||||
body: contentJson({
|
||||
schema: z.any(),
|
||||
}),
|
||||
},
|
||||
responses: {
|
||||
200: {
|
||||
description: 'Successful response',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
async handle(c: Context<HonoCustomType>) {
|
||||
const authHeader = c.req.raw.headers.get('Authorization');
|
||||
const apiKey = authHeader?.replace("Bearer ", "").trim();
|
||||
if (!apiKey) {
|
||||
return c.text("Authorization header not found", 401);
|
||||
}
|
||||
|
||||
// Get token from database using direct SQL query
|
||||
const tokenResult = await c.env.DB.prepare(
|
||||
`SELECT * FROM api_token WHERE key = ?`
|
||||
).bind(apiKey).first();
|
||||
|
||||
if (!tokenResult || !tokenResult.value) {
|
||||
return c.text("Invalid API key", 401);
|
||||
}
|
||||
|
||||
const tokenData = JSON.parse(tokenResult.value as string) as ApiTokenData;
|
||||
const usage = tokenResult.usage as number || 0;
|
||||
|
||||
// Check if token has sufficient quota
|
||||
if (usage >= tokenData.total_quota) {
|
||||
return c.text("Quota exceeded", 402);
|
||||
} // Get available channel configs based on token permissions
|
||||
const channelKeys = tokenData.channel_keys;
|
||||
|
||||
let channelsResult;
|
||||
if (!channelKeys || channelKeys.length === 0) {
|
||||
// If channel_keys is empty, get all channels
|
||||
channelsResult = await c.env.DB.prepare(
|
||||
`SELECT key, value FROM channel_config`
|
||||
).all<ChannelConfigRow>();
|
||||
} else {
|
||||
// Query specified channels
|
||||
const channelQuery = channelKeys.map(() => '?').join(',');
|
||||
channelsResult = await c.env.DB.prepare(
|
||||
`SELECT key, value FROM channel_config WHERE key IN (${channelQuery})`
|
||||
).bind(...channelKeys).all<ChannelConfigRow>();
|
||||
}
|
||||
|
||||
if (!channelsResult.results || channelsResult.results.length === 0) {
|
||||
return c.text("No available channels for this token", 401);
|
||||
}
|
||||
|
||||
// Parse channel configs and get the model from request
|
||||
const requestBody = await c.req.json();
|
||||
const model = requestBody.model || 'gpt-3.5-turbo';
|
||||
|
||||
// Filter channels that support the requested model
|
||||
const availableChannels: Array<{ key: string, config: ChannelConfig }> = [];
|
||||
|
||||
for (const row of channelsResult.results) {
|
||||
const config = JSON.parse(row.value) as ChannelConfig;
|
||||
|
||||
// Check if channel supports the model (has deployment mapping for it)
|
||||
if (config.deployment_mapper && config.deployment_mapper[model]) {
|
||||
availableChannels.push({
|
||||
key: row.key,
|
||||
config: config
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (availableChannels.length === 0) {
|
||||
return c.text(`No channels available for model: ${model}`, 400);
|
||||
}
|
||||
|
||||
// Randomly select one available channel
|
||||
const randomIndex = Math.floor(Math.random() * availableChannels.length);
|
||||
const selectedChannel = availableChannels[randomIndex];
|
||||
const targetChannelKey = selectedChannel.key;
|
||||
const targetChannelConfig = selectedChannel.config;
|
||||
|
||||
if (!targetChannelConfig.type) {
|
||||
return c.text("Channel type invalid", 400);
|
||||
}
|
||||
|
||||
const proxyFetch = providerMap[targetChannelConfig.type];
|
||||
if (!proxyFetch) {
|
||||
return c.text("Channel type not supported", 400);
|
||||
}
|
||||
|
||||
return proxyFetch(c, targetChannelConfig,
|
||||
async (usage: Usage) => {
|
||||
console.log("Usage data:", usage);
|
||||
|
||||
// Calculate cost based on usage and model pricing
|
||||
// Model was already extracted above
|
||||
|
||||
// Get global pricing as base configuration
|
||||
let pricing = null;
|
||||
const globalPricing = await utils.getSetting(c, CONSTANTS.MODEL_PRICING_KEY);
|
||||
if (globalPricing) {
|
||||
const globalPricingMap = JSON.parse(globalPricing);
|
||||
pricing = globalPricingMap[model];
|
||||
}
|
||||
|
||||
// Override with channel-specific pricing if available
|
||||
if (targetChannelConfig.model_pricing?.[model]) {
|
||||
pricing = targetChannelConfig.model_pricing[model];
|
||||
}
|
||||
|
||||
if (pricing && usage.prompt_tokens && usage.completion_tokens) {
|
||||
const inputCost = usage.prompt_tokens * pricing.input;
|
||||
const outputCost = usage.completion_tokens * pricing.output;
|
||||
const totalCost = inputCost + outputCost;
|
||||
|
||||
// Update token usage using TokenUtils
|
||||
const updated = await TokenUtils.updateUsage(c, apiKey, totalCost);
|
||||
if (!updated) {
|
||||
console.error("Failed to update token usage");
|
||||
}
|
||||
|
||||
console.log(`Model: ${model}, Channel: ${targetChannelKey}, Channel pricing: ${!!targetChannelConfig.model_pricing?.[model]}, Cost: ${totalCost}, Updated: ${updated}`);
|
||||
}
|
||||
else {
|
||||
console.warn(`No pricing found for model: ${model} in channel: ${targetChannelKey}`);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
api.post("/v1/chat/completions", ProxyEndpoint)
|
||||
@@ -0,0 +1,69 @@
|
||||
import { Context } from "hono";
|
||||
|
||||
const getJsonObjectValue = <T = any>(
|
||||
value: string | any
|
||||
): T | null => {
|
||||
if (value == undefined || value == null) {
|
||||
return null;
|
||||
}
|
||||
if (typeof value === "object") {
|
||||
return value as T;
|
||||
}
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch (e) {
|
||||
console.error(`GetJsonValue: Failed to parse ${value}`, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
export const getJsonSetting = async <T = any>(
|
||||
c: Context<HonoCustomType>, key: string
|
||||
): Promise<T | null> => {
|
||||
const value = await getSetting(c, key);
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch (e) {
|
||||
console.error(`GetJsonSetting: Failed to parse ${key}`, e);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const getSetting = async (
|
||||
c: Context<HonoCustomType>, key: string
|
||||
): Promise<string | null> => {
|
||||
try {
|
||||
const value = await c.env.DB.prepare(
|
||||
`SELECT value FROM settings where key = ?`
|
||||
).bind(key).first<string>("value");
|
||||
return value;
|
||||
} catch (error) {
|
||||
console.error(`GetSetting: Failed to get ${key}`, error);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export const saveSetting = async (
|
||||
c: Context<HonoCustomType>,
|
||||
key: string, value: string
|
||||
) => {
|
||||
await c.env.DB.prepare(
|
||||
`INSERT INTO settings (key, value) VALUES (?, ?)`
|
||||
+ ` ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = datetime('now')`
|
||||
).bind(key, value, value).run();
|
||||
return true;
|
||||
}
|
||||
|
||||
export default {
|
||||
getJsonObjectValue,
|
||||
getJsonSetting,
|
||||
getSetting,
|
||||
saveSetting,
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ESNext",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"lib": [
|
||||
"ESNext"
|
||||
],
|
||||
"jsx": "react-jsx",
|
||||
"jsxImportSource": "hono/jsx"
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
type Variables = {
|
||||
lang: string | undefined | null
|
||||
}
|
||||
|
||||
type CloudflareBindings = {
|
||||
DB: D1Database;
|
||||
ASSETS: Fetcher;
|
||||
ADMIN_TOKEN: string;
|
||||
}
|
||||
|
||||
type HonoCustomType = {
|
||||
"Bindings": CloudflareBindings;
|
||||
"Variables": Variables;
|
||||
}
|
||||
|
||||
// 数据库表行的基础结构
|
||||
type BaseDbRow = {
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
// channel_config 表的行结构
|
||||
type ChannelConfigRow = BaseDbRow & {
|
||||
key: string;
|
||||
value: string; // JSON 字符串,解析后为 ChannelConfig 类型
|
||||
}
|
||||
|
||||
// api_token 表的行结构
|
||||
type ApiTokenRow = BaseDbRow & {
|
||||
key: string;
|
||||
value: string; // JSON 字符串,解析后为 ApiTokenData 类型
|
||||
usage: number;
|
||||
}
|
||||
|
||||
type ChannelType = "azure-openai" | undefined | null;
|
||||
|
||||
type ChannelConfig = {
|
||||
name: string;
|
||||
type: ChannelType;
|
||||
endpoint: string;
|
||||
api_key: string;
|
||||
api_version: string;
|
||||
deployment_mapper: Record<string, string>;
|
||||
model_pricing?: Record<string, ModelPricing>;
|
||||
}
|
||||
|
||||
type ChannelConfigMap = {
|
||||
[key: string]: ChannelConfig;
|
||||
}
|
||||
|
||||
type OpenAIResponse = {
|
||||
usage?: Usage
|
||||
}
|
||||
|
||||
type Usage = {
|
||||
prompt_tokens?: number,
|
||||
completion_tokens?: number,
|
||||
total_tokens?: number,
|
||||
}
|
||||
|
||||
type AwslKeyPayload = {
|
||||
channel_key: string;
|
||||
multipler: number | undefined | null;
|
||||
}
|
||||
|
||||
type CommonResponse = {
|
||||
success?: boolean;
|
||||
message?: string;
|
||||
data?: any;
|
||||
}
|
||||
|
||||
type ModelPricing = {
|
||||
input: number;
|
||||
output: number;
|
||||
}
|
||||
|
||||
type ApiTokenData = {
|
||||
name: string;
|
||||
channel_keys: string[];
|
||||
total_quota: number;
|
||||
}
|
||||
Vendored
+5731
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,19 @@
|
||||
name = "awsl-one-api"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2025-04-28"
|
||||
routes = [
|
||||
{ pattern = "your-domain.com-here", custom_domain = true },
|
||||
]
|
||||
|
||||
[vars]
|
||||
ADMIN_TOKEN = "your-secure-admin-token-here"
|
||||
|
||||
[assets]
|
||||
directory = "public"
|
||||
binding = "ASSETS"
|
||||
run_worker_first = true
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "your-database-name-here"
|
||||
database_id = "your-database-id-here"
|
||||
Reference in new issue
Block a user