Compare commits
12 Commits
b3258e76df
...
1d7e839b49
| Author | SHA1 | Date | |
|---|---|---|---|
| 1d7e839b49 | |||
| fa7babf13b | |||
| 280099b89c | |||
| 0a92a25451 | |||
| 8c075194e5 | |||
| 53e477d383 | |||
| 1522c87c74 | |||
| e0d05c9869 | |||
| 5b401e29cb | |||
| 65ac9f740a | |||
| 58ebcaa299 | |||
| 5b765c8b5e |
139
Makefile
139
Makefile
@@ -1,22 +1,45 @@
|
|||||||
.PHONY: all clean \
|
.PHONY: all dev build test lint clean \
|
||||||
backend-build backend-run backend-test backend-test-unit backend-test-integration backend-test-coverage \
|
backend-build backend-run backend-dev backend-test backend-test-unit backend-test-integration backend-test-coverage \
|
||||||
backend-lint backend-deps backend-generate \
|
backend-lint backend-clean backend-deps backend-generate \
|
||||||
backend-migrate-up backend-migrate-down backend-migrate-status backend-migrate-create \
|
backend-db-up backend-db-down backend-db-status backend-db-create \
|
||||||
frontend-build frontend-dev frontend-test frontend-test-watch frontend-test-coverage frontend-test-e2e frontend-lint \
|
test-mysql-up test-mysql-down test-mysql test-mysql-quick \
|
||||||
desktop-mac desktop-win desktop-linux package-macos
|
frontend-build frontend-dev frontend-test frontend-test-watch frontend-test-coverage frontend-test-e2e frontend-lint frontend-clean \
|
||||||
|
desktop-build desktop-build-mac desktop-build-win desktop-build-linux \
|
||||||
|
desktop-dev desktop-package-mac desktop-package-win desktop-package-linux desktop-clean \
|
||||||
|
desktop-prepare-frontend desktop-prepare-embedfs
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# 顶层便捷命令
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
dev:
|
||||||
|
@echo "🚀 Starting development environment..."
|
||||||
|
@$(MAKE) -j2 backend-dev frontend-dev
|
||||||
|
|
||||||
|
build: backend-build frontend-build
|
||||||
|
@echo "✅ Build complete"
|
||||||
|
|
||||||
|
test: backend-test frontend-test
|
||||||
|
@echo "✅ All tests passed"
|
||||||
|
|
||||||
|
lint: backend-lint frontend-lint
|
||||||
|
@echo "✅ Lint complete"
|
||||||
|
|
||||||
|
all: build test lint
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 后端
|
# 后端
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
all: backend-build
|
|
||||||
|
|
||||||
backend-build:
|
backend-build:
|
||||||
cd backend && go build -o bin/server ./cmd/server
|
cd backend && go build -o bin/server ./cmd/server
|
||||||
|
|
||||||
backend-run:
|
backend-run:
|
||||||
cd backend && go run ./cmd/server
|
cd backend && go run ./cmd/server
|
||||||
|
|
||||||
|
backend-dev:
|
||||||
|
cd backend && go run ./cmd/server
|
||||||
|
|
||||||
backend-test:
|
backend-test:
|
||||||
cd backend && go test ./... -v
|
cd backend && go test ./... -v
|
||||||
|
|
||||||
@@ -34,24 +57,63 @@ backend-test-coverage:
|
|||||||
backend-lint:
|
backend-lint:
|
||||||
cd backend && go tool golangci-lint run ./...
|
cd backend && go tool golangci-lint run ./...
|
||||||
|
|
||||||
|
backend-clean:
|
||||||
|
rm -rf backend/bin/ backend/coverage.out backend/coverage.html
|
||||||
|
|
||||||
backend-deps:
|
backend-deps:
|
||||||
cd backend && go mod tidy
|
cd backend && go mod tidy
|
||||||
|
|
||||||
backend-generate:
|
backend-generate:
|
||||||
cd backend && go generate ./...
|
cd backend && go generate ./...
|
||||||
|
|
||||||
backend-migrate-up:
|
backend-db-up:
|
||||||
cd backend && goose -dir migrations sqlite3 $(DB_PATH) up
|
@echo "Running database migration up..."
|
||||||
|
cd backend && goose -dir migrations/sqlite sqlite3 "$(DB_PATH)" up
|
||||||
|
|
||||||
backend-migrate-down:
|
backend-db-down:
|
||||||
cd backend && goose -dir migrations sqlite3 $(DB_PATH) down
|
@echo "Running database migration down..."
|
||||||
|
cd backend && goose -dir migrations/sqlite sqlite3 "$(DB_PATH)" down
|
||||||
|
|
||||||
backend-migrate-status:
|
backend-db-status:
|
||||||
cd backend && goose -dir migrations sqlite3 $(DB_PATH) status
|
@echo "Checking database migration status..."
|
||||||
|
cd backend && goose -dir migrations/sqlite sqlite3 "$(DB_PATH)" status
|
||||||
|
|
||||||
backend-migrate-create:
|
backend-db-create:
|
||||||
@read -p "Migration name: " name; \
|
@read -p "Migration name: " name; \
|
||||||
cd backend && goose -dir migrations create $$name sql
|
cd backend && goose -dir migrations/sqlite create $$name sql; \
|
||||||
|
cd backend && goose -dir migrations/mysql create $$name sql
|
||||||
|
|
||||||
|
# ============================================
|
||||||
|
# MySQL 专项测试
|
||||||
|
# ============================================
|
||||||
|
|
||||||
|
test-mysql-up:
|
||||||
|
@echo "Starting MySQL test container..."
|
||||||
|
cd backend/tests/mysql && docker-compose up -d
|
||||||
|
@echo "Waiting for MySQL to be ready..."
|
||||||
|
@for i in $$(seq 1 30); do \
|
||||||
|
if docker exec nex-mysql-test mysqladmin ping -h localhost -u root -ptestpass --silent 2>/dev/null; then \
|
||||||
|
echo "MySQL is ready!"; \
|
||||||
|
exit 0; \
|
||||||
|
fi; \
|
||||||
|
echo "Waiting... ($$i/30)"; \
|
||||||
|
sleep 1; \
|
||||||
|
done; \
|
||||||
|
echo "MySQL failed to start"; \
|
||||||
|
exit 1
|
||||||
|
|
||||||
|
test-mysql-down:
|
||||||
|
@echo "Stopping MySQL test container..."
|
||||||
|
cd backend/tests/mysql && docker-compose down -v
|
||||||
|
|
||||||
|
test-mysql: test-mysql-up
|
||||||
|
@echo "Running MySQL tests..."
|
||||||
|
cd backend && go test -tags=mysql ./tests/mysql/... -v -count=1
|
||||||
|
$(MAKE) test-mysql-down
|
||||||
|
|
||||||
|
test-mysql-quick:
|
||||||
|
@echo "Running MySQL tests (without container management)..."
|
||||||
|
cd backend && go test -tags=mysql ./tests/mysql/... -v -count=1
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 前端
|
# 前端
|
||||||
@@ -78,35 +140,60 @@ frontend-test-e2e:
|
|||||||
frontend-lint:
|
frontend-lint:
|
||||||
cd frontend && bun run lint
|
cd frontend && bun run lint
|
||||||
|
|
||||||
|
frontend-clean:
|
||||||
|
rm -rf frontend/dist frontend/.next frontend/node_modules
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 桌面应用
|
# 桌面应用
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
frontend-build-desktop:
|
desktop-build: desktop-build-mac desktop-build-win desktop-build-linux
|
||||||
cd frontend && cp .env.desktop .env.production.local && bun install && bun run build && rm -f .env.production.local
|
@echo "✅ Desktop builds complete for all platforms"
|
||||||
|
|
||||||
embedfs-prepare:
|
desktop-prepare-frontend:
|
||||||
|
@echo "📦 Preparing frontend for desktop..."
|
||||||
|
cd frontend && cp .env.desktop .env.production.local
|
||||||
|
cd frontend && bun install && bun run build
|
||||||
|
rm -f frontend/.env.production.local
|
||||||
|
|
||||||
|
desktop-prepare-embedfs:
|
||||||
|
@echo "📦 Preparing embedded filesystem..."
|
||||||
rm -rf embedfs/assets embedfs/frontend-dist
|
rm -rf embedfs/assets embedfs/frontend-dist
|
||||||
cp -r assets embedfs/assets
|
cp -r assets embedfs/assets
|
||||||
cp -r frontend/dist embedfs/frontend-dist
|
cp -r frontend/dist embedfs/frontend-dist
|
||||||
|
|
||||||
desktop-mac: frontend-build-desktop embedfs-prepare
|
desktop-build-mac: desktop-prepare-frontend desktop-prepare-embedfs
|
||||||
|
@echo "🍎 Building macOS..."
|
||||||
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../build/nex-mac-arm64 ./cmd/desktop
|
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=arm64 go build -o ../build/nex-mac-arm64 ./cmd/desktop
|
||||||
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-mac-amd64 ./cmd/desktop
|
cd backend && CGO_ENABLED=1 GOOS=darwin GOARCH=amd64 go build -o ../build/nex-mac-amd64 ./cmd/desktop
|
||||||
|
|
||||||
desktop-win: frontend-build-desktop embedfs-prepare
|
desktop-build-win: desktop-prepare-frontend desktop-prepare-embedfs
|
||||||
|
@echo "🪟 Building Windows..."
|
||||||
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o ../build/nex-win-amd64.exe ./cmd/desktop
|
cd backend && CGO_ENABLED=1 GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o ../build/nex-win-amd64.exe ./cmd/desktop
|
||||||
|
|
||||||
desktop-linux: frontend-build-desktop embedfs-prepare
|
desktop-build-linux: desktop-prepare-frontend desktop-prepare-embedfs
|
||||||
|
@echo "🐧 Building Linux..."
|
||||||
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o ../build/nex-linux-amd64 ./cmd/desktop
|
cd backend && CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o ../build/nex-linux-amd64 ./cmd/desktop
|
||||||
|
|
||||||
package-macos:
|
desktop-dev: desktop-prepare-frontend desktop-prepare-embedfs
|
||||||
|
@echo "🖥️ Starting desktop app in dev mode..."
|
||||||
|
cd backend && go run ./cmd/desktop
|
||||||
|
|
||||||
|
desktop-package-mac:
|
||||||
./scripts/build/package-macos.sh
|
./scripts/build/package-macos.sh
|
||||||
|
|
||||||
|
desktop-package-win:
|
||||||
|
@echo "⚠️ Windows packaging not implemented yet"
|
||||||
|
|
||||||
|
desktop-package-linux:
|
||||||
|
@echo "⚠️ Linux packaging not implemented yet"
|
||||||
|
|
||||||
|
desktop-clean:
|
||||||
|
rm -rf build/ embedfs/assets embedfs/frontend-dist
|
||||||
|
|
||||||
# ============================================
|
# ============================================
|
||||||
# 清理
|
# 清理
|
||||||
# ============================================
|
# ============================================
|
||||||
|
|
||||||
clean:
|
clean: backend-clean frontend-clean desktop-clean
|
||||||
rm -rf backend/bin/ backend/coverage.out backend/coverage.html
|
@echo "✅ Clean complete"
|
||||||
rm -rf build/
|
|
||||||
|
|||||||
75
README.md
75
README.md
@@ -66,12 +66,26 @@ nex/
|
|||||||
- **语言**: Go 1.26+
|
- **语言**: Go 1.26+
|
||||||
- **HTTP 框架**: Gin
|
- **HTTP 框架**: Gin
|
||||||
- **ORM**: GORM
|
- **ORM**: GORM
|
||||||
- **数据库**: SQLite
|
- **数据库**: SQLite / MySQL
|
||||||
- **日志**: zap + lumberjack(结构化日志 + 日志轮转)
|
- **日志**: zap + lumberjack(结构化日志 + 日志轮转 + 模块标识)
|
||||||
- **配置**: Viper + pflag(多层配置:CLI > 环境变量 > 配置文件 > 默认值)
|
- **配置**: Viper + pflag(多层配置:CLI > 环境变量 > 配置文件 > 默认值)
|
||||||
- **验证**: go-playground/validator/v10
|
- **验证**: go-playground/validator/v10
|
||||||
- **迁移**: goose
|
- **迁移**: goose
|
||||||
|
|
||||||
|
#### 日志模块标识规范
|
||||||
|
|
||||||
|
每个模块通过依赖注入获取带模块标识的 logger,日志输出格式为 `[module.name]`:
|
||||||
|
|
||||||
|
```
|
||||||
|
Console: INFO [handler.proxy] 处理请求 method=POST path=/v1/chat
|
||||||
|
JSON: {"level":"info","logger":"handler.proxy","msg":"处理请求","method":"POST"}
|
||||||
|
```
|
||||||
|
|
||||||
|
模块命名规范:
|
||||||
|
- 单一职责包:`database`、`config`
|
||||||
|
- 多实体包:`handler.proxy`、`service.provider`
|
||||||
|
- 子包:`handler.middleware`
|
||||||
|
|
||||||
### 前端
|
### 前端
|
||||||
- **运行时**: Bun
|
- **运行时**: Bun
|
||||||
- **构建工具**: Vite
|
- **构建工具**: Vite
|
||||||
@@ -92,14 +106,17 @@ nex/
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
# macOS (arm64 + amd64)
|
# macOS (arm64 + amd64)
|
||||||
make desktop-mac
|
make desktop-build-mac
|
||||||
make package-macos # 打包为 .app
|
make desktop-package-mac # 打包为 .app
|
||||||
|
|
||||||
# Windows
|
# Windows
|
||||||
make desktop-win
|
make desktop-build-win
|
||||||
|
|
||||||
# Linux
|
# Linux
|
||||||
make desktop-linux
|
make desktop-build-linux
|
||||||
|
|
||||||
|
# 构建所有平台
|
||||||
|
make desktop-build
|
||||||
```
|
```
|
||||||
|
|
||||||
**使用桌面应用**:
|
**使用桌面应用**:
|
||||||
@@ -201,7 +218,14 @@ server:
|
|||||||
write_timeout: 30s
|
write_timeout: 30s
|
||||||
|
|
||||||
database:
|
database:
|
||||||
path: ~/.nex/config.db
|
driver: sqlite # sqlite 或 mysql
|
||||||
|
path: ~/.nex/config.db # SQLite 数据库文件路径
|
||||||
|
# --- MySQL 配置(driver=mysql 时生效)---
|
||||||
|
# host: localhost
|
||||||
|
# port: 3306
|
||||||
|
# user: nex
|
||||||
|
# password: ""
|
||||||
|
# dbname: nex
|
||||||
max_idle_conns: 10
|
max_idle_conns: 10
|
||||||
max_open_conns: 100
|
max_open_conns: 100
|
||||||
conn_max_lifetime: 1h
|
conn_max_lifetime: 1h
|
||||||
@@ -223,6 +247,14 @@ log:
|
|||||||
export NEX_SERVER_PORT=9000
|
export NEX_SERVER_PORT=9000
|
||||||
export NEX_DATABASE_PATH=/data/nex.db
|
export NEX_DATABASE_PATH=/data/nex.db
|
||||||
export NEX_LOG_LEVEL=debug
|
export NEX_LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# MySQL 模式
|
||||||
|
export NEX_DATABASE_DRIVER=mysql
|
||||||
|
export NEX_DATABASE_HOST=db.example.com
|
||||||
|
export NEX_DATABASE_PORT=3306
|
||||||
|
export NEX_DATABASE_USER=nex
|
||||||
|
export NEX_DATABASE_PASSWORD=secret
|
||||||
|
export NEX_DATABASE_DBNAME=nex
|
||||||
```
|
```
|
||||||
|
|
||||||
命名规则:配置路径转大写 + 下划线(如 `server.port` → `NEX_SERVER_PORT`)。
|
命名规则:配置路径转大写 + 下划线(如 `server.port` → `NEX_SERVER_PORT`)。
|
||||||
@@ -238,29 +270,54 @@ export NEX_LOG_LEVEL=debug
|
|||||||
### 数据文件
|
### 数据文件
|
||||||
|
|
||||||
- `~/.nex/config.yaml` - 配置文件
|
- `~/.nex/config.yaml` - 配置文件
|
||||||
- `~/.nex/config.db` - SQLite 数据库
|
- `~/.nex/config.db` - SQLite 数据库(MySQL 模式下不使用本地数据库文件)
|
||||||
- `~/.nex/log/` - 日志目录
|
- `~/.nex/log/` - 日志目录
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 顶层便捷命令
|
||||||
|
make test # 运行所有测试
|
||||||
|
|
||||||
|
# 后端测试
|
||||||
make backend-test # 后端测试
|
make backend-test # 后端测试
|
||||||
make backend-test-coverage # 后端覆盖率
|
make backend-test-coverage # 后端覆盖率
|
||||||
|
make backend-test-unit # 后端单元测试
|
||||||
|
make backend-test-integration # 后端集成测试
|
||||||
|
|
||||||
|
# 前端测试
|
||||||
make frontend-test # 前端测试
|
make frontend-test # 前端测试
|
||||||
make frontend-test-e2e # 前端 E2E 测试
|
make frontend-test-e2e # 前端 E2E 测试
|
||||||
|
make frontend-test-coverage # 前端覆盖率
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发
|
## 开发
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 顶层便捷命令
|
||||||
|
make dev # 启动开发环境(并行启动后端和前端)
|
||||||
|
make build # 构建所有产物
|
||||||
|
make lint # 检查所有代码
|
||||||
|
make clean # 清理所有构建产物
|
||||||
|
|
||||||
|
# 后端开发
|
||||||
make backend-build # 构建后端
|
make backend-build # 构建后端
|
||||||
make backend-run # 运行后端
|
make backend-run # 运行后端
|
||||||
|
make backend-dev # 后端开发模式
|
||||||
make backend-lint # 后端代码检查
|
make backend-lint # 后端代码检查
|
||||||
make backend-migrate-up # 数据库迁移
|
make backend-clean # 清理后端构建产物
|
||||||
|
|
||||||
|
# 数据库操作
|
||||||
|
make backend-db-up # 数据库迁移
|
||||||
|
make backend-db-down # 数据库回滚
|
||||||
|
make backend-db-status # 数据库迁移状态
|
||||||
|
make backend-db-create # 创建新迁移
|
||||||
|
|
||||||
|
# 前端开发
|
||||||
make frontend-build # 构建前端
|
make frontend-build # 构建前端
|
||||||
make frontend-dev # 前端开发模式
|
make frontend-dev # 前端开发模式
|
||||||
make frontend-lint # 前端代码检查
|
make frontend-lint # 前端代码检查
|
||||||
|
make frontend-clean # 清理前端构建产物
|
||||||
```
|
```
|
||||||
|
|
||||||
## 开发规范
|
## 开发规范
|
||||||
|
|||||||
@@ -14,17 +14,63 @@ AI 网关后端服务,提供统一的大模型 API 代理接口。
|
|||||||
- 支持扩展层接口(Models、Embeddings、Rerank)
|
- 支持扩展层接口(Models、Embeddings、Rerank)
|
||||||
- 多供应商配置和路由
|
- 多供应商配置和路由
|
||||||
- 用量统计
|
- 用量统计
|
||||||
- 结构化日志(zap + lumberjack)
|
- 结构化日志(zap + lumberjack + 模块标识)
|
||||||
- YAML 配置管理
|
- YAML 配置管理
|
||||||
- 请求验证
|
- 请求验证
|
||||||
- 中间件支持(请求 ID、日志、恢复、CORS)
|
- 中间件支持(请求 ID、日志、恢复、CORS)
|
||||||
|
|
||||||
|
## 日志规范
|
||||||
|
|
||||||
|
### 模块标识
|
||||||
|
|
||||||
|
每个模块通过依赖注入获取带模块标识的 logger:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func NewProxyHandler(..., logger *zap.Logger) *ProxyHandler {
|
||||||
|
return &ProxyHandler{
|
||||||
|
logger: pkglogger.WithModule(logger, "handler.proxy"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
输出格式:
|
||||||
|
- Console: `INFO [handler.proxy] 处理请求 method=POST path=/v1/chat`
|
||||||
|
- JSON: `{"level":"info","logger":"handler.proxy","msg":"处理请求"}`
|
||||||
|
|
||||||
|
### 模块命名规范
|
||||||
|
|
||||||
|
| 模块 | 命名 |
|
||||||
|
|------|------|
|
||||||
|
| ProxyHandler | `handler.proxy` |
|
||||||
|
| ProviderHandler | `handler.provider` |
|
||||||
|
| Provider Client | `provider.client` |
|
||||||
|
| ConversionEngine | `conversion.engine` |
|
||||||
|
| RoutingCache | `service.routing_cache` |
|
||||||
|
| StatsBuffer | `service.stats_buffer` |
|
||||||
|
| Database | `database` |
|
||||||
|
|
||||||
|
### 标准字段
|
||||||
|
|
||||||
|
使用 `pkg/logger/field.go` 中定义的字段构造函数:
|
||||||
|
|
||||||
|
```go
|
||||||
|
logger.Info("请求开始",
|
||||||
|
pkglogger.Method("POST"),
|
||||||
|
pkglogger.Path("/v1/chat"),
|
||||||
|
pkglogger.RequestID("xxx"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
### GORM 日志
|
||||||
|
|
||||||
|
GORM 日志自动桥接到 zap,SQL 查询映射到 Debug 级别。
|
||||||
|
|
||||||
## 技术栈
|
## 技术栈
|
||||||
|
|
||||||
- **语言**: Go 1.26+
|
- **语言**: Go 1.26+
|
||||||
- **HTTP 框架**: Gin
|
- **HTTP 框架**: Gin
|
||||||
- **ORM**: GORM
|
- **ORM**: GORM
|
||||||
- **数据库**: SQLite
|
- **数据库**: SQLite / MySQL
|
||||||
- **日志**: zap + lumberjack
|
- **日志**: zap + lumberjack
|
||||||
- **配置**: Viper + pflag(多层配置:CLI > 环境变量 > 配置文件 > 默认值)
|
- **配置**: Viper + pflag(多层配置:CLI > 环境变量 > 配置文件 > 默认值)
|
||||||
- **验证**: go-playground/validator/v10
|
- **验证**: go-playground/validator/v10
|
||||||
@@ -105,9 +151,13 @@ backend/
|
|||||||
│ │ ├── errors.go
|
│ │ ├── errors.go
|
||||||
│ │ └── wrap.go
|
│ │ └── wrap.go
|
||||||
│ ├── logger/ # 日志系统
|
│ ├── logger/ # 日志系统
|
||||||
│ │ ├── logger.go
|
│ │ ├── logger.go # 核心初始化
|
||||||
│ │ ├── rotate.go
|
│ │ ├── field.go # 标准字段定义
|
||||||
│ │ └── context.go
|
│ │ ├── module.go # 模块日志器
|
||||||
|
│ │ ├── context.go # Context 辅助函数
|
||||||
|
│ │ ├── gorm.go # GORM 适配器
|
||||||
|
│ │ ├── minimal.go # 最小化 logger
|
||||||
|
│ │ └── rotate.go # 日志轮转
|
||||||
│ ├── modelid/ # 统一模型 ID 工具包
|
│ ├── modelid/ # 统一模型 ID 工具包
|
||||||
│ │ ├── model_id.go
|
│ │ ├── model_id.go
|
||||||
│ │ └── model_id_test.go
|
│ │ └── model_id_test.go
|
||||||
@@ -294,7 +344,14 @@ server:
|
|||||||
write_timeout: 30s
|
write_timeout: 30s
|
||||||
|
|
||||||
database:
|
database:
|
||||||
path: ~/.nex/config.db
|
driver: sqlite # sqlite 或 mysql
|
||||||
|
path: ~/.nex/config.db # SQLite 数据库文件路径
|
||||||
|
# --- MySQL 配置(driver=mysql 时生效)---
|
||||||
|
# host: localhost
|
||||||
|
# port: 3306
|
||||||
|
# user: nex
|
||||||
|
# password: ""
|
||||||
|
# dbname: nex
|
||||||
max_idle_conns: 10
|
max_idle_conns: 10
|
||||||
max_open_conns: 100
|
max_open_conns: 100
|
||||||
conn_max_lifetime: 1h
|
conn_max_lifetime: 1h
|
||||||
@@ -316,6 +373,14 @@ log:
|
|||||||
export NEX_SERVER_PORT=9000
|
export NEX_SERVER_PORT=9000
|
||||||
export NEX_DATABASE_PATH=/data/nex.db
|
export NEX_DATABASE_PATH=/data/nex.db
|
||||||
export NEX_LOG_LEVEL=debug
|
export NEX_LOG_LEVEL=debug
|
||||||
|
|
||||||
|
# MySQL 模式
|
||||||
|
export NEX_DATABASE_DRIVER=mysql
|
||||||
|
export NEX_DATABASE_HOST=db.example.com
|
||||||
|
export NEX_DATABASE_PORT=3306
|
||||||
|
export NEX_DATABASE_USER=nex
|
||||||
|
export NEX_DATABASE_PASSWORD=secret
|
||||||
|
export NEX_DATABASE_DBNAME=nex
|
||||||
```
|
```
|
||||||
|
|
||||||
命名规则:配置路径转大写 + 下划线 + `NEX_` 前缀(如 `server.port` → `NEX_SERVER_PORT`)。
|
命名规则:配置路径转大写 + 下划线 + `NEX_` 前缀(如 `server.port` → `NEX_SERVER_PORT`)。
|
||||||
@@ -332,7 +397,7 @@ export NEX_LOG_LEVEL=debug
|
|||||||
|
|
||||||
```
|
```
|
||||||
服务器: --server-port, --server-read-timeout, --server-write-timeout
|
服务器: --server-port, --server-read-timeout, --server-write-timeout
|
||||||
数据库: --database-path, --database-max-idle-conns, --database-max-open-conns, --database-conn-max-lifetime
|
数据库: --database-driver, --database-path, --database-host, --database-port, --database-user, --database-password, --database-dbname, --database-max-idle-conns, --database-max-open-conns, --database-conn-max-lifetime
|
||||||
日志: --log-level, --log-path, --log-max-size, --log-max-backups, --log-max-age, --log-compress
|
日志: --log-level, --log-path, --log-max-size, --log-max-backups, --log-max-age, --log-compress
|
||||||
通用: --config (指定配置文件路径)
|
通用: --config (指定配置文件路径)
|
||||||
```
|
```
|
||||||
@@ -352,15 +417,20 @@ export NEX_LOG_LEVEL=debug
|
|||||||
# Docker 部署
|
# Docker 部署
|
||||||
docker run -d -e NEX_SERVER_PORT=9000 -e NEX_LOG_LEVEL=info nex-server
|
docker run -d -e NEX_SERVER_PORT=9000 -e NEX_LOG_LEVEL=info nex-server
|
||||||
|
|
||||||
|
# MySQL 模式
|
||||||
|
./server --database-driver mysql --database-host db.example.com --database-user nex --database-password secret --database-dbname nex
|
||||||
|
|
||||||
# 自定义配置文件
|
# 自定义配置文件
|
||||||
./server --config /path/to/custom.yaml
|
./server --config /path/to/custom.yaml
|
||||||
```
|
```
|
||||||
|
|
||||||
数据文件:
|
数据文件:
|
||||||
- `~/.nex/config.yaml` - 配置文件
|
- `~/.nex/config.yaml` - 配置文件
|
||||||
- `~/.nex/config.db` - SQLite 数据库
|
- `~/.nex/config.db` - SQLite 数据库(MySQL 模式下不使用本地数据库文件)
|
||||||
- `~/.nex/log/` - 日志目录
|
- `~/.nex/log/` - 日志目录
|
||||||
|
|
||||||
|
**MySQL 连接说明**:MySQL 连接使用 DSN 格式: `user:password@tcp(host:port)/dbname?charset=utf8mb4&parseTime=true&loc=Local`,最低支持 MySQL 8.0+。
|
||||||
|
|
||||||
## 测试
|
## 测试
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
28
backend/cmd/desktop/dialog_darwin.go
Normal file
28
backend/cmd/desktop/dialog_darwin.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
//go:build darwin
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func showError(title, message string) {
|
||||||
|
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`,
|
||||||
|
escapeAppleScript(message), escapeAppleScript(title))
|
||||||
|
exec.Command("osascript", "-e", script).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAbout() {
|
||||||
|
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
|
||||||
|
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "关于 Nex Gateway"`,
|
||||||
|
escapeAppleScript(message))
|
||||||
|
exec.Command("osascript", "-e", script).Run()
|
||||||
|
}
|
||||||
|
|
||||||
|
func escapeAppleScript(s string) string {
|
||||||
|
s = strings.ReplaceAll(s, "\\", "\\\\")
|
||||||
|
s = strings.ReplaceAll(s, "\"", "\\\"")
|
||||||
|
return s
|
||||||
|
}
|
||||||
88
backend/cmd/desktop/dialog_linux.go
Normal file
88
backend/cmd/desktop/dialog_linux.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
//go:build linux
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dialogToolType int
|
||||||
|
|
||||||
|
const (
|
||||||
|
toolNone dialogToolType = iota
|
||||||
|
toolZenity
|
||||||
|
toolKdialog
|
||||||
|
toolNotifySend
|
||||||
|
toolXmessage
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
dialogTool dialogToolType
|
||||||
|
dialogToolOnce sync.Once
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
dialogToolOnce.Do(detectDialogTool)
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectDialogTool() {
|
||||||
|
tools := []struct {
|
||||||
|
name string
|
||||||
|
typ dialogToolType
|
||||||
|
}{
|
||||||
|
{"zenity", toolZenity},
|
||||||
|
{"kdialog", toolKdialog},
|
||||||
|
{"notify-send", toolNotifySend},
|
||||||
|
{"xmessage", toolXmessage},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tool := range tools {
|
||||||
|
if _, err := exec.LookPath(tool.name); err == nil {
|
||||||
|
dialogTool = tool.typ
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dialogTool = toolNone
|
||||||
|
}
|
||||||
|
|
||||||
|
func showError(title, message string) {
|
||||||
|
switch dialogTool {
|
||||||
|
case toolZenity:
|
||||||
|
exec.Command("zenity", "--error",
|
||||||
|
fmt.Sprintf("--title=%s", title),
|
||||||
|
fmt.Sprintf("--text=%s", message)).Run()
|
||||||
|
case toolKdialog:
|
||||||
|
exec.Command("kdialog", "--error", message, "--title", title).Run()
|
||||||
|
case toolNotifySend:
|
||||||
|
exec.Command("notify-send", "-u", "critical", title, message).Run()
|
||||||
|
case toolXmessage:
|
||||||
|
exec.Command("xmessage", "-center",
|
||||||
|
fmt.Sprintf("%s: %s", title, message)).Run()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "错误: %s: %s\n", title, message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAbout() {
|
||||||
|
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
|
||||||
|
|
||||||
|
switch dialogTool {
|
||||||
|
case toolZenity:
|
||||||
|
exec.Command("zenity", "--info",
|
||||||
|
"--title=关于 Nex Gateway",
|
||||||
|
fmt.Sprintf("--text=%s", message)).Run()
|
||||||
|
case toolKdialog:
|
||||||
|
exec.Command("kdialog", "--msgbox", message, "--title", "关于 Nex Gateway").Run()
|
||||||
|
case toolNotifySend:
|
||||||
|
exec.Command("notify-send", "关于 Nex Gateway", message).Run()
|
||||||
|
case toolXmessage:
|
||||||
|
exec.Command("xmessage", "-center",
|
||||||
|
fmt.Sprintf("关于 Nex Gateway: %s", message)).Run()
|
||||||
|
default:
|
||||||
|
fmt.Fprintf(os.Stderr, "关于 Nex Gateway: %s\n", message)
|
||||||
|
}
|
||||||
|
}
|
||||||
38
backend/cmd/desktop/dialog_windows.go
Normal file
38
backend/cmd/desktop/dialog_windows.go
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MB_ICONERROR = 0x10
|
||||||
|
MB_ICONINFORMATION = 0x40
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
user32 = syscall.NewLazyDLL("user32.dll")
|
||||||
|
procMessageBoxW = user32.NewProc("MessageBoxW")
|
||||||
|
)
|
||||||
|
|
||||||
|
func showError(title, message string) {
|
||||||
|
messageBox(title, message, MB_ICONERROR)
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAbout() {
|
||||||
|
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
|
||||||
|
messageBox("关于 Nex Gateway", message, MB_ICONINFORMATION)
|
||||||
|
}
|
||||||
|
|
||||||
|
func messageBox(title, message string, flags uint) {
|
||||||
|
titlePtr, _ := syscall.UTF16PtrFromString(title)
|
||||||
|
messagePtr, _ := syscall.UTF16PtrFromString(message)
|
||||||
|
procMessageBoxW.Call(
|
||||||
|
0,
|
||||||
|
uintptr(unsafe.Pointer(messagePtr)),
|
||||||
|
uintptr(unsafe.Pointer(titlePtr)),
|
||||||
|
uintptr(flags),
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"log"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
@@ -12,23 +11,18 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"strings"
|
"strings"
|
||||||
"syscall"
|
|
||||||
"time"
|
"time"
|
||||||
"unsafe"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
|
||||||
"github.com/getlantern/systray"
|
"github.com/getlantern/systray"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gofrs/flock"
|
"github.com/gofrs/flock"
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
|
|
||||||
"nex/backend/internal/config"
|
"nex/backend/internal/config"
|
||||||
"nex/backend/internal/conversion"
|
"nex/backend/internal/conversion"
|
||||||
"nex/backend/internal/conversion/anthropic"
|
"nex/backend/internal/conversion/anthropic"
|
||||||
"nex/backend/internal/conversion/openai"
|
"nex/backend/internal/conversion/openai"
|
||||||
|
"nex/backend/internal/database"
|
||||||
"nex/backend/internal/handler"
|
"nex/backend/internal/handler"
|
||||||
"nex/backend/internal/handler/middleware"
|
"nex/backend/internal/handler/middleware"
|
||||||
"nex/backend/internal/provider"
|
"nex/backend/internal/provider"
|
||||||
@@ -49,25 +43,28 @@ var (
|
|||||||
func main() {
|
func main() {
|
||||||
port := 9826
|
port := 9826
|
||||||
|
|
||||||
|
minimalLogger := pkgLogger.NewMinimal()
|
||||||
|
|
||||||
singleLock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway.lock"))
|
singleLock := NewSingletonLock(filepath.Join(os.TempDir(), "nex-gateway.lock"))
|
||||||
if err := singleLock.Lock(); err != nil {
|
if err := singleLock.Lock(); err != nil {
|
||||||
|
minimalLogger.Error("已有 Nex 实例运行")
|
||||||
showError("Nex Gateway", "已有 Nex 实例运行")
|
showError("Nex Gateway", "已有 Nex 实例运行")
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
defer singleLock.Unlock()
|
defer singleLock.Unlock()
|
||||||
|
|
||||||
if err := checkPortAvailable(port); err != nil {
|
if err := checkPortAvailable(port); err != nil {
|
||||||
|
minimalLogger.Error("端口不可用", zap.Error(err))
|
||||||
showError("Nex Gateway", err.Error())
|
showError("Nex Gateway", err.Error())
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
showError("Nex Gateway", fmt.Sprintf("加载配置失败: %v", err))
|
minimalLogger.Fatal("加载配置失败", zap.Error(err))
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
zapLogger, err = pkgLogger.New(pkgLogger.Config{
|
zapLogger, err = pkgLogger.Upgrade(minimalLogger, pkgLogger.Config{
|
||||||
Level: cfg.Log.Level,
|
Level: cfg.Log.Level,
|
||||||
Path: cfg.Log.Path,
|
Path: cfg.Log.Path,
|
||||||
MaxSize: cfg.Log.MaxSize,
|
MaxSize: cfg.Log.MaxSize,
|
||||||
@@ -76,17 +73,17 @@ func main() {
|
|||||||
Compress: cfg.Log.Compress,
|
Compress: cfg.Log.Compress,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
showError("Nex Gateway", fmt.Sprintf("初始化日志失败: %v", err))
|
minimalLogger.Fatal("初始化日志失败", zap.Error(err))
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
defer zapLogger.Sync()
|
defer zapLogger.Sync()
|
||||||
|
|
||||||
db, err := initDatabase(cfg)
|
cfg.PrintSummary(zapLogger)
|
||||||
|
|
||||||
|
db, err := database.Init(&cfg.Database, zapLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
showError("Nex Gateway", fmt.Sprintf("初始化数据库失败: %v", err))
|
zapLogger.Fatal("初始化数据库失败", zap.Error(err))
|
||||||
os.Exit(1)
|
|
||||||
}
|
}
|
||||||
defer closeDB(db)
|
defer database.Close(db)
|
||||||
|
|
||||||
providerRepo := repository.NewProviderRepository(db)
|
providerRepo := repository.NewProviderRepository(db)
|
||||||
modelRepo := repository.NewModelRepository(db)
|
modelRepo := repository.NewModelRepository(db)
|
||||||
@@ -110,16 +107,16 @@ func main() {
|
|||||||
|
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
if err := registry.Register(openai.NewAdapter()); err != nil {
|
if err := registry.Register(openai.NewAdapter()); err != nil {
|
||||||
zapLogger.Fatal("注册 OpenAI 适配器失败", zap.String("error", err.Error()))
|
zapLogger.Fatal("注册 OpenAI 适配器失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
if err := registry.Register(anthropic.NewAdapter()); err != nil {
|
if err := registry.Register(anthropic.NewAdapter()); err != nil {
|
||||||
zapLogger.Fatal("注册 Anthropic 适配器失败", zap.String("error", err.Error()))
|
zapLogger.Fatal("注册 Anthropic 适配器失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
engine := conversion.NewConversionEngine(registry, zapLogger)
|
engine := conversion.NewConversionEngine(registry, zapLogger)
|
||||||
|
|
||||||
providerClient := provider.NewClient()
|
providerClient := provider.NewClient(zapLogger)
|
||||||
|
|
||||||
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService)
|
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService, zapLogger)
|
||||||
providerHandler := handler.NewProviderHandler(providerService)
|
providerHandler := handler.NewProviderHandler(providerService)
|
||||||
modelHandler := handler.NewModelHandler(modelService)
|
modelHandler := handler.NewModelHandler(modelService)
|
||||||
statsHandler := handler.NewStatsHandler(statsService)
|
statsHandler := handler.NewStatsHandler(statsService)
|
||||||
@@ -161,76 +158,6 @@ func main() {
|
|||||||
setupSystray(port)
|
setupSystray(port)
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDatabase(cfg *config.Config) (*gorm.DB, error) {
|
|
||||||
dbDir := filepath.Dir(cfg.Database.Path)
|
|
||||||
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
|
||||||
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
db, err := gorm.Open(sqlite.Open(cfg.Database.Path), &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runMigrations(db); err != nil {
|
|
||||||
return nil, fmt.Errorf("数据库迁移失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil {
|
|
||||||
log.Printf("警告: 启用 WAL 模式失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
|
|
||||||
sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)
|
|
||||||
sqlDB.SetConnMaxLifetime(cfg.Database.ConnMaxLifetime)
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runMigrations(db *gorm.DB) error {
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
migrationsDir := getMigrationsDir()
|
|
||||||
if _, err := os.Stat(migrationsDir); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("迁移目录不存在: %s", migrationsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
goose.SetDialect("sqlite3")
|
|
||||||
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMigrationsDir() string {
|
|
||||||
_, filename, _, ok := runtime.Caller(0)
|
|
||||||
if ok {
|
|
||||||
dir := filepath.Join(filepath.Dir(filename), "..", "..", "migrations")
|
|
||||||
if abs, err := filepath.Abs(dir); err == nil {
|
|
||||||
return abs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "./migrations"
|
|
||||||
}
|
|
||||||
|
|
||||||
func closeDB(db *gorm.DB) {
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sqlDB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHandler *handler.ProviderHandler, modelHandler *handler.ModelHandler, statsHandler *handler.StatsHandler) {
|
func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHandler *handler.ProviderHandler, modelHandler *handler.ModelHandler, statsHandler *handler.StatsHandler) {
|
||||||
r.Any("/v1/*path", proxyHandler.HandleProxy)
|
r.Any("/v1/*path", proxyHandler.HandleProxy)
|
||||||
|
|
||||||
@@ -447,49 +374,3 @@ func openBrowser(url string) error {
|
|||||||
|
|
||||||
return cmd.Start()
|
return cmd.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func showError(title, message string) {
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "%s"`, message, title)
|
|
||||||
exec.Command("osascript", "-e", script).Run()
|
|
||||||
case "windows":
|
|
||||||
messageBox(title, message, MB_ICONERROR)
|
|
||||||
case "linux":
|
|
||||||
exec.Command("zenity", "--error", fmt.Sprintf("--title=%s", title), fmt.Sprintf("--text=%s", message)).Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func showAbout() {
|
|
||||||
message := "Nex Gateway\n\nAI Gateway - 统一的大模型 API 网关\n\nhttps://github.com/nex/gateway"
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
script := fmt.Sprintf(`display dialog "%s" buttons {"OK"} default button "OK" with title "关于 Nex Gateway"`, message)
|
|
||||||
exec.Command("osascript", "-e", script).Run()
|
|
||||||
case "windows":
|
|
||||||
messageBox("关于 Nex Gateway", message, MB_ICONINFORMATION)
|
|
||||||
case "linux":
|
|
||||||
exec.Command("zenity", "--info", "--title=关于 Nex Gateway", fmt.Sprintf("--text=%s", message)).Run()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
|
||||||
MB_ICONERROR = 0x10
|
|
||||||
MB_ICONINFORMATION = 0x40
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
user32 = syscall.NewLazyDLL("user32.dll")
|
|
||||||
procMessageBoxW = user32.NewProc("MessageBoxW")
|
|
||||||
)
|
|
||||||
|
|
||||||
func messageBox(title, message string, flags uint) {
|
|
||||||
titlePtr, _ := syscall.UTF16PtrFromString(title)
|
|
||||||
messagePtr, _ := syscall.UTF16PtrFromString(message)
|
|
||||||
procMessageBoxW.Call(
|
|
||||||
0,
|
|
||||||
uintptr(unsafe.Pointer(messagePtr)),
|
|
||||||
uintptr(unsafe.Pointer(titlePtr)),
|
|
||||||
uintptr(flags),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,30 +1,19 @@
|
|||||||
|
//go:build windows
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"runtime"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestMessageBoxW_WindowsOnly(t *testing.T) {
|
func TestMessageBoxW_WindowsOnly(t *testing.T) {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("MessageBoxW 仅在 Windows 上测试")
|
|
||||||
}
|
|
||||||
|
|
||||||
messageBox("测试标题", "测试消息", MB_ICONINFORMATION)
|
messageBox("测试标题", "测试消息", MB_ICONINFORMATION)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShowError_WindowsBranch(t *testing.T) {
|
func TestShowError_WindowsBranch(t *testing.T) {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Windows 原生对话框测试仅在 Windows 上运行")
|
|
||||||
}
|
|
||||||
|
|
||||||
showError("测试错误", "这是一条测试错误消息")
|
showError("测试错误", "这是一条测试错误消息")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestShowAbout_WindowsBranch(t *testing.T) {
|
func TestShowAbout_WindowsBranch(t *testing.T) {
|
||||||
if runtime.GOOS != "windows" {
|
|
||||||
t.Skip("Windows 原生对话框测试仅在 Windows 上运行")
|
|
||||||
}
|
|
||||||
|
|
||||||
showAbout()
|
showAbout()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,26 +3,20 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/pressly/goose/v3"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/driver/sqlite"
|
|
||||||
"gorm.io/gorm"
|
|
||||||
"gorm.io/gorm/logger"
|
|
||||||
|
|
||||||
"nex/backend/internal/config"
|
"nex/backend/internal/config"
|
||||||
"nex/backend/internal/conversion"
|
"nex/backend/internal/conversion"
|
||||||
"nex/backend/internal/conversion/anthropic"
|
"nex/backend/internal/conversion/anthropic"
|
||||||
"nex/backend/internal/conversion/openai"
|
"nex/backend/internal/conversion/openai"
|
||||||
|
"nex/backend/internal/database"
|
||||||
"nex/backend/internal/handler"
|
"nex/backend/internal/handler"
|
||||||
"nex/backend/internal/handler/middleware"
|
"nex/backend/internal/handler/middleware"
|
||||||
"nex/backend/internal/provider"
|
"nex/backend/internal/provider"
|
||||||
@@ -32,17 +26,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 1. 加载配置(已包含 CLI 参数解析、环境变量绑定、配置文件读取和验证)
|
minimalLogger := pkgLogger.NewMinimal()
|
||||||
|
|
||||||
cfg, err := config.LoadConfig()
|
cfg, err := config.LoadConfig()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("加载配置失败: %v", err)
|
minimalLogger.Fatal("加载配置失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. 打印配置摘要
|
zapLogger, err := pkgLogger.Upgrade(minimalLogger, pkgLogger.Config{
|
||||||
cfg.PrintSummary()
|
|
||||||
|
|
||||||
// 3. 初始化日志
|
|
||||||
zapLogger, err := pkgLogger.New(pkgLogger.Config{
|
|
||||||
Level: cfg.Log.Level,
|
Level: cfg.Log.Level,
|
||||||
Path: cfg.Log.Path,
|
Path: cfg.Log.Path,
|
||||||
MaxSize: cfg.Log.MaxSize,
|
MaxSize: cfg.Log.MaxSize,
|
||||||
@@ -51,60 +42,53 @@ func main() {
|
|||||||
Compress: cfg.Log.Compress,
|
Compress: cfg.Log.Compress,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("初始化日志失败: %v", err)
|
minimalLogger.Fatal("初始化日志失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
defer zapLogger.Sync()
|
defer zapLogger.Sync()
|
||||||
|
|
||||||
// 3. 初始化数据库
|
cfg.PrintSummary(zapLogger)
|
||||||
db, err := initDatabase(cfg)
|
|
||||||
if err != nil {
|
db, err := database.Init(&cfg.Database, zapLogger)
|
||||||
zapLogger.Fatal("初始化数据库失败", zap.String("error", err.Error()))
|
if err != nil {
|
||||||
}
|
zapLogger.Fatal("初始化数据库失败", zap.Error(err))
|
||||||
defer closeDB(db)
|
}
|
||||||
|
defer database.Close(db)
|
||||||
|
|
||||||
// 4. 初始化 repository 层
|
|
||||||
providerRepo := repository.NewProviderRepository(db)
|
providerRepo := repository.NewProviderRepository(db)
|
||||||
modelRepo := repository.NewModelRepository(db)
|
modelRepo := repository.NewModelRepository(db)
|
||||||
statsRepo := repository.NewStatsRepository(db)
|
statsRepo := repository.NewStatsRepository(db)
|
||||||
|
|
||||||
// 5. 初始化缓存
|
|
||||||
routingCache := service.NewRoutingCache(modelRepo, providerRepo, zapLogger)
|
routingCache := service.NewRoutingCache(modelRepo, providerRepo, zapLogger)
|
||||||
if err := routingCache.Preload(); err != nil {
|
if err := routingCache.Preload(); err != nil {
|
||||||
zapLogger.Warn("缓存预热失败,将使用懒加载", zap.Error(err))
|
zapLogger.Warn("缓存预热失败,将使用懒加载", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
// 6. 初始化统计缓冲
|
|
||||||
statsBuffer := service.NewStatsBuffer(statsRepo, zapLogger,
|
statsBuffer := service.NewStatsBuffer(statsRepo, zapLogger,
|
||||||
service.WithFlushInterval(5*time.Second),
|
service.WithFlushInterval(5*time.Second),
|
||||||
service.WithFlushThreshold(100))
|
service.WithFlushThreshold(100))
|
||||||
statsBuffer.Start()
|
statsBuffer.Start()
|
||||||
|
|
||||||
// 7. 初始化 service 层
|
|
||||||
providerService := service.NewProviderService(providerRepo, modelRepo, routingCache)
|
providerService := service.NewProviderService(providerRepo, modelRepo, routingCache)
|
||||||
modelService := service.NewModelService(modelRepo, providerRepo, routingCache)
|
modelService := service.NewModelService(modelRepo, providerRepo, routingCache)
|
||||||
routingService := service.NewRoutingService(routingCache)
|
routingService := service.NewRoutingService(routingCache)
|
||||||
statsService := service.NewStatsService(statsRepo, statsBuffer)
|
statsService := service.NewStatsService(statsRepo, statsBuffer)
|
||||||
|
|
||||||
// 8. 创建 ConversionEngine
|
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
if err := registry.Register(openai.NewAdapter()); err != nil {
|
if err := registry.Register(openai.NewAdapter()); err != nil {
|
||||||
zapLogger.Fatal("注册 OpenAI 适配器失败", zap.String("error", err.Error()))
|
zapLogger.Fatal("注册 OpenAI 适配器失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
if err := registry.Register(anthropic.NewAdapter()); err != nil {
|
if err := registry.Register(anthropic.NewAdapter()); err != nil {
|
||||||
zapLogger.Fatal("注册 Anthropic 适配器失败", zap.String("error", err.Error()))
|
zapLogger.Fatal("注册 Anthropic 适配器失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
engine := conversion.NewConversionEngine(registry, zapLogger)
|
engine := conversion.NewConversionEngine(registry, zapLogger)
|
||||||
|
|
||||||
// 9. 初始化 provider client
|
providerClient := provider.NewClient(zapLogger)
|
||||||
providerClient := provider.NewClient()
|
|
||||||
|
|
||||||
// 10. 初始化 handler 层
|
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService, zapLogger)
|
||||||
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService)
|
|
||||||
providerHandler := handler.NewProviderHandler(providerService)
|
providerHandler := handler.NewProviderHandler(providerService)
|
||||||
modelHandler := handler.NewModelHandler(modelService)
|
modelHandler := handler.NewModelHandler(modelService)
|
||||||
statsHandler := handler.NewStatsHandler(statsService)
|
statsHandler := handler.NewStatsHandler(statsService)
|
||||||
|
|
||||||
// 11. 创建 Gin 引擎
|
|
||||||
gin.SetMode(gin.ReleaseMode)
|
gin.SetMode(gin.ReleaseMode)
|
||||||
r := gin.New()
|
r := gin.New()
|
||||||
|
|
||||||
@@ -115,9 +99,8 @@ func main() {
|
|||||||
|
|
||||||
setupRoutes(r, proxyHandler, providerHandler, modelHandler, statsHandler)
|
setupRoutes(r, proxyHandler, providerHandler, modelHandler, statsHandler)
|
||||||
|
|
||||||
// 12. 启动服务器
|
|
||||||
srv := &http.Server{
|
srv := &http.Server{
|
||||||
Addr: formatAddr(cfg.Server.Port),
|
Addr: fmt.Sprintf(":%d", cfg.Server.Port),
|
||||||
Handler: r,
|
Handler: r,
|
||||||
ReadTimeout: cfg.Server.ReadTimeout,
|
ReadTimeout: cfg.Server.ReadTimeout,
|
||||||
WriteTimeout: cfg.Server.WriteTimeout,
|
WriteTimeout: cfg.Server.WriteTimeout,
|
||||||
@@ -126,7 +109,7 @@ func main() {
|
|||||||
go func() {
|
go func() {
|
||||||
zapLogger.Info("AI Gateway 启动", zap.String("addr", srv.Addr))
|
zapLogger.Info("AI Gateway 启动", zap.String("addr", srv.Addr))
|
||||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
zapLogger.Fatal("服务器启动失败", zap.String("error", err.Error()))
|
zapLogger.Fatal("服务器启动失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
@@ -140,7 +123,7 @@ func main() {
|
|||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
if err := srv.Shutdown(ctx); err != nil {
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
zapLogger.Fatal("服务器强制关闭", zap.String("error", err.Error()))
|
zapLogger.Fatal("服务器强制关闭", zap.Error(err))
|
||||||
}
|
}
|
||||||
|
|
||||||
statsBuffer.Stop()
|
statsBuffer.Stop()
|
||||||
@@ -148,83 +131,9 @@ func main() {
|
|||||||
zapLogger.Info("服务器已关闭")
|
zapLogger.Info("服务器已关闭")
|
||||||
}
|
}
|
||||||
|
|
||||||
func initDatabase(cfg *config.Config) (*gorm.DB, error) {
|
|
||||||
db, err := gorm.Open(sqlite.Open(cfg.Database.Path), &gorm.Config{
|
|
||||||
Logger: logger.Default.LogMode(logger.Info),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := runMigrations(db); err != nil {
|
|
||||||
return nil, fmt.Errorf("数据库迁移失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil {
|
|
||||||
log.Printf("警告: 启用 WAL 模式失败: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sqlDB.SetMaxIdleConns(cfg.Database.MaxIdleConns)
|
|
||||||
sqlDB.SetMaxOpenConns(cfg.Database.MaxOpenConns)
|
|
||||||
sqlDB.SetConnMaxLifetime(cfg.Database.ConnMaxLifetime)
|
|
||||||
|
|
||||||
log.Printf("数据库连接池配置: MaxIdle=%d, MaxOpen=%d, MaxLifetime=%v",
|
|
||||||
cfg.Database.MaxIdleConns, cfg.Database.MaxOpenConns, cfg.Database.ConnMaxLifetime)
|
|
||||||
|
|
||||||
return db, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runMigrations(db *gorm.DB) error {
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
migrationsDir := getMigrationsDir()
|
|
||||||
if _, err := os.Stat(migrationsDir); os.IsNotExist(err) {
|
|
||||||
return fmt.Errorf("迁移目录不存在: %s", migrationsDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
goose.SetDialect("sqlite3")
|
|
||||||
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getMigrationsDir() string {
|
|
||||||
_, filename, _, ok := runtime.Caller(0)
|
|
||||||
if ok {
|
|
||||||
dir := filepath.Join(filepath.Dir(filename), "..", "..", "migrations")
|
|
||||||
if abs, err := filepath.Abs(dir); err == nil {
|
|
||||||
return abs
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return "./migrations"
|
|
||||||
}
|
|
||||||
|
|
||||||
func closeDB(db *gorm.DB) {
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
sqlDB.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func formatAddr(port int) string {
|
|
||||||
return fmt.Sprintf(":%d", port)
|
|
||||||
}
|
|
||||||
|
|
||||||
func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHandler *handler.ProviderHandler, modelHandler *handler.ModelHandler, statsHandler *handler.StatsHandler) {
|
func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHandler *handler.ProviderHandler, modelHandler *handler.ModelHandler, statsHandler *handler.StatsHandler) {
|
||||||
// 统一代理入口: /{protocol}/{path}
|
|
||||||
r.Any("/:protocol/*path", proxyHandler.HandleProxy)
|
r.Any("/:protocol/*path", proxyHandler.HandleProxy)
|
||||||
|
|
||||||
// 供应商管理 API
|
|
||||||
providers := r.Group("/api/providers")
|
providers := r.Group("/api/providers")
|
||||||
{
|
{
|
||||||
providers.GET("", providerHandler.ListProviders)
|
providers.GET("", providerHandler.ListProviders)
|
||||||
@@ -234,7 +143,6 @@ func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHand
|
|||||||
providers.DELETE("/:id", providerHandler.DeleteProvider)
|
providers.DELETE("/:id", providerHandler.DeleteProvider)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 模型管理 API
|
|
||||||
models := r.Group("/api/models")
|
models := r.Group("/api/models")
|
||||||
{
|
{
|
||||||
models.GET("", modelHandler.ListModels)
|
models.GET("", modelHandler.ListModels)
|
||||||
@@ -244,14 +152,12 @@ func setupRoutes(r *gin.Engine, proxyHandler *handler.ProxyHandler, providerHand
|
|||||||
models.DELETE("/:id", modelHandler.DeleteModel)
|
models.DELETE("/:id", modelHandler.DeleteModel)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 统计查询 API
|
|
||||||
stats := r.Group("/api/stats")
|
stats := r.Group("/api/stats")
|
||||||
{
|
{
|
||||||
stats.GET("", statsHandler.GetStats)
|
stats.GET("", statsHandler.GetStats)
|
||||||
stats.GET("/aggregate", statsHandler.AggregateStats)
|
stats.GET("/aggregate", statsHandler.AggregateStats)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 健康检查
|
|
||||||
r.GET("/health", func(c *gin.Context) {
|
r.GET("/health", func(c *gin.Context) {
|
||||||
c.JSON(200, gin.H{"status": "ok"})
|
c.JSON(200, gin.H{"status": "ok"})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ require (
|
|||||||
go.uber.org/zap v1.27.1
|
go.uber.org/zap v1.27.1
|
||||||
gopkg.in/lumberjack.v2 v2.0.0
|
gopkg.in/lumberjack.v2 v2.0.0
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/mysql v1.6.0
|
||||||
gorm.io/driver/sqlite v1.6.0
|
gorm.io/driver/sqlite v1.6.0
|
||||||
gorm.io/gorm v1.31.1
|
gorm.io/gorm v1.31.1
|
||||||
nex/embedfs v0.0.0-00010101000000-000000000000
|
nex/embedfs v0.0.0-00010101000000-000000000000
|
||||||
@@ -32,6 +33,7 @@ require (
|
|||||||
require (
|
require (
|
||||||
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
4d63.com/gocheckcompilerdirectives v1.3.0 // indirect
|
||||||
4d63.com/gochecknoglobals v0.2.2 // indirect
|
4d63.com/gochecknoglobals v0.2.2 // indirect
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
github.com/4meepo/tagalign v1.4.2 // indirect
|
github.com/4meepo/tagalign v1.4.2 // indirect
|
||||||
github.com/Abirdcfly/dupword v0.1.3 // indirect
|
github.com/Abirdcfly/dupword v0.1.3 // indirect
|
||||||
github.com/Antonboom/errname v1.0.0 // indirect
|
github.com/Antonboom/errname v1.0.0 // indirect
|
||||||
@@ -90,6 +92,7 @@ require (
|
|||||||
github.com/go-critic/go-critic v0.12.0 // indirect
|
github.com/go-critic/go-critic v0.12.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 // indirect
|
||||||
github.com/go-stack/stack v1.8.0 // indirect
|
github.com/go-stack/stack v1.8.0 // indirect
|
||||||
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
github.com/go-toolsmith/astcast v1.1.0 // indirect
|
||||||
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
github.com/go-toolsmith/astcopy v1.1.0 // indirect
|
||||||
|
|||||||
@@ -35,6 +35,8 @@ cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohl
|
|||||||
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs=
|
||||||
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0=
|
||||||
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
|
github.com/4meepo/tagalign v1.4.2 h1:0hcLHPGMjDyM1gHG58cS73aQF8J4TdVR96TZViorO9E=
|
||||||
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
|
github.com/4meepo/tagalign v1.4.2/go.mod h1:+p4aMyFM+ra7nb41CnFG6aSDXqRxU/w1VQqScKqDARI=
|
||||||
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
|
github.com/Abirdcfly/dupword v0.1.3 h1:9Pa1NuAsZvpFPi9Pqkd93I7LIYRURj+A//dFd5tgBeE=
|
||||||
@@ -206,6 +208,8 @@ github.com/go-playground/validator/v10 v10.30.2 h1:JiFIMtSSHb2/XBUbWM4i/MpeQm9ZK
|
|||||||
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
github.com/go-playground/validator/v10 v10.30.2/go.mod h1:mAf2pIOVXjTEBrwUMGKkCWKKPs9NheYGabeB04txQSc=
|
||||||
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
github.com/go-quicktest/qt v1.101.0 h1:O1K29Txy5P2OK0dGo59b7b0LR6wKfIhttaAhHUyn7eI=
|
||||||
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
github.com/go-quicktest/qt v1.101.0/go.mod h1:14Bz/f7NwaXPtdYEgzsx46kqSxVwTbzVZsDC26tQJow=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
github.com/go-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
@@ -1052,6 +1056,8 @@ gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
|||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gorm.io/driver/mysql v1.6.0 h1:eNbLmNTpPpTOVZi8MMxCi2aaIm0ZpInbORNXDwyLGvg=
|
||||||
|
gorm.io/driver/mysql v1.6.0/go.mod h1:D/oCC2GWK3M/dqoLxnOlaNKmXz8WNTfcS9y5ovaSqKo=
|
||||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import (
|
|||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
appErrors "nex/backend/pkg/errors"
|
appErrors "nex/backend/pkg/errors"
|
||||||
@@ -32,7 +33,13 @@ type ServerConfig struct {
|
|||||||
|
|
||||||
// DatabaseConfig 数据库配置
|
// DatabaseConfig 数据库配置
|
||||||
type DatabaseConfig struct {
|
type DatabaseConfig struct {
|
||||||
Path string `yaml:"path" mapstructure:"path" validate:"required"`
|
Driver string `yaml:"driver" mapstructure:"driver" validate:"required,oneof=sqlite mysql"`
|
||||||
|
Path string `yaml:"path" mapstructure:"path" validate:"required_if=Driver sqlite"`
|
||||||
|
Host string `yaml:"host" mapstructure:"host" validate:"required_if=Driver mysql"`
|
||||||
|
Port int `yaml:"port" mapstructure:"port" validate:"required_if=Driver mysql,min=1,max=65535"`
|
||||||
|
User string `yaml:"user" mapstructure:"user" validate:"required_if=Driver mysql"`
|
||||||
|
Password string `yaml:"password" mapstructure:"password"`
|
||||||
|
DBName string `yaml:"dbname" mapstructure:"dbname" validate:"required_if=Driver mysql"`
|
||||||
MaxIdleConns int `yaml:"max_idle_conns" mapstructure:"max_idle_conns" validate:"required,min=1"`
|
MaxIdleConns int `yaml:"max_idle_conns" mapstructure:"max_idle_conns" validate:"required,min=1"`
|
||||||
MaxOpenConns int `yaml:"max_open_conns" mapstructure:"max_open_conns" validate:"required,min=1"`
|
MaxOpenConns int `yaml:"max_open_conns" mapstructure:"max_open_conns" validate:"required,min=1"`
|
||||||
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime" mapstructure:"conn_max_lifetime" validate:"required"`
|
ConnMaxLifetime time.Duration `yaml:"conn_max_lifetime" mapstructure:"conn_max_lifetime" validate:"required"`
|
||||||
@@ -61,7 +68,13 @@ func DefaultConfig() *Config {
|
|||||||
WriteTimeout: 30 * time.Second,
|
WriteTimeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
|
Driver: "sqlite",
|
||||||
Path: filepath.Join(nexDir, "config.db"),
|
Path: filepath.Join(nexDir, "config.db"),
|
||||||
|
Host: "",
|
||||||
|
Port: 3306,
|
||||||
|
User: "",
|
||||||
|
Password: "",
|
||||||
|
DBName: "nex",
|
||||||
MaxIdleConns: 10,
|
MaxIdleConns: 10,
|
||||||
MaxOpenConns: 100,
|
MaxOpenConns: 100,
|
||||||
ConnMaxLifetime: 1 * time.Hour,
|
ConnMaxLifetime: 1 * time.Hour,
|
||||||
@@ -117,7 +130,13 @@ func setupDefaults(v *viper.Viper) {
|
|||||||
v.SetDefault("server.read_timeout", "30s")
|
v.SetDefault("server.read_timeout", "30s")
|
||||||
v.SetDefault("server.write_timeout", "30s")
|
v.SetDefault("server.write_timeout", "30s")
|
||||||
|
|
||||||
|
v.SetDefault("database.driver", "sqlite")
|
||||||
v.SetDefault("database.path", filepath.Join(nexDir, "config.db"))
|
v.SetDefault("database.path", filepath.Join(nexDir, "config.db"))
|
||||||
|
v.SetDefault("database.host", "")
|
||||||
|
v.SetDefault("database.port", 3306)
|
||||||
|
v.SetDefault("database.user", "")
|
||||||
|
v.SetDefault("database.password", "")
|
||||||
|
v.SetDefault("database.dbname", "nex")
|
||||||
v.SetDefault("database.max_idle_conns", 10)
|
v.SetDefault("database.max_idle_conns", 10)
|
||||||
v.SetDefault("database.max_open_conns", 100)
|
v.SetDefault("database.max_open_conns", 100)
|
||||||
v.SetDefault("database.conn_max_lifetime", "1h")
|
v.SetDefault("database.conn_max_lifetime", "1h")
|
||||||
@@ -138,7 +157,13 @@ func setupFlags(v *viper.Viper, flagSet *pflag.FlagSet) {
|
|||||||
flagSet.Duration("server-read-timeout", 0, "读超时")
|
flagSet.Duration("server-read-timeout", 0, "读超时")
|
||||||
flagSet.Duration("server-write-timeout", 0, "写超时")
|
flagSet.Duration("server-write-timeout", 0, "写超时")
|
||||||
|
|
||||||
|
flagSet.String("database-driver", "", "数据库驱动:sqlite/mysql")
|
||||||
flagSet.String("database-path", "", "数据库文件路径")
|
flagSet.String("database-path", "", "数据库文件路径")
|
||||||
|
flagSet.String("database-host", "", "MySQL 主机地址")
|
||||||
|
flagSet.Int("database-port", 0, "MySQL 端口")
|
||||||
|
flagSet.String("database-user", "", "MySQL 用户名")
|
||||||
|
flagSet.String("database-password", "", "MySQL 密码")
|
||||||
|
flagSet.String("database-dbname", "", "MySQL 数据库名")
|
||||||
flagSet.Int("database-max-idle-conns", 0, "最大空闲连接数")
|
flagSet.Int("database-max-idle-conns", 0, "最大空闲连接数")
|
||||||
flagSet.Int("database-max-open-conns", 0, "最大打开连接数")
|
flagSet.Int("database-max-open-conns", 0, "最大打开连接数")
|
||||||
flagSet.Duration("database-conn-max-lifetime", 0, "连接最大生命周期")
|
flagSet.Duration("database-conn-max-lifetime", 0, "连接最大生命周期")
|
||||||
@@ -156,7 +181,13 @@ func setupFlags(v *viper.Viper, flagSet *pflag.FlagSet) {
|
|||||||
v.BindPFlag("server.read_timeout", flagSet.Lookup("server-read-timeout"))
|
v.BindPFlag("server.read_timeout", flagSet.Lookup("server-read-timeout"))
|
||||||
v.BindPFlag("server.write_timeout", flagSet.Lookup("server-write-timeout"))
|
v.BindPFlag("server.write_timeout", flagSet.Lookup("server-write-timeout"))
|
||||||
|
|
||||||
|
v.BindPFlag("database.driver", flagSet.Lookup("database-driver"))
|
||||||
v.BindPFlag("database.path", flagSet.Lookup("database-path"))
|
v.BindPFlag("database.path", flagSet.Lookup("database-path"))
|
||||||
|
v.BindPFlag("database.host", flagSet.Lookup("database-host"))
|
||||||
|
v.BindPFlag("database.port", flagSet.Lookup("database-port"))
|
||||||
|
v.BindPFlag("database.user", flagSet.Lookup("database-user"))
|
||||||
|
v.BindPFlag("database.password", flagSet.Lookup("database-password"))
|
||||||
|
v.BindPFlag("database.dbname", flagSet.Lookup("database-dbname"))
|
||||||
v.BindPFlag("database.max_idle_conns", flagSet.Lookup("database-max-idle-conns"))
|
v.BindPFlag("database.max_idle_conns", flagSet.Lookup("database-max-idle-conns"))
|
||||||
v.BindPFlag("database.max_open_conns", flagSet.Lookup("database-max-open-conns"))
|
v.BindPFlag("database.max_open_conns", flagSet.Lookup("database-max-open-conns"))
|
||||||
v.BindPFlag("database.conn_max_lifetime", flagSet.Lookup("database-conn-max-lifetime"))
|
v.BindPFlag("database.conn_max_lifetime", flagSet.Lookup("database-conn-max-lifetime"))
|
||||||
@@ -268,7 +299,7 @@ func SaveConfig(cfg *Config) error {
|
|||||||
return appErrors.Wrap(appErrors.ErrInternal, err)
|
return appErrors.Wrap(appErrors.ErrInternal, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return os.WriteFile(configPath, data, 0644)
|
return os.WriteFile(configPath, data, 0600)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the config
|
// Validate validates the config
|
||||||
@@ -281,16 +312,24 @@ func (c *Config) Validate() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// PrintSummary 打印配置摘要
|
// PrintSummary 打印配置摘要
|
||||||
func (c *Config) PrintSummary() {
|
func (c *Config) PrintSummary(logger *zap.Logger) {
|
||||||
fmt.Println("\nAI Gateway 启动配置")
|
logger.Info("AI Gateway 启动配置",
|
||||||
fmt.Println("==================")
|
zap.Int("server_port", c.Server.Port),
|
||||||
fmt.Printf("服务器端口: %d\n", c.Server.Port)
|
zap.String("database_driver", c.Database.Driver),
|
||||||
fmt.Printf("数据库路径: %s\n", c.Database.Path)
|
zap.String("log_level", c.Log.Level),
|
||||||
fmt.Printf("日志级别: %s\n", c.Log.Level)
|
)
|
||||||
fmt.Println("\n配置来源:")
|
|
||||||
configPath, _ := GetConfigPath()
|
if c.Database.Driver == "mysql" {
|
||||||
fmt.Printf(" 配置文件: %s\n", configPath)
|
logger.Info("数据库配置",
|
||||||
fmt.Println(" 环境变量: 待统计")
|
zap.String("driver", "mysql"),
|
||||||
fmt.Println(" CLI 参数: 待统计")
|
zap.String("host", c.Database.Host),
|
||||||
fmt.Println()
|
zap.Int("port", c.Database.Port),
|
||||||
|
zap.String("database", c.Database.DBName),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
logger.Info("数据库配置",
|
||||||
|
zap.String("driver", "sqlite"),
|
||||||
|
zap.String("path", c.Database.Path),
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -19,6 +20,12 @@ func TestDefaultConfig(t *testing.T) {
|
|||||||
assert.Equal(t, 30*time.Second, cfg.Server.ReadTimeout)
|
assert.Equal(t, 30*time.Second, cfg.Server.ReadTimeout)
|
||||||
assert.Equal(t, 30*time.Second, cfg.Server.WriteTimeout)
|
assert.Equal(t, 30*time.Second, cfg.Server.WriteTimeout)
|
||||||
|
|
||||||
|
assert.Equal(t, "sqlite", cfg.Database.Driver)
|
||||||
|
assert.Equal(t, "", cfg.Database.Host)
|
||||||
|
assert.Equal(t, 3306, cfg.Database.Port)
|
||||||
|
assert.Equal(t, "", cfg.Database.User)
|
||||||
|
assert.Equal(t, "", cfg.Database.Password)
|
||||||
|
assert.Equal(t, "nex", cfg.Database.DBName)
|
||||||
assert.Equal(t, 10, cfg.Database.MaxIdleConns)
|
assert.Equal(t, 10, cfg.Database.MaxIdleConns)
|
||||||
assert.Equal(t, 100, cfg.Database.MaxOpenConns)
|
assert.Equal(t, 100, cfg.Database.MaxOpenConns)
|
||||||
assert.Equal(t, 1*time.Hour, cfg.Database.ConnMaxLifetime)
|
assert.Equal(t, 1*time.Hour, cfg.Database.ConnMaxLifetime)
|
||||||
@@ -86,11 +93,76 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
wantErr: false,
|
wantErr: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "数据库路径为空无效",
|
name: "SQLite模式路径为空无效",
|
||||||
modify: func(c *Config) { c.Database.Path = "" },
|
modify: func(c *Config) { c.Database.Path = "" },
|
||||||
wantErr: true,
|
wantErr: true,
|
||||||
errMsg: "配置验证失败",
|
errMsg: "配置验证失败",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "driver值不合法",
|
||||||
|
modify: func(c *Config) { c.Database.Driver = "postgres" },
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "配置验证失败",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL配置有效",
|
||||||
|
modify: func(c *Config) {
|
||||||
|
c.Database.Driver = "mysql"
|
||||||
|
c.Database.Host = "localhost"
|
||||||
|
c.Database.Port = 3306
|
||||||
|
c.Database.User = "root"
|
||||||
|
c.Database.DBName = "nex"
|
||||||
|
c.Database.Path = ""
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL模式host为空无效",
|
||||||
|
modify: func(c *Config) {
|
||||||
|
c.Database.Driver = "mysql"
|
||||||
|
c.Database.Host = ""
|
||||||
|
c.Database.User = "root"
|
||||||
|
c.Database.DBName = "nex"
|
||||||
|
c.Database.Path = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "配置验证失败",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL模式user为空无效",
|
||||||
|
modify: func(c *Config) {
|
||||||
|
c.Database.Driver = "mysql"
|
||||||
|
c.Database.Host = "localhost"
|
||||||
|
c.Database.User = ""
|
||||||
|
c.Database.DBName = "nex"
|
||||||
|
c.Database.Path = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "配置验证失败",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL模式dbname为空无效",
|
||||||
|
modify: func(c *Config) {
|
||||||
|
c.Database.Driver = "mysql"
|
||||||
|
c.Database.Host = "localhost"
|
||||||
|
c.Database.User = "root"
|
||||||
|
c.Database.DBName = ""
|
||||||
|
c.Database.Path = ""
|
||||||
|
},
|
||||||
|
wantErr: true,
|
||||||
|
errMsg: "配置验证失败",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MySQL模式忽略path字段",
|
||||||
|
modify: func(c *Config) {
|
||||||
|
c.Database.Driver = "mysql"
|
||||||
|
c.Database.Host = "localhost"
|
||||||
|
c.Database.User = "root"
|
||||||
|
c.Database.DBName = "nex"
|
||||||
|
c.Database.Path = ""
|
||||||
|
},
|
||||||
|
wantErr: false,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -100,7 +172,9 @@ func TestConfig_Validate(t *testing.T) {
|
|||||||
err := cfg.Validate()
|
err := cfg.Validate()
|
||||||
if tt.wantErr {
|
if tt.wantErr {
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
|
if err != nil {
|
||||||
assert.Contains(t, err.Error(), tt.errMsg)
|
assert.Contains(t, err.Error(), tt.errMsg)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
}
|
}
|
||||||
@@ -140,7 +214,10 @@ func TestSaveAndLoadConfig(t *testing.T) {
|
|||||||
WriteTimeout: 20 * time.Second,
|
WriteTimeout: 20 * time.Second,
|
||||||
},
|
},
|
||||||
Database: DatabaseConfig{
|
Database: DatabaseConfig{
|
||||||
|
Driver: "sqlite",
|
||||||
Path: filepath.Join(dir, "test.db"),
|
Path: filepath.Join(dir, "test.db"),
|
||||||
|
Port: 3306,
|
||||||
|
DBName: "nex",
|
||||||
MaxIdleConns: 5,
|
MaxIdleConns: 5,
|
||||||
MaxOpenConns: 50,
|
MaxOpenConns: 50,
|
||||||
ConnMaxLifetime: 30 * time.Minute,
|
ConnMaxLifetime: 30 * time.Minute,
|
||||||
@@ -210,6 +287,9 @@ func TestConfigPriority(t *testing.T) {
|
|||||||
assert.Equal(t, 9826, cfg.Server.Port)
|
assert.Equal(t, 9826, cfg.Server.Port)
|
||||||
assert.Equal(t, 30*time.Second, cfg.Server.ReadTimeout)
|
assert.Equal(t, 30*time.Second, cfg.Server.ReadTimeout)
|
||||||
assert.Equal(t, 30*time.Second, cfg.Server.WriteTimeout)
|
assert.Equal(t, 30*time.Second, cfg.Server.WriteTimeout)
|
||||||
|
assert.Equal(t, "sqlite", cfg.Database.Driver)
|
||||||
|
assert.Equal(t, 3306, cfg.Database.Port)
|
||||||
|
assert.Equal(t, "nex", cfg.Database.DBName)
|
||||||
assert.Equal(t, 10, cfg.Database.MaxIdleConns)
|
assert.Equal(t, 10, cfg.Database.MaxIdleConns)
|
||||||
assert.Equal(t, 100, cfg.Database.MaxOpenConns)
|
assert.Equal(t, 100, cfg.Database.MaxOpenConns)
|
||||||
assert.Equal(t, 1*time.Hour, cfg.Database.ConnMaxLifetime)
|
assert.Equal(t, 1*time.Hour, cfg.Database.ConnMaxLifetime)
|
||||||
@@ -222,13 +302,21 @@ func TestConfigPriority(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestPrintSummary(t *testing.T) {
|
func TestPrintSummary(t *testing.T) {
|
||||||
// 测试配置摘要输出
|
t.Run("SQLite模式摘要", func(t *testing.T) {
|
||||||
t.Run("打印配置摘要", func(t *testing.T) {
|
|
||||||
cfg := DefaultConfig()
|
cfg := DefaultConfig()
|
||||||
// PrintSummary 只是打印,不会返回错误
|
|
||||||
// 这里主要验证不会 panic
|
|
||||||
assert.NotPanics(t, func() {
|
assert.NotPanics(t, func() {
|
||||||
cfg.PrintSummary()
|
cfg.PrintSummary(zap.NewNop())
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("MySQL模式摘要", func(t *testing.T) {
|
||||||
|
cfg := DefaultConfig()
|
||||||
|
cfg.Database.Driver = "mysql"
|
||||||
|
cfg.Database.Host = "db.example.com"
|
||||||
|
cfg.Database.Port = 3306
|
||||||
|
cfg.Database.User = "nex"
|
||||||
|
cfg.Database.DBName = "nex"
|
||||||
|
assert.NotPanics(t, func() {
|
||||||
|
cfg.PrintSummary(zap.NewNop())
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,8 +29,8 @@ type Model struct {
|
|||||||
// UsageStats 用量统计
|
// UsageStats 用量统计
|
||||||
type UsageStats struct {
|
type UsageStats struct {
|
||||||
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
ID uint `gorm:"primaryKey;autoIncrement" json:"id"`
|
||||||
ProviderID string `gorm:"not null;index" json:"provider_id"`
|
ProviderID string `gorm:"not null;index;uniqueIndex:idx_provider_model_date" json:"provider_id"`
|
||||||
ModelName string `gorm:"not null;index" json:"model_name"`
|
ModelName string `gorm:"not null;index;uniqueIndex:idx_provider_model_date" json:"model_name"`
|
||||||
RequestCount int `gorm:"default:0" json:"request_count"`
|
RequestCount int `gorm:"default:0" json:"request_count"`
|
||||||
Date time.Time `gorm:"type:date;not null;uniqueIndex:idx_provider_model_date" json:"date"`
|
Date time.Time `gorm:"type:date;not null;uniqueIndex:idx_provider_model_date" json:"date"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,6 +7,8 @@ import (
|
|||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HTTPRequestSpec HTTP 请求规格
|
// HTTPRequestSpec HTTP 请求规格
|
||||||
@@ -33,13 +35,10 @@ type ConversionEngine struct {
|
|||||||
|
|
||||||
// NewConversionEngine 创建转换引擎
|
// NewConversionEngine 创建转换引擎
|
||||||
func NewConversionEngine(registry AdapterRegistry, logger *zap.Logger) *ConversionEngine {
|
func NewConversionEngine(registry AdapterRegistry, logger *zap.Logger) *ConversionEngine {
|
||||||
if logger == nil {
|
|
||||||
logger = zap.L()
|
|
||||||
}
|
|
||||||
return &ConversionEngine{
|
return &ConversionEngine{
|
||||||
registry: registry,
|
registry: registry,
|
||||||
middlewareChain: NewMiddlewareChain(),
|
middlewareChain: NewMiddlewareChain(),
|
||||||
logger: logger,
|
logger: pkglogger.WithModule(logger, "conversion.engine"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -90,7 +89,7 @@ func (e *ConversionEngine) ConvertHttpRequest(spec HTTPRequestSpec, clientProtoc
|
|||||||
rewrittenBody, err = providerAdapter.RewriteRequestModelName(spec.Body, provider.ModelName, interfaceType)
|
rewrittenBody, err = providerAdapter.RewriteRequestModelName(spec.Body, provider.ModelName, interfaceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Warn("Smart Passthrough 改写请求失败,使用原始请求体",
|
e.logger.Warn("Smart Passthrough 改写请求失败,使用原始请求体",
|
||||||
zap.String("error", err.Error()),
|
zap.Error(err),
|
||||||
zap.String("interface", string(interfaceType)))
|
zap.String("interface", string(interfaceType)))
|
||||||
rewrittenBody = spec.Body
|
rewrittenBody = spec.Body
|
||||||
}
|
}
|
||||||
@@ -143,7 +142,7 @@ func (e *ConversionEngine) ConvertHttpResponse(spec HTTPResponseSpec, clientProt
|
|||||||
rewrittenBody, err := adapter.RewriteResponseModelName(spec.Body, modelOverride, interfaceType)
|
rewrittenBody, err := adapter.RewriteResponseModelName(spec.Body, modelOverride, interfaceType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Warn("Smart Passthrough 改写响应失败,使用原始响应体",
|
e.logger.Warn("Smart Passthrough 改写响应失败,使用原始响应体",
|
||||||
zap.String("error", err.Error()),
|
zap.Error(err),
|
||||||
zap.String("interface", string(interfaceType)))
|
zap.String("interface", string(interfaceType)))
|
||||||
return &spec, nil
|
return &spec, nil
|
||||||
}
|
}
|
||||||
@@ -312,7 +311,7 @@ func (e *ConversionEngine) convertModelsResponseBody(clientAdapter, providerAdap
|
|||||||
}
|
}
|
||||||
encoded, err := clientAdapter.EncodeModelsResponse(models)
|
encoded, err := clientAdapter.EncodeModelsResponse(models)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
e.logger.Warn("编码 Models 响应失败,返回原始响应", zap.String("error", err.Error()))
|
e.logger.Warn("编码 Models 响应失败,返回原始响应", zap.Error(err))
|
||||||
return body, nil
|
return body, nil
|
||||||
}
|
}
|
||||||
return encoded, nil
|
return encoded, nil
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestConversionError_WithProviderProtocol(t *testing.T) {
|
func TestConversionError_WithProviderProtocol(t *testing.T) {
|
||||||
@@ -39,7 +40,7 @@ func TestConversionError_FullBuilder(t *testing.T) {
|
|||||||
|
|
||||||
func TestEngine_Use(t *testing.T) {
|
func TestEngine_Use(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
called := false
|
called := false
|
||||||
engine.Use(&testMiddleware{fn: func(req *canonical.CanonicalRequest, cp, pp string, ctx *ConversionContext) (*canonical.CanonicalRequest, error) {
|
engine.Use(&testMiddleware{fn: func(req *canonical.CanonicalRequest, cp, pp string, ctx *ConversionContext) (*canonical.CanonicalRequest, error) {
|
||||||
called = true
|
called = true
|
||||||
@@ -66,7 +67,7 @@ func TestEngine_Use(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_DecodeError(t *testing.T) {
|
func TestConvertHttpRequest_DecodeError(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
||||||
return nil, errors.New("decode failed")
|
return nil, errors.New("decode failed")
|
||||||
@@ -82,7 +83,7 @@ func TestConvertHttpRequest_DecodeError(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_EncodeError(t *testing.T) {
|
func TestConvertHttpRequest_EncodeError(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("client", false))
|
_ = engine.RegisterAdapter(newMockAdapter("client", false))
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
providerAdapter.encodeReqFn = func(req *canonical.CanonicalRequest, p *TargetProvider) ([]byte, error) {
|
providerAdapter.encodeReqFn = func(req *canonical.CanonicalRequest, p *TargetProvider) ([]byte, error) {
|
||||||
@@ -98,7 +99,7 @@ func TestConvertHttpRequest_EncodeError(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_CrossProtocol(t *testing.T) {
|
func TestConvertHttpResponse_CrossProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.encodeRespFn = func(resp *canonical.CanonicalResponse) ([]byte, error) {
|
clientAdapter.encodeRespFn = func(resp *canonical.CanonicalResponse) ([]byte, error) {
|
||||||
@@ -121,7 +122,7 @@ func TestConvertHttpResponse_CrossProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_DecodeError(t *testing.T) {
|
func TestConvertHttpResponse_DecodeError(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
providerAdapter.decodeRespFn = func(raw []byte) (*canonical.CanonicalResponse, error) {
|
providerAdapter.decodeRespFn = func(raw []byte) (*canonical.CanonicalResponse, error) {
|
||||||
return nil, errors.New("decode error")
|
return nil, errors.New("decode error")
|
||||||
@@ -135,7 +136,7 @@ func TestConvertHttpResponse_DecodeError(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_EmbeddingInterface(t *testing.T) {
|
func TestConvertHttpRequest_EmbeddingInterface(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.ifaceType = InterfaceTypeEmbeddings
|
clientAdapter.ifaceType = InterfaceTypeEmbeddings
|
||||||
@@ -158,7 +159,7 @@ func TestConvertHttpRequest_EmbeddingInterface(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_RerankInterface(t *testing.T) {
|
func TestConvertHttpRequest_RerankInterface(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.ifaceType = InterfaceTypeRerank
|
clientAdapter.ifaceType = InterfaceTypeRerank
|
||||||
@@ -178,7 +179,7 @@ func TestConvertHttpRequest_RerankInterface(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_EmbeddingInterface(t *testing.T) {
|
func TestConvertHttpResponse_EmbeddingInterface(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
|
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeEmbeddings: true}
|
||||||
@@ -196,7 +197,7 @@ func TestConvertHttpResponse_EmbeddingInterface(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_RerankInterface(t *testing.T) {
|
func TestConvertHttpResponse_RerankInterface(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
|
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeRerank: true}
|
||||||
@@ -214,7 +215,7 @@ func TestConvertHttpResponse_RerankInterface(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_ModelsInterface_Passthrough(t *testing.T) {
|
func TestConvertHttpRequest_ModelsInterface_Passthrough(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.ifaceType = InterfaceTypeModels
|
clientAdapter.ifaceType = InterfaceTypeModels
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
@@ -232,7 +233,7 @@ func TestConvertHttpRequest_ModelsInterface_Passthrough(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_ModelsInterface(t *testing.T) {
|
func TestConvertHttpResponse_ModelsInterface(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModels: true}
|
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModels: true}
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
@@ -249,7 +250,7 @@ func TestConvertHttpResponse_ModelsInterface(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_ModelInfoInterface(t *testing.T) {
|
func TestConvertHttpResponse_ModelInfoInterface(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModelInfo: true}
|
clientAdapter.supportsIface = map[InterfaceType]bool{InterfaceTypeModelInfo: true}
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
@@ -324,7 +325,7 @@ var _ = json.Marshal
|
|||||||
|
|
||||||
func TestConvertEmbeddingBody_DecodeError(t *testing.T) {
|
func TestConvertEmbeddingBody_DecodeError(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.decodeEmbeddingReqFn = func(raw []byte) (*canonical.CanonicalEmbeddingRequest, error) {
|
clientAdapter.decodeEmbeddingReqFn = func(raw []byte) (*canonical.CanonicalEmbeddingRequest, error) {
|
||||||
@@ -344,7 +345,7 @@ func TestConvertEmbeddingBody_DecodeError(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertRerankBody_DecodeError(t *testing.T) {
|
func TestConvertRerankBody_DecodeError(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.decodeRerankReqFn = func(raw []byte) (*canonical.CanonicalRerankRequest, error) {
|
clientAdapter.decodeRerankReqFn = func(raw []byte) (*canonical.CanonicalRerankRequest, error) {
|
||||||
@@ -364,7 +365,7 @@ func TestConvertRerankBody_DecodeError(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertBody_UnknownInterfaceType(t *testing.T) {
|
func TestConvertBody_UnknownInterfaceType(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
|
|||||||
@@ -203,7 +203,7 @@ func (e *noopStreamEncoder) Flush() [][]byte
|
|||||||
|
|
||||||
func TestNewConversionEngine(t *testing.T) {
|
func TestNewConversionEngine(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
assert.NotNil(t, engine)
|
assert.NotNil(t, engine)
|
||||||
assert.Equal(t, registry, engine.GetRegistry())
|
assert.Equal(t, registry, engine.GetRegistry())
|
||||||
}
|
}
|
||||||
@@ -211,7 +211,7 @@ func TestNewConversionEngine(t *testing.T) {
|
|||||||
func TestNewConversionEngine_LoggerInjection(t *testing.T) {
|
func TestNewConversionEngine_LoggerInjection(t *testing.T) {
|
||||||
t.Run("nil_logger_uses_global", func(t *testing.T) {
|
t.Run("nil_logger_uses_global", func(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
assert.NotNil(t, engine.logger)
|
assert.NotNil(t, engine.logger)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -219,13 +219,14 @@ func TestNewConversionEngine_LoggerInjection(t *testing.T) {
|
|||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
customLogger := zap.NewNop()
|
customLogger := zap.NewNop()
|
||||||
engine := NewConversionEngine(registry, customLogger)
|
engine := NewConversionEngine(registry, customLogger)
|
||||||
assert.Equal(t, customLogger, engine.logger)
|
assert.NotNil(t, engine.logger)
|
||||||
|
assert.Contains(t, engine.logger.Name(), "conversion.engine")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRegisterAdapter(t *testing.T) {
|
func TestRegisterAdapter(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
adapter := newMockAdapter("test-proto", true)
|
adapter := newMockAdapter("test-proto", true)
|
||||||
err := engine.RegisterAdapter(adapter)
|
err := engine.RegisterAdapter(adapter)
|
||||||
@@ -237,7 +238,7 @@ func TestRegisterAdapter(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsPassthrough_SameProtocol(t *testing.T) {
|
func TestIsPassthrough_SameProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
adapter := newMockAdapter("openai", true)
|
adapter := newMockAdapter("openai", true)
|
||||||
_ = engine.RegisterAdapter(adapter)
|
_ = engine.RegisterAdapter(adapter)
|
||||||
|
|
||||||
@@ -246,7 +247,7 @@ func TestIsPassthrough_SameProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsPassthrough_DifferentProtocol(t *testing.T) {
|
func TestIsPassthrough_DifferentProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("anthropic", true))
|
_ = engine.RegisterAdapter(newMockAdapter("anthropic", true))
|
||||||
|
|
||||||
@@ -255,7 +256,7 @@ func TestIsPassthrough_DifferentProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestIsPassthrough_NoPassthrough(t *testing.T) {
|
func TestIsPassthrough_NoPassthrough(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("custom", false))
|
_ = engine.RegisterAdapter(newMockAdapter("custom", false))
|
||||||
|
|
||||||
assert.False(t, engine.IsPassthrough("custom", "custom"))
|
assert.False(t, engine.IsPassthrough("custom", "custom"))
|
||||||
@@ -263,7 +264,7 @@ func TestIsPassthrough_NoPassthrough(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectInterfaceType(t *testing.T) {
|
func TestDetectInterfaceType(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
adapter := newMockAdapter("test", true)
|
adapter := newMockAdapter("test", true)
|
||||||
adapter.ifaceType = InterfaceTypeChat
|
adapter.ifaceType = InterfaceTypeChat
|
||||||
_ = engine.RegisterAdapter(adapter)
|
_ = engine.RegisterAdapter(adapter)
|
||||||
@@ -275,7 +276,7 @@ func TestDetectInterfaceType(t *testing.T) {
|
|||||||
|
|
||||||
func TestDetectInterfaceType_NonExistentProtocol(t *testing.T) {
|
func TestDetectInterfaceType_NonExistentProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
_, err := engine.DetectInterfaceType("/v1/chat", "nonexistent")
|
_, err := engine.DetectInterfaceType("/v1/chat", "nonexistent")
|
||||||
assert.Error(t, err)
|
assert.Error(t, err)
|
||||||
@@ -283,7 +284,7 @@ func TestDetectInterfaceType_NonExistentProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_Passthrough(t *testing.T) {
|
func TestConvertHttpRequest_Passthrough(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
||||||
|
|
||||||
provider := NewTargetProvider("https://api.openai.com/v1", "sk-test", "gpt-4")
|
provider := NewTargetProvider("https://api.openai.com/v1", "sk-test", "gpt-4")
|
||||||
@@ -301,7 +302,7 @@ func TestConvertHttpRequest_Passthrough(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpRequest_CrossProtocol(t *testing.T) {
|
func TestConvertHttpRequest_CrossProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client-proto", false)
|
clientAdapter := newMockAdapter("client-proto", false)
|
||||||
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
clientAdapter.decodeReqFn = func(raw []byte) (*canonical.CanonicalRequest, error) {
|
||||||
@@ -333,7 +334,7 @@ func TestConvertHttpRequest_CrossProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_Passthrough(t *testing.T) {
|
func TestConvertHttpResponse_Passthrough(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
||||||
|
|
||||||
spec := HTTPResponseSpec{
|
spec := HTTPResponseSpec{
|
||||||
@@ -349,7 +350,7 @@ func TestConvertHttpResponse_Passthrough(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateStreamConverter_Passthrough(t *testing.T) {
|
func TestCreateStreamConverter_Passthrough(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
||||||
|
|
||||||
converter, err := engine.CreateStreamConverter("openai", "openai", "", InterfaceTypeChat)
|
converter, err := engine.CreateStreamConverter("openai", "openai", "", InterfaceTypeChat)
|
||||||
@@ -360,7 +361,7 @@ func TestCreateStreamConverter_Passthrough(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateStreamConverter_Canonical(t *testing.T) {
|
func TestCreateStreamConverter_Canonical(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("client", false))
|
_ = engine.RegisterAdapter(newMockAdapter("client", false))
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("provider", false))
|
_ = engine.RegisterAdapter(newMockAdapter("provider", false))
|
||||||
|
|
||||||
@@ -372,7 +373,7 @@ func TestCreateStreamConverter_Canonical(t *testing.T) {
|
|||||||
|
|
||||||
func TestEncodeError(t *testing.T) {
|
func TestEncodeError(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
_ = engine.RegisterAdapter(newMockAdapter("openai", true))
|
||||||
|
|
||||||
convErr := NewConversionError(ErrorCodeInvalidInput, "测试错误")
|
convErr := NewConversionError(ErrorCodeInvalidInput, "测试错误")
|
||||||
@@ -384,7 +385,7 @@ func TestEncodeError(t *testing.T) {
|
|||||||
|
|
||||||
func TestEncodeError_NonExistentProtocol(t *testing.T) {
|
func TestEncodeError_NonExistentProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
convErr := NewConversionError(ErrorCodeInvalidInput, "测试错误")
|
convErr := NewConversionError(ErrorCodeInvalidInput, "测试错误")
|
||||||
body, statusCode, err := engine.EncodeError(convErr, "nonexistent")
|
body, statusCode, err := engine.EncodeError(convErr, "nonexistent")
|
||||||
@@ -417,7 +418,7 @@ func TestRegistry_GetNonExistent(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_ModelOverride_CrossProtocol(t *testing.T) {
|
func TestConvertHttpResponse_ModelOverride_CrossProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
clientAdapter := newMockAdapter("client", false)
|
clientAdapter := newMockAdapter("client", false)
|
||||||
clientAdapter.encodeRespFn = func(resp *canonical.CanonicalResponse) ([]byte, error) {
|
clientAdapter.encodeRespFn = func(resp *canonical.CanonicalResponse) ([]byte, error) {
|
||||||
@@ -446,7 +447,7 @@ func TestConvertHttpResponse_ModelOverride_CrossProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestConvertHttpResponse_ModelOverride_SameProtocol(t *testing.T) {
|
func TestConvertHttpResponse_ModelOverride_SameProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
// 使用真实 OpenAI adapter 验证 Smart Passthrough 改写
|
// 使用真实 OpenAI adapter 验证 Smart Passthrough 改写
|
||||||
openaiAdapter := newMockAdapter("openai", true)
|
openaiAdapter := newMockAdapter("openai", true)
|
||||||
@@ -476,7 +477,7 @@ func TestConvertHttpResponse_ModelOverride_SameProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateStreamConverter_ModelOverride_SmartPassthrough(t *testing.T) {
|
func TestCreateStreamConverter_ModelOverride_SmartPassthrough(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
openaiAdapter := newMockAdapter("openai", true)
|
openaiAdapter := newMockAdapter("openai", true)
|
||||||
openaiAdapter.rewriteRespFn = func(body []byte, newModel string, ifaceType InterfaceType) ([]byte, error) {
|
openaiAdapter.rewriteRespFn = func(body []byte, newModel string, ifaceType InterfaceType) ([]byte, error) {
|
||||||
@@ -506,7 +507,7 @@ func TestCreateStreamConverter_ModelOverride_SmartPassthrough(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateStreamConverter_ModelOverride_CrossProtocol(t *testing.T) {
|
func TestCreateStreamConverter_ModelOverride_CrossProtocol(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
// provider adapter 解码出含 model 的流式事件
|
// provider adapter 解码出含 model 的流式事件
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
@@ -560,7 +561,7 @@ func TestCreateStreamConverter_ModelOverride_CrossProtocol(t *testing.T) {
|
|||||||
|
|
||||||
func TestCreateStreamConverter_ModelOverride_CrossProtocol_Empty(t *testing.T) {
|
func TestCreateStreamConverter_ModelOverride_CrossProtocol_Empty(t *testing.T) {
|
||||||
registry := NewMemoryRegistry()
|
registry := NewMemoryRegistry()
|
||||||
engine := NewConversionEngine(registry, nil)
|
engine := NewConversionEngine(registry, zap.NewNop())
|
||||||
|
|
||||||
providerAdapter := newMockAdapter("provider", false)
|
providerAdapter := newMockAdapter("provider", false)
|
||||||
providerAdapter.streamDecoderFn = func() StreamDecoder {
|
providerAdapter.streamDecoderFn = func() StreamDecoder {
|
||||||
|
|||||||
149
backend/internal/database/database.go
Normal file
149
backend/internal/database/database.go
Normal file
@@ -0,0 +1,149 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"nex/backend/internal/config"
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func Init(cfg *config.DatabaseConfig, zapLogger *zap.Logger) (*gorm.DB, error) {
|
||||||
|
moduleLogger := pkglogger.WithModule(zapLogger, "database")
|
||||||
|
|
||||||
|
db, err := initDB(cfg, moduleLogger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("初始化数据库失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := runMigrations(db, cfg.Driver, moduleLogger); err != nil {
|
||||||
|
return nil, fmt.Errorf("数据库迁移失败: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
configurePool(db, cfg, moduleLogger)
|
||||||
|
|
||||||
|
return db, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Close(db *gorm.DB) {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func initDB(cfg *config.DatabaseConfig, zapLogger *zap.Logger) (*gorm.DB, error) {
|
||||||
|
gormLogger := pkglogger.NewGormLogger(zapLogger)
|
||||||
|
|
||||||
|
gormConfig := &gorm.Config{
|
||||||
|
Logger: gormLogger,
|
||||||
|
}
|
||||||
|
|
||||||
|
switch cfg.Driver {
|
||||||
|
case "mysql":
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
|
||||||
|
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)
|
||||||
|
if zapLogger != nil {
|
||||||
|
zapLogger.Info("连接 MySQL 数据库",
|
||||||
|
zap.String("host", cfg.Host),
|
||||||
|
zap.Int("port", cfg.Port),
|
||||||
|
zap.String("database", cfg.DBName))
|
||||||
|
}
|
||||||
|
return gorm.Open(mysql.Open(dsn), gormConfig)
|
||||||
|
default:
|
||||||
|
dbDir := filepath.Dir(cfg.Path)
|
||||||
|
if err := os.MkdirAll(dbDir, 0755); err != nil {
|
||||||
|
return nil, fmt.Errorf("创建数据库目录失败: %w", err)
|
||||||
|
}
|
||||||
|
if zapLogger != nil {
|
||||||
|
zapLogger.Info("连接 SQLite 数据库", zap.String("path", cfg.Path))
|
||||||
|
}
|
||||||
|
return gorm.Open(sqlite.Open(cfg.Path), gormConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations(db *gorm.DB, driver string, zapLogger *zap.Logger) error {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
gooseDialect := "sqlite3"
|
||||||
|
migrationsSubDir := "sqlite"
|
||||||
|
if driver == "mysql" {
|
||||||
|
gooseDialect = "mysql"
|
||||||
|
migrationsSubDir = "mysql"
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationsDir := getMigrationsDir(driver)
|
||||||
|
if _, err := os.Stat(migrationsDir); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("迁移目录不存在: %s", migrationsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
if zapLogger != nil {
|
||||||
|
zapLogger.Info("执行数据库迁移",
|
||||||
|
zap.String("dialect", gooseDialect),
|
||||||
|
zap.String("dir", migrationsSubDir))
|
||||||
|
}
|
||||||
|
|
||||||
|
goose.SetDialect(gooseDialect)
|
||||||
|
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func configurePool(db *gorm.DB, cfg *config.DatabaseConfig, zapLogger *zap.Logger) {
|
||||||
|
if cfg.Driver == "sqlite" {
|
||||||
|
if err := db.Exec("PRAGMA journal_mode=WAL").Error; err != nil {
|
||||||
|
if zapLogger != nil {
|
||||||
|
zapLogger.Warn("启用 WAL 模式失败", zap.Error(err))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxIdleConns(cfg.MaxIdleConns)
|
||||||
|
sqlDB.SetMaxOpenConns(cfg.MaxOpenConns)
|
||||||
|
sqlDB.SetConnMaxLifetime(cfg.ConnMaxLifetime)
|
||||||
|
|
||||||
|
if zapLogger != nil {
|
||||||
|
zapLogger.Info("数据库连接池配置",
|
||||||
|
zap.Int("max_idle_conns", cfg.MaxIdleConns),
|
||||||
|
zap.Int("max_open_conns", cfg.MaxOpenConns),
|
||||||
|
zap.Duration("conn_max_lifetime", cfg.ConnMaxLifetime))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMigrationsDir(driver string) string {
|
||||||
|
_, filename, _, ok := runtime.Caller(0)
|
||||||
|
if ok {
|
||||||
|
subDir := "sqlite"
|
||||||
|
if driver == "mysql" {
|
||||||
|
subDir = "mysql"
|
||||||
|
}
|
||||||
|
dir := filepath.Join(filepath.Dir(filename), "..", "..", "migrations", subDir)
|
||||||
|
if abs, err := filepath.Abs(dir); err == nil {
|
||||||
|
return abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "./migrations"
|
||||||
|
}
|
||||||
|
|
||||||
|
func BuildDSN(cfg *config.DatabaseConfig) string {
|
||||||
|
return fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
|
||||||
|
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.DBName)
|
||||||
|
}
|
||||||
78
backend/internal/database/database_test.go
Normal file
78
backend/internal/database/database_test.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
"nex/backend/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInit_SQLite(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfg := &config.DatabaseConfig{
|
||||||
|
Driver: "sqlite",
|
||||||
|
Path: filepath.Join(dir, "test.db"),
|
||||||
|
MaxIdleConns: 5,
|
||||||
|
MaxOpenConns: 10,
|
||||||
|
ConnMaxLifetime: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
zapLogger := zap.NewNop()
|
||||||
|
db, err := Init(cfg, zapLogger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, db)
|
||||||
|
defer Close(db)
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, sqlDB)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClose(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
cfg := &config.DatabaseConfig{
|
||||||
|
Driver: "sqlite",
|
||||||
|
Path: filepath.Join(dir, "test.db"),
|
||||||
|
MaxIdleConns: 5,
|
||||||
|
MaxOpenConns: 10,
|
||||||
|
ConnMaxLifetime: 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
zapLogger := zap.NewNop()
|
||||||
|
db, err := Init(cfg, zapLogger)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, db)
|
||||||
|
|
||||||
|
Close(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildDSN(t *testing.T) {
|
||||||
|
cfg := &config.DatabaseConfig{
|
||||||
|
Driver: "mysql",
|
||||||
|
Host: "db.example.com",
|
||||||
|
Port: 3306,
|
||||||
|
User: "nexuser",
|
||||||
|
Password: "secretpass",
|
||||||
|
DBName: "nexdb",
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := BuildDSN(cfg)
|
||||||
|
assert.Equal(t, "nexuser:secretpass@tcp(db.example.com:3306)/nexdb?charset=utf8mb4&parseTime=true&loc=Local", dsn)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBuildDSN_EmptyPassword(t *testing.T) {
|
||||||
|
cfg := &config.DatabaseConfig{
|
||||||
|
Driver: "mysql",
|
||||||
|
Host: "localhost",
|
||||||
|
Port: 3306,
|
||||||
|
User: "root",
|
||||||
|
DBName: "nex",
|
||||||
|
}
|
||||||
|
|
||||||
|
dsn := BuildDSN(cfg)
|
||||||
|
assert.Equal(t, "root:@tcp(localhost:3306)/nex?charset=utf8mb4&parseTime=true&loc=Local", dsn)
|
||||||
|
}
|
||||||
@@ -5,9 +5,10 @@ import (
|
|||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Logging 日志中间件
|
|
||||||
func Logging(logger *zap.Logger) gin.HandlerFunc {
|
func Logging(logger *zap.Logger) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
start := time.Now()
|
start := time.Now()
|
||||||
@@ -15,12 +16,17 @@ func Logging(logger *zap.Logger) gin.HandlerFunc {
|
|||||||
query := c.Request.URL.RawQuery
|
query := c.Request.URL.RawQuery
|
||||||
|
|
||||||
requestID, _ := c.Get(RequestIDKey)
|
requestID, _ := c.Get(RequestIDKey)
|
||||||
|
var requestIDStr string
|
||||||
|
if id, ok := requestID.(string); ok {
|
||||||
|
requestIDStr = id
|
||||||
|
}
|
||||||
|
|
||||||
logger.Info("请求开始",
|
logger.Info("请求开始",
|
||||||
zap.String("method", c.Request.Method),
|
pkglogger.Method(c.Request.Method),
|
||||||
zap.String("path", path),
|
pkglogger.Path(path),
|
||||||
zap.String("query", query),
|
pkglogger.Query(query),
|
||||||
zap.String("client_ip", c.ClientIP()),
|
pkglogger.ClientIP(c.ClientIP()),
|
||||||
zap.Any("request_id", requestID),
|
pkglogger.RequestID(requestIDStr),
|
||||||
)
|
)
|
||||||
|
|
||||||
c.Next()
|
c.Next()
|
||||||
@@ -29,12 +35,12 @@ func Logging(logger *zap.Logger) gin.HandlerFunc {
|
|||||||
statusCode := c.Writer.Status()
|
statusCode := c.Writer.Status()
|
||||||
|
|
||||||
logger.Info("请求结束",
|
logger.Info("请求结束",
|
||||||
zap.Int("status", statusCode),
|
pkglogger.StatusCode(statusCode),
|
||||||
zap.String("method", c.Request.Method),
|
pkglogger.Method(c.Request.Method),
|
||||||
zap.String("path", path),
|
pkglogger.Path(path),
|
||||||
zap.Duration("latency", latency),
|
pkglogger.Latency(latency),
|
||||||
zap.Int("body_size", c.Writer.Size()),
|
pkglogger.BodySize(c.Writer.Size()),
|
||||||
zap.Any("request_id", requestID),
|
pkglogger.RequestID(requestIDStr),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import (
|
|||||||
"nex/backend/internal/provider"
|
"nex/backend/internal/provider"
|
||||||
"nex/backend/internal/service"
|
"nex/backend/internal/service"
|
||||||
"nex/backend/pkg/modelid"
|
"nex/backend/pkg/modelid"
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ProxyHandler 统一代理处理器
|
// ProxyHandler 统一代理处理器
|
||||||
@@ -29,14 +30,14 @@ type ProxyHandler struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewProxyHandler 创建统一代理处理器
|
// NewProxyHandler 创建统一代理处理器
|
||||||
func NewProxyHandler(engine *conversion.ConversionEngine, client provider.ProviderClient, routingService service.RoutingService, providerService service.ProviderService, statsService service.StatsService) *ProxyHandler {
|
func NewProxyHandler(engine *conversion.ConversionEngine, client provider.ProviderClient, routingService service.RoutingService, providerService service.ProviderService, statsService service.StatsService, logger *zap.Logger) *ProxyHandler {
|
||||||
return &ProxyHandler{
|
return &ProxyHandler{
|
||||||
engine: engine,
|
engine: engine,
|
||||||
client: client,
|
client: client,
|
||||||
routingService: routingService,
|
routingService: routingService,
|
||||||
providerService: providerService,
|
providerService: providerService,
|
||||||
statsService: statsService,
|
statsService: statsService,
|
||||||
logger: zap.L(),
|
logger: pkglogger.WithModule(logger, "handler.proxy"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -331,7 +332,7 @@ func (h *ProxyHandler) handleModelInfo(c *gin.Context, unifiedID string, adapter
|
|||||||
// 使用 adapter 编码返回
|
// 使用 adapter 编码返回
|
||||||
body, err := adapter.EncodeModelInfoResponse(modelInfo)
|
body, err := adapter.EncodeModelInfoResponse(modelInfo)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
h.logger.Error("编码 ModelInfo 响应失败", zap.String("error", err.Error()))
|
h.logger.Error("编码 ModelInfo 响应失败", zap.Error(err))
|
||||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "编码响应失败"})
|
c.JSON(http.StatusInternalServerError, gin.H{"error": "编码响应失败"})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
"go.uber.org/mock/gomock"
|
"go.uber.org/mock/gomock"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"nex/backend/internal/conversion"
|
"nex/backend/internal/conversion"
|
||||||
"nex/backend/internal/conversion/anthropic"
|
"nex/backend/internal/conversion/anthropic"
|
||||||
@@ -31,7 +32,7 @@ func init() {
|
|||||||
func setupProxyEngine(t *testing.T) *conversion.ConversionEngine {
|
func setupProxyEngine(t *testing.T) *conversion.ConversionEngine {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, zap.NewNop())
|
||||||
require.NoError(t, registry.Register(openai.NewAdapter()))
|
require.NoError(t, registry.Register(openai.NewAdapter()))
|
||||||
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
||||||
return engine
|
return engine
|
||||||
@@ -44,6 +45,7 @@ func newTestProxyHandler(engine *conversion.ConversionEngine, client *mocks.Mock
|
|||||||
routingSvc,
|
routingSvc,
|
||||||
providerSvc,
|
providerSvc,
|
||||||
statsSvc,
|
statsSvc,
|
||||||
|
zap.NewNop(),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,7 +501,7 @@ func TestProxyHandler_HandleStream_CreateStreamConverterError(t *testing.T) {
|
|||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, zap.NewNop())
|
||||||
err := registry.Register(openai.NewAdapter())
|
err := registry.Register(openai.NewAdapter())
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
@@ -527,7 +529,7 @@ func TestProxyHandler_HandleStream_ConvertRequestError(t *testing.T) {
|
|||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, zap.NewNop())
|
||||||
require.NoError(t, registry.Register(openai.NewAdapter()))
|
require.NoError(t, registry.Register(openai.NewAdapter()))
|
||||||
|
|
||||||
routingSvc := mocks.NewMockRoutingService(ctrl)
|
routingSvc := mocks.NewMockRoutingService(ctrl)
|
||||||
@@ -554,7 +556,7 @@ func TestProxyHandler_HandleNonStream_ConvertResponseError(t *testing.T) {
|
|||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, zap.NewNop())
|
||||||
require.NoError(t, registry.Register(openai.NewAdapter()))
|
require.NoError(t, registry.Register(openai.NewAdapter()))
|
||||||
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
||||||
|
|
||||||
@@ -623,7 +625,7 @@ func TestProxyHandler_ForwardPassthrough_CrossProtocol(t *testing.T) {
|
|||||||
defer ctrl.Finish()
|
defer ctrl.Finish()
|
||||||
|
|
||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, zap.NewNop())
|
||||||
require.NoError(t, registry.Register(openai.NewAdapter()))
|
require.NoError(t, registry.Register(openai.NewAdapter()))
|
||||||
|
|
||||||
anthropicAdapter := anthropic.NewAdapter()
|
anthropicAdapter := anthropic.NewAdapter()
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import (
|
|||||||
|
|
||||||
"nex/backend/internal/conversion"
|
"nex/backend/internal/conversion"
|
||||||
pkgErrors "nex/backend/pkg/errors"
|
pkgErrors "nex/backend/pkg/errors"
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// StreamConfig 流式处理配置
|
// StreamConfig 流式处理配置
|
||||||
@@ -57,12 +58,12 @@ type ProviderClient interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewClient 创建供应商客户端
|
// NewClient 创建供应商客户端
|
||||||
func NewClient() *Client {
|
func NewClient(logger *zap.Logger) *Client {
|
||||||
return &Client{
|
return &Client{
|
||||||
httpClient: &http.Client{
|
httpClient: &http.Client{
|
||||||
Timeout: 30 * time.Second,
|
Timeout: 30 * time.Second,
|
||||||
},
|
},
|
||||||
logger: zap.L(),
|
logger: pkglogger.WithModule(logger, "provider.client"),
|
||||||
streamCfg: DefaultStreamConfig(),
|
streamCfg: DefaultStreamConfig(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -186,7 +187,7 @@ func (c *Client) readStream(ctx context.Context, cancel context.CancelFunc, body
|
|||||||
c.logger.Error("流网络错误", zap.String("error", err.Error()))
|
c.logger.Error("流网络错误", zap.String("error", err.Error()))
|
||||||
eventChan <- StreamEvent{Error: fmt.Errorf("网络错误: %w", err)}
|
eventChan <- StreamEvent{Error: fmt.Errorf("网络错误: %w", err)}
|
||||||
} else {
|
} else {
|
||||||
c.logger.Error("流读取错误", zap.String("error", err.Error()))
|
c.logger.Error("流读取错误", zap.Error(err))
|
||||||
eventChan <- StreamEvent{Error: fmt.Errorf("读取错误: %w", err)}
|
eventChan <- StreamEvent{Error: fmt.Errorf("读取错误: %w", err)}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -13,12 +13,13 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"nex/backend/internal/conversion"
|
"nex/backend/internal/conversion"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNewClient(t *testing.T) {
|
func TestNewClient(t *testing.T) {
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
require.NotNil(t, client)
|
require.NotNil(t, client)
|
||||||
assert.NotNil(t, client.httpClient)
|
assert.NotNil(t, client.httpClient)
|
||||||
assert.Equal(t, 4096, client.streamCfg.InitialBufferSize)
|
assert.Equal(t, 4096, client.streamCfg.InitialBufferSize)
|
||||||
@@ -44,7 +45,7 @@ func TestClient_Send_Success(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -68,7 +69,7 @@ func TestClient_Send_ErrorResponse(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -82,7 +83,7 @@ func TestClient_Send_ErrorResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestClient_Send_ConnectionError(t *testing.T) {
|
func TestClient_Send_ConnectionError(t *testing.T) {
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: "http://localhost:1/v1/chat/completions",
|
URL: "http://localhost:1/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -99,7 +100,7 @@ func TestClient_SendStream_CreatesChannel(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -121,7 +122,7 @@ func TestClient_SendStream_ErrorResponse(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -150,7 +151,7 @@ func TestClient_SendStream_SSEEvents(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -188,7 +189,7 @@ func TestClient_SendStream_ContextCancellation(t *testing.T) {
|
|||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -218,7 +219,7 @@ func TestClient_Send_EmptyBody(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/models",
|
URL: server.URL + "/v1/models",
|
||||||
Method: "GET",
|
Method: "GET",
|
||||||
@@ -246,7 +247,7 @@ func TestClient_SendStream_SlowSSE(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -287,7 +288,7 @@ func TestClient_SendStream_SplitSSEEvents(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
@@ -375,7 +376,7 @@ func TestClient_SendStream_MidStreamNetworkError(t *testing.T) {
|
|||||||
}))
|
}))
|
||||||
defer server.Close()
|
defer server.Close()
|
||||||
|
|
||||||
client := NewClient()
|
client := NewClient(zap.NewNop())
|
||||||
spec := conversion.HTTPRequestSpec{
|
spec := conversion.HTTPRequestSpec{
|
||||||
URL: server.URL + "/v1/chat/completions",
|
URL: server.URL + "/v1/chat/completions",
|
||||||
Method: "POST",
|
Method: "POST",
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
package repository
|
package repository
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
|
||||||
"nex/backend/internal/config"
|
"nex/backend/internal/config"
|
||||||
"nex/backend/internal/domain"
|
"nex/backend/internal/domain"
|
||||||
@@ -22,47 +22,43 @@ func (r *statsRepository) Record(providerID, modelName string) error {
|
|||||||
today := time.Now().Format("2006-01-02")
|
today := time.Now().Format("2006-01-02")
|
||||||
todayTime, _ := time.Parse("2006-01-02", today)
|
todayTime, _ := time.Parse("2006-01-02", today)
|
||||||
|
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
stats := config.UsageStats{
|
||||||
var stats config.UsageStats
|
|
||||||
err := tx.Where("provider_id = ? AND model_name = ? AND date = ?",
|
|
||||||
providerID, modelName, todayTime).First(&stats).Error
|
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
stats = config.UsageStats{
|
|
||||||
ProviderID: providerID,
|
ProviderID: providerID,
|
||||||
ModelName: modelName,
|
ModelName: modelName,
|
||||||
RequestCount: 1,
|
RequestCount: 1,
|
||||||
Date: todayTime,
|
Date: todayTime,
|
||||||
}
|
}
|
||||||
return tx.Create(&stats).Error
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return tx.Model(&stats).Update("request_count", gorm.Expr("request_count + 1")).Error
|
return r.db.Clauses(clause.OnConflict{
|
||||||
})
|
Columns: []clause.Column{
|
||||||
|
{Name: "provider_id"},
|
||||||
|
{Name: "model_name"},
|
||||||
|
{Name: "date"},
|
||||||
|
},
|
||||||
|
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||||
|
"request_count": gorm.Expr("request_count + 1"),
|
||||||
|
}),
|
||||||
|
}).Create(&stats).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *statsRepository) BatchUpdate(providerID, modelName string, date time.Time, delta int) error {
|
func (r *statsRepository) BatchUpdate(providerID, modelName string, date time.Time, delta int) error {
|
||||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
stats := config.UsageStats{
|
||||||
var stats config.UsageStats
|
|
||||||
err := tx.Where("provider_id = ? AND model_name = ? AND date = ?",
|
|
||||||
providerID, modelName, date).First(&stats).Error
|
|
||||||
|
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
||||||
return tx.Create(&config.UsageStats{
|
|
||||||
ProviderID: providerID,
|
ProviderID: providerID,
|
||||||
ModelName: modelName,
|
ModelName: modelName,
|
||||||
RequestCount: delta,
|
RequestCount: delta,
|
||||||
Date: date,
|
Date: date,
|
||||||
}).Error
|
|
||||||
} else if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tx.Model(&stats).
|
return r.db.Clauses(clause.OnConflict{
|
||||||
Update("request_count", gorm.Expr("request_count + ?", delta)).Error
|
Columns: []clause.Column{
|
||||||
})
|
{Name: "provider_id"},
|
||||||
|
{Name: "model_name"},
|
||||||
|
{Name: "date"},
|
||||||
|
},
|
||||||
|
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||||
|
"request_count": gorm.Expr("request_count + ?", delta),
|
||||||
|
}),
|
||||||
|
}).Create(&stats).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *statsRepository) Query(providerID, modelName string, startDate, endDate *time.Time) ([]domain.UsageStats, error) {
|
func (r *statsRepository) Query(providerID, modelName string, startDate, endDate *time.Time) ([]domain.UsageStats, error) {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"nex/backend/internal/domain"
|
"nex/backend/internal/domain"
|
||||||
"nex/backend/internal/repository"
|
"nex/backend/internal/repository"
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type RoutingCache struct {
|
type RoutingCache struct {
|
||||||
@@ -27,7 +28,7 @@ func NewRoutingCache(
|
|||||||
return &RoutingCache{
|
return &RoutingCache{
|
||||||
modelRepo: modelRepo,
|
modelRepo: modelRepo,
|
||||||
providerRepo: providerRepo,
|
providerRepo: providerRepo,
|
||||||
logger: logger,
|
logger: pkglogger.WithModule(logger, "service.routing_cache"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"nex/backend/internal/domain"
|
"nex/backend/internal/domain"
|
||||||
"nex/backend/internal/repository"
|
"nex/backend/internal/repository"
|
||||||
@@ -119,7 +120,7 @@ func TestModelService_Delete_NotFound(t *testing.T) {
|
|||||||
|
|
||||||
func TestStatsService_Aggregate_Default(t *testing.T) {
|
func TestStatsService_Aggregate_Default(t *testing.T) {
|
||||||
statsRepo := repository.NewStatsRepository(nil)
|
statsRepo := repository.NewStatsRepository(nil)
|
||||||
buffer := NewStatsBuffer(statsRepo, nil)
|
buffer := NewStatsBuffer(statsRepo, zap.NewNop())
|
||||||
svc := NewStatsService(statsRepo, buffer)
|
svc := NewStatsService(statsRepo, buffer)
|
||||||
|
|
||||||
stats := []domain.UsageStats{
|
stats := []domain.UsageStats{
|
||||||
|
|||||||
@@ -318,7 +318,7 @@ func TestStatsService_Aggregate_ByModel(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
db := setupServiceTestDB(t)
|
db := setupServiceTestDB(t)
|
||||||
statsRepo := repository.NewStatsRepository(db)
|
statsRepo := repository.NewStatsRepository(db)
|
||||||
buffer := NewStatsBuffer(statsRepo, nil); svc := NewStatsService(statsRepo, buffer)
|
buffer := NewStatsBuffer(statsRepo, zap.NewNop()); svc := NewStatsService(statsRepo, buffer)
|
||||||
|
|
||||||
result := svc.Aggregate(tt.stats, "model")
|
result := svc.Aggregate(tt.stats, "model")
|
||||||
|
|
||||||
@@ -379,7 +379,7 @@ func TestStatsService_Aggregate_ByDate(t *testing.T) {
|
|||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
db := setupServiceTestDB(t)
|
db := setupServiceTestDB(t)
|
||||||
statsRepo := repository.NewStatsRepository(db)
|
statsRepo := repository.NewStatsRepository(db)
|
||||||
buffer := NewStatsBuffer(statsRepo, nil); svc := NewStatsService(statsRepo, buffer)
|
buffer := NewStatsBuffer(statsRepo, zap.NewNop()); svc := NewStatsService(statsRepo, buffer)
|
||||||
|
|
||||||
result := svc.Aggregate(tt.stats, "date")
|
result := svc.Aggregate(tt.stats, "date")
|
||||||
|
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"nex/backend/internal/repository"
|
"nex/backend/internal/repository"
|
||||||
|
pkglogger "nex/backend/pkg/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
type StatsBuffer struct {
|
type StatsBuffer struct {
|
||||||
@@ -46,7 +47,7 @@ func NewStatsBuffer(
|
|||||||
) *StatsBuffer {
|
) *StatsBuffer {
|
||||||
b := &StatsBuffer{
|
b := &StatsBuffer{
|
||||||
statsRepo: statsRepo,
|
statsRepo: statsRepo,
|
||||||
logger: logger,
|
logger: pkglogger.WithModule(logger, "service.stats_buffer"),
|
||||||
flushInterval: 5 * time.Second,
|
flushInterval: 5 * time.Second,
|
||||||
flushThreshold: 100,
|
flushThreshold: 100,
|
||||||
stopCh: make(chan struct{}),
|
stopCh: make(chan struct{}),
|
||||||
|
|||||||
44
backend/migrations/mysql/20260421000001_initial_schema.sql
Normal file
44
backend/migrations/mysql/20260421000001_initial_schema.sql
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
-- +goose Up
|
||||||
|
-- MySQL 方言初始迁移:providers、models、usage_stats 完整表结构
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS providers (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
name VARCHAR(255) NOT NULL,
|
||||||
|
api_key VARCHAR(255) NOT NULL,
|
||||||
|
base_url VARCHAR(255) NOT NULL,
|
||||||
|
protocol VARCHAR(50) DEFAULT 'openai',
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at DATETIME(3),
|
||||||
|
updated_at DATETIME(3)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS models (
|
||||||
|
id VARCHAR(36) PRIMARY KEY,
|
||||||
|
provider_id VARCHAR(36) NOT NULL,
|
||||||
|
model_name VARCHAR(255) NOT NULL,
|
||||||
|
enabled BOOLEAN DEFAULT TRUE,
|
||||||
|
created_at DATETIME(3),
|
||||||
|
FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE,
|
||||||
|
UNIQUE(provider_id, model_name)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS usage_stats (
|
||||||
|
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
|
||||||
|
provider_id VARCHAR(36) NOT NULL,
|
||||||
|
model_name VARCHAR(255) NOT NULL,
|
||||||
|
request_count INT DEFAULT 0,
|
||||||
|
date DATE NOT NULL,
|
||||||
|
UNIQUE(provider_id, model_name, date)
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX idx_models_provider_id ON models(provider_id);
|
||||||
|
CREATE INDEX idx_models_model_name ON models(model_name);
|
||||||
|
CREATE INDEX idx_usage_stats_provider_model_date ON usage_stats(provider_id, model_name, date);
|
||||||
|
|
||||||
|
-- +goose Down
|
||||||
|
DROP INDEX IF EXISTS idx_usage_stats_provider_model_date ON usage_stats;
|
||||||
|
DROP INDEX IF EXISTS idx_models_model_name ON models;
|
||||||
|
DROP INDEX IF EXISTS idx_models_provider_id ON models;
|
||||||
|
DROP TABLE IF EXISTS usage_stats;
|
||||||
|
DROP TABLE IF EXISTS models;
|
||||||
|
DROP TABLE IF EXISTS providers;
|
||||||
@@ -1,13 +1,20 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import "go.uber.org/zap"
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ctxKey struct{}
|
||||||
|
|
||||||
|
const requestIDKey = "request_id"
|
||||||
|
|
||||||
// WithRequestID 向 logger 添加 request_id 字段
|
|
||||||
func WithRequestID(logger *zap.Logger, requestID string) *zap.Logger {
|
func WithRequestID(logger *zap.Logger, requestID string) *zap.Logger {
|
||||||
return logger.With(zap.String("request_id", requestID))
|
return logger.With(zap.String(requestIDKey, requestID))
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithContext 向 logger 添加多个自定义字段
|
|
||||||
func WithContext(logger *zap.Logger, fields map[string]interface{}) *zap.Logger {
|
func WithContext(logger *zap.Logger, fields map[string]interface{}) *zap.Logger {
|
||||||
zapFields := make([]zap.Field, 0, len(fields))
|
zapFields := make([]zap.Field, 0, len(fields))
|
||||||
for k, v := range fields {
|
for k, v := range fields {
|
||||||
@@ -15,3 +22,37 @@ func WithContext(logger *zap.Logger, fields map[string]interface{}) *zap.Logger
|
|||||||
}
|
}
|
||||||
return logger.With(zapFields...)
|
return logger.With(zapFields...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func RequestIDFromGinContext(c *gin.Context) zap.Field {
|
||||||
|
requestID, exists := c.Get("request_id")
|
||||||
|
if !exists {
|
||||||
|
return zap.Skip()
|
||||||
|
}
|
||||||
|
if id, ok := requestID.(string); ok {
|
||||||
|
return RequestID(id)
|
||||||
|
}
|
||||||
|
return zap.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RequestIDFromContext(ctx context.Context) zap.Field {
|
||||||
|
requestID := ctx.Value(ctxKey{})
|
||||||
|
if requestID == nil {
|
||||||
|
return zap.Skip()
|
||||||
|
}
|
||||||
|
if id, ok := requestID.(string); ok {
|
||||||
|
return RequestID(id)
|
||||||
|
}
|
||||||
|
return zap.Skip()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContextWithRequestID(ctx context.Context, requestID string) context.Context {
|
||||||
|
return context.WithValue(ctx, ctxKey{}, requestID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func LoggerFromContext(ctx context.Context, baseLogger *zap.Logger) *zap.Logger {
|
||||||
|
field := RequestIDFromContext(ctx)
|
||||||
|
if field == zap.Skip() {
|
||||||
|
return baseLogger
|
||||||
|
}
|
||||||
|
return baseLogger.With(field)
|
||||||
|
}
|
||||||
|
|||||||
77
backend/pkg/logger/field.go
Normal file
77
backend/pkg/logger/field.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import "go.uber.org/zap"
|
||||||
|
|
||||||
|
const (
|
||||||
|
FieldRequestID = "request_id"
|
||||||
|
FieldProviderID = "provider_id"
|
||||||
|
FieldModelName = "model_name"
|
||||||
|
FieldMethod = "method"
|
||||||
|
FieldPath = "path"
|
||||||
|
FieldStatusCode = "status"
|
||||||
|
FieldLatency = "latency"
|
||||||
|
FieldClientIP = "client_ip"
|
||||||
|
FieldQuery = "query"
|
||||||
|
FieldBodySize = "body_size"
|
||||||
|
FieldSQL = "sql"
|
||||||
|
FieldRows = "rows_affected"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RequestID(id string) zap.Field {
|
||||||
|
return zap.String(FieldRequestID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ProviderID(id string) zap.Field {
|
||||||
|
return zap.String(FieldProviderID, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ModelName(name string) zap.Field {
|
||||||
|
return zap.String(FieldModelName, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Method(method string) zap.Field {
|
||||||
|
return zap.String(FieldMethod, method)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Path(path string) zap.Field {
|
||||||
|
return zap.String(FieldPath, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StatusCode(code int) zap.Field {
|
||||||
|
return zap.Int(FieldStatusCode, code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Latency(latency interface{}) zap.Field {
|
||||||
|
switch v := latency.(type) {
|
||||||
|
case int64:
|
||||||
|
return zap.Int64(FieldLatency, v)
|
||||||
|
case int:
|
||||||
|
return zap.Int(FieldLatency, v)
|
||||||
|
default:
|
||||||
|
return zap.Any(FieldLatency, latency)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClientIP(ip string) zap.Field {
|
||||||
|
return zap.String(FieldClientIP, ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Query(query string) zap.Field {
|
||||||
|
return zap.String(FieldQuery, query)
|
||||||
|
}
|
||||||
|
|
||||||
|
func BodySize(size int) zap.Field {
|
||||||
|
return zap.Int(FieldBodySize, size)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SQL(sql string) zap.Field {
|
||||||
|
return zap.String(FieldSQL, sql)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Rows(rows int64) zap.Field {
|
||||||
|
return zap.Int64(FieldRows, rows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Err(err error) zap.Field {
|
||||||
|
return zap.Error(err)
|
||||||
|
}
|
||||||
99
backend/pkg/logger/field_test.go
Normal file
99
backend/pkg/logger/field_test.go
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestFieldConstants(t *testing.T) {
|
||||||
|
assert.Equal(t, "request_id", FieldRequestID)
|
||||||
|
assert.Equal(t, "provider_id", FieldProviderID)
|
||||||
|
assert.Equal(t, "model_name", FieldModelName)
|
||||||
|
assert.Equal(t, "method", FieldMethod)
|
||||||
|
assert.Equal(t, "path", FieldPath)
|
||||||
|
assert.Equal(t, "status", FieldStatusCode)
|
||||||
|
assert.Equal(t, "latency", FieldLatency)
|
||||||
|
assert.Equal(t, "client_ip", FieldClientIP)
|
||||||
|
assert.Equal(t, "query", FieldQuery)
|
||||||
|
assert.Equal(t, "body_size", FieldBodySize)
|
||||||
|
assert.Equal(t, "sql", FieldSQL)
|
||||||
|
assert.Equal(t, "rows_affected", FieldRows)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFieldConstructors(t *testing.T) {
|
||||||
|
t.Run("RequestID", func(t *testing.T) {
|
||||||
|
field := RequestID("test-id")
|
||||||
|
assert.Equal(t, FieldRequestID, field.Key)
|
||||||
|
assert.Equal(t, "test-id", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ProviderID", func(t *testing.T) {
|
||||||
|
field := ProviderID("provider-123")
|
||||||
|
assert.Equal(t, FieldProviderID, field.Key)
|
||||||
|
assert.Equal(t, "provider-123", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ModelName", func(t *testing.T) {
|
||||||
|
field := ModelName("gpt-4")
|
||||||
|
assert.Equal(t, FieldModelName, field.Key)
|
||||||
|
assert.Equal(t, "gpt-4", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Method", func(t *testing.T) {
|
||||||
|
field := Method("POST")
|
||||||
|
assert.Equal(t, FieldMethod, field.Key)
|
||||||
|
assert.Equal(t, "POST", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Path", func(t *testing.T) {
|
||||||
|
field := Path("/v1/chat")
|
||||||
|
assert.Equal(t, FieldPath, field.Key)
|
||||||
|
assert.Equal(t, "/v1/chat", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("StatusCode", func(t *testing.T) {
|
||||||
|
field := StatusCode(200)
|
||||||
|
assert.Equal(t, FieldStatusCode, field.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Latency", func(t *testing.T) {
|
||||||
|
field := Latency(int64(100))
|
||||||
|
assert.Equal(t, FieldLatency, field.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ClientIP", func(t *testing.T) {
|
||||||
|
field := ClientIP("127.0.0.1")
|
||||||
|
assert.Equal(t, FieldClientIP, field.Key)
|
||||||
|
assert.Equal(t, "127.0.0.1", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Query", func(t *testing.T) {
|
||||||
|
field := Query("key=value")
|
||||||
|
assert.Equal(t, FieldQuery, field.Key)
|
||||||
|
assert.Equal(t, "key=value", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("BodySize", func(t *testing.T) {
|
||||||
|
field := BodySize(1024)
|
||||||
|
assert.Equal(t, FieldBodySize, field.Key)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("SQL", func(t *testing.T) {
|
||||||
|
field := SQL("SELECT * FROM users")
|
||||||
|
assert.Equal(t, FieldSQL, field.Key)
|
||||||
|
assert.Equal(t, "SELECT * FROM users", field.String)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Rows", func(t *testing.T) {
|
||||||
|
field := Rows(42)
|
||||||
|
assert.Equal(t, FieldRows, field.Key)
|
||||||
|
assert.Equal(t, int64(42), field.Integer)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Err", func(t *testing.T) {
|
||||||
|
err := assert.AnError
|
||||||
|
field := Err(err)
|
||||||
|
assert.Equal(t, "error", field.Key)
|
||||||
|
})
|
||||||
|
}
|
||||||
130
backend/pkg/logger/gorm.go
Normal file
130
backend/pkg/logger/gorm.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gormlogger "gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GormLogger struct {
|
||||||
|
logger *zap.Logger
|
||||||
|
level zapcore.Level
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGormLogger(logger *zap.Logger) *GormLogger {
|
||||||
|
return &GormLogger{
|
||||||
|
logger: logger.Named("database"),
|
||||||
|
level: zapcore.DebugLevel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) LogMode(level gormlogger.LogLevel) gormlogger.Interface {
|
||||||
|
newLogger := &GormLogger{
|
||||||
|
logger: l.logger,
|
||||||
|
level: l.gormLevelToZap(level),
|
||||||
|
}
|
||||||
|
return newLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) Info(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
if l.level > zapcore.DebugLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log(ctx, zapcore.DebugLevel, msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) Warn(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
if l.level > zapcore.WarnLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log(ctx, zapcore.WarnLevel, msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) Error(ctx context.Context, msg string, data ...interface{}) {
|
||||||
|
if l.level > zapcore.ErrorLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
l.log(ctx, zapcore.ErrorLevel, msg, data...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) Trace(ctx context.Context, begin time.Time, fc func() (string, int64), err error) {
|
||||||
|
if l.level > zapcore.DebugLevel {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
elapsed := time.Since(begin)
|
||||||
|
sql, rows := fc()
|
||||||
|
|
||||||
|
fields := []zap.Field{
|
||||||
|
zap.String("sql", l.formatSQL(sql)),
|
||||||
|
zap.Int64("rows", rows),
|
||||||
|
zap.Duration("latency", elapsed),
|
||||||
|
}
|
||||||
|
|
||||||
|
if requestIDField := RequestIDFromContext(ctx); requestIDField != zap.Skip() {
|
||||||
|
fields = append([]zap.Field{requestIDField}, fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
fields = append(fields, zap.Error(err))
|
||||||
|
l.logger.Error("SQL执行错误", fields...)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
l.logger.Debug("SQL查询", fields...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) log(ctx context.Context, level zapcore.Level, msg string, data ...interface{}) {
|
||||||
|
fields := make([]zap.Field, 0, len(data)/2+1)
|
||||||
|
|
||||||
|
if requestIDField := RequestIDFromContext(ctx); requestIDField != zap.Skip() {
|
||||||
|
fields = append(fields, requestIDField)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(data); i += 2 {
|
||||||
|
if i+1 < len(data) {
|
||||||
|
key, ok := data[i].(string)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
fields = append(fields, zap.Any(key, data[i+1]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch level {
|
||||||
|
case zapcore.DebugLevel:
|
||||||
|
l.logger.Debug(fmt.Sprintf(msg, data...), fields...)
|
||||||
|
case zapcore.WarnLevel:
|
||||||
|
l.logger.Warn(fmt.Sprintf(msg, data...), fields...)
|
||||||
|
case zapcore.ErrorLevel:
|
||||||
|
l.logger.Error(fmt.Sprintf(msg, data...), fields...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) gormLevelToZap(level gormlogger.LogLevel) zapcore.Level {
|
||||||
|
switch level {
|
||||||
|
case gormlogger.Silent:
|
||||||
|
return zapcore.PanicLevel
|
||||||
|
case gormlogger.Error:
|
||||||
|
return zapcore.ErrorLevel
|
||||||
|
case gormlogger.Warn:
|
||||||
|
return zapcore.WarnLevel
|
||||||
|
case gormlogger.Info:
|
||||||
|
return zapcore.DebugLevel
|
||||||
|
default:
|
||||||
|
return zapcore.DebugLevel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *GormLogger) formatSQL(sql string) string {
|
||||||
|
re := regexp.MustCompile(`\s+`)
|
||||||
|
return strings.TrimSpace(re.ReplaceAllString(sql, " "))
|
||||||
|
}
|
||||||
131
backend/pkg/logger/gorm_test.go
Normal file
131
backend/pkg/logger/gorm_test.go
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
gormlogger "gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNewGormLogger(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
assert.NotNil(t, gormLogger)
|
||||||
|
assert.NotNil(t, gormLogger.logger)
|
||||||
|
assert.Equal(t, zap.DebugLevel, gormLogger.level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_LogMode(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
level gormlogger.LogLevel
|
||||||
|
expectedLevel zapcore.Level
|
||||||
|
}{
|
||||||
|
{"Silent", gormlogger.Silent, zapcore.PanicLevel},
|
||||||
|
{"Error", gormlogger.Error, zapcore.ErrorLevel},
|
||||||
|
{"Warn", gormlogger.Warn, zapcore.WarnLevel},
|
||||||
|
{"Info", gormlogger.Info, zapcore.DebugLevel},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
newLogger := gormLogger.LogMode(tt.level)
|
||||||
|
assert.NotNil(t, newLogger)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_Trace(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
begin := time.Now()
|
||||||
|
|
||||||
|
fc := func() (string, int64) {
|
||||||
|
return "SELECT * FROM users", 10
|
||||||
|
}
|
||||||
|
|
||||||
|
gormLogger.Trace(ctx, begin, fc, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_TraceWithError(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
begin := time.Now()
|
||||||
|
|
||||||
|
fc := func() (string, int64) {
|
||||||
|
return "SELECT * FROM users", 0
|
||||||
|
}
|
||||||
|
|
||||||
|
gormLogger.Trace(ctx, begin, fc, gorm.ErrRecordNotFound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_Info(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
gormLogger.Info(ctx, "test info message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_Warn(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
gormLogger.Warn(ctx, "test warn message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_Error(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
gormLogger.Error(ctx, "test error message")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestGormLogger_FormatSQL(t *testing.T) {
|
||||||
|
logger := zap.NewNop()
|
||||||
|
gormLogger := NewGormLogger(logger)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
input string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "simple query",
|
||||||
|
input: "SELECT * FROM users",
|
||||||
|
expected: "SELECT * FROM users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query with extra spaces",
|
||||||
|
input: "SELECT * FROM users",
|
||||||
|
expected: "SELECT * FROM users",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "query with newlines",
|
||||||
|
input: "SELECT\n*\nFROM\nusers",
|
||||||
|
expected: "SELECT * FROM users",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
result := gormLogger.formatSQL(tt.input)
|
||||||
|
assert.Equal(t, tt.expected, result)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -50,6 +50,7 @@ func New(cfg Config) (*zap.Logger, error) {
|
|||||||
EncodeTime: zapcore.ISO8601TimeEncoder,
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
EncodeDuration: zapcore.StringDurationEncoder,
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
EncodeCaller: zapcore.ShortCallerEncoder,
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
EncodeName: encodeLoggerName,
|
||||||
})
|
})
|
||||||
|
|
||||||
stdoutCore := zapcore.NewCore(
|
stdoutCore := zapcore.NewCore(
|
||||||
@@ -115,3 +116,10 @@ func logFileName() string {
|
|||||||
func logFilePath(dir string) string {
|
func logFilePath(dir string) string {
|
||||||
return filepath.Join(dir, logFileName())
|
return filepath.Join(dir, logFileName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeLoggerName 自定义 logger 名称编码器,输出 [name] 格式
|
||||||
|
func encodeLoggerName(name string, enc zapcore.PrimitiveArrayEncoder) {
|
||||||
|
if name != "" {
|
||||||
|
enc.AppendString("[" + name + "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
35
backend/pkg/logger/minimal.go
Normal file
35
backend/pkg/logger/minimal.go
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"go.uber.org/zap"
|
||||||
|
"go.uber.org/zap/zapcore"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewMinimal() *zap.Logger {
|
||||||
|
encoder := zapcore.NewConsoleEncoder(zapcore.EncoderConfig{
|
||||||
|
TimeKey: "ts",
|
||||||
|
LevelKey: "level",
|
||||||
|
NameKey: "logger",
|
||||||
|
CallerKey: "caller",
|
||||||
|
FunctionKey: zapcore.OmitKey,
|
||||||
|
MessageKey: "msg",
|
||||||
|
StacktraceKey: "stacktrace",
|
||||||
|
LineEnding: zapcore.DefaultLineEnding,
|
||||||
|
EncodeLevel: zapcore.CapitalColorLevelEncoder,
|
||||||
|
EncodeTime: zapcore.ISO8601TimeEncoder,
|
||||||
|
EncodeDuration: zapcore.StringDurationEncoder,
|
||||||
|
EncodeCaller: zapcore.ShortCallerEncoder,
|
||||||
|
})
|
||||||
|
|
||||||
|
core := zapcore.NewCore(
|
||||||
|
encoder,
|
||||||
|
zapcore.AddSync(stdoutWriter{}),
|
||||||
|
zapcore.DebugLevel,
|
||||||
|
)
|
||||||
|
|
||||||
|
return zap.New(core, zap.AddCaller(), zap.AddStacktrace(zap.ErrorLevel))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Upgrade(minimalLogger *zap.Logger, cfg Config) (*zap.Logger, error) {
|
||||||
|
return New(cfg)
|
||||||
|
}
|
||||||
7
backend/pkg/logger/module.go
Normal file
7
backend/pkg/logger/module.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import "go.uber.org/zap"
|
||||||
|
|
||||||
|
func WithModule(logger *zap.Logger, moduleName string) *zap.Logger {
|
||||||
|
return logger.Named(moduleName)
|
||||||
|
}
|
||||||
25
backend/pkg/logger/module_test.go
Normal file
25
backend/pkg/logger/module_test.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"go.uber.org/zap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestWithModule(t *testing.T) {
|
||||||
|
baseLogger := zap.NewNop()
|
||||||
|
moduleLogger := WithModule(baseLogger, "handler.proxy")
|
||||||
|
|
||||||
|
assert.NotNil(t, moduleLogger)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestWithModuleMultiple(t *testing.T) {
|
||||||
|
baseLogger := zap.NewNop()
|
||||||
|
|
||||||
|
logger1 := WithModule(baseLogger, "handler.proxy")
|
||||||
|
logger2 := WithModule(baseLogger, "provider.client")
|
||||||
|
|
||||||
|
assert.NotNil(t, logger1)
|
||||||
|
assert.NotNil(t, logger2)
|
||||||
|
}
|
||||||
@@ -158,7 +158,10 @@ func TestSaveAndLoadConfig(t *testing.T) {
|
|||||||
WriteTimeout: 45 * time.Second,
|
WriteTimeout: 45 * time.Second,
|
||||||
},
|
},
|
||||||
Database: config.DatabaseConfig{
|
Database: config.DatabaseConfig{
|
||||||
|
Driver: "sqlite",
|
||||||
Path: filepath.Join(tmpDir, "test.db"),
|
Path: filepath.Join(tmpDir, "test.db"),
|
||||||
|
Port: 3306,
|
||||||
|
DBName: "nex",
|
||||||
MaxIdleConns: 15,
|
MaxIdleConns: 15,
|
||||||
MaxOpenConns: 150,
|
MaxOpenConns: 150,
|
||||||
ConnMaxLifetime: 2 * time.Hour,
|
ConnMaxLifetime: 2 * time.Hour,
|
||||||
|
|||||||
@@ -68,10 +68,10 @@ func setupConversionTest(t *testing.T) (*gin.Engine, *gorm.DB, *httptest.Server)
|
|||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
require.NoError(t, registry.Register(openaiConv.NewAdapter()))
|
require.NoError(t, registry.Register(openaiConv.NewAdapter()))
|
||||||
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, logger)
|
||||||
|
|
||||||
providerClient := provider.NewClient()
|
providerClient := provider.NewClient(logger)
|
||||||
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService)
|
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService, logger)
|
||||||
providerHandler := handler.NewProviderHandler(providerService)
|
providerHandler := handler.NewProviderHandler(providerService)
|
||||||
modelHandler := handler.NewModelHandler(modelService)
|
modelHandler := handler.NewModelHandler(modelService)
|
||||||
statsHandler := handler.NewStatsHandler(statsService)
|
statsHandler := handler.NewStatsHandler(statsService)
|
||||||
|
|||||||
@@ -61,10 +61,10 @@ func setupE2ETest(t *testing.T) (*gin.Engine, *httptest.Server) {
|
|||||||
registry := conversion.NewMemoryRegistry()
|
registry := conversion.NewMemoryRegistry()
|
||||||
require.NoError(t, registry.Register(openaiConv.NewAdapter()))
|
require.NoError(t, registry.Register(openaiConv.NewAdapter()))
|
||||||
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
require.NoError(t, registry.Register(anthropic.NewAdapter()))
|
||||||
engine := conversion.NewConversionEngine(registry, nil)
|
engine := conversion.NewConversionEngine(registry, logger)
|
||||||
|
|
||||||
providerClient := provider.NewClient()
|
providerClient := provider.NewClient(logger)
|
||||||
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService)
|
proxyHandler := handler.NewProxyHandler(engine, providerClient, routingService, providerService, statsService, logger)
|
||||||
providerHandler := handler.NewProviderHandler(providerService)
|
providerHandler := handler.NewProviderHandler(providerService)
|
||||||
modelHandler := handler.NewModelHandler(modelService)
|
modelHandler := handler.NewModelHandler(modelService)
|
||||||
|
|
||||||
|
|||||||
158
backend/tests/mysql/concurrent_test.go
Normal file
158
backend/tests/mysql/concurrent_test.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
//go:build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sync"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"nex/backend/internal/config"
|
||||||
|
"nex/backend/internal/repository"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConcurrent_UsageStatsRecord(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
statsRepo := repository.NewStatsRepository(db)
|
||||||
|
|
||||||
|
providerID := "concurrent-test-provider"
|
||||||
|
modelName := "gpt-4"
|
||||||
|
|
||||||
|
concurrency := 10
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(concurrency)
|
||||||
|
|
||||||
|
errChan := make(chan error, concurrency)
|
||||||
|
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
err := statsRepo.Record(providerID, modelName)
|
||||||
|
if err != nil {
|
||||||
|
errChan <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
close(errChan)
|
||||||
|
|
||||||
|
var errorCount int
|
||||||
|
uniqueErrors := make(map[string]int)
|
||||||
|
for err := range errChan {
|
||||||
|
errorCount++
|
||||||
|
uniqueErrors[err.Error()]++
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Logf("并发 %d 次,错误 %d 次", concurrency, errorCount)
|
||||||
|
for errMsg, count := range uniqueErrors {
|
||||||
|
t.Logf(" 错误: %s (出现 %d 次)", errMsg, count)
|
||||||
|
}
|
||||||
|
|
||||||
|
var stats config.UsageStats
|
||||||
|
err := db.Where("provider_id = ? AND model_name = ?", providerID, modelName).
|
||||||
|
First(&stats).Error
|
||||||
|
require.NoError(t, err, "应能查到 usage_stats 记录")
|
||||||
|
|
||||||
|
successCount := concurrency - errorCount
|
||||||
|
t.Logf("成功次数: %d, 最终 request_count: %d", successCount, stats.RequestCount)
|
||||||
|
|
||||||
|
assert.Equal(t, concurrency, stats.RequestCount, "request_count 应等于并发数,无数据丢失或重复")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrent_ProviderCreate(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
providerID := "concurrent-provider-id"
|
||||||
|
concurrency := 10
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(concurrency)
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
provider := config.Provider{
|
||||||
|
ID: providerID,
|
||||||
|
Name: "Concurrent Provider",
|
||||||
|
APIKey: "test-key",
|
||||||
|
BaseURL: "https://test.com",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Create(&provider).Error
|
||||||
|
if err == nil {
|
||||||
|
mu.Lock()
|
||||||
|
successCount++
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.Equal(t, 1, successCount, "仅 1 个创建应成功")
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&config.Provider{}).Where("id = ?", providerID).Count(&count)
|
||||||
|
assert.Equal(t, int64(1), count, "最终应有 1 条记录")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConcurrent_ModelCreate(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
provider := config.Provider{
|
||||||
|
ID: "concurrent-model-provider",
|
||||||
|
Name: "Test Provider",
|
||||||
|
APIKey: "test-key",
|
||||||
|
BaseURL: "https://test.com",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
err := db.Create(&provider).Error
|
||||||
|
require.NoError(t, err, "创建 provider 应成功")
|
||||||
|
|
||||||
|
modelName := "gpt-4-concurrent"
|
||||||
|
concurrency := 10
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(concurrency)
|
||||||
|
|
||||||
|
successCount := 0
|
||||||
|
var mu sync.Mutex
|
||||||
|
|
||||||
|
for i := 0; i < concurrency; i++ {
|
||||||
|
go func(idx int) {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
model := config.Model{
|
||||||
|
ID: uuid.New().String(),
|
||||||
|
ProviderID: provider.ID,
|
||||||
|
ModelName: modelName,
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Create(&model).Error
|
||||||
|
if err == nil {
|
||||||
|
mu.Lock()
|
||||||
|
successCount++
|
||||||
|
mu.Unlock()
|
||||||
|
}
|
||||||
|
}(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
assert.Equal(t, 1, successCount, "仅 1 个创建应成功")
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
db.Model(&config.Model{}).Where("provider_id = ? AND model_name = ?", provider.ID, modelName).Count(&count)
|
||||||
|
assert.Equal(t, int64(1), count, "最终应有 1 条记录")
|
||||||
|
}
|
||||||
130
backend/tests/mysql/constraint_test.go
Normal file
130
backend/tests/mysql/constraint_test.go
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
//go:build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
|
||||||
|
"nex/backend/internal/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConstraint_ForeignKeyEnforced(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
model := config.Model{
|
||||||
|
ID: "test-model-id",
|
||||||
|
ProviderID: "non-existent-provider",
|
||||||
|
ModelName: "gpt-4",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
err := db.Create(&model).Error
|
||||||
|
assert.Error(t, err, "创建 model 时 provider_id 不存在应失败")
|
||||||
|
assert.Contains(t, err.Error(), "foreign key constraint", "错误应为外键约束错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstraint_CascadeDelete(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
provider := config.Provider{
|
||||||
|
ID: "test-provider-cascade",
|
||||||
|
Name: "Test Provider",
|
||||||
|
APIKey: "test-key",
|
||||||
|
BaseURL: "https://test.com",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
err := db.Create(&provider).Error
|
||||||
|
require.NoError(t, err, "创建 provider 应成功")
|
||||||
|
|
||||||
|
model := config.Model{
|
||||||
|
ID: "test-model-cascade",
|
||||||
|
ProviderID: provider.ID,
|
||||||
|
ModelName: "gpt-4",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
err = db.Create(&model).Error
|
||||||
|
require.NoError(t, err, "创建 model 应成功")
|
||||||
|
|
||||||
|
err = db.Delete(&provider).Error
|
||||||
|
require.NoError(t, err, "删除 provider 应成功")
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
err = db.Model(&config.Model{}).Where("provider_id = ?", provider.ID).Count(&count).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.Equal(t, int64(0), count, "删除 provider 后其 models 应被级联删除")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstraint_UniqueProviderModel(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
provider := config.Provider{
|
||||||
|
ID: "test-provider-unique",
|
||||||
|
Name: "Test Provider",
|
||||||
|
APIKey: "test-key",
|
||||||
|
BaseURL: "https://test.com",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
err := db.Create(&provider).Error
|
||||||
|
require.NoError(t, err, "创建 provider 应成功")
|
||||||
|
|
||||||
|
model1 := config.Model{
|
||||||
|
ID: "test-model-unique-1",
|
||||||
|
ProviderID: provider.ID,
|
||||||
|
ModelName: "gpt-4",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
err = db.Create(&model1).Error
|
||||||
|
require.NoError(t, err, "创建第一个 model 应成功")
|
||||||
|
|
||||||
|
model2 := config.Model{
|
||||||
|
ID: "test-model-unique-2",
|
||||||
|
ProviderID: provider.ID,
|
||||||
|
ModelName: "gpt-4",
|
||||||
|
Enabled: true,
|
||||||
|
}
|
||||||
|
err = db.Create(&model2).Error
|
||||||
|
assert.Error(t, err, "创建相同 (provider_id, model_name) 的 model 应失败")
|
||||||
|
assert.True(t, errors.Is(err, gorm.ErrDuplicatedKey) ||
|
||||||
|
(err != nil && (err.Error() == "Error 1062" || containsDuplicateError(err.Error()))),
|
||||||
|
"错误应为唯一约束错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConstraint_UniqueUsageStats(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
todayTime, _ := time.Parse("2006-01-02", today)
|
||||||
|
|
||||||
|
providerID := "test-provider-unique-stats"
|
||||||
|
|
||||||
|
stats1 := config.UsageStats{
|
||||||
|
ProviderID: providerID,
|
||||||
|
ModelName: "gpt-4",
|
||||||
|
RequestCount: 10,
|
||||||
|
Date: todayTime,
|
||||||
|
}
|
||||||
|
err := db.Create(&stats1).Error
|
||||||
|
require.NoError(t, err, "创建第一个 usage_stats 应成功")
|
||||||
|
|
||||||
|
stats2 := config.UsageStats{
|
||||||
|
ProviderID: providerID,
|
||||||
|
ModelName: "gpt-4",
|
||||||
|
RequestCount: 20,
|
||||||
|
Date: todayTime,
|
||||||
|
}
|
||||||
|
err = db.Create(&stats2).Error
|
||||||
|
assert.Error(t, err, "创建相同 (provider_id, model_name, date) 的 usage_stats 应失败")
|
||||||
|
assert.True(t, errors.Is(err, gorm.ErrDuplicatedKey) ||
|
||||||
|
(err != nil && (err.Error() == "Error 1062" || containsDuplicateError(err.Error()))),
|
||||||
|
"错误应为唯一约束错误")
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsDuplicateError(errStr string) bool {
|
||||||
|
return len(errStr) > 0 && (errStr[0:8] == "Error 10" || errStr[0:5] == "Dupli")
|
||||||
|
}
|
||||||
21
backend/tests/mysql/docker-compose.yml
Normal file
21
backend/tests/mysql/docker-compose.yml
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
version: '3.8'
|
||||||
|
|
||||||
|
services:
|
||||||
|
mysql:
|
||||||
|
image: mysql:8.0
|
||||||
|
container_name: nex-mysql-test
|
||||||
|
environment:
|
||||||
|
MYSQL_ROOT_PASSWORD: testpass
|
||||||
|
MYSQL_DATABASE: nex_test
|
||||||
|
MYSQL_USER: nex_test
|
||||||
|
MYSQL_PASSWORD: testpass
|
||||||
|
ports:
|
||||||
|
- "13306:3306"
|
||||||
|
tmpfs:
|
||||||
|
- /var/lib/mysql
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD", "mysqladmin", "ping", "-h", "localhost", "-u", "root", "-p$$MYSQL_ROOT_PASSWORD"]
|
||||||
|
interval: 1s
|
||||||
|
timeout: 5s
|
||||||
|
retries: 10
|
||||||
|
start_period: 10s
|
||||||
126
backend/tests/mysql/migration_test.go
Normal file
126
backend/tests/mysql/migration_test.go
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
//go:build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMigration_TablesExist(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
var tables []string
|
||||||
|
err := db.Raw("SHOW TABLES").Scan(&tables).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
expectedTables := []string{"providers", "models", "usage_stats"}
|
||||||
|
for _, expected := range expectedTables {
|
||||||
|
assert.Contains(t, tables, expected, "表 %s 应存在", expected)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigration_TableColumns(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
t.Run("providers 表字段", func(t *testing.T) {
|
||||||
|
var columns []struct {
|
||||||
|
Field string
|
||||||
|
Type string
|
||||||
|
Null string
|
||||||
|
}
|
||||||
|
err := db.Raw("SHOW COLUMNS FROM providers").Scan(&columns).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
columnMap := make(map[string]string)
|
||||||
|
for _, col := range columns {
|
||||||
|
columnMap[col.Field] = col.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Contains(t, columnMap["id"], "varchar", "id 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["name"], "varchar", "name 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["api_key"], "varchar", "api_key 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["base_url"], "varchar", "base_url 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["protocol"], "varchar", "protocol 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["enabled"], "tinyint", "enabled 应为 TINYINT (BOOLEAN) 类型")
|
||||||
|
assert.Contains(t, columnMap["created_at"], "datetime", "created_at 应为 DATETIME 类型")
|
||||||
|
assert.Contains(t, columnMap["updated_at"], "datetime", "updated_at 应为 DATETIME 类型")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("models 表字段", func(t *testing.T) {
|
||||||
|
var columns []struct {
|
||||||
|
Field string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
err := db.Raw("SHOW COLUMNS FROM models").Scan(&columns).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
columnMap := make(map[string]string)
|
||||||
|
for _, col := range columns {
|
||||||
|
columnMap[col.Field] = col.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Contains(t, columnMap["id"], "varchar", "id 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["provider_id"], "varchar", "provider_id 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["model_name"], "varchar", "model_name 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["enabled"], "tinyint", "enabled 应为 TINYINT (BOOLEAN) 类型")
|
||||||
|
assert.Contains(t, columnMap["created_at"], "datetime", "created_at 应为 DATETIME 类型")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("usage_stats 表字段", func(t *testing.T) {
|
||||||
|
var columns []struct {
|
||||||
|
Field string
|
||||||
|
Type string
|
||||||
|
}
|
||||||
|
err := db.Raw("SHOW COLUMNS FROM usage_stats").Scan(&columns).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
columnMap := make(map[string]string)
|
||||||
|
for _, col := range columns {
|
||||||
|
columnMap[col.Field] = col.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Contains(t, columnMap["id"], "int", "id 应为 INT 类型")
|
||||||
|
assert.Contains(t, columnMap["provider_id"], "varchar", "provider_id 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["model_name"], "varchar", "model_name 应为 VARCHAR 类型")
|
||||||
|
assert.Contains(t, columnMap["request_count"], "int", "request_count 应为 INT 类型")
|
||||||
|
assert.Contains(t, columnMap["date"], "date", "date 应为 DATE 类型")
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMigration_IndexesExist(t *testing.T) {
|
||||||
|
db := SetupMySQLTestDB(t)
|
||||||
|
|
||||||
|
t.Run("models 表索引", func(t *testing.T) {
|
||||||
|
var indexes []struct {
|
||||||
|
KeyName string
|
||||||
|
}
|
||||||
|
err := db.Raw("SHOW INDEX FROM models").Scan(&indexes).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexMap := make(map[string]bool)
|
||||||
|
for _, idx := range indexes {
|
||||||
|
indexMap[idx.KeyName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, indexMap["idx_models_provider_id"], "idx_models_provider_id 索引应存在")
|
||||||
|
assert.True(t, indexMap["idx_models_model_name"], "idx_models_model_name 索引应存在")
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("usage_stats 表索引", func(t *testing.T) {
|
||||||
|
var indexes []struct {
|
||||||
|
KeyName string
|
||||||
|
}
|
||||||
|
err := db.Raw("SHOW INDEX FROM usage_stats").Scan(&indexes).Error
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
indexMap := make(map[string]bool)
|
||||||
|
for _, idx := range indexes {
|
||||||
|
indexMap[idx.KeyName] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.True(t, indexMap["idx_usage_stats_provider_model_date"], "idx_usage_stats_provider_model_date 索引应存在")
|
||||||
|
})
|
||||||
|
}
|
||||||
160
backend/tests/mysql/testhelper.go
Normal file
160
backend/tests/mysql/testhelper.go
Normal file
@@ -0,0 +1,160 @@
|
|||||||
|
//go:build mysql
|
||||||
|
|
||||||
|
package mysql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/pressly/goose/v3"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gorm.io/driver/mysql"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MySQLTestConfig struct {
|
||||||
|
Host string
|
||||||
|
Port int
|
||||||
|
User string
|
||||||
|
Password string
|
||||||
|
Database string
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMySQLTestConfig() *MySQLTestConfig {
|
||||||
|
return &MySQLTestConfig{
|
||||||
|
Host: getEnvOrDefault("NEX_TEST_MYSQL_HOST", "localhost"),
|
||||||
|
Port: getEnvOrDefaultInt("NEX_TEST_MYSQL_PORT", 13306),
|
||||||
|
User: getEnvOrDefault("NEX_TEST_MYSQL_USER", "nex_test"),
|
||||||
|
Password: getEnvOrDefault("NEX_TEST_MYSQL_PASSWORD", "testpass"),
|
||||||
|
Database: getEnvOrDefault("NEX_TEST_MYSQL_DATABASE", "nex_test"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefault(key, defaultValue string) string {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
return value
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func getEnvOrDefaultInt(key string, defaultValue int) int {
|
||||||
|
if value := os.Getenv(key); value != "" {
|
||||||
|
var intValue int
|
||||||
|
if _, err := fmt.Sscanf(value, "%d", &intValue); err == nil {
|
||||||
|
return intValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return defaultValue
|
||||||
|
}
|
||||||
|
|
||||||
|
func SkipIfMySQLUnavailable(t *testing.T) {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cfg := getMySQLTestConfig()
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
|
||||||
|
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||||
|
|
||||||
|
db, err := sql.Open("mysql", dsn)
|
||||||
|
if err != nil {
|
||||||
|
t.Skipf("MySQL 不可用: %v", err)
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
if err := db.Ping(); err != nil {
|
||||||
|
t.Skipf("MySQL 不可用: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetupMySQLTestDB(t *testing.T) *gorm.DB {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
SkipIfMySQLUnavailable(t)
|
||||||
|
|
||||||
|
cfg := getMySQLTestConfig()
|
||||||
|
dsn := fmt.Sprintf("%s:%s@tcp(%s:%d)/%s?charset=utf8mb4&parseTime=true&loc=Local",
|
||||||
|
cfg.User, cfg.Password, cfg.Host, cfg.Port, cfg.Database)
|
||||||
|
|
||||||
|
db, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
|
||||||
|
Logger: logger.Default.LogMode(logger.Silent),
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "连接 MySQL 失败")
|
||||||
|
|
||||||
|
if err := runMigrations(db); err != nil {
|
||||||
|
require.NoError(t, err, "运行迁移失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := cleanupTables(db); err != nil {
|
||||||
|
require.NoError(t, err, "清理表数据失败")
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
require.NoError(t, err)
|
||||||
|
sqlDB.SetMaxIdleConns(10)
|
||||||
|
sqlDB.SetMaxOpenConns(100)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
|
||||||
|
t.Cleanup(func() {
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err == nil {
|
||||||
|
sqlDB.Close()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupTables(db *gorm.DB) error {
|
||||||
|
if err := db.Exec("SET FOREIGN_KEY_CHECKS = 0").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec("TRUNCATE TABLE usage_stats").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec("TRUNCATE TABLE models").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec("TRUNCATE TABLE providers").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := db.Exec("SET FOREIGN_KEY_CHECKS = 1").Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func runMigrations(db *gorm.DB) error {
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
migrationsDir := getMigrationsDir()
|
||||||
|
if _, err := os.Stat(migrationsDir); os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("迁移目录不存在: %s", migrationsDir)
|
||||||
|
}
|
||||||
|
|
||||||
|
goose.SetDialect("mysql")
|
||||||
|
if err := goose.Up(sqlDB, migrationsDir); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMigrationsDir() string {
|
||||||
|
_, filename, _, ok := runtime.Caller(0)
|
||||||
|
if ok {
|
||||||
|
dir := filepath.Join(filepath.Dir(filename), "..", "..", "migrations", "mysql")
|
||||||
|
if abs, err := filepath.Abs(dir); err == nil {
|
||||||
|
return abs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "./migrations/mysql"
|
||||||
|
}
|
||||||
@@ -33,6 +33,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.9.0",
|
||||||
|
"javascript-obfuscator": "^5.4.1",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"msw": "^2.8.2",
|
"msw": "^2.8.2",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
@@ -40,6 +41,7 @@
|
|||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.4",
|
"vite": "^8.0.4",
|
||||||
|
"vite-plugin-javascript-obfuscator": "^3.1.0",
|
||||||
"vitest": "^3.2.1",
|
"vitest": "^3.2.1",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
@@ -191,10 +193,20 @@
|
|||||||
|
|
||||||
"@inquirer/type": ["@inquirer/type@3.0.10", "https://registry.npmmirror.com/@inquirer/type/-/type-3.0.10.tgz", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
|
"@inquirer/type": ["@inquirer/type@3.0.10", "https://registry.npmmirror.com/@inquirer/type/-/type-3.0.10.tgz", { "peerDependencies": { "@types/node": ">=18" }, "optionalPeers": ["@types/node"] }, "sha512-BvziSRxfz5Ov8ch0z/n3oijRSEcEsHnhggm4xFZe93DHcUCTlutlq9Ox4SVENAfcRD22UQq7T/atg9Wr3k09eA=="],
|
||||||
|
|
||||||
|
"@inversifyjs/common": ["@inversifyjs/common@1.3.3", "https://registry.npmmirror.com/@inversifyjs/common/-/common-1.3.3.tgz", {}, "sha512-ZH0wrgaJwIo3s9gMCDM2wZoxqrJ6gB97jWXncROfYdqZJv8f3EkqT57faZqN5OTeHWgtziQ6F6g3L8rCvGceCw=="],
|
||||||
|
|
||||||
|
"@inversifyjs/core": ["@inversifyjs/core@1.3.4", "https://registry.npmmirror.com/@inversifyjs/core/-/core-1.3.4.tgz", { "dependencies": { "@inversifyjs/common": "1.3.3", "@inversifyjs/reflect-metadata-utils": "0.2.3" } }, "sha512-gCCmA4BdbHEFwvVZ2elWgHuXZWk6AOu/1frxsS+2fWhjEk2c/IhtypLo5ytSUie1BCiT6i9qnEo4bruBomQsAA=="],
|
||||||
|
|
||||||
|
"@inversifyjs/reflect-metadata-utils": ["@inversifyjs/reflect-metadata-utils@0.2.3", "https://registry.npmmirror.com/@inversifyjs/reflect-metadata-utils/-/reflect-metadata-utils-0.2.3.tgz", { "peerDependencies": { "reflect-metadata": "0.2.2" } }, "sha512-d3D0o9TeSlvaGM2I24wcNw/Aj3rc4OYvHXOKDC09YEph5fMMiKd6fq1VTQd9tOkDNWvVbw+cnt45Wy9P/t5Lvw=="],
|
||||||
|
|
||||||
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
"@isaacs/cliui": ["@isaacs/cliui@8.0.2", "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="],
|
||||||
|
|
||||||
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.6.tgz", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="],
|
"@istanbuljs/schema": ["@istanbuljs/schema@0.1.6", "https://registry.npmmirror.com/@istanbuljs/schema/-/schema-0.1.6.tgz", {}, "sha512-+Sg6GCR/wy1oSmQDFq4LQDAhm3ETKnorxN+y5nbLULOR3P0c14f2Wurzj3/xqPXtasLFfHd5iRFQ7AJt4KH2cw=="],
|
||||||
|
|
||||||
|
"@javascript-obfuscator/escodegen": ["@javascript-obfuscator/escodegen@2.4.1", "https://registry.npmmirror.com/@javascript-obfuscator/escodegen/-/escodegen-2.4.1.tgz", { "dependencies": { "@javascript-obfuscator/estraverse": "^5.3.0", "esprima": "^4.0.1", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" } }, "sha512-YrEJJDr4cb+pIQKWzHFoDlDkQzatcrNB6OhAD6iTSwiKwzZUMVdobwbOuLpF4EiLxUj0qP28Xl1saTHYzIPCLg=="],
|
||||||
|
|
||||||
|
"@javascript-obfuscator/estraverse": ["@javascript-obfuscator/estraverse@5.4.0", "https://registry.npmmirror.com/@javascript-obfuscator/estraverse/-/estraverse-5.4.0.tgz", {}, "sha512-CZFX7UZVN9VopGbjTx4UXaXsi9ewoM1buL0kY7j1ftYdSs7p2spv9opxFjHlQ/QGTgh4UqufYqJJ0WKLml7b6w=="],
|
||||||
|
|
||||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
|
||||||
|
|
||||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "https://registry.npmmirror.com/@jridgewell/remapping/-/remapping-2.3.5.tgz", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ=="],
|
||||||
@@ -389,6 +401,8 @@
|
|||||||
|
|
||||||
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
"@types/json5": ["@types/json5@0.0.29", "https://registry.npmmirror.com/@types/json5/-/json5-0.0.29.tgz", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="],
|
||||||
|
|
||||||
|
"@types/minimatch": ["@types/minimatch@3.0.5", "https://registry.npmmirror.com/@types/minimatch/-/minimatch-3.0.5.tgz", {}, "sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ=="],
|
||||||
|
|
||||||
"@types/node": ["@types/node@24.12.2", "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
|
"@types/node": ["@types/node@24.12.2", "https://registry.npmmirror.com/@types/node/-/node-24.12.2.tgz", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g=="],
|
||||||
|
|
||||||
"@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
"@types/react": ["@types/react@19.2.14", "https://registry.npmmirror.com/@types/react/-/react-19.2.14.tgz", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w=="],
|
||||||
@@ -429,6 +443,8 @@
|
|||||||
|
|
||||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="],
|
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "https://registry.npmmirror.com/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="],
|
||||||
|
|
||||||
|
"@vercel/blob": ["@vercel/blob@2.3.3", "https://registry.npmmirror.com/@vercel/blob/-/blob-2.3.3.tgz", { "dependencies": { "async-retry": "^1.3.3", "is-buffer": "^2.0.5", "is-node-process": "^1.2.0", "throttleit": "^2.1.0", "undici": "^6.23.0" } }, "sha512-MtD7VLo6hU07eHR7bmk5SIMD290q574UaNYTe46qeyRT+hWrCy26CoAqfd7PnIefVXvRehRZBzukxuTO9iGTVg=="],
|
||||||
|
|
||||||
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
"@vitejs/plugin-react": ["@vitejs/plugin-react@6.0.1", "https://registry.npmmirror.com/@vitejs/plugin-react/-/plugin-react-6.0.1.tgz", { "dependencies": { "@rolldown/pluginutils": "1.0.0-rc.7" }, "peerDependencies": { "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0", "babel-plugin-react-compiler": "^1.0.0", "vite": "^8.0.0" }, "optionalPeers": ["@rolldown/plugin-babel", "babel-plugin-react-compiler"] }, "sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ=="],
|
||||||
|
|
||||||
"@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="],
|
"@vitest/coverage-v8": ["@vitest/coverage-v8@3.2.4", "https://registry.npmmirror.com/@vitest/coverage-v8/-/coverage-v8-3.2.4.tgz", { "dependencies": { "@ampproject/remapping": "^2.3.0", "@bcoe/v8-coverage": "^1.0.2", "ast-v8-to-istanbul": "^0.3.3", "debug": "^4.4.1", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", "istanbul-lib-source-maps": "^5.0.6", "istanbul-reports": "^3.1.7", "magic-string": "^0.30.17", "magicast": "^0.3.5", "std-env": "^3.9.0", "test-exclude": "^7.0.1", "tinyrainbow": "^2.0.0" }, "peerDependencies": { "@vitest/browser": "3.2.4", "vitest": "3.2.4" }, "optionalPeers": ["@vitest/browser"] }, "sha512-EyF9SXU6kS5Ku/U82E259WSnvg6c8KTjppUncuNdm5QHpe17mwREHnjDzozC8x9MZ0xfBUFSaLkRv4TMA75ALQ=="],
|
||||||
@@ -447,7 +463,9 @@
|
|||||||
|
|
||||||
"@vitest/utils": ["@vitest/utils@3.2.4", "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
"@vitest/utils": ["@vitest/utils@3.2.4", "https://registry.npmmirror.com/@vitest/utils/-/utils-3.2.4.tgz", { "dependencies": { "@vitest/pretty-format": "3.2.4", "loupe": "^3.1.4", "tinyrainbow": "^2.0.0" } }, "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA=="],
|
||||||
|
|
||||||
"acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
"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-import-attributes": ["acorn-import-attributes@1.9.5", "https://registry.npmmirror.com/acorn-import-attributes/-/acorn-import-attributes-1.9.5.tgz", { "peerDependencies": { "acorn": "^8" } }, "sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ=="],
|
||||||
|
|
||||||
"acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
"acorn-jsx": ["acorn-jsx@5.3.2", "https://registry.npmmirror.com/acorn-jsx/-/acorn-jsx-5.3.2.tgz", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="],
|
||||||
|
|
||||||
@@ -455,18 +473,26 @@
|
|||||||
|
|
||||||
"ajv": ["ajv@6.14.0", "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
"ajv": ["ajv@6.14.0", "https://registry.npmmirror.com/ajv/-/ajv-6.14.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw=="],
|
||||||
|
|
||||||
|
"ajv-formats": ["ajv-formats@3.0.1", "https://registry.npmmirror.com/ajv-formats/-/ajv-formats-3.0.1.tgz", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="],
|
||||||
|
|
||||||
"ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
"ansi-regex": ["ansi-regex@5.0.1", "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
|
||||||
|
|
||||||
"ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
"ansi-styles": ["ansi-styles@4.3.0", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
|
||||||
|
|
||||||
|
"anymatch": ["anymatch@3.1.3", "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
|
||||||
|
|
||||||
"argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
"argparse": ["argparse@2.0.1", "https://registry.npmmirror.com/argparse/-/argparse-2.0.1.tgz", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="],
|
||||||
|
|
||||||
"aria-query": ["aria-query@5.3.2", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
"aria-query": ["aria-query@5.3.2", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.2.tgz", {}, "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw=="],
|
||||||
|
|
||||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "https://registry.npmmirror.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw=="],
|
||||||
|
|
||||||
|
"array-differ": ["array-differ@3.0.0", "https://registry.npmmirror.com/array-differ/-/array-differ-3.0.0.tgz", {}, "sha512-THtfYS6KtME/yIAhKjZ2ul7XI96lQGHRputJQHO80LAWQnuGP4iCIN8vdMRboGbIEYBwU33q8Tch1os2+X0kMg=="],
|
||||||
|
|
||||||
"array-includes": ["array-includes@3.1.9", "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
"array-includes": ["array-includes@3.1.9", "https://registry.npmmirror.com/array-includes/-/array-includes-3.1.9.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ=="],
|
||||||
|
|
||||||
|
"array-union": ["array-union@2.1.0", "https://registry.npmmirror.com/array-union/-/array-union-2.1.0.tgz", {}, "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw=="],
|
||||||
|
|
||||||
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "https://registry.npmmirror.com/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ=="],
|
||||||
|
|
||||||
"array.prototype.flat": ["array.prototype.flat@1.3.3", "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
|
"array.prototype.flat": ["array.prototype.flat@1.3.3", "https://registry.npmmirror.com/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg=="],
|
||||||
@@ -475,12 +501,20 @@
|
|||||||
|
|
||||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "https://registry.npmmirror.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="],
|
||||||
|
|
||||||
|
"arrify": ["arrify@2.0.1", "https://registry.npmmirror.com/arrify/-/arrify-2.0.1.tgz", {}, "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug=="],
|
||||||
|
|
||||||
|
"assert": ["assert@2.1.0", "https://registry.npmmirror.com/assert/-/assert-2.1.0.tgz", { "dependencies": { "call-bind": "^1.0.2", "is-nan": "^1.3.2", "object-is": "^1.1.5", "object.assign": "^4.1.4", "util": "^0.12.5" } }, "sha512-eLHpSK/Y4nhMJ07gDaAzoX/XAKS8PSaojml3M0DM4JpV1LAi5JOJ/p6H/XWrl8L+DzVEvVCW1z3vWAaB9oTsQw=="],
|
||||||
|
|
||||||
"assertion-error": ["assertion-error@2.0.1", "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
"assertion-error": ["assertion-error@2.0.1", "https://registry.npmmirror.com/assertion-error/-/assertion-error-2.0.1.tgz", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="],
|
||||||
|
|
||||||
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="],
|
"ast-v8-to-istanbul": ["ast-v8-to-istanbul@0.3.12", "https://registry.npmmirror.com/ast-v8-to-istanbul/-/ast-v8-to-istanbul-0.3.12.tgz", { "dependencies": { "@jridgewell/trace-mapping": "^0.3.31", "estree-walker": "^3.0.3", "js-tokens": "^10.0.0" } }, "sha512-BRRC8VRZY2R4Z4lFIL35MwNXmwVqBityvOIwETtsCSwvjl0IdgFsy9NhdaA6j74nUdtJJlIypeRhpDam19Wq3g=="],
|
||||||
|
|
||||||
"async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
"async-function": ["async-function@1.0.0", "https://registry.npmmirror.com/async-function/-/async-function-1.0.0.tgz", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="],
|
||||||
|
|
||||||
|
"async-retry": ["async-retry@1.3.3", "https://registry.npmmirror.com/async-retry/-/async-retry-1.3.3.tgz", { "dependencies": { "retry": "0.13.1" } }, "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw=="],
|
||||||
|
|
||||||
|
"atomically": ["atomically@2.1.1", "https://registry.npmmirror.com/atomically/-/atomically-2.1.1.tgz", { "dependencies": { "stubborn-fs": "^2.0.0", "when-exit": "^2.1.4" } }, "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ=="],
|
||||||
|
|
||||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
"available-typed-arrays": ["available-typed-arrays@1.0.7", "https://registry.npmmirror.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ=="],
|
||||||
|
|
||||||
"balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
"balanced-match": ["balanced-match@1.0.2", "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
|
||||||
@@ -491,6 +525,8 @@
|
|||||||
|
|
||||||
"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
"browserslist": ["browserslist@4.28.2", "https://registry.npmmirror.com/browserslist/-/browserslist-4.28.2.tgz", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="],
|
||||||
|
|
||||||
|
"buffer-from": ["buffer-from@1.1.2", "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="],
|
||||||
|
|
||||||
"cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
"cac": ["cac@6.7.14", "https://registry.npmmirror.com/cac/-/cac-6.7.14.tgz", {}, "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ=="],
|
||||||
|
|
||||||
"call-bind": ["call-bind@1.0.9", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
"call-bind": ["call-bind@1.0.9", "https://registry.npmmirror.com/call-bind/-/call-bind-1.0.9.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, "sha512-a/hy+pNsFUTR+Iz8TCJvXudKVLAnz/DyeSUo10I5yvFDQJBFU2s9uqQpoSrJlroHUKoKqzg+epxyP9lqFdzfBQ=="],
|
||||||
@@ -507,10 +543,18 @@
|
|||||||
|
|
||||||
"chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
"chalk": ["chalk@4.1.2", "https://registry.npmmirror.com/chalk/-/chalk-4.1.2.tgz", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="],
|
||||||
|
|
||||||
|
"chance": ["chance@1.1.13", "https://registry.npmmirror.com/chance/-/chance-1.1.13.tgz", {}, "sha512-V6lQCljcLznE7tUYUM9EOAnnKXbctE6j/rdQkYOHIWbfGQbrzTsAXNW9CdU5XCo4ArXQCj/rb6HgxPlmGJcaUg=="],
|
||||||
|
|
||||||
|
"char-regex": ["char-regex@1.0.2", "https://registry.npmmirror.com/char-regex/-/char-regex-1.0.2.tgz", {}, "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw=="],
|
||||||
|
|
||||||
|
"charenc": ["charenc@0.0.2", "https://registry.npmmirror.com/charenc/-/charenc-0.0.2.tgz", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="],
|
||||||
|
|
||||||
"check-error": ["check-error@2.1.3", "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
"check-error": ["check-error@2.1.3", "https://registry.npmmirror.com/check-error/-/check-error-2.1.3.tgz", {}, "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "https://registry.npmmirror.com/chokidar/-/chokidar-4.0.3.tgz", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
|
"class-validator": ["class-validator@0.14.3", "https://registry.npmmirror.com/class-validator/-/class-validator-0.14.3.tgz", { "dependencies": { "@types/validator": "^13.15.3", "libphonenumber-js": "^1.11.1", "validator": "^13.15.20" } }, "sha512-rXXekcjofVN1LTOSw+u4u9WXVEUvNBVjORW154q/IdmYWy1nMbOU9aNtZB0t8m+FJQ9q91jlr2f9CwwUFdFMRA=="],
|
||||||
|
|
||||||
"classnames": ["classnames@2.5.1", "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
"classnames": ["classnames@2.5.1", "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz", {}, "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow=="],
|
||||||
|
|
||||||
"cli-width": ["cli-width@4.1.0", "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
"cli-width": ["cli-width@4.1.0", "https://registry.npmmirror.com/cli-width/-/cli-width-4.1.0.tgz", {}, "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ=="],
|
||||||
@@ -523,14 +567,20 @@
|
|||||||
|
|
||||||
"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-name": ["color-name@1.1.4", "https://registry.npmmirror.com/color-name/-/color-name-1.1.4.tgz", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
|
||||||
|
|
||||||
|
"commander": ["commander@12.1.0", "https://registry.npmmirror.com/commander/-/commander-12.1.0.tgz", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
|
||||||
|
|
||||||
"concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
"concat-map": ["concat-map@0.0.1", "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
|
||||||
|
|
||||||
|
"conf": ["conf@15.0.2", "https://registry.npmmirror.com/conf/-/conf-15.0.2.tgz", { "dependencies": { "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "atomically": "^2.0.3", "debounce-fn": "^6.0.0", "dot-prop": "^10.0.0", "env-paths": "^3.0.0", "json-schema-typed": "^8.0.1", "semver": "^7.7.2", "uint8array-extras": "^1.5.0" } }, "sha512-JBSrutapCafTrddF9dH3lc7+T2tBycGF4uPkI4Js+g4vLLEhG6RZcFi3aJd5zntdf5tQxAejJt8dihkoQ/eSJw=="],
|
||||||
|
|
||||||
"convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
"convert-source-map": ["convert-source-map@2.0.0", "https://registry.npmmirror.com/convert-source-map/-/convert-source-map-2.0.0.tgz", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
|
||||||
|
|
||||||
"cookie": ["cookie@1.1.1", "https://registry.npmmirror.com/cookie/-/cookie-1.1.1.tgz", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
|
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"crypt": ["crypt@0.0.2", "https://registry.npmmirror.com/crypt/-/crypt-0.0.2.tgz", {}, "sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow=="],
|
||||||
|
|
||||||
"css.escape": ["css.escape@1.5.1", "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
|
"css.escape": ["css.escape@1.5.1", "https://registry.npmmirror.com/css.escape/-/css.escape-1.5.1.tgz", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="],
|
||||||
|
|
||||||
"cssstyle": ["cssstyle@4.6.0", "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
|
"cssstyle": ["cssstyle@4.6.0", "https://registry.npmmirror.com/cssstyle/-/cssstyle-4.6.0.tgz", { "dependencies": { "@asamuzakjp/css-color": "^3.2.0", "rrweb-cssom": "^0.8.0" } }, "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg=="],
|
||||||
@@ -569,6 +619,8 @@
|
|||||||
|
|
||||||
"dayjs": ["dayjs@1.11.10", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz", {}, "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="],
|
"dayjs": ["dayjs@1.11.10", "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.10.tgz", {}, "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ=="],
|
||||||
|
|
||||||
|
"debounce-fn": ["debounce-fn@6.0.0", "https://registry.npmmirror.com/debounce-fn/-/debounce-fn-6.0.0.tgz", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-rBMW+F2TXryBwB54Q0d8drNEI+TfoS9JpNTAoVpukbWEhjXQq4rySFYLaqXMFXwdv61Zb2OHtj5bviSoimqxRQ=="],
|
||||||
|
|
||||||
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
"debug": ["debug@4.4.3", "https://registry.npmmirror.com/debug/-/debug-4.4.3.tgz", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||||
|
|
||||||
"decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
"decimal.js": ["decimal.js@10.6.0", "https://registry.npmmirror.com/decimal.js/-/decimal.js-10.6.0.tgz", {}, "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg=="],
|
||||||
@@ -593,6 +645,8 @@
|
|||||||
|
|
||||||
"dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
"dom-helpers": ["dom-helpers@5.2.1", "https://registry.npmmirror.com/dom-helpers/-/dom-helpers-5.2.1.tgz", { "dependencies": { "@babel/runtime": "^7.8.7", "csstype": "^3.0.2" } }, "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA=="],
|
||||||
|
|
||||||
|
"dot-prop": ["dot-prop@10.1.0", "https://registry.npmmirror.com/dot-prop/-/dot-prop-10.1.0.tgz", { "dependencies": { "type-fest": "^5.0.0" } }, "sha512-MVUtAugQMOff5RnBy2d9N31iG0lNwg1qAoAOn7pOK5wf94WIaE3My2p3uwTQuvS2AcqchkcR3bHByjaM0mmi7Q=="],
|
||||||
|
|
||||||
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
"dunder-proto": ["dunder-proto@1.0.1", "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
|
||||||
|
|
||||||
"eastasianwidth": ["eastasianwidth@0.2.0", "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
"eastasianwidth": ["eastasianwidth@0.2.0", "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
|
||||||
@@ -603,6 +657,8 @@
|
|||||||
|
|
||||||
"entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
"entities": ["entities@7.0.1", "https://registry.npmmirror.com/entities/-/entities-7.0.1.tgz", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
|
||||||
|
|
||||||
|
"env-paths": ["env-paths@4.0.0", "https://registry.npmmirror.com/env-paths/-/env-paths-4.0.0.tgz", { "dependencies": { "is-safe-filename": "^0.1.0" } }, "sha512-pxP8eL2SwwaTRi/KHYwLYXinDs7gL3jxFcBYmEdYfZmZXbaVDvdppd0XBU8qVz03rDfKZMXg1omHCbsJjZrMsw=="],
|
||||||
|
|
||||||
"es-abstract": ["es-abstract@1.24.2", "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
"es-abstract": ["es-abstract@1.24.2", "https://registry.npmmirror.com/es-abstract/-/es-abstract-1.24.2.tgz", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, "sha512-2FpH9Q5i2RRwyEP1AylXe6nYLR5OhaJTZwmlcP0dL/+JCbgg7yyEo/sEK6HeGZRf3dFpWwThaRHVApXSkW3xeg=="],
|
||||||
|
|
||||||
"es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
"es-define-property": ["es-define-property@1.0.1", "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
|
||||||
@@ -645,6 +701,8 @@
|
|||||||
|
|
||||||
"espree": ["espree@10.4.0", "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
"espree": ["espree@10.4.0", "https://registry.npmmirror.com/espree/-/espree-10.4.0.tgz", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ=="],
|
||||||
|
|
||||||
|
"esprima": ["esprima@4.0.1", "https://registry.npmmirror.com/esprima/-/esprima-4.0.1.tgz", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="],
|
||||||
|
|
||||||
"esquery": ["esquery@1.7.0", "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
"esquery": ["esquery@1.7.0", "https://registry.npmmirror.com/esquery/-/esquery-1.7.0.tgz", { "dependencies": { "estraverse": "^5.1.0" } }, "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g=="],
|
||||||
|
|
||||||
"esrecurse": ["esrecurse@4.3.0", "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
"esrecurse": ["esrecurse@4.3.0", "https://registry.npmmirror.com/esrecurse/-/esrecurse-4.3.0.tgz", { "dependencies": { "estraverse": "^5.2.0" } }, "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag=="],
|
||||||
@@ -665,6 +723,8 @@
|
|||||||
|
|
||||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
"fast-levenshtein": ["fast-levenshtein@2.0.6", "https://registry.npmmirror.com/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", {}, "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw=="],
|
||||||
|
|
||||||
|
"fast-uri": ["fast-uri@3.1.0", "https://registry.npmmirror.com/fast-uri/-/fast-uri-3.1.0.tgz", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="],
|
||||||
|
|
||||||
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
"fdir": ["fdir@6.5.0", "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="],
|
||||||
|
|
||||||
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
"file-entry-cache": ["file-entry-cache@8.0.0", "https://registry.npmmirror.com/file-entry-cache/-/file-entry-cache-8.0.0.tgz", { "dependencies": { "flat-cache": "^4.0.0" } }, "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ=="],
|
||||||
@@ -757,10 +817,16 @@
|
|||||||
|
|
||||||
"indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
"indent-string": ["indent-string@4.0.0", "https://registry.npmmirror.com/indent-string/-/indent-string-4.0.0.tgz", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
|
||||||
|
|
||||||
|
"inherits": ["inherits@2.0.4", "https://registry.npmmirror.com/inherits/-/inherits-2.0.4.tgz", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
|
||||||
|
|
||||||
"internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
"internal-slot": ["internal-slot@1.1.0", "https://registry.npmmirror.com/internal-slot/-/internal-slot-1.1.0.tgz", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw=="],
|
||||||
|
|
||||||
"internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
"internmap": ["internmap@2.0.3", "https://registry.npmmirror.com/internmap/-/internmap-2.0.3.tgz", {}, "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg=="],
|
||||||
|
|
||||||
|
"inversify": ["inversify@6.1.4", "https://registry.npmmirror.com/inversify/-/inversify-6.1.4.tgz", { "dependencies": { "@inversifyjs/common": "1.3.3", "@inversifyjs/core": "1.3.4" } }, "sha512-PbxrZH/gTa1fpPEEGAjJQzK8tKMIp5gRg6EFNJlCtzUcycuNdmhv3uk5P8Itm/RIjgHJO16oQRLo9IHzQN51bA=="],
|
||||||
|
|
||||||
|
"is-arguments": ["is-arguments@1.2.0", "https://registry.npmmirror.com/is-arguments/-/is-arguments-1.2.0.tgz", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA=="],
|
||||||
|
|
||||||
"is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
"is-array-buffer": ["is-array-buffer@3.0.5", "https://registry.npmmirror.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A=="],
|
||||||
|
|
||||||
"is-async-function": ["is-async-function@2.1.1", "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
"is-async-function": ["is-async-function@2.1.1", "https://registry.npmmirror.com/is-async-function/-/is-async-function-2.1.1.tgz", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ=="],
|
||||||
@@ -769,6 +835,8 @@
|
|||||||
|
|
||||||
"is-boolean-object": ["is-boolean-object@1.2.2", "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
"is-boolean-object": ["is-boolean-object@1.2.2", "https://registry.npmmirror.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, "sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A=="],
|
||||||
|
|
||||||
|
"is-buffer": ["is-buffer@2.0.5", "https://registry.npmmirror.com/is-buffer/-/is-buffer-2.0.5.tgz", {}, "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ=="],
|
||||||
|
|
||||||
"is-callable": ["is-callable@1.2.7", "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
"is-callable": ["is-callable@1.2.7", "https://registry.npmmirror.com/is-callable/-/is-callable-1.2.7.tgz", {}, "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA=="],
|
||||||
|
|
||||||
"is-core-module": ["is-core-module@2.16.1", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
"is-core-module": ["is-core-module@2.16.1", "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="],
|
||||||
@@ -789,6 +857,8 @@
|
|||||||
|
|
||||||
"is-map": ["is-map@2.0.3", "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
"is-map": ["is-map@2.0.3", "https://registry.npmmirror.com/is-map/-/is-map-2.0.3.tgz", {}, "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw=="],
|
||||||
|
|
||||||
|
"is-nan": ["is-nan@1.3.2", "https://registry.npmmirror.com/is-nan/-/is-nan-1.3.2.tgz", { "dependencies": { "call-bind": "^1.0.0", "define-properties": "^1.1.3" } }, "sha512-E+zBKpQ2t6MEo1VsonYmluk9NxGrbzpeeLC2xIViuO2EjU2xsXsBPwTr3Ykv9l08UYEVEdWeRZNouaZqF6RN0w=="],
|
||||||
|
|
||||||
"is-negative-zero": ["is-negative-zero@2.0.3", "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
"is-negative-zero": ["is-negative-zero@2.0.3", "https://registry.npmmirror.com/is-negative-zero/-/is-negative-zero-2.0.3.tgz", {}, "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw=="],
|
||||||
|
|
||||||
"is-node-process": ["is-node-process@1.2.0", "https://registry.npmmirror.com/is-node-process/-/is-node-process-1.2.0.tgz", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
|
"is-node-process": ["is-node-process@1.2.0", "https://registry.npmmirror.com/is-node-process/-/is-node-process-1.2.0.tgz", {}, "sha512-Vg4o6/fqPxIjtxgUH5QLJhwZ7gW5diGCVlXpuUfELC62CuxM1iHcRe51f2W1FDy04Ai4KJkagKjx3XaqyfRKXw=="],
|
||||||
@@ -799,6 +869,8 @@
|
|||||||
|
|
||||||
"is-regex": ["is-regex@1.2.1", "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
"is-regex": ["is-regex@1.2.1", "https://registry.npmmirror.com/is-regex/-/is-regex-1.2.1.tgz", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g=="],
|
||||||
|
|
||||||
|
"is-safe-filename": ["is-safe-filename@0.1.1", "https://registry.npmmirror.com/is-safe-filename/-/is-safe-filename-0.1.1.tgz", {}, "sha512-4SrR7AdnY11LHfDKTZY1u6Ga3RuxZdl3YKWWShO5iyuG5h8QS4GD2tOb04peBJ5I7pXbR+CGBNEhTcwK+FzN3g=="],
|
||||||
|
|
||||||
"is-set": ["is-set@2.0.3", "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
"is-set": ["is-set@2.0.3", "https://registry.npmmirror.com/is-set/-/is-set-2.0.3.tgz", {}, "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg=="],
|
||||||
|
|
||||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "https://registry.npmmirror.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", { "dependencies": { "call-bound": "^1.0.3" } }, "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A=="],
|
||||||
@@ -829,6 +901,10 @@
|
|||||||
|
|
||||||
"jackspeak": ["jackspeak@3.4.3", "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
"jackspeak": ["jackspeak@3.4.3", "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
|
||||||
|
|
||||||
|
"javascript-obfuscator": ["javascript-obfuscator@5.4.1", "https://registry.npmmirror.com/javascript-obfuscator/-/javascript-obfuscator-5.4.1.tgz", { "dependencies": { "@javascript-obfuscator/escodegen": "2.4.1", "@javascript-obfuscator/estraverse": "5.4.0", "@vercel/blob": ">=0.23.0", "acorn": "8.15.0", "acorn-import-attributes": "^1.9.5", "assert": "2.1.0", "chalk": "4.1.2", "chance": "1.1.13", "class-validator": "0.14.3", "commander": "12.1.0", "env-paths": "4.0.0", "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "fast-deep-equal": "3.1.3", "inversify": "6.1.4", "js-string-escape": "1.0.1", "md5": "2.3.0", "multimatch": "5.0.0", "process": "0.11.10", "reflect-metadata": "0.2.2", "string-template": "1.0.0", "stringz": "2.1.0", "tslib": "2.8.1" }, "bin": { "javascript-obfuscator": "bin/javascript-obfuscator" } }, "sha512-HoG2vdmo0xM37YQtcjYXNBTlRvypW5G6gtTMFrCBzLkVMLWQy97+XISDNL1DUMfKQQ6Njp8SddfPJix1h2FhVg=="],
|
||||||
|
|
||||||
|
"js-string-escape": ["js-string-escape@1.0.1", "https://registry.npmmirror.com/js-string-escape/-/js-string-escape-1.0.1.tgz", {}, "sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg=="],
|
||||||
|
|
||||||
"js-tokens": ["js-tokens@10.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
"js-tokens": ["js-tokens@10.0.0", "https://registry.npmmirror.com/js-tokens/-/js-tokens-10.0.0.tgz", {}, "sha512-lM/UBzQmfJRo9ABXbPWemivdCW8V2G8FHaHdypQaIy523snUjog0W71ayWXTjiR+ixeMyVHN2XcpnTd/liPg/Q=="],
|
||||||
|
|
||||||
"js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
"js-yaml": ["js-yaml@4.1.1", "https://registry.npmmirror.com/js-yaml/-/js-yaml-4.1.1.tgz", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA=="],
|
||||||
@@ -841,6 +917,8 @@
|
|||||||
|
|
||||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
"json-schema-traverse": ["json-schema-traverse@0.4.1", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", {}, "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="],
|
||||||
|
|
||||||
|
"json-schema-typed": ["json-schema-typed@8.0.2", "https://registry.npmmirror.com/json-schema-typed/-/json-schema-typed-8.0.2.tgz", {}, "sha512-fQhoXdcvc3V28x7C7BMs4P5+kNlgUURe2jmUT1T//oBRMDrqy1QPelJimwZGo7Hg9VPV3EQV5Bnq4hbFy2vetA=="],
|
||||||
|
|
||||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "https://registry.npmmirror.com/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", {}, "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw=="],
|
||||||
|
|
||||||
"json5": ["json5@1.0.2", "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
"json5": ["json5@1.0.2", "https://registry.npmmirror.com/json5/-/json5-1.0.2.tgz", { "dependencies": { "minimist": "^1.2.0" }, "bin": { "json5": "lib/cli.js" } }, "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA=="],
|
||||||
@@ -849,6 +927,8 @@
|
|||||||
|
|
||||||
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
"levn": ["levn@0.4.1", "https://registry.npmmirror.com/levn/-/levn-0.4.1.tgz", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ=="],
|
||||||
|
|
||||||
|
"libphonenumber-js": ["libphonenumber-js@1.12.42", "https://registry.npmmirror.com/libphonenumber-js/-/libphonenumber-js-1.12.42.tgz", {}, "sha512-oKQFPTibqQwZZkChCDVMFVJXMZdyJNqDWZWYNn8BgyAaK/6yFJEowxCY0RVFirRyWP63hMRuKlkSEd9qlvbWXg=="],
|
||||||
|
|
||||||
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
"lightningcss": ["lightningcss@1.32.0", "https://registry.npmmirror.com/lightningcss/-/lightningcss-1.32.0.tgz", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-android-arm64": "1.32.0", "lightningcss-darwin-arm64": "1.32.0", "lightningcss-darwin-x64": "1.32.0", "lightningcss-freebsd-x64": "1.32.0", "lightningcss-linux-arm-gnueabihf": "1.32.0", "lightningcss-linux-arm64-gnu": "1.32.0", "lightningcss-linux-arm64-musl": "1.32.0", "lightningcss-linux-x64-gnu": "1.32.0", "lightningcss-linux-x64-musl": "1.32.0", "lightningcss-win32-arm64-msvc": "1.32.0", "lightningcss-win32-x64-msvc": "1.32.0" } }, "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ=="],
|
||||||
|
|
||||||
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
"lightningcss-android-arm64": ["lightningcss-android-arm64@1.32.0", "https://registry.npmmirror.com/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", { "os": "android", "cpu": "arm64" }, "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg=="],
|
||||||
@@ -895,6 +975,10 @@
|
|||||||
|
|
||||||
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
"math-intrinsics": ["math-intrinsics@1.1.0", "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
|
||||||
|
|
||||||
|
"md5": ["md5@2.3.0", "https://registry.npmmirror.com/md5/-/md5-2.3.0.tgz", { "dependencies": { "charenc": "0.0.2", "crypt": "0.0.2", "is-buffer": "~1.1.6" } }, "sha512-T1GITYmFaKuO91vxyoQMFETst+O71VUPEU3ze5GNzDm0OWdP8v1ziTaAEPUr/3kLsY3Sftgz242A1SetQiDL7g=="],
|
||||||
|
|
||||||
|
"mimic-function": ["mimic-function@5.0.1", "https://registry.npmmirror.com/mimic-function/-/mimic-function-5.0.1.tgz", {}, "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA=="],
|
||||||
|
|
||||||
"min-indent": ["min-indent@1.0.1", "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
"min-indent": ["min-indent@1.0.1", "https://registry.npmmirror.com/min-indent/-/min-indent-1.0.1.tgz", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="],
|
||||||
|
|
||||||
"minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
"minimatch": ["minimatch@3.1.5", "https://registry.npmmirror.com/minimatch/-/minimatch-3.1.5.tgz", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w=="],
|
||||||
@@ -905,10 +989,14 @@
|
|||||||
|
|
||||||
"mitt": ["mitt@3.0.1", "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
"mitt": ["mitt@3.0.1", "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz", {}, "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw=="],
|
||||||
|
|
||||||
|
"mkdirp": ["mkdirp@3.0.1", "https://registry.npmmirror.com/mkdirp/-/mkdirp-3.0.1.tgz", { "bin": { "mkdirp": "dist/cjs/src/bin.js" } }, "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg=="],
|
||||||
|
|
||||||
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
"ms": ["ms@2.1.3", "https://registry.npmmirror.com/ms/-/ms-2.1.3.tgz", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
|
||||||
|
|
||||||
"msw": ["msw@2.13.3", "https://registry.npmmirror.com/msw/-/msw-2.13.3.tgz", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-/F49bxavkNGfreMlrKmTxZs6YorjfMbbDLd89Q3pWi+cXGtQQNXXaHt4MkXN7li91xnQJ24HWXqW9QDm5id33w=="],
|
"msw": ["msw@2.13.3", "https://registry.npmmirror.com/msw/-/msw-2.13.3.tgz", { "dependencies": { "@inquirer/confirm": "^5.0.0", "@mswjs/interceptors": "^0.41.2", "@open-draft/deferred-promise": "^2.2.0", "@types/statuses": "^2.0.6", "cookie": "^1.0.2", "graphql": "^16.12.0", "headers-polyfill": "^4.0.2", "is-node-process": "^1.2.0", "outvariant": "^1.4.3", "path-to-regexp": "^6.3.0", "picocolors": "^1.1.1", "rettime": "^0.11.7", "statuses": "^2.0.2", "strict-event-emitter": "^0.5.1", "tough-cookie": "^6.0.0", "type-fest": "^5.2.0", "until-async": "^3.0.2", "yargs": "^17.7.2" }, "peerDependencies": { "typescript": ">= 4.8.x" }, "optionalPeers": ["typescript"], "bin": { "msw": "cli/index.js" } }, "sha512-/F49bxavkNGfreMlrKmTxZs6YorjfMbbDLd89Q3pWi+cXGtQQNXXaHt4MkXN7li91xnQJ24HWXqW9QDm5id33w=="],
|
||||||
|
|
||||||
|
"multimatch": ["multimatch@5.0.0", "https://registry.npmmirror.com/multimatch/-/multimatch-5.0.0.tgz", { "dependencies": { "@types/minimatch": "^3.0.3", "array-differ": "^3.0.0", "array-union": "^2.1.0", "arrify": "^2.0.1", "minimatch": "^3.0.4" } }, "sha512-ypMKuglUrZUD99Tk2bUQ+xNQj43lPEfAeX2o9cTteAmShXy2VHDJpuwu1o0xqoKCt9jLVAvwyFKdLTPXKAfJyA=="],
|
||||||
|
|
||||||
"mute-stream": ["mute-stream@2.0.0", "https://registry.npmmirror.com/mute-stream/-/mute-stream-2.0.0.tgz", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
"mute-stream": ["mute-stream@2.0.0", "https://registry.npmmirror.com/mute-stream/-/mute-stream-2.0.0.tgz", {}, "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
@@ -921,12 +1009,16 @@
|
|||||||
|
|
||||||
"node-releases": ["node-releases@2.0.37", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
"node-releases": ["node-releases@2.0.37", "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.37.tgz", {}, "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg=="],
|
||||||
|
|
||||||
|
"normalize-path": ["normalize-path@3.0.0", "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
|
||||||
|
|
||||||
"nwsapi": ["nwsapi@2.2.23", "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
|
"nwsapi": ["nwsapi@2.2.23", "https://registry.npmmirror.com/nwsapi/-/nwsapi-2.2.23.tgz", {}, "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ=="],
|
||||||
|
|
||||||
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
"object-assign": ["object-assign@4.1.1", "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
|
||||||
|
|
||||||
"object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
"object-inspect": ["object-inspect@1.13.4", "https://registry.npmmirror.com/object-inspect/-/object-inspect-1.13.4.tgz", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
|
||||||
|
|
||||||
|
"object-is": ["object-is@1.1.6", "https://registry.npmmirror.com/object-is/-/object-is-1.1.6.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1" } }, "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q=="],
|
||||||
|
|
||||||
"object-keys": ["object-keys@1.1.1", "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
"object-keys": ["object-keys@1.1.1", "https://registry.npmmirror.com/object-keys/-/object-keys-1.1.1.tgz", {}, "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA=="],
|
||||||
|
|
||||||
"object.assign": ["object.assign@4.1.7", "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
"object.assign": ["object.assign@4.1.7", "https://registry.npmmirror.com/object.assign/-/object.assign-4.1.7.tgz", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw=="],
|
||||||
@@ -987,6 +1079,8 @@
|
|||||||
|
|
||||||
"pretty-format": ["pretty-format@27.5.1", "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
"pretty-format": ["pretty-format@27.5.1", "https://registry.npmmirror.com/pretty-format/-/pretty-format-27.5.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1", "ansi-styles": "^5.0.0", "react-is": "^17.0.1" } }, "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ=="],
|
||||||
|
|
||||||
|
"process": ["process@0.11.10", "https://registry.npmmirror.com/process/-/process-0.11.10.tgz", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="],
|
||||||
|
|
||||||
"prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
"prop-types": ["prop-types@15.8.1", "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg=="],
|
||||||
|
|
||||||
"punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
"punycode": ["punycode@2.3.1", "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz", {}, "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg=="],
|
||||||
@@ -1017,6 +1111,8 @@
|
|||||||
|
|
||||||
"redux-thunk": ["redux-thunk@3.1.0", "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
"redux-thunk": ["redux-thunk@3.1.0", "https://registry.npmmirror.com/redux-thunk/-/redux-thunk-3.1.0.tgz", { "peerDependencies": { "redux": "^5.0.0" } }, "sha512-NW2r5T6ksUKXCabzhL9z+h206HQw/NJkcLm1GPImRQ8IzfXwRGqjVhKJGauHirT0DAuyy6hjdnMZaRoAcy0Klw=="],
|
||||||
|
|
||||||
|
"reflect-metadata": ["reflect-metadata@0.2.2", "https://registry.npmmirror.com/reflect-metadata/-/reflect-metadata-0.2.2.tgz", {}, "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q=="],
|
||||||
|
|
||||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "https://registry.npmmirror.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw=="],
|
||||||
|
|
||||||
"regenerator-runtime": ["regenerator-runtime@0.14.1", "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
"regenerator-runtime": ["regenerator-runtime@0.14.1", "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", {}, "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="],
|
||||||
@@ -1025,12 +1121,16 @@
|
|||||||
|
|
||||||
"require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
"require-directory": ["require-directory@2.1.1", "https://registry.npmmirror.com/require-directory/-/require-directory-2.1.1.tgz", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
|
||||||
|
|
||||||
|
"require-from-string": ["require-from-string@2.0.2", "https://registry.npmmirror.com/require-from-string/-/require-from-string-2.0.2.tgz", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||||
|
|
||||||
"reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
"reselect": ["reselect@5.1.1", "https://registry.npmmirror.com/reselect/-/reselect-5.1.1.tgz", {}, "sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w=="],
|
||||||
|
|
||||||
"resolve": ["resolve@2.0.0-next.6", "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
|
"resolve": ["resolve@2.0.0-next.6", "https://registry.npmmirror.com/resolve/-/resolve-2.0.0-next.6.tgz", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA=="],
|
||||||
|
|
||||||
"resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
"resolve-from": ["resolve-from@4.0.0", "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz", {}, "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g=="],
|
||||||
|
|
||||||
|
"retry": ["retry@0.13.1", "https://registry.npmmirror.com/retry/-/retry-0.13.1.tgz", {}, "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg=="],
|
||||||
|
|
||||||
"rettime": ["rettime@0.11.7", "https://registry.npmmirror.com/rettime/-/rettime-0.11.7.tgz", {}, "sha512-DoAm1WjR1eH7z8sHPtvvUMIZh4/CSKkGCz6CxPqOrEAnOGtOuHSnSE9OC+razqxKuf4ub7pAYyl/vZV0vGs5tg=="],
|
"rettime": ["rettime@0.11.7", "https://registry.npmmirror.com/rettime/-/rettime-0.11.7.tgz", {}, "sha512-DoAm1WjR1eH7z8sHPtvvUMIZh4/CSKkGCz6CxPqOrEAnOGtOuHSnSE9OC+razqxKuf4ub7pAYyl/vZV0vGs5tg=="],
|
||||||
|
|
||||||
"rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
|
"rolldown": ["rolldown@1.0.0-rc.15", "https://registry.npmmirror.com/rolldown/-/rolldown-1.0.0-rc.15.tgz", { "dependencies": { "@oxc-project/types": "=0.124.0", "@rolldown/pluginutils": "1.0.0-rc.15" }, "optionalDependencies": { "@rolldown/binding-android-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-arm64": "1.0.0-rc.15", "@rolldown/binding-darwin-x64": "1.0.0-rc.15", "@rolldown/binding-freebsd-x64": "1.0.0-rc.15", "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.15", "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.15", "@rolldown/binding-linux-x64-musl": "1.0.0-rc.15", "@rolldown/binding-openharmony-arm64": "1.0.0-rc.15", "@rolldown/binding-wasm32-wasi": "1.0.0-rc.15", "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.15", "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.15" }, "bin": { "rolldown": "bin/cli.mjs" } }, "sha512-Ff31guA5zT6WjnGp0SXw76X6hzGRk/OQq2hE+1lcDe+lJdHSgnSX6nK3erbONHyCbpSj9a9E+uX/OvytZoWp2g=="],
|
||||||
@@ -1081,8 +1181,12 @@
|
|||||||
|
|
||||||
"sortablejs": ["sortablejs@1.15.7", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="],
|
"sortablejs": ["sortablejs@1.15.7", "https://registry.npmmirror.com/sortablejs/-/sortablejs-1.15.7.tgz", {}, "sha512-Kk8wLQPlS+yi1ZEf48a4+fzHa4yxjC30M/Sr2AnQu+f/MPwvvX9XjZ6OWejiz8crBsLwSq8GHqaxaET7u6ux0A=="],
|
||||||
|
|
||||||
|
"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=="],
|
"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=="],
|
||||||
|
|
||||||
|
"source-map-support": ["source-map-support@0.5.21", "https://registry.npmmirror.com/source-map-support/-/source-map-support-0.5.21.tgz", { "dependencies": { "buffer-from": "^1.0.0", "source-map": "^0.6.0" } }, "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w=="],
|
||||||
|
|
||||||
"sql.js": ["sql.js@1.14.1", "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", {}, "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A=="],
|
"sql.js": ["sql.js@1.14.1", "https://registry.npmmirror.com/sql.js/-/sql.js-1.14.1.tgz", {}, "sha512-gcj8zBWU5cFsi9WUP+4bFNXAyF1iRpA3LLyS/DP5xlrNzGmPIizUeBggKa8DbDwdqaKwUcTEnChtd2grWo/x/A=="],
|
||||||
|
|
||||||
"stackback": ["stackback@0.0.2", "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
"stackback": ["stackback@0.0.2", "https://registry.npmmirror.com/stackback/-/stackback-0.0.2.tgz", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="],
|
||||||
@@ -1095,6 +1199,8 @@
|
|||||||
|
|
||||||
"strict-event-emitter": ["strict-event-emitter@0.5.1", "https://registry.npmmirror.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
|
"strict-event-emitter": ["strict-event-emitter@0.5.1", "https://registry.npmmirror.com/strict-event-emitter/-/strict-event-emitter-0.5.1.tgz", {}, "sha512-vMgjE/GGEPEFnhFub6pa4FmJBRBVOLpIII2hvCZ8Kzb7K0hlHo7mQv6xYrBvCL2LtAIBwFUK8wvuJgTVSQ5MFQ=="],
|
||||||
|
|
||||||
|
"string-template": ["string-template@1.0.0", "https://registry.npmmirror.com/string-template/-/string-template-1.0.0.tgz", {}, "sha512-SLqR3GBUXuoPP5MmYtD7ompvXiG87QjT6lzOszyXjTM86Uu7At7vNnt2xgyTLq5o9T4IxTYFyGxcULqpsmsfdg=="],
|
||||||
|
|
||||||
"string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
|
|
||||||
"string-width-cjs": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
"string-width-cjs": ["string-width@4.2.3", "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||||
@@ -1105,6 +1211,8 @@
|
|||||||
|
|
||||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "https://registry.npmmirror.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg=="],
|
||||||
|
|
||||||
|
"stringz": ["stringz@2.1.0", "https://registry.npmmirror.com/stringz/-/stringz-2.1.0.tgz", { "dependencies": { "char-regex": "^1.0.2" } }, "sha512-KlywLT+MZ+v0IRepfMxRtnSvDCMc3nR1qqCs3m/qIbSOWkNZYT8XHQA31rS3TnKp0c5xjZu3M4GY/2aRKSi/6A=="],
|
||||||
|
|
||||||
"strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
|
|
||||||
"strip-ansi-cjs": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
"strip-ansi-cjs": ["strip-ansi@6.0.1", "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
|
||||||
@@ -1117,6 +1225,10 @@
|
|||||||
|
|
||||||
"strip-literal": ["strip-literal@3.1.0", "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
"strip-literal": ["strip-literal@3.1.0", "https://registry.npmmirror.com/strip-literal/-/strip-literal-3.1.0.tgz", { "dependencies": { "js-tokens": "^9.0.1" } }, "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg=="],
|
||||||
|
|
||||||
|
"stubborn-fs": ["stubborn-fs@2.0.0", "https://registry.npmmirror.com/stubborn-fs/-/stubborn-fs-2.0.0.tgz", { "dependencies": { "stubborn-utils": "^1.0.1" } }, "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA=="],
|
||||||
|
|
||||||
|
"stubborn-utils": ["stubborn-utils@1.0.2", "https://registry.npmmirror.com/stubborn-utils/-/stubborn-utils-1.0.2.tgz", {}, "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg=="],
|
||||||
|
|
||||||
"supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
"supports-color": ["supports-color@7.2.0", "https://registry.npmmirror.com/supports-color/-/supports-color-7.2.0.tgz", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="],
|
||||||
|
|
||||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
|
||||||
@@ -1131,6 +1243,8 @@
|
|||||||
|
|
||||||
"test-exclude": ["test-exclude@7.0.2", "https://registry.npmmirror.com/test-exclude/-/test-exclude-7.0.2.tgz", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="],
|
"test-exclude": ["test-exclude@7.0.2", "https://registry.npmmirror.com/test-exclude/-/test-exclude-7.0.2.tgz", { "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^10.4.1", "minimatch": "^10.2.2" } }, "sha512-u9E6A+ZDYdp7a4WnarkXPZOx8Ilz46+kby6p1yZ8zsGTz9gYa6FIS7lj2oezzNKmtdyyJNNmmXDppga5GB7kSw=="],
|
||||||
|
|
||||||
|
"throttleit": ["throttleit@2.1.0", "https://registry.npmmirror.com/throttleit/-/throttleit-2.1.0.tgz", {}, "sha512-nt6AMGKW1p/70DF/hGBdJB57B8Tspmbp5gfJ8ilhLnt7kkr2ye7hzD6NVG8GGErk2HWF34igrL2CXmNIkzKqKw=="],
|
||||||
|
|
||||||
"tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
"tiny-invariant": ["tiny-invariant@1.3.3", "https://registry.npmmirror.com/tiny-invariant/-/tiny-invariant-1.3.3.tgz", {}, "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg=="],
|
||||||
|
|
||||||
"tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
"tinybench": ["tinybench@2.9.0", "https://registry.npmmirror.com/tinybench/-/tinybench-2.9.0.tgz", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="],
|
||||||
@@ -1157,7 +1271,7 @@
|
|||||||
|
|
||||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
"tsconfig-paths": ["tsconfig-paths@3.15.0", "https://registry.npmmirror.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg=="],
|
||||||
|
|
||||||
"tslib": ["tslib@2.3.1", "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz", {}, "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="],
|
"tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
||||||
|
|
||||||
"type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
"type-check": ["type-check@0.4.0", "https://registry.npmmirror.com/type-check/-/type-check-0.4.0.tgz", { "dependencies": { "prelude-ls": "^1.2.1" } }, "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew=="],
|
||||||
|
|
||||||
@@ -1175,8 +1289,12 @@
|
|||||||
|
|
||||||
"typescript-eslint": ["typescript-eslint@8.58.2", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.58.2.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="],
|
"typescript-eslint": ["typescript-eslint@8.58.2", "https://registry.npmmirror.com/typescript-eslint/-/typescript-eslint-8.58.2.tgz", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="],
|
||||||
|
|
||||||
|
"uint8array-extras": ["uint8array-extras@1.5.0", "https://registry.npmmirror.com/uint8array-extras/-/uint8array-extras-1.5.0.tgz", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
|
||||||
|
|
||||||
"unbox-primitive": ["unbox-primitive@1.1.0", "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
"unbox-primitive": ["unbox-primitive@1.1.0", "https://registry.npmmirror.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw=="],
|
||||||
|
|
||||||
|
"undici": ["undici@6.25.0", "https://registry.npmmirror.com/undici/-/undici-6.25.0.tgz", {}, "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg=="],
|
||||||
|
|
||||||
"undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
"undici-types": ["undici-types@7.16.0", "https://registry.npmmirror.com/undici-types/-/undici-types-7.16.0.tgz", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||||
|
|
||||||
"until-async": ["until-async@3.0.2", "https://registry.npmmirror.com/until-async/-/until-async-3.0.2.tgz", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
|
"until-async": ["until-async@3.0.2", "https://registry.npmmirror.com/until-async/-/until-async-3.0.2.tgz", {}, "sha512-IiSk4HlzAMqTUseHHe3VhIGyuFmN90zMTpD3Z3y8jeQbzLIq500MVM7Jq2vUAnTKAFPJrqwkzr6PoTcPhGcOiw=="],
|
||||||
@@ -1187,6 +1305,8 @@
|
|||||||
|
|
||||||
"use-sync-external-store": ["use-sync-external-store@1.6.0", "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
"use-sync-external-store": ["use-sync-external-store@1.6.0", "https://registry.npmmirror.com/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz", { "peerDependencies": { "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w=="],
|
||||||
|
|
||||||
|
"util": ["util@0.12.5", "https://registry.npmmirror.com/util/-/util-0.12.5.tgz", { "dependencies": { "inherits": "^2.0.3", "is-arguments": "^1.0.4", "is-generator-function": "^1.0.7", "is-typed-array": "^1.1.3", "which-typed-array": "^1.1.2" } }, "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA=="],
|
||||||
|
|
||||||
"validator": ["validator@13.15.35", "https://registry.npmmirror.com/validator/-/validator-13.15.35.tgz", {}, "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw=="],
|
"validator": ["validator@13.15.35", "https://registry.npmmirror.com/validator/-/validator-13.15.35.tgz", {}, "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw=="],
|
||||||
|
|
||||||
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
"victory-vendor": ["victory-vendor@37.3.6", "https://registry.npmmirror.com/victory-vendor/-/victory-vendor-37.3.6.tgz", { "dependencies": { "@types/d3-array": "^3.0.3", "@types/d3-ease": "^3.0.0", "@types/d3-interpolate": "^3.0.1", "@types/d3-scale": "^4.0.2", "@types/d3-shape": "^3.1.0", "@types/d3-time": "^3.0.0", "@types/d3-timer": "^3.0.0", "d3-array": "^3.1.6", "d3-ease": "^3.0.1", "d3-interpolate": "^3.0.1", "d3-scale": "^4.0.2", "d3-shape": "^3.1.0", "d3-time": "^3.0.0", "d3-timer": "^3.0.1" } }, "sha512-SbPDPdDBYp+5MJHhBCAyI7wKM3d5ivekigc2Dk2s7pgbZ9wIgIBYGVw4zGHBml/qTFbexrofXW6Gu4noGxrOwQ=="],
|
||||||
@@ -1195,6 +1315,8 @@
|
|||||||
|
|
||||||
"vite-node": ["vite-node@3.2.4", "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
"vite-node": ["vite-node@3.2.4", "https://registry.npmmirror.com/vite-node/-/vite-node-3.2.4.tgz", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.1", "es-module-lexer": "^1.7.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator": ["vite-plugin-javascript-obfuscator@3.1.0", "https://registry.npmmirror.com/vite-plugin-javascript-obfuscator/-/vite-plugin-javascript-obfuscator-3.1.0.tgz", { "dependencies": { "anymatch": "~3.1.3", "javascript-obfuscator": "^4.1.0" } }, "sha512-sf4JFlG1iUPl7bLXHGOy+bKWOQUFyXzJFWa+n2S2xMMvyfM+V9R40HhpZoIF1eAjifArM1SF7fbSFIaTuUIbPA=="],
|
||||||
|
|
||||||
"vitest": ["vitest@3.2.4", "https://registry.npmmirror.com/vitest/-/vitest-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
"vitest": ["vitest@3.2.4", "https://registry.npmmirror.com/vitest/-/vitest-3.2.4.tgz", { "dependencies": { "@types/chai": "^5.2.2", "@vitest/expect": "3.2.4", "@vitest/mocker": "3.2.4", "@vitest/pretty-format": "^3.2.4", "@vitest/runner": "3.2.4", "@vitest/snapshot": "3.2.4", "@vitest/spy": "3.2.4", "@vitest/utils": "3.2.4", "chai": "^5.2.0", "debug": "^4.4.1", "expect-type": "^1.2.1", "magic-string": "^0.30.17", "pathe": "^2.0.3", "picomatch": "^4.0.2", "std-env": "^3.9.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinyglobby": "^0.2.14", "tinypool": "^1.1.1", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", "vite-node": "3.2.4", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.2.4", "@vitest/ui": "3.2.4", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A=="],
|
||||||
|
|
||||||
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
"w3c-xmlserializer": ["w3c-xmlserializer@5.0.0", "https://registry.npmmirror.com/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", { "dependencies": { "xml-name-validator": "^5.0.0" } }, "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA=="],
|
||||||
@@ -1207,6 +1329,8 @@
|
|||||||
|
|
||||||
"whatwg-url": ["whatwg-url@14.2.0", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
"whatwg-url": ["whatwg-url@14.2.0", "https://registry.npmmirror.com/whatwg-url/-/whatwg-url-14.2.0.tgz", { "dependencies": { "tr46": "^5.1.0", "webidl-conversions": "^7.0.0" } }, "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw=="],
|
||||||
|
|
||||||
|
"when-exit": ["when-exit@2.1.5", "https://registry.npmmirror.com/when-exit/-/when-exit-2.1.5.tgz", {}, "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg=="],
|
||||||
|
|
||||||
"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=="],
|
"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=="],
|
||||||
|
|
||||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "https://registry.npmmirror.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA=="],
|
||||||
@@ -1255,12 +1379,6 @@
|
|||||||
|
|
||||||
"@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
"@babel/core/json5": ["json5@2.2.3", "https://registry.npmmirror.com/json5/-/json5-2.2.3.tgz", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
|
||||||
|
|
||||||
"@emnapi/core/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
||||||
|
|
||||||
"@emnapi/runtime/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
||||||
|
|
||||||
"@emnapi/wasi-threads/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
||||||
|
|
||||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="],
|
||||||
|
|
||||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
"@eslint/eslintrc/globals": ["globals@14.0.0", "https://registry.npmmirror.com/globals/-/globals-14.0.0.tgz", {}, "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ=="],
|
||||||
@@ -1271,14 +1389,14 @@
|
|||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
"@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
|
||||||
|
|
||||||
|
"@javascript-obfuscator/escodegen/optionator": ["optionator@0.8.3", "https://registry.npmmirror.com/optionator/-/optionator-0.8.3.tgz", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="],
|
||||||
|
|
||||||
"@reduxjs/toolkit/immer": ["immer@11.1.4", "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
"@reduxjs/toolkit/immer": ["immer@11.1.4", "https://registry.npmmirror.com/immer/-/immer-11.1.4.tgz", {}, "sha512-XREFCPo6ksxVzP4E0ekD5aMdf8WMwmdNaz6vuvxgI40UaEiu6q3p8X52aU6GdyvLY3XXX/8R7JOTXStz/nBbRw=="],
|
||||||
|
|
||||||
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
"@testing-library/dom/aria-query": ["aria-query@5.3.0", "https://registry.npmmirror.com/aria-query/-/aria-query-5.3.0.tgz", { "dependencies": { "dequal": "^2.0.3" } }, "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A=="],
|
||||||
|
|
||||||
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
"@testing-library/dom/dom-accessibility-api": ["dom-accessibility-api@0.5.16", "https://registry.npmmirror.com/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz", {}, "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg=="],
|
||||||
|
|
||||||
"@tybys/wasm-util/tslib": ["tslib@2.8.1", "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="],
|
|
||||||
|
|
||||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "https://registry.npmmirror.com/ignore/-/ignore-7.0.5.tgz", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
@@ -1287,8 +1405,18 @@
|
|||||||
|
|
||||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "https://registry.npmmirror.com/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", {}, "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA=="],
|
||||||
|
|
||||||
|
"ajv-formats/ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
|
"anymatch/picomatch": ["picomatch@2.3.2", "https://registry.npmmirror.com/picomatch/-/picomatch-2.3.2.tgz", {}, "sha512-V7+vQEJ06Z+c5tSye8S+nHUfI51xoXIXjHQ99cQtKUkQqqO1kO/KCJUfZXuB47h/YBlDhah2H3hdUGXn8ie0oA=="],
|
||||||
|
|
||||||
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
"cliui/wrap-ansi": ["wrap-ansi@7.0.0", "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
|
||||||
|
|
||||||
|
"conf/ajv": ["ajv@8.18.0", "https://registry.npmmirror.com/ajv/-/ajv-8.18.0.tgz", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="],
|
||||||
|
|
||||||
|
"conf/env-paths": ["env-paths@3.0.0", "https://registry.npmmirror.com/env-paths/-/env-paths-3.0.0.tgz", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||||
|
|
||||||
|
"conf/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
"data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
"data-urls/whatwg-mimetype": ["whatwg-mimetype@4.0.0", "https://registry.npmmirror.com/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", {}, "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg=="],
|
||||||
|
|
||||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-import-resolver-node/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
@@ -1297,6 +1425,8 @@
|
|||||||
|
|
||||||
"eslint-plugin-import/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
"eslint-plugin-import/debug": ["debug@3.2.7", "https://registry.npmmirror.com/debug/-/debug-3.2.7.tgz", { "dependencies": { "ms": "^2.1.1" } }, "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ=="],
|
||||||
|
|
||||||
|
"espree/acorn": ["acorn@8.16.0", "https://registry.npmmirror.com/acorn/-/acorn-8.16.0.tgz", { "bin": { "acorn": "bin/acorn" } }, "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw=="],
|
||||||
|
|
||||||
"glob/minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
"glob/minimatch": ["minimatch@9.0.9", "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.9.tgz", { "dependencies": { "brace-expansion": "^2.0.2" } }, "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg=="],
|
||||||
|
|
||||||
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
"hoist-non-react-statics/react-is": ["react-is@16.13.1", "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz", {}, "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ=="],
|
||||||
@@ -1307,6 +1437,8 @@
|
|||||||
|
|
||||||
"make-dir/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
"make-dir/semver": ["semver@7.7.4", "https://registry.npmmirror.com/semver/-/semver-7.7.4.tgz", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="],
|
||||||
|
|
||||||
|
"md5/is-buffer": ["is-buffer@1.1.6", "https://registry.npmmirror.com/is-buffer/-/is-buffer-1.1.6.tgz", {}, "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w=="],
|
||||||
|
|
||||||
"msw/tough-cookie": ["tough-cookie@6.0.1", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
|
"msw/tough-cookie": ["tough-cookie@6.0.1", "https://registry.npmmirror.com/tough-cookie/-/tough-cookie-6.0.1.tgz", { "dependencies": { "tldts": "^7.0.5" } }, "sha512-LktZQb3IeoUWB9lqR5EWTHgW/VTITCXg4D21M+lvybRVdylLrRMnqaIONLVb5mav8vM19m44HIcGq4qASeu2Qw=="],
|
||||||
|
|
||||||
"parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
"parse5/entities": ["entities@6.0.1", "https://registry.npmmirror.com/entities/-/entities-6.0.1.tgz", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
|
||||||
@@ -1327,10 +1459,14 @@
|
|||||||
|
|
||||||
"tdesign-react/@babel/runtime": ["@babel/runtime@7.26.10", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.10.tgz", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
|
"tdesign-react/@babel/runtime": ["@babel/runtime@7.26.10", "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.26.10.tgz", { "dependencies": { "regenerator-runtime": "^0.14.0" } }, "sha512-2WJMeRQPHKSPemqk/awGrAiuFfzBmOIPXKizAsVhWH9YJqLZ0H+HS4c8loHGgW6utJ3E/ejXQUsiGaQy2NZ9Fw=="],
|
||||||
|
|
||||||
|
"tdesign-react/tslib": ["tslib@2.3.1", "https://registry.npmmirror.com/tslib/-/tslib-2.3.1.tgz", {}, "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw=="],
|
||||||
|
|
||||||
"test-exclude/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
"test-exclude/minimatch": ["minimatch@10.2.5", "https://registry.npmmirror.com/minimatch/-/minimatch-10.2.5.tgz", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="],
|
||||||
|
|
||||||
"vite-node/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
|
"vite-node/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator/javascript-obfuscator": ["javascript-obfuscator@4.2.2", "https://registry.npmmirror.com/javascript-obfuscator/-/javascript-obfuscator-4.2.2.tgz", { "dependencies": { "@javascript-obfuscator/escodegen": "2.3.1", "@javascript-obfuscator/estraverse": "5.4.0", "acorn": "8.15.0", "assert": "2.1.0", "chalk": "4.1.2", "chance": "1.1.13", "class-validator": "0.14.3", "commander": "12.1.0", "conf": "15.0.2", "eslint-scope": "8.4.0", "eslint-visitor-keys": "4.2.1", "fast-deep-equal": "3.1.3", "inversify": "6.1.4", "js-string-escape": "1.0.1", "md5": "2.3.0", "mkdirp": "3.0.1", "multimatch": "5.0.0", "process": "0.11.10", "reflect-metadata": "0.2.2", "source-map-support": "0.5.21", "string-template": "1.0.0", "stringz": "2.1.0", "tslib": "2.8.1" }, "bin": { "javascript-obfuscator": "bin/javascript-obfuscator" } }, "sha512-+7oXAUnFCA6vS0omIGHcWpSr67dUBIF7FKGYSXyzxShSLqM6LBgdugWKFl0XrYtGWyJMGfQR5F4LL85iCefkRA=="],
|
||||||
|
|
||||||
"vitest/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
|
"vitest/vite": ["vite@7.3.2", "https://registry.npmmirror.com/vite/-/vite-7.3.2.tgz", { "dependencies": { "esbuild": "^0.27.0", "fdir": "^6.5.0", "picomatch": "^4.0.3", "postcss": "^8.5.6", "rollup": "^4.43.0", "tinyglobby": "^0.2.15" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", "jiti": ">=1.21.0", "less": "^4.0.0", "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", "sugarss": "^5.0.0", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-Bby3NOsna2jsjfLVOHKes8sGwgl4TT0E6vvpYgnAYDIF/tie7MRaFthmKuHx1NSXjiTueXH3do80FMQgvEktRg=="],
|
||||||
|
|
||||||
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
|
||||||
@@ -1339,18 +1475,38 @@
|
|||||||
|
|
||||||
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
"@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="],
|
||||||
|
|
||||||
|
"@javascript-obfuscator/escodegen/optionator/levn": ["levn@0.3.0", "https://registry.npmmirror.com/levn/-/levn-0.3.0.tgz", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="],
|
||||||
|
|
||||||
|
"@javascript-obfuscator/escodegen/optionator/prelude-ls": ["prelude-ls@1.1.2", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.1.2.tgz", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="],
|
||||||
|
|
||||||
|
"@javascript-obfuscator/escodegen/optionator/type-check": ["type-check@0.3.2", "https://registry.npmmirror.com/type-check/-/type-check-0.3.2.tgz", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
|
"ajv-formats/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
|
"conf/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "https://registry.npmmirror.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||||
|
|
||||||
"glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
"glob/minimatch/brace-expansion": ["brace-expansion@2.1.0", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.1.0.tgz", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="],
|
||||||
|
|
||||||
"msw/tough-cookie/tldts": ["tldts@7.0.28", "https://registry.npmmirror.com/tldts/-/tldts-7.0.28.tgz", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="],
|
"msw/tough-cookie/tldts": ["tldts@7.0.28", "https://registry.npmmirror.com/tldts/-/tldts-7.0.28.tgz", { "dependencies": { "tldts-core": "^7.0.28" }, "bin": { "tldts": "bin/cli.js" } }, "sha512-+Zg3vWhRUv8B1maGSTFdev9mjoo8Etn2Ayfs4cnjlD3CsGkxXX4QyW3j2WJ0wdjYcYmy7Lx2RDsZMhgCWafKIw=="],
|
||||||
|
|
||||||
"test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
"test-exclude/minimatch/brace-expansion": ["brace-expansion@5.0.5", "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-5.0.5.tgz", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen": ["@javascript-obfuscator/escodegen@2.3.1", "https://registry.npmmirror.com/@javascript-obfuscator/escodegen/-/escodegen-2.3.1.tgz", { "dependencies": { "@javascript-obfuscator/estraverse": "^5.3.0", "esprima": "^4.0.1", "esutils": "^2.0.2", "optionator": "^0.8.1" }, "optionalDependencies": { "source-map": "~0.6.1" } }, "sha512-Z0HEAVwwafOume+6LFXirAVZeuEMKWuPzpFbQhCEU9++BMz0IwEa9bmedJ+rMn/IlXRBID9j3gQ0XYAa6jM10g=="],
|
||||||
|
|
||||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
"msw/tough-cookie/tldts/tldts-core": ["tldts-core@7.0.28", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.28.tgz", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="],
|
"msw/tough-cookie/tldts/tldts-core": ["tldts-core@7.0.28", "https://registry.npmmirror.com/tldts-core/-/tldts-core-7.0.28.tgz", {}, "sha512-7W5Efjhsc3chVdFhqtaU0KtK32J37Zcr9RKtID54nG+tIpcY79CQK/veYPODxtD/LJ4Lue66jvrQzIX2Z2/pUQ=="],
|
||||||
|
|
||||||
"test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
"test-exclude/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "https://registry.npmmirror.com/balanced-match/-/balanced-match-4.0.4.tgz", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator": ["optionator@0.8.3", "https://registry.npmmirror.com/optionator/-/optionator-0.8.3.tgz", { "dependencies": { "deep-is": "~0.1.3", "fast-levenshtein": "~2.0.6", "levn": "~0.3.0", "prelude-ls": "~1.1.2", "type-check": "~0.3.2", "word-wrap": "~1.2.3" } }, "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator/levn": ["levn@0.3.0", "https://registry.npmmirror.com/levn/-/levn-0.3.0.tgz", { "dependencies": { "prelude-ls": "~1.1.2", "type-check": "~0.3.2" } }, "sha512-0OO4y2iOHix2W6ujICbKIaEQXvFQHue65vUG3pb5EUomzPI90z9hsA1VsO/dbIIpC53J8gxM9Q4Oho0jrCM/yA=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator/prelude-ls": ["prelude-ls@1.1.2", "https://registry.npmmirror.com/prelude-ls/-/prelude-ls-1.1.2.tgz", {}, "sha512-ESF23V4SKG6lVSGZgYNpbsiaAkdab6ZgOxe52p7+Kid3W3u3bxR4Vfd/o21dmN7jSt0IwgZ4v5MUd26FEtXE9w=="],
|
||||||
|
|
||||||
|
"vite-plugin-javascript-obfuscator/javascript-obfuscator/@javascript-obfuscator/escodegen/optionator/type-check": ["type-check@0.3.2", "https://registry.npmmirror.com/type-check/-/type-check-0.3.2.tgz", { "dependencies": { "prelude-ls": "~1.1.2" } }, "sha512-ZCmOJdvOWDBYJlzAoFkC+Q0+bUyEOS1ltgp1MGU03fqHG+dbi9tBFU2Rd9QKiDZFAYrhPh2JUf7rZRIuHRKtOg=="],
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
"eslint-plugin-react-refresh": "^0.5.2",
|
"eslint-plugin-react-refresh": "^0.5.2",
|
||||||
"globals": "^17.4.0",
|
"globals": "^17.4.0",
|
||||||
"happy-dom": "^20.9.0",
|
"happy-dom": "^20.9.0",
|
||||||
|
"javascript-obfuscator": "^5.4.1",
|
||||||
"jsdom": "^26.1.0",
|
"jsdom": "^26.1.0",
|
||||||
"msw": "^2.8.2",
|
"msw": "^2.8.2",
|
||||||
"sass": "^1.99.0",
|
"sass": "^1.99.0",
|
||||||
@@ -49,6 +50,7 @@
|
|||||||
"typescript": "~6.0.2",
|
"typescript": "~6.0.2",
|
||||||
"typescript-eslint": "^8.58.0",
|
"typescript-eslint": "^8.58.0",
|
||||||
"vite": "^8.0.4",
|
"vite": "^8.0.4",
|
||||||
|
"vite-plugin-javascript-obfuscator": "^3.1.0",
|
||||||
"vitest": "^3.2.1"
|
"vitest": "^3.2.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { defineConfig } from 'vite'
|
import { defineConfig } from 'vite'
|
||||||
import react from '@vitejs/plugin-react'
|
import react from '@vitejs/plugin-react'
|
||||||
|
import obfuscatorPlugin from 'vite-plugin-javascript-obfuscator'
|
||||||
import path from 'node:path'
|
import path from 'node:path'
|
||||||
|
|
||||||
const vendorChunks: Record<string, string[]> = {
|
const vendorChunks: Record<string, string[]> = {
|
||||||
@@ -10,7 +11,42 @@ const vendorChunks: Record<string, string[]> = {
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [react()],
|
plugins: [
|
||||||
|
react(),
|
||||||
|
obfuscatorPlugin({
|
||||||
|
apply: 'build',
|
||||||
|
include: ['src/**/*.ts', 'src/**/*.tsx', 'src/**/*.js', 'src/**/*.jsx'],
|
||||||
|
exclude: [/node_modules/],
|
||||||
|
options: {
|
||||||
|
compact: true,
|
||||||
|
simplify: true,
|
||||||
|
identifierNamesGenerator: 'mangled-shuffled',
|
||||||
|
stringArray: true,
|
||||||
|
stringArrayCallsTransform: true,
|
||||||
|
stringArrayCallsTransformThreshold: 0.5,
|
||||||
|
stringArrayEncoding: ['base64'],
|
||||||
|
stringArrayIndexShift: true,
|
||||||
|
stringArrayRotate: true,
|
||||||
|
stringArrayShuffle: true,
|
||||||
|
stringArrayWrappersCount: 2,
|
||||||
|
stringArrayWrappersChainedCalls: true,
|
||||||
|
stringArrayThreshold: 0.75,
|
||||||
|
splitStrings: true,
|
||||||
|
splitStringsChunkLength: 10,
|
||||||
|
numbersToExpressions: true,
|
||||||
|
transformObjectKeys: true,
|
||||||
|
unicodeEscapeSequence: true,
|
||||||
|
debugProtection: true,
|
||||||
|
debugProtectionInterval: 2000,
|
||||||
|
disableConsoleOutput: true,
|
||||||
|
ignoreImports: true,
|
||||||
|
controlFlowFlattening: false,
|
||||||
|
deadCodeInjection: false,
|
||||||
|
selfDefending: false,
|
||||||
|
log: false,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
@@ -26,6 +62,7 @@ export default defineConfig({
|
|||||||
},
|
},
|
||||||
build: {
|
build: {
|
||||||
chunkSizeWarningLimit: 700,
|
chunkSizeWarningLimit: 700,
|
||||||
|
sourcemap: false,
|
||||||
rollupOptions: {
|
rollupOptions: {
|
||||||
output: {
|
output: {
|
||||||
manualChunks(id) {
|
manualChunks(id) {
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ cloud.google.com/go/auth v0.15.0/go.mod h1:WJDGqZ1o9E9wKIL+IwStfyn/+s59zl4Bi+1KQ
|
|||||||
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
cloud.google.com/go/auth/oauth2adapt v0.2.7/go.mod h1:NTbTTzfvPl1Y3V1nPpOgl2w6d/FjO7NNUQaWSox6ZMc=
|
||||||
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
|
||||||
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
cloud.google.com/go/longrunning v0.5.7/go.mod h1:8GClkudohy1Fxm3owmBGid8W0pSgodEMwEAztp38Xng=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
|
github.com/ClickHouse/ch-go v0.71.0/go.mod h1:NwbNc+7jaqfY58dmdDUbG4Jl22vThgx1cYjBw0vtgXw=
|
||||||
github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=
|
github.com/ClickHouse/clickhouse-go/v2 v2.43.0/go.mod h1:o6jf7JM/zveWC/PP277BLxjHy5KjnGX/jfljhM4s34g=
|
||||||
@@ -25,6 +26,7 @@ github.com/go-faster/city v1.0.1/go.mod h1:jKcUJId49qdW3L1qKHH/3wPeUstCVpVSXTM6v
|
|||||||
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
github.com/go-faster/errors v0.7.1/go.mod h1:5ySTjWFiphBs07IKuiL69nxdfd5+fzh1u7FPGZP2quo=
|
||||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-sql-driver/mysql v1.9.3 h1:U/N249h2WzJ3Ukj8SowVFjdtZKfu9vlLZxjPXV1aweo=
|
||||||
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
github.com/go-sql-driver/mysql v1.9.3/go.mod h1:qn46aNg1333BRMNU69Lq93t8du/dwxI64Gl8i5p1WMU=
|
||||||
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
|
||||||
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0=
|
||||||
@@ -35,6 +37,7 @@ github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0
|
|||||||
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
github.com/googleapis/enterprise-certificate-proxy v0.3.4/go.mod h1:YKe7cfqYXjKGpGvmSg28/fFvhNzinZQm8DGnaburhGA=
|
||||||
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
github.com/googleapis/gax-go/v2 v2.14.1/go.mod h1:Hb/NubMaVM88SrNkvl8X/o8XWwDJEPqouaLeN2IUxoA=
|
||||||
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
|
||||||
|
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
|
||||||
|
|||||||
@@ -1,2 +0,0 @@
|
|||||||
schema: spec-driven
|
|
||||||
created: 2026-04-22
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
## Context
|
|
||||||
|
|
||||||
Nex 桌面应用是一个将后端服务(Go/Gin)与前端静态资源(embed.FS)打包为单一可执行文件的跨平台应用。当前 Windows 构建存在三类问题:
|
|
||||||
|
|
||||||
1. **通用 `desktop` target 无平台感知**:输出 `build/nex` 无 `.exe` 后缀,且缺少 `-H=windowsgui` linker flag 导致控制台窗口闪现
|
|
||||||
2. **系统托盘图标加载失败**:`getlantern/systray` 在 Windows 上期望 ICO 格式,代码传入了 64x64 的 PNG
|
|
||||||
3. **`showError`/`showAbout` 使用 `msg *`**:Windows Home 版本可能不可用,配合 `-H=windowsgui` 后行为不可预测,且不支持自定义标题栏
|
|
||||||
|
|
||||||
项目已有 `assets/icon.ico`(256x256)但代码未使用。
|
|
||||||
|
|
||||||
## Goals / Non-Goals
|
|
||||||
|
|
||||||
**Goals:**
|
|
||||||
- Windows 构建产物可直接双击运行(.exe 后缀、无控制台窗口)
|
|
||||||
- 系统托盘图标在所有平台上正确加载
|
|
||||||
- Windows 上使用原生 `MessageBoxW` 对话框替代 `msg *`
|
|
||||||
- Makefile target 命名简洁统一
|
|
||||||
|
|
||||||
**Non-Goals:**
|
|
||||||
- 不引入新的第三方依赖
|
|
||||||
- 不改变 macOS/Linux 上的现有行为
|
|
||||||
- 不涉及应用签名或代码公证(属于发布流程)
|
|
||||||
- 不重构整体打包架构
|
|
||||||
|
|
||||||
## Decisions
|
|
||||||
|
|
||||||
### 1. 删除通用 `desktop` target,重命名平台 target
|
|
||||||
|
|
||||||
**决策**:删除 `desktop` target,将 `desktop-darwin`/`desktop-windows`/`desktop-linux` 重命名为 `desktop-mac`/`desktop-win`/`desktop-linux`。
|
|
||||||
|
|
||||||
**理由**:通用 target 在跨平台构建时必然需要条件判断,增加复杂度。按平台分离更明确,且项目已有先例。短命名 `win`/`mac`/`linux` 更简洁。
|
|
||||||
|
|
||||||
**产物命名统一**:`nex-{os}-{arch}[.exe]`
|
|
||||||
- `nex-mac-arm64`、`nex-mac-amd64`
|
|
||||||
- `nex-win-amd64.exe`
|
|
||||||
- `nex-linux-amd64`
|
|
||||||
|
|
||||||
### 2. 托盘图标运行时按平台选择格式
|
|
||||||
|
|
||||||
**决策**:在 `setupSystray` 中根据 `runtime.GOOS` 选择图标文件:
|
|
||||||
- Windows:加载 `assets/icon.ico`(256x256 ICO)
|
|
||||||
- 其他:加载 `assets/icon.png`(PNG)
|
|
||||||
|
|
||||||
**备选方案**:
|
|
||||||
- ~~Build tags + 编译时选择~~:增加文件数,维护成本高
|
|
||||||
- ~~所有平台统一用 ICO~~:Linux/macOS 的 systray 实现对 ICO 支持不一致
|
|
||||||
|
|
||||||
**理由**:运行时判断最简单,两个文件都已通过 `embedfs.Assets`(`assets/*`)嵌入,零额外成本。
|
|
||||||
|
|
||||||
### 3. Windows 原生对话框使用 `user32.MessageBoxW`
|
|
||||||
|
|
||||||
**决策**:通过 `syscall` 调用 `user32.dll` 的 `MessageBoxW`,替换 `msg *`。
|
|
||||||
|
|
||||||
**实现方式**:
|
|
||||||
```go
|
|
||||||
var (
|
|
||||||
user32 = syscall.NewLazyDLL("user32.dll")
|
|
||||||
procMessageBoxW = user32.NewProc("MessageBoxW")
|
|
||||||
)
|
|
||||||
func messageBox(title, message string) {
|
|
||||||
procMessageBoxW.Call(0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(message))),
|
|
||||||
uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr(title))), 0x10)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
**备选方案**:
|
|
||||||
- ~~继续用 `msg *`~~:不解决 Home 版不可用、标题栏不支持的问题
|
|
||||||
- ~~`rundll32` 调用~~:同样不可靠
|
|
||||||
- ~~引入 `lxn/walk` 等 GUI 库~~:引入重依赖,过度
|
|
||||||
|
|
||||||
**理由**:`MessageBoxW` 是 Windows 原生 API,所有版本都有,与 `-H=windowsgui` 完美兼容,支持标题栏和图标类型,零依赖。使用 `syscall`(非 `unsafe` 外部依赖)即可。
|
|
||||||
|
|
||||||
### 4. `showError`/`showAbout` 统一用平台 switch
|
|
||||||
|
|
||||||
**决策**:保持现有的 `switch runtime.GOOS` 结构,仅替换 Windows 分支实现。macOS(osascript)和 Linux(zenity)不变。
|
|
||||||
|
|
||||||
## Risks / Trade-offs
|
|
||||||
|
|
||||||
- **[syscall 跨架构]** `MessageBoxW` 的 `syscall.NewLazyDLL` 仅在 Windows 上有效 → 使用 `runtime.GOOS` 守卫,非 Windows 不会执行该路径,编译时通过 build 文件或运行时判断确保不触发
|
|
||||||
- **[ICO 嵌入体积]** `icon.ico` 270KB,已在 `embedfs` 中,不增加新体积 → 无风险
|
|
||||||
- **[Makefile 兼容性]** 删除 `desktop` target 后,CI/本地脚本如果引用它需更新 → 需检查是否有外部引用
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
## Why
|
|
||||||
|
|
||||||
Windows 桌面应用存在三个影响用户体验的问题:构建产物无 `.exe` 后缀无法双击运行、运行时弹出控制台窗口、系统托盘图标加载失败。此外 `showError`/`showAbout` 在 Windows 上使用 `msg *` 命令不可靠。这些问题导致应用在 Windows 上不够专业,需要统一修复。
|
|
||||||
|
|
||||||
## What Changes
|
|
||||||
|
|
||||||
- 删除通用 `desktop` Makefile target,仅保留按平台分离的 target
|
|
||||||
- Makefile target 重命名为简短形式:`desktop-win`、`desktop-mac`、`desktop-linux`
|
|
||||||
- 构建产物文件名统一为 `nex-{os}-{arch}[.exe]` 格式
|
|
||||||
- 系统托盘图标在 Windows 上使用 `.ico` 格式(运行时 `runtime.GOOS` 判断)
|
|
||||||
- Windows `showError`/`showAbout` 改用 `user32.dll` 的 `MessageBoxW` 原生对话框
|
|
||||||
- 同步更新已有 `desktop-app` spec 中的构建产物命名和图标格式要求
|
|
||||||
|
|
||||||
## Capabilities
|
|
||||||
|
|
||||||
### New Capabilities
|
|
||||||
|
|
||||||
无
|
|
||||||
|
|
||||||
### Modified Capabilities
|
|
||||||
|
|
||||||
- `desktop-app`: 构建产物命名规范变更(`nex-{os}-{arch}`);Windows 托盘图标需使用 `.ico` 格式;Windows 原生对话框替代 `msg *` 命令
|
|
||||||
|
|
||||||
## Impact
|
|
||||||
|
|
||||||
- `Makefile`:删除 `desktop` target,重命名其余三个 target 和产物文件名
|
|
||||||
- `backend/cmd/desktop/main.go`:修改 `setupSystray` 图标加载逻辑、`showError`/`showAbout` Windows 实现
|
|
||||||
- `openspec/specs/desktop-app/spec.md`:更新构建产物命名和 Windows 图标格式要求
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
## MODIFIED Requirements
|
|
||||||
|
|
||||||
### Requirement: 系统托盘
|
|
||||||
|
|
||||||
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择。
|
|
||||||
|
|
||||||
#### Scenario: 托盘图标显示
|
|
||||||
- **WHEN** 桌面应用启动成功
|
|
||||||
- **THEN** 系统根据平台加载正确的图标格式
|
|
||||||
- **AND** 在 Windows 上加载 ICO 格式图标(`assets/icon.ico`)
|
|
||||||
- **AND** 在 macOS 和 Linux 上加载 PNG 格式图标(`assets/icon.png`)
|
|
||||||
- **AND** 托盘图标 tooltip 显示"AI Gateway"
|
|
||||||
|
|
||||||
#### Scenario: 托盘菜单显示
|
|
||||||
- **WHEN** 用户点击托盘图标(左键或右键)
|
|
||||||
- **THEN** 显示托盘菜单
|
|
||||||
- **AND** 菜单包含"打开管理界面"选项
|
|
||||||
- **AND** 菜单包含"状态: 运行中"选项(禁用状态)
|
|
||||||
- **AND** 菜单包含"端口: 9826"选项(禁用状态)
|
|
||||||
- **AND** 菜单包含"关于"选项
|
|
||||||
- **AND** 菜单包含"退出"选项
|
|
||||||
|
|
||||||
#### Scenario: 打开管理界面
|
|
||||||
- **WHEN** 用户点击托盘菜单"打开管理界面"
|
|
||||||
- **THEN** 系统在浏览器中打开 `http://localhost:9826`
|
|
||||||
|
|
||||||
#### Scenario: 浏览器打开失败
|
|
||||||
- **WHEN** 系统无法打开浏览器(浏览器未安装等)
|
|
||||||
- **THEN** 托盘菜单仍可正常使用
|
|
||||||
- **AND** 用户可手动访问 `http://localhost:9826`
|
|
||||||
|
|
||||||
#### Scenario: 退出应用
|
|
||||||
- **WHEN** 用户点击托盘菜单"退出"
|
|
||||||
- **THEN** 系统优雅关闭后端服务
|
|
||||||
- **AND** 托盘图标消失
|
|
||||||
- **AND** 应用进程退出
|
|
||||||
|
|
||||||
### Requirement: 跨平台构建
|
|
||||||
|
|
||||||
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,产物文件名 SHALL 使用 `nex-{os}-{arch}[.exe]` 格式。
|
|
||||||
|
|
||||||
#### Scenario: macOS 构建
|
|
||||||
- **WHEN** 执行 `desktop-mac` 构建命令
|
|
||||||
- **THEN** 生成 `nex-mac-arm64` 和 `nex-mac-amd64` 可执行文件
|
|
||||||
- **AND** 可打包为 `.app` bundle
|
|
||||||
|
|
||||||
#### Scenario: Windows 构建
|
|
||||||
- **WHEN** 执行 `desktop-win` 构建命令
|
|
||||||
- **THEN** 生成 `nex-win-amd64.exe` 可执行文件
|
|
||||||
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
|
|
||||||
|
|
||||||
#### Scenario: Linux 构建
|
|
||||||
- **WHEN** 执行 `desktop-linux` 构建命令
|
|
||||||
- **THEN** 生成 `nex-linux-amd64` 可执行文件
|
|
||||||
|
|
||||||
### Requirement: 关于对话框
|
|
||||||
|
|
||||||
系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll` 的 `MessageBoxW` API 实现。
|
|
||||||
|
|
||||||
#### Scenario: 显示关于
|
|
||||||
- **WHEN** 用户点击托盘菜单"关于"
|
|
||||||
- **THEN** 显示对话框包含应用名称、项目链接
|
|
||||||
- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现
|
|
||||||
|
|
||||||
## ADDED Requirements
|
|
||||||
|
|
||||||
### Requirement: Windows 原生对话框
|
|
||||||
|
|
||||||
系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。
|
|
||||||
|
|
||||||
#### Scenario: 错误提示对话框
|
|
||||||
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
|
|
||||||
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
|
||||||
- **AND** 对话框标题栏显示应用名称
|
|
||||||
- **AND** 对话框包含错误描述文本
|
|
||||||
- **AND** 对话框显示错误图标(MB_ICONERROR)
|
|
||||||
|
|
||||||
#### Scenario: 关于对话框
|
|
||||||
- **WHEN** 用户在 Windows 上点击托盘菜单"关于"
|
|
||||||
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
|
||||||
- **AND** 对话框标题栏显示"关于 Nex Gateway"
|
|
||||||
- **AND** 对话框包含应用信息文本
|
|
||||||
- **AND** 对话框显示信息图标(MB_ICONINFORMATION)
|
|
||||||
|
|
||||||
#### Scenario: 非 Windows 平台不受影响
|
|
||||||
- **WHEN** 应用运行在 macOS 或 Linux 上
|
|
||||||
- **THEN** 错误和关于对话框仍使用平台原有实现(osascript / zenity)
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
## 1. Makefile 重构
|
|
||||||
|
|
||||||
- [x] 1.1 删除通用 `desktop` target 及其相关 `.PHONY` 声明
|
|
||||||
- [x] 1.2 将 `desktop-darwin` 重命名为 `desktop-mac`,产物文件名改为 `nex-mac-arm64` 和 `nex-mac-amd64`
|
|
||||||
- [x] 1.3 将 `desktop-windows` 重命名为 `desktop-win`,产物文件名改为 `nex-win-amd64.exe`
|
|
||||||
- [x] 1.4 将 `desktop-linux` 产物文件名改为 `nex-linux-amd64`
|
|
||||||
- [x] 1.5 更新 `.PHONY` 声明和 `all` target(如引用了旧名称)
|
|
||||||
|
|
||||||
## 2. Windows 原生对话框
|
|
||||||
|
|
||||||
- [x] 2.1 在 `backend/cmd/desktop/main.go` 中添加 Windows 平台的 `user32.MessageBoxW` 调用封装(`syscall.NewLazyDLL` + `syscall.StringToUTF16Ptr`),在 `showError`/`showAbout` 的 Windows `runtime.GOOS` 分支内直接调用
|
|
||||||
- [x] 2.2 替换 `showError` 函数的 Windows 分支,使用 `MessageBoxW` 替代 `msg *`
|
|
||||||
- [x] 2.3 替换 `showAbout` 函数的 Windows 分支,使用 `MessageBoxW` 替代 `msg *`
|
|
||||||
|
|
||||||
## 3. 系统托盘图标修复
|
|
||||||
|
|
||||||
- [x] 3.1 修改 `setupSystray` 函数中的图标加载逻辑,根据 `runtime.GOOS` 在 Windows 上加载 `assets/icon.ico`,其他平台加载 `assets/icon.png`
|
|
||||||
|
|
||||||
## 4. 文档和脚本更新
|
|
||||||
|
|
||||||
- [x] 4.1 更新 `README.md` 中的构建命令引用(`desktop-darwin` → `desktop-mac`,`desktop-windows` → `desktop-win`,`desktop-linux` 保持不变或改为 `desktop-linux`)
|
|
||||||
- [x] 4.2 更新 `scripts/build/package-macos.sh` 中对 `desktop-darwin` 的引用
|
|
||||||
|
|
||||||
## 5. 测试验证
|
|
||||||
|
|
||||||
- [x] 5.1 为 `showError`/`showAbout` 的 Windows `MessageBoxW` 封装编写单元测试(验证参数传递和调用正确性)
|
|
||||||
- [x] 5.2 为图标加载的平台选择逻辑编写单元测试(验证 Windows 选 `.ico`,其他选 `.png`)
|
|
||||||
- [x] 5.3 运行 `make desktop-win` 在 Windows 上验证:产物有 `.exe` 后缀、双击无控制台窗口、托盘图标正常显示、错误对话框使用原生样式
|
|
||||||
@@ -77,6 +77,7 @@
|
|||||||
- **THEN** SHALL 支持 `required` 规则
|
- **THEN** SHALL 支持 `required` 规则
|
||||||
- **THEN** SHALL 支持 `min`、`max` 规则
|
- **THEN** SHALL 支持 `min`、`max` 规则
|
||||||
- **THEN** SHALL 支持 `oneof` 规则
|
- **THEN** SHALL 支持 `oneof` 规则
|
||||||
|
- **THEN** SHALL 支持 `required_if` 条件验证规则
|
||||||
|
|
||||||
#### Scenario: 验证执行
|
#### Scenario: 验证执行
|
||||||
|
|
||||||
@@ -85,6 +86,17 @@
|
|||||||
- **THEN** SHALL 返回验证错误
|
- **THEN** SHALL 返回验证错误
|
||||||
- **THEN** SHALL NOT 启动应用(如果验证失败)
|
- **THEN** SHALL NOT 启动应用(如果验证失败)
|
||||||
|
|
||||||
|
#### Scenario: 数据库驱动条件验证
|
||||||
|
|
||||||
|
- **WHEN** `database.driver` 为 `sqlite`
|
||||||
|
- **THEN** SHALL 验证 `database.path` 必填
|
||||||
|
- **THEN** SHALL NOT 要求 MySQL 字段(host/port/user/password/dbname)
|
||||||
|
- **WHEN** `database.driver` 为 `mysql`
|
||||||
|
- **THEN** SHALL 验证 `database.host` 必填
|
||||||
|
- **THEN** SHALL 验证 `database.user` 必填
|
||||||
|
- **THEN** SHALL 验证 `database.dbname` 必填
|
||||||
|
- **THEN** SHALL NOT 要求 `database.path`
|
||||||
|
|
||||||
### Requirement: 配置结构定义
|
### Requirement: 配置结构定义
|
||||||
|
|
||||||
系统 SHALL 定义清晰的配置结构。
|
系统 SHALL 定义清晰的配置结构。
|
||||||
@@ -98,7 +110,14 @@
|
|||||||
#### Scenario: Database 配置
|
#### Scenario: Database 配置
|
||||||
|
|
||||||
- **WHEN** 加载 database 配置
|
- **WHEN** 加载 database 配置
|
||||||
- **THEN** SHALL 包含 path、max_idle_conns、max_open_conns、conn_max_lifetime 字段
|
- **THEN** SHALL 包含 driver 字段(值为 `sqlite` 或 `mysql`,默认 `sqlite`)
|
||||||
|
- **THEN** SHALL 包含 path 字段(SQLite 模式下的数据库文件路径)
|
||||||
|
- **THEN** SHALL 包含 host 字段(MySQL 主机地址)
|
||||||
|
- **THEN** SHALL 包含 port 字段(MySQL 端口,默认 3306)
|
||||||
|
- **THEN** SHALL 包含 user 字段(MySQL 用户名)
|
||||||
|
- **THEN** SHALL 包含 password 字段(MySQL 密码,选填)
|
||||||
|
- **THEN** SHALL 包含 dbname 字段(MySQL 数据库名)
|
||||||
|
- **THEN** SHALL 包含 max_idle_conns、max_open_conns、conn_max_lifetime 字段
|
||||||
- **THEN** SHALL 使用合理的默认值
|
- **THEN** SHALL 使用合理的默认值
|
||||||
|
|
||||||
#### Scenario: Log 配置
|
#### Scenario: Log 配置
|
||||||
@@ -121,7 +140,13 @@
|
|||||||
#### Scenario: Database 默认值
|
#### Scenario: Database 默认值
|
||||||
|
|
||||||
- **WHEN** 使用默认配置
|
- **WHEN** 使用默认配置
|
||||||
|
- **THEN** database.driver SHALL 为 `sqlite`
|
||||||
- **THEN** database.path SHALL 为 `~/.nex/config.db`
|
- **THEN** database.path SHALL 为 `~/.nex/config.db`
|
||||||
|
- **THEN** database.host SHALL 为空字符串
|
||||||
|
- **THEN** database.port SHALL 为 3306
|
||||||
|
- **THEN** database.user SHALL 为空字符串
|
||||||
|
- **THEN** database.password SHALL 为空字符串
|
||||||
|
- **THEN** database.dbname SHALL 为 `nex`
|
||||||
- **THEN** database.max_idle_conns SHALL 为 10
|
- **THEN** database.max_idle_conns SHALL 为 10
|
||||||
- **THEN** database.max_open_conns SHALL 为 100
|
- **THEN** database.max_open_conns SHALL 为 100
|
||||||
- **THEN** database.conn_max_lifetime SHALL 为 1h
|
- **THEN** database.conn_max_lifetime SHALL 为 1h
|
||||||
@@ -248,18 +273,38 @@
|
|||||||
- **THEN** SHALL 在日志中记录覆盖信息
|
- **THEN** SHALL 在日志中记录覆盖信息
|
||||||
- **THEN** SHALL 显示被覆盖的配置项名称
|
- **THEN** SHALL 显示被覆盖的配置项名称
|
||||||
|
|
||||||
|
### Requirement: 配置文件安全
|
||||||
|
|
||||||
|
系统 SHALL 使用安全的文件权限保存配置文件。
|
||||||
|
|
||||||
|
#### Scenario: 配置文件权限
|
||||||
|
|
||||||
|
- **WHEN** 保存配置文件(`SaveConfig`)
|
||||||
|
- **THEN** SHALL 使用 `0600` 权限写入文件(仅 owner 可读写)
|
||||||
|
- **THEN** SHALL 防止其他用户读取配置中的 MySQL 密码等敏感信息
|
||||||
|
|
||||||
### Requirement: 配置摘要输出
|
### Requirement: 配置摘要输出
|
||||||
|
|
||||||
系统 SHALL 在启动时输出配置摘要。
|
系统 SHALL 在启动时输出配置摘要。
|
||||||
|
|
||||||
#### Scenario: 摘要内容
|
#### Scenario: SQLite 模式摘要
|
||||||
|
|
||||||
- **WHEN** 配置加载完成
|
- **WHEN** `database.driver` 为 `sqlite`
|
||||||
- **THEN** SHALL 打印关键配置项(端口、数据库路径、日志级别等)
|
- **THEN** SHALL 打印关键配置项(端口、数据库路径、日志级别等)
|
||||||
- **THEN** SHALL 打印配置文件路径
|
- **THEN** SHALL 打印配置文件路径
|
||||||
- **THEN** SHALL 打印环境变量数量
|
- **THEN** SHALL 打印环境变量数量
|
||||||
- **THEN** SHALL 打印 CLI 参数数量
|
- **THEN** SHALL 打印 CLI 参数数量
|
||||||
|
|
||||||
|
#### Scenario: MySQL 模式摘要
|
||||||
|
|
||||||
|
- **WHEN** `database.driver` 为 `mysql`
|
||||||
|
- **THEN** SHALL 打印关键配置项(端口、数据库类型、数据库地址、日志级别等)
|
||||||
|
- **THEN** SHALL 打印数据库地址格式为 `{host}:{port}/{dbname}`
|
||||||
|
- **THEN** SHALL 不打印密码
|
||||||
|
- **THEN** SHALL 打印配置文件路径
|
||||||
|
- **THEN** SHALL 打印环境变量数量
|
||||||
|
- **THEN** SHALL 打印 CLI 参数数量
|
||||||
|
|
||||||
#### Scenario: 摘要格式
|
#### Scenario: 摘要格式
|
||||||
|
|
||||||
- **WHEN** 打印配置摘要
|
- **WHEN** 打印配置摘要
|
||||||
@@ -297,7 +342,7 @@
|
|||||||
- **WHEN** 使用服务器相关参数
|
- **WHEN** 使用服务器相关参数
|
||||||
- **THEN** SHALL 支持 `--server-port`、`--server-read-timeout`、`--server-write-timeout`
|
- **THEN** SHALL 支持 `--server-port`、`--server-read-timeout`、`--server-write-timeout`
|
||||||
- **WHEN** 使用数据库相关参数
|
- **WHEN** 使用数据库相关参数
|
||||||
- **THEN** SHALL 支持 `--database-path`、`--database-max-idle-conns`、`--database-max-open-conns`、`--database-conn-max-lifetime`
|
- **THEN** SHALL 支持 `--database-driver`、`--database-path`、`--database-host`、`--database-port`、`--database-user`、`--database-password`、`--database-dbname`、`--database-max-idle-conns`、`--database-max-open-conns`、`--database-conn-max-lifetime`
|
||||||
- **WHEN** 使用日志相关参数
|
- **WHEN** 使用日志相关参数
|
||||||
- **THEN** SHALL 支持 `--log-level`、`--log-path`、`--log-max-size`、`--log-max-backups`、`--log-max-age`、`--log-compress`
|
- **THEN** SHALL 支持 `--log-level`、`--log-path`、`--log-max-size`、`--log-max-backups`、`--log-max-age`、`--log-compress`
|
||||||
|
|
||||||
@@ -348,7 +393,7 @@
|
|||||||
- **WHEN** 设置服务器相关环境变量
|
- **WHEN** 设置服务器相关环境变量
|
||||||
- **THEN** SHALL 支持 `NEX_SERVER_PORT`、`NEX_SERVER_READ_TIMEOUT`、`NEX_SERVER_WRITE_TIMEOUT`
|
- **THEN** SHALL 支持 `NEX_SERVER_PORT`、`NEX_SERVER_READ_TIMEOUT`、`NEX_SERVER_WRITE_TIMEOUT`
|
||||||
- **WHEN** 设置数据库相关环境变量
|
- **WHEN** 设置数据库相关环境变量
|
||||||
- **THEN** SHALL 支持 `NEX_DATABASE_PATH`、`NEX_DATABASE_MAX_IDLE_CONNS`、`NEX_DATABASE_MAX_OPEN_CONNS`、`NEX_DATABASE_CONN_MAX_LIFETIME`
|
- **THEN** SHALL 支持 `NEX_DATABASE_DRIVER`、`NEX_DATABASE_PATH`、`NEX_DATABASE_HOST`、`NEX_DATABASE_PORT`、`NEX_DATABASE_USER`、`NEX_DATABASE_PASSWORD`、`NEX_DATABASE_DBNAME`、`NEX_DATABASE_MAX_IDLE_CONNS`、`NEX_DATABASE_MAX_OPEN_CONNS`、`NEX_DATABASE_CONN_MAX_LIFETIME`
|
||||||
- **WHEN** 设置日志相关环境变量
|
- **WHEN** 设置日志相关环境变量
|
||||||
- **THEN** SHALL 支持 `NEX_LOG_LEVEL`、`NEX_LOG_PATH`、`NEX_LOG_MAX_SIZE`、`NEX_LOG_MAX_BACKUPS`、`NEX_LOG_MAX_AGE`、`NEX_LOG_COMPRESS`
|
- **THEN** SHALL 支持 `NEX_LOG_LEVEL`、`NEX_LOG_PATH`、`NEX_LOG_MAX_SIZE`、`NEX_LOG_MAX_BACKUPS`、`NEX_LOG_MAX_AGE`、`NEX_LOG_COMPRESS`
|
||||||
|
|
||||||
|
|||||||
@@ -46,6 +46,14 @@
|
|||||||
- **THEN** SHALL 删除所有表和索引
|
- **THEN** SHALL 删除所有表和索引
|
||||||
- **THEN** SHALL 按正确顺序删除(避免外键约束错误)
|
- **THEN** SHALL 按正确顺序删除(避免外键约束错误)
|
||||||
|
|
||||||
|
#### Scenario: 按数据库方言拆分迁移目录
|
||||||
|
|
||||||
|
- **WHEN** 组织迁移文件
|
||||||
|
- **THEN** SHALL 将 SQLite 方言迁移文件存储在 `migrations/sqlite/` 目录
|
||||||
|
- **THEN** SHALL 将 MySQL 方言迁移文件存储在 `migrations/mysql/` 目录
|
||||||
|
- **THEN** SHALL 两个目录维护独立的版本号序列
|
||||||
|
- **THEN** SHALL 两个目录的迁移文件内容在逻辑上一致(相同的表结构和约束),但使用各自方言的 DDL
|
||||||
|
|
||||||
### Requirement: models 表 schema 变更
|
### Requirement: models 表 schema 变更
|
||||||
|
|
||||||
系统 SHALL 在初始迁移脚本中直接创建新的 models 表结构(服务未上线,无需考虑数据迁移,迁移脚本已合并为单个初始迁移文件)。
|
系统 SHALL 在初始迁移脚本中直接创建新的 models 表结构(服务未上线,无需考虑数据迁移,迁移脚本已合并为单个初始迁移文件)。
|
||||||
@@ -63,28 +71,37 @@
|
|||||||
|
|
||||||
#### Scenario: 迁移 up 命令
|
#### Scenario: 迁移 up 命令
|
||||||
|
|
||||||
- **WHEN** 执行 `make migrate-up`
|
- **WHEN** 执行 `make backend-migrate-up`
|
||||||
- **THEN** SHALL 执行所有待执行的迁移
|
- **THEN** SHALL 执行所有待执行的迁移
|
||||||
|
- **THEN** SHALL 使用 `DB_DRIVER` 变量选择方言目录(默认 `sqlite3`)
|
||||||
|
- **THEN** SHALL 使用 `DB_DSN` 变量作为数据库连接串
|
||||||
- **THEN** SHALL 显示迁移进度
|
- **THEN** SHALL 显示迁移进度
|
||||||
|
|
||||||
#### Scenario: 迁移 down 命令
|
#### Scenario: 迁移 down 命令
|
||||||
|
|
||||||
- **WHEN** 执行 `make migrate-down`
|
- **WHEN** 执行 `make backend-migrate-down`
|
||||||
- **THEN** SHALL 回滚最后一个迁移
|
- **THEN** SHALL 回滚最后一个迁移
|
||||||
|
- **THEN** SHALL 使用 `DB_DRIVER` 和 `DB_DSN` 变量
|
||||||
- **THEN** SHALL 显示回滚进度
|
- **THEN** SHALL 显示回滚进度
|
||||||
|
|
||||||
#### Scenario: 迁移状态命令
|
#### Scenario: 迁移状态命令
|
||||||
|
|
||||||
- **WHEN** 执行 `make migrate-status`
|
- **WHEN** 执行 `make backend-migrate-status`
|
||||||
- **THEN** SHALL 显示当前迁移状态
|
- **THEN** SHALL 显示当前迁移状态
|
||||||
- **THEN** SHALL 显示已执行和待执行的迁移
|
- **THEN** SHALL 显示已执行和待执行的迁移
|
||||||
|
|
||||||
#### Scenario: 创建迁移命令
|
#### Scenario: 创建迁移命令
|
||||||
|
|
||||||
- **WHEN** 执行 `make migrate-create name=<name>`
|
- **WHEN** 执行 `make backend-migrate-create`
|
||||||
- **THEN** SHALL 创建新的迁移文件模板
|
- **THEN** SHALL 同时在 `migrations/sqlite/` 和 `migrations/mysql/` 两个目录创建新的迁移文件模板
|
||||||
- **THEN** SHALL 使用递增的版本号
|
- **THEN** SHALL 使用递增的版本号
|
||||||
|
|
||||||
|
#### Scenario: MySQL 迁移命令使用
|
||||||
|
|
||||||
|
- **WHEN** 使用 MySQL 驱动执行迁移
|
||||||
|
- **THEN** SHALL 设置 `DB_DRIVER=mysql`
|
||||||
|
- **THEN** SHALL 设置 `DB_DSN` 为 MySQL 连接串(如 `user:pass@tcp(localhost:3306)/nex`)
|
||||||
|
|
||||||
### Requirement: 应用启动时迁移
|
### Requirement: 应用启动时迁移
|
||||||
|
|
||||||
应用 SHALL 在启动时执行迁移。
|
应用 SHALL 在启动时执行迁移。
|
||||||
@@ -92,6 +109,9 @@
|
|||||||
#### Scenario: 自动迁移
|
#### Scenario: 自动迁移
|
||||||
|
|
||||||
- **WHEN** 应用启动
|
- **WHEN** 应用启动
|
||||||
|
- **THEN** SHALL 根据 `database.driver` 配置选择对应的迁移目录和 goose dialect
|
||||||
|
- **THEN** SHALL 在 `driver=sqlite` 时使用 `migrations/sqlite/` 目录,goose dialect 为 `sqlite3`
|
||||||
|
- **THEN** SHALL 在 `driver=mysql` 时使用 `migrations/mysql/` 目录,goose dialect 为 `mysql`
|
||||||
- **THEN** SHALL 自动执行待执行的迁移
|
- **THEN** SHALL 自动执行待执行的迁移
|
||||||
- **THEN** SHALL 在迁移失败时拒绝启动
|
- **THEN** SHALL 在迁移失败时拒绝启动
|
||||||
- **THEN** SHALL 记录迁移日志
|
- **THEN** SHALL 记录迁移日志
|
||||||
@@ -149,5 +169,5 @@
|
|||||||
#### Scenario: 迁移文件存储
|
#### Scenario: 迁移文件存储
|
||||||
|
|
||||||
- **WHEN** 创建迁移文件
|
- **WHEN** 创建迁移文件
|
||||||
- **THEN** SHALL 存储在 migrations/ 目录
|
- **THEN** SHALL 按 SQL 方言存储在对应子目录(`migrations/sqlite/` 或 `migrations/mysql/`)
|
||||||
- **THEN** SHALL 提交到版本控制系统
|
- **THEN** SHALL 提交到版本控制系统
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 支持作为桌面应用启动,将后端服务与前端静态资源打包为单一可执行文件。
|
系统 SHALL 支持作为桌面应用启动,将后端服务与前端静态资源打包为单一可执行文件。
|
||||||
|
|
||||||
#### Scenario: 双击启动
|
#### Scenario: 双击启动
|
||||||
|
|
||||||
- **WHEN** 用户双击桌面应用可执行文件
|
- **WHEN** 用户双击桌面应用可执行文件
|
||||||
- **THEN** 系统使用 `gofrs/flock` 尝试获取排他文件锁
|
- **THEN** 系统使用 `gofrs/flock` 尝试获取排他文件锁
|
||||||
- **AND** 锁文件路径为系统临时目录下的 `nex-gateway.lock`
|
- **AND** 锁文件路径为系统临时目录下的 `nex-gateway.lock`
|
||||||
@@ -19,12 +20,14 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
- **AND** 浏览器自动打开 `http://localhost:9826` 显示管理界面
|
- **AND** 浏览器自动打开 `http://localhost:9826` 显示管理界面
|
||||||
|
|
||||||
#### Scenario: 单实例检查
|
#### Scenario: 单实例检查
|
||||||
|
|
||||||
- **WHEN** 用户尝试启动第二个实例
|
- **WHEN** 用户尝试启动第二个实例
|
||||||
- **THEN** 系统检测到已有实例持有文件锁
|
- **THEN** 系统检测到已有实例持有文件锁
|
||||||
- **AND** 显示错误提示"已有 Nex 实例运行"
|
- **AND** 显示错误提示"已有 Nex 实例运行"
|
||||||
- **AND** 新实例退出
|
- **AND** 新实例退出
|
||||||
|
|
||||||
#### Scenario: 退出释放锁
|
#### Scenario: 退出释放锁
|
||||||
|
|
||||||
- **WHEN** 用户点击托盘菜单"退出"
|
- **WHEN** 用户点击托盘菜单"退出"
|
||||||
- **THEN** 系统释放文件锁
|
- **THEN** 系统释放文件锁
|
||||||
- **AND** 应用进程退出
|
- **AND** 应用进程退出
|
||||||
@@ -34,6 +37,7 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择。
|
系统 SHALL 提供跨平台系统托盘功能,支持托盘图标和菜单。图标格式 SHALL 根据平台自动选择。
|
||||||
|
|
||||||
#### Scenario: 托盘图标显示
|
#### Scenario: 托盘图标显示
|
||||||
|
|
||||||
- **WHEN** 桌面应用启动成功
|
- **WHEN** 桌面应用启动成功
|
||||||
- **THEN** 系统根据平台加载正确的图标格式
|
- **THEN** 系统根据平台加载正确的图标格式
|
||||||
- **AND** 在 Windows 上加载 ICO 格式图标(`assets/icon.ico`)
|
- **AND** 在 Windows 上加载 ICO 格式图标(`assets/icon.ico`)
|
||||||
@@ -41,6 +45,7 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
- **AND** 托盘图标 tooltip 显示"AI Gateway"
|
- **AND** 托盘图标 tooltip 显示"AI Gateway"
|
||||||
|
|
||||||
#### Scenario: 托盘菜单显示
|
#### Scenario: 托盘菜单显示
|
||||||
|
|
||||||
- **WHEN** 用户点击托盘图标(左键或右键)
|
- **WHEN** 用户点击托盘图标(左键或右键)
|
||||||
- **THEN** 显示托盘菜单
|
- **THEN** 显示托盘菜单
|
||||||
- **AND** 菜单包含"打开管理界面"选项
|
- **AND** 菜单包含"打开管理界面"选项
|
||||||
@@ -50,15 +55,18 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
- **AND** 菜单包含"退出"选项
|
- **AND** 菜单包含"退出"选项
|
||||||
|
|
||||||
#### Scenario: 打开管理界面
|
#### Scenario: 打开管理界面
|
||||||
|
|
||||||
- **WHEN** 用户点击托盘菜单"打开管理界面"
|
- **WHEN** 用户点击托盘菜单"打开管理界面"
|
||||||
- **THEN** 系统在浏览器中打开 `http://localhost:9826`
|
- **THEN** 系统在浏览器中打开 `http://localhost:9826`
|
||||||
|
|
||||||
#### Scenario: 浏览器打开失败
|
#### Scenario: 浏览器打开失败
|
||||||
|
|
||||||
- **WHEN** 系统无法打开浏览器(浏览器未安装等)
|
- **WHEN** 系统无法打开浏览器(浏览器未安装等)
|
||||||
- **THEN** 托盘菜单仍可正常使用
|
- **THEN** 托盘菜单仍可正常使用
|
||||||
- **AND** 用户可手动访问 `http://localhost:9826`
|
- **AND** 用户可手动访问 `http://localhost:9826`
|
||||||
|
|
||||||
#### Scenario: 退出应用
|
#### Scenario: 退出应用
|
||||||
|
|
||||||
- **WHEN** 用户点击托盘菜单"退出"
|
- **WHEN** 用户点击托盘菜单"退出"
|
||||||
- **THEN** 系统优雅关闭后端服务
|
- **THEN** 系统优雅关闭后端服务
|
||||||
- **AND** 托盘图标消失
|
- **AND** 托盘图标消失
|
||||||
@@ -69,14 +77,17 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 通过 Gin 同时服务 API 和前端静态资源。
|
系统 SHALL 通过 Gin 同时服务 API 和前端静态资源。
|
||||||
|
|
||||||
#### Scenario: API 请求路由
|
#### Scenario: API 请求路由
|
||||||
|
|
||||||
- **WHEN** 请求路径以 `/api/` 或 `/v1/` 开头
|
- **WHEN** 请求路径以 `/api/` 或 `/v1/` 开头
|
||||||
- **THEN** 请求由现有业务 handler 处理
|
- **THEN** 请求由现有业务 handler 处理
|
||||||
|
|
||||||
#### Scenario: 静态资源路由
|
#### Scenario: 静态资源路由
|
||||||
|
|
||||||
- **WHEN** 请求路径为 `/assets/*`
|
- **WHEN** 请求路径为 `/assets/*`
|
||||||
- **THEN** 返回嵌入的前端静态资源文件
|
- **THEN** 返回嵌入的前端静态资源文件
|
||||||
|
|
||||||
#### Scenario: SPA 路由回退
|
#### Scenario: SPA 路由回退
|
||||||
|
|
||||||
- **WHEN** 请求路径不匹配任何 API 或静态资源路由
|
- **WHEN** 请求路径不匹配任何 API 或静态资源路由
|
||||||
- **THEN** 返回 `index.html`(支持前端 SPA 路由)
|
- **THEN** 返回 `index.html`(支持前端 SPA 路由)
|
||||||
|
|
||||||
@@ -85,10 +96,12 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 在启动前检测端口是否可用。
|
系统 SHALL 在启动前检测端口是否可用。
|
||||||
|
|
||||||
#### Scenario: 端口可用
|
#### Scenario: 端口可用
|
||||||
|
|
||||||
- **WHEN** 端口 9826 未被占用
|
- **WHEN** 端口 9826 未被占用
|
||||||
- **THEN** 服务正常启动
|
- **THEN** 服务正常启动
|
||||||
|
|
||||||
#### Scenario: 端口被占用
|
#### Scenario: 端口被占用
|
||||||
|
|
||||||
- **WHEN** 端口 9826 已被其他程序占用
|
- **WHEN** 端口 9826 已被其他程序占用
|
||||||
- **THEN** 显示错误提示"端口 9826 已被占用"
|
- **THEN** 显示错误提示"端口 9826 已被占用"
|
||||||
- **AND** 应用退出
|
- **AND** 应用退出
|
||||||
@@ -98,17 +111,20 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,产物文件名 SHALL 使用 `nex-{os}-{arch}[.exe]` 格式。
|
系统 SHALL 支持跨平台构建和打包。构建 target SHALL 按平台分离,产物文件名 SHALL 使用 `nex-{os}-{arch}[.exe]` 格式。
|
||||||
|
|
||||||
#### Scenario: macOS 构建
|
#### Scenario: macOS 构建
|
||||||
- **WHEN** 执行 `desktop-mac` 构建命令
|
|
||||||
|
- **WHEN** 执行 `desktop-build-mac` 构建命令
|
||||||
- **THEN** 生成 `nex-mac-arm64` 和 `nex-mac-amd64` 可执行文件
|
- **THEN** 生成 `nex-mac-arm64` 和 `nex-mac-amd64` 可执行文件
|
||||||
- **AND** 可打包为 `.app` bundle
|
- **AND** 可打包为 `.app` bundle
|
||||||
|
|
||||||
#### Scenario: Windows 构建
|
#### Scenario: Windows 构建
|
||||||
- **WHEN** 执行 `desktop-win` 构建命令
|
|
||||||
|
- **WHEN** 执行 `desktop-build-win` 构建命令
|
||||||
- **THEN** 生成 `nex-win-amd64.exe` 可执行文件
|
- **THEN** 生成 `nex-win-amd64.exe` 可执行文件
|
||||||
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
|
- **AND** 使用 `-H=windowsgui` linker flag 隐藏控制台窗口
|
||||||
|
|
||||||
#### Scenario: Linux 构建
|
#### Scenario: Linux 构建
|
||||||
- **WHEN** 执行 `desktop-linux` 构建命令
|
|
||||||
|
- **WHEN** 执行 `desktop-build-linux` 构建命令
|
||||||
- **THEN** 生成 `nex-linux-amd64` 可执行文件
|
- **THEN** 生成 `nex-linux-amd64` 可执行文件
|
||||||
|
|
||||||
### Requirement: macOS .app 打包
|
### Requirement: macOS .app 打包
|
||||||
@@ -116,11 +132,12 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 支持打包为 macOS .app bundle。
|
系统 SHALL 支持打包为 macOS .app bundle。
|
||||||
|
|
||||||
#### Scenario: .app 结构
|
#### Scenario: .app 结构
|
||||||
|
|
||||||
- **WHEN** 执行打包脚本
|
- **WHEN** 执行打包脚本
|
||||||
- **THEN** 生成 `Nex.app` 目录结构
|
- **THEN** 生成 `Nex.app` 目录结构
|
||||||
- **AND** 包含 `Contents/Info.plist` 元数据
|
- **AND** 包含 `Contents/Info.plist` 元数据
|
||||||
- **AND** 包含 `Contents/MacOS/nex` 可执行文件
|
- **AND** 包含 `Contents/MacOS/nex` 可执行文件
|
||||||
- **AND** 包含 `Contents/Resources/AppIcon.icns` 图标
|
- **AND** 包含 `Contents/Resources/icon.icns` 图标
|
||||||
- **AND** `Info.plist` 中 `LSUIElement` 为 `true`(不显示 Dock 图标)
|
- **AND** `Info.plist` 中 `LSUIElement` 为 `true`(不显示 Dock 图标)
|
||||||
|
|
||||||
### Requirement: 关于对话框
|
### Requirement: 关于对话框
|
||||||
@@ -128,6 +145,7 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll` 的 `MessageBoxW` API 实现。
|
系统 SHALL 提供关于对话框显示应用信息。在 Windows 上 SHALL 使用 `user32.dll` 的 `MessageBoxW` API 实现。
|
||||||
|
|
||||||
#### Scenario: 显示关于
|
#### Scenario: 显示关于
|
||||||
|
|
||||||
- **WHEN** 用户点击托盘菜单"关于"
|
- **WHEN** 用户点击托盘菜单"关于"
|
||||||
- **THEN** 显示对话框包含应用名称、项目链接
|
- **THEN** 显示对话框包含应用名称、项目链接
|
||||||
- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现
|
- **AND** 在 Windows 上使用 `MessageBoxW` 原生对话框实现
|
||||||
@@ -137,6 +155,7 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。
|
系统 SHALL 在 Windows 上使用 `user32.dll` 的 `MessageBoxW` API 显示错误和关于对话框,替代 `msg *` 命令。
|
||||||
|
|
||||||
#### Scenario: 错误提示对话框
|
#### Scenario: 错误提示对话框
|
||||||
|
|
||||||
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
|
- **WHEN** 应用在 Windows 上遇到启动错误(端口占用、配置加载失败等)
|
||||||
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
||||||
- **AND** 对话框标题栏显示应用名称
|
- **AND** 对话框标题栏显示应用名称
|
||||||
@@ -144,6 +163,7 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
- **AND** 对话框显示错误图标(MB_ICONERROR)
|
- **AND** 对话框显示错误图标(MB_ICONERROR)
|
||||||
|
|
||||||
#### Scenario: 关于对话框
|
#### Scenario: 关于对话框
|
||||||
|
|
||||||
- **WHEN** 用户在 Windows 上点击托盘菜单"关于"
|
- **WHEN** 用户在 Windows 上点击托盘菜单"关于"
|
||||||
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
- **THEN** 使用 `MessageBoxW` 显示模态对话框
|
||||||
- **AND** 对话框标题栏显示"关于 Nex Gateway"
|
- **AND** 对话框标题栏显示"关于 Nex Gateway"
|
||||||
@@ -151,5 +171,60 @@ TBD - 提供跨平台桌面应用支持,将后端服务与前端静态资源
|
|||||||
- **AND** 对话框显示信息图标(MB_ICONINFORMATION)
|
- **AND** 对话框显示信息图标(MB_ICONINFORMATION)
|
||||||
|
|
||||||
#### Scenario: 非 Windows 平台不受影响
|
#### Scenario: 非 Windows 平台不受影响
|
||||||
|
|
||||||
- **WHEN** 应用运行在 macOS 或 Linux 上
|
- **WHEN** 应用运行在 macOS 或 Linux 上
|
||||||
- **THEN** 错误和关于对话框仍使用平台原有实现(osascript / zenity)
|
- **THEN** 错误和关于对话框仍使用平台原有实现(osascript / zenity)
|
||||||
|
|
||||||
|
### Requirement: Linux 对话框降级策略
|
||||||
|
|
||||||
|
系统 SHALL 在 Linux 上按优先级检测并使用可用的对话框工具,确保在不同桌面环境下都能显示对话框。
|
||||||
|
|
||||||
|
#### Scenario: zenity 可用
|
||||||
|
- **WHEN** 系统检测到 `zenity` 命令可用
|
||||||
|
- **THEN** 使用 zenity 显示 GTK 风格对话框
|
||||||
|
- **AND** 错误对话框使用 `zenity --error` 命令
|
||||||
|
- **AND** 信息对话框使用 `zenity --info` 命令
|
||||||
|
|
||||||
|
#### Scenario: kdialog 可用
|
||||||
|
- **WHEN** zenity 不可用且 `kdialog` 命令可用
|
||||||
|
- **THEN** 使用 kdialog 显示 KDE 风格对话框
|
||||||
|
- **AND** 错误对话框使用 `kdialog --error` 命令
|
||||||
|
- **AND** 信息对话框使用 `kdialog --msgbox` 命令
|
||||||
|
|
||||||
|
#### Scenario: notify-send 可用
|
||||||
|
- **WHEN** zenity 和 kdialog 均不可用且 `notify-send` 命令可用
|
||||||
|
- **THEN** 使用 notify-send 显示系统通知
|
||||||
|
- **AND** 错误通知使用 `-u critical` 参数
|
||||||
|
- **AND** 信息通知使用默认参数
|
||||||
|
|
||||||
|
#### Scenario: xmessage 可用
|
||||||
|
- **WHEN** zenity、kdialog、notify-send 均不可用且 `xmessage` 命令可用
|
||||||
|
- **THEN** 使用 xmessage 显示基础 X11 对话框
|
||||||
|
- **AND** 对话框居中显示(`-center` 参数)
|
||||||
|
|
||||||
|
#### Scenario: 无对话框工具可用
|
||||||
|
- **WHEN** 所有对话框工具均不可用
|
||||||
|
- **THEN** 降级到标准错误输出
|
||||||
|
- **AND** 输出格式为 `错误: <title>: <message>`
|
||||||
|
|
||||||
|
#### Scenario: 工具检测缓存
|
||||||
|
- **WHEN** 应用启动
|
||||||
|
- **THEN** 系统检测一次可用对话框工具
|
||||||
|
- **AND** 检测结果缓存在包级变量
|
||||||
|
- **AND** 后续对话框调用直接使用缓存结果,不重复检测
|
||||||
|
|
||||||
|
### Requirement: macOS AppleScript 字符转义
|
||||||
|
|
||||||
|
系统 SHALL 对 AppleScript 对话框中的特殊字符进行转义,确保脚本正确执行。
|
||||||
|
|
||||||
|
#### Scenario: 转义反斜杠
|
||||||
|
- **WHEN** 对话框消息包含反斜杠字符 `\`
|
||||||
|
- **THEN** 转义为 `\\`
|
||||||
|
|
||||||
|
#### Scenario: 转义双引号
|
||||||
|
- **WHEN** 对话框消息包含双引号字符 `"`
|
||||||
|
- **THEN** 转义为 `\"`
|
||||||
|
|
||||||
|
#### Scenario: 多行文本处理
|
||||||
|
- **WHEN** 对话框消息包含换行符 `\n`
|
||||||
|
- **THEN** AppleScript 正确显示多行文本
|
||||||
|
|||||||
117
openspec/specs/frontend-obfuscation/spec.md
Normal file
117
openspec/specs/frontend-obfuscation/spec.md
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
# 前端代码混淆
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
在生产构建时对前端业务代码进行混淆处理,增加逆向成本,防止代码被轻易复制。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 生产构建时代码混淆
|
||||||
|
|
||||||
|
前端 SHALL 在生产构建时对业务代码进行混淆处理。
|
||||||
|
|
||||||
|
#### Scenario: 安装混淆依赖
|
||||||
|
|
||||||
|
- **WHEN** 添加代码混淆功能
|
||||||
|
- **THEN** 前端 SHALL 使用 bun 安装 `vite-plugin-javascript-obfuscator` 和 `javascript-obfuscator` 依赖
|
||||||
|
- **THEN** 前端 SHALL NOT 使用 npm 或 pnpm 安装依赖
|
||||||
|
|
||||||
|
#### Scenario: 配置混淆插件
|
||||||
|
|
||||||
|
- **WHEN** 配置 Vite 构建流程
|
||||||
|
- **THEN** vite.config.ts SHALL 导入 `vite-plugin-javascript-obfuscator`
|
||||||
|
- **THEN** 插件 SHALL 配置 `apply: 'build'` 仅在构建时生效
|
||||||
|
- **THEN** 插件 SHALL 配置 `include` 仅包含业务代码(src/**/*.ts, src/**/*.tsx, src/**/*.js, src/**/*.jsx)
|
||||||
|
- **THEN** 插件 SHALL 配置 `exclude` 排除 node_modules
|
||||||
|
|
||||||
|
#### Scenario: 变量名混淆
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 业务代码中的变量名 SHALL 被转换为十六进制形式(如 _0x2b3c)
|
||||||
|
- **THEN** identifierNamesGenerator SHALL 配置为 'mangled-shuffled'
|
||||||
|
|
||||||
|
#### Scenario: 字符串混淆
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 业务代码中的字符串 SHALL 被编码为字符串数组
|
||||||
|
- **THEN** stringArray SHALL 配置为 true
|
||||||
|
- **THEN** stringArrayEncoding SHALL 配置为 ['base64']
|
||||||
|
- **THEN** stringArrayRotate SHALL 配置为 true
|
||||||
|
- **THEN** stringArrayShuffle SHALL 配置为 true
|
||||||
|
- **THEN** stringArrayThreshold SHALL 配置为 0.75
|
||||||
|
- **THEN** splitStrings SHALL 配置为 true
|
||||||
|
- **THEN** splitStringsChunkLength SHALL 配置为 10
|
||||||
|
|
||||||
|
#### Scenario: 对象键混淆
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 业务代码中的对象键 SHALL 被混淆
|
||||||
|
- **THEN** transformObjectKeys SHALL 配置为 true
|
||||||
|
|
||||||
|
#### Scenario: 数字混淆
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 业务代码中的数字 SHALL 被转换为表达式
|
||||||
|
- **THEN** numbersToExpressions SHALL 配置为 true
|
||||||
|
|
||||||
|
#### Scenario: 调试保护
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 混淆代码 SHALL 禁用浏览器调试器
|
||||||
|
- **THEN** debugProtection SHALL 配置为 true
|
||||||
|
- **THEN** debugProtectionInterval SHALL 配置为 2000
|
||||||
|
|
||||||
|
#### Scenario: 禁用 console 输出
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 混淆代码 SHALL 移除所有 console 输出
|
||||||
|
- **THEN** disableConsoleOutput SHALL 配置为 true
|
||||||
|
|
||||||
|
#### Scenario: 不混淆第三方库
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** node_modules 中的代码 SHALL NOT 被混淆
|
||||||
|
- **THEN** vendor chunks(React、TDesign、Recharts)SHALL 保持原有压缩状态
|
||||||
|
|
||||||
|
#### Scenario: 不生成 Source Map
|
||||||
|
|
||||||
|
- **WHEN** 生产构建执行
|
||||||
|
- **THEN** 构建产物 SHALL NOT 包含 Source Map 文件
|
||||||
|
- **THEN** vite.config.ts 中 build.sourcemap SHALL 配置为 false
|
||||||
|
|
||||||
|
#### Scenario: 不启用控制流扁平化
|
||||||
|
|
||||||
|
- **WHEN** 配置混淆选项
|
||||||
|
- **THEN** controlFlowFlattening SHALL 配置为 false
|
||||||
|
- **THEN** 前端 SHALL NOT 使用控制流扁平化混淆
|
||||||
|
|
||||||
|
#### Scenario: 不注入死代码
|
||||||
|
|
||||||
|
- **WHEN** 配置混淆选项
|
||||||
|
- **THEN** deadCodeInjection SHALL 配置为 false
|
||||||
|
- **THEN** 前端 SHALL NOT 注入死代码
|
||||||
|
|
||||||
|
#### Scenario: 不启用自我保护
|
||||||
|
|
||||||
|
- **WHEN** 配置混淆选项
|
||||||
|
- **THEN** selfDefending SHALL 配置为 false
|
||||||
|
- **THEN** 前端 SHALL NOT 启用自我保护机制
|
||||||
|
|
||||||
|
#### Scenario: 构建产物验证
|
||||||
|
|
||||||
|
- **WHEN** 生产构建完成
|
||||||
|
- **THEN** 构建产物 SHALL 通过功能测试
|
||||||
|
- **THEN** 关键业务功能 SHALL 正常工作
|
||||||
|
- **THEN** 页面 SHALL 正常渲染
|
||||||
|
- **THEN** API 调用 SHALL 正常执行
|
||||||
|
|
||||||
|
### Requirement: 开发模式不混淆
|
||||||
|
|
||||||
|
前端 SHALL 仅在生产构建时启用混淆,开发模式不受影响。
|
||||||
|
|
||||||
|
#### Scenario: 开发模式跳过混淆
|
||||||
|
|
||||||
|
- **WHEN** 执行 vite dev 启动开发服务器
|
||||||
|
- **THEN** 代码 SHALL NOT 被混淆
|
||||||
|
- **THEN** 开发体验 SHALL 保持不变
|
||||||
|
- **THEN** 热更新 SHALL 正常工作
|
||||||
@@ -502,6 +502,9 @@ TBD - 提供供应商、模型配置和用量统计的前端管理界面
|
|||||||
|
|
||||||
- **WHEN** 为生产构建前端
|
- **WHEN** 为生产构建前端
|
||||||
- **THEN** Vite SHALL 生成优化的静态文件
|
- **THEN** Vite SHALL 生成优化的静态文件
|
||||||
|
- **THEN** Vite SHALL 对业务代码执行混淆处理
|
||||||
|
- **THEN** 混淆 SHALL 仅应用于 src 目录下的业务代码
|
||||||
|
- **THEN** 混淆 SHALL NOT 应用于 node_modules 中的第三方库
|
||||||
|
|
||||||
### Requirement: 与后端 API 通信
|
### Requirement: 与后端 API 通信
|
||||||
|
|
||||||
|
|||||||
92
openspec/specs/gorm-logging/spec.md
Normal file
92
openspec/specs/gorm-logging/spec.md
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
# GORM Logging
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
定义 GORM 日志适配器规范,将 GORM 日志桥接到 zap,实现数据库操作日志统一格式和请求追踪。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: GORM 日志适配器
|
||||||
|
|
||||||
|
系统 SHALL 提供 GORM 日志适配器,桥接到 zap。
|
||||||
|
|
||||||
|
#### Scenario: 适配器实现
|
||||||
|
|
||||||
|
- **WHEN** 初始化数据库连接
|
||||||
|
- **THEN** SHALL 使用 `logger.NewGormLogger(zapLogger)` 作为 GORM logger
|
||||||
|
- **THEN** 适配器 SHALL 实现 `gorm.logger.Interface` 接口
|
||||||
|
|
||||||
|
#### Scenario: 日志级别映射
|
||||||
|
|
||||||
|
- **WHEN** GORM 记录日志
|
||||||
|
- **THEN** GORM Silent SHALL 映射到不输出
|
||||||
|
- **THEN** GORM Error SHALL 映射到 `zap.ErrorLevel`
|
||||||
|
- **THEN** GORM Warn SHALL 映射到 `zap.WarnLevel`
|
||||||
|
- **THEN** GORM Info SHALL 映射到 `zap.DebugLevel`
|
||||||
|
|
||||||
|
#### Scenario: SQL 查询日志
|
||||||
|
|
||||||
|
- **WHEN** GORM 执行 SQL 查询
|
||||||
|
- **THEN** SHALL 使用 zap Debug 级别记录
|
||||||
|
- **THEN** SHALL 包含 SQL 语句
|
||||||
|
- **THEN** SHALL 包含执行耗时
|
||||||
|
- **THEN** SHALL 包含影响行数(如有)
|
||||||
|
|
||||||
|
### Requirement: Request ID 关联
|
||||||
|
|
||||||
|
GORM 日志 SHALL 支持 request_id 关联。
|
||||||
|
|
||||||
|
#### Scenario: 从 Context 提取 request_id
|
||||||
|
|
||||||
|
- **WHEN** GORM 记录日志
|
||||||
|
- **THEN** SHALL 从 `context.Context` 提取 request_id
|
||||||
|
- **THEN** 日志 SHALL 包含 request_id 字段
|
||||||
|
|
||||||
|
#### Scenario: Context 传播
|
||||||
|
|
||||||
|
- **WHEN** 使用 GORM 进行数据库操作
|
||||||
|
- **THEN** SHALL 使用 `db.WithContext(ctx)` 传递 context
|
||||||
|
- **THEN** GORM 日志 SHALL 自动包含 request_id
|
||||||
|
|
||||||
|
### Requirement: 模块标识
|
||||||
|
|
||||||
|
GORM 日志 SHALL 包含模块标识。
|
||||||
|
|
||||||
|
#### Scenario: 数据库模块标识
|
||||||
|
|
||||||
|
- **WHEN** GORM 记录日志
|
||||||
|
- **THEN** SHALL 包含 `logger: database` 标识
|
||||||
|
- **THEN** Console 格式 SHALL 输出为 `[database]`
|
||||||
|
|
||||||
|
### Requirement: 单行输出
|
||||||
|
|
||||||
|
GORM 日志 SHALL 单行输出。
|
||||||
|
|
||||||
|
#### Scenario: SQL 单行输出
|
||||||
|
|
||||||
|
- **WHEN** 记录 SQL 查询日志
|
||||||
|
- **THEN** SHALL 单行输出
|
||||||
|
- **THEN** SQL 语句中的换行 SHALL 替换为空格(除非保留原始格式)
|
||||||
|
|
||||||
|
#### Scenario: 错误日志单行
|
||||||
|
|
||||||
|
- **WHEN** 记录数据库错误日志
|
||||||
|
- **THEN** SHALL 单行输出
|
||||||
|
- **THEN** 错误详情 SHALL 作为字段值
|
||||||
|
|
||||||
|
### Requirement: 日志字段标准化
|
||||||
|
|
||||||
|
GORM 日志 SHALL 使用标准化字段。
|
||||||
|
|
||||||
|
#### Scenario: SQL 日志字段
|
||||||
|
|
||||||
|
- **WHEN** 记录 SQL 查询
|
||||||
|
- **THEN** SHALL 使用 `sql` 字段记录 SQL 语句
|
||||||
|
- **THEN** SHALL 使用 `rows_affected` 字段记录影响行数
|
||||||
|
- **THEN** SHALL 使用 `latency` 字段记录执行耗时
|
||||||
|
|
||||||
|
#### Scenario: 错误日志字段
|
||||||
|
|
||||||
|
- **WHEN** 记录数据库错误
|
||||||
|
- **THEN** SHALL 使用 `zap.Error(err)` 记录错误
|
||||||
|
- **THEN** SHALL NOT 使用 `zap.String("error", err.Error())`
|
||||||
140
openspec/specs/module-logging/spec.md
Normal file
140
openspec/specs/module-logging/spec.md
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
# Module Logging
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
定义模块化日志器规范,支持模块标识和 Context 传播,实现日志来源快速定位和请求追踪链完整性。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: 模块标识注入
|
||||||
|
|
||||||
|
系统 SHALL 支持通过构造函数注入带模块标识的 logger。
|
||||||
|
|
||||||
|
#### Scenario: 构造函数注入模块 logger
|
||||||
|
|
||||||
|
- **WHEN** 创建需要记录日志的组件
|
||||||
|
- **THEN** 构造函数 SHALL 接受 `*zap.Logger` 参数
|
||||||
|
- **THEN** SHALL 使用 `logger.Named("module.name")` 绑定模块标识
|
||||||
|
- **THEN** SHALL 将 logger 存储在结构体字段中
|
||||||
|
|
||||||
|
#### Scenario: 模块标识格式
|
||||||
|
|
||||||
|
- **WHEN** 绑定模块标识
|
||||||
|
- **THEN** 单一职责包 SHALL 使用包名(如 `database`)
|
||||||
|
- **THEN** 多实体包 SHALL 使用 `包名.实体名`(如 `handler.proxy`)
|
||||||
|
- **THEN** 子包 SHALL 使用 `包名.子包名`(如 `handler.middleware`)
|
||||||
|
|
||||||
|
#### Scenario: 模块标识输出
|
||||||
|
|
||||||
|
- **WHEN** 记录日志
|
||||||
|
- **THEN** Console 格式 SHALL 输出为 `[module.name]`
|
||||||
|
- **THEN** JSON 格式 SHALL 包含 `"logger":"module.name"` 字段
|
||||||
|
|
||||||
|
### Requirement: 禁止全局 logger
|
||||||
|
|
||||||
|
系统 SHALL 禁止在业务代码中使用全局 logger。
|
||||||
|
|
||||||
|
#### Scenario: 移除 zap.L() 调用
|
||||||
|
|
||||||
|
- **WHEN** 重构现有代码
|
||||||
|
- **THEN** SHALL 移除所有 `zap.L()` 调用
|
||||||
|
- **THEN** SHALL 通过构造函数注入 logger
|
||||||
|
- **THEN** 允许仅在测试代码中使用 `zap.L()` 或 `zap.NewNop()`
|
||||||
|
|
||||||
|
#### Scenario: 移除 zap.L() fallback
|
||||||
|
|
||||||
|
- **WHEN** 构造函数 logger 参数为 nil
|
||||||
|
- **THEN** SHALL NOT 使用 `zap.L()` 作为默认值
|
||||||
|
- **THEN** 调用方 SHALL 必须传入有效的 logger
|
||||||
|
|
||||||
|
### Requirement: Request ID 传播
|
||||||
|
|
||||||
|
系统 SHALL 通过 Context 传播 request_id。
|
||||||
|
|
||||||
|
#### Scenario: Context 注入 request_id
|
||||||
|
|
||||||
|
- **WHEN** 中间件生成 request_id
|
||||||
|
- **THEN** SHALL 存储到 `context.Context` 中
|
||||||
|
- **THEN** SHALL 使用类型安全的 context key
|
||||||
|
|
||||||
|
#### Scenario: 从 Context 提取 request_id
|
||||||
|
|
||||||
|
- **WHEN** 业务层需要记录日志
|
||||||
|
- **THEN** SHALL 从 `gin.Context` 提取 request_id
|
||||||
|
- **THEN** SHALL 使用 `logger.With(zap.String("request_id", id))` 创建带 request_id 的 logger
|
||||||
|
- **THEN** 日志 SHALL 自动包含 request_id 字段
|
||||||
|
|
||||||
|
#### Scenario: Request ID 辅助函数
|
||||||
|
|
||||||
|
- **WHEN** 使用 request_id
|
||||||
|
- **THEN** `pkg/logger` SHALL 提供 `RequestIDFromContext(ctx)` 辅助函数
|
||||||
|
- **THEN** 辅助函数 SHALL 返回 `zap.Field` 类型
|
||||||
|
|
||||||
|
### Requirement: 单行输出
|
||||||
|
|
||||||
|
系统 SHALL 保证所有日志单行输出。
|
||||||
|
|
||||||
|
#### Scenario: Console 单行输出
|
||||||
|
|
||||||
|
- **WHEN** 记录日志到 stdout
|
||||||
|
- **THEN** SHALL 单行输出
|
||||||
|
- **THEN** 字段之间 SHALL 使用空格分隔
|
||||||
|
|
||||||
|
#### Scenario: JSON 单行输出
|
||||||
|
|
||||||
|
- **WHEN** 记录日志到文件
|
||||||
|
- **THEN** SHALL 单行紧凑输出
|
||||||
|
- **THEN** SHALL NOT 使用美化缩进
|
||||||
|
|
||||||
|
#### Scenario: 多行数据保留
|
||||||
|
|
||||||
|
- **WHEN** 日志数据本身包含多行(如堆栈跟踪、SQL 换行)
|
||||||
|
- **THEN** SHALL 保留原始多行格式
|
||||||
|
|
||||||
|
### Requirement: 模块命名规范
|
||||||
|
|
||||||
|
系统 SHALL 遵循模块命名规范。
|
||||||
|
|
||||||
|
#### Scenario: Handler 层命名
|
||||||
|
|
||||||
|
- **WHEN** 创建 handler 层 logger
|
||||||
|
- **THEN** ProxyHandler SHALL 使用 `handler.proxy`
|
||||||
|
- **THEN** ProviderHandler SHALL 使用 `handler.provider`
|
||||||
|
- **THEN** ModelHandler SHALL 使用 `handler.model`
|
||||||
|
- **THEN** StatsHandler SHALL 使用 `handler.stats`
|
||||||
|
- **THEN** Middleware SHALL 使用 `handler.middleware`
|
||||||
|
|
||||||
|
#### Scenario: Provider 层命名
|
||||||
|
|
||||||
|
- **WHEN** 创建 provider 层 logger
|
||||||
|
- **THEN** Client SHALL 使用 `provider.client`
|
||||||
|
|
||||||
|
#### Scenario: Conversion 层命名
|
||||||
|
|
||||||
|
- **WHEN** 创建 conversion 层 logger
|
||||||
|
- **THEN** Engine SHALL 使用 `conversion.engine`
|
||||||
|
- **THEN** OpenAI Adapter SHALL 使用 `conversion.openai`
|
||||||
|
- **THEN** Anthropic Adapter SHALL 使用 `conversion.anthropic`
|
||||||
|
|
||||||
|
#### Scenario: Service 层命名
|
||||||
|
|
||||||
|
- **WHEN** 创建 service 层 logger
|
||||||
|
- **THEN** RoutingCache SHALL 使用 `service.routing_cache`
|
||||||
|
- **THEN** StatsBuffer SHALL 使用 `service.stats_buffer`
|
||||||
|
- **THEN** ProviderService SHALL 使用 `service.provider`
|
||||||
|
- **THEN** ModelService SHALL 使用 `service.model`
|
||||||
|
- **THEN** RoutingService SHALL 使用 `service.routing`
|
||||||
|
- **THEN** StatsService SHALL 使用 `service.stats`
|
||||||
|
|
||||||
|
#### Scenario: Repository 层命名
|
||||||
|
|
||||||
|
- **WHEN** 创建 repository 层 logger
|
||||||
|
- **THEN** ProviderRepository SHALL 使用 `repository.provider`
|
||||||
|
- **THEN** ModelRepository SHALL 使用 `repository.model`
|
||||||
|
- **THEN** StatsRepository SHALL 使用 `repository.stats`
|
||||||
|
|
||||||
|
#### Scenario: Infrastructure 层命名
|
||||||
|
|
||||||
|
- **WHEN** 创建基础设施层 logger
|
||||||
|
- **THEN** Database SHALL 使用 `database`
|
||||||
|
- **THEN** Config SHALL 使用 `config`
|
||||||
107
openspec/specs/mysql-driver/spec.md
Normal file
107
openspec/specs/mysql-driver/spec.md
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
# MySQL Driver
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
支持 MySQL 作为可选数据库后端,通过配置选择 sqlite 或 mysql 驱动,提供 MySQL 连接管理、初始化和方言迁移文件。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: MySQL 数据库驱动支持
|
||||||
|
|
||||||
|
系统 SHALL 支持通过配置项 `database.driver` 选择 `sqlite` 或 `mysql` 数据库驱动,默认值为 `sqlite`。
|
||||||
|
|
||||||
|
#### Scenario: 默认使用 SQLite 驱动
|
||||||
|
|
||||||
|
- **WHEN** 配置中未指定 `database.driver`
|
||||||
|
- **THEN** SHALL 使用 `sqlite` 作为数据库驱动
|
||||||
|
- **THEN** SHALL 行为与现有逻辑完全一致
|
||||||
|
|
||||||
|
#### Scenario: 配置 MySQL 驱动
|
||||||
|
|
||||||
|
- **WHEN** 配置 `database.driver` 设置为 `mysql`
|
||||||
|
- **THEN** SHALL 使用 MySQL 驱动连接远程数据库
|
||||||
|
- **THEN** SHALL 使用 `gorm.io/driver/mysql` 打开连接
|
||||||
|
- **THEN** SHALL 构建 DSN 格式为 `{user}:{password}@tcp({host}:{port})/{dbname}?charset=utf8mb4&parseTime=true&loc=Local`
|
||||||
|
|
||||||
|
#### Scenario: driver 值不合法
|
||||||
|
|
||||||
|
- **WHEN** 配置 `database.driver` 不是 `sqlite` 或 `mysql`
|
||||||
|
- **THEN** SHALL 配置验证失败,拒绝启动
|
||||||
|
|
||||||
|
### Requirement: MySQL 连接配置
|
||||||
|
|
||||||
|
系统 SHALL 在 `DatabaseConfig` 中支持 MySQL 连接参数。
|
||||||
|
|
||||||
|
#### Scenario: MySQL 连接参数字段
|
||||||
|
|
||||||
|
- **WHEN** `database.driver` 为 `mysql`
|
||||||
|
- **THEN** SHALL 读取 `host`(MySQL 主机地址,必填)
|
||||||
|
- **THEN** SHALL 读取 `port`(MySQL 端口,默认 3306)
|
||||||
|
- **THEN** SHALL 读取 `user`(MySQL 用户名,必填)
|
||||||
|
- **THEN** SHALL 读取 `password`(MySQL 密码,选填)
|
||||||
|
- **THEN** SHALL 读取 `dbname`(数据库名,必填)
|
||||||
|
|
||||||
|
#### Scenario: SQLite 模式忽略 MySQL 参数
|
||||||
|
|
||||||
|
- **WHEN** `database.driver` 为 `sqlite`
|
||||||
|
- **THEN** SHALL 忽略 MySQL 相关配置字段(host/port/user/password/dbname)
|
||||||
|
- **THEN** SHALL 仅使用 `path` 字段作为数据库文件路径
|
||||||
|
|
||||||
|
#### Scenario: MySQL 模式忽略 SQLite 参数
|
||||||
|
|
||||||
|
- **WHEN** `database.driver` 为 `mysql`
|
||||||
|
- **THEN** SHALL 忽略 `path` 字段
|
||||||
|
|
||||||
|
### Requirement: 数据库初始化公共包
|
||||||
|
|
||||||
|
系统 SHALL 提供 `internal/database` 公共包,封装数据库初始化、迁移执行和连接关闭逻辑,供 `cmd/server` 和 `cmd/desktop` 共同调用。
|
||||||
|
|
||||||
|
#### Scenario: 公共包 Init 函数
|
||||||
|
|
||||||
|
- **WHEN** 调用 `database.Init(cfg, logger)`
|
||||||
|
- **THEN** SHALL 根据 `cfg.Driver` 选择对应的 GORM 驱动打开连接
|
||||||
|
- **THEN** SHALL 执行对应方言的 goose 迁移
|
||||||
|
- **THEN** SHALL 配置连接池参数
|
||||||
|
- **THEN** SHALL 在 `driver=sqlite` 时执行 `PRAGMA journal_mode=WAL`
|
||||||
|
- **THEN** SHALL 在 `driver=mysql` 时跳过 SQLite 专有 PRAGMA
|
||||||
|
- **THEN** SHALL 返回 `*gorm.DB` 实例
|
||||||
|
|
||||||
|
#### Scenario: 公共包 Close 函数
|
||||||
|
|
||||||
|
- **WHEN** 调用 `database.Close(db)`
|
||||||
|
- **THEN** SHALL 获取底层 `sql.DB` 并关闭连接
|
||||||
|
|
||||||
|
#### Scenario: 迁移目录选择
|
||||||
|
|
||||||
|
- **WHEN** 执行迁移
|
||||||
|
- **THEN** SHALL 在 `driver=sqlite` 时使用 `migrations/sqlite/` 目录,goose dialect 为 `sqlite3`
|
||||||
|
- **THEN** SHALL 在 `driver=mysql` 时使用 `migrations/mysql/` 目录,goose dialect 为 `mysql`
|
||||||
|
|
||||||
|
### Requirement: MySQL 方言迁移文件
|
||||||
|
|
||||||
|
系统 SHALL 提供 MySQL 方言的初始迁移文件 `migrations/mysql/20260421000001_initial_schema.sql`。
|
||||||
|
|
||||||
|
#### Scenario: providers 表
|
||||||
|
|
||||||
|
- **WHEN** 执行 MySQL 初始迁移
|
||||||
|
- **THEN** SHALL 创建 `providers` 表,字段:`id VARCHAR(36) PRIMARY KEY`、`name VARCHAR(255) NOT NULL`、`api_key VARCHAR(255) NOT NULL`、`base_url VARCHAR(255) NOT NULL`、`protocol VARCHAR(50) DEFAULT 'openai'`、`enabled BOOLEAN DEFAULT TRUE`、`created_at DATETIME(3)`、`updated_at DATETIME(3)`
|
||||||
|
|
||||||
|
#### Scenario: models 表
|
||||||
|
|
||||||
|
- **WHEN** 执行 MySQL 初始迁移
|
||||||
|
- **THEN** SHALL 创建 `models` 表,字段:`id VARCHAR(36) PRIMARY KEY`、`provider_id VARCHAR(36) NOT NULL`、`model_name VARCHAR(255) NOT NULL`、`enabled BOOLEAN DEFAULT TRUE`、`created_at DATETIME(3)`
|
||||||
|
- **THEN** SHALL 创建 `UNIQUE(provider_id, model_name)` 约束
|
||||||
|
- **THEN** SHALL 创建 `FOREIGN KEY (provider_id) REFERENCES providers(id) ON DELETE CASCADE` 约束
|
||||||
|
- **THEN** SHALL 创建 `idx_models_provider_id` 和 `idx_models_model_name` 索引
|
||||||
|
|
||||||
|
#### Scenario: usage_stats 表
|
||||||
|
|
||||||
|
- **WHEN** 执行 MySQL 初始迁移
|
||||||
|
- **THEN** SHALL 创建 `usage_stats` 表,字段:`id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY`、`provider_id VARCHAR(36) NOT NULL`、`model_name VARCHAR(255) NOT NULL`、`request_count INT DEFAULT 0`、`date DATE NOT NULL`
|
||||||
|
- **THEN** SHALL 创建 `UNIQUE(provider_id, model_name, date)` 约束
|
||||||
|
- **THEN** SHALL 创建 `idx_usage_stats_provider_model_date` 复合索引
|
||||||
|
|
||||||
|
#### Scenario: Down 迁移
|
||||||
|
|
||||||
|
- **WHEN** 执行 MySQL down 迁移
|
||||||
|
- **THEN** SHALL 按正确顺序删除索引和表(usage_stats → models → providers)
|
||||||
104
openspec/specs/mysql-testing/spec.md
Normal file
104
openspec/specs/mysql-testing/spec.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# MySQL Testing
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
提供 MySQL 数据库专项测试能力,验证迁移正确性、外键约束、并发写入等数据库特定行为。
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
### Requirement: MySQL 测试环境可启动
|
||||||
|
|
||||||
|
系统 SHALL 提供 Docker Compose 配置以启动 MySQL 8.0 测试环境。
|
||||||
|
|
||||||
|
#### Scenario: 启动 MySQL 测试容器
|
||||||
|
- **WHEN** 执行 `make test-mysql-up`
|
||||||
|
- **THEN** 启动 MySQL 8.0 容器,端口 13306
|
||||||
|
- **AND** 创建数据库 `nex_test`
|
||||||
|
- **AND** 容器数据存储在内存盘(tmpfs)
|
||||||
|
|
||||||
|
#### Scenario: 销毁 MySQL 测试容器
|
||||||
|
- **WHEN** 执行 `make test-mysql-down`
|
||||||
|
- **THEN** 停止并删除容器
|
||||||
|
- **AND** 所有数据被销毁
|
||||||
|
|
||||||
|
### Requirement: MySQL 测试可通过 build tag 控制
|
||||||
|
|
||||||
|
MySQL 测试 SHALL 使用 `// +build mysql` build tag,默认不运行。
|
||||||
|
|
||||||
|
#### Scenario: 默认测试不包含 MySQL 测试
|
||||||
|
- **WHEN** 执行 `go test ./...`
|
||||||
|
- **THEN** 不运行 `tests/mysql/` 下的测试
|
||||||
|
|
||||||
|
#### Scenario: 启用 MySQL 测试
|
||||||
|
- **WHEN** 执行 `go test -tags=mysql ./tests/mysql/...`
|
||||||
|
- **THEN** 运行所有 MySQL 测试
|
||||||
|
|
||||||
|
### Requirement: MySQL 迁移正确执行
|
||||||
|
|
||||||
|
MySQL 测试 SHALL 验证迁移脚本在 MySQL 环境下正确执行。
|
||||||
|
|
||||||
|
#### Scenario: 迁移创建所有表
|
||||||
|
- **WHEN** 运行 MySQL 迁移
|
||||||
|
- **THEN** 创建 `providers`、`models`、`usage_stats` 表
|
||||||
|
- **AND** 字段类型符合 MySQL 迁移文件定义(VARCHAR、DATETIME(3)、BOOLEAN 等)
|
||||||
|
- **AND** 索引 `idx_models_provider_id`、`idx_models_model_name`、`idx_usage_stats_provider_model_date` 创建成功
|
||||||
|
|
||||||
|
#### Scenario: 迁移可重复执行
|
||||||
|
- **WHEN** 在已迁移的数据库上再次运行迁移
|
||||||
|
- **THEN** 不报错,数据库状态不变
|
||||||
|
|
||||||
|
### Requirement: MySQL 外键约束生效
|
||||||
|
|
||||||
|
MySQL 测试 SHALL 验证外键约束行为符合预期。
|
||||||
|
|
||||||
|
#### Scenario: 外键约束阻止无效引用
|
||||||
|
- **WHEN** 创建 model 时 `provider_id` 不存在
|
||||||
|
- **THEN** 操作失败,返回外键约束错误
|
||||||
|
|
||||||
|
#### Scenario: 级联删除生效
|
||||||
|
- **WHEN** 删除 provider
|
||||||
|
- **THEN** 该 provider 的所有 models 被级联删除
|
||||||
|
|
||||||
|
### Requirement: MySQL UNIQUE 约束生效
|
||||||
|
|
||||||
|
MySQL 测试 SHALL 验证 UNIQUE 约束行为符合预期。
|
||||||
|
|
||||||
|
#### Scenario: models 表 UNIQUE 约束
|
||||||
|
- **WHEN** 尝试创建相同 `(provider_id, model_name)` 组合的 model
|
||||||
|
- **THEN** 操作失败,返回唯一约束错误
|
||||||
|
|
||||||
|
#### Scenario: usage_stats 表 UNIQUE 约束
|
||||||
|
- **WHEN** 尝试创建相同 `(provider_id, model_name, date)` 组合的 usage_stats
|
||||||
|
- **THEN** 操作失败,返回唯一约束错误
|
||||||
|
|
||||||
|
### Requirement: MySQL 并发写入正确
|
||||||
|
|
||||||
|
MySQL 测试 SHALL 验证并发写入不丢失数据。
|
||||||
|
|
||||||
|
#### Scenario: 并发记录 usage_stats
|
||||||
|
- **WHEN** 10 个 goroutine 并发调用 `statsRepo.Record(providerID, modelName)`
|
||||||
|
- **THEN** 最终 `request_count` 等于 10
|
||||||
|
- **AND** 无数据丢失或重复
|
||||||
|
|
||||||
|
#### Scenario: 并发创建相同 provider
|
||||||
|
- **WHEN** 10 个 goroutine 并发创建相同 ID 的 provider
|
||||||
|
- **THEN** 仅 1 个成功,其他 9 个失败
|
||||||
|
|
||||||
|
#### Scenario: 并发创建相同 model
|
||||||
|
- **WHEN** 10 个 goroutine 并发创建相同 `(provider_id, model_name)` 的 model
|
||||||
|
- **THEN** 仅 1 个成功,其他 9 个失败
|
||||||
|
|
||||||
|
### Requirement: MySQL 测试命令完整
|
||||||
|
|
||||||
|
Makefile SHALL 提供完整的 MySQL 测试命令。
|
||||||
|
|
||||||
|
#### Scenario: 完整测试流程
|
||||||
|
- **WHEN** 执行 `make test-mysql`
|
||||||
|
- **THEN** 启动 Docker MySQL
|
||||||
|
- **AND** 等待 MySQL 就绪
|
||||||
|
- **AND** 运行所有 MySQL 测试
|
||||||
|
- **AND** 销毁容器
|
||||||
|
|
||||||
|
#### Scenario: 快速测试(容器已运行)
|
||||||
|
- **WHEN** 执行 `make test-mysql-quick`
|
||||||
|
- **THEN** 直接运行测试,不管理容器生命周期
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
- **WHEN** 应用启动
|
- **WHEN** 应用启动
|
||||||
- **THEN** SHALL 初始化 zap logger
|
- **THEN** SHALL 初始化 zap logger
|
||||||
- **THEN** SHALL 根据配置设置日志级别
|
- **THEN** SHALL 根据配置设置日志级别
|
||||||
- **THEN** SHALL 配置日志输出格式为 JSON
|
- **THEN** SHALL 配置日志输出格式为 JSON(文件)和 Console(stdout)
|
||||||
|
|
||||||
#### Scenario: 日志字段
|
#### Scenario: 日志字段
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@
|
|||||||
#### Scenario: 日志注入
|
#### Scenario: 日志注入
|
||||||
|
|
||||||
- **WHEN** 创建需要记录日志的组件
|
- **WHEN** 创建需要记录日志的组件
|
||||||
- **THEN** SHALL 通过构造函数注入 *zap.Logger
|
- **THEN** SHALL 通过构造函数注入 `*zap.Logger`
|
||||||
- **THEN** SHALL 允许 logger 参数为 nil,此时使用全局 logger zap.L()
|
- **THEN** 调用方 SHALL 必须传入有效的 logger
|
||||||
- **THEN** SHALL NOT 直接使用全局 logger zap.L()(除非在构造函数默认值中)
|
- **THEN** SHALL NOT 使用全局 logger `zap.L()`(测试代码除外)
|
||||||
|
|
||||||
### Requirement: 支持日志滚动
|
### Requirement: 支持日志滚动
|
||||||
|
|
||||||
@@ -76,7 +76,8 @@
|
|||||||
#### Scenario: 日志关联请求 ID
|
#### Scenario: 日志关联请求 ID
|
||||||
|
|
||||||
- **WHEN** 记录请求相关的日志
|
- **WHEN** 记录请求相关的日志
|
||||||
- **THEN** SHALL 自动包含请求 ID 字段
|
- **THEN** SHALL 从 Context 提取 request_id
|
||||||
|
- **THEN** SHALL 自动包含 request_id 字段
|
||||||
- **THEN** SHALL 支持通过请求 ID 检索日志
|
- **THEN** SHALL 支持通过请求 ID 检索日志
|
||||||
|
|
||||||
### Requirement: 记录请求日志
|
### Requirement: 记录请求日志
|
||||||
@@ -116,7 +117,7 @@
|
|||||||
|
|
||||||
- **WHEN** 配置为生产模式
|
- **WHEN** 配置为生产模式
|
||||||
- **THEN** SHALL 使用 info 级别
|
- **THEN** SHALL 使用 info 级别
|
||||||
- **THEN** SHALL 仅输出到文件
|
- **THEN** SHALL 输出到控制台和文件
|
||||||
|
|
||||||
### Requirement: 日志存储位置
|
### Requirement: 日志存储位置
|
||||||
|
|
||||||
@@ -141,12 +142,77 @@ ConversionEngine SHALL 通过依赖注入获取 logger。
|
|||||||
#### Scenario: ConversionEngine 构造函数
|
#### Scenario: ConversionEngine 构造函数
|
||||||
|
|
||||||
- **WHEN** 创建 ConversionEngine 实例
|
- **WHEN** 创建 ConversionEngine 实例
|
||||||
- **THEN** 构造函数 SHALL 接受 *zap.Logger 参数
|
- **THEN** 构造函数 SHALL 接受 `*zap.Logger` 参数
|
||||||
- **THEN** 参数为 nil 时 SHALL 使用 zap.L() 作为默认值
|
- **THEN** 调用方 SHALL 必须传入有效的 logger
|
||||||
- **THEN** SHALL 将 logger 存储在结构体字段中
|
- **THEN** SHALL 将 logger 存储在结构体字段中
|
||||||
|
|
||||||
#### Scenario: ConversionEngine 日志使用
|
#### Scenario: ConversionEngine 日志使用
|
||||||
|
|
||||||
- **WHEN** ConversionEngine 记录日志
|
- **WHEN** ConversionEngine 记录日志
|
||||||
- **THEN** SHALL 使用注入的 logger 字段
|
- **THEN** SHALL 使用注入的 logger 字段
|
||||||
- **THEN** SHALL NOT 直接调用 zap.L()
|
- **THEN** SHALL NOT 直接调用 `zap.L()`
|
||||||
|
|
||||||
|
### Requirement: 字段标准化
|
||||||
|
|
||||||
|
系统 SHALL 使用标准化字段定义。
|
||||||
|
|
||||||
|
#### Scenario: 标准字段常量
|
||||||
|
|
||||||
|
- **WHEN** 记录日志字段
|
||||||
|
- **THEN** SHALL 使用 `pkg/logger/field.go` 中定义的常量
|
||||||
|
- **THEN** 字段名 SHALL 包括:`request_id`、`provider_id`、`model_name`、`method`、`path`、`status`、`latency`
|
||||||
|
|
||||||
|
#### Scenario: 错误字段统一
|
||||||
|
|
||||||
|
- **WHEN** 记录错误日志
|
||||||
|
- **THEN** SHALL 使用 `zap.Error(err)`
|
||||||
|
- **THEN** SHALL NOT 使用 `zap.String("error", err.Error())`
|
||||||
|
|
||||||
|
#### Scenario: 字段构造函数
|
||||||
|
|
||||||
|
- **WHEN** 构造日志字段
|
||||||
|
- **THEN** SHALL 优先使用 `pkg/logger` 提供的辅助函数
|
||||||
|
- **THEN** 辅助函数 SHALL 返回 `zap.Field` 类型
|
||||||
|
|
||||||
|
### Requirement: 启动日志统一
|
||||||
|
|
||||||
|
系统 SHALL 在启动阶段使用结构化日志。
|
||||||
|
|
||||||
|
#### Scenario: 最小化 logger 初始化
|
||||||
|
|
||||||
|
- **WHEN** 应用启动时配置加载前
|
||||||
|
- **THEN** SHALL 初始化最小化 logger(仅 stdout,console 格式)
|
||||||
|
- **THEN** SHALL 支持记录启动错误
|
||||||
|
|
||||||
|
#### Scenario: Logger 升级
|
||||||
|
|
||||||
|
- **WHEN** 配置加载完成
|
||||||
|
- **THEN** SHALL 升级为完整 logger(文件 + stdout)
|
||||||
|
- **THEN** SHALL 应用配置的日志级别和轮转策略
|
||||||
|
|
||||||
|
#### Scenario: 配置摘要结构化
|
||||||
|
|
||||||
|
- **WHEN** 打印配置摘要
|
||||||
|
- **THEN** SHALL 使用结构化日志记录
|
||||||
|
- **THEN** SHALL NOT 使用 `fmt.Printf` 或 `fmt.Println`
|
||||||
|
|
||||||
|
### Requirement: 单行输出
|
||||||
|
|
||||||
|
系统 SHALL 保证所有日志单行输出。
|
||||||
|
|
||||||
|
#### Scenario: Console 单行
|
||||||
|
|
||||||
|
- **WHEN** 输出到 stdout
|
||||||
|
- **THEN** SHALL 单行输出
|
||||||
|
- **THEN** 字段之间 SHALL 使用空格分隔
|
||||||
|
|
||||||
|
#### Scenario: JSON 单行
|
||||||
|
|
||||||
|
- **WHEN** 输出到文件
|
||||||
|
- **THEN** SHALL 单行紧凑 JSON
|
||||||
|
- **THEN** SHALL NOT 使用美化缩进
|
||||||
|
|
||||||
|
#### Scenario: 多行数据保留
|
||||||
|
|
||||||
|
- **WHEN** 日志数据本身包含多行(堆栈跟踪、SQL 换行等)
|
||||||
|
- **THEN** SHALL 保留原始多行格式
|
||||||
|
|||||||
@@ -93,7 +93,21 @@
|
|||||||
- **WHEN** 同时处理多个并发请求
|
- **WHEN** 同时处理多个并发请求
|
||||||
- **THEN** 网关 SHALL 使用原子操作正确增加每个请求的计数
|
- **THEN** 网关 SHALL 使用原子操作正确增加每个请求的计数
|
||||||
- **THEN** 不 SHALL 因并发写入而丢失统计
|
- **THEN** 不 SHALL 因并发写入而丢失统计
|
||||||
- **THEN** SHALL 使用 StatsBuffer 的内存计数器
|
- **THEN** SHALL 使用 upsert 操作保证原子性
|
||||||
|
|
||||||
|
#### Scenario: 并发调用 Record 方法
|
||||||
|
|
||||||
|
- **WHEN** 多个 goroutine 并发调用 StatsRepository.Record
|
||||||
|
- **THEN** SHALL 使用 INSERT ... ON DUPLICATE KEY UPDATE (MySQL) 或 INSERT ... ON CONFLICT DO UPDATE (SQLite)
|
||||||
|
- **THEN** SHALL 保证所有并发调用的计数都被正确累加
|
||||||
|
- **THEN** 不 SHALL 因 UNIQUE 约束冲突而丢失数据
|
||||||
|
|
||||||
|
#### Scenario: 并发调用 BatchUpdate 方法
|
||||||
|
|
||||||
|
- **WHEN** 多个 goroutine 并发调用 StatsRepository.BatchUpdate
|
||||||
|
- **THEN** SHALL 使用 upsert 操作保证原子性
|
||||||
|
- **THEN** SHALL 正确累加所有 delta 值
|
||||||
|
- **THEN** 不 SHALL 因并发写入而丢失统计
|
||||||
|
|
||||||
### Requirement: 使用 service 层处理业务逻辑
|
### Requirement: 使用 service 层处理业务逻辑
|
||||||
|
|
||||||
@@ -125,14 +139,14 @@ Service SHALL 通过 StatsRepository 访问数据。
|
|||||||
|
|
||||||
- **WHEN** StatsBuffer 刷新统计
|
- **WHEN** StatsBuffer 刷新统计
|
||||||
- **THEN** SHALL 调用 StatsRepository.BatchUpdate
|
- **THEN** SHALL 调用 StatsRepository.BatchUpdate
|
||||||
- **THEN** SHALL 使用事务更新或创建统计记录
|
- **THEN** SHALL 使用 upsert 操作更新或创建统计记录
|
||||||
- **THEN** SHALL 支持增量更新(request_count + delta)
|
- **THEN** SHALL 支持增量更新(request_count + delta)
|
||||||
|
|
||||||
#### Scenario: 事务处理
|
#### Scenario: upsert 操作
|
||||||
|
|
||||||
- **WHEN** 记录统计
|
- **WHEN** 记录统计
|
||||||
- **THEN** SHALL 在 repository 层使用数据库事务
|
- **THEN** SHALL 在 repository 层使用 upsert 操作
|
||||||
- **THEN** SHALL 确保并发安全
|
- **THEN** SHALL 保证原子性和并发安全
|
||||||
|
|
||||||
### Requirement: 统计查询优化
|
### Requirement: 统计查询优化
|
||||||
|
|
||||||
@@ -168,11 +182,18 @@ StatsRepository SHALL 新增 BatchUpdate 方法支持批量增量更新。
|
|||||||
#### Scenario: BatchUpdate 更新现有记录
|
#### Scenario: BatchUpdate 更新现有记录
|
||||||
|
|
||||||
- **WHEN** 调用 BatchUpdate 且当日记录存在
|
- **WHEN** 调用 BatchUpdate 且当日记录存在
|
||||||
- **THEN** SHALL 使用事务更新 request_count = request_count + delta
|
- **THEN** SHALL 使用 upsert 操作更新 request_count = request_count + delta
|
||||||
|
- **THEN** SHALL 保证原子性,无竞态条件
|
||||||
- **THEN** SHALL 不创建新记录
|
- **THEN** SHALL 不创建新记录
|
||||||
|
|
||||||
#### Scenario: BatchUpdate 创建新记录
|
#### Scenario: BatchUpdate 创建新记录
|
||||||
|
|
||||||
- **WHEN** 调用 BatchUpdate 且当日记录不存在
|
- **WHEN** 调用 BatchUpdate 且当日记录不存在
|
||||||
- **THEN** SHALL 创建新记录,request_count = delta
|
- **THEN** SHALL 创建新记录,request_count = delta
|
||||||
- **THEN** SHALL 使用事务保证原子性
|
- **THEN** SHALL 使用 upsert 操作保证原子性
|
||||||
|
|
||||||
|
#### Scenario: BatchUpdate 并发安全
|
||||||
|
|
||||||
|
- **WHEN** 多个 BatchUpdate 调用同时执行
|
||||||
|
- **THEN** SHALL 保证所有 delta 都被正确累加
|
||||||
|
- **THEN** SHALL 不因并发冲突而丢失数据
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ if [ -f "${BUILD_DIR}/nex-mac-arm64" ]; then
|
|||||||
elif [ -f "${BUILD_DIR}/nex-mac-amd64" ]; then
|
elif [ -f "${BUILD_DIR}/nex-mac-amd64" ]; then
|
||||||
cp "${BUILD_DIR}/nex-mac-amd64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
|
cp "${BUILD_DIR}/nex-mac-amd64" "${BUILD_DIR}/${APP_NAME}.app/Contents/MacOS/nex"
|
||||||
else
|
else
|
||||||
echo "错误: 未找到 macOS 二进制文件,请先运行 make desktop-mac"
|
echo "错误: 未找到 macOS 二进制文件,请先运行 make desktop-build-mac"
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
354
scripts/init/init-dev-branch.py
Executable file
354
scripts/init/init-dev-branch.py
Executable file
@@ -0,0 +1,354 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""开发分支工作区初始化脚本
|
||||||
|
|
||||||
|
用于创建基于远端分支或新建的开发分支工作区。
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
def run_git(args: list[str]) -> subprocess.CompletedProcess:
|
||||||
|
"""执行 git 命令
|
||||||
|
|
||||||
|
Args:
|
||||||
|
args: git 命令参数列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
subprocess.CompletedProcess 对象
|
||||||
|
"""
|
||||||
|
return subprocess.run(
|
||||||
|
["git"] + args,
|
||||||
|
capture_output=True,
|
||||||
|
text=True,
|
||||||
|
check=False
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_root_dir() -> Path:
|
||||||
|
"""获取 git 仓库根目录
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 不在 git 仓库中
|
||||||
|
"""
|
||||||
|
result = run_git(["rev-parse", "--show-toplevel"])
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError("不在 git 仓库中")
|
||||||
|
return Path(result.stdout.strip())
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_remote() -> bool:
|
||||||
|
"""从远端获取最新分支信息
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否成功获取
|
||||||
|
"""
|
||||||
|
result = run_git(["fetch", "--quiet"])
|
||||||
|
if result.returncode != 0:
|
||||||
|
print("警告: 无法获取远端信息,继续使用本地数据")
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def get_all_remote_branches() -> list[str]:
|
||||||
|
"""获取所有远端分支列表
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
远端分支列表
|
||||||
|
"""
|
||||||
|
result = run_git(["branch", "-r"])
|
||||||
|
if result.returncode != 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [line.strip() for line in result.stdout.strip().split("\n") if line.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def get_remote_branches(branch_name: str) -> list[str]:
|
||||||
|
"""获取远端匹配的分支列表
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch_name: 分支名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
匹配的远端分支列表
|
||||||
|
"""
|
||||||
|
result = run_git(["branch", "-r"])
|
||||||
|
if result.returncode != 0:
|
||||||
|
return []
|
||||||
|
|
||||||
|
return [
|
||||||
|
line.strip()
|
||||||
|
for line in result.stdout.strip().split("\n")
|
||||||
|
if line.strip() and line.strip().endswith(f"/{branch_name}")
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def branch_exists_local(branch_name: str) -> bool:
|
||||||
|
"""检查本地分支是否存在
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch_name: 分支名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否存在
|
||||||
|
"""
|
||||||
|
result = run_git(["show-ref", "--verify", "--quiet", f"refs/heads/{branch_name}"])
|
||||||
|
return result.returncode == 0
|
||||||
|
|
||||||
|
|
||||||
|
def worktree_exists(worktree_path: Path) -> bool:
|
||||||
|
"""检查工作区是否存在
|
||||||
|
|
||||||
|
Args:
|
||||||
|
worktree_path: 工作区路径
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
是否存在
|
||||||
|
"""
|
||||||
|
result = run_git(["worktree", "list"])
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False
|
||||||
|
return str(worktree_path) in result.stdout
|
||||||
|
|
||||||
|
|
||||||
|
def extract_branch_name(remote_branch: str) -> str:
|
||||||
|
"""从远端分支名提取分支名
|
||||||
|
|
||||||
|
Args:
|
||||||
|
remote_branch: 远端分支名(如 origin/feature-123)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
分支名(如 feature-123)
|
||||||
|
"""
|
||||||
|
return remote_branch.split("/", 1)[1] if "/" in remote_branch else remote_branch
|
||||||
|
|
||||||
|
|
||||||
|
def validate_branch_creation(branch_name: str, worktree_path: Path) -> None:
|
||||||
|
"""验证是否可以创建分支和工作区
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch_name: 分支名称
|
||||||
|
worktree_path: 工作区路径
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 验证失败
|
||||||
|
"""
|
||||||
|
if worktree_path.exists():
|
||||||
|
raise RuntimeError(f"工作区已存在于 {worktree_path}")
|
||||||
|
|
||||||
|
if worktree_exists(worktree_path):
|
||||||
|
raise RuntimeError(f"工作区 '{branch_name}' 已存在")
|
||||||
|
|
||||||
|
if branch_exists_local(branch_name):
|
||||||
|
raise RuntimeError(f"本地分支 '{branch_name}' 已存在")
|
||||||
|
|
||||||
|
|
||||||
|
def select_from_list(items: list[str], prompt: str, allow_create: bool = False) -> str | None:
|
||||||
|
"""让用户从列表中选择
|
||||||
|
|
||||||
|
Args:
|
||||||
|
items: 选项列表
|
||||||
|
prompt: 提示信息
|
||||||
|
allow_create: 是否允许创建新分支
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
用户选择的项,或 None 表示创建新分支
|
||||||
|
"""
|
||||||
|
if not items:
|
||||||
|
return None
|
||||||
|
|
||||||
|
print(prompt)
|
||||||
|
for i, item in enumerate(items, 1):
|
||||||
|
print(f" {i}\t{item}")
|
||||||
|
|
||||||
|
if allow_create:
|
||||||
|
print(f" {len(items) + 1}\t创建新分支")
|
||||||
|
|
||||||
|
print()
|
||||||
|
|
||||||
|
max_choice = len(items) + 1 if allow_create else len(items)
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
selection = int(input(f"请选择 (1-{max_choice}): "))
|
||||||
|
if 1 <= selection <= len(items):
|
||||||
|
selected = items[selection - 1]
|
||||||
|
print(f"已选择: {selected}")
|
||||||
|
return selected
|
||||||
|
if allow_create and selection == len(items) + 1:
|
||||||
|
return None
|
||||||
|
print(f"错误: 请输入 1-{max_choice} 之间的数字")
|
||||||
|
except ValueError:
|
||||||
|
print("错误: 请输入有效的数字")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n操作已取消")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def input_branch_name() -> str:
|
||||||
|
"""让用户输入分支名称
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
分支名称
|
||||||
|
"""
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
name = input("请输入新分支名称: ").strip()
|
||||||
|
if name:
|
||||||
|
return name
|
||||||
|
print("错误: 分支名称不能为空")
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
print("\n操作已取消")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def create_worktree(branch_name: str, worktree_path: Path, base_branch: str | None = None) -> None:
|
||||||
|
"""创建工作区
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch_name: 新分支名称
|
||||||
|
worktree_path: 工作区路径
|
||||||
|
base_branch: 基础分支(可选)
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
RuntimeError: 创建失败
|
||||||
|
"""
|
||||||
|
args = ["worktree", "add", "-b", branch_name, str(worktree_path)]
|
||||||
|
if base_branch:
|
||||||
|
args.append(base_branch)
|
||||||
|
|
||||||
|
result = run_git(args)
|
||||||
|
if result.returncode != 0:
|
||||||
|
raise RuntimeError(f"创建工作区失败: {result.stderr.strip()}")
|
||||||
|
|
||||||
|
|
||||||
|
def create_worktree_with_validation(
|
||||||
|
branch_name: str,
|
||||||
|
worktree_path: Path,
|
||||||
|
base_branch: str | None = None
|
||||||
|
) -> None:
|
||||||
|
"""验证并创建工作区
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch_name: 新分支名称
|
||||||
|
worktree_path: 工作区路径
|
||||||
|
base_branch: 基础分支(可选)
|
||||||
|
"""
|
||||||
|
validate_branch_creation(branch_name, worktree_path)
|
||||||
|
create_worktree(branch_name, worktree_path, base_branch)
|
||||||
|
print(f"工作区已创建于 {worktree_path}")
|
||||||
|
|
||||||
|
|
||||||
|
def handle_with_branch_name(branch_name: str, worktrees_dir: Path) -> None:
|
||||||
|
"""处理有分支名参数的情况
|
||||||
|
|
||||||
|
Args:
|
||||||
|
branch_name: 分支名称
|
||||||
|
worktrees_dir: 工作区目录
|
||||||
|
"""
|
||||||
|
worktree_path = worktrees_dir / branch_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_branch_creation(branch_name, worktree_path)
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
remote_branches = get_remote_branches(branch_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
if remote_branches:
|
||||||
|
selected_branch = select_from_list(
|
||||||
|
remote_branches,
|
||||||
|
"找到远端分支:",
|
||||||
|
allow_create=True
|
||||||
|
)
|
||||||
|
if selected_branch:
|
||||||
|
create_worktree(branch_name, worktree_path, selected_branch)
|
||||||
|
else:
|
||||||
|
create_worktree(branch_name, worktree_path)
|
||||||
|
else:
|
||||||
|
print("未找到远端分支,创建新分支")
|
||||||
|
create_worktree(branch_name, worktree_path)
|
||||||
|
|
||||||
|
print(f"工作区已创建于 {worktree_path}")
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def handle_without_branch_name(worktrees_dir: Path) -> None:
|
||||||
|
"""处理无分支名参数的情况
|
||||||
|
|
||||||
|
Args:
|
||||||
|
worktrees_dir: 工作区目录
|
||||||
|
"""
|
||||||
|
remote_branches = get_all_remote_branches()
|
||||||
|
|
||||||
|
if not remote_branches:
|
||||||
|
print("未找到远端分支")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
selected_branch = select_from_list(
|
||||||
|
remote_branches,
|
||||||
|
"远端分支列表:",
|
||||||
|
allow_create=True
|
||||||
|
)
|
||||||
|
|
||||||
|
if selected_branch:
|
||||||
|
branch_name = extract_branch_name(selected_branch)
|
||||||
|
worktree_path = worktrees_dir / branch_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
create_worktree_with_validation(branch_name, worktree_path, selected_branch)
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
else:
|
||||||
|
branch_name = input_branch_name()
|
||||||
|
worktree_path = worktrees_dir / branch_name
|
||||||
|
|
||||||
|
try:
|
||||||
|
create_worktree_with_validation(branch_name, worktree_path)
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="创建开发分支工作区",
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog="""
|
||||||
|
示例:
|
||||||
|
%(prog)s # 列出远端分支供选择
|
||||||
|
%(prog)s feature-123 # 创建 feature-123 工作区
|
||||||
|
%(prog)s bugfix-456 # 创建 bugfix-456 工作区
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
parser.add_argument("branch_name", nargs="?", help="分支名称(可选)")
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
try:
|
||||||
|
root_dir = get_root_dir()
|
||||||
|
except RuntimeError as e:
|
||||||
|
print(f"错误: {e}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
worktrees_dir = root_dir / ".worktrees"
|
||||||
|
worktrees_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
print("正在从远端获取最新分支信息...")
|
||||||
|
fetch_remote()
|
||||||
|
|
||||||
|
if args.branch_name:
|
||||||
|
handle_with_branch_name(args.branch_name, worktrees_dir)
|
||||||
|
else:
|
||||||
|
handle_without_branch_name(worktrees_dir)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [ $# -eq 0 ]; then
|
|
||||||
echo "Usage: $0 <branch-name>"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
BRANCH_NAME="$1"
|
|
||||||
ROOT_DIR=$(git rev-parse --show-toplevel)
|
|
||||||
WORKTREES_DIR="$ROOT_DIR/.worktrees"
|
|
||||||
|
|
||||||
mkdir -p "$WORKTREES_DIR"
|
|
||||||
|
|
||||||
git worktree add -b "$BRANCH_NAME" "$WORKTREES_DIR/$BRANCH_NAME"
|
|
||||||
|
|
||||||
echo "Worktree created at $WORKTREES_DIR/$BRANCH_NAME"
|
|
||||||
Reference in New Issue
Block a user