init: 初始化项目
This commit is contained in:
commit
dd3fe3033b
60 files changed
+4514
No files matched your search
+11
@@ -0,0 +1,11 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.DS_Store
|
||||
.env
|
||||
.env.*
|
||||
apps/admin/dist/
|
||||
apps/worker/.wrangler/
|
||||
tests/
|
||||
|
||||
helloagents/plan
|
||||
helloagents/archive
|
||||
@@ -0,0 +1,119 @@
|
||||
# new-api-lite
|
||||
|
||||
简易版 new-api:Cloudflare Workers + D1 后端,Vite 静态管理台。
|
||||
|
||||
## 目录结构
|
||||
|
||||
- `apps/worker` Cloudflare Worker (Hono)
|
||||
- `apps/admin` 管理台 (Vite)
|
||||
- `tests` 基础单元测试
|
||||
|
||||
## 本地开发
|
||||
|
||||
### 1) 安装依赖
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
### 2) Worker
|
||||
|
||||
```bash
|
||||
bun run dev:worker
|
||||
```
|
||||
|
||||
环境变量(`apps/worker/wrangler.toml` 或 `wrangler secret put`):
|
||||
|
||||
- `ADMIN_PASSWORD` 管理员密码
|
||||
- `SESSION_TTL_HOURS` 管理会话时长(小时)
|
||||
- `LOG_RETENTION_DAYS` 日志保留天数
|
||||
- `CORS_ORIGIN` 允许的管理台来源
|
||||
- `PROXY_RETRY_ROUNDS` 代理失败轮询次数(默认 2)
|
||||
- `PROXY_RETRY_DELAY_MS` 轮询间隔(毫秒,默认 200)
|
||||
- `NEW_API_ADMIN_TOKEN` New API 兼容接口的管理员令牌
|
||||
|
||||
### 3) Admin UI
|
||||
|
||||
```bash
|
||||
bun run dev:admin
|
||||
```
|
||||
|
||||
前端配置(`apps/admin/.env` 可选):
|
||||
|
||||
- `VITE_API_BASE` 管理 API 基址(默认同域)
|
||||
- `VITE_API_TARGET` 本地开发代理目标(默认 http://localhost:8787)
|
||||
|
||||
### 4) 常用脚本
|
||||
|
||||
```bash
|
||||
bun run fix
|
||||
```
|
||||
|
||||
用于自动修正 package.json 常见问题(等价于 `bun pm pkg fix`)。
|
||||
|
||||
## New API 兼容接口
|
||||
|
||||
为了支持 all-api-hub 等插件同步渠道,Worker 提供 New API 风格的渠道管理接口:
|
||||
|
||||
- 路径前缀:`/api/channel`
|
||||
- 认证方式:`Authorization: Bearer {NEW_API_ADMIN_TOKEN}`(或管理台登录 token)
|
||||
- 可选请求头:`New-Api-User: 1`
|
||||
|
||||
支持的常用接口:
|
||||
- `GET /api/channel` 渠道列表
|
||||
- `GET /api/channel/` 渠道列表(兼容尾斜杠)
|
||||
- `GET /api/group` 渠道分组列表
|
||||
- `GET /api/channel/search` 渠道搜索
|
||||
- `POST /api/channel` 新增渠道(单条)
|
||||
- `PUT /api/channel` 更新渠道
|
||||
- `DELETE /api/channel/:id` 删除渠道
|
||||
- `GET /api/channel/test/:id` 渠道连通性测试
|
||||
- `GET /api/channel/fetch_models/:id` 拉取渠道模型
|
||||
- `GET /api/user/models` 用户可用模型列表
|
||||
|
||||
## D1 数据库
|
||||
|
||||
在 `apps/worker/migrations/0001_init.sql` 定义表结构,使用 Wrangler 迁移:
|
||||
|
||||
```bash
|
||||
bun run --filter new-api-lite-worker db:migrate
|
||||
```
|
||||
|
||||
## 生产部署
|
||||
|
||||
### 1) 准备 Cloudflare 资源
|
||||
|
||||
1. 创建 D1 数据库并回填 `apps/worker/wrangler.toml` 的 `database_id`:
|
||||
|
||||
```bash
|
||||
bunx wrangler d1 create new_api_lite
|
||||
```
|
||||
|
||||
2. 设置 Worker 变量与密钥(推荐用 secret):
|
||||
|
||||
```bash
|
||||
bunx wrangler secret put ADMIN_PASSWORD
|
||||
bunx wrangler secret put NEW_API_ADMIN_TOKEN
|
||||
```
|
||||
|
||||
其他变量可继续放在 `apps/worker/wrangler.toml` 的 `[vars]` 中,`CORS_ORIGIN` 需指向管理台域名。
|
||||
|
||||
### 2) 执行生产迁移
|
||||
|
||||
```bash
|
||||
bunx wrangler d1 migrations apply DB
|
||||
```
|
||||
|
||||
### 3) 部署 Worker
|
||||
|
||||
```bash
|
||||
bun run --filter new-api-lite-worker deploy
|
||||
```
|
||||
|
||||
### 4) 构建并部署 Admin
|
||||
|
||||
```bash
|
||||
bun run --filter new-api-lite-admin build
|
||||
```
|
||||
|
||||
将 `apps/admin/dist` 发布到 Cloudflare Pages 或任意静态托管;如需指定 API 基址,请在构建前设置 `VITE_API_BASE`(例如 `apps/admin/.env.production`)。
|
||||
@@ -0,0 +1,18 @@
|
||||
<!doctype html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1.0"
|
||||
/>
|
||||
<title>api-workers</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script
|
||||
type="module"
|
||||
src="/src/main.ts"
|
||||
></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"name": "new-api-lite-admin",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"preview": "vite preview"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,718 @@
|
||||
import './styles.css';
|
||||
|
||||
type Channel = {
|
||||
id: string;
|
||||
name: string;
|
||||
base_url: string;
|
||||
api_key: string;
|
||||
weight: number;
|
||||
status: string;
|
||||
rate_limit: number;
|
||||
models_json?: string;
|
||||
};
|
||||
|
||||
type Token = {
|
||||
id: string;
|
||||
name: string;
|
||||
key_prefix: string;
|
||||
quota_total: number | null;
|
||||
quota_used: number;
|
||||
status: string;
|
||||
};
|
||||
|
||||
type UsageLog = {
|
||||
id: string;
|
||||
model: string;
|
||||
channel_id: string;
|
||||
channel_name?: string | null;
|
||||
token_id: string;
|
||||
token_name?: string | null;
|
||||
total_tokens: number;
|
||||
latency_ms: number;
|
||||
status: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
type DashboardData = {
|
||||
summary: {
|
||||
total_requests: number;
|
||||
total_tokens: number;
|
||||
avg_latency: number;
|
||||
total_errors: number;
|
||||
};
|
||||
byDay: Array<{ day: string; requests: number; tokens: number }>;
|
||||
byModel: Array<{ model: string; requests: number; tokens: number }>;
|
||||
byChannel: Array<{ channel_name: string; requests: number; tokens: number }>;
|
||||
byToken: Array<{ token_name: string; requests: number; tokens: number }>;
|
||||
};
|
||||
|
||||
type Settings = {
|
||||
log_retention_days: number;
|
||||
};
|
||||
|
||||
const root = document.querySelector<HTMLDivElement>('#app');
|
||||
const apiBase = import.meta.env.VITE_API_BASE ?? '';
|
||||
const tabs = [
|
||||
{ id: 'dashboard', label: '数据面板' },
|
||||
{ id: 'channels', label: '渠道管理' },
|
||||
{ id: 'models', label: '模型广场' },
|
||||
{ id: 'tokens', label: '令牌管理' },
|
||||
{ id: 'usage', label: '使用日志' },
|
||||
{ id: 'settings', label: '系统设置' }
|
||||
];
|
||||
|
||||
const state = {
|
||||
token: localStorage.getItem('admin_token'),
|
||||
activeTab: 'dashboard',
|
||||
loading: false,
|
||||
notice: '',
|
||||
data: {
|
||||
channels: [] as Channel[],
|
||||
tokens: [] as Token[],
|
||||
models: [] as Array<{ id: string; channels: Array<{ id: string; name: string }> }>,
|
||||
usage: [] as UsageLog[],
|
||||
dashboard: null as DashboardData | null,
|
||||
settings: null as Settings | null
|
||||
}
|
||||
};
|
||||
|
||||
if (!root) {
|
||||
throw new Error('Missing #app root');
|
||||
}
|
||||
|
||||
function setNotice(message: string) {
|
||||
state.notice = message;
|
||||
render();
|
||||
}
|
||||
|
||||
function setToken(token: string | null) {
|
||||
state.token = token;
|
||||
if (token) {
|
||||
localStorage.setItem('admin_token', token);
|
||||
} else {
|
||||
localStorage.removeItem('admin_token');
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
async function apiFetch<T>(path: string, options: RequestInit = {}): Promise<T> {
|
||||
const headers = new Headers(options.headers ?? {});
|
||||
headers.set('Content-Type', 'application/json');
|
||||
if (state.token) {
|
||||
headers.set('Authorization', `Bearer ${state.token}`);
|
||||
}
|
||||
const response = await fetch(`${apiBase}${path}`, { ...options, headers });
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
setToken(null);
|
||||
}
|
||||
const payload = await response.json().catch(() => null);
|
||||
throw new Error(payload?.error ?? `HTTP ${response.status}`);
|
||||
}
|
||||
return response.json() as Promise<T>;
|
||||
}
|
||||
|
||||
async function loadDashboard() {
|
||||
state.data.dashboard = await apiFetch<DashboardData>('/api/dashboard');
|
||||
}
|
||||
|
||||
async function loadChannels() {
|
||||
const result = await apiFetch<{ channels: Channel[] }>('/api/channels');
|
||||
state.data.channels = result.channels;
|
||||
}
|
||||
|
||||
async function loadModels() {
|
||||
const result = await apiFetch<{ models: Array<{ id: string; channels: Array<{ id: string; name: string }> }> }>('/api/models');
|
||||
state.data.models = result.models;
|
||||
}
|
||||
|
||||
async function loadTokens() {
|
||||
const result = await apiFetch<{ tokens: Token[] }>('/api/tokens');
|
||||
state.data.tokens = result.tokens;
|
||||
}
|
||||
|
||||
async function loadUsage() {
|
||||
const result = await apiFetch<{ logs: UsageLog[] }>('/api/usage?limit=50');
|
||||
state.data.usage = result.logs;
|
||||
}
|
||||
|
||||
async function loadSettings() {
|
||||
state.data.settings = await apiFetch<Settings>('/api/settings');
|
||||
}
|
||||
|
||||
async function loadTab(tabId: string) {
|
||||
state.loading = true;
|
||||
state.notice = '';
|
||||
render();
|
||||
try {
|
||||
if (tabId === 'dashboard') {
|
||||
await loadDashboard();
|
||||
}
|
||||
if (tabId === 'channels') {
|
||||
await loadChannels();
|
||||
}
|
||||
if (tabId === 'models') {
|
||||
await loadModels();
|
||||
}
|
||||
if (tabId === 'tokens') {
|
||||
await loadTokens();
|
||||
}
|
||||
if (tabId === 'usage') {
|
||||
await loadUsage();
|
||||
}
|
||||
if (tabId === 'settings') {
|
||||
await loadSettings();
|
||||
}
|
||||
} catch (error) {
|
||||
setNotice((error as Error).message);
|
||||
} finally {
|
||||
state.loading = false;
|
||||
render();
|
||||
}
|
||||
}
|
||||
|
||||
function renderLogin() {
|
||||
return `
|
||||
<div class="login-card">
|
||||
<h1>api-workers</h1>
|
||||
<p>请输入管理员密码登录管理台。</p>
|
||||
<form id="login-form" class="form-grid">
|
||||
<div>
|
||||
<label for="password">管理员密码</label>
|
||||
<input id="password" name="password" type="password" required />
|
||||
</div>
|
||||
<button class="button" type="submit">登录</button>
|
||||
</form>
|
||||
${state.notice ? `<div class="notice">${state.notice}</div>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSidebar() {
|
||||
return `
|
||||
<aside class="sidebar">
|
||||
<div class="brand">
|
||||
<h2>api-workers</h2>
|
||||
<span>console</span>
|
||||
</div>
|
||||
<nav class="nav-list">
|
||||
${tabs
|
||||
.map(
|
||||
(tab) => `
|
||||
<button class="nav-item ${state.activeTab === tab.id ? 'active' : ''}" data-tab="${tab.id}" type="button">
|
||||
${tab.label}
|
||||
</button>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</nav>
|
||||
</aside>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
const data = state.data.dashboard;
|
||||
if (!data) {
|
||||
return '<div class="card">暂无数据</div>';
|
||||
}
|
||||
const errorRate = data.summary.total_requests
|
||||
? Math.round((data.summary.total_errors / data.summary.total_requests) * 100)
|
||||
: 0;
|
||||
return `
|
||||
<div class="grid three">
|
||||
<div class="card kpi">
|
||||
<span class="badge">总请求</span>
|
||||
<div class="value">${data.summary.total_requests}</div>
|
||||
<span class="mono">最近窗口</span>
|
||||
</div>
|
||||
<div class="card kpi">
|
||||
<span class="badge">总 Tokens</span>
|
||||
<div class="value">${data.summary.total_tokens}</div>
|
||||
<span class="mono">累计消耗</span>
|
||||
</div>
|
||||
<div class="card kpi">
|
||||
<span class="badge">错误率</span>
|
||||
<div class="value">${errorRate}%</div>
|
||||
<span class="mono">平均延迟 ${Math.round(data.summary.avg_latency)}ms</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid two" style="margin-top: 20px;">
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>按日趋势</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>日期</th><th>请求</th><th>Tokens</th></tr></thead>
|
||||
<tbody>
|
||||
${data.byDay
|
||||
.map((row) => `<tr><td>${row.day}</td><td>${row.requests}</td><td>${row.tokens}</td></tr>`)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>模型排行</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>模型</th><th>请求</th><th>Tokens</th></tr></thead>
|
||||
<tbody>
|
||||
${data.byModel
|
||||
.map((row) => `<tr><td>${row.model ?? '-'}</td><td>${row.requests}</td><td>${row.tokens}</td></tr>`)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid two" style="margin-top: 20px;">
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>渠道贡献</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>渠道</th><th>请求</th><th>Tokens</th></tr></thead>
|
||||
<tbody>
|
||||
${data.byChannel
|
||||
.map((row) => `<tr><td>${row.channel_name ?? '-'}</td><td>${row.requests}</td><td>${row.tokens}</td></tr>`)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>令牌贡献</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>令牌</th><th>请求</th><th>Tokens</th></tr></thead>
|
||||
<tbody>
|
||||
${data.byToken
|
||||
.map((row) => `<tr><td>${row.token_name ?? '-'}</td><td>${row.requests}</td><td>${row.tokens}</td></tr>`)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderChannels() {
|
||||
return `
|
||||
<div class="grid two">
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>新增渠道</h3>
|
||||
</div>
|
||||
<form id="channel-form" class="form-grid">
|
||||
<div>
|
||||
<label for="channel-id">渠道 ID(可选)</label>
|
||||
<input id="channel-id" name="id" placeholder="例如 ch_openai" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="channel-name">名称</label>
|
||||
<input id="channel-name" name="name" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="channel-base">Base URL</label>
|
||||
<input id="channel-base" name="base_url" placeholder="https://api.openai.com" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="channel-key">API Key</label>
|
||||
<input id="channel-key" name="api_key" required />
|
||||
</div>
|
||||
<div class="form-grid two">
|
||||
<div>
|
||||
<label for="channel-weight">权重</label>
|
||||
<input id="channel-weight" name="weight" type="number" min="1" value="1" />
|
||||
</div>
|
||||
<div>
|
||||
<label for="channel-rate">限流</label>
|
||||
<input id="channel-rate" name="rate_limit" type="number" min="0" value="0" />
|
||||
</div>
|
||||
</div>
|
||||
<button class="button" type="submit">创建渠道</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>渠道列表</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>ID</th><th>名称</th><th>状态</th><th>权重</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.data.channels
|
||||
.map(
|
||||
(channel) => `
|
||||
<tr>
|
||||
<td class="mono">${channel.id}</td>
|
||||
<td>${channel.name}</td>
|
||||
<td>${channel.status}</td>
|
||||
<td>${channel.weight}</td>
|
||||
<td>
|
||||
<button class="button secondary" type="button" data-action="channel-test" data-id="${channel.id}">连通测试</button>
|
||||
<button class="button ghost" type="button" data-action="channel-toggle" data-id="${channel.id}" data-status="${channel.status}">切换状态</button>
|
||||
<button class="button ghost" type="button" data-action="channel-delete" data-id="${channel.id}">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderModels() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>模型广场</h3>
|
||||
<span class="badge">${state.data.models.length} 个模型</span>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>模型</th><th>渠道</th></tr></thead>
|
||||
<tbody>
|
||||
${state.data.models
|
||||
.map(
|
||||
(model) => `
|
||||
<tr>
|
||||
<td>${model.id}</td>
|
||||
<td>${model.channels.map((channel) => channel.name).join(' / ')}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderTokens() {
|
||||
return `
|
||||
<div class="grid two">
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>生成令牌</h3>
|
||||
</div>
|
||||
<form id="token-form" class="form-grid">
|
||||
<div>
|
||||
<label for="token-name">名称</label>
|
||||
<input id="token-name" name="name" required />
|
||||
</div>
|
||||
<div>
|
||||
<label for="token-quota">额度(可选)</label>
|
||||
<input id="token-quota" name="quota_total" type="number" min="0" placeholder="留空表示无限" />
|
||||
</div>
|
||||
<button class="button" type="submit">生成令牌</button>
|
||||
</form>
|
||||
</div>
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>令牌列表</h3>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr><th>名称</th><th>状态</th><th>已用/额度</th><th>操作</th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
${state.data.tokens
|
||||
.map(
|
||||
(token) => `
|
||||
<tr>
|
||||
<td>${token.name}</td>
|
||||
<td>${token.status}</td>
|
||||
<td>${token.quota_used} / ${token.quota_total ?? '∞'}</td>
|
||||
<td>
|
||||
<button class="button secondary" type="button" data-action="token-reveal" data-id="${token.id}">查看</button>
|
||||
<button class="button secondary" type="button" data-action="token-toggle" data-id="${token.id}" data-status="${token.status}">切换</button>
|
||||
<button class="button ghost" type="button" data-action="token-delete" data-id="${token.id}">删除</button>
|
||||
</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderUsage() {
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>使用日志</h3>
|
||||
<button class="button secondary" type="button" data-action="usage-refresh">刷新</button>
|
||||
</div>
|
||||
<table class="table">
|
||||
<thead><tr><th>时间</th><th>模型</th><th>渠道</th><th>Tokens</th><th>延迟</th><th>状态</th></tr></thead>
|
||||
<tbody>
|
||||
${state.data.usage
|
||||
.map(
|
||||
(log) => `
|
||||
<tr>
|
||||
<td>${log.created_at?.slice(0, 19).replace('T', ' ')}</td>
|
||||
<td>${log.model ?? '-'}</td>
|
||||
<td>${log.channel_name ?? log.channel_id ?? '-'}</td>
|
||||
<td>${log.total_tokens ?? 0}</td>
|
||||
<td>${log.latency_ms ?? 0} ms</td>
|
||||
<td>${log.status}</td>
|
||||
</tr>
|
||||
`
|
||||
)
|
||||
.join('')}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderSettings() {
|
||||
const retention = state.data.settings?.log_retention_days ?? 30;
|
||||
return `
|
||||
<div class="card">
|
||||
<div class="section-header">
|
||||
<h3>系统设置</h3>
|
||||
</div>
|
||||
<form id="settings-form" class="form-grid two">
|
||||
<div>
|
||||
<label for="retention">日志保留天数</label>
|
||||
<input id="retention" name="log_retention_days" type="number" min="1" value="${retention}" />
|
||||
</div>
|
||||
<div style="display: flex; align-items: flex-end;">
|
||||
<button class="button" type="submit">保存设置</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
if (state.loading) {
|
||||
return '<div class="card">加载中...</div>';
|
||||
}
|
||||
if (state.activeTab === 'dashboard') {
|
||||
return renderDashboard();
|
||||
}
|
||||
if (state.activeTab === 'channels') {
|
||||
return renderChannels();
|
||||
}
|
||||
if (state.activeTab === 'models') {
|
||||
return renderModels();
|
||||
}
|
||||
if (state.activeTab === 'tokens') {
|
||||
return renderTokens();
|
||||
}
|
||||
if (state.activeTab === 'usage') {
|
||||
return renderUsage();
|
||||
}
|
||||
if (state.activeTab === 'settings') {
|
||||
return renderSettings();
|
||||
}
|
||||
return '<div class="card">未知模块</div>';
|
||||
}
|
||||
|
||||
function renderApp() {
|
||||
return `
|
||||
<div class="app-shell">
|
||||
${renderSidebar()}
|
||||
<main class="main">
|
||||
<div class="top-bar">
|
||||
<div>
|
||||
<h1>${tabs.find((tab) => tab.id === state.activeTab)?.label ?? '管理台'}</h1>
|
||||
<p>集中管理渠道、模型、令牌与使用情况。</p>
|
||||
</div>
|
||||
<div class="top-bar-actions">
|
||||
<span class="badge">${state.token ? '已登录' : '未登录'}</span>
|
||||
<button class="button ghost" type="button" data-action="logout">退出</button>
|
||||
</div>
|
||||
</div>
|
||||
${state.notice ? `<div class="notice">${state.notice}</div>` : ''}
|
||||
${renderContent()}
|
||||
</main>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function render() {
|
||||
root!.innerHTML = state.token ? renderApp() : renderLogin();
|
||||
bindEvents();
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
const loginForm = document.querySelector<HTMLFormElement>('#login-form');
|
||||
if (loginForm) {
|
||||
loginForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(loginForm);
|
||||
const password = String(formData.get('password') ?? '');
|
||||
try {
|
||||
const result = await apiFetch<{ token: string }>('/api/auth/login', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ password })
|
||||
});
|
||||
setToken(result.token);
|
||||
state.notice = '';
|
||||
await loadTab(state.activeTab);
|
||||
} catch (error) {
|
||||
setNotice((error as Error).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const channelForm = document.querySelector<HTMLFormElement>('#channel-form');
|
||||
if (channelForm) {
|
||||
channelForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(channelForm);
|
||||
const payload = Object.fromEntries(formData.entries());
|
||||
const channelId = String(payload.id ?? '').trim();
|
||||
try {
|
||||
const body = {
|
||||
...payload,
|
||||
weight: Number(payload.weight ?? 1),
|
||||
rate_limit: Number(payload.rate_limit ?? 0)
|
||||
} as Record<string, unknown>;
|
||||
if (channelId) {
|
||||
body.id = channelId;
|
||||
} else {
|
||||
delete body.id;
|
||||
}
|
||||
await apiFetch('/api/channels', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
channelForm.reset();
|
||||
await loadChannels();
|
||||
render();
|
||||
} catch (error) {
|
||||
setNotice((error as Error).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const tokenForm = document.querySelector<HTMLFormElement>('#token-form');
|
||||
if (tokenForm) {
|
||||
tokenForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(tokenForm);
|
||||
const payload = Object.fromEntries(formData.entries());
|
||||
try {
|
||||
const result = await apiFetch<{ token: string }>('/api/tokens', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: payload.name,
|
||||
quota_total: payload.quota_total ? Number(payload.quota_total) : null
|
||||
})
|
||||
});
|
||||
setNotice(`新令牌: ${result.token}`);
|
||||
tokenForm.reset();
|
||||
await loadTokens();
|
||||
render();
|
||||
} catch (error) {
|
||||
setNotice((error as Error).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const settingsForm = document.querySelector<HTMLFormElement>('#settings-form');
|
||||
if (settingsForm) {
|
||||
settingsForm.addEventListener('submit', async (event) => {
|
||||
event.preventDefault();
|
||||
const formData = new FormData(settingsForm);
|
||||
const value = Number(formData.get('log_retention_days'));
|
||||
try {
|
||||
await apiFetch('/api/settings', {
|
||||
method: 'PUT',
|
||||
body: JSON.stringify({ log_retention_days: value })
|
||||
});
|
||||
await loadSettings();
|
||||
setNotice('设置已更新');
|
||||
} catch (error) {
|
||||
setNotice((error as Error).message);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
root!.querySelectorAll('[data-tab]').forEach((item) => {
|
||||
item.addEventListener('click', () => {
|
||||
const tab = (item as HTMLElement).dataset.tab ?? 'dashboard';
|
||||
state.activeTab = tab;
|
||||
loadTab(tab);
|
||||
});
|
||||
});
|
||||
|
||||
root!.querySelectorAll('[data-action]').forEach((item) => {
|
||||
item.addEventListener('click', async () => {
|
||||
const action = (item as HTMLElement).dataset.action ?? '';
|
||||
const id = (item as HTMLElement).dataset.id ?? '';
|
||||
try {
|
||||
if (action === 'logout') {
|
||||
await apiFetch('/api/auth/logout', { method: 'POST' }).catch(() => null);
|
||||
setToken(null);
|
||||
return;
|
||||
}
|
||||
if (action === 'channel-test') {
|
||||
const result = await apiFetch<{ models: Array<{ id: string }> }>(`/api/channels/${id}/test`, { method: 'POST' });
|
||||
await loadChannels();
|
||||
setNotice(`连通测试完成,模型数 ${result.models?.length ?? 0}`);
|
||||
render();
|
||||
}
|
||||
if (action === 'channel-delete') {
|
||||
await apiFetch(`/api/channels/${id}`, { method: 'DELETE' });
|
||||
await loadChannels();
|
||||
setNotice('渠道已删除');
|
||||
render();
|
||||
}
|
||||
if (action === 'channel-toggle') {
|
||||
const status = (item as HTMLElement).dataset.status === 'active' ? 'disabled' : 'active';
|
||||
await apiFetch(`/api/channels/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
await loadChannels();
|
||||
setNotice(`渠道已${status === 'active' ? '启用' : '停用'}`);
|
||||
render();
|
||||
}
|
||||
if (action === 'token-delete') {
|
||||
await apiFetch(`/api/tokens/${id}`, { method: 'DELETE' });
|
||||
await loadTokens();
|
||||
setNotice('令牌已删除');
|
||||
render();
|
||||
}
|
||||
if (action === 'token-reveal') {
|
||||
const result = await apiFetch<{ token: string | null }>(`/api/tokens/${id}/reveal`);
|
||||
setNotice(result.token ? `令牌: ${result.token}` : '未找到令牌');
|
||||
}
|
||||
if (action === 'token-toggle') {
|
||||
const status = (item as HTMLElement).dataset.status === 'active' ? 'disabled' : 'active';
|
||||
await apiFetch(`/api/tokens/${id}`, {
|
||||
method: 'PATCH',
|
||||
body: JSON.stringify({ status })
|
||||
});
|
||||
await loadTokens();
|
||||
setNotice(`令牌已${status === 'active' ? '启用' : '停用'}`);
|
||||
render();
|
||||
}
|
||||
if (action === 'usage-refresh') {
|
||||
await loadUsage();
|
||||
setNotice('日志已刷新');
|
||||
render();
|
||||
}
|
||||
} catch (error) {
|
||||
setNotice((error as Error).message);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
render();
|
||||
|
||||
if (state.token) {
|
||||
loadTab(state.activeTab);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Sans:wght@300;400;500;600&family=Space+Grotesk:wght@400;500;600;700&display=swap');
|
||||
|
||||
:root {
|
||||
--bg: #f6f4ef;
|
||||
--surface: #ffffff;
|
||||
--surface-alt: #f0ede7;
|
||||
--text: #1b1b19;
|
||||
--muted: #6b6760;
|
||||
--border: #e2ddd3;
|
||||
--accent: #d4af37;
|
||||
--accent-dark: #b9972a;
|
||||
--shadow: 0 12px 30px rgba(27, 27, 25, 0.08);
|
||||
--radius: 16px;
|
||||
--radius-sm: 10px;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
font-family: 'IBM Plex Sans', sans-serif;
|
||||
background: radial-gradient(circle at top, #ffffff 0%, #f3efe8 40%, #efeae2 100%);
|
||||
color: var(--text);
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4 {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
margin: 0 0 0.75rem 0;
|
||||
letter-spacing: -0.02em;
|
||||
}
|
||||
|
||||
p {
|
||||
margin: 0 0 1rem 0;
|
||||
color: var(--muted);
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
button,
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.app-shell {
|
||||
display: grid;
|
||||
grid-template-columns: 260px 1fr;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
padding: 32px 20px;
|
||||
background: var(--surface);
|
||||
border-right: 1px solid var(--border);
|
||||
position: sticky;
|
||||
top: 0;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.brand {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
|
||||
.brand span {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.24em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.nav-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.nav-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 10px 14px;
|
||||
border-radius: 12px;
|
||||
border: none;
|
||||
background: transparent;
|
||||
text-align: left;
|
||||
width: 100%;
|
||||
min-height: 44px;
|
||||
color: var(--muted);
|
||||
font-weight: 500;
|
||||
transition: all 0.2s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.nav-item.active {
|
||||
background: var(--surface-alt);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.nav-item:hover {
|
||||
background: var(--surface-alt);
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.main {
|
||||
padding: 32px 40px 60px;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.top-bar-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.badge {
|
||||
padding: 4px 10px;
|
||||
border-radius: 999px;
|
||||
background: var(--surface-alt);
|
||||
font-size: 12px;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.grid.two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grid.three {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.card {
|
||||
background: var(--surface);
|
||||
border-radius: var(--radius);
|
||||
padding: 20px;
|
||||
box-shadow: var(--shadow);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.card h3 {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.button {
|
||||
background: var(--text);
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 10px 16px;
|
||||
border-radius: 10px;
|
||||
cursor: pointer;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s ease;
|
||||
min-height: 44px;
|
||||
}
|
||||
|
||||
.button.secondary {
|
||||
background: var(--surface-alt);
|
||||
color: var(--text);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.button.ghost {
|
||||
background: transparent;
|
||||
color: var(--muted);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
.button:hover:not(:disabled) {
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 8px 18px rgba(27, 27, 25, 0.12);
|
||||
}
|
||||
|
||||
.button:focus-visible,
|
||||
.nav-item:focus-visible {
|
||||
outline: 2px solid rgba(212, 175, 55, 0.6);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.table th,
|
||||
.table td {
|
||||
text-align: left;
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.table th {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.table tr:hover {
|
||||
background: #faf8f3;
|
||||
}
|
||||
|
||||
.form-grid {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.form-grid.two {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
label {
|
||||
font-size: 12px;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.12em;
|
||||
color: var(--muted);
|
||||
display: block;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
padding: 10px 12px;
|
||||
border-radius: 10px;
|
||||
border: 1px solid var(--border);
|
||||
background: #fff;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
input:focus,
|
||||
select:focus,
|
||||
textarea:focus {
|
||||
outline: 2px solid rgba(212, 175, 55, 0.4);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.notice {
|
||||
padding: 12px 16px;
|
||||
background: #fff8e1;
|
||||
border: 1px solid #f1e0b3;
|
||||
border-radius: 12px;
|
||||
color: #7a5d00;
|
||||
margin-top: 12px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.login-card {
|
||||
max-width: 420px;
|
||||
margin: 12vh auto 0;
|
||||
background: var(--surface);
|
||||
padding: 32px;
|
||||
border-radius: 20px;
|
||||
border: 1px solid var(--border);
|
||||
box-shadow: var(--shadow);
|
||||
}
|
||||
|
||||
.login-card h1 {
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.kpi {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.kpi .value {
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.mono {
|
||||
font-family: 'Space Grotesk', sans-serif;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
@media (max-width: 1024px) {
|
||||
.app-shell {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
position: static;
|
||||
height: auto;
|
||||
border-right: none;
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
|
||||
.grid.two,
|
||||
.grid.three,
|
||||
.form-grid.two {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.main {
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.top-bar {
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
gap: 12px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
* {
|
||||
transition: none !important;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"lib": ["ES2022", "DOM"],
|
||||
"types": ["vite/client"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { defineConfig } from 'vite';
|
||||
|
||||
const apiTarget = process.env.VITE_API_TARGET ?? 'http://localhost:8787';
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
outDir: 'dist'
|
||||
},
|
||||
server: {
|
||||
proxy: {
|
||||
'/api': apiTarget,
|
||||
'/v1': apiTarget
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,55 @@
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
base_url TEXT NOT NULL,
|
||||
api_key TEXT NOT NULL,
|
||||
weight INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
rate_limit INTEGER DEFAULT 0,
|
||||
models_json TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
quota_total INTEGER,
|
||||
quota_used INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
allowed_channels TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_id TEXT,
|
||||
channel_id TEXT,
|
||||
model TEXT,
|
||||
request_path TEXT,
|
||||
total_tokens INTEGER,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
cost REAL,
|
||||
latency_ms INTEGER,
|
||||
status TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS usage_logs_created_at ON usage_logs(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_hash TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1 @@
|
||||
ALTER TABLE tokens ADD COLUMN token_plain TEXT;
|
||||
@@ -0,0 +1,6 @@
|
||||
ALTER TABLE channels ADD COLUMN type INTEGER NOT NULL DEFAULT 1;
|
||||
ALTER TABLE channels ADD COLUMN group_name TEXT;
|
||||
ALTER TABLE channels ADD COLUMN priority INTEGER NOT NULL DEFAULT 0;
|
||||
ALTER TABLE channels ADD COLUMN metadata_json TEXT;
|
||||
ALTER TABLE channels ADD COLUMN test_time INTEGER;
|
||||
ALTER TABLE channels ADD COLUMN response_time_ms INTEGER;
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "new-api-lite-worker",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wrangler dev",
|
||||
"deploy": "wrangler deploy",
|
||||
"db:migrate": "wrangler d1 migrations apply DB --local"
|
||||
},
|
||||
"dependencies": {
|
||||
"hono": "^4.2.7"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240222.0",
|
||||
"typescript": "^5.4.5",
|
||||
"wrangler": "^3.48.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
CREATE TABLE IF NOT EXISTS channels (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
base_url TEXT NOT NULL,
|
||||
api_key TEXT NOT NULL,
|
||||
type INTEGER NOT NULL DEFAULT 1,
|
||||
group_name TEXT,
|
||||
priority INTEGER NOT NULL DEFAULT 0,
|
||||
weight INTEGER NOT NULL DEFAULT 1,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
rate_limit INTEGER DEFAULT 0,
|
||||
models_json TEXT,
|
||||
metadata_json TEXT,
|
||||
test_time INTEGER,
|
||||
response_time_ms INTEGER,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tokens (
|
||||
id TEXT PRIMARY KEY,
|
||||
name TEXT NOT NULL,
|
||||
key_hash TEXT NOT NULL,
|
||||
key_prefix TEXT NOT NULL,
|
||||
token_plain TEXT,
|
||||
quota_total INTEGER,
|
||||
quota_used INTEGER NOT NULL DEFAULT 0,
|
||||
status TEXT NOT NULL DEFAULT 'active',
|
||||
allowed_channels TEXT,
|
||||
created_at TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS usage_logs (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_id TEXT,
|
||||
channel_id TEXT,
|
||||
model TEXT,
|
||||
request_path TEXT,
|
||||
total_tokens INTEGER,
|
||||
prompt_tokens INTEGER,
|
||||
completion_tokens INTEGER,
|
||||
cost REAL,
|
||||
latency_ms INTEGER,
|
||||
status TEXT,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS usage_logs_created_at ON usage_logs(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS admin_sessions (
|
||||
id TEXT PRIMARY KEY,
|
||||
token_hash TEXT NOT NULL,
|
||||
expires_at TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,15 @@
|
||||
export type Bindings = {
|
||||
DB: D1Database;
|
||||
ADMIN_PASSWORD: string;
|
||||
SESSION_TTL_HOURS?: string;
|
||||
LOG_RETENTION_DAYS?: string;
|
||||
CORS_ORIGIN?: string;
|
||||
PROXY_RETRY_ROUNDS?: string;
|
||||
PROXY_RETRY_DELAY_MS?: string;
|
||||
NEW_API_ADMIN_TOKEN?: string;
|
||||
NEWAPI_ADMIN_TOKEN?: string;
|
||||
};
|
||||
|
||||
export type AppEnv = {
|
||||
Bindings: Bindings;
|
||||
};
|
||||
@@ -0,0 +1,68 @@
|
||||
import { Hono } from 'hono';
|
||||
import { cors } from 'hono/cors';
|
||||
import { logger } from 'hono/logger';
|
||||
import type { AppEnv } from './env';
|
||||
import authRoutes from './routes/auth';
|
||||
import channelRoutes from './routes/channels';
|
||||
import modelRoutes from './routes/models';
|
||||
import tokenRoutes from './routes/tokens';
|
||||
import usageRoutes from './routes/usage';
|
||||
import dashboardRoutes from './routes/dashboard';
|
||||
import settingsRoutes from './routes/settings';
|
||||
import proxyRoutes from './routes/proxy';
|
||||
import newapiChannelRoutes from './routes/newapiChannels';
|
||||
import newapiUserRoutes from './routes/newapiUsers';
|
||||
import newapiGroupRoutes from './routes/newapiGroups';
|
||||
import { adminAuth } from './middleware/adminAuth';
|
||||
|
||||
const app = new Hono<AppEnv>({ strict: false });
|
||||
|
||||
app.use('*', logger());
|
||||
app.use(
|
||||
'/api/*',
|
||||
cors({
|
||||
origin: (origin, c) => {
|
||||
const allowed = c.env.CORS_ORIGIN ?? '*';
|
||||
return allowed === '*' ? '*' : allowed.split(',').map((item) => item.trim());
|
||||
},
|
||||
allowHeaders: ['Content-Type', 'Authorization', 'x-api-key', 'x-admin-token', 'New-Api-User'],
|
||||
allowMethods: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS']
|
||||
})
|
||||
);
|
||||
app.use(
|
||||
'/v1/*',
|
||||
cors({
|
||||
origin: '*',
|
||||
allowHeaders: ['Content-Type', 'Authorization', 'x-api-key'],
|
||||
allowMethods: ['GET', 'POST', 'OPTIONS']
|
||||
})
|
||||
);
|
||||
|
||||
app.use('/api/*', async (c, next) => {
|
||||
if (
|
||||
c.req.path === '/api/auth/login' ||
|
||||
c.req.path.startsWith('/api/channel') ||
|
||||
c.req.path.startsWith('/api/user') ||
|
||||
c.req.path.startsWith('/api/group')
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
return adminAuth(c, next);
|
||||
});
|
||||
|
||||
app.get('/health', (c) => c.json({ ok: true }));
|
||||
|
||||
app.route('/api/auth', authRoutes);
|
||||
app.route('/api/channels', channelRoutes);
|
||||
app.route('/api/models', modelRoutes);
|
||||
app.route('/api/tokens', tokenRoutes);
|
||||
app.route('/api/usage', usageRoutes);
|
||||
app.route('/api/dashboard', dashboardRoutes);
|
||||
app.route('/api/settings', settingsRoutes);
|
||||
app.route('/api/channel', newapiChannelRoutes);
|
||||
app.route('/api/user', newapiUserRoutes);
|
||||
app.route('/api/group', newapiGroupRoutes);
|
||||
|
||||
app.route('/v1', proxyRoutes);
|
||||
|
||||
export default app;
|
||||
@@ -0,0 +1,36 @@
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { sha256Hex } from '../utils/crypto';
|
||||
import { getBearerToken } from '../utils/request';
|
||||
import { jsonError } from '../utils/http';
|
||||
import type { AppEnv } from '../env';
|
||||
|
||||
/**
|
||||
* Validates admin session tokens for management APIs.
|
||||
*/
|
||||
export const adminAuth = createMiddleware<AppEnv>(async (c, next) => {
|
||||
const token = getBearerToken(c);
|
||||
if (!token) {
|
||||
return jsonError(c, 401, 'admin_token_required', 'admin_token_required');
|
||||
}
|
||||
|
||||
const tokenHash = await sha256Hex(token);
|
||||
const session = await c.env.DB.prepare(
|
||||
'SELECT id, expires_at FROM admin_sessions WHERE token_hash = ?'
|
||||
)
|
||||
.bind(tokenHash)
|
||||
.first();
|
||||
|
||||
if (!session) {
|
||||
return jsonError(c, 401, 'invalid_admin_token', 'invalid_admin_token');
|
||||
}
|
||||
|
||||
if (new Date(String(session.expires_at)).getTime() <= Date.now()) {
|
||||
await c.env.DB.prepare('DELETE FROM admin_sessions WHERE id = ?')
|
||||
.bind(String(session.id))
|
||||
.run();
|
||||
return jsonError(c, 401, 'admin_session_expired', 'admin_session_expired');
|
||||
}
|
||||
|
||||
c.set('adminSessionId', String(session.id));
|
||||
await next();
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { sha256Hex } from '../utils/crypto';
|
||||
import { getBearerToken } from '../utils/request';
|
||||
import type { AppEnv } from '../env';
|
||||
|
||||
function newApiError(c: any, status: number, message: string) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
message
|
||||
},
|
||||
status
|
||||
);
|
||||
}
|
||||
|
||||
function readNewApiUserId(c: any): string | null {
|
||||
return c.req.header('New-Api-User') ?? c.req.header('new-api-user') ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates New API admin tokens or admin sessions.
|
||||
*/
|
||||
export const newApiAuth = createMiddleware<AppEnv>(async (c, next) => {
|
||||
const token = getBearerToken(c);
|
||||
if (!token) {
|
||||
return newApiError(c, 401, 'unauthorized');
|
||||
}
|
||||
|
||||
const envToken = c.env.NEW_API_ADMIN_TOKEN ?? c.env.NEWAPI_ADMIN_TOKEN;
|
||||
if (envToken && token === envToken) {
|
||||
c.set('newApiUserId', readNewApiUserId(c));
|
||||
await next();
|
||||
return;
|
||||
}
|
||||
|
||||
const tokenHash = await sha256Hex(token);
|
||||
const session = await c.env.DB.prepare(
|
||||
'SELECT id, expires_at FROM admin_sessions WHERE token_hash = ?'
|
||||
)
|
||||
.bind(tokenHash)
|
||||
.first<{ id: string; expires_at: string }>();
|
||||
|
||||
if (!session) {
|
||||
return newApiError(c, 401, 'unauthorized');
|
||||
}
|
||||
|
||||
if (new Date(String(session.expires_at)).getTime() <= Date.now()) {
|
||||
await c.env.DB.prepare('DELETE FROM admin_sessions WHERE id = ?')
|
||||
.bind(String(session.id))
|
||||
.run();
|
||||
return newApiError(c, 401, 'session_expired');
|
||||
}
|
||||
|
||||
c.set('newApiUserId', readNewApiUserId(c));
|
||||
await next();
|
||||
});
|
||||
@@ -0,0 +1,56 @@
|
||||
import { createMiddleware } from 'hono/factory';
|
||||
import { sha256Hex } from '../utils/crypto';
|
||||
import { getBearerToken } from '../utils/request';
|
||||
import { jsonError } from '../utils/http';
|
||||
import { canConsumeQuota, normalizeQuota } from '../services/quota';
|
||||
import type { AppEnv } from '../env';
|
||||
|
||||
export type TokenRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
quota_total: number | null;
|
||||
quota_used: number;
|
||||
status: string;
|
||||
allowed_channels: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Validates API tokens for the OpenAI-compatible proxy.
|
||||
*/
|
||||
export const tokenAuth = createMiddleware<AppEnv>(async (c, next) => {
|
||||
const token = getBearerToken(c);
|
||||
if (!token) {
|
||||
return jsonError(c, 401, 'token_required', 'token_required');
|
||||
}
|
||||
|
||||
const tokenHash = await sha256Hex(token);
|
||||
const record = await c.env.DB.prepare(
|
||||
'SELECT id, name, quota_total, quota_used, status, allowed_channels FROM tokens WHERE key_hash = ?'
|
||||
)
|
||||
.bind(tokenHash)
|
||||
.first<TokenRecord>();
|
||||
|
||||
if (!record) {
|
||||
return jsonError(c, 401, 'invalid_token', 'invalid_token');
|
||||
}
|
||||
|
||||
if (record.status !== 'active') {
|
||||
return jsonError(c, 403, 'token_disabled', 'token_disabled');
|
||||
}
|
||||
|
||||
const quotaTotal = record.quota_total === null || record.quota_total === undefined
|
||||
? null
|
||||
: Number(record.quota_total);
|
||||
const quotaUsed = Number(record.quota_used ?? 0);
|
||||
const normalized = normalizeQuota(Number.isNaN(quotaTotal) ? null : quotaTotal, Number.isNaN(quotaUsed) ? 0 : quotaUsed);
|
||||
if (!canConsumeQuota(normalized.quotaTotal, normalized.quotaUsed, 1)) {
|
||||
return jsonError(c, 402, 'quota_exceeded', 'quota_exceeded');
|
||||
}
|
||||
|
||||
c.set('tokenRecord', {
|
||||
...record,
|
||||
quota_total: normalized.quotaTotal,
|
||||
quota_used: normalized.quotaUsed
|
||||
});
|
||||
await next();
|
||||
});
|
||||
@@ -0,0 +1,53 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { addHours, nowIso } from '../utils/time';
|
||||
import { generateToken, sha256Hex } from '../utils/crypto';
|
||||
import { jsonError } from '../utils/http';
|
||||
import { adminAuth } from '../middleware/adminAuth';
|
||||
|
||||
const auth = new Hono<AppEnv>();
|
||||
|
||||
/**
|
||||
* Logs in the admin user with a password.
|
||||
*/
|
||||
auth.post('/login', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body?.password) {
|
||||
return jsonError(c, 400, 'password_required', 'password_required');
|
||||
}
|
||||
|
||||
if (body.password !== c.env.ADMIN_PASSWORD) {
|
||||
return jsonError(c, 401, 'invalid_password', 'invalid_password');
|
||||
}
|
||||
|
||||
const rawToken = generateToken('admin_');
|
||||
const tokenHash = await sha256Hex(rawToken);
|
||||
const ttlHours = Number(c.env.SESSION_TTL_HOURS ?? '12');
|
||||
const expiresAt = addHours(new Date(), Number.isNaN(ttlHours) ? 12 : ttlHours).toISOString();
|
||||
|
||||
await c.env.DB.prepare(
|
||||
'INSERT INTO admin_sessions (id, token_hash, expires_at, created_at) VALUES (?, ?, ?, ?)'
|
||||
)
|
||||
.bind(crypto.randomUUID(), tokenHash, expiresAt, nowIso())
|
||||
.run();
|
||||
|
||||
return c.json({
|
||||
token: rawToken,
|
||||
expires_at: expiresAt
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Invalidates the current admin session token.
|
||||
*/
|
||||
auth.post('/logout', adminAuth, async (c) => {
|
||||
const sessionId = c.get('adminSessionId');
|
||||
if (sessionId) {
|
||||
await c.env.DB.prepare('DELETE FROM admin_sessions WHERE id = ?')
|
||||
.bind(String(sessionId))
|
||||
.run();
|
||||
}
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
export default auth;
|
||||
@@ -0,0 +1,157 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { nowIso } from '../utils/time';
|
||||
import { jsonError } from '../utils/http';
|
||||
import { safeJsonParse } from '../utils/json';
|
||||
import { generateToken } from '../utils/crypto';
|
||||
import { normalizeBaseUrl } from '../utils/url';
|
||||
|
||||
const channels = new Hono<AppEnv>();
|
||||
|
||||
/**
|
||||
* Resolves a channel id from request payload.
|
||||
*
|
||||
* Args:
|
||||
* body: Request payload.
|
||||
*
|
||||
* Returns:
|
||||
* Channel id if provided.
|
||||
*/
|
||||
function resolveChannelId(body: any): string | null {
|
||||
const candidate = body?.id ?? body?.channel_id ?? body?.channelId;
|
||||
if (!candidate) {
|
||||
return null;
|
||||
}
|
||||
const normalized = String(candidate).trim();
|
||||
return normalized.length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Lists all channels.
|
||||
*/
|
||||
channels.get('/', async (c) => {
|
||||
const result = await c.env.DB.prepare('SELECT * FROM channels ORDER BY created_at DESC').all();
|
||||
return c.json({ channels: result.results ?? [] });
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new channel.
|
||||
*/
|
||||
channels.post('/', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body?.name || !body?.base_url || !body?.api_key) {
|
||||
return jsonError(c, 400, 'missing_fields', 'missing_fields');
|
||||
}
|
||||
|
||||
const requestedId = resolveChannelId(body);
|
||||
if (requestedId) {
|
||||
const exists = await c.env.DB.prepare('SELECT id FROM channels WHERE id = ?').bind(requestedId).first();
|
||||
if (exists) {
|
||||
return jsonError(c, 409, 'channel_id_exists', 'channel_id_exists');
|
||||
}
|
||||
}
|
||||
|
||||
const id = requestedId ?? generateToken('ch_');
|
||||
const now = nowIso();
|
||||
|
||||
await c.env.DB.prepare(
|
||||
'INSERT INTO channels (id, name, base_url, api_key, weight, status, rate_limit, models_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
id,
|
||||
body.name,
|
||||
normalizeBaseUrl(body.base_url),
|
||||
body.api_key,
|
||||
Number(body.weight ?? 1),
|
||||
body.status ?? 'active',
|
||||
body.rate_limit ?? 0,
|
||||
JSON.stringify(body.models ?? []),
|
||||
now,
|
||||
now
|
||||
)
|
||||
.run();
|
||||
|
||||
return c.json({ id });
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates a channel.
|
||||
*/
|
||||
channels.patch('/:id', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const id = c.req.param('id');
|
||||
if (!body) {
|
||||
return jsonError(c, 400, 'missing_body', 'missing_body');
|
||||
}
|
||||
|
||||
const current = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first();
|
||||
if (!current) {
|
||||
return jsonError(c, 404, 'channel_not_found', 'channel_not_found');
|
||||
}
|
||||
|
||||
const models = body.models ?? safeJsonParse(current.models_json, []);
|
||||
|
||||
await c.env.DB.prepare(
|
||||
'UPDATE channels SET name = ?, base_url = ?, api_key = ?, weight = ?, status = ?, rate_limit = ?, models_json = ?, updated_at = ? WHERE id = ?'
|
||||
)
|
||||
.bind(
|
||||
body.name ?? current.name,
|
||||
normalizeBaseUrl(body.base_url ?? current.base_url),
|
||||
body.api_key ?? current.api_key,
|
||||
Number(body.weight ?? current.weight ?? 1),
|
||||
body.status ?? current.status,
|
||||
body.rate_limit ?? current.rate_limit ?? 0,
|
||||
JSON.stringify(models),
|
||||
nowIso(),
|
||||
id
|
||||
)
|
||||
.run();
|
||||
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes a channel.
|
||||
*/
|
||||
channels.delete('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
await c.env.DB.prepare('DELETE FROM channels WHERE id = ?').bind(id).run();
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Tests channel connectivity and updates model list.
|
||||
*/
|
||||
channels.post('/:id/test', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first();
|
||||
if (!channel) {
|
||||
return jsonError(c, 404, 'channel_not_found', 'channel_not_found');
|
||||
}
|
||||
|
||||
const target = `${normalizeBaseUrl(String(channel.base_url))}/v1/models`;
|
||||
const response = await fetch(target, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${channel.api_key}`,
|
||||
'x-api-key': String(channel.api_key),
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
await c.env.DB.prepare('UPDATE channels SET status = ?, updated_at = ? WHERE id = ?')
|
||||
.bind('error', nowIso(), id)
|
||||
.run();
|
||||
return jsonError(c, 502, 'channel_unreachable', 'channel_unreachable');
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({ data: [] }));
|
||||
await c.env.DB.prepare('UPDATE channels SET status = ?, models_json = ?, updated_at = ? WHERE id = ?')
|
||||
.bind('active', JSON.stringify(data), nowIso(), id)
|
||||
.run();
|
||||
|
||||
return c.json({ ok: true, models: data?.data ?? [] });
|
||||
});
|
||||
|
||||
export default channels;
|
||||
@@ -0,0 +1,66 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
|
||||
const dashboard = new Hono<AppEnv>();
|
||||
|
||||
function buildDateFilters(query: Record<string, string>) {
|
||||
let sql = ' WHERE 1=1';
|
||||
const params: Array<string> = [];
|
||||
if (query.from) {
|
||||
sql += ' AND created_at >= ?';
|
||||
params.push(query.from);
|
||||
}
|
||||
if (query.to) {
|
||||
sql += ' AND created_at <= ?';
|
||||
params.push(query.to);
|
||||
}
|
||||
return { sql, params };
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns aggregated usage metrics.
|
||||
*/
|
||||
dashboard.get('/', async (c) => {
|
||||
const query = c.req.query();
|
||||
const { sql, params } = buildDateFilters(query);
|
||||
|
||||
const summary = await c.env.DB.prepare(
|
||||
`SELECT COUNT(*) as total_requests, COALESCE(SUM(total_tokens), 0) as total_tokens, COALESCE(AVG(latency_ms), 0) as avg_latency, COALESCE(SUM(CASE WHEN status != 'ok' THEN 1 ELSE 0 END), 0) as total_errors FROM usage_logs${sql}`
|
||||
)
|
||||
.bind(...params)
|
||||
.first();
|
||||
|
||||
const byDay = await c.env.DB.prepare(
|
||||
`SELECT substr(created_at, 1, 10) as day, COUNT(*) as requests, COALESCE(SUM(total_tokens), 0) as tokens FROM usage_logs${sql} GROUP BY day ORDER BY day DESC LIMIT 30`
|
||||
)
|
||||
.bind(...params)
|
||||
.all();
|
||||
|
||||
const byModel = await c.env.DB.prepare(
|
||||
`SELECT model, COUNT(*) as requests, COALESCE(SUM(total_tokens), 0) as tokens FROM usage_logs${sql} GROUP BY model ORDER BY requests DESC LIMIT 20`
|
||||
)
|
||||
.bind(...params)
|
||||
.all();
|
||||
|
||||
const byChannel = await c.env.DB.prepare(
|
||||
`SELECT channels.id as channel_id, channels.name as channel_name, COUNT(usage_logs.id) as requests, COALESCE(SUM(usage_logs.total_tokens), 0) as tokens FROM usage_logs LEFT JOIN channels ON channels.id = usage_logs.channel_id${sql} GROUP BY channels.id, channels.name ORDER BY requests DESC LIMIT 20`
|
||||
)
|
||||
.bind(...params)
|
||||
.all();
|
||||
|
||||
const byToken = await c.env.DB.prepare(
|
||||
`SELECT tokens.id as token_id, tokens.name as token_name, COUNT(usage_logs.id) as requests, COALESCE(SUM(usage_logs.total_tokens), 0) as tokens FROM usage_logs LEFT JOIN tokens ON tokens.id = usage_logs.token_id${sql} GROUP BY tokens.id, tokens.name ORDER BY requests DESC LIMIT 20`
|
||||
)
|
||||
.bind(...params)
|
||||
.all();
|
||||
|
||||
return c.json({
|
||||
summary: summary ?? { total_requests: 0, total_tokens: 0, avg_latency: 0, total_errors: 0 },
|
||||
byDay: byDay.results ?? [],
|
||||
byModel: byModel.results ?? [],
|
||||
byChannel: byChannel.results ?? [],
|
||||
byToken: byToken.results ?? []
|
||||
});
|
||||
});
|
||||
|
||||
export default dashboard;
|
||||
@@ -0,0 +1,26 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { extractModels } from '../services/channels';
|
||||
|
||||
const models = new Hono<AppEnv>();
|
||||
|
||||
/**
|
||||
* Returns aggregated models from all channels.
|
||||
*/
|
||||
models.get('/', async (c) => {
|
||||
const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?').bind('active').all();
|
||||
const entries = (result.results ?? []).flatMap((channel) => extractModels(channel as any));
|
||||
|
||||
const map = new Map<string, { id: string; channels: { id: string; name: string }[] }>();
|
||||
for (const entry of entries) {
|
||||
const existing = map.get(entry.id) ?? { id: entry.id, channels: [] };
|
||||
existing.channels.push({ id: entry.channelId, name: entry.channelName });
|
||||
map.set(entry.id, existing);
|
||||
}
|
||||
|
||||
return c.json({
|
||||
models: Array.from(map.values())
|
||||
});
|
||||
});
|
||||
|
||||
export default models;
|
||||
@@ -0,0 +1,466 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { newApiAuth } from '../middleware/newApiAuth';
|
||||
import { nowIso } from '../utils/time';
|
||||
import { generateToken } from '../utils/crypto';
|
||||
import { normalizeBaseUrl } from '../utils/url';
|
||||
import {
|
||||
extractModelIds,
|
||||
mergeMetadata,
|
||||
modelsToJson,
|
||||
normalizeChannelInput,
|
||||
normalizeModelsInput,
|
||||
normalizeBaseUrlInput,
|
||||
toNewApiChannel
|
||||
} from '../services/newapi';
|
||||
import { safeJsonParse } from '../utils/json';
|
||||
|
||||
const newapi = new Hono<AppEnv>({ strict: false });
|
||||
newapi.use('*', newApiAuth);
|
||||
|
||||
function success(c: any, data?: any, message = '') {
|
||||
return c.json(
|
||||
{
|
||||
success: true,
|
||||
message,
|
||||
...(data !== undefined ? { data } : {})
|
||||
},
|
||||
200
|
||||
);
|
||||
}
|
||||
|
||||
function failure(c: any, status: number, message: string) {
|
||||
return c.json(
|
||||
{
|
||||
success: false,
|
||||
message
|
||||
},
|
||||
status
|
||||
);
|
||||
}
|
||||
|
||||
function normalizePage(value: string | undefined | null, fallback: number): number {
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.floor(parsed);
|
||||
}
|
||||
|
||||
function normalizePageSize(value: string | undefined | null, fallback: number): number {
|
||||
const parsed = Number(value);
|
||||
if (Number.isNaN(parsed) || parsed <= 0) {
|
||||
return fallback;
|
||||
}
|
||||
return Math.min(200, Math.floor(parsed));
|
||||
}
|
||||
|
||||
function normalizeStatusFilter(value: string | undefined | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
if (normalized === 'all') {
|
||||
return null;
|
||||
}
|
||||
if (['enabled', 'enable', '1', 'active'].includes(normalized)) {
|
||||
return 'active';
|
||||
}
|
||||
if (['disabled', 'disable', '0', '2', 'inactive'].includes(normalized)) {
|
||||
return 'disabled';
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeBoolean(value: string | undefined | null): boolean {
|
||||
if (!value) {
|
||||
return false;
|
||||
}
|
||||
const normalized = value.trim().toLowerCase();
|
||||
return normalized === '1' || normalized === 'true' || normalized === 'yes';
|
||||
}
|
||||
|
||||
async function fetchModels(baseUrl: string, apiKey: string) {
|
||||
const target = `${normalizeBaseUrl(baseUrl)}/v1/models`;
|
||||
const start = Date.now();
|
||||
const response = await fetch(target, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
'x-api-key': apiKey,
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
const elapsed = Date.now() - start;
|
||||
if (!response.ok) {
|
||||
return { ok: false, elapsed, models: [] as string[] };
|
||||
}
|
||||
|
||||
const data = await response.json().catch(() => ({ data: [] }));
|
||||
const models = normalizeModelsInput(data?.data ?? data);
|
||||
return { ok: true, elapsed, models };
|
||||
}
|
||||
|
||||
async function updateChannelTestResult(
|
||||
c: any,
|
||||
id: string,
|
||||
ok: boolean,
|
||||
elapsed: number,
|
||||
models?: string[]
|
||||
) {
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
const status = ok ? 'active' : 'error';
|
||||
const modelsJson = models ? modelsToJson(models) : undefined;
|
||||
const sql = modelsJson
|
||||
? 'UPDATE channels SET status = ?, models_json = ?, test_time = ?, response_time_ms = ?, updated_at = ? WHERE id = ?'
|
||||
: 'UPDATE channels SET status = ?, test_time = ?, response_time_ms = ?, updated_at = ? WHERE id = ?';
|
||||
|
||||
const stmt = c.env.DB.prepare(sql);
|
||||
if (modelsJson) {
|
||||
await stmt.bind(status, modelsJson, now, elapsed, nowIso(), id).run();
|
||||
} else {
|
||||
await stmt.bind(status, now, elapsed, nowIso(), id).run();
|
||||
}
|
||||
}
|
||||
|
||||
async function loadChannels(c: any, where: string[], bindings: Array<string | number>) {
|
||||
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT * FROM channels ${whereSql} ORDER BY priority DESC, created_at DESC`
|
||||
)
|
||||
.bind(...bindings)
|
||||
.all();
|
||||
return rows.results ?? [];
|
||||
}
|
||||
|
||||
newapi.get('/', async (c) => {
|
||||
const page = normalizePage(c.req.query('p') ?? c.req.query('page'), 1);
|
||||
const pageSize = normalizePageSize(c.req.query('page_size') ?? c.req.query('limit'), 20);
|
||||
const idSort = normalizeBoolean(c.req.query('id_sort'));
|
||||
const statusFilter = normalizeStatusFilter(c.req.query('status'));
|
||||
const typeFilter = c.req.query('type');
|
||||
|
||||
const where: string[] = [];
|
||||
const bindings: Array<string | number> = [];
|
||||
|
||||
if (statusFilter) {
|
||||
where.push('status = ?');
|
||||
bindings.push(statusFilter);
|
||||
}
|
||||
if (typeFilter) {
|
||||
where.push('type = ?');
|
||||
bindings.push(Number(typeFilter));
|
||||
}
|
||||
|
||||
const whereSql = where.length > 0 ? `WHERE ${where.join(' AND ')}` : '';
|
||||
const orderSql = idSort ? 'ORDER BY id ASC' : 'ORDER BY priority DESC, created_at DESC';
|
||||
const offset = (page - 1) * pageSize;
|
||||
|
||||
const totalRow = await c.env.DB.prepare(
|
||||
`SELECT COUNT(*) as count FROM channels ${whereSql}`
|
||||
)
|
||||
.bind(...bindings)
|
||||
.first<{ count: number }>();
|
||||
|
||||
const rows = await c.env.DB.prepare(
|
||||
`SELECT * FROM channels ${whereSql} ${orderSql} LIMIT ? OFFSET ?`
|
||||
)
|
||||
.bind(...bindings, pageSize, offset)
|
||||
.all();
|
||||
|
||||
const counts = await c.env.DB.prepare(
|
||||
`SELECT type, COUNT(*) as count FROM channels ${whereSql} GROUP BY type`
|
||||
)
|
||||
.bind(...bindings)
|
||||
.all();
|
||||
|
||||
const typeCounts: Record<string, number> = { all: Number(totalRow?.count ?? 0) };
|
||||
for (const entry of counts.results ?? []) {
|
||||
typeCounts[String(entry.type)] = Number(entry.count ?? 0);
|
||||
}
|
||||
|
||||
const items = (rows.results ?? []).map((row) => {
|
||||
const channel = toNewApiChannel(row as any);
|
||||
delete (channel as any).key;
|
||||
return channel;
|
||||
});
|
||||
|
||||
return success(c, {
|
||||
items,
|
||||
total: Number(totalRow?.count ?? 0),
|
||||
page,
|
||||
page_size: pageSize,
|
||||
type_counts: typeCounts
|
||||
});
|
||||
});
|
||||
|
||||
newapi.get('/search', async (c) => {
|
||||
const page = normalizePage(c.req.query('p') ?? c.req.query('page'), 1);
|
||||
const pageSize = normalizePageSize(c.req.query('page_size') ?? c.req.query('limit'), 20);
|
||||
const statusFilter = normalizeStatusFilter(c.req.query('status'));
|
||||
const typeFilter = c.req.query('type');
|
||||
const keyword = c.req.query('keyword') ?? '';
|
||||
const group = c.req.query('group') ?? '';
|
||||
const model = c.req.query('model') ?? '';
|
||||
|
||||
const where: string[] = [];
|
||||
const bindings: Array<string | number> = [];
|
||||
|
||||
if (statusFilter) {
|
||||
where.push('status = ?');
|
||||
bindings.push(statusFilter);
|
||||
}
|
||||
if (typeFilter) {
|
||||
where.push('type = ?');
|
||||
bindings.push(Number(typeFilter));
|
||||
}
|
||||
|
||||
const rows = await loadChannels(c, where, bindings);
|
||||
const filtered = rows.filter((row) => {
|
||||
const channel = row as any;
|
||||
const models = extractModelIds(channel as any);
|
||||
if (keyword && !String(channel.name).includes(keyword) && !String(channel.id).includes(keyword)) {
|
||||
return false;
|
||||
}
|
||||
if (group && !String(channel.group_name ?? '').includes(group)) {
|
||||
return false;
|
||||
}
|
||||
if (model && !models.includes(model)) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
const total = filtered.length;
|
||||
const offset = (page - 1) * pageSize;
|
||||
const items = filtered.slice(offset, offset + pageSize).map((row) => {
|
||||
const channel = toNewApiChannel(row as any);
|
||||
delete (channel as any).key;
|
||||
return channel;
|
||||
});
|
||||
|
||||
return success(c, {
|
||||
items,
|
||||
total,
|
||||
page,
|
||||
page_size: pageSize
|
||||
});
|
||||
});
|
||||
|
||||
newapi.get('/models', async (c) => {
|
||||
const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?')
|
||||
.bind('active')
|
||||
.all();
|
||||
const models = new Set<string>();
|
||||
for (const row of result.results ?? []) {
|
||||
for (const id of extractModelIds(row as any)) {
|
||||
models.add(id);
|
||||
}
|
||||
}
|
||||
const data = Array.from(models).map((id) => ({ id, name: id }));
|
||||
return success(c, data);
|
||||
});
|
||||
|
||||
newapi.get('/models_enabled', async (c) => {
|
||||
const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?')
|
||||
.bind('active')
|
||||
.all();
|
||||
const models = new Set<string>();
|
||||
for (const row of result.results ?? []) {
|
||||
for (const id of extractModelIds(row as any)) {
|
||||
models.add(id);
|
||||
}
|
||||
}
|
||||
const data = Array.from(models).map((id) => ({ id, name: id }));
|
||||
return success(c, data);
|
||||
});
|
||||
|
||||
newapi.post('/', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body) {
|
||||
return failure(c, 400, '请求体为空');
|
||||
}
|
||||
|
||||
const mode = body.mode ?? 'single';
|
||||
if (mode !== 'single') {
|
||||
return failure(c, 400, '仅支持单渠道添加');
|
||||
}
|
||||
|
||||
const payload = body.channel ?? body;
|
||||
const parsed = normalizeChannelInput(payload);
|
||||
if (!parsed.name || !parsed.base_url || !parsed.api_key) {
|
||||
return failure(c, 400, '缺少必要参数');
|
||||
}
|
||||
|
||||
const existingId = parsed.id ?? generateToken('ch_');
|
||||
const exists = await c.env.DB.prepare('SELECT id FROM channels WHERE id = ?').bind(existingId).first();
|
||||
if (exists) {
|
||||
return failure(c, 409, '渠道已存在');
|
||||
}
|
||||
|
||||
const now = nowIso();
|
||||
const baseUrl = normalizeBaseUrlInput(parsed.base_url);
|
||||
await c.env.DB.prepare(
|
||||
'INSERT INTO channels (id, name, base_url, api_key, weight, status, rate_limit, models_json, type, group_name, priority, metadata_json, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
existingId,
|
||||
parsed.name,
|
||||
baseUrl ?? normalizeBaseUrl(String(parsed.base_url)),
|
||||
parsed.api_key,
|
||||
parsed.weight ?? 1,
|
||||
parsed.status ?? 'active',
|
||||
parsed.rate_limit ?? 0,
|
||||
parsed.models_json,
|
||||
parsed.type ?? 1,
|
||||
parsed.group_name,
|
||||
parsed.priority ?? 0,
|
||||
parsed.metadata_json,
|
||||
now,
|
||||
now
|
||||
)
|
||||
.run();
|
||||
|
||||
return success(c);
|
||||
});
|
||||
|
||||
newapi.put('/', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body) {
|
||||
return failure(c, 400, '请求体为空');
|
||||
}
|
||||
const payload = body.channel ?? body;
|
||||
const id = payload?.id ?? body?.id;
|
||||
if (!id) {
|
||||
return failure(c, 400, '缺少渠道ID');
|
||||
}
|
||||
|
||||
const current = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(String(id)).first();
|
||||
if (!current) {
|
||||
return failure(c, 404, '渠道不存在');
|
||||
}
|
||||
|
||||
const parsed = normalizeChannelInput(payload);
|
||||
const models = parsed.models.length > 0 ? parsed.models : extractModelIds(current as any);
|
||||
const mergedMetadata = mergeMetadata(current.metadata_json, parsed.metadata_json);
|
||||
const nextBaseUrl = normalizeBaseUrlInput(parsed.base_url ?? current.base_url) ?? String(current.base_url);
|
||||
|
||||
await c.env.DB.prepare(
|
||||
'UPDATE channels SET name = ?, base_url = ?, api_key = ?, weight = ?, status = ?, rate_limit = ?, models_json = ?, type = ?, group_name = ?, priority = ?, metadata_json = ?, updated_at = ? WHERE id = ?'
|
||||
)
|
||||
.bind(
|
||||
parsed.name ?? current.name,
|
||||
nextBaseUrl,
|
||||
parsed.api_key ?? current.api_key,
|
||||
parsed.weight ?? current.weight ?? 1,
|
||||
parsed.status ?? current.status,
|
||||
parsed.rate_limit ?? current.rate_limit ?? 0,
|
||||
modelsToJson(models),
|
||||
parsed.type ?? current.type ?? 1,
|
||||
parsed.group_name ?? current.group_name,
|
||||
parsed.priority ?? current.priority ?? 0,
|
||||
mergedMetadata,
|
||||
nowIso(),
|
||||
String(id)
|
||||
)
|
||||
.run();
|
||||
|
||||
return success(c);
|
||||
});
|
||||
|
||||
newapi.delete('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const existing = await c.env.DB.prepare('SELECT id FROM channels WHERE id = ?').bind(id).first();
|
||||
if (!existing) {
|
||||
return failure(c, 404, '渠道不存在');
|
||||
}
|
||||
await c.env.DB.prepare('DELETE FROM channels WHERE id = ?').bind(id).run();
|
||||
return success(c);
|
||||
});
|
||||
|
||||
newapi.get('/test/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first();
|
||||
if (!channel) {
|
||||
return failure(c, 404, '渠道不存在');
|
||||
}
|
||||
|
||||
const result = await fetchModels(String(channel.base_url), String(channel.api_key));
|
||||
if (!result.ok) {
|
||||
await updateChannelTestResult(c, id, false, result.elapsed);
|
||||
return failure(c, 502, '渠道测试失败');
|
||||
}
|
||||
|
||||
await updateChannelTestResult(c, id, true, result.elapsed);
|
||||
|
||||
return success(c, undefined, '测试成功');
|
||||
});
|
||||
|
||||
newapi.post('/test', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
const id = body?.id;
|
||||
if (!id) {
|
||||
return failure(c, 400, '缺少渠道ID');
|
||||
}
|
||||
const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(String(id)).first();
|
||||
if (!channel) {
|
||||
return failure(c, 404, '渠道不存在');
|
||||
}
|
||||
const result = await fetchModels(String(channel.base_url), String(channel.api_key));
|
||||
if (!result.ok) {
|
||||
await updateChannelTestResult(c, String(id), false, result.elapsed);
|
||||
return failure(c, 502, '渠道测试失败');
|
||||
}
|
||||
await updateChannelTestResult(c, String(id), true, result.elapsed);
|
||||
return success(c, undefined, '测试成功');
|
||||
});
|
||||
|
||||
newapi.get('/fetch_models/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first();
|
||||
if (!channel) {
|
||||
return failure(c, 404, '渠道不存在');
|
||||
}
|
||||
|
||||
const result = await fetchModels(String(channel.base_url), String(channel.api_key));
|
||||
if (!result.ok) {
|
||||
await updateChannelTestResult(c, id, false, result.elapsed);
|
||||
return failure(c, 502, '获取模型失败');
|
||||
}
|
||||
|
||||
await updateChannelTestResult(c, id, true, result.elapsed, result.models);
|
||||
|
||||
return success(c, result.models);
|
||||
});
|
||||
|
||||
newapi.post('/fetch_models', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body?.base_url || !body?.key) {
|
||||
return failure(c, 400, '缺少必要参数');
|
||||
}
|
||||
|
||||
const result = await fetchModels(String(body.base_url), String(body.key));
|
||||
if (!result.ok) {
|
||||
return failure(c, 502, '获取模型失败');
|
||||
}
|
||||
|
||||
return success(c, result.models);
|
||||
});
|
||||
|
||||
newapi.get('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const channel = await c.env.DB.prepare('SELECT * FROM channels WHERE id = ?').bind(id).first();
|
||||
if (!channel) {
|
||||
return failure(c, 404, '渠道不存在');
|
||||
}
|
||||
const metadata = safeJsonParse<Record<string, unknown>>(channel.metadata_json, {});
|
||||
const output = toNewApiChannel(channel as any);
|
||||
return success(c, {
|
||||
...output,
|
||||
model_mapping: metadata.model_mapping ?? '{}',
|
||||
channel_info: metadata.channel_info ?? { is_multi_key: false, multi_key_mode: 'random' }
|
||||
});
|
||||
});
|
||||
|
||||
export default newapi;
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { newApiAuth } from '../middleware/newApiAuth';
|
||||
|
||||
const groups = new Hono<AppEnv>({ strict: false });
|
||||
groups.use('*', newApiAuth);
|
||||
|
||||
function collectGroups(rows: Array<{ group_name?: string | null }>): string[] {
|
||||
const names = new Set<string>();
|
||||
for (const row of rows) {
|
||||
const raw = row.group_name;
|
||||
if (!raw) {
|
||||
continue;
|
||||
}
|
||||
raw
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0)
|
||||
.forEach((item) => names.add(item));
|
||||
}
|
||||
if (names.size === 0) {
|
||||
names.add('default');
|
||||
}
|
||||
return Array.from(names).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
|
||||
groups.get('/', async (c) => {
|
||||
const result = await c.env.DB.prepare('SELECT group_name FROM channels').all();
|
||||
const data = collectGroups((result.results ?? []) as Array<{ group_name?: string | null }>);
|
||||
return c.json({
|
||||
success: true,
|
||||
message: '',
|
||||
data
|
||||
});
|
||||
});
|
||||
|
||||
export default groups;
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { newApiAuth } from '../middleware/newApiAuth';
|
||||
import { extractModelIds } from '../services/newapi';
|
||||
|
||||
const users = new Hono<AppEnv>({ strict: false });
|
||||
users.use('*', newApiAuth);
|
||||
|
||||
users.get('/models', async (c) => {
|
||||
const result = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?')
|
||||
.bind('active')
|
||||
.all();
|
||||
const models = new Set<string>();
|
||||
for (const row of result.results ?? []) {
|
||||
for (const id of extractModelIds(row as any)) {
|
||||
models.add(id);
|
||||
}
|
||||
}
|
||||
const data = Array.from(models).map((id) => ({ id, name: id }));
|
||||
return c.json({
|
||||
success: true,
|
||||
message: '',
|
||||
data
|
||||
});
|
||||
});
|
||||
|
||||
export default users;
|
||||
@@ -0,0 +1,172 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { tokenAuth, type TokenRecord } from '../middleware/tokenAuth';
|
||||
import { createWeightedOrder, extractModels, type ChannelRecord } from '../services/channels';
|
||||
import { recordUsage } from '../services/usage';
|
||||
import { jsonError } from '../utils/http';
|
||||
import { safeJsonParse } from '../utils/json';
|
||||
import { normalizeBaseUrl } from '../utils/url';
|
||||
|
||||
const proxy = new Hono<AppEnv>();
|
||||
|
||||
function channelSupportsModel(channel: ChannelRecord, model?: string | null): boolean {
|
||||
if (!model) {
|
||||
return true;
|
||||
}
|
||||
const models = extractModels(channel);
|
||||
return models.some((entry) => entry.id === model);
|
||||
}
|
||||
|
||||
function filterAllowedChannels(channels: ChannelRecord[], tokenRecord: TokenRecord): ChannelRecord[] {
|
||||
const allowed = safeJsonParse<string[] | null>(tokenRecord.allowed_channels, null);
|
||||
if (!allowed || allowed.length === 0) {
|
||||
return channels;
|
||||
}
|
||||
const allowedSet = new Set(allowed);
|
||||
return channels.filter((channel) => allowedSet.has(channel.id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines whether a response status should be retried.
|
||||
*
|
||||
* Args:
|
||||
* status: HTTP response status code.
|
||||
*
|
||||
* Returns:
|
||||
* True if the status is retryable.
|
||||
*/
|
||||
function isRetryableStatus(status: number): boolean {
|
||||
return status === 408 || status === 429 || status >= 500;
|
||||
}
|
||||
|
||||
/**
|
||||
* Waits before the next retry round.
|
||||
*
|
||||
* Args:
|
||||
* ms: Delay in milliseconds.
|
||||
*
|
||||
* Returns:
|
||||
* Promise resolved after delay.
|
||||
*/
|
||||
async function sleep(ms: number): Promise<void> {
|
||||
if (ms <= 0) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* OpenAI-compatible proxy handler.
|
||||
*/
|
||||
proxy.all('/*', tokenAuth, async (c) => {
|
||||
const tokenRecord = c.get('tokenRecord') as TokenRecord;
|
||||
const requestText = await c.req.text();
|
||||
const parsedBody = requestText ? safeJsonParse<any>(requestText, null) : null;
|
||||
const model = parsedBody?.model ?? null;
|
||||
const isStream = parsedBody?.stream === true;
|
||||
|
||||
const channelResult = await c.env.DB.prepare('SELECT * FROM channels WHERE status = ?')
|
||||
.bind('active')
|
||||
.all();
|
||||
const activeChannels = (channelResult.results ?? []) as ChannelRecord[];
|
||||
const allowedChannels = filterAllowedChannels(activeChannels, tokenRecord);
|
||||
const modelChannels = allowedChannels.filter((channel) => channelSupportsModel(channel, model));
|
||||
const candidates = modelChannels.length > 0 ? modelChannels : allowedChannels;
|
||||
|
||||
if (candidates.length === 0) {
|
||||
return jsonError(c, 503, 'no_available_channels', 'no_available_channels');
|
||||
}
|
||||
|
||||
const ordered = createWeightedOrder(candidates);
|
||||
const targetPath = c.req.path;
|
||||
const retryRounds = Math.max(1, Number(c.env.PROXY_RETRY_ROUNDS ?? '1'));
|
||||
const retryDelayMs = Math.max(0, Number(c.env.PROXY_RETRY_DELAY_MS ?? '200'));
|
||||
let lastResponse: Response | null = null;
|
||||
let lastChannel: ChannelRecord | null = null;
|
||||
const start = Date.now();
|
||||
let selectedChannel: ChannelRecord | null = null;
|
||||
let usagePayload: any = null;
|
||||
|
||||
let round = 0;
|
||||
while (round < retryRounds && !selectedChannel) {
|
||||
let shouldRetry = false;
|
||||
for (const channel of ordered) {
|
||||
lastChannel = channel;
|
||||
const target = `${normalizeBaseUrl(channel.base_url)}${targetPath}${c.req.url.includes('?') ? `?${c.req.url.split('?')[1]}` : ''}`;
|
||||
const headers = new Headers(c.req.header());
|
||||
headers.set('Authorization', `Bearer ${channel.api_key}`);
|
||||
headers.set('x-api-key', String(channel.api_key));
|
||||
headers.delete('host');
|
||||
headers.delete('content-length');
|
||||
|
||||
try {
|
||||
const response = await fetch(target, {
|
||||
method: c.req.method,
|
||||
headers,
|
||||
body: requestText || undefined
|
||||
});
|
||||
|
||||
lastResponse = response;
|
||||
if (response.ok) {
|
||||
selectedChannel = channel;
|
||||
if (!isStream && response.headers.get('content-type')?.includes('application/json')) {
|
||||
const data = await response.clone().json().catch(() => null);
|
||||
usagePayload = data?.usage ?? null;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (isRetryableStatus(response.status)) {
|
||||
shouldRetry = true;
|
||||
}
|
||||
} catch {
|
||||
lastResponse = null;
|
||||
shouldRetry = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedChannel || !shouldRetry) {
|
||||
break;
|
||||
}
|
||||
|
||||
round += 1;
|
||||
if (round < retryRounds) {
|
||||
await sleep(retryDelayMs);
|
||||
}
|
||||
}
|
||||
|
||||
const latencyMs = Date.now() - start;
|
||||
|
||||
if (!lastResponse) {
|
||||
await recordUsage(c.env.DB, {
|
||||
tokenId: tokenRecord.id,
|
||||
model,
|
||||
requestPath: targetPath,
|
||||
totalTokens: 0,
|
||||
latencyMs,
|
||||
status: 'error'
|
||||
});
|
||||
return jsonError(c, 502, 'upstream_unavailable', 'upstream_unavailable');
|
||||
}
|
||||
|
||||
const channelForUsage = selectedChannel ?? lastChannel;
|
||||
if (channelForUsage) {
|
||||
const totalTokens = Number(usagePayload?.total_tokens ?? 0);
|
||||
await recordUsage(c.env.DB, {
|
||||
tokenId: tokenRecord.id,
|
||||
channelId: channelForUsage.id,
|
||||
model,
|
||||
requestPath: targetPath,
|
||||
totalTokens: Number.isNaN(totalTokens) ? 0 : totalTokens,
|
||||
promptTokens: Number(usagePayload?.prompt_tokens ?? 0),
|
||||
completionTokens: Number(usagePayload?.completion_tokens ?? 0),
|
||||
cost: 0,
|
||||
latencyMs,
|
||||
status: lastResponse.ok ? 'ok' : 'error'
|
||||
});
|
||||
}
|
||||
|
||||
return lastResponse;
|
||||
});
|
||||
|
||||
export default proxy;
|
||||
@@ -0,0 +1,33 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { getRetentionDays, setRetentionDays } from '../services/settings';
|
||||
import { jsonError } from '../utils/http';
|
||||
|
||||
const settings = new Hono<AppEnv>();
|
||||
|
||||
/**
|
||||
* Returns settings values.
|
||||
*/
|
||||
settings.get('/', async (c) => {
|
||||
const fallback = Number(c.env.LOG_RETENTION_DAYS ?? '30');
|
||||
const retention = await getRetentionDays(c.env.DB, Number.isNaN(fallback) ? 30 : fallback);
|
||||
return c.json({ log_retention_days: retention });
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates settings values.
|
||||
*/
|
||||
settings.put('/', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body?.log_retention_days) {
|
||||
return jsonError(c, 400, 'log_retention_days_required', 'log_retention_days_required');
|
||||
}
|
||||
const days = Number(body.log_retention_days);
|
||||
if (Number.isNaN(days) || days < 1) {
|
||||
return jsonError(c, 400, 'invalid_log_retention_days', 'invalid_log_retention_days');
|
||||
}
|
||||
await setRetentionDays(c.env.DB, days);
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
export default settings;
|
||||
@@ -0,0 +1,127 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { generateToken, sha256Hex } from '../utils/crypto';
|
||||
import { nowIso } from '../utils/time';
|
||||
import { jsonError } from '../utils/http';
|
||||
import { safeJsonParse } from '../utils/json';
|
||||
|
||||
const tokens = new Hono<AppEnv>();
|
||||
|
||||
/**
|
||||
* Lists API tokens.
|
||||
*/
|
||||
tokens.get('/', async (c) => {
|
||||
const result = await c.env.DB
|
||||
.prepare(
|
||||
'SELECT id, name, key_prefix, quota_total, quota_used, status, allowed_channels, created_at, updated_at FROM tokens ORDER BY created_at DESC'
|
||||
)
|
||||
.all();
|
||||
return c.json({ tokens: result.results ?? [] });
|
||||
});
|
||||
|
||||
/**
|
||||
* Creates a new API token.
|
||||
*/
|
||||
tokens.post('/', async (c) => {
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body?.name) {
|
||||
return jsonError(c, 400, 'name_required', 'name_required');
|
||||
}
|
||||
|
||||
const rawToken = generateToken('sk-');
|
||||
const tokenHash = await sha256Hex(rawToken);
|
||||
const id = crypto.randomUUID();
|
||||
const now = nowIso();
|
||||
const keyPrefix = rawToken.slice(0, 8);
|
||||
const quotaTotal = body.quota_total === null || body.quota_total === undefined
|
||||
? null
|
||||
: Number(body.quota_total);
|
||||
|
||||
await c.env.DB
|
||||
.prepare(
|
||||
'INSERT INTO tokens (id, name, key_hash, key_prefix, token_plain, quota_total, quota_used, status, allowed_channels, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
id,
|
||||
body.name,
|
||||
tokenHash,
|
||||
keyPrefix,
|
||||
rawToken,
|
||||
Number.isNaN(quotaTotal) ? null : quotaTotal,
|
||||
0,
|
||||
body.status ?? 'active',
|
||||
JSON.stringify(body.allowed_channels ?? null),
|
||||
now,
|
||||
now
|
||||
)
|
||||
.run();
|
||||
|
||||
return c.json({
|
||||
id,
|
||||
token: rawToken
|
||||
});
|
||||
});
|
||||
|
||||
/**
|
||||
* Updates an API token.
|
||||
*/
|
||||
tokens.patch('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const body = await c.req.json().catch(() => null);
|
||||
if (!body) {
|
||||
return jsonError(c, 400, 'missing_body', 'missing_body');
|
||||
}
|
||||
|
||||
const existing = await c.env.DB.prepare('SELECT * FROM tokens WHERE id = ?').bind(id).first();
|
||||
if (!existing) {
|
||||
return jsonError(c, 404, 'token_not_found', 'token_not_found');
|
||||
}
|
||||
|
||||
const existingAllowed = safeJsonParse(existing.allowed_channels, null);
|
||||
const quotaTotalUpdate = body.quota_total === null || body.quota_total === undefined
|
||||
? existing.quota_total
|
||||
: Number(body.quota_total);
|
||||
const quotaUsedUpdate = body.quota_used === null || body.quota_used === undefined
|
||||
? existing.quota_used
|
||||
: Number(body.quota_used);
|
||||
|
||||
await c.env.DB
|
||||
.prepare(
|
||||
'UPDATE tokens SET name = ?, quota_total = ?, quota_used = ?, status = ?, allowed_channels = ?, updated_at = ? WHERE id = ?'
|
||||
)
|
||||
.bind(
|
||||
body.name ?? existing.name,
|
||||
Number.isNaN(quotaTotalUpdate) ? existing.quota_total : quotaTotalUpdate,
|
||||
Number.isNaN(quotaUsedUpdate) ? existing.quota_used : quotaUsedUpdate,
|
||||
body.status ?? existing.status,
|
||||
JSON.stringify(body.allowed_channels ?? existingAllowed ?? null),
|
||||
nowIso(),
|
||||
id
|
||||
)
|
||||
.run();
|
||||
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
/**
|
||||
* Reveals a stored API token.
|
||||
*/
|
||||
tokens.get('/:id/reveal', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
const record = await c.env.DB.prepare('SELECT token_plain FROM tokens WHERE id = ?').bind(id).first();
|
||||
if (!record) {
|
||||
return jsonError(c, 404, 'token_not_found', 'token_not_found');
|
||||
}
|
||||
return c.json({ token: record.token_plain ?? null });
|
||||
});
|
||||
|
||||
/**
|
||||
* Deletes an API token.
|
||||
*/
|
||||
tokens.delete('/:id', async (c) => {
|
||||
const id = c.req.param('id');
|
||||
await c.env.DB.prepare('DELETE FROM tokens WHERE id = ?').bind(id).run();
|
||||
return c.json({ ok: true });
|
||||
});
|
||||
|
||||
export default tokens;
|
||||
@@ -0,0 +1,49 @@
|
||||
import { Hono } from 'hono';
|
||||
import type { AppEnv } from '../env';
|
||||
import { getRetentionDays } from '../services/settings';
|
||||
import { pruneUsageLogs } from '../services/usage';
|
||||
|
||||
const usage = new Hono<AppEnv>();
|
||||
|
||||
/**
|
||||
* Lists usage logs with filters.
|
||||
*/
|
||||
usage.get('/', async (c) => {
|
||||
const query = c.req.query();
|
||||
const params: Array<string | number> = [];
|
||||
let sql = 'SELECT usage_logs.*, channels.name as channel_name, tokens.name as token_name FROM usage_logs LEFT JOIN channels ON channels.id = usage_logs.channel_id LEFT JOIN tokens ON tokens.id = usage_logs.token_id WHERE 1=1';
|
||||
|
||||
if (query.from) {
|
||||
sql += ' AND usage_logs.created_at >= ?';
|
||||
params.push(query.from);
|
||||
}
|
||||
if (query.to) {
|
||||
sql += ' AND usage_logs.created_at <= ?';
|
||||
params.push(query.to);
|
||||
}
|
||||
if (query.model) {
|
||||
sql += ' AND usage_logs.model = ?';
|
||||
params.push(query.model);
|
||||
}
|
||||
if (query.channel_id) {
|
||||
sql += ' AND usage_logs.channel_id = ?';
|
||||
params.push(query.channel_id);
|
||||
}
|
||||
if (query.token_id) {
|
||||
sql += ' AND usage_logs.token_id = ?';
|
||||
params.push(query.token_id);
|
||||
}
|
||||
|
||||
const limit = Math.min(Number(query.limit ?? 50), 200);
|
||||
sql += ' ORDER BY usage_logs.created_at DESC LIMIT ?';
|
||||
params.push(Number.isNaN(limit) ? 50 : limit);
|
||||
|
||||
const fallback = Number(c.env.LOG_RETENTION_DAYS ?? '30');
|
||||
const retention = await getRetentionDays(c.env.DB, Number.isNaN(fallback) ? 30 : fallback);
|
||||
await pruneUsageLogs(c.env.DB, retention);
|
||||
|
||||
const result = await c.env.DB.prepare(sql).bind(...params).all();
|
||||
return c.json({ logs: result.results ?? [] });
|
||||
});
|
||||
|
||||
export default usage;
|
||||
@@ -0,0 +1,62 @@
|
||||
import { safeJsonParse } from '../utils/json';
|
||||
|
||||
export type ChannelRecord = {
|
||||
id: string;
|
||||
name: string;
|
||||
base_url: string;
|
||||
api_key: string;
|
||||
weight: number;
|
||||
status: string;
|
||||
rate_limit?: number | null;
|
||||
models_json?: string | null;
|
||||
};
|
||||
|
||||
export type ModelEntry = {
|
||||
id: string;
|
||||
label: string;
|
||||
channelId: string;
|
||||
channelName: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns channels in a weighted random order.
|
||||
*/
|
||||
export function createWeightedOrder(channels: ChannelRecord[]): ChannelRecord[] {
|
||||
const pool = channels.map((channel) => ({
|
||||
...channel,
|
||||
weight: Math.max(1, Number(channel.weight) || 1)
|
||||
}));
|
||||
const ordered: ChannelRecord[] = [];
|
||||
while (pool.length > 0) {
|
||||
const total = pool.reduce((sum, channel) => sum + channel.weight, 0);
|
||||
let roll = Math.random() * total;
|
||||
const index = pool.findIndex((channel) => {
|
||||
roll -= channel.weight;
|
||||
return roll <= 0;
|
||||
});
|
||||
const [selected] = pool.splice(index < 0 ? 0 : index, 1);
|
||||
ordered.push(selected);
|
||||
}
|
||||
return ordered;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts model entries from a channel record.
|
||||
*/
|
||||
export function extractModels(channel: ChannelRecord): ModelEntry[] {
|
||||
const raw = safeJsonParse<any>(channel.models_json, null);
|
||||
const models = Array.isArray(raw)
|
||||
? raw
|
||||
: Array.isArray(raw?.data)
|
||||
? raw.data
|
||||
: [];
|
||||
|
||||
return models
|
||||
.filter((model) => model?.id)
|
||||
.map((model) => ({
|
||||
id: String(model.id),
|
||||
label: String(model.id),
|
||||
channelId: channel.id,
|
||||
channelName: channel.name
|
||||
}));
|
||||
}
|
||||
@@ -0,0 +1,253 @@
|
||||
import { safeJsonParse } from '../utils/json';
|
||||
import { normalizeBaseUrl } from '../utils/url';
|
||||
|
||||
export type ChannelRow = {
|
||||
id: string;
|
||||
name: string;
|
||||
base_url: string;
|
||||
api_key: string;
|
||||
weight: number;
|
||||
status: string;
|
||||
rate_limit?: number | null;
|
||||
models_json?: string | null;
|
||||
type?: number | null;
|
||||
group_name?: string | null;
|
||||
priority?: number | null;
|
||||
metadata_json?: string | null;
|
||||
test_time?: number | string | null;
|
||||
response_time_ms?: number | null;
|
||||
created_at?: string | null;
|
||||
updated_at?: string | null;
|
||||
};
|
||||
|
||||
export type NewApiChannel = {
|
||||
id: string;
|
||||
name: string;
|
||||
type: number;
|
||||
status: number;
|
||||
priority: number;
|
||||
weight: number;
|
||||
models: string;
|
||||
group: string;
|
||||
response_time: number;
|
||||
test_time: number;
|
||||
base_url?: string;
|
||||
key?: string;
|
||||
created_time?: number;
|
||||
updated_time?: number;
|
||||
};
|
||||
|
||||
export type ParsedChannelInput = {
|
||||
id?: string;
|
||||
name?: string;
|
||||
type?: number;
|
||||
base_url?: string;
|
||||
api_key?: string;
|
||||
weight?: number;
|
||||
status?: string;
|
||||
rate_limit?: number;
|
||||
models: string[];
|
||||
models_json: string;
|
||||
group_name?: string | null;
|
||||
priority?: number;
|
||||
metadata_json?: string | null;
|
||||
};
|
||||
|
||||
const KNOWN_KEYS = new Set([
|
||||
'id',
|
||||
'name',
|
||||
'type',
|
||||
'key',
|
||||
'api_key',
|
||||
'base_url',
|
||||
'baseUrl',
|
||||
'weight',
|
||||
'status',
|
||||
'rate_limit',
|
||||
'rateLimit',
|
||||
'models',
|
||||
'model',
|
||||
'model_list',
|
||||
'models_list',
|
||||
'group',
|
||||
'group_name',
|
||||
'groups',
|
||||
'priority'
|
||||
]);
|
||||
|
||||
function toNumber(value: unknown, fallback: number | null = null): number | null {
|
||||
if (value === null || value === undefined) {
|
||||
return fallback;
|
||||
}
|
||||
const parsed = Number(value);
|
||||
return Number.isNaN(parsed) ? fallback : parsed;
|
||||
}
|
||||
|
||||
export function normalizeModelsInput(input: unknown): string[] {
|
||||
if (!input) {
|
||||
return [];
|
||||
}
|
||||
if (Array.isArray(input)) {
|
||||
return input.map((item) => String(item)).filter((item) => item.length > 0);
|
||||
}
|
||||
if (typeof input === 'string') {
|
||||
return input
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
if (typeof input === 'object') {
|
||||
const raw = input as { data?: unknown[] };
|
||||
if (Array.isArray(raw.data)) {
|
||||
return raw.data
|
||||
.map((item: any) => String(item?.id ?? item))
|
||||
.filter((item) => item.length > 0);
|
||||
}
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
export function modelsToJson(models: string[]): string {
|
||||
const normalized = models
|
||||
.map((model) => String(model).trim())
|
||||
.filter((model) => model.length > 0);
|
||||
return JSON.stringify(normalized.map((id) => ({ id })));
|
||||
}
|
||||
|
||||
export function extractModelIds(channel: ChannelRow): string[] {
|
||||
const raw = safeJsonParse<any>(channel.models_json, null);
|
||||
const models = Array.isArray(raw)
|
||||
? raw
|
||||
: Array.isArray(raw?.data)
|
||||
? raw.data
|
||||
: [];
|
||||
return models
|
||||
.map((model: any) => String(model?.id ?? model))
|
||||
.filter((model: string) => model.length > 0);
|
||||
}
|
||||
|
||||
export function toInternalStatus(status: unknown): string {
|
||||
if (status === undefined || status === null) {
|
||||
return 'active';
|
||||
}
|
||||
if (typeof status === 'string') {
|
||||
const normalized = status.trim().toLowerCase();
|
||||
if (['1', 'true', 'enabled', 'enable', 'active'].includes(normalized)) {
|
||||
return 'active';
|
||||
}
|
||||
if (['0', '2', 'false', 'disabled', 'disable', 'inactive'].includes(normalized)) {
|
||||
return 'disabled';
|
||||
}
|
||||
}
|
||||
if (typeof status === 'number') {
|
||||
return status === 1 ? 'active' : 'disabled';
|
||||
}
|
||||
if (typeof status === 'boolean') {
|
||||
return status ? 'active' : 'disabled';
|
||||
}
|
||||
return 'active';
|
||||
}
|
||||
|
||||
export function toNewApiStatus(status: string | null | undefined): number {
|
||||
return status === 'active' ? 1 : 2;
|
||||
}
|
||||
|
||||
export function normalizeChannelInput(body: any): ParsedChannelInput {
|
||||
const hasModels =
|
||||
body &&
|
||||
(Object.prototype.hasOwnProperty.call(body, 'models') ||
|
||||
Object.prototype.hasOwnProperty.call(body, 'model') ||
|
||||
Object.prototype.hasOwnProperty.call(body, 'model_list') ||
|
||||
Object.prototype.hasOwnProperty.call(body, 'models_list'));
|
||||
const models = hasModels
|
||||
? normalizeModelsInput(body?.models ?? body?.model ?? body?.model_list ?? body?.models_list)
|
||||
: [];
|
||||
const hasGroup =
|
||||
body &&
|
||||
(Object.prototype.hasOwnProperty.call(body, 'group') ||
|
||||
Object.prototype.hasOwnProperty.call(body, 'group_name') ||
|
||||
Object.prototype.hasOwnProperty.call(body, 'groups'));
|
||||
const groupInput = body?.group ?? body?.group_name ?? null;
|
||||
const groupsInput = Array.isArray(body?.groups) ? body.groups.map((item: any) => String(item)) : null;
|
||||
const groupName = !hasGroup
|
||||
? undefined
|
||||
: groupInput
|
||||
? String(groupInput)
|
||||
: groupsInput && groupsInput.length > 0
|
||||
? groupsInput.join(',')
|
||||
: null;
|
||||
|
||||
const metadata: Record<string, unknown> = {};
|
||||
if (body && typeof body === 'object') {
|
||||
for (const [key, value] of Object.entries(body)) {
|
||||
if (KNOWN_KEYS.has(key)) {
|
||||
continue;
|
||||
}
|
||||
metadata[key] = value;
|
||||
}
|
||||
}
|
||||
|
||||
const hasType = body && Object.prototype.hasOwnProperty.call(body, 'type');
|
||||
const hasWeight = body && Object.prototype.hasOwnProperty.call(body, 'weight');
|
||||
const hasStatus = body && Object.prototype.hasOwnProperty.call(body, 'status');
|
||||
const hasRateLimit =
|
||||
body &&
|
||||
(Object.prototype.hasOwnProperty.call(body, 'rate_limit') ||
|
||||
Object.prototype.hasOwnProperty.call(body, 'rateLimit'));
|
||||
const hasPriority = body && Object.prototype.hasOwnProperty.call(body, 'priority');
|
||||
|
||||
return {
|
||||
id: body?.id !== undefined && body?.id !== null ? String(body.id) : undefined,
|
||||
name: body?.name ? String(body.name) : undefined,
|
||||
type: hasType ? toNumber(body?.type, 1) ?? 1 : undefined,
|
||||
base_url: body?.base_url ?? body?.baseUrl,
|
||||
api_key: body?.key ?? body?.api_key,
|
||||
weight: hasWeight ? toNumber(body?.weight, 1) ?? 1 : undefined,
|
||||
status: hasStatus ? toInternalStatus(body?.status) : undefined,
|
||||
rate_limit: hasRateLimit ? toNumber(body?.rate_limit ?? body?.rateLimit, 0) ?? 0 : undefined,
|
||||
models,
|
||||
models_json: modelsToJson(models),
|
||||
group_name: groupName,
|
||||
priority: hasPriority ? toNumber(body?.priority, 0) ?? 0 : undefined,
|
||||
metadata_json: Object.keys(metadata).length > 0 ? JSON.stringify(metadata) : null
|
||||
};
|
||||
}
|
||||
|
||||
export function mergeMetadata(existing: string | null | undefined, incoming: string | null | undefined): string | null {
|
||||
const base = safeJsonParse<Record<string, unknown>>(existing, {});
|
||||
const updates = safeJsonParse<Record<string, unknown>>(incoming, {});
|
||||
const merged = { ...base, ...updates };
|
||||
return Object.keys(merged).length > 0 ? JSON.stringify(merged) : null;
|
||||
}
|
||||
|
||||
export function toNewApiChannel(channel: ChannelRow): NewApiChannel {
|
||||
const models = extractModelIds(channel);
|
||||
const metadata = safeJsonParse<Record<string, unknown>>(channel.metadata_json, {});
|
||||
const createdTime = channel.created_at ? Date.parse(channel.created_at) : NaN;
|
||||
const updatedTime = channel.updated_at ? Date.parse(channel.updated_at) : NaN;
|
||||
|
||||
return {
|
||||
id: channel.id,
|
||||
name: channel.name,
|
||||
type: Number(channel.type ?? 1),
|
||||
status: toNewApiStatus(channel.status),
|
||||
priority: Number(channel.priority ?? 0),
|
||||
weight: Number(channel.weight ?? 1),
|
||||
models: models.join(','),
|
||||
group: channel.group_name ?? '',
|
||||
response_time: Number(channel.response_time_ms ?? 0),
|
||||
test_time: toNumber(channel.test_time, 0) ?? 0,
|
||||
base_url: channel.base_url,
|
||||
key: channel.api_key,
|
||||
created_time: Number.isNaN(createdTime) ? undefined : Math.floor(createdTime / 1000),
|
||||
updated_time: Number.isNaN(updatedTime) ? undefined : Math.floor(updatedTime / 1000),
|
||||
...metadata
|
||||
};
|
||||
}
|
||||
|
||||
export function normalizeBaseUrlInput(value: string | undefined | null): string | null {
|
||||
if (!value) {
|
||||
return null;
|
||||
}
|
||||
return normalizeBaseUrl(String(value));
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
/**
|
||||
* Determines whether a token can consume more quota.
|
||||
*/
|
||||
export function canConsumeQuota(quotaTotal: number | null, quotaUsed: number, increment: number): boolean {
|
||||
if (quotaTotal === null || quotaTotal === undefined) {
|
||||
return true;
|
||||
}
|
||||
return quotaUsed + increment <= quotaTotal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes quota numbers to safe values.
|
||||
*/
|
||||
export function normalizeQuota(quotaTotal?: number | null, quotaUsed?: number | null) {
|
||||
return {
|
||||
quotaTotal: quotaTotal ?? null,
|
||||
quotaUsed: quotaUsed ?? 0
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
import type { D1Database } from '@cloudflare/workers-types';
|
||||
import { nowIso } from '../utils/time';
|
||||
|
||||
const RETENTION_KEY = 'log_retention_days';
|
||||
|
||||
/**
|
||||
* Returns the log retention days from settings or env fallback.
|
||||
*/
|
||||
export async function getRetentionDays(db: D1Database, fallback: number): Promise<number> {
|
||||
const setting = await db.prepare('SELECT value FROM settings WHERE key = ?').bind(RETENTION_KEY).first();
|
||||
if (setting?.value) {
|
||||
const parsed = Number(setting.value);
|
||||
if (!Number.isNaN(parsed) && parsed > 0) {
|
||||
return parsed;
|
||||
}
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the log retention days setting.
|
||||
*/
|
||||
export async function setRetentionDays(db: D1Database, days: number): Promise<void> {
|
||||
const value = Math.max(1, Math.floor(days)).toString();
|
||||
await db
|
||||
.prepare('INSERT INTO settings (key, value, updated_at) VALUES (?, ?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value, updated_at = excluded.updated_at')
|
||||
.bind(RETENTION_KEY, value, nowIso())
|
||||
.run();
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads generic settings as a key/value map.
|
||||
*/
|
||||
export async function listSettings(db: D1Database): Promise<Record<string, string>> {
|
||||
const result = await db.prepare('SELECT key, value FROM settings').all();
|
||||
const map: Record<string, string> = {};
|
||||
for (const row of result.results ?? []) {
|
||||
map[String(row.key)] = String(row.value);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
import type { D1Database } from '@cloudflare/workers-types';
|
||||
import { nowIso } from '../utils/time';
|
||||
|
||||
export type UsageInput = {
|
||||
tokenId?: string | null;
|
||||
channelId?: string | null;
|
||||
model?: string | null;
|
||||
requestPath?: string | null;
|
||||
totalTokens?: number | null;
|
||||
promptTokens?: number | null;
|
||||
completionTokens?: number | null;
|
||||
cost?: number | null;
|
||||
latencyMs?: number | null;
|
||||
status?: string | null;
|
||||
};
|
||||
|
||||
/**
|
||||
* Inserts a usage record and updates token quota.
|
||||
*/
|
||||
export async function recordUsage(db: D1Database, input: UsageInput): Promise<void> {
|
||||
const id = crypto.randomUUID();
|
||||
const createdAt = nowIso();
|
||||
await db
|
||||
.prepare(
|
||||
'INSERT INTO usage_logs (id, token_id, channel_id, model, request_path, total_tokens, prompt_tokens, completion_tokens, cost, latency_ms, status, created_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)'
|
||||
)
|
||||
.bind(
|
||||
id,
|
||||
input.tokenId ?? null,
|
||||
input.channelId ?? null,
|
||||
input.model ?? null,
|
||||
input.requestPath ?? null,
|
||||
input.totalTokens ?? 0,
|
||||
input.promptTokens ?? 0,
|
||||
input.completionTokens ?? 0,
|
||||
input.cost ?? 0,
|
||||
input.latencyMs ?? 0,
|
||||
input.status ?? 'ok',
|
||||
createdAt
|
||||
)
|
||||
.run();
|
||||
|
||||
if (input.tokenId && input.totalTokens) {
|
||||
await db
|
||||
.prepare('UPDATE tokens SET quota_used = quota_used + ?, updated_at = ? WHERE id = ?')
|
||||
.bind(input.totalTokens, createdAt, input.tokenId)
|
||||
.run();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes usage logs older than the retention window.
|
||||
*/
|
||||
export async function pruneUsageLogs(db: D1Database, retentionDays: number): Promise<void> {
|
||||
const cutoff = new Date();
|
||||
cutoff.setDate(cutoff.getDate() - retentionDays);
|
||||
await db.prepare('DELETE FROM usage_logs WHERE created_at < ?').bind(cutoff.toISOString()).run();
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
const encoder = new TextEncoder();
|
||||
|
||||
/**
|
||||
* Creates a SHA-256 hex digest from input text.
|
||||
*/
|
||||
export async function sha256Hex(input: string): Promise<string> {
|
||||
const data = encoder.encode(input);
|
||||
const hashBuffer = await crypto.subtle.digest('SHA-256', data);
|
||||
const hashArray = Array.from(new Uint8Array(hashBuffer));
|
||||
return hashArray.map((b) => b.toString(16).padStart(2, '0')).join('');
|
||||
}
|
||||
|
||||
/**
|
||||
* Generates a URL-safe random token with an optional prefix.
|
||||
*/
|
||||
export function generateToken(prefix = ''): string {
|
||||
const bytes = crypto.getRandomValues(new Uint8Array(24));
|
||||
const base = btoa(String.fromCharCode(...bytes))
|
||||
.replace(/\+/g, '-')
|
||||
.replace(/\//g, '_')
|
||||
.replace(/=+$/, '');
|
||||
return `${prefix}${base}`;
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { Context } from 'hono';
|
||||
|
||||
/**
|
||||
* Sends a JSON error response with a consistent shape.
|
||||
*/
|
||||
export function jsonError(c: Context, status: number, message: string, code?: string) {
|
||||
return c.json(
|
||||
{
|
||||
error: message,
|
||||
code
|
||||
},
|
||||
status
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Safely parses JSON or returns a fallback value.
|
||||
*/
|
||||
export function safeJsonParse<T>(value: string | null | undefined, fallback: T): T {
|
||||
if (!value) {
|
||||
return fallback;
|
||||
}
|
||||
try {
|
||||
return JSON.parse(value) as T;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import type { Context } from 'hono';
|
||||
|
||||
/**
|
||||
* Reads a bearer token from common headers.
|
||||
*/
|
||||
export function getBearerToken(c: Context): string | null {
|
||||
const auth = c.req.header('Authorization');
|
||||
if (auth && auth.toLowerCase().startsWith('bearer ')) {
|
||||
return auth.slice(7).trim();
|
||||
}
|
||||
return c.req.header('x-api-key') ?? c.req.header('x-admin-token') ?? null;
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
/**
|
||||
* Returns the current ISO timestamp.
|
||||
*/
|
||||
export function nowIso(): string {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hours to a date and returns a new Date.
|
||||
*/
|
||||
export function addHours(date: Date, hours: number): Date {
|
||||
const copy = new Date(date.getTime());
|
||||
copy.setHours(copy.getHours() + hours);
|
||||
return copy;
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
/**
|
||||
* Normalizes upstream base URLs and strips a trailing /v1 segment.
|
||||
*
|
||||
* Args:
|
||||
* baseUrl: Raw upstream base URL.
|
||||
*
|
||||
* Returns:
|
||||
* Normalized base URL without trailing slashes or /v1.
|
||||
*/
|
||||
export function normalizeBaseUrl(baseUrl: string): string {
|
||||
const trimmed = baseUrl.trim().replace(/\/+$/, '');
|
||||
return trimmed.replace(/\/v1$/i, '');
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"types": ["@cloudflare/workers-types"],
|
||||
"skipLibCheck": true
|
||||
},
|
||||
"include": ["src/**/*.ts"]
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
name = "new-api-lite"
|
||||
main = "src/index.ts"
|
||||
compatibility_date = "2026-02-14"
|
||||
|
||||
[vars]
|
||||
ADMIN_PASSWORD = "lsy"
|
||||
CORS_ORIGIN = "http://localhost:5173"
|
||||
SESSION_TTL_HOURS = "12"
|
||||
LOG_RETENTION_DAYS = "30"
|
||||
PROXY_RETRY_ROUNDS = "2"
|
||||
PROXY_RETRY_DELAY_MS = "200"
|
||||
NEW_API_ADMIN_TOKEN = "sk-newapi-admin"
|
||||
|
||||
[[d1_databases]]
|
||||
binding = "DB"
|
||||
database_name = "new_api_lite"
|
||||
database_id = "00000000-0000-0000-0000-000000000000"
|
||||
@@ -0,0 +1,652 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "new-api-lite",
|
||||
"dependencies": {
|
||||
"wrangler": "4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.3",
|
||||
},
|
||||
},
|
||||
"apps/admin": {
|
||||
"name": "new-api-lite-admin",
|
||||
"devDependencies": {
|
||||
"typescript": "^5.4.5",
|
||||
"vite": "^5.2.0",
|
||||
},
|
||||
},
|
||||
"apps/worker": {
|
||||
"name": "new-api-lite-worker",
|
||||
"dependencies": {
|
||||
"hono": "^4.2.7",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@cloudflare/workers-types": "^4.20240222.0",
|
||||
"typescript": "^5.4.5",
|
||||
"wrangler": "^3.48.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.4.2", "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.4.2.tgz", {}, "sha512-SIOD2DxrRRwQ+jgzlXCqoEFiKOFqaPjhnNTGKXSRLvp1HiOvapLaFG2kEr9dYQTYe8rKrd9uvDUzmAITeNyaHQ=="],
|
||||
|
||||
"@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.12.1", "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.12.1.tgz", { "peerDependencies": { "unenv": "2.0.0-rc.24", "workerd": "^1.20260115.0" }, "optionalPeers": ["workerd"] }, "sha512-tP/Wi+40aBJovonSNJSsS7aFJY0xjuckKplmzDs2Xat06BJ68B6iG7YDUWXJL8gNn0gqW7YC5WhlYhO3QbugQA=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20260212.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-kLxuYutk88Wlo7edp8mlkN68TgZZ9237SUnuX9kNaD5jcOdblUqiBctMRZeRcPsuoX/3g2t0vS4ga02NBEVRNg=="],
|
||||
|
||||
"@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20260212.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-fqoqQWMA1D0ZzDOD8sp0allREM2M8GHdpxMXQ8EdZpZ70z5bJbJ9Vr4qe35++FNIZJspsDHfTw3Xm/M4ELm/dQ=="],
|
||||
|
||||
"@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20260212.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-bCSQoZzDzV5MSh4ueWo1DgmOn4Hf3QBu4Yo3eQFXA2llYFIu/sZgRtkEehw1X2/SY5Sn6O0EMCqxJYRf82Wdeg=="],
|
||||
|
||||
"@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20260212.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-GPvp1iiKQodtbUDi6OmR5I0vD75lawB54tdYGtmypuHC7ZOI2WhBmhb3wCxgnQNOG1z7mhCQrzRCoqrKwYbVWQ=="],
|
||||
|
||||
"@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20260212.0", "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20260212.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-wHRI218Xn4ndgWJCUHH4Zx0YlU5q/o6OmcxXkcw95tJOsQn4lDrhppioPh4eScxJZALf2X+ODeZcyQTCq5exGw=="],
|
||||
|
||||
"@cloudflare/workers-types": ["@cloudflare/workers-types@4.20260214.0", "https://registry.npmmirror.com/@cloudflare/workers-types/-/workers-types-4.20260214.0.tgz", {}, "sha512-qb8rgbAdJR4BAPXolXhFL/wuGtecHLh1veOyZ1mK6QqWuCdI3vK1biKC0i3lzmzdLR/DZvsN3mNtpUE8zpWGEg=="],
|
||||
|
||||
"@cspotcode/source-map-support": ["@cspotcode/source-map-support@0.8.1", "https://registry.npmmirror.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", { "dependencies": { "@jridgewell/trace-mapping": "0.3.9" } }, "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw=="],
|
||||
|
||||
"@emnapi/runtime": ["@emnapi/runtime@1.8.1", "https://registry.npmmirror.com/@emnapi/runtime/-/runtime-1.8.1.tgz", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg=="],
|
||||
|
||||
"@esbuild-plugins/node-globals-polyfill": ["@esbuild-plugins/node-globals-polyfill@0.2.3", "https://registry.npmmirror.com/@esbuild-plugins/node-globals-polyfill/-/node-globals-polyfill-0.2.3.tgz", { "peerDependencies": { "esbuild": "*" } }, "sha512-r3MIryXDeXDOZh7ih1l/yE9ZLORCd5e8vWg02azWRGj5SPTuoh69A2AIyn0Z31V/kHBfZ4HgWJ+OK3GTTwLmnw=="],
|
||||
|
||||
"@esbuild-plugins/node-modules-polyfill": ["@esbuild-plugins/node-modules-polyfill@0.2.2", "https://registry.npmmirror.com/@esbuild-plugins/node-modules-polyfill/-/node-modules-polyfill-0.2.2.tgz", { "dependencies": { "escape-string-regexp": "^4.0.0", "rollup-plugin-node-polyfills": "^0.2.1" }, "peerDependencies": { "esbuild": "*" } }, "sha512-LXV7QsWJxRuMYvKbiznh+U1ilIop3g2TeKRzUxOG5X3YITc8JyyTa90BmLwqqv0YnX4v32CSlG+vsziZp9dMvA=="],
|
||||
|
||||
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg=="],
|
||||
|
||||
"@esbuild/android-arm": ["@esbuild/android-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz", { "os": "android", "cpu": "arm" }, "sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA=="],
|
||||
|
||||
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg=="],
|
||||
|
||||
"@esbuild/android-x64": ["@esbuild/android-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz", { "os": "android", "cpu": "x64" }, "sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ=="],
|
||||
|
||||
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg=="],
|
||||
|
||||
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg=="],
|
||||
|
||||
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w=="],
|
||||
|
||||
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA=="],
|
||||
|
||||
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz", { "os": "linux", "cpu": "arm" }, "sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw=="],
|
||||
|
||||
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg=="],
|
||||
|
||||
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg=="],
|
||||
|
||||
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA=="],
|
||||
|
||||
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw=="],
|
||||
|
||||
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA=="],
|
||||
|
||||
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz", { "os": "linux", "cpu": "none" }, "sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ=="],
|
||||
|
||||
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw=="],
|
||||
|
||||
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz", { "os": "linux", "cpu": "x64" }, "sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA=="],
|
||||
|
||||
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA=="],
|
||||
|
||||
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz", { "os": "none", "cpu": "x64" }, "sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA=="],
|
||||
|
||||
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz", { "os": "openbsd", "cpu": "arm64" }, "sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw=="],
|
||||
|
||||
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ=="],
|
||||
|
||||
"@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz", { "os": "none", "cpu": "arm64" }, "sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g=="],
|
||||
|
||||
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA=="],
|
||||
|
||||
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA=="],
|
||||
|
||||
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q=="],
|
||||
|
||||
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.3", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz", { "os": "win32", "cpu": "x64" }, "sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA=="],
|
||||
|
||||
"@fastify/busboy": ["@fastify/busboy@2.1.1", "https://registry.npmmirror.com/@fastify/busboy/-/busboy-2.1.1.tgz", {}, "sha512-vBZP4NlzfOlerQTnba4aqZoMhE/a9HY7HRqoOPaETQcSQuWEIyZMHGfVu6w9wGtGK5fED5qRs2DteVCjOH60sA=="],
|
||||
|
||||
"@img/colour": ["@img/colour@1.0.0", "https://registry.npmmirror.com/@img/colour/-/colour-1.0.0.tgz", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.0.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-UT4p+iz/2H4twwAoLCqfA9UH5pI6DggwKEGuaPy7nCVQ8ZsiY5PIcrRvD1DzuY3qYL07NtIQcWnBSY/heikIFQ=="],
|
||||
|
||||
"@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.0.4" }, "os": "darwin", "cpu": "x64" }, "sha512-fyHac4jIc1ANYGRDxtiqelIbdWkIuQaI84Mv45KvGRRxSAa7o7d1ZKAOBaYbnepLC1WqxfpimdeWfvqqSGwR2Q=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.0.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-XblONe153h0O2zuFfTAbQYAX2JhYmDHeWikp1LM9Hul9gVPjFY427k6dFEcOL72O01QxQsWi761svJ/ev9xEDg=="],
|
||||
|
||||
"@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.0.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-xnGR8YuZYfJGmWPvmlunFaWJsb9T/AO2ykoP3Fz/0X5XV2aoYBPkX6xqCQvUTKKiLddarLaxpzNe+b1hjeWHAQ=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.0.5", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.0.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g=="],
|
||||
|
||||
"@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-ppc64": ["@img/sharp-libvips-linux-ppc64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-riscv64": ["@img/sharp-libvips-linux-riscv64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", { "os": "linux", "cpu": "none" }, "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.0.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA=="],
|
||||
|
||||
"@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.0.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA=="],
|
||||
|
||||
"@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.0.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.0.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw=="],
|
||||
|
||||
"@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.0.5" }, "os": "linux", "cpu": "arm" }, "sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ=="],
|
||||
|
||||
"@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA=="],
|
||||
|
||||
"@img/sharp-linux-ppc64": ["@img/sharp-linux-ppc64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-ppc64": "1.2.4" }, "os": "linux", "cpu": "ppc64" }, "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA=="],
|
||||
|
||||
"@img/sharp-linux-riscv64": ["@img/sharp-linux-riscv64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-riscv64": "1.2.4" }, "os": "linux", "cpu": "none" }, "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw=="],
|
||||
|
||||
"@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.0.4" }, "os": "linux", "cpu": "s390x" }, "sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q=="],
|
||||
|
||||
"@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA=="],
|
||||
|
||||
"@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.0.4" }, "os": "linux", "cpu": "arm64" }, "sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g=="],
|
||||
|
||||
"@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.33.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.0.4" }, "os": "linux", "cpu": "x64" }, "sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw=="],
|
||||
|
||||
"@img/sharp-wasm32": ["@img/sharp-wasm32@0.33.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.33.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.2.0" }, "cpu": "none" }, "sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg=="],
|
||||
|
||||
"@img/sharp-win32-arm64": ["@img/sharp-win32-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g=="],
|
||||
|
||||
"@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.33.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-T36PblLaTwuVJ/zw/LaH0PdZkRz5rd3SmMHX8GSmR7vtNSP5Z6bQkExdSK7xGWyxLw4sUknBuugTelgw2faBbQ=="],
|
||||
|
||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.33.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.33.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-MpY/o8/8kj+EcnxwvrP4aTJSWw/aZ7JIGR4aBeZkZw5B7/Jn+tY9/VNwtcoGmdT7GfggGIU4kygOMSbYnOrAbg=="],
|
||||
|
||||
"@jest/schemas": ["@jest/schemas@29.6.3", "https://registry.npmmirror.com/@jest/schemas/-/schemas-29.6.3.tgz", { "dependencies": { "@sinclair/typebox": "^0.27.8" } }, "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA=="],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.9", "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", { "dependencies": { "@jridgewell/resolve-uri": "^3.0.3", "@jridgewell/sourcemap-codec": "^1.4.10" } }, "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ=="],
|
||||
|
||||
"@poppinss/colors": ["@poppinss/colors@4.1.6", "https://registry.npmmirror.com/@poppinss/colors/-/colors-4.1.6.tgz", { "dependencies": { "kleur": "^4.1.5" } }, "sha512-H9xkIdFswbS8n1d6vmRd8+c10t2Qe+rZITbbDHHkQixH5+2x1FDGmi/0K+WgWiqQFKPSlIYB7jlH6Kpfn6Fleg=="],
|
||||
|
||||
"@poppinss/dumper": ["@poppinss/dumper@0.6.5", "https://registry.npmmirror.com/@poppinss/dumper/-/dumper-0.6.5.tgz", { "dependencies": { "@poppinss/colors": "^4.1.5", "@sindresorhus/is": "^7.0.2", "supports-color": "^10.0.0" } }, "sha512-NBdYIb90J7LfOI32dOewKI1r7wnkiH6m920puQ3qHUeZkxNkQiFnXVWoE6YtFSv6QOiPPf7ys6i+HWWecDz7sw=="],
|
||||
|
||||
"@poppinss/exception": ["@poppinss/exception@1.2.3", "https://registry.npmmirror.com/@poppinss/exception/-/exception-1.2.3.tgz", {}, "sha512-dCED+QRChTVatE9ibtoaxc+WkdzOSjYTKi/+uacHWIsfodVfpsueo3+DKpgU5Px8qXjgmXkSvhXvSCz3fnP9lw=="],
|
||||
|
||||
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.57.1.tgz", { "os": "android", "cpu": "arm" }, "sha512-A6ehUVSiSaaliTxai040ZpZ2zTevHYbvu/lDoeAteHI8QnaosIzm4qwtezfRg1jOYaUmnzLX1AOD6Z+UJjtifg=="],
|
||||
|
||||
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.57.1.tgz", { "os": "android", "cpu": "arm64" }, "sha512-dQaAddCY9YgkFHZcFNS/606Exo8vcLHwArFZ7vxXq4rigo2bb494/xKMMwRRQW6ug7Js6yXmBZhSBRuBvCCQ3w=="],
|
||||
|
||||
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.57.1.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-crNPrwJOrRxagUYeMn/DZwqN88SDmwaJ8Cvi/TN1HnWBU7GwknckyosC2gd0IqYRsHDEnXf328o9/HC6OkPgOg=="],
|
||||
|
||||
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.57.1.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-Ji8g8ChVbKrhFtig5QBV7iMaJrGtpHelkB3lsaKzadFBe58gmjfGXAOfI5FV0lYMH8wiqsxKQ1C9B0YTRXVy4w=="],
|
||||
|
||||
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.57.1.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-R+/WwhsjmwodAcz65guCGFRkMb4gKWTcIeLy60JJQbXrJ97BOXHxnkPFrP+YwFlaS0m+uWJTstrUA9o+UchFug=="],
|
||||
|
||||
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.57.1.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-IEQTCHeiTOnAUC3IDQdzRAGj3jOAYNr9kBguI7MQAAZK3caezRrg0GxAb6Hchg4lxdZEI5Oq3iov/w/hnFWY9Q=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.57.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-F8sWbhZ7tyuEfsmOxwc2giKDQzN3+kuBLPwwZGyVkLlKGdV1nvnNwYD0fKQ8+XS6hp9nY7B+ZeK01EBUE7aHaw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.57.1.tgz", { "os": "linux", "cpu": "arm" }, "sha512-rGfNUfn0GIeXtBP1wL5MnzSj98+PZe/AXaGBCRmT0ts80lU5CATYGxXukeTX39XBKsxzFpEeK+Mrp9faXOlmrw=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-MMtej3YHWeg/0klK2Qodf3yrNzz6CGjo2UntLvk2RSPlhzgLvYEB3frRvbEF2wRKh1Z2fDIg9KRPe1fawv7C+g=="],
|
||||
|
||||
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.57.1.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-1a/qhaaOXhqXGpMFMET9VqwZakkljWHLmZOX48R0I/YLbhdxr1m4gtG1Hq7++VhVUmf+L3sTAf9op4JlhQ5u1Q=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-gnu": ["@rollup/rollup-linux-loong64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-QWO6RQTZ/cqYtJMtxhkRkidoNGXc7ERPbZN7dVW5SdURuLeVU7lwKMpo18XdcmpWYd0qsP1bwKPf7DNSUinhvA=="],
|
||||
|
||||
"@rollup/rollup-linux-loong64-musl": ["@rollup/rollup-linux-loong64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-xpObYIf+8gprgWaPP32xiN5RVTi/s5FCR+XMXSKmhfoJjrpRAjCuuqQXyxUa/eJTdAE6eJ+KDKaoEqjZQxh3Gw=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-gnu": ["@rollup/rollup-linux-ppc64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-4BrCgrpZo4hvzMDKRqEaW1zeecScDCR+2nZ86ATLhAoJ5FQ+lbHVD3ttKe74/c7tNT9c6F2viwB3ufwp01Oh2w=="],
|
||||
|
||||
"@rollup/rollup-linux-ppc64-musl": ["@rollup/rollup-linux-ppc64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.57.1.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-NOlUuzesGauESAyEYFSe3QTUguL+lvrN1HtwEEsU2rOwdUDeTMJdO5dUYl/2hKf9jWydJrO9OL/XSSf65R5+Xw=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-ptA88htVp0AwUUqhVghwDIKlvJMD/fmL/wrQj99PRHFRAG6Z5nbWoWG4o81Nt9FT+IuqUQi+L31ZKAFeJ5Is+A=="],
|
||||
|
||||
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.57.1.tgz", { "os": "linux", "cpu": "none" }, "sha512-S51t7aMMTNdmAMPpBg7OOsTdn4tySRQvklmL3RpDRyknk87+Sp3xaumlatU+ppQ+5raY7sSTcC2beGgvhENfuw=="],
|
||||
|
||||
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.57.1.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-Bl00OFnVFkL82FHbEqy3k5CUCKH6OEJL54KCyx2oqsmZnFTR8IoNqBF+mjQVcRCT5sB6yOvK8A37LNm/kPJiZg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.57.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-ABca4ceT4N+Tv/GtotnWAeXZUZuM/9AQyCyKYyKnpk4yoA7QIAuBt6Hkgpw8kActYlew2mvckXkvx0FfoInnLg=="],
|
||||
|
||||
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.57.1.tgz", { "os": "linux", "cpu": "x64" }, "sha512-HFps0JeGtuOR2convgRRkHCekD7j+gdAuXM+/i6kGzQtFhlCtQkpwtNzkNj6QhCDp7DRJ7+qC/1Vg2jt5iSOFw=="],
|
||||
|
||||
"@rollup/rollup-openbsd-x64": ["@rollup/rollup-openbsd-x64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.57.1.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-H+hXEv9gdVQuDTgnqD+SQffoWoc0Of59AStSzTEj/feWTBAnSfSD3+Dql1ZruJQxmykT/JVY0dE8Ka7z0DH1hw=="],
|
||||
|
||||
"@rollup/rollup-openharmony-arm64": ["@rollup/rollup-openharmony-arm64@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.57.1.tgz", { "os": "none", "cpu": "arm64" }, "sha512-4wYoDpNg6o/oPximyc/NG+mYUejZrCU2q+2w6YZqrAs2UcNUChIZXjtafAiiZSUc7On8v5NyNj34Kzj/Ltk6dQ=="],
|
||||
|
||||
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.57.1.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-O54mtsV/6LW3P8qdTcamQmuC990HDfR71lo44oZMZlXU4tzLrbvTii87Ni9opq60ds0YzuAlEr/GNwuNluZyMQ=="],
|
||||
|
||||
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.57.1.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-P3dLS+IerxCT/7D2q2FYcRdWRl22dNbrbBEtxdWhXrfIMPP9lQhb5h4Du04mdl5Woq05jVCDPCMF7Ub0NAjIew=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-gnu": ["@rollup/rollup-win32-x64-gnu@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.57.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-VMBH2eOOaKGtIJYleXsi2B8CPVADrh+TyNxJ4mWPnKfLB/DBUmzW+5m1xUrcwWoMfSLagIRpjUFeW5CO5hyciQ=="],
|
||||
|
||||
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.57.1", "https://registry.npmmirror.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.57.1.tgz", { "os": "win32", "cpu": "x64" }, "sha512-mxRFDdHIWRxg3UfIIAwCm6NzvxG0jDX/wBN6KsQFTvKFqqg9vTrWUE68qEjHt19A5wwx5X5aUi2zuZT7YR0jrA=="],
|
||||
|
||||
"@sinclair/typebox": ["@sinclair/typebox@0.27.10", "https://registry.npmmirror.com/@sinclair/typebox/-/typebox-0.27.10.tgz", {}, "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA=="],
|
||||
|
||||
"@sindresorhus/is": ["@sindresorhus/is@7.2.0", "https://registry.npmmirror.com/@sindresorhus/is/-/is-7.2.0.tgz", {}, "sha512-P1Cz1dWaFfR4IR+U13mqqiGsLFf1KbayybWwdd2vfctdV6hDpUkgCY0nKOLLTMSoRd/jJNjtbqzf13K8DCCXQw=="],
|
||||
|
||||
"@speed-highlight/core": ["@speed-highlight/core@1.2.14", "https://registry.npmmirror.com/@speed-highlight/core/-/core-1.2.14.tgz", {}, "sha512-G4ewlBNhUtlLvrJTb88d2mdy2KRijzs4UhnlrOSRT4bmjh/IqNElZa3zkrZ+TC47TwtlDWzVLFADljF1Ijp5hA=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.8", "https://registry.npmmirror.com/@types/estree/-/estree-1.0.8.tgz", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
|
||||
|
||||
"@types/node": ["@types/node@20.19.33", "https://registry.npmmirror.com/@types/node/-/node-20.19.33.tgz", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-Rs1bVAIdBs5gbTIKza/tgpMuG1k3U/UMJLWecIMxNdJFDMzcM5LOiLVRYh3PilWEYDIeUDv7bpiHPLPsbydGcw=="],
|
||||
|
||||
"@vitest/expect": ["@vitest/expect@1.6.1", "https://registry.npmmirror.com/@vitest/expect/-/expect-1.6.1.tgz", { "dependencies": { "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "chai": "^4.3.10" } }, "sha512-jXL+9+ZNIJKruofqXuuTClf44eSpcHlgj3CiuNihUF3Ioujtmc0zIa3UJOW5RjDK1YLBJZnWBlPuqhYycLioog=="],
|
||||
|
||||
"@vitest/runner": ["@vitest/runner@1.6.1", "https://registry.npmmirror.com/@vitest/runner/-/runner-1.6.1.tgz", { "dependencies": { "@vitest/utils": "1.6.1", "p-limit": "^5.0.0", "pathe": "^1.1.1" } }, "sha512-3nSnYXkVkf3mXFfE7vVyPmi3Sazhb/2cfZGGs0JRzFsPFvAMBEcrweV1V1GsrstdXeKCTXlJbvnQwGWgEIHmOA=="],
|
||||
|
||||
"@vitest/snapshot": ["@vitest/snapshot@1.6.1", "https://registry.npmmirror.com/@vitest/snapshot/-/snapshot-1.6.1.tgz", { "dependencies": { "magic-string": "^0.30.5", "pathe": "^1.1.1", "pretty-format": "^29.7.0" } }, "sha512-WvidQuWAzU2p95u8GAKlRMqMyN1yOJkGHnx3M1PL9Raf7AQ1kwLKg04ADlCa3+OXUZE7BceOhVZiuWAbzCKcUQ=="],
|
||||
|
||||
"@vitest/spy": ["@vitest/spy@1.6.1", "https://registry.npmmirror.com/@vitest/spy/-/spy-1.6.1.tgz", { "dependencies": { "tinyspy": "^2.2.0" } }, "sha512-MGcMmpGkZebsMZhbQKkAf9CX5zGvjkBTqf8Zx3ApYWXr3wG+QvEu2eXWfnIIWYSJExIp4V9FCKDEeygzkYrXMw=="],
|
||||
|
||||
"@vitest/utils": ["@vitest/utils@1.6.1", "https://registry.npmmirror.com/@vitest/utils/-/utils-1.6.1.tgz", { "dependencies": { "diff-sequences": "^29.6.3", "estree-walker": "^3.0.3", "loupe": "^2.3.7", "pretty-format": "^29.7.0" } }, "sha512-jOrrUvXM4Av9ZWiG1EajNto0u96kWAhJ1LmPmJhXXQx/32MecEKd10pOLYgS2BQx1TgkGhloPU1ArDW2vvaY6g=="],
|
||||
|
||||
"acorn": ["acorn@8.15.0", "https://registry.npmmirror.com/acorn/-/acorn-8.15.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg=="],
|
||||
|
||||
"acorn-walk": ["acorn-walk@8.3.4", "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.4.tgz", { "dependencies": { "acorn": "^8.11.0" } }, "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g=="],
|
||||
|
||||
"ansi-styles": ["ansi-styles@5.2.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-5.2.0.tgz", {}, "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA=="],
|
||||
|
||||
"as-table": ["as-table@1.0.55", "https://registry.npmmirror.com/as-table/-/as-table-1.0.55.tgz", { "dependencies": { "printable-characters": "^1.0.42" } }, "sha512-xvsWESUJn0JN421Xb9MQw6AsMHRCUknCe0Wjlxvjud80mU4E6hQf1A6NzQKcYNmYw62MfzEtXc+badstZP3JpQ=="],
|
||||
|
||||
"assertion-error": ["assertion-error@1.1.0", "https://registry.npmmirror.com/assertion-error/-/assertion-error-1.1.0.tgz", {}, "sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw=="],
|
||||
|
||||
"blake3-wasm": ["blake3-wasm@2.1.5", "https://registry.npmmirror.com/blake3-wasm/-/blake3-wasm-2.1.5.tgz", {}, "sha512-F1+K8EbfOZE49dtoPtmxUQrpXaBIl3ICvasLh+nJta0xkz+9kF/7uet9fLnwKqhDrmj6g+6K3Tw9yQPUg2ka5g=="],
|
||||
|
||||
"cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||
|
||||
"chai": ["chai@4.5.0", "https://registry.npmmirror.com/chai/-/chai-4.5.0.tgz", { "dependencies": { "assertion-error": "^1.1.0", "check-error": "^1.0.3", "deep-eql": "^4.1.3", "get-func-name": "^2.0.2", "loupe": "^2.3.6", "pathval": "^1.1.1", "type-detect": "^4.1.0" } }, "sha512-RITGBfijLkBddZvnn8jdqoTypxvqbOLYQkGGxXzeFjVHvudaPw0HNFD9x928/eUwYWd2dPCugVqspGALTZZQKw=="],
|
||||
|
||||
"check-error": ["check-error@1.0.3", "https://registry.npmmirror.com/check-error/-/check-error-1.0.3.tgz", { "dependencies": { "get-func-name": "^2.0.2" } }, "sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg=="],
|
||||
|
||||
"color": ["color@4.2.3", "https://registry.npmmirror.com/color/-/color-4.2.3.tgz", { "dependencies": { "color-convert": "^2.0.1", "color-string": "^1.9.0" } }, "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A=="],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||
|
||||
"color-string": ["color-string@1.9.1", "https://registry.npmmirror.com/color-string/-/color-string-1.9.1.tgz", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
|
||||
|
||||
"confbox": ["confbox@0.1.8", "https://registry.npmmirror.com/confbox/-/confbox-0.1.8.tgz", {}, "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w=="],
|
||||
|
||||
"cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
|
||||
|
||||
"data-uri-to-buffer": ["data-uri-to-buffer@2.0.2", "https://registry.npmmirror.com/data-uri-to-buffer/-/data-uri-to-buffer-2.0.2.tgz", {}, "sha512-ND9qDTLc6diwj+Xe5cdAgVTbLVdXbtxTJRXRhli8Mowuaan+0EJOtdqJ0QCHNSSPyoXGx9HX2/VMnKeC34AChA=="],
|
||||
|
||||
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"deep-eql": ["deep-eql@4.1.4", "https://registry.npmmirror.com/deep-eql/-/deep-eql-4.1.4.tgz", { "dependencies": { "type-detect": "^4.0.0" } }, "sha512-SUwdGfqdKOwxCPeVYjwSyRpJ7Z+fhpwIAtmCUdZIWZ/YP5R9WAsyuSgpLVDi9bjWoN2LXHNss/dk3urXtdQxGg=="],
|
||||
|
||||
"defu": ["defu@6.1.4", "https://registry.npmmirror.com/defu/-/defu-6.1.4.tgz", {}, "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.1.2.tgz", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"diff-sequences": ["diff-sequences@29.6.3", "https://registry.npmmirror.com/diff-sequences/-/diff-sequences-29.6.3.tgz", {}, "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q=="],
|
||||
|
||||
"error-stack-parser-es": ["error-stack-parser-es@1.0.5", "https://registry.npmmirror.com/error-stack-parser-es/-/error-stack-parser-es-1.0.5.tgz", {}, "sha512-5qucVt2XcuGMcEGgWI7i+yZpmpByQ8J1lHhcL7PwqCwu9FPP3VUXzT4ltHe5i2z9dePwEHcDVOAfSnHsOlCXRA=="],
|
||||
|
||||
"esbuild": ["esbuild@0.27.3", "https://registry.npmmirror.com/esbuild/-/esbuild-0.27.3.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.27.3", "@esbuild/android-arm": "0.27.3", "@esbuild/android-arm64": "0.27.3", "@esbuild/android-x64": "0.27.3", "@esbuild/darwin-arm64": "0.27.3", "@esbuild/darwin-x64": "0.27.3", "@esbuild/freebsd-arm64": "0.27.3", "@esbuild/freebsd-x64": "0.27.3", "@esbuild/linux-arm": "0.27.3", "@esbuild/linux-arm64": "0.27.3", "@esbuild/linux-ia32": "0.27.3", "@esbuild/linux-loong64": "0.27.3", "@esbuild/linux-mips64el": "0.27.3", "@esbuild/linux-ppc64": "0.27.3", "@esbuild/linux-riscv64": "0.27.3", "@esbuild/linux-s390x": "0.27.3", "@esbuild/linux-x64": "0.27.3", "@esbuild/netbsd-arm64": "0.27.3", "@esbuild/netbsd-x64": "0.27.3", "@esbuild/openbsd-arm64": "0.27.3", "@esbuild/openbsd-x64": "0.27.3", "@esbuild/openharmony-arm64": "0.27.3", "@esbuild/sunos-x64": "0.27.3", "@esbuild/win32-arm64": "0.27.3", "@esbuild/win32-ia32": "0.27.3", "@esbuild/win32-x64": "0.27.3" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg=="],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "https://registry.npmmirror.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", {}, "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA=="],
|
||||
|
||||
"estree-walker": ["estree-walker@3.0.3", "https://registry.npmmirror.com/estree-walker/-/estree-walker-3.0.3.tgz", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="],
|
||||
|
||||
"execa": ["execa@8.0.1", "https://registry.npmmirror.com/execa/-/execa-8.0.1.tgz", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="],
|
||||
|
||||
"exit-hook": ["exit-hook@2.2.1", "https://registry.npmmirror.com/exit-hook/-/exit-hook-2.2.1.tgz", {}, "sha512-eNTPlAD67BmP31LDINZ3U7HSF8l57TxOY2PmBJ1shpCvpnxBF93mWCE8YHBnXs8qiUZJc9WDcWIeC3a2HIAMfw=="],
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "https://registry.npmmirror.com/exsolve/-/exsolve-1.0.8.tgz", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"fsevents": ["fsevents@2.3.3", "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
|
||||
|
||||
"get-func-name": ["get-func-name@2.0.2", "https://registry.npmmirror.com/get-func-name/-/get-func-name-2.0.2.tgz", {}, "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ=="],
|
||||
|
||||
"get-source": ["get-source@2.0.12", "https://registry.npmmirror.com/get-source/-/get-source-2.0.12.tgz", { "dependencies": { "data-uri-to-buffer": "^2.0.0", "source-map": "^0.6.1" } }, "sha512-X5+4+iD+HoSeEED+uwrQ07BOQr0kEDFMVqqpBuI+RaZBpBpHCuXxo70bjar6f0b0u/DQJsJ7ssurpP0V60Az+w=="],
|
||||
|
||||
"get-stream": ["get-stream@8.0.1", "https://registry.npmmirror.com/get-stream/-/get-stream-8.0.1.tgz", {}, "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA=="],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "https://registry.npmmirror.com/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"hono": ["hono@4.11.9", "https://registry.npmmirror.com/hono/-/hono-4.11.9.tgz", {}, "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ=="],
|
||||
|
||||
"human-signals": ["human-signals@5.0.0", "https://registry.npmmirror.com/human-signals/-/human-signals-5.0.0.tgz", {}, "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ=="],
|
||||
|
||||
"is-arrayish": ["is-arrayish@0.3.4", "https://registry.npmmirror.com/is-arrayish/-/is-arrayish-0.3.4.tgz", {}, "sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA=="],
|
||||
|
||||
"is-stream": ["is-stream@3.0.0", "https://registry.npmmirror.com/is-stream/-/is-stream-3.0.0.tgz", {}, "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA=="],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@9.0.1", "https://registry.npmmirror.com/js-tokens/-/js-tokens-9.0.1.tgz", {}, "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ=="],
|
||||
|
||||
"kleur": ["kleur@4.1.5", "https://registry.npmmirror.com/kleur/-/kleur-4.1.5.tgz", {}, "sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ=="],
|
||||
|
||||
"local-pkg": ["local-pkg@0.5.1", "https://registry.npmmirror.com/local-pkg/-/local-pkg-0.5.1.tgz", { "dependencies": { "mlly": "^1.7.3", "pkg-types": "^1.2.1" } }, "sha512-9rrA30MRRP3gBD3HTGnC6cDFpaE1kVDWxWgqWJUN0RvDNAo+Nz/9GxB+nHOH0ifbVFy0hSA1V6vFDvnx54lTEQ=="],
|
||||
|
||||
"loupe": ["loupe@2.3.7", "https://registry.npmmirror.com/loupe/-/loupe-2.3.7.tgz", { "dependencies": { "get-func-name": "^2.0.1" } }, "sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "https://registry.npmmirror.com/magic-string/-/magic-string-0.30.21.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ=="],
|
||||
|
||||
"merge-stream": ["merge-stream@2.0.0", "https://registry.npmmirror.com/merge-stream/-/merge-stream-2.0.0.tgz", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="],
|
||||
|
||||
"mime": ["mime@3.0.0", "https://registry.npmmirror.com/mime/-/mime-3.0.0.tgz", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="],
|
||||
|
||||
"mimic-fn": ["mimic-fn@4.0.0", "https://registry.npmmirror.com/mimic-fn/-/mimic-fn-4.0.0.tgz", {}, "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw=="],
|
||||
|
||||
"miniflare": ["miniflare@4.20260212.0", "https://registry.npmmirror.com/miniflare/-/miniflare-4.20260212.0.tgz", { "dependencies": { "@cspotcode/source-map-support": "0.8.1", "sharp": "^0.34.5", "undici": "7.18.2", "workerd": "1.20260212.0", "ws": "8.18.0", "youch": "4.1.0-beta.10" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-Lgxq83EuR2q/0/DAVOSGXhXS1V7GDB04HVggoPsenQng8sqEDR3hO4FigIw5ZI2Sv2X7kIc30NCzGHJlCFIYWg=="],
|
||||
|
||||
"mlly": ["mlly@1.8.0", "https://registry.npmmirror.com/mlly/-/mlly-1.8.0.tgz", { "dependencies": { "acorn": "^8.15.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.1" } }, "sha512-l8D9ODSRWLe2KHJSifWGwBqpTZXIXTeo8mlKjY+E2HAakaTeNpqAyBZ8GSqLzHgw4XmHmC8whvpjJNMbFZN7/g=="],
|
||||
|
||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||
|
||||
"mustache": ["mustache@4.2.0", "https://registry.npmmirror.com/mustache/-/mustache-4.2.0.tgz", { "bin": { "mustache": "bin/mustache" } }, "sha512-71ippSywq5Yb7/tVYyGbkBggbU8H3u5Rz56fH60jGFgr8uHwxs+aSKeqmluIVzM0m0kB7xQjKS6qPfd0b2ZoqQ=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.11", "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
|
||||
|
||||
"new-api-lite-admin": ["new-api-lite-admin@workspace:apps/admin"],
|
||||
|
||||
"new-api-lite-worker": ["new-api-lite-worker@workspace:apps/worker"],
|
||||
|
||||
"npm-run-path": ["npm-run-path@5.3.0", "https://registry.npmmirror.com/npm-run-path/-/npm-run-path-5.3.0.tgz", { "dependencies": { "path-key": "^4.0.0" } }, "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ=="],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "https://registry.npmmirror.com/ohash/-/ohash-2.0.11.tgz", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"onetime": ["onetime@6.0.0", "https://registry.npmmirror.com/onetime/-/onetime-6.0.0.tgz", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="],
|
||||
|
||||
"p-limit": ["p-limit@5.0.0", "https://registry.npmmirror.com/p-limit/-/p-limit-5.0.0.tgz", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-/Eaoq+QyLSiXQ4lyYV23f14mZRQcXnxfHrN0vCai+ak9G0pp9iEQukIIZq5NccEvwRB8PUnZT0KsOoDCINS1qQ=="],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "https://registry.npmmirror.com/path-key/-/path-key-3.1.1.tgz", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
|
||||
|
||||
"path-to-regexp": ["path-to-regexp@6.3.0", "https://registry.npmmirror.com/path-to-regexp/-/path-to-regexp-6.3.0.tgz", {}, "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ=="],
|
||||
|
||||
"pathe": ["pathe@1.1.2", "https://registry.npmmirror.com/pathe/-/pathe-1.1.2.tgz", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="],
|
||||
|
||||
"pathval": ["pathval@1.1.1", "https://registry.npmmirror.com/pathval/-/pathval-1.1.1.tgz", {}, "sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
|
||||
|
||||
"pkg-types": ["pkg-types@1.3.1", "https://registry.npmmirror.com/pkg-types/-/pkg-types-1.3.1.tgz", { "dependencies": { "confbox": "^0.1.8", "mlly": "^1.7.4", "pathe": "^2.0.1" } }, "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ=="],
|
||||
|
||||
"postcss": ["postcss@8.5.6", "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
|
||||
|
||||
"pretty-format": ["pretty-format@29.7.0", "https://registry.npmmirror.com/pretty-format/-/pretty-format-29.7.0.tgz", { "dependencies": { "@jest/schemas": "^29.6.3", "ansi-styles": "^5.0.0", "react-is": "^18.0.0" } }, "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ=="],
|
||||
|
||||
"printable-characters": ["printable-characters@1.0.42", "https://registry.npmmirror.com/printable-characters/-/printable-characters-1.0.42.tgz", {}, "sha512-dKp+C4iXWK4vVYZmYSd0KBH5F/h1HoZRsbJ82AVKRO3PEo8L4lBS/vLwhVtpwwuYcoIsVY+1JYKR268yn480uQ=="],
|
||||
|
||||
"react-is": ["react-is@18.3.1", "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz", {}, "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg=="],
|
||||
|
||||
"rollup": ["rollup@4.57.1", "https://registry.npmmirror.com/rollup/-/rollup-4.57.1.tgz", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.57.1", "@rollup/rollup-android-arm64": "4.57.1", "@rollup/rollup-darwin-arm64": "4.57.1", "@rollup/rollup-darwin-x64": "4.57.1", "@rollup/rollup-freebsd-arm64": "4.57.1", "@rollup/rollup-freebsd-x64": "4.57.1", "@rollup/rollup-linux-arm-gnueabihf": "4.57.1", "@rollup/rollup-linux-arm-musleabihf": "4.57.1", "@rollup/rollup-linux-arm64-gnu": "4.57.1", "@rollup/rollup-linux-arm64-musl": "4.57.1", "@rollup/rollup-linux-loong64-gnu": "4.57.1", "@rollup/rollup-linux-loong64-musl": "4.57.1", "@rollup/rollup-linux-ppc64-gnu": "4.57.1", "@rollup/rollup-linux-ppc64-musl": "4.57.1", "@rollup/rollup-linux-riscv64-gnu": "4.57.1", "@rollup/rollup-linux-riscv64-musl": "4.57.1", "@rollup/rollup-linux-s390x-gnu": "4.57.1", "@rollup/rollup-linux-x64-gnu": "4.57.1", "@rollup/rollup-linux-x64-musl": "4.57.1", "@rollup/rollup-openbsd-x64": "4.57.1", "@rollup/rollup-openharmony-arm64": "4.57.1", "@rollup/rollup-win32-arm64-msvc": "4.57.1", "@rollup/rollup-win32-ia32-msvc": "4.57.1", "@rollup/rollup-win32-x64-gnu": "4.57.1", "@rollup/rollup-win32-x64-msvc": "4.57.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-oQL6lgK3e2QZeQ7gcgIkS2YZPg5slw37hYufJ3edKlfQSGGm8ICoxswK15ntSzF/a8+h7ekRy7k7oWc3BQ7y8A=="],
|
||||
|
||||
"rollup-plugin-inject": ["rollup-plugin-inject@3.0.2", "https://registry.npmmirror.com/rollup-plugin-inject/-/rollup-plugin-inject-3.0.2.tgz", { "dependencies": { "estree-walker": "^0.6.1", "magic-string": "^0.25.3", "rollup-pluginutils": "^2.8.1" } }, "sha512-ptg9PQwzs3orn4jkgXJ74bfs5vYz1NCZlSQMBUA0wKcGp5i5pA1AO3fOUEte8enhGUC+iapTCzEWw2jEFFUO/w=="],
|
||||
|
||||
"rollup-plugin-node-polyfills": ["rollup-plugin-node-polyfills@0.2.1", "https://registry.npmmirror.com/rollup-plugin-node-polyfills/-/rollup-plugin-node-polyfills-0.2.1.tgz", { "dependencies": { "rollup-plugin-inject": "^3.0.0" } }, "sha512-4kCrKPTJ6sK4/gLL/U5QzVT8cxJcofO0OU74tnB19F40cmuAKSzH5/siithxlofFEjwvw1YAhPmbvGNA6jEroA=="],
|
||||
|
||||
"rollup-pluginutils": ["rollup-pluginutils@2.8.2", "https://registry.npmmirror.com/rollup-pluginutils/-/rollup-pluginutils-2.8.2.tgz", { "dependencies": { "estree-walker": "^0.6.1" } }, "sha512-EEp9NhnUkwY8aif6bxgovPHMoMoNr2FulJziTndpt5H9RdwC47GSGuII9XxpSdzVGM0GWrNPHV6ie1LTNJPaLQ=="],
|
||||
|
||||
"semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||
|
||||
"sharp": ["sharp@0.33.5", "https://registry.npmmirror.com/sharp/-/sharp-0.33.5.tgz", { "dependencies": { "color": "^4.2.3", "detect-libc": "^2.0.3", "semver": "^7.6.3" }, "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" } }, "sha512-haPVm1EkS9pgvHrQ/F3Xy+hgcuMV0Wm9vfIBSiwZ05k+xgb0PkBQpGsAA/oWdDobNaZTH5ppvHtzCFbnSEwHVw=="],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "https://registry.npmmirror.com/shebang-regex/-/shebang-regex-3.0.0.tgz", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
|
||||
|
||||
"siginfo": ["siginfo@2.0.0", "https://registry.npmmirror.com/siginfo/-/siginfo-2.0.0.tgz", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"simple-swizzle": ["simple-swizzle@0.2.4", "https://registry.npmmirror.com/simple-swizzle/-/simple-swizzle-0.2.4.tgz", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw=="],
|
||||
|
||||
"source-map": ["source-map@0.6.1", "https://registry.npmmirror.com/source-map/-/source-map-0.6.1.tgz", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
|
||||
|
||||
"sourcemap-codec": ["sourcemap-codec@1.4.8", "https://registry.npmmirror.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz", {}, "sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA=="],
|
||||
|
||||
"stackback": ["stackback@0.0.2", "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||
|
||||
"stacktracey": ["stacktracey@2.1.8", "https://registry.npmmirror.com/stacktracey/-/stacktracey-2.1.8.tgz", { "dependencies": { "as-table": "^1.0.36", "get-source": "^2.0.12" } }, "sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw=="],
|
||||
|
||||
"std-env": ["std-env@3.10.0", "https://registry.npmmirror.com/std-env/-/std-env-3.10.0.tgz", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
|
||||
"stoppable": ["stoppable@1.1.0", "https://registry.npmmirror.com/stoppable/-/stoppable-1.1.0.tgz", {}, "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw=="],
|
||||
|
||||
"strip-final-newline": ["strip-final-newline@3.0.0", "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-3.0.0.tgz", {}, "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw=="],
|
||||
|
||||
"strip-literal": ["strip-literal@2.1.1", "https://registry.npmmirror.com/strip-literal/-/strip-literal-2.1.1.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-631UJ6O00eNGfMiWG78ck80dfBab8X6IVFB51jZK5Icd7XAs60Z5y7QdSd/wGIklnWvRbUNloVzhOKKmutxQ6Q=="],
|
||||
|
||||
"supports-color": ["supports-color@10.2.2", "https://registry.npmmirror.com/supports-color/-/supports-color-10.2.2.tgz", {}, "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g=="],
|
||||
|
||||
"tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||
|
||||
"tinypool": ["tinypool@0.8.4", "https://registry.npmmirror.com/tinypool/-/tinypool-0.8.4.tgz", {}, "sha512-i11VH5gS6IFeLY3gMBQ00/MmLncVP7JLXOw1vlgkytLmJK7QnEr7NXf0LBdxfmNPAeyetukOk0bOYrJrFGjYJQ=="],
|
||||
|
||||
"tinyspy": ["tinyspy@2.2.1", "https://registry.npmmirror.com/tinyspy/-/tinyspy-2.2.1.tgz", {}, "sha512-KYad6Vy5VDWV4GH3fjpseMQ/XU2BhIYP7Vzd0LG44qRWm/Yt2WCOTicFdvmgo6gWaqooMQCawTtILVQJupKu7A=="],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||
|
||||
"type-detect": ["type-detect@4.1.0", "https://registry.npmmirror.com/type-detect/-/type-detect-4.1.0.tgz", {}, "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
|
||||
|
||||
"ufo": ["ufo@1.6.3", "https://registry.npmmirror.com/ufo/-/ufo-1.6.3.tgz", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="],
|
||||
|
||||
"undici": ["undici@7.18.2", "https://registry.npmmirror.com/undici/-/undici-7.18.2.tgz", {}, "sha512-y+8YjDFzWdQlSE9N5nzKMT3g4a5UBX1HKowfdXh0uvAnTaqqwqB92Jt4UXBAeKekDs5IaDKyJFR4X1gYVCgXcw=="],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "https://registry.npmmirror.com/undici-types/-/undici-types-6.21.0.tgz", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
|
||||
|
||||
"unenv": ["unenv@2.0.0-rc.24", "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.24.tgz", { "dependencies": { "pathe": "^2.0.3" } }, "sha512-i7qRCmY42zmCwnYlh9H2SvLEypEFGye5iRmEMKjcGi7zk9UquigRjFtTLz0TYqr0ZGLZhaMHl/foy1bZR+Cwlw=="],
|
||||
|
||||
"vite": ["vite@5.4.21", "https://registry.npmmirror.com/vite/-/vite-5.4.21.tgz", { "dependencies": { "esbuild": "^0.21.3", "postcss": "^8.4.43", "rollup": "^4.20.0" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || >=20.0.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.4.0" }, "optionalPeers": ["@types/node", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser"], "bin": { "vite": "bin/vite.js" } }, "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw=="],
|
||||
|
||||
"vite-node": ["vite-node@1.6.1", "https://registry.npmmirror.com/vite-node/-/vite-node-1.6.1.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.3.4", "pathe": "^1.1.1", "picocolors": "^1.0.0", "vite": "^5.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-YAXkfvGtuTzwWbDSACdJSg4A4DZiAqckWe90Zapc/sEX3XvHcw1NdurM/6od8J207tSDqNbSsgdCacBgvJKFuA=="],
|
||||
|
||||
"vitest": ["vitest@1.6.1", "https://registry.npmmirror.com/vitest/-/vitest-1.6.1.tgz", { "dependencies": { "@vitest/expect": "1.6.1", "@vitest/runner": "1.6.1", "@vitest/snapshot": "1.6.1", "@vitest/spy": "1.6.1", "@vitest/utils": "1.6.1", "acorn-walk": "^8.3.2", "chai": "^4.3.10", "debug": "^4.3.4", "execa": "^8.0.1", "local-pkg": "^0.5.0", "magic-string": "^0.30.5", "pathe": "^1.1.1", "picocolors": "^1.0.0", "std-env": "^3.5.0", "strip-literal": "^2.0.0", "tinybench": "^2.5.1", "tinypool": "^0.8.3", "vite": "^5.0.0", "vite-node": "1.6.1", "why-is-node-running": "^2.2.2" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/node": "^18.0.0 || >=20.0.0", "@vitest/browser": "1.6.1", "@vitest/ui": "1.6.1", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-Ljb1cnSJSivGN0LqXd/zmDbWEM0RNNg2t1QW/XUhYl/qPqyu7CsqeWtqQXHVaJsecLPuDoak2oJcZN2QoRIOag=="],
|
||||
|
||||
"which": ["which@2.0.2", "https://registry.npmmirror.com/which/-/which-2.0.2.tgz", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
|
||||
|
||||
"why-is-node-running": ["why-is-node-running@2.3.0", "https://registry.npmmirror.com/why-is-node-running/-/why-is-node-running-2.3.0.tgz", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="],
|
||||
|
||||
"workerd": ["workerd@1.20260212.0", "https://registry.npmmirror.com/workerd/-/workerd-1.20260212.0.tgz", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20260212.0", "@cloudflare/workerd-darwin-arm64": "1.20260212.0", "@cloudflare/workerd-linux-64": "1.20260212.0", "@cloudflare/workerd-linux-arm64": "1.20260212.0", "@cloudflare/workerd-windows-64": "1.20260212.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-4B9BoZUzKSRv3pVZGEPh7OX+Q817hpUqAUtz5O0TxJVqo4OsYJAUA/sY177Q5ha/twjT9KaJt2DtQzE+oyCOzw=="],
|
||||
|
||||
"wrangler": ["wrangler@4.65.0", "https://registry.npmmirror.com/wrangler/-/wrangler-4.65.0.tgz", { "dependencies": { "@cloudflare/kv-asset-handler": "0.4.2", "@cloudflare/unenv-preset": "2.12.1", "blake3-wasm": "2.1.5", "esbuild": "0.27.3", "miniflare": "4.20260212.0", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.24", "workerd": "1.20260212.0" }, "optionalDependencies": { "fsevents": "~2.3.2" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20260212.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-R+n3o3tlGzLK9I4fGocPReOuvcnjhtOL2aCVKkHMeuEwt9pPbOO4FxJtx/ec5cIUG/otRyJnfQGCAr9DplBVng=="],
|
||||
|
||||
"ws": ["ws@8.18.0", "https://registry.npmmirror.com/ws/-/ws-8.18.0.tgz", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw=="],
|
||||
|
||||
"yocto-queue": ["yocto-queue@1.2.2", "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-1.2.2.tgz", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="],
|
||||
|
||||
"youch": ["youch@4.1.0-beta.10", "https://registry.npmmirror.com/youch/-/youch-4.1.0-beta.10.tgz", { "dependencies": { "@poppinss/colors": "^4.1.5", "@poppinss/dumper": "^0.6.4", "@speed-highlight/core": "^1.2.7", "cookie": "^1.0.2", "youch-core": "^0.3.3" } }, "sha512-rLfVLB4FgQneDr0dv1oddCVZmKjcJ6yX6mS4pU82Mq/Dt9a3cLZQ62pDBL4AUO+uVrCvtWz3ZFUL2HFAFJ/BXQ=="],
|
||||
|
||||
"youch-core": ["youch-core@0.3.3", "https://registry.npmmirror.com/youch-core/-/youch-core-0.3.3.tgz", { "dependencies": { "@poppinss/exception": "^1.2.2", "error-stack-parser-es": "^1.0.5" } }, "sha512-ho7XuGjLaJ2hWHoK8yFnsUGy2Y5uDpqSTq1FkHLK4/oqKtyUU1AFbOOxY4IpC9f0fTLjwYbslUz0Po5BpD1wrA=="],
|
||||
|
||||
"zod": ["zod@3.22.3", "https://registry.npmmirror.com/zod/-/zod-3.22.3.tgz", {}, "sha512-EjIevzuJRiRPbVH4mGc8nApb/lVLKVpmUhAaR5R5doKGfAnGJ6Gr3CViAVjP+4FWSxCsybeWQdcgCtbX+7oZug=="],
|
||||
|
||||
"miniflare/sharp": ["sharp@0.34.5", "https://registry.npmmirror.com/sharp/-/sharp-0.34.5.tgz", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-darwin-x64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4", "@img/sharp-libvips-darwin-x64": "1.2.4", "@img/sharp-libvips-linux-arm": "1.2.4", "@img/sharp-libvips-linux-arm64": "1.2.4", "@img/sharp-libvips-linux-ppc64": "1.2.4", "@img/sharp-libvips-linux-riscv64": "1.2.4", "@img/sharp-libvips-linux-s390x": "1.2.4", "@img/sharp-libvips-linux-x64": "1.2.4", "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", "@img/sharp-libvips-linuxmusl-x64": "1.2.4", "@img/sharp-linux-arm": "0.34.5", "@img/sharp-linux-arm64": "0.34.5", "@img/sharp-linux-ppc64": "0.34.5", "@img/sharp-linux-riscv64": "0.34.5", "@img/sharp-linux-s390x": "0.34.5", "@img/sharp-linux-x64": "0.34.5", "@img/sharp-linuxmusl-arm64": "0.34.5", "@img/sharp-linuxmusl-x64": "0.34.5", "@img/sharp-wasm32": "0.34.5", "@img/sharp-win32-arm64": "0.34.5", "@img/sharp-win32-ia32": "0.34.5", "@img/sharp-win32-x64": "0.34.5" } }, "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg=="],
|
||||
|
||||
"mlly/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"new-api-lite-worker/wrangler": ["wrangler@3.114.17", "https://registry.npmmirror.com/wrangler/-/wrangler-3.114.17.tgz", { "dependencies": { "@cloudflare/kv-asset-handler": "0.3.4", "@cloudflare/unenv-preset": "2.0.2", "@esbuild-plugins/node-globals-polyfill": "0.2.3", "@esbuild-plugins/node-modules-polyfill": "0.2.2", "blake3-wasm": "2.1.5", "esbuild": "0.17.19", "miniflare": "3.20250718.3", "path-to-regexp": "6.3.0", "unenv": "2.0.0-rc.14", "workerd": "1.20250718.0" }, "optionalDependencies": { "fsevents": "~2.3.2", "sharp": "^0.33.5" }, "peerDependencies": { "@cloudflare/workers-types": "^4.20250408.0" }, "optionalPeers": ["@cloudflare/workers-types"], "bin": { "wrangler": "bin/wrangler.js", "wrangler2": "bin/wrangler.js" } }, "sha512-tAvf7ly+tB+zwwrmjsCyJ2pJnnc7SZhbnNwXbH+OIdVas3zTSmjcZOjmLKcGGptssAA3RyTKhcF9BvKZzMUycA=="],
|
||||
|
||||
"npm-run-path/path-key": ["path-key@4.0.0", "https://registry.npmmirror.com/path-key/-/path-key-4.0.0.tgz", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="],
|
||||
|
||||
"pkg-types/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"rollup-plugin-inject/estree-walker": ["estree-walker@0.6.1", "https://registry.npmmirror.com/estree-walker/-/estree-walker-0.6.1.tgz", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
|
||||
|
||||
"rollup-plugin-inject/magic-string": ["magic-string@0.25.9", "https://registry.npmmirror.com/magic-string/-/magic-string-0.25.9.tgz", { "dependencies": { "sourcemap-codec": "^1.4.8" } }, "sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ=="],
|
||||
|
||||
"rollup-pluginutils/estree-walker": ["estree-walker@0.6.1", "https://registry.npmmirror.com/estree-walker/-/estree-walker-0.6.1.tgz", {}, "sha512-SqmZANLWS0mnatqbSfRP5g8OXZC12Fgg1IwNtLsyHDzJizORW4khDfjPqJZsemPWBB2uqykUah5YpQ6epsqC/w=="],
|
||||
|
||||
"unenv/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"vite/esbuild": ["esbuild@0.21.5", "https://registry.npmmirror.com/esbuild/-/esbuild-0.21.5.tgz", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.21.5", "@esbuild/android-arm": "0.21.5", "@esbuild/android-arm64": "0.21.5", "@esbuild/android-x64": "0.21.5", "@esbuild/darwin-arm64": "0.21.5", "@esbuild/darwin-x64": "0.21.5", "@esbuild/freebsd-arm64": "0.21.5", "@esbuild/freebsd-x64": "0.21.5", "@esbuild/linux-arm": "0.21.5", "@esbuild/linux-arm64": "0.21.5", "@esbuild/linux-ia32": "0.21.5", "@esbuild/linux-loong64": "0.21.5", "@esbuild/linux-mips64el": "0.21.5", "@esbuild/linux-ppc64": "0.21.5", "@esbuild/linux-riscv64": "0.21.5", "@esbuild/linux-s390x": "0.21.5", "@esbuild/linux-x64": "0.21.5", "@esbuild/netbsd-x64": "0.21.5", "@esbuild/openbsd-x64": "0.21.5", "@esbuild/sunos-x64": "0.21.5", "@esbuild/win32-arm64": "0.21.5", "@esbuild/win32-ia32": "0.21.5", "@esbuild/win32-x64": "0.21.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-darwin-x64": ["@img/sharp-darwin-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-darwin-x64": "1.2.4" }, "os": "darwin", "cpu": "x64" }, "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-darwin-x64": ["@img/sharp-libvips-darwin-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-arm": ["@img/sharp-libvips-linux-arm@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-arm64": ["@img/sharp-libvips-linux-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-s390x": ["@img/sharp-libvips-linux-s390x@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linux-x64": ["@img/sharp-libvips-linux-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linuxmusl-arm64": ["@img/sharp-libvips-linuxmusl-arm64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-libvips-linuxmusl-x64": ["@img/sharp-libvips-linuxmusl-x64@1.2.4", "https://registry.npmmirror.com/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", { "os": "linux", "cpu": "x64" }, "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-arm": ["@img/sharp-linux-arm@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm": "1.2.4" }, "os": "linux", "cpu": "arm" }, "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-arm64": ["@img/sharp-linux-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-s390x": ["@img/sharp-linux-s390x@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-s390x": "1.2.4" }, "os": "linux", "cpu": "s390x" }, "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linux-x64": ["@img/sharp-linux-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linux-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linuxmusl-arm64": ["@img/sharp-linuxmusl-arm64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" }, "os": "linux", "cpu": "arm64" }, "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-linuxmusl-x64": ["@img/sharp-linuxmusl-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", { "optionalDependencies": { "@img/sharp-libvips-linuxmusl-x64": "1.2.4" }, "os": "linux", "cpu": "x64" }, "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-wasm32": ["@img/sharp-wasm32@0.34.5", "https://registry.npmmirror.com/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", { "dependencies": { "@emnapi/runtime": "^1.7.0" }, "cpu": "none" }, "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-ia32": ["@img/sharp-win32-ia32@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg=="],
|
||||
|
||||
"miniflare/sharp/@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "https://registry.npmmirror.com/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/@cloudflare/kv-asset-handler": ["@cloudflare/kv-asset-handler@0.3.4", "https://registry.npmmirror.com/@cloudflare/kv-asset-handler/-/kv-asset-handler-0.3.4.tgz", { "dependencies": { "mime": "^3.0.0" } }, "sha512-YLPHc8yASwjNkmcDMQMY35yiWjoKAKnhUbPRszBRS0YgH+IXtsMp61j+yTcnCE3oO2DgP0U3iejLC8FTtKDC8Q=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/@cloudflare/unenv-preset": ["@cloudflare/unenv-preset@2.0.2", "https://registry.npmmirror.com/@cloudflare/unenv-preset/-/unenv-preset-2.0.2.tgz", { "peerDependencies": { "unenv": "2.0.0-rc.14", "workerd": "^1.20250124.0" }, "optionalPeers": ["workerd"] }, "sha512-nyzYnlZjjV5xT3LizahG1Iu6mnrCaxglJ04rZLpDwlDVDZ7v46lNsfxhV3A/xtfgQuSHmLnc6SVI+KwBpc3Lwg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild": ["esbuild@0.17.19", "https://registry.npmmirror.com/esbuild/-/esbuild-0.17.19.tgz", { "optionalDependencies": { "@esbuild/android-arm": "0.17.19", "@esbuild/android-arm64": "0.17.19", "@esbuild/android-x64": "0.17.19", "@esbuild/darwin-arm64": "0.17.19", "@esbuild/darwin-x64": "0.17.19", "@esbuild/freebsd-arm64": "0.17.19", "@esbuild/freebsd-x64": "0.17.19", "@esbuild/linux-arm": "0.17.19", "@esbuild/linux-arm64": "0.17.19", "@esbuild/linux-ia32": "0.17.19", "@esbuild/linux-loong64": "0.17.19", "@esbuild/linux-mips64el": "0.17.19", "@esbuild/linux-ppc64": "0.17.19", "@esbuild/linux-riscv64": "0.17.19", "@esbuild/linux-s390x": "0.17.19", "@esbuild/linux-x64": "0.17.19", "@esbuild/netbsd-x64": "0.17.19", "@esbuild/openbsd-x64": "0.17.19", "@esbuild/sunos-x64": "0.17.19", "@esbuild/win32-arm64": "0.17.19", "@esbuild/win32-ia32": "0.17.19", "@esbuild/win32-x64": "0.17.19" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/miniflare": ["miniflare@3.20250718.3", "https://registry.npmmirror.com/miniflare/-/miniflare-3.20250718.3.tgz", { "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.28.5", "workerd": "1.20250718.0", "ws": "8.18.0", "youch": "3.3.4", "zod": "3.22.3" }, "bin": { "miniflare": "bootstrap.js" } }, "sha512-JuPrDJhwLrNLEJiNLWO7ZzJrv/Vv9kZuwMYCfv0LskQDM6Eonw4OvywO3CH/wCGjgHzha/qyjUh8JQ068TjDgQ=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/unenv": ["unenv@2.0.0-rc.14", "https://registry.npmmirror.com/unenv/-/unenv-2.0.0-rc.14.tgz", { "dependencies": { "defu": "^6.1.4", "exsolve": "^1.0.1", "ohash": "^2.0.10", "pathe": "^2.0.3", "ufo": "^1.5.4" } }, "sha512-od496pShMen7nOy5VmVJCnq8rptd45vh6Nx/r2iPbrba6pa6p+tS2ywuIHRZ/OBvSbQZB0kWvpO9XBNVFXHD3Q=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/workerd": ["workerd@1.20250718.0", "https://registry.npmmirror.com/workerd/-/workerd-1.20250718.0.tgz", { "optionalDependencies": { "@cloudflare/workerd-darwin-64": "1.20250718.0", "@cloudflare/workerd-darwin-arm64": "1.20250718.0", "@cloudflare/workerd-linux-64": "1.20250718.0", "@cloudflare/workerd-linux-arm64": "1.20250718.0", "@cloudflare/workerd-windows-64": "1.20250718.0" }, "bin": { "workerd": "bin/workerd" } }, "sha512-kqkIJP/eOfDlUyBzU7joBg+tl8aB25gEAGqDap+nFWb+WHhnooxjGHgxPBy3ipw2hnShPFNOQt5lFRxbwALirg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.21.5", "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", { "os": "aix", "cpu": "ppc64" }, "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.21.5", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz", { "os": "android", "cpu": "arm" }, "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", { "os": "android", "cpu": "arm64" }, "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A=="],
|
||||
|
||||
"vite/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz", { "os": "android", "cpu": "x64" }, "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw=="],
|
||||
|
||||
"vite/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g=="],
|
||||
|
||||
"vite/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", { "os": "linux", "cpu": "arm" }, "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", { "os": "linux", "cpu": "none" }, "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A=="],
|
||||
|
||||
"vite/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", { "os": "linux", "cpu": "x64" }, "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ=="],
|
||||
|
||||
"vite/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", { "os": "none", "cpu": "x64" }, "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow=="],
|
||||
|
||||
"vite/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA=="],
|
||||
|
||||
"vite/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.21.5", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", { "os": "win32", "cpu": "x64" }, "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/android-arm": ["@esbuild/android-arm@0.17.19", "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz", { "os": "android", "cpu": "arm" }, "sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/android-arm64": ["@esbuild/android-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz", { "os": "android", "cpu": "arm64" }, "sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/android-x64": ["@esbuild/android-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz", { "os": "android", "cpu": "x64" }, "sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz", { "os": "freebsd", "cpu": "arm64" }, "sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz", { "os": "freebsd", "cpu": "x64" }, "sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-arm": ["@esbuild/linux-arm@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz", { "os": "linux", "cpu": "arm" }, "sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz", { "os": "linux", "cpu": "ia32" }, "sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz", { "os": "linux", "cpu": "none" }, "sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz", { "os": "linux", "cpu": "none" }, "sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz", { "os": "linux", "cpu": "ppc64" }, "sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz", { "os": "linux", "cpu": "none" }, "sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz", { "os": "linux", "cpu": "s390x" }, "sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/linux-x64": ["@esbuild/linux-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz", { "os": "linux", "cpu": "x64" }, "sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz", { "os": "none", "cpu": "x64" }, "sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz", { "os": "openbsd", "cpu": "x64" }, "sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz", { "os": "sunos", "cpu": "x64" }, "sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.17.19", "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz", { "os": "win32", "cpu": "arm64" }, "sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.17.19", "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz", { "os": "win32", "cpu": "ia32" }, "sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/esbuild/@esbuild/win32-x64": ["@esbuild/win32-x64@0.17.19", "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz", { "os": "win32", "cpu": "x64" }, "sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/miniflare/acorn": ["acorn@8.14.0", "https://registry.npmmirror.com/acorn/-/acorn-8.14.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/miniflare/acorn-walk": ["acorn-walk@8.3.2", "https://registry.npmmirror.com/acorn-walk/-/acorn-walk-8.3.2.tgz", {}, "sha512-cjkyv4OtNCIeqhHrfS81QWXoCBPExR/J62oyEqepVw8WaQeSqpW2uhuLPh1m9eWhDuOo/jUXVTlifvesOWp/4A=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/miniflare/undici": ["undici@5.29.0", "https://registry.npmmirror.com/undici/-/undici-5.29.0.tgz", { "dependencies": { "@fastify/busboy": "^2.0.0" } }, "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/miniflare/youch": ["youch@3.3.4", "https://registry.npmmirror.com/youch/-/youch-3.3.4.tgz", { "dependencies": { "cookie": "^0.7.1", "mustache": "^4.2.0", "stacktracey": "^2.1.8" } }, "sha512-UeVBXie8cA35DS6+nBkls68xaBBXCye0CNznrhszZjTbRVnJKQuNsyLKBTTL4ln1o1rh2PKtv35twV7irj5SEg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/unenv/pathe": ["pathe@2.0.3", "https://registry.npmmirror.com/pathe/-/pathe-2.0.3.tgz", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-darwin-64": ["@cloudflare/workerd-darwin-64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-64/-/workerd-darwin-64-1.20250718.0.tgz", { "os": "darwin", "cpu": "x64" }, "sha512-FHf4t7zbVN8yyXgQ/r/GqLPaYZSGUVzeR7RnL28Mwj2djyw2ZergvytVc7fdGcczl6PQh+VKGfZCfUqpJlbi9g=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-darwin-arm64": ["@cloudflare/workerd-darwin-arm64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-darwin-arm64/-/workerd-darwin-arm64-1.20250718.0.tgz", { "os": "darwin", "cpu": "arm64" }, "sha512-fUiyUJYyqqp4NqJ0YgGtp4WJh/II/YZsUnEb6vVy5Oeas8lUOxnN+ZOJ8N/6/5LQCVAtYCChRiIrBbfhTn5Z8Q=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-linux-64": ["@cloudflare/workerd-linux-64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-64/-/workerd-linux-64-1.20250718.0.tgz", { "os": "linux", "cpu": "x64" }, "sha512-5+eb3rtJMiEwp08Kryqzzu8d1rUcK+gdE442auo5eniMpT170Dz0QxBrqkg2Z48SFUPYbj+6uknuA5tzdRSUSg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-linux-arm64": ["@cloudflare/workerd-linux-arm64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-linux-arm64/-/workerd-linux-arm64-1.20250718.0.tgz", { "os": "linux", "cpu": "arm64" }, "sha512-Aa2M/DVBEBQDdATMbn217zCSFKE+ud/teS+fFS+OQqKABLn0azO2qq6ANAHYOIE6Q3Sq4CxDIQr8lGdaJHwUog=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/workerd/@cloudflare/workerd-windows-64": ["@cloudflare/workerd-windows-64@1.20250718.0", "https://registry.npmmirror.com/@cloudflare/workerd-windows-64/-/workerd-windows-64-1.20250718.0.tgz", { "os": "win32", "cpu": "x64" }, "sha512-dY16RXKffmugnc67LTbyjdDHZn5NoTF1yHEf2fN4+OaOnoGSp3N1x77QubTDwqZ9zECWxgQfDLjddcH8dWeFhg=="],
|
||||
|
||||
"new-api-lite-worker/wrangler/miniflare/youch/cookie": ["cookie@0.7.2", "https://registry.npmmirror.com/cookie/-/cookie-0.7.2.tgz", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
# 变更日志
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### 微调
|
||||
- **[admin-ui]**: 本地开发增加 Vite proxy 解决前后端端口不一致
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/admin/vite.config.ts, README.md
|
||||
- **[worker]**: 补充 wrangler.toml 示例配置占位
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/wrangler.toml
|
||||
- **[admin-ui]**: 渠道 ID 与日志渠道可见,操作反馈更清晰
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/admin/src/main.ts
|
||||
- **[worker]**: 使用日志关联渠道/令牌,base_url 自动纠正
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/routes/usage.ts, apps/worker/src/routes/channels.ts, apps/worker/src/routes/proxy.ts
|
||||
- **[worker]**: 渠道 ID 支持自定义,令牌可二次查看
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/routes/channels.ts, apps/worker/src/routes/tokens.ts, apps/worker/src/db/schema.sql, apps/worker/migrations/0002_add_token_plain.sql
|
||||
- **[admin-ui]**: 渠道 ID 可录入、令牌查看按钮
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/admin/src/main.ts
|
||||
- **[tests]**: 补充 URL 规范化单测
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: tests/worker/url.test.ts
|
||||
- **[proxy]**: 增加失败轮询重试与相关配置
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/routes/proxy.ts, apps/worker/src/env.ts, apps/worker/wrangler.toml
|
||||
- **[docs]**: 更新代理重试与本地配置说明
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: README.md, helloagents/modules/proxy.md
|
||||
- **[worker]**: 放宽路由严格匹配以兼容 `/api/channel/` 尾斜杠
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/index.ts
|
||||
- **[worker]**: 新增 `/api/group` 兼容接口并放行鉴权
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: apps/worker/src/index.ts, apps/worker/src/routes/newapiGroups.ts, tests/worker/newapi.test.ts
|
||||
|
||||
## [0.2.1] - 2026-02-15
|
||||
|
||||
### 变更
|
||||
- **[tooling]**: 切换为 Bun 作为包管理器,补充部署说明与 fix 命令
|
||||
- 方案: [202602150153_bun-tooling](archive/2026-02/202602150153_bun-tooling/)
|
||||
|
||||
## [0.2.0] - 2026-02-15
|
||||
|
||||
### 新增
|
||||
- **[channels/auth/models]**: 新增 New API 兼容渠道管理接口、用户模型接口与管理员令牌鉴权
|
||||
- 方案: [202602150127_newapi-channel-compat](archive/2026-02/202602150127_newapi-channel-compat/)
|
||||
- 决策: newapi-channel-compat#D001(新增兼容层并保留扩展字段)
|
||||
|
||||
## [0.1.0] - 2026-02-14
|
||||
|
||||
### 新增
|
||||
- **[核心服务]**: 初始化 Worker + D1 后端与 Vite 管理台,提供渠道/模型/令牌/日志/面板与 OpenAI 兼容代理
|
||||
- 方案: [202602142217_new-api-lite](archive/2026-02/202602142217_new-api-lite/)
|
||||
- 决策: new-api-lite#D001(单 Worker + Hono), new-api-lite#D002(Vite + Pages), new-api-lite#D003(Token 默认全渠道), new-api-lite#D004(日志保留可配置)
|
||||
|
||||
### 修复
|
||||
- **[{模块名}]**: {修复描述}
|
||||
- 方案: [{YYYYMMDDHHMM}_{fix}](archive/{YYYY-MM}/{YYYYMMDDHHMM}_{fix}/)
|
||||
|
||||
### 微调
|
||||
- **[{模块名}]**: {微调描述}
|
||||
- 类型: 微调(无方案包)
|
||||
- 文件: {文件路径}:{行号范围}
|
||||
|
||||
### 回滚
|
||||
- **[{模块名}]**: 回滚至 {版本/提交}
|
||||
- 原因: {回滚原因}
|
||||
- 方案: [{原方案包}](archive/{YYYY-MM}/{原方案包}/)
|
||||
@@ -0,0 +1,36 @@
|
||||
# new-api-lite 知识库
|
||||
|
||||
> 本文件是知识库的入口点
|
||||
|
||||
## 快速导航
|
||||
|
||||
| 需要了解 | 读取文件 |
|
||||
|---------|---------|
|
||||
| 项目概况、技术栈、开发约定 | [context.md](context.md) |
|
||||
| 模块索引 | [modules/_index.md](modules/_index.md) |
|
||||
| 某个模块的职责和接口 | [modules/{模块名}.md](modules/{模块名}.md) |
|
||||
| 项目变更历史 | [CHANGELOG.md](CHANGELOG.md) |
|
||||
| 历史方案索引 | [archive/_index.md](archive/_index.md) |
|
||||
| 当前待执行的方案 | [plan/](plan/) |
|
||||
|
||||
## 知识库状态
|
||||
|
||||
```yaml
|
||||
最后更新: 2026-02-15 02:03
|
||||
模块数量: 9
|
||||
待执行方案: 0
|
||||
```
|
||||
|
||||
## 读取指引
|
||||
|
||||
```yaml
|
||||
启动任务:
|
||||
1. 读取本文件获取导航
|
||||
2. 读取 context.md 获取项目上下文
|
||||
3. 检查 plan/ 是否有进行中方案包
|
||||
|
||||
任务相关:
|
||||
- 涉及特定模块: 读取 modules/{模块名}.md
|
||||
- 需要历史决策: 搜索 CHANGELOG.md → 读取对应 archive/{YYYY-MM}/{方案包}/proposal.md
|
||||
- 继续之前任务: 读取 plan/{方案包}/*
|
||||
```
|
||||
@@ -0,0 +1,17 @@
|
||||
version = 1
|
||||
|
||||
[commands]
|
||||
# mode:
|
||||
# - "allowlist": 只允许 allow 里的命令
|
||||
# - "denylist": 允许除 deny 以外的命令
|
||||
mode = "denylist"
|
||||
|
||||
# 默认策略:允许全部命令(deny 为空)
|
||||
allow = [
|
||||
"~auto", "~plan", "~exec",
|
||||
"~init", "~upgrade", "~clean",
|
||||
"~commit", "~test", "~review",
|
||||
"~validate", "~rollback", "~help"
|
||||
]
|
||||
|
||||
deny = []
|
||||
@@ -0,0 +1,89 @@
|
||||
# 项目上下文
|
||||
|
||||
## 1. 基本信息
|
||||
|
||||
```yaml
|
||||
名称: new-api-lite
|
||||
描述: 基于 Cloudflare Workers + D1 的简易版 new-api
|
||||
类型: Web服务
|
||||
状态: 开发中
|
||||
```
|
||||
|
||||
## 2. 技术上下文
|
||||
|
||||
```yaml
|
||||
语言: TypeScript
|
||||
框架: Hono + Vite
|
||||
包管理器: bun
|
||||
构建工具: Wrangler + Vite
|
||||
```
|
||||
|
||||
### 主要依赖
|
||||
| 依赖 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| Hono | 待定 | Worker 路由与中间件 |
|
||||
| Wrangler | 待定 | Workers 构建与部署 |
|
||||
| Vite | 待定 | 管理台构建 |
|
||||
|
||||
## 3. 项目概述
|
||||
|
||||
### 核心功能
|
||||
- OpenAI 兼容代理(/v1/*)
|
||||
- 渠道管理、模型广场、令牌管理、使用日志、数据面板
|
||||
- 管理员登录与权限
|
||||
- New API 兼容渠道管理接口(/api/channel)
|
||||
- 系统设置(日志保留天数)
|
||||
|
||||
### 项目边界
|
||||
```yaml
|
||||
范围内:
|
||||
- 单管理员密码登录
|
||||
- Token 默认全渠道
|
||||
- 使用日志可配置保留天数
|
||||
范围外:
|
||||
- 多租户/复杂角色体系
|
||||
- 复杂计费结算与对账
|
||||
```
|
||||
|
||||
## 4. 开发约定
|
||||
|
||||
### 代码规范
|
||||
```yaml
|
||||
命名风格: camelCase
|
||||
文件命名: kebab-case
|
||||
目录组织: apps/worker + apps/admin
|
||||
```
|
||||
|
||||
### 错误处理
|
||||
```yaml
|
||||
错误码格式: ERR_{模块}_{含义}
|
||||
日志级别: info/warn/error
|
||||
```
|
||||
|
||||
### 测试要求
|
||||
```yaml
|
||||
测试框架: vitest
|
||||
覆盖率要求: 核心逻辑覆盖
|
||||
测试文件位置: tests/
|
||||
```
|
||||
|
||||
### Git规范
|
||||
```yaml
|
||||
分支策略: trunk-based
|
||||
提交格式: conventional commits
|
||||
```
|
||||
|
||||
## 5. 当前约束(源自历史决策)
|
||||
|
||||
| 约束 | 原因 | 决策来源 |
|
||||
|------|------|---------|
|
||||
| Cloudflare Workers + D1 | 技术栈约束 | new-api-lite#D001 |
|
||||
| 登录密码来自环境变量 | 安全要求 | new-api-lite#D002 |
|
||||
| Token 默认全渠道 | 产品规则 | new-api-lite#D003 |
|
||||
| 使用日志保留天数可配置 | 合规/成本 | new-api-lite#D004 |
|
||||
|
||||
## 6. 已知技术债务(可选)
|
||||
|
||||
| 债务描述 | 优先级 | 来源 | 建议处理时机 |
|
||||
|---------|--------|------|-------------|
|
||||
| 暂无 | - | - | - |
|
||||
@@ -0,0 +1,32 @@
|
||||
# 模块索引
|
||||
|
||||
> 通过此文件快速定位模块文档
|
||||
|
||||
## 模块清单
|
||||
|
||||
| 模块 | 职责 | 状态 | 文档 |
|
||||
|------|------|------|------|
|
||||
| auth | 管理员登录与会话 | 🚧 | [auth.md](./auth.md) |
|
||||
| proxy | OpenAI 兼容代理与转发 | 🚧 | [proxy.md](./proxy.md) |
|
||||
| channels | 渠道管理与健康检查 | 🚧 | [channels.md](./channels.md) |
|
||||
| models | 模型广场与聚合 | 🚧 | [models.md](./models.md) |
|
||||
| tokens | 令牌管理与额度 | 🚧 | [tokens.md](./tokens.md) |
|
||||
| usage | 使用日志与留存 | 🚧 | [usage.md](./usage.md) |
|
||||
| dashboard | 数据面板统计 | 🚧 | [dashboard.md](./dashboard.md) |
|
||||
| settings | 系统配置 | 🚧 | [settings.md](./settings.md) |
|
||||
| admin-ui | 管理台前端 | 🚧 | [admin-ui.md](./admin-ui.md) |
|
||||
|
||||
## 模块依赖关系
|
||||
|
||||
```
|
||||
admin-ui → auth, channels, models, tokens, usage, dashboard, settings
|
||||
proxy → channels, tokens, usage
|
||||
dashboard → usage, tokens
|
||||
models → channels
|
||||
usage → settings
|
||||
```
|
||||
|
||||
## 状态说明
|
||||
- ✅ 稳定
|
||||
- 🚧 开发中
|
||||
- 📝 规划中
|
||||
@@ -0,0 +1,17 @@
|
||||
# admin-ui 模块
|
||||
|
||||
## 职责
|
||||
- 管理台前端展示与操作
|
||||
- 覆盖渠道、模型、令牌、日志、面板与设置
|
||||
|
||||
## 接口定义
|
||||
- 调用 `/api/*` 管理接口
|
||||
- 登录后使用 Bearer token
|
||||
|
||||
## 行为规范
|
||||
- Vite 构建静态文件
|
||||
- 前端状态集中在单页
|
||||
- Token 仅本地保存,不回显
|
||||
|
||||
## 依赖关系
|
||||
- `auth` / `channels` / `models` / `tokens` / `usage` / `dashboard` / `settings` 模块
|
||||
@@ -0,0 +1,20 @@
|
||||
# auth 模块
|
||||
|
||||
## 职责
|
||||
- 管理员密码登录与会话签发
|
||||
- 管理员会话注销与失效处理
|
||||
- New API 兼容接口鉴权(管理员令牌或会话)
|
||||
|
||||
## 接口定义
|
||||
- `POST /api/auth/login` 管理员登录
|
||||
- `POST /api/auth/logout` 管理员退出
|
||||
|
||||
## 行为规范
|
||||
- 密码仅来自环境变量 `ADMIN_PASSWORD`
|
||||
- 会话 token 使用哈希后存储
|
||||
- 过期会话自动清理
|
||||
- New API 兼容接口允许使用 `NEW_API_ADMIN_TOKEN` 直接鉴权
|
||||
|
||||
## 依赖关系
|
||||
- `admin_sessions` 表
|
||||
- `ADMIN_PASSWORD` / `SESSION_TTL_HOURS` / `NEW_API_ADMIN_TOKEN` 环境变量
|
||||
@@ -0,0 +1,34 @@
|
||||
# channels 模块
|
||||
|
||||
## 职责
|
||||
- 渠道配置的增删改查
|
||||
- 渠道连通性测试与模型同步
|
||||
- 渠道权重与状态管理
|
||||
|
||||
## 接口定义
|
||||
- `GET /api/channels` 渠道列表
|
||||
- `POST /api/channels` 新建渠道
|
||||
- `PATCH /api/channels/:id` 更新渠道
|
||||
- `DELETE /api/channels/:id` 删除渠道
|
||||
- `POST /api/channels/:id/test` 连通性测试
|
||||
- `GET /api/channel` New API 兼容列表
|
||||
- `GET /api/channel/search` New API 兼容搜索
|
||||
- `POST /api/channel` New API 兼容新增
|
||||
- `PUT /api/channel` New API 兼容更新
|
||||
- `DELETE /api/channel/:id` New API 兼容删除
|
||||
- `GET /api/channel/test/:id` New API 兼容连通性测试
|
||||
- `GET /api/channel/fetch_models/:id` New API 兼容模型拉取
|
||||
- `GET /api/group` New API 兼容分组列表
|
||||
|
||||
## 行为规范
|
||||
- `base_url` 保存为无尾斜杠格式
|
||||
- 创建渠道时可传入自定义 `id`,未提供则自动生成
|
||||
- 连通性测试调用 `/v1/models`
|
||||
- 测试成功后更新 `models_json`
|
||||
- New API 兼容层支持 `type`/`group`/`priority` 等字段映射,并保留扩展字段到 `metadata_json`
|
||||
- New API 分组列表从 `group_name` 字段解析,空时返回 `default`
|
||||
|
||||
## 依赖关系
|
||||
- `channels` 表
|
||||
- `models` 模块(聚合读取)
|
||||
- `newApiAuth` 中间件
|
||||
@@ -0,0 +1,16 @@
|
||||
# dashboard 模块
|
||||
|
||||
## 职责
|
||||
- 汇总调用统计指标
|
||||
- 输出按时间、模型、渠道、令牌的聚合数据
|
||||
|
||||
## 接口定义
|
||||
- `GET /api/dashboard` 数据面板汇总
|
||||
|
||||
## 行为规范
|
||||
- 以 `usage_logs` 为统计来源
|
||||
- 默认返回最近 30 条趋势数据
|
||||
|
||||
## 依赖关系
|
||||
- `usage_logs` 表
|
||||
- `tokens` / `channels` 表
|
||||
@@ -0,0 +1,16 @@
|
||||
# models 模块
|
||||
|
||||
## 职责
|
||||
- 汇总各渠道模型列表
|
||||
- 输出模型与渠道的映射关系
|
||||
|
||||
## 接口定义
|
||||
- `GET /api/models` 模型广场列表
|
||||
- `GET /api/user/models` New API 兼容用户模型列表
|
||||
|
||||
## 行为规范
|
||||
- 仅聚合启用状态的渠道
|
||||
- 优先使用渠道连通性测试返回的模型数据
|
||||
|
||||
## 依赖关系
|
||||
- `channels` 表
|
||||
@@ -0,0 +1,19 @@
|
||||
# proxy 模块
|
||||
|
||||
## 职责
|
||||
- 提供 OpenAI 兼容 API 代理
|
||||
- 根据权重选择渠道并支持故障回退
|
||||
- 记录用量日志并回写额度
|
||||
|
||||
## 接口定义
|
||||
- `/v1/*` OpenAI 兼容代理路径
|
||||
|
||||
## 行为规范
|
||||
- 基于令牌鉴权
|
||||
- 按渠道权重随机排序
|
||||
- 非流式响应解析 usage 字段
|
||||
- 可配置失败重试轮询(响应 5xx/429 时触发)
|
||||
|
||||
## 依赖关系
|
||||
- `channels` / `tokens` / `usage_logs` 表
|
||||
- `tokenAuth` 中间件
|
||||
@@ -0,0 +1,16 @@
|
||||
# settings 模块
|
||||
|
||||
## 职责
|
||||
- 维护系统级配置(如日志保留天数)
|
||||
- 为使用日志与面板提供统一配置读取
|
||||
|
||||
## 接口定义
|
||||
- `GET /api/settings` 读取设置
|
||||
- `PUT /api/settings` 更新设置
|
||||
|
||||
## 行为规范
|
||||
- 配置写入后即时生效
|
||||
- 未配置时使用环境变量默认值
|
||||
|
||||
## 依赖关系
|
||||
- `settings` 表
|
||||
@@ -0,0 +1,22 @@
|
||||
# tokens 模块
|
||||
|
||||
## 职责
|
||||
- 管理 API 令牌的生成与生命周期
|
||||
- 维护额度与已用额度统计
|
||||
- 控制令牌启用/禁用状态
|
||||
|
||||
## 接口定义
|
||||
- `GET /api/tokens` 令牌列表
|
||||
- `POST /api/tokens` 生成令牌
|
||||
- `PATCH /api/tokens/:id` 更新令牌
|
||||
- `DELETE /api/tokens/:id` 删除令牌
|
||||
- `GET /api/tokens/:id/reveal` 再次查看令牌
|
||||
|
||||
## 行为规范
|
||||
- 令牌可通过 reveal 接口再次查看
|
||||
- 默认允许所有渠道
|
||||
- 额度消耗由代理调用记录
|
||||
|
||||
## 依赖关系
|
||||
- `tokens` 表
|
||||
- `usage` 模块(用量回写)
|
||||
@@ -0,0 +1,18 @@
|
||||
# usage 模块
|
||||
|
||||
## 职责
|
||||
- 记录代理调用日志
|
||||
- 提供日志查询与筛选
|
||||
- 执行日志保留策略
|
||||
|
||||
## 接口定义
|
||||
- `GET /api/usage` 使用日志查询
|
||||
|
||||
## 行为规范
|
||||
- 代理调用统一写入 `usage_logs`
|
||||
- 保留天数可配置,查询时触发清理
|
||||
- 查询结果附带渠道与令牌名称
|
||||
|
||||
## 依赖关系
|
||||
- `usage_logs` 表
|
||||
- `settings` 模块
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "new-api-lite",
|
||||
"private": true,
|
||||
"packageManager": "bun@1.3.9",
|
||||
"workspaces": [
|
||||
"apps/*"
|
||||
],
|
||||
"scripts": {
|
||||
"dev:worker": "npm --workspace apps/worker run dev",
|
||||
"dev:admin": "npm --workspace apps/admin run dev",
|
||||
"test": "vitest",
|
||||
"fix": "bun pm pkg fix"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20.11.30",
|
||||
"typescript": "^5.4.5",
|
||||
"vitest": "^1.5.3"
|
||||
},
|
||||
"dependencies": {
|
||||
"wrangler": "4"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"module": "ESNext",
|
||||
"moduleResolution": "Bundler",
|
||||
"strict": true,
|
||||
"types": ["vitest/globals"],
|
||||
"skipLibCheck": true
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
import { defineConfig } from 'vitest/config';
|
||||
|
||||
export default defineConfig({
|
||||
test: {
|
||||
include: ['tests/**/*.test.ts'],
|
||||
environment: 'node'
|
||||
}
|
||||
});
|
||||
Reference in new issue
Block a user