commit 8540f76fcf0135c6ea56693af7749e3888f70b1f
Author: Sake <1246665453@qq.com>
Date: Thu Jan 15 19:32:33 2026 +0800
init
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3a89eb0
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,71 @@
+# If you prefer the allow list template instead of the deny list, see community template:
+# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
+#
+# Binaries for programs and plugins
+*.exe
+*.exe~
+*.dll
+*.so
+*.dylib
+
+# Test binary, built with `go test -c`
+*.test
+
+# Output of the go coverage tool, specifically when used with LiteIDE
+*.out
+
+# Dependency directories (remove the comment below to include it)
+# vendor/
+
+# Go workspace file
+go.work
+go.work.sum
+
+# env file
+.env
+*.log
+*.db
+build/
+.vscode/
+__debug_*
+www/
+*.db
+tables/
+*.tar
+*.zip
+.idea/
+data/
+logs/
+build/
+__debug_bin*
+tables/
+*.pprof
+*.test
+snap/*
+
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+uploads/*
+dish*
\ No newline at end of file
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..fb10a77
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,18 @@
+FROM alpine:latest
+
+ARG TARGETARCH
+
+ENV TZ=Asia/Shanghai
+
+RUN apk --no-cache add ca-certificates \
+ tzdata
+
+WORKDIR /app
+
+COPY ./build/linux_${TARGETARCH} /app/
+
+LABEL Name=GoDDD Version=0.0.1
+
+EXPOSE 8080
+
+CMD [ "./bin" ]
\ No newline at end of file
diff --git a/LICENSE.txt b/LICENSE.txt
new file mode 100644
index 0000000..56d26be
--- /dev/null
+++ b/LICENSE.txt
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2024 Jérémy LAMBERT (SystemGlitch)
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..5b7c673
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,271 @@
+# Makefile 使用文档
+# https://www.gnu.org/software/make/manual/html_node/index.html
+
+# include .envrc
+SHELL = /bin/bash
+
+# ==================================================================================== #
+# HELPERS
+# ==================================================================================== #
+
+## help: print this help message
+help:
+ @echo 'Usage:'
+ @sed -n 's/^##//p' ${MAKEFILE_LIST} | column -t -s ':' | sed -e 's/^/ /'
+
+.PHONY: confirm
+confirm:
+ @echo -n 'Are you sure? [y/n] ' && read ans && [ $${ans:-N} = y ]
+
+.PHONY: title
+title:
+ @echo -e "\033[34m$(content)\033[0m"
+
+.PHONY: rename
+## rename: clone 后的模板,需要更新 module 名
+rename:
+ @if [ -z "$(name)" ]; then \
+ echo "错误: 请提供 name 参数,例如: make rename name=github.com/name/project"; \
+ exit 1; \
+ fi
+ @rm -rf domain/* pkg/*
+ @echo "正在替换模块名为: $(name)"
+ @find . -type f -name "*.go" -exec sed -i.bak 's|github\.com/ixugo/goddd/internal|$(name)/internal|g' {} \;
+ @sed -i.bak 's|github\.com/ixugo/goddd|$(name)|g' go.mod
+ @find . -name "*.bak" -delete
+ @go mod tidy
+ @echo -e "\n模块名替换完成"
+
+# ==================================================================================== #
+# DEVELOPMENT
+# ==================================================================================== #
+
+## init: 安装开发环境
+init:
+ go install github.com/google/wire/cmd/wire@latest
+ go install github.com/divan/expvarmon@latest
+ go install github.com/rakyll/hey@latest
+ go install mvdan.cc/gofumpt@latest
+
+## wire: 生成依赖注入代码
+wire:
+ go mod tidy
+ go get github.com/google/wire/cmd/wire@latest
+ go generate ./...
+ go mod tidy
+
+## expva/http: 监听网络请求指标
+expva/http:
+ expvarmon --ports=":9999" -i 1s -vars="version,request,requests,responses,goroutines,errors,panics,mem:memstats.Alloc"
+
+## expva/db: 监听数据库连接指标
+expva/db:
+ expvarmon --ports=":9999" -i 5s -vars="databse.MaxOpenConnections,databse.OpenConnections,database.InUse,databse.Idle"
+
+# 发起 100 次请求,每次并发 50
+# hey -n 100 -c 50 http://localhost:9999/healthcheck
+
+
+# ==================================================================================== #
+# QUALITY CONTROL
+# ==================================================================================== #
+
+## audit: 检查代码依赖/格式化/测试
+.PHONY: audit
+audit:
+ @make title content='Formatting code...'
+ gofumpt -l -w .
+ @make title content='Vetting code...'
+ go vet ./...
+ @make title content='Running tests...'
+ go test -race -vet=off ./...
+
+## vendor: 整理并下载依赖
+.PHONY: vendor
+vendor:
+ @make title content='Tidying and verifying module dependencies...'
+ go mod tidy && go mod verify
+ @make title content='Vendoring dependencies...'
+ go mod vendor
+
+# ==================================================================================== #
+# VERSION
+# ==================================================================================== #
+
+# 版本号规则说明
+# 1. 版本号使用 Git tag,格式为 v1.0.0。
+# 2. 如果当前提交没有 tag,找到最近的 tag,计算从该 tag 到当前提交的提交次数。例如,最近的 tag 为 v1.0.1,当前提交距离它有 10 次提交,则版本号为 v1.0.11(v1.0.1 + 10 次提交)。
+# 3. 如果没有任何 tag,则默认版本号为 v0.0.0,后续提交次数作为版本号的次版本号。
+
+# Get the current module name
+MODULE_NAME := $(shell pwd | awk -F "/" '{print $$NF}')
+# Get the latest commit hash and date
+HASH_AND_DATE := $(shell git log -n1 --pretty=format:"%h-%cd" --date=format:%y%m%d | awk '{print $1}')
+BRANCH := $(shell git rev-parse --abbrev-ref HEAD)
+
+# 如果想仅支持注释标签,可以去掉 --tags,否则会包含轻量标签
+RECENT_TAG := $(shell git describe --tags --abbrev=0 2>&1 | grep -v -e "fatal" -e "Try" || echo "v0.0.0")
+
+ifeq ($(RECENT_TAG),v0.0.0)
+ COMMITS := $(shell git rev-list --count HEAD)
+else
+ COMMITS := $(shell git log --first-parent --format='%ae' $(RECENT_TAG)..$(BRANCH) | wc -l)
+ COMMITS := $(shell echo $(COMMITS) | sed 's/ //g')
+endif
+
+# 从版本字符串中提取主版本号、次版本号和修订号
+GIT_VERSION_MAJOR := $(shell echo $(RECENT_TAG) | cut -d. -f1 | sed 's/v//')
+GIT_VERSION_MINOR := $(shell echo $(RECENT_TAG) | cut -d. -f2)
+GIT_VERSION_PATCH := $(shell echo $(RECENT_TAG) | cut -d. -f3)
+
+# windows 系统 git bash 没有 bc
+# FINAL_PATCH := $(shell echo $(GIT_VERSION_PATCH) + $(COMMITS) | bc)
+FINAL_PATCH := $(shell echo '$(GIT_VERSION_PATCH) $(COMMITS)' | awk '{print $$1 + $$2}')
+VERSION := v$(GIT_VERSION_MAJOR).$(GIT_VERSION_MINOR).$(FINAL_PATCH)
+
+# test:
+# @echo ">>>${RECENT_TAG}"
+
+## info: 查看构建版本相关信息
+.PHONY: info
+info:
+ @echo "dir: $(MODULE_NAME)"
+ @echo "version: $(VERSION)"
+ @echo "branch $(BRANCH)"
+ @echo "hash: $(HASH_AND_DATE)"
+ @echo "support $$(go tool dist list | grep amd64 | grep linux)"
+
+
+# ==================================================================================== #
+# BUILD
+# ==================================================================================== #
+
+BUILD_DIR_ROOT := ./build
+GOOS = $(shell go env GOOS)
+GOARCH = $(shell go env GOARCH)
+IMAGE_NAME := $(MODULE_NAME):latest
+
+## build/clean: 清理构建缓存目录
+.PHONY: build/clean
+build/clean:
+ @rm -rf $(BUILD_DIR_ROOT)/*
+
+pack/linux/arm64:
+ $(eval GOARCH := arm64)
+ $(eval GOOS := linux)
+ $(eval CGO_ENABLED := 1)
+ $(eval CC := aarch64-linux-gnu-gcc)
+ $(eval GOARM := 7)
+ $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH) CGO_ENABLED=$(CGO_ENABLED) CC=$(CC) GOARM=$(GOARM)
+ $(eval dir := $(BUILD_DIR_ROOT)/linux_arm64)
+ @cp -r deploy/easyvqd/* $(dir)
+ @mv $(dir)/bin $(dir)/easyvqd
+ @upx $(dir)/easyvqd
+ @make zip/arm64
+
+pack/linux:
+ $(eval GOARCH := amd64)
+ $(eval GOOS := linux)
+ $(eval CGO_ENABLED := 0)
+ $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
+ $(eval dir := $(BUILD_DIR_ROOT)/linux_amd64)
+ @cp -r deploy/easyvqd/* $(dir)
+ @mv $(dir)/bin $(dir)/easyvqd
+ @upx $(dir)/easyvqd
+ @make zip/linux
+
+pack/windows:
+ $(eval GOARCH := amd64)
+ $(eval GOOS := windows)
+ $(eval CGO_ENABLED := 0)
+ $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
+ $(eval dir := $(BUILD_DIR_ROOT)/windows_amd64)
+ @cp -r deploy/easyvqd/* $(dir)
+ @mv $(dir)/bin $(dir)/easyvqd.exe
+ @upx $(dir)/easyvqd.exe
+ @make zip/windows
+
+## zip/linux: 压缩 Linux 构建产物
+.PHONY: zip/linux
+zip/linux:
+ $(eval BUILD_DIR := $(BUILD_DIR_ROOT)/linux_amd64)
+ @cd build/linux_$(GOARCH) && zip -r ../easyvqd-linux-$(GOARCH)-$(VERSION)-$(HASH_AND_DATE).zip .
+
+## zip/windows: 压缩 Linux 构建产物
+.PHONY: zip/windows
+zip/windows:
+ $(eval BUILD_DIR := $(BUILD_DIR_ROOT)/windows_amd64)
+ @cd build/windows_$(GOARCH) && zip -r ../easyvqd-windows-$(GOARCH)-$(VERSION)-$(HASH_AND_DATE).zip .
+
+## build/local: 构建本地应用
+.PHONY: build/local
+build/local:
+ $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
+ @echo 'Building $(VERSION) $(dir)...'
+ @rm -rf $(dir)
+ @GOOS=$(GOOS) GOARCH=$(GOARCH) go build \
+ -trimpath \
+ -ldflags="-s -w \
+ -X main.buildVersion=$(VERSION) \
+ -X main.gitBranch=$(BRANCH_NAME) \
+ -X main.gitHash=$(HASH_AND_DATE) \
+ -X main.buildTimeAt=$(shell date +%s) \
+ -X main.release=true \
+ " -o=$(dir)/bin ./main.go
+ @echo '>>> OK'
+
+## build/linux: 构建 linux 应用
+.PHONY: build/linux
+BUILD_LINUX_AMD64_DIR := ./build/linux_amd64
+build/linux:
+ $(eval GOARCH := amd64)
+ $(eval GOOS := linux)
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
+
+## build/windows: 构建 windows 应用
+.PHONY: build/windows
+BUILD_WINDOWS_AMD64_DIR := ./build/windows_amd64
+build/windows:
+ $(eval GOARCH := amd64)
+ $(eval GOOS := windows)
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
+
+docker/build:
+ @docker build --force-rm=true --platform linux/amd64 -t $(IMAGE_NAME) .
+
+docker/save:
+ @docker save -o $(MODULE_NAME)_$(VERSION).tar $(IMAGE_NAME)
+
+docker/push:
+ @docker push $(IMAGE_NAME)
+
+docker/deploy: build/clean
+ $(eval GOARCH := amd64)
+ $(eval GOOS := linux)
+ $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
+ @upx $(dir)/bin
+
+ $(eval GOARCH := arm64)
+ $(eval GOOS := linux)
+ $(eval dir := $(BUILD_DIR_ROOT)/$(GOOS)_$(GOARCH))
+ @make build/local GOOS=$(GOOS) GOARCH=$(GOARCH)
+ @upx $(dir)/bin
+
+ @docker build --force-rm=true --platform linux/amd64,linux/arm64 -t $(IMAGE_NAME) --push .
+
+
+# ==================================================================================== #
+# PRODUCTION
+# ==================================================================================== #
+
+PRODUCTION_HOST = remoteHost
+
+## release/push: 发布产品到服务器,仅上传文件
+# 中小项目可以引入 CI/CD,也可以通过命令快速发布到测试服务器上。
+release/push:
+ @scp build/linux_amd64/bin $(PRODUCTION_HOST):/home/app/$(MODULE_NAME)
+ @echo "push Successed"
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..1f37d06
--- /dev/null
+++ b/README.md
@@ -0,0 +1,535 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+[English](./README.md) | [简体中文](./README_zh.md)
+
+# Enterprise REST API Template
+
+This is a complete CRUD solution focused on REST API.
+
+The goal of GoDDD is to:
+
++ Provide a clean architecture suitable for small and medium-sized projects.
++ Provide a modular structure for quickly starting a project, focusing on business development.
++ Simplify projects, making development more efficient and enjoyable.
+
+If you think the above description fits your needs, then let's get started quickly.
+
+Supports [code generation](github.com/ixugo/godddx).
+
+Supports [event bus/transaction messages](github.com/ixugo/nsqite).
+
+## Quick start
+
+1. Golang version >= 1.23.0
+2. `git clone github.com/ixugo/goddd`
+3. `cd goddd && go build -o goddd ./cmd/server && ./goddd`
+4. Open a new terminal and access `curl http://localhost:8080/health`
+
+5. Modify the module package name:
+ `make rename name=github.com/name/project`
+ Replace `name` with your actual module name.
+
+
+
+
+## References
+
+[Google API Design Guide](https://google-cloud.gitbook.io/api-design-guide)
+
+Best Practices for This Project: https://github.com/gowvp/gb28181
+
+
+## Directory Structure
+
+```bash
+.
+├── cmd Executable program
+│ └── server
+├── configs Configuration files
+├── docs Design/User documentation
+├── internal Private business
+│ ├── conf Configuration models
+│ ├── core Business domain
+│ │ └── version Actual business
+│ │ └── store
+│ │ └── versiondb Database operations
+│ ├── data Database initialization
+│ └── web
+│ └── api RESTful API
+└── pkg Dependencies
+```
+
+## Project Description
+
+1. Components strongly relied upon by the program will trigger a panic on error, so that issues are resolved as quickly as possible.
+
+2. The core directory represents the business domain, containing domain models and domain business functions.
+
+3. The store is the database operation module, dependent on models with dependency inversion towards the core, avoiding the need to define models at each layer.
+
+4. Input/output parameters in the API layer may directly depend on models defined in the core layer, with input and output models distinguished by appending `Input/Output` to the model names.
+
+
+## Request Parameter Wrapping
+
+This project uses GIN as the web framework, and the route functions need to implement `gin.HandlerFunc`. The first issue encountered when implementing API functions is binding parameters. Almost every function involves deserialization, and the function heads are cluttered with `ctx.ShouldBindJSON` and similar code.
+
+To follow the DRY (Don't Repeat Yourself) design principle, we reduce repetitive code to improve maintainability and reusability. The project wraps `web.WrapH`, which returns a `gin.HandlerFunc`. The parameters for `web.WrapH` are similar to gRPC, with a signature like `func(ctx *gin.Context, in *struct{}) (*Output, error)`.
+
+`WrapH` internally recognizes POST/PUT/DELETE/PATCH requests and binds the Request Body, while GET requests bind Request URL parameters.
+
+The second parameter of the input must be a pointer, and `*struct{}` is used when no parameters need to be bound. When defining the structure, especially note that the struct tags should be `json` or `form`. More details are available in the GIN framework's parameter binding documentation.
+
++ `json`: Can bind request body parameters.
++ `form`: Can bind query parameters.
+
+The first parameter of the return value is the actual response body content, and it is recommended to avoid using `any`. The type can be either a value or a pointer, providing more flexibility.
+
+When parameters exist in multiple places, such as route parameters, query parameters, and request body parameters, you can implement a new `web.WrapH2` or directly implement `gin.HandlerFunc`.
+
+Here are two code examples:
+
+
+```go
+func findUser(ctx *gin.Context) {
+ var in findUserInput
+ if err := ctx.ShouldBindQuery(&in);err!=nil {
+ ctx.JSON(...)
+ return
+ }
+ out,err := serviceFunc(in)
+ // ....
+}
+```
+
+```go
+func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
+ return serviceFunc(in)
+}
+```
+
+## Response Parameter Wrapping
+
+Clearly defining the response type can make the code easier to understand. The goal is to improve code readability and maintainability by paying attention to more details.
+
+The web.WrapH wrapper defaults to returning a response with the application/json content type.
+
+During development, new colleagues may forget the return statement when implementing gin.HandlerFunc. Using web.WrapH ensures that the return statement is not omitted.
+
+Here are two code examples:
+
+```go
+func findUsers(ctx *gin.Context) {
+ // Maybe out is obtained from the business layer
+ // At this point, you need to find the response body inside the function
+ out, err := serviceFunc()
+ if err != nil {
+ ctx.JSON(...)
+ return
+ }
+ ctx.JSON(out)
+}
+```
+
+```go
+func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
+ return serviceFunc(in)
+}
+```
+
+## Error Handling
+
+From the above code, we can see that errors are directly returned. But doesn't this expose the underlying error information to the user? And what about the HTTP status code for errors?
+
+In fact, `web.Warn` does some additional work. For example, when there is an error during binding, it can pinpoint the specific error cause: Is the type wrong? Which property is incorrect? For example, when responding, we can extract information from the `err` and return the corresponding HTTP status code. Let's take a closer look at error handling.
+
+`pkg/web` is an HTTP-related handling package, which includes middleware, response handling, error handling, authentication, logging, rate limiting, metrics, performance analysis, input validation, and more.
+
+We define a custom `Error` type, where `reason` represents the error cause. Some third-party APIs also use a `Code`.
+
+When designing the project, we considered that status codes might be hard to interpret, for example, error `10020`—what does that error mean? Therefore, we defined `reason`, which should describe the error cause in a concise, camel-case English format. If you just want to use the status code, then use the HTTP StatusCode.
+
+`msg` should be an error description in the developer's native language, while `reason` is used internally by the program, and `msg` is for user-friendly messaging. `details` is an extension of the error, providing additional information for developers. It can describe solutions to the error, provide documentation, give more detailed error information, or even expose lower-level errors.
+
+In front-end and back-end separated projects, when the front-end encounters an error, they often need to ask the back-end what happened. Through `details`, the front-end can reduce the number of inquiries.
+
+In the `web.WrapH` wrapper, errors are actually handled by calling `web.Fail(err)`. This method determines which HTTP status code should be returned based on the `reason`. Developers can implement more HTTP status code extensions in the `pkg/web/error.go` file through the `HTTPCode()` function. By default, three status codes are provided: 200, 400, and 401.
+
+`details` should only be visible in development mode. You can set the release mode using `web.SetRelease()`, in which case `details` will not be included in the HTTP response body.
+
+
+Functions exported from the core layer or errors returned from the API layer should return errors of type `reason.Error`.
+
+In the wrapped `web.WrapH`, errors are correctly logged and returned to the front-end.
+
+```go
+func findUser(in *Input) (*Output, error) {
+ // Database operation error
+ if err != nil {
+ return nil, reason.ErrDB.SetMsg() // The response type is a DB layer error, and the Msg function can modify the user-friendly message
+ }
+ // Business logic error
+ if err != nil {
+ return nil, reason.ErrServer.Withf("err[%s] ....", err) // Withf can write details to provide more hints to the developer
+ }
+}
+```
+
+
+## Makefile
+
+For Windows systems, please use the Git Bash terminal to run the Makefile instead of the default cmd/powershell terminal, as issues may arise.
+
+Use `make` or `make help` to get more help.
+
+When writing a Makefile, add comments above each command in the format `## : ` for readability, with available parameters provided in the Makefile. The goal is to make `make help` output more informative.
+
+Some default operations are provided in the Makefile to assist with rapid development.
+
+`make confirm` confirms the next step.
+
+`make title content=Title` highlights a title in the output.
+
+`make info` fetches build version information.
+
+**Versioning Rules in the Makefile**
+
+1. Git tags are used for versioning, in the format v1.0.0.
+
+2. If the current commit lacks a tag, the closest tag is found, and the number of commits from that tag is calculated. For example, if the latest tag is v1.0.1, and there have been 10 commits since, the version number becomes v1.0.11 (v1.0.1 + 10 commits).
+
+3. If there are no tags, the default version is v0.0.0, with the minor version incremented based on the number of commits.
+
+
+## How to use the library?
+
+### hook.UseCache Temporary cache
+
+old
+
+```go
+cache := make(map[string]string)
+for i := range 10 {
+ v, ok := cache[i]
+ if ok {
+ // Business processing
+ continue
+ }
+ v,err := fn()
+ if err == nil {
+ cache[v.ID] = v
+ }
+ // Business processing
+}
+```
+
+new
+
+```go
+ cacheFn := hook.UseCache(fn)
+ for i := range 10 {
+ v,_,err := cacheFn(i)
+ if err == nil {
+ // Business processing
+ }
+ }
+```
+
+### hook.UseTiming Log the cost of function computation
+
+old
+
+```go
+ now := time.Now()
+ // Business logic is intermingled with time calculation
+ if sub :=time.Since(now); sub > time.Second {
+ slog.Error("func name", "cost", cost)
+ }else {
+ slog.Debug("func name", "cost", cost)
+ }
+```
+
+new
+
+```go
+ cost := hook.UseTiming(time.Second)
+ defer cost()
+
+ // Business processing
+```
+
+### hook.UseTimer Timer with flexible intervals
+
+old
+
+```go
+func scheduleTask() {
+ for {
+ // Business processing
+ processTask()
+
+ // Complex time calculation logic mixed with business logic
+ now := time.Now()
+ nextRun := time.Date(now.Year(), now.Month(), now.Day()+1, 2, 0, 0, 0, now.Location()) // Run at 2 AM tomorrow
+ if nextRun.Before(now) {
+ nextRun = nextRun.Add(24 * time.Hour)
+ }
+ time.Sleep(nextRun.Sub(now))
+ }
+}
+```
+
+new
+
+```go
+func scheduleTask(ctx context.Context) {
+ hook.UseTimer(ctx, processTask, func() time.Duration {
+ return hook.NextTimeTomorrow(2, 0, 0) // Run at 2 AM every day
+ })
+}
+
+// Or for immediate execution with different intervals
+func scheduleTaskWithFirstRun(ctx context.Context) {
+ nextTime := hook.NextTimeWithFirst(
+ time.Second, // Run immediately (after 1 second)
+ func() time.Duration {
+ return 10 * time.Minute // Then run every 10 minutes
+ },
+ )
+ hook.UseTimer(ctx, processTask, nextTime)
+}
+```
+
+**Check out the source code in pkg/hook for more hooks.**
+
+
+
+## Quick Start
+
+Example business logic:
+
+Assume we want to implement version management. The CRUD steps are as follows:
+
+Under "internal" - "core," create the "version" directory, then create `model.go` and define the domain model representing the database table structure.
+
+Create `core.go` and add the following content:
+
+```go
+package version
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Storer Interface for dependency inversion in data persistence.
+type Storer interface {
+ First(*Version) error
+ Add(*Version) error
+}
+
+// Core Business object
+type Core struct {
+ Storer Storer
+}
+
+// NewCore Creates a business object.
+func NewCore(store Storer) *Core {
+ return &Core{
+ Storer: store,
+ }
+}
+
+// IsAutoMigrate Checks if table migration is required
+// Compares the hard-coded database table version with the stored version.
+func (c *Core) IsAutoMigrate(currentVer, remark string) bool {
+ var ver Version
+ if err := c.Storer.First(&ver); err != nil {
+ isMigrate := true
+ c.IsMigrate = &isMigrate
+ return isMigrate
+ }
+ isMigrate := compareVersionFunc(currentVer, ver.Version, func(a, b string) bool {
+ return a > b
+ })
+ c.IsMigrate = &isMigrate
+ return isMigrate
+}
+
+func compareVersionFunc(a, b string, f func(a, b string) bool) bool {
+ s1 := versionToStr(a)
+ s2 := versionToStr(b)
+ if len(s1) != len(s2) {
+ return true
+ }
+ return f(s1, s2)
+}
+
+func versionToStr(str string) string {
+ var result strings.Builder
+ arr := strings.Split(str, ".")
+ for _, item := range arr {
+ if idx := strings.Index(item, "-"); idx != -1 {
+ item = item[0:idx]
+ }
+ result.WriteString(fmt.Sprintf("%03s", item))
+ }
+ return result.String()
+}
+```
+
+Under "store/versiondb," create the `db.go` file with the following content:
+
+```go
+type DB struct {
+ db *gorm.DB
+}
+
+func NewDB(db *gorm.DB) DB {
+ return DB{db: db}
+}
+
+// AutoMigrate Table migration.
+func (d DB) AutoMigrate(ok bool) DB {
+ if !ok {
+ return d
+ }
+ if err := d.db.AutoMigrate(
+ new(version.Version),
+ ); err != nil {
+ panic(err)
+ }
+ return d
+}
+
+func (d DB) First(v *version.Version) error {
+ return d.db.Order("id DESC").First(v).Error
+}
+
+func (d DB) Add(v *version.Version) error {
+ return d.db.Create(v).Error
+}
+```
+
+In the API layer, inject dependencies by adding a function in `web/api/provider.go` to inject the business object into Usecase:
+
+```go
+var ProviderSet = wire.NewSet(
+ wire.Struct(new(Usecase), "*"),
+ NewHTTPHandler,
+ NewVersion,
+)
+
+func NewVersion(db *gorm.DB) *version.Core {
+ vdb := versiondb.NewDB(db)
+ core := version.NewCore(vdb)
+ isOK := core.IsAutoMigrate(dbVersion, dbRemark)
+ vdb.AutoMigrate(isOK)
+ if isOK {
+ slog.Info("Updating database schema")
+ if err := core.RecordVersion(dbVersion, dbRemark); err != nil {
+ slog.Error("RecordVersion", "err", err)
+ }
+ }
+ return core
+}
+```
+
+Create a new `version.go` file in the API layer with the following content:
+
+```go
+// VersionAPI Namespace for version business functions.
+type VersionAPI struct {
+ ver *version.Core
+}
+
+func NewVersionAPI(ver *version.Core) VersionAPI {
+ return VersionAPI{ver: ver}
+}
+// registerVersion Registers business interface with the router.
+func registerVersion(r gin.IRouter, verAPI VersionAPI, handler ...gin.HandlerFunc) {
+ ver := r.Group("/version", handler...)
+ ver.GET("", web.WrapH(verAPI.getVersion))
+}
+
+func (v VersionAPI) getVersion(_ *gin.Context, _ *struct{}) (any, error) {
+ return gin.H{"msg": "test"}, nil
+}
+```
+
+## FAQ
+
+> Why not define models in each layer separately?
+
+This is a trade-off between development efficiency and decoupling, balancing code readability and efficiency.
+
+> Where should API layer parameter models and table mapping models be defined?
+
+Understanding the dependency relationships between layers is crucial. The API directly depends on the core, while the DB layer is inverted to depend on the core. Thus, domain models are defined in the core, and input/output parameter models can also be defined in the core. If they are unused in the core, defining them in the API layer is fine too.
+
+> Why does the API layer directly depend on the core layer rather than an interface?
+
+Interfaces aim to decouple, but in practice, it is more common to replace the API layer than the core layer.
+
+The API only retrieves parameters and returns response parameters, doing the minimum necessary to facilitate the transition from HTTP to GRPC.
+
+Design for the future, but program for the present. Increasing development efficiency now allows for a better approach in the future when needed.
+
+> Why is the DB layer inverted to depend on the core?
+
+Data persistence is not independent; it serves the business. That is, persistence serves the
+
+ business and depends on it.
+
+Through dependency inversion, other databases, such as Redis cache, can be inserted between business operations.
+
+> Why suffix input/output models with `Input/Output`?
+
+Convention is preferable to configuration. Some projects use `Request/Response` as suffixes to standardize parameter names.
+
+Of course, output parameters can also serve as input, and you can define an alias or use them directly.
+
+Frequently, we want clarity on what we're doing and why. This FAQ aims to offer some insight.
+
+> How to write business plugins for GoDDD?
+
+```go
+// RegisterVersion Some general business functions are depended upon by other business functions, such as table version control, dictionary, verification code, scheduled tasks, user management, etc.
+// Conventionally, write functions in the format Register, injecting three parameters: gin router, namespace, and middleware.
+// Refer to project code for specifics.
+func RegisterVersion(r gin.IRouter, verAPI VersionAPI, handler ...gin.HandlerFunc) {
+ ver := r.Group("/version", handler...)
+ ver.GET("", web.WrapH(verAPI.getVersion))
+}
+```
+
+## Table Migration
+
+Executing table migration on every program start is too slow.
+
+Therefore, migration control is implemented through the version table, so migration only occurs when the database table version is outdated. Modify the `dbVersion` in api/db.go to control the version number.
+
+
+## Custom Configuration Directory
+
+The default configuration directory is `configs`, located in the same directory as the executable. You can also specify other configuration directories.
+
+`./bin -conf ./configs`
+
+## Main Project Dependencies
+
++ gin
++ gorm
++ slog / zap
++ wire
\ No newline at end of file
diff --git a/README_zh.md b/README_zh.md
new file mode 100644
index 0000000..1e68193
--- /dev/null
+++ b/README_zh.md
@@ -0,0 +1,594 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+[English](./README.md) | [简体中文](./README_zh.md)
+
+# 企业 REST API 模板
+
+这是一个专注于 REST API 的完整 CURD 解决方案。
+
+GoDDD 目标是:
+
++ 整洁架构,适用于中小型项目
++ 提供积木套装,快速开始项目,专注于业务开发
++ 令项目更简单,令研发心情更好
+
+如果你觉得以上描述符合你的需求,那就快速开始吧。
+
+支持[代码自动生成](github.com/ixugo/godddx)
+
+支持[事件总线/事务消息](github.com/ixugo/nsqite)
+
+## 设计说明
+
+传统 MVC 单体架构,随着业务扩展,团队越难以有效开发,新入职的同事也很难去了解这个臃肿的单体。
+
+模块化单体架构具有单体架构的许多优点和缺点,也具有微服务架构的大量优点少数缺点。
+
+将完整业务拆分成多个领域模块,例如 用户领域 / 银行领域 / 商品领域,每个领域都有各自一套完善的
+
++ API(接口)
++ Core(业务)
++ Store(缓存/持久化存储)
+
+不同的开发人员或团队,可以独立地处理这些领域模块,降低开发新功能而导致的冲突混乱。相比微服务而言,这样拆分模块代码,更小更简洁更易于测试。
+
+当程序超出领域模块规模后,团队能够在需要时轻松地将领域模块提取到微服务中。
+
+
+## 快速开始
+
+1. Golang 版本 >= 1.23.0
+2. `git clone github.com/ixugo/goddd`
+3. `cd goddd && go build -o goddd ./cmd/server && ./goddd`
+4. 新开一个终端访问 `curl http://localhost:8080/health`
+
+5. 修改模块包名
+`make rename name=github.com/name/project`
+将 name 替换成实际的模块名
+
+
+
+## 引用文章
+
+[Google API Design Guide](https://google-cloud.gitbook.io/api-design-guide)
+
+此项目的最佳实践: https://github.com/gowvp/gb28181
+
+## 目录说明
+
+
+```bash
+.
+├── main.go 主函数入口
+├── cmd 更多可执行程序
+├── configs 配置文件
+├── docs 设计文档/用户文档
+├── domain 提供一些通用模型
+│ ├── token token 过期与延迟,权限管理
+│ ├── version 数据库版本控制,避免每次启动执行 gorm 迁移
+│ └── uniqueid 全局唯一 id 生成器
+├── internal 私有业务
+│ ├── conf 配置模型
+│ ├── core 业务领域
+│ │ └── version 实际业务
+│ │ └── store
+│ │ └── versiondb 数据库操作
+│ ├── data 数据库初始化
+│ └── web
+│ └── api RESTful API
+└── pkg 依赖库
+```
+
+
+## 项目说明
+
+1. 程序启动强依赖的组件,发生异常时主动 panic,尽快崩溃尽快解决错误。
+
+2. core 为业务领域,包含领域模型,领域业务功能
+
+3. store 为数据库操作模块,需要依赖模型,此处依赖反转 core,避免每一层都定义模型。
+
+4. api 层的入参/出参,可以正向依赖 core 层定义模型,参数模型以 `Input/Output` 来简单区分入参出数。
+
+
+## 请求入参封装
+
+本项目使用 GIN 作为 web 处理框架,路由函数需要实现 `gin.HandlerFunc`,在实现 API 层函数时,遇到的第一个问题是绑定参数,几乎每个函数都会涉及到反序列化,函数开头都充斥了 `ctx.ShouldBindJSON` 之类的代码。
+
+根据 DRY(Don't Repeat Yourself)设计原则,通过减少重复代码来提高代码的可维护性和可重用性。该项目封装了 `web.WrapH` 其返回 `gin.HandlerFunc`,`web.WrapH` 的参数类似 GRPC,`func(ctx *gin.Context, in *struct{}) (*Output, error)`。
+
+WrapH 内部识别 POST/PUT/DELETE/PATCH 请求则绑定 Request Body,Get 请求则绑定 Request URL params。
+
+入参第二个参数类型必须是指针,使用 `*struct{}` 表示没有参数,不需要绑定。在定义结构体时,尤其要注意结构体的 tag 应该是 `json` 或者 `form`,更多细节参考 GIN 框架参数绑定。
+
++ `json` 可绑定 request body 参数
++ `form` 可绑定 params 参数
+
+返回值第一个参数是具体的 response body 内容,建议避免使用 any,其类型即可以是值,也可以是指针,赋予了更多灵活性。
+
+当参数在多个位置时,即路由参数/查询参数/请求体参数同时存在,可以实现新的 web.WrapH2 或直接实现 `gin.HandlerFunc`。
+
+以下是两种代码的示例:
+
+```go
+func findUser(ctx *gin.Context) {
+ var in findUserInput
+ if err := ctx.ShouldBindQuery(&in);err!=nil {
+ ctx.JSON(...)
+ return
+ }
+ out,err := serviceFunc(in)
+ // ....
+}
+```
+
+```go
+func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
+ return serviceFunc(in)
+}
+```
+
+## 响应出参封装
+
+明确的定义出参类型,可以使代码更容易读懂,我希望通过更多细节提升代码的可读性,可维护性。
+
+`web.Warh` 的封装默认是响应 application/json 类型。
+
+在开发过程中,新同事实现 `gin.HandlerFunc` 时更容易遗忘 `return` 语句。使用 `web.WrapH` 能确保不遗落 `return`。
+
+以下是两种代码的示例
+
+```go
+func findUsers(ctx *gin.Context) {
+ // 可能 out 是从业务层获取的
+ // 此时想知道 response body 需要往函数内部找
+ out,err := serviceFunc()
+ if err != nil {
+ ctx.JSON(...)
+ return
+ }
+ ctx.JSON(out)
+}
+```
+
+```go
+func findUsers(ctx *gin.Context, in *Input) (*Output, error) {
+ return serviceFunc(in)
+}
+```
+
+## 错误处理
+
+通过上面的代码了解到,错误是直接 return 的,难倒不担心底层的错误信息暴露给用户吗? 还有错误的 http statusCode 又是多少呢?
+
+其实在 `web.Warn` 中还做了一些事情,比如在绑定过程中出错,可以定位到具体的错误原因,是类型不对? 错在哪个属性上? 比如响应的时候,通过 err 提取出信息,返回对应的 HTTP 状态码,接下来详细介绍错误处理。
+
+`pkg/web` 是 HTTP 相关的处理包,包含中间件,响应,错误处理,鉴权,日志,限流,指标,性能分析,入参校验等等。
+
+我们自定义一个 Error 类型, `reason` 是错误原因,有些第三方 API 也会用 Code。
+
+该项目在设计的时候,考虑到状态码不易读,比如错误 `10020`,请问是什么错误? 所以定义了 `reason`,应该用大驼峰英语简略描述错误原因。那如果就是想用状态码表示呢? 请用 HTTP StatusCode。
+
+msg 应当是开发者母语的错误描述,`reason` 用于程序内部判定,`msg` 用于友好提示给用户。`details` 是错误的扩展,提供给开发者,可以描述错误的解决方案,提供文档,错误的更细节详情,甚至暴露更底层的错误信息。
+
+通常在前后端分离项目中,前端遇到一些错误,都需要询问后端发生了什么情况,通过 `details` 前端可以减少更多提问。
+
+在 `web.WrapH` 的封装中,错误实际是调用的 `web.Fail(err)`,此方法会判断 `reason` 应该返回怎样的 http statusCode,开发者可以在 `pkg/web/error.go` 中 `HTTPCode()` 函数实现更多 http statusCode 扩展,默认提供了 200/400/401 三种状态码。
+
+details 应该仅开发模式可见,`web.SetRelease()` 可以设置为生产发布模式,此时 details 将不会写入 http response body。
+
+
+core 层导出的函数或 API 层返回的错误,应该返回 reason.Error 类型的错误。
+
+在封装的 web.WrapH 中,会正确记录错误到日志并返回给前端。
+
+```go
+func findUser(in *Input) (*Output,error){
+ // 数据库操作发生错误
+ if err != nil {
+ return nil, reason.ErrDB.SetMsg() // 错误的 respon 类型是 db 层错误,Msg 函数可以更改给用户的友好提示
+ }
+ // 业务发生错误
+ if err != nil {
+ return nil, reason.ErrServer.Withf("err[%s] ....",err) // Withf 可以写入 details 给开发者更多提示
+ }
+}
+```
+
+
+## Makefile
+
+Windows 系统使用 makefile 时,请使用 git bash 终端,不要使用系统默认的 cmd/powershell 终端,否则可能会出现异常情况。
+
+执行 `make` 或 `make help` 来获取更多帮助
+
+在编写 makefile 时,应主动在命令上面增加注释,以 `## <命令>: <描述>` 格式书写,具体参数 Makefile 文件已有命令。其目的是 `make help` 时提供更多信息。
+
+makefile 中提供了一些默认的操作便于快速编写
+
+`make confirm` 用于确认下一步
+
+`make title content=标题` 用于重点突出输出标题
+
+`make info` 获取构建版本相关信息
+
+**makefile 构建的版本号规则说明**
+
+1. 版本号使用 Git tag,格式为 v1.0.0。
+
+2. 如果当前提交没有 tag,找到最近的 tag,计算从该 tag 到当前提交的提交次数。例如,最近的 tag 为 v1.0.1,当前提交距离它有 10 次提交,则版本号为 v1.0.11(v1.0.1 + 10 次提交)。
+
+3. 如果没有任何 tag,则默认版本号为 v0.0.0,后续提交次数作为版本号的次版本号。
+
+## 库如何使用?
+
+### hook.UseCache 临时缓存
+
+old
+
+```go
+cache := make(map[string]string)
+for i := range 10 {
+ v, ok := cache[i]
+ if ok {
+ // 业务处理
+ continue
+ }
+ v,err := fn()
+ if err == nil {
+ cache[v.ID] = v
+ }
+ // 业务处理
+}
+```
+
+new
+
+```go
+ cacheFn := hook.UseCache(fn)
+ for i := range 10 {
+ v,_,err := cacheFn(i)
+ if err == nil {
+ // 业务处理
+ }
+ }
+```
+
+### hook.UseTiming 计算函数花销写日志
+
+old
+
+```go
+ now := time.Now()
+ // 业务处理夹杂在时间计算中
+ if sub :=time.Since(now); sub > time.Second {
+ slog.Error("函数名", "cost", cost)
+ }else {
+ slog.Debug("函数名", "cost", cost)
+ }
+```
+
+new
+
+```go
+ cost := hook.UseTiming(time.Second)
+ defer cost()
+
+ // 业务处理
+```
+
+### hook.UseMemoryUsage 计算函数内存花销写日志
+
+old
+
+```go
+ var m1, m2 runtime.MemStats
+ runtime.GC()
+ runtime.ReadMemStats(&m1)
+ // 业务处理夹杂在时间计算中
+ runtime.ReadMemStats(&m2)
+ memUsed := m2.Alloc - m1.Alloc
+ slog.Info("函数内存占用: ", "KB", float32(memUsed)/1024)
+```
+
+new
+```go
+ cost := hook.UseMemoryUsage(time.Second)
+ defer cost()
+
+ // 业务处理
+
+```
+
+### hook.FileMD5 计算文件的 md5
+
+避免将文件全部读取到内存,按照 8k 缓存分块计算文件的 md5
+
+### hook.UseTimer 灵活间隔的定时器
+
+old
+
+```go
+func scheduleTask() {
+ for {
+ // 业务处理
+ processTask()
+
+ // 复杂的时间计算逻辑夹杂在业务中
+ now := time.Now()
+ nextRun := time.Date(now.Year(), now.Month(), now.Day()+1, 2, 0, 0, 0, now.Location()) // 明天凌晨2点执行
+ if nextRun.Before(now) {
+ nextRun = nextRun.Add(24 * time.Hour)
+ }
+ time.Sleep(nextRun.Sub(now))
+ }
+}
+```
+
+new
+
+```go
+func scheduleTask(ctx context.Context) {
+ hook.UseTimer(ctx, processTask, func() time.Duration {
+ return hook.NextTimeTomorrow(2, 0, 0) // 每天凌晨2点执行
+ })
+}
+
+// 或者立即执行一次,然后按不同间隔执行
+func scheduleTaskWithFirstRun(ctx context.Context) {
+ nextTime := hook.NextTimeWithFirst(
+ time.Second, // 立即执行(1秒后)
+ func() time.Duration {
+ return 10 * time.Minute // 之后每10分钟执行一次
+ },
+ )
+ hook.UseTimer(ctx, processTask, nextTime)
+}
+```
+
+**更多 hook 直接看 pkg/hook 源码吧**
+
+
+
+
+## 快速开始
+
+业务说明:
+
+假设我们要做一个版本管理的业务,curd 步骤如下:
+
+在 「internal」-「core」 创建 「version」 目录,创建「model.go」写入领域模型,该模型为数据库表结构映射。
+
+创建「core.go」 写入如下内容
+
+```go
+package version
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Storer 依赖反转的数据持久化接口
+type Storer interface {
+ First(*Version) error
+ Add(*Version) error
+}
+
+// Core 业务对象
+type Core struct {
+ Storer Storer
+}
+
+// NewCore 创建业务对象
+func NewCore(store Storer) *Core {
+ return &Core{
+ Storer: store,
+ }
+}
+
+// IsAutoMigrate 是否需要进行表迁移
+// 判断硬编码在代码中的数据库表版本号,与数据库存储的版本号做对比
+func (c *Core) IsAutoMigrate(currentVer, remark string) bool {
+ var ver Version
+ if err := c.Storer.First(&ver); err != nil {
+ isMigrate := true
+ c.IsMigrate = &isMigrate
+ return isMigrate
+ }
+ isMigrate := compareVersionFunc(currentVer, ver.Version, func(a, b string) bool {
+ return a > b
+ })
+ c.IsMigrate = &isMigrate
+ return isMigrate
+}
+
+func compareVersionFunc(a, b string, f func(a, b string) bool) bool {
+ s1 := versionToStr(a)
+ s2 := versionToStr(b)
+ if len(s1) != len(s2) {
+ return true
+ }
+ return f(s1, s2)
+}
+
+func versionToStr(str string) string {
+ var result strings.Builder
+ arr := strings.Split(str, ".")
+ for _, item := range arr {
+ if idx := strings.Index(item, "-"); idx != -1 {
+ item = item[0:idx]
+ }
+ result.WriteString(fmt.Sprintf("%03s", item))
+ }
+ return result.String()
+}
+```
+
+创建 「store/versiondb」 目录,创建「db.go」 文件写入
+
+```go
+type DB struct {
+ db *gorm.DB
+}
+
+func NewDB(db *gorm.DB) DB {
+ return DB{db: db}
+}
+
+// AutoMigrate 表迁移
+func (d DB) AutoMigrate(ok bool) DB {
+ if !ok {
+ return d
+ }
+ if err := d.db.AutoMigrate(
+ new(version.Version),
+ ); err != nil {
+ panic(err)
+ }
+ return d
+}
+
+func (d DB) First(v *version.Version) error {
+ return d.db.Order("id DESC").First(v).Error
+}
+
+func (d DB) Add(v *version.Version) error {
+ return d.db.Create(v).Error
+}
+```
+
+在 API 层做依赖注入,对 「web/api/provider.go」 写入函数,往 Usecase 中注入业务对象
+
+```go
+var ProviderSet = wire.NewSet(
+ wire.Struct(new(Usecase), "*"),
+ NewHTTPHandler,
+ NewVersion,
+)
+
+func NewVersion(db *gorm.DB) *version.Core {
+ vdb := versiondb.NewDB(db)
+ core := version.NewCore(vdb)
+ isOK := core.IsAutoMigrate(dbVersion, dbRemark)
+ vdb.AutoMigrate(isOK)
+ if isOK {
+ slog.Info("更新数据库表结构")
+ if err := core.RecordVersion(dbVersion, dbRemark); err != nil {
+ slog.Error("RecordVersion", "err", err)
+ }
+ }
+ // 其它组件可以调用此变量,判断是否需要表迁移
+ orm.SetEnabledAutoMigrate(isOK)
+ return core
+}
+```
+
+在 API 层新建「version.go」文件,写入
+
+```go
+// version 业务函数命名空间
+type VersionAPI struct {
+ ver *version.Core
+}
+
+func NewVersionAPI(ver *version.Core) VersionAPI {
+ return VersionAPI{ver: ver}
+}
+// registerVersion 向路由注册业务接口
+func registerVersion(r gin.IRouter, verAPI VersionAPI, handler ...gin.HandlerFunc) {
+ ver := r.Group("/version", handler...)
+ ver.GET("", web.WrapH(verAPI.getVersion))
+}
+
+func (v VersionAPI) getVersion(_ *gin.Context, _ *struct{}) (any, error) {
+ return gin.H{"msg": "test"}, nil
+}
+```
+
+
+## 常见问题
+
+> 为什么不在每一层分别定义模型?
+
+开发效率与解耦的取舍,在代码通俗易懂和效率之间取的平衡。
+
+> 那 api 层参数模型,表映射模型到底应该定义在哪里?
+
+要清楚各层之间的依赖关系,api 直接依赖 core,db 依赖反转 core。故而领域模型定义在 core 中,api 的入参和出参也可以定义在 core,当然 core 层用不上的结构体,定义在 API 层也无妨。
+
+> 为什么 api 层直接依赖 core 层,而不是依赖接口?
+
+接口的目的是为了解耦,在实际开发过程中,更多是替换 api 层,而不是替换 core 层。
+
+API 只做参数获取,返回响应参数,只做最少的事情,方便从 HTTP 快速过度的 GRPC。
+
+面向未来设计,面向当下编程,先提高搬砖效率,等未来需要的那天会有更好的方式重构。
+
+> 为什么 db 依赖反转 core?
+
+数据持久化不是独立的,它为业务而服务。即持久化服务于业务,依赖于业务。
+
+通过依赖反转,业务可以在中间穿插 redis cache 等其它 db 。
+
+> 为什么入参/出参模型以 Input/Output 单词结尾
+
+约定大于配置,类似有些项目以 Request/Response 单词结尾,只是为了有一个统一的,大家都明确的参数。
+
+当然,有可能出参也是入参,你可以定义别名,也可以直接使用。
+
+很多时候,我们都想明确自己在做什么,为什么这样做,这个「常见问题」希望能提供一点解惑思路。
+
+> 如何为 goddd 编写业务插件?
+
+```go
+// RegisterVersion 有一些通用的业务,它们被其它业务依赖,属于业务的基层模块,例如表版本控制,字典,验证码,定时任务,用户管理等等。
+// 约定以 Register 方式编写函数,注入 gin 路由,命名空间,中间件三个参数。
+// 具体可以参考项目代码
+func RegisterVersion(r gin.IRouter, verAPI VersionAPI, handler ...gin.HandlerFunc) {
+ ver := r.Group("/version", handler...)
+ ver.GET("", web.WrapH(verAPI.getVersion))
+}
+```
+
+## 表迁移
+
+每次程序启动都执行一遍,太慢了。
+
+所以通过 version 表来控制,是否要进行表迁移操作。
+
+当发现数据库表版本已经是最新时,即不执行。通过修改 api/db.go 文件中 dbVersion 控制版本号。
+
+## 自定义配置目录
+
+默认配置目录为可执行文件同目录下的 configs,也可以指定其它配置目录
+
+`./bin -conf ./configs`
+
+
+## 项目主要依赖
+
++ gin
++ gorm
++ slog / zap
++ wire
+
+## 你也许还关心
+
+[github.com/ixugo/amap 高德地图 API](https://github.com/ixugo/amap)
+[github.com/ixugo/netpulse 免费API,查询IP信息](https://github.com/ixugo/netpulse)
+[github.com/ixugo/bytepool 带引用计数的内存池](https://github.com/ixugo/bytepool)
diff --git a/changelog b/changelog
new file mode 100644
index 0000000..fa130d9
--- /dev/null
+++ b/changelog
@@ -0,0 +1,2 @@
+v1.0.0 (2025-11-10)
+版本初始化
\ No newline at end of file
diff --git a/configs/config.toml b/configs/config.toml
new file mode 100644
index 0000000..3d5255f
--- /dev/null
+++ b/configs/config.toml
@@ -0,0 +1,124 @@
+[Server]
+ # 对外提供的服务,建议由 nginx 代理
+ [Server.HTTP]
+ # http 端口
+ Port = 8089
+ # 请求超时时间
+ Timeout = '1m0s'
+ # jwt 秘钥,空串时,每次启动程序将随机赋值
+ JwtSecret = 'ZsLfiuBfYQOL6UHG1I4hfcea9tRLbM9i'
+
+ [Server.HTTP.PProf]
+ # 是否启用 pprof, 建议设置为 true
+ Enabled = true
+ # 访问白名单
+ AccessIps = ['::1', '127.0.0.1']
+
+[Data]
+ # 数据库支持 sqlite 和 postgres 两种,使用 sqlite 时 dsn 应当填写文件存储路径
+ [Data.Database]
+ Dsn = './configs/data.db'
+ MaxIdleConns = 1
+ MaxOpenConns = 1
+ ConnMaxLifetime = '6h0m0s'
+ SlowThreshold = '200ms'
+
+[Plugin]
+ # http 地址
+ HttpAPI = 'http://127.0.0.1:10000'
+ # 通信端口
+ GrpcPort = 50051
+ # 是否开启
+ AllDebug = false
+
+[VqdConfig]
+ # 数据保存天数
+ SaveDay = 5
+ # 连续分析帧数(2-64), 默认为10, 最大为 64
+ FrmNum = 10
+ # 是否使用深度学习版本, 默认使用深度学习版本
+ IsDeepLearn = false
+
+[VqdLgtDark]
+ # 默认 0.4, 取值范围: 0~1, 建议范围: 0.2~0.6
+ DarkThr = 0.4
+ # 默认 0.1, 取值范围: 0~1, 建议范围: 0.1~0.5
+ LgtThr = 0.1
+ # 默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9
+ LgtDarkAbnNumRatio = 0.5
+
+[VqdBlue]
+ # 默认为 0.6, 取值范围: 0~1, 建议范围 0.4~0.9
+ BlueThr = 0.6
+ # 默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9
+ BlueAbnNumRatio = 0.5
+
+[VqdClarity]
+ # 默认为0.4, 取值范围: 0~1, 建议范围: 0.3~0.99
+ ClarityThr = 0.4
+ # 默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9
+ ClarityAbnNumRatio = 0.5
+
+[VqdShark]
+ # 默认为 0.2, 取值范围: 0~1, 建议范围: 0.1~0.8
+ SharkThr = 0.2
+ # 默认为0.2, 取值范围: 0~1, 建议范围: 0.1~0.6
+ SharkAbnNumRatio = 0.2
+
+[VqdFreeze]
+ # 默认 0.4, 取值范围: 0~1, 建议范围: 0.2~0.6
+ FreezeThr = 0.999
+ # 默认为0.99, 取值范围: 0.8~1, 建议范围: 0.95~1
+ FreezeAbnNumRatio = 0.99
+
+[VqdColor]
+ # 默认为0.18, 取值范围: 0~1, 建议范围: 0.1~0.5
+ ColorThr = 0.18
+ # 默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9
+ ColorAbnNumRatio = 0.5
+
+[VqdOcclusion]
+ # 默认为0.1, 取值范围: 0~1, 建议范围: 0.05~0.5
+ OcclusionThr = 0.1
+ # 默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9
+ OcclusionAbnNumRatio = 0.5
+
+[VqdNoise]
+ # 默认为 0.3, 取值范围: 0~1, 建议范围: 0.2~0.8
+ NoiseThr = 0.3
+ # 默认为0.6, 取值范围: 0~1, 建议范围: 0.3~0.9
+ NoiseAbnNumRatio = 0.6
+
+[VqdContrast]
+ # 默认为 0.2, 取值范围: 0~1, 建议范围: 0.1~0.3
+ CtraLowThr = 0.2
+ # 默认为 0.8, 取值范围: 0~1, 建议范围: 0.7~0.9
+ CtraHighThr = 0.8
+ # 默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9
+ CtraAbnNumRatio = 0.5
+
+[VqdMosaic]
+ # 默认为 0.1 取值范围: 0~1, 建议范围: 0.1~0.9
+ MosaicThr = 0.1
+ # 默认为0.5,取值范围: 0~1, 建议范围: 0.3
+ MosaicAbnNumRatio = 0.5
+
+[VqdFlower]
+ # 默认为 0.3 取值范围: 0~1, 建议范围: 0.1~0.9
+ FlowerThr = 0.1
+ # 默认为0.6, 取值范围: 0~1, 建议范围: 0.3
+ FlowerAbnNumRatio = 0.5
+ # 默认为 0.3 取值范围: 0~1, 建议范围: 0.1~0.9
+ MosaicThr = 0.1
+
+[Log]
+ # 日志存储目录,不能使用特殊符号
+ Dir = './logs'
+ # 记录级别 debug/info/warn/error
+ Level = 'debug'
+ # 保留日志多久,超过时间自动删除
+ MaxAge = '744h0m0s'
+ # 多久时间,分割一个新的日志文件
+ RotationTime = '8h0m0s'
+ # 多大文件,分割一个新的日志文件(MB)
+ RotationSize = 50
diff --git a/deploy/easyvqd/1.png b/deploy/easyvqd/1.png
new file mode 100644
index 0000000..14ab8dd
Binary files /dev/null and b/deploy/easyvqd/1.png differ
diff --git a/deploy/easyvqd/EasyVQD.html b/deploy/easyvqd/EasyVQD.html
new file mode 100644
index 0000000..7f601b4
--- /dev/null
+++ b/deploy/easyvqd/EasyVQD.html
@@ -0,0 +1,38 @@
+
+
+
+
+
+
+ EasyVQD
+
+
+
+
+ EasyVQD核心视频诊断分析能力
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/deploy/easyvqd/EasyVQD.ico b/deploy/easyvqd/EasyVQD.ico
new file mode 100644
index 0000000..85a1228
Binary files /dev/null and b/deploy/easyvqd/EasyVQD.ico differ
diff --git a/deploy/easyvqd/package.json b/deploy/easyvqd/package.json
new file mode 100644
index 0000000..22fe071
--- /dev/null
+++ b/deploy/easyvqd/package.json
@@ -0,0 +1,25 @@
+{
+ "name": "easyvqd",
+ "author": "TSINGSEE",
+ "auto_update": true,
+ "description": "将音频文件推送到GB设备",
+ "display_name": "EasyVQD",
+ "pid": 0,
+ "web_server": true,
+ "args": [
+
+ ],
+ "enabled": false,
+ "engines": {
+ "easygbs": "v0.0.1"
+ },
+ "features": [
+ ],
+ "homepage": "EasyVQD.html",
+ "icon": "EasyVQD.ico",
+ "updated_at": "2025-12-26",
+ "version": "v0.0.2",
+ "website": "https://www.tsingsee.com",
+ "activation": "service",
+ "status": ""
+}
\ No newline at end of file
diff --git a/deploy/ffmpeg/ffmpeg_amd64 b/deploy/ffmpeg/ffmpeg_amd64
new file mode 100644
index 0000000..67dcec7
Binary files /dev/null and b/deploy/ffmpeg/ffmpeg_amd64 differ
diff --git a/deploy/ffmpeg/ffmpeg_arm64 b/deploy/ffmpeg/ffmpeg_arm64
new file mode 100644
index 0000000..ec23c29
Binary files /dev/null and b/deploy/ffmpeg/ffmpeg_arm64 differ
diff --git a/docs/1_introduce.md b/docs/1_introduce.md
new file mode 100644
index 0000000..07b1b74
--- /dev/null
+++ b/docs/1_introduce.md
@@ -0,0 +1,72 @@
+# GoDDD:企业级 REST API 开发模板
+
+## 项目简介
+
+[GoDDD](https://github.com/ixugo/goddd) 是一个专注于 REST API 开发的企业级模板,旨在为 Go 开发者提供完整的 CURD 解决方案。它采用领域驱动设计(DDD)理念,通过模块化单体架构,让开发者能够快速开始项目,专注于业务开发。
+
+## 为什么选择 GoDDD?
+
+作为一名 Go 开发者,你是否遇到过这些问题:
+
+- 项目结构混乱,随着业务增长变得越来越难以维护
+- 新成员加入后需要很长时间才能理解项目
+- 团队协作时经常出现代码冲突
+- 需要重复编写大量相似的 CRUD 代码
+- 错误处理方式不统一,日志记录混乱
+- 项目启动时需要配置大量基础设施
+
+GoDDD 正是为了解决这些问题而设计的。它提供了一个清晰的项目结构和完整的开发工具链,包括:
+
+- 模块化的项目结构,让代码组织更清晰
+- 统一的错误处理和日志记录
+- 自动化的代码生成工具 [godddx](https://github.com/ixugo/godddx)
+
+## 技术架构
+
+### 架构设计理念
+
+在传统的 MVC 单体架构中,随着业务规模的增长,项目会变得越来越臃肿,导致团队开发效率降低,新成员也难以快速理解整个系统。
+
+GoDDD 采用模块化单体架构,这种架构既保留了单体架构的简单性,又吸收了微服务架构的模块化优势。通过将业务拆分为多个独立的领域模块(如用户领域、银行领域、商品领域等),每个领域都包含完整的:
+
+- API(接口层)
+- Core(业务核心)
+- Store(数据存储)
+
+这种设计让不同团队可以独立开发和维护各自的领域模块,有效减少代码冲突和开发混乱。与微服务相比,这种模块化方式使代码更简洁、更易于测试和维护。
+
+更重要的是,当某个领域模块的规模超出预期时,团队可以轻松地将其提取为独立的微服务,实现平滑的架构演进。
+
+### 技术栈
+
+- **Web 框架**:Gin
+- **ORM**:GORM
+- **架构设计**:领域驱动设计(DDD)
+- **代码生成**:支持自动生成代码
+- **消息处理**:使用 [NSQite](github.com/ixugo/nsqite) 支持事件总线/事务消息
+
+## 项目结构
+
+```
+.
+├── cmd # 可执行程序
+├── configs # 配置文件
+├── makefile # 提供构建/镜像/工具链等命令
+├── internal # 私有业务
+│ ├── conf # 配置模型
+│ ├── core # 业务领域
+| ├── domain # 公共领域,提供了一些模块化的组件,用于快速开发
+| ├── data # 数据库初始化逻辑
+│ └── web # 公共 Web 层
+└── pkg # 依赖库
+```
+
+## 应用案例
+
+- GB/T28181 视频平台(github.com/gowvp/gb28181)
+
+## 总结
+
+对于初级 Go 开发者来说,GoDDD 提供了一个清晰的项目结构和完整的开发工具链,让你能够快速上手企业级项目开发。它解决了项目结构混乱、团队协作困难、代码重复等常见问题,让开发者能够专注于业务逻辑的实现。
+
+如果你正在寻找一个能够帮助你快速构建企业级 REST API 的框架,GoDDD 绝对值得一试。它的模块化设计和完整的工具链,能够显著提升你的开发效率和代码质量。
diff --git a/docs/1_introduce_en.md b/docs/1_introduce_en.md
new file mode 100644
index 0000000..9716b01
--- /dev/null
+++ b/docs/1_introduce_en.md
@@ -0,0 +1,72 @@
+# GoDDD: Enterprise REST API Development Template
+
+## Introduction
+
+[GoDDD](https://github.com/ixugo/goddd) is an enterprise-level template focused on REST API development, designed to provide Go developers with a complete CRUD solution. It adopts Domain-Driven Design (DDD) principles and a modular monolithic architecture, enabling developers to quickly start projects and focus on business development.
+
+## Why Choose GoDDD?
+
+As a Go developer, have you ever encountered these challenges:
+
+- Project structure becomes increasingly difficult to maintain as the business grows
+- New team members take a long time to understand the project
+- Frequent code conflicts during team collaboration
+- Need to write large amounts of similar CRUD code repeatedly
+- Inconsistent error handling and messy logging
+- Require extensive infrastructure configuration when starting a project
+
+GoDDD is designed to solve these problems. It provides a clear project structure and a complete development toolchain, including:
+
+- Modular project structure for better code organization
+- Unified error handling and logging
+- Automated code generation tool [godddx](https://github.com/ixugo/godddx)
+
+## Technical Architecture
+
+### Architecture Design Philosophy
+
+In traditional MVC monolithic architecture, as business scale grows, projects become increasingly bloated, leading to reduced team development efficiency and difficulty for new members to quickly understand the system.
+
+GoDDD adopts a modular monolithic architecture that preserves the simplicity of monolithic architecture while incorporating the modular advantages of microservices. By breaking down the business into independent domain modules (such as user domain, banking domain, product domain, etc.), each domain contains a complete set of:
+
+- API (Interface Layer)
+- Core (Business Core)
+- Store (Data Storage)
+
+This design allows different teams to independently develop and maintain their domain modules, effectively reducing code conflicts and development chaos. Compared to microservices, this modular approach makes the code more concise, easier to test, and maintain.
+
+More importantly, when a domain module's scale exceeds expectations, teams can easily extract it as an independent microservice, achieving smooth architecture evolution.
+
+### Technology Stack
+
+- **Web Framework**: Gin
+- **ORM**: GORM
+- **Architecture Design**: Domain-Driven Design (DDD)
+- **Code Generation**: Support for automated code generation
+- **Message Processing**: Event bus/transaction messages using NSQite
+
+## Project Structure
+
+```
+.
+├── cmd # Executable programs
+├── configs # Configuration files
+├── makefile # Provides build/image/toolchain commands
+├── internal # Private business logic
+│ ├── conf # Configuration models
+│ ├── core # Business domains
+| ├── domain # Public domains, providing modular components for rapid development
+| ├── data # Database initialization logic
+│ └── web # Public Web layer
+└── pkg # Dependency libraries
+```
+
+## Application Cases
+
+- GB/T28181 Video Platform (github.com/gowvp/gb28181)
+
+## Conclusion
+
+For junior Go developers, GoDDD provides a clear project structure and complete development toolchain, enabling you to quickly get started with enterprise-level project development. It solves common problems such as messy project structure, difficult team collaboration, and code duplication, allowing developers to focus on business logic implementation.
+
+If you're looking for a framework to help you quickly build enterprise-level REST APIs, GoDDD is definitely worth trying. Its modular design and complete toolchain can significantly improve your development efficiency and code quality.
\ No newline at end of file
diff --git a/docs/2_request_warp.md b/docs/2_request_warp.md
new file mode 100644
index 0000000..5b0a98b
--- /dev/null
+++ b/docs/2_request_warp.md
@@ -0,0 +1,205 @@
+# 揭秘:如何用 Gin 框架打造优雅的 API 接口
+
+## 从重复代码到优雅封装
+
+在 Gin 框架开发中,你是否经常遇到这样的场景:每个接口都需要重复编写参数绑定、错误处理和响应格式化的代码?这不仅增加了代码量,还降低了开发效率和代码可维护性。
+
+让我们看一个典型的例子:
+
+```go
+func getUser(ctx *gin.Context) {
+ var in UserInput
+ if err := ctx.ShouldBindQuery(&in); err != nil {
+ ctx.JSON(400, gin.H{"error": err.Error()})
+ return
+ }
+ user, err := userService.GetUser(in.ID)
+ if err != nil {
+ ctx.JSON(500, gin.H{"error": "服务器错误"})
+ return
+ }
+ ctx.JSON(200, user)
+}
+```
+
+这样的代码模式在项目中反复出现,导致:
+1. 大量重复的样板代码
+2. 错误处理逻辑分散
+3. 响应格式不统一
+4. 业务逻辑被淹没在技术细节中
+
+本文将介绍如何通过 `web.WrapH` 这个优雅的封装方案,解决这些问题。
+
+## 优雅的解决方案
+
+`web.WrapH` 是一个基于泛型(Generic)的封装函数,它通过以下方式解决上述问题:
+
+1. 自动处理参数绑定
+2. 统一错误处理
+3. 标准化响应格式
+4. 让开发者专注于业务逻辑
+
+### 核心实现
+
+`web.WrapH` 的实现基于 Go 1.18 引入的泛型特性,它通过类型参数 `I` 和 `O` 分别表示输入和输出类型:
+
+```go
+func WrapH[I any, O any](fn func(*gin.Context, *I) (O, error)) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var in I
+ if unsafe.Sizeof(in) != 0 {
+ switch c.Request.Method {
+ case http.MethodGet:
+ if err := c.ShouldBindQuery(&in); err != nil {
+ Fail(c, ErrBadRequest.With(HanddleJSONErr(err).Error()))
+ return
+ }
+ case http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch:
+ if c.Request.ContentLength > 0 {
+ if err := c.ShouldBindJSON(&in); err != nil {
+ Fail(c, ErrBadRequest.With(HanddleJSONErr(err).Error()))
+ return
+ }
+ }
+ }
+ }
+ out, err := fn(c, &in)
+ if err != nil {
+ Fail(c, err)
+ return
+ }
+ Success(c, out)
+ }
+}
+```
+
+这个实现有几个关键点:
+1. 使用泛型支持任意输入输出类型
+2. 根据请求方法自动选择参数绑定方式
+3. 统一的错误处理和响应格式化
+4. 零值参数自动跳过绑定
+
+### 使用示例
+
+使用 `web.WrapH` 后,代码变得异常简洁:
+
+```go
+func getUser(ctx *gin.Context, in *UserInput) (*UserOutput, error) {
+ return userService.GetUser(in.ID)
+}
+```
+
+### 优势分析
+
+`web.WrapH` 通过泛型和统一的错误处理机制,让开发者专注于业务逻辑的实现。它提供了标准化的参数绑定、错误处理和响应格式化,大幅减少了重复代码。同时,泛型带来的类型安全性和 IDE 支持,让开发过程更加高效和可靠。
+
+## 实际应用场景
+
+### 场景一:查询单个用户
+
+```go
+// 路由定义
+router.GET("/users/:id", web.WrapH(getUser))
+
+// 处理函数,使用 `*struct{}` 空值来避免底层执行参数绑定
+func getUser(ctx *gin.Context, in *struct{}) (*UserOutput, error) {
+ id := ctx.Param("id")
+ return userService.GetUser(id)
+}
+```
+
+这个场景展示了如何处理没有请求参数的 GET 请求,通过 `ctx.Param` 获取路径参数。
+
+### 场景二:查询用户列表
+
+```go
+// 路由定义
+router.GET("/users", web.WrapH(listUsers))
+
+// 请求参数(定义在 user 包中)
+type FindUsersInput struct {
+ Page int `form:"page"`
+ Size int `form:"size"`
+ Name string `form:"name"`
+}
+
+// 处理函数
+func findUsers(ctx *gin.Context, in *FindUsersInput) (*FindUsersOutput, error) {
+ return userService.ListUsers(in)
+}
+```
+
+这个场景展示了如何使用 `form` 标签处理查询参数。
+
+### 场景三:修改用户信息
+
+```go
+// 路由定义
+router.PUT("/users/:id", web.WrapH(updateUser))
+
+// 请求参数
+type UpdateUserInput struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Age int `json:"age"`
+}
+
+// 处理函数
+func updateUser(ctx *gin.Context, in *UpdateUserInput) (*UserOutput, error) {
+ id := ctx.Param("id")
+ return userService.UpdateUser(in,id)
+}
+```
+
+这个场景展示了如何处理 PUT 请求,同时使用路径参数和请求体参数。
+
+### 场景四:删除用户
+
+```go
+// 路由定义
+router.DELETE("/users/:id", web.WrapH(deleteUser))
+
+// 处理函数
+func deleteUser(ctx *gin.Context, in *struct{}) (any, error) {
+ id := ctx.Param("id")
+ return userService.DeleteUser(id)
+}
+```
+
+这个场景展示了如何处理 DELETE 请求,以及无返回值的处理方式。
+
+遇到下载文件或非 curd 的复杂场景,你可以不使用 `web.WrapH`,而是 `gin.HandlerFunc`。
+
+
+细心的读者可能会发现,在本文的示例代码中,API 层直接返回了 error,那么状态码和错误内容是如何处理的呢?这个问题我们将在下一篇文章中详细讨论~
+
+## 最佳实践
+
+- 使用结构体(Struct)定义清晰的输入输出参数,提高代码可读性
+- 合理使用标签(Tag),如 `json`、`form` 等
+- 添加必要的参数验证
+- 使用指针类型避免不必要的内存分配
+- 使用 RESTful 风格设计 API
+
+## 总结
+
+通过使用 `web.WrapH`,我们可以:
+1. 大幅减少重复代码
+2. 提高代码可维护性
+3. 统一错误处理
+4. 提升开发效率
+
+这种封装方式特别适合团队协作开发,能够帮助团队快速构建高质量的 API 服务。
+
+## 关于 goddd
+
+本文介绍的 `web.WrapH` 是 [goddd](https://github.com/ixugo/goddd) 项目中的一个核心组件。goddd 是一个基于 DDD(领域驱动设计)理念的 Go 项目目标,它提供了一系列工具和最佳实践,帮助开发者构建可维护、可扩展的应用程序。
+
+如果你对本文介绍的内容感兴趣,欢迎访问 [goddd 项目](https://github.com/ixugo/goddd) 了解更多细节。项目提供了完整的示例代码和详细的文档,可以帮助你快速上手。
+
+
+## 相关资源
+
+- [Gin 框架官方文档](https://gin-gonic.com/docs/)
+- [Go 泛型教程](https://go.dev/doc/tutorial/generics)
+- [goddd 项目源码](https://github.com/ixugo/goddd)
\ No newline at end of file
diff --git a/docs/2_request_warp_en.md b/docs/2_request_warp_en.md
new file mode 100644
index 0000000..b4eb91f
--- /dev/null
+++ b/docs/2_request_warp_en.md
@@ -0,0 +1,201 @@
+# Unveiling: How to Build Elegant API Interfaces with Gin Framework
+
+## From Repetitive Code to Elegant Encapsulation
+
+In Gin framework development, have you ever encountered this scenario: each API endpoint requires repetitive code for parameter binding, error handling, and response formatting? This not only increases code volume but also reduces development efficiency and code maintainability.
+
+Let's look at a typical example:
+
+```go
+func getUser(ctx *gin.Context) {
+ var in UserInput
+ if err := ctx.ShouldBindQuery(&in); err != nil {
+ ctx.JSON(400, gin.H{"error": err.Error()})
+ return
+ }
+ user, err := userService.GetUser(in.ID)
+ if err != nil {
+ ctx.JSON(500, gin.H{"error": "Server error"})
+ return
+ }
+ ctx.JSON(200, user)
+}
+```
+
+This code pattern repeatedly appears in projects, leading to:
+1. Large amounts of boilerplate code
+2. Scattered error handling logic
+3. Inconsistent response formats
+4. Business logic buried in technical details
+
+This article will introduce how to solve these problems through the elegant encapsulation solution of `web.WrapH`.
+
+## Elegant Solution
+
+`web.WrapH` is a generic-based encapsulation function that solves the above problems through:
+
+1. Automatic parameter binding
+2. Unified error handling
+3. Standardized response formatting
+4. Allowing developers to focus on business logic
+
+### Core Implementation
+
+`web.WrapH`'s implementation is based on the generic feature introduced in Go 1.18, using type parameters `I` and `O` to represent input and output types respectively:
+
+```go
+func WrapH[I any, O any](fn func(*gin.Context, *I) (O, error)) gin.HandlerFunc {
+ return func(c *gin.Context) {
+ var in I
+ if unsafe.Sizeof(in) != 0 {
+ switch c.Request.Method {
+ case http.MethodGet:
+ if err := c.ShouldBindQuery(&in); err != nil {
+ Fail(c, ErrBadRequest.With(HanddleJSONErr(err).Error()))
+ return
+ }
+ case http.MethodPost, http.MethodPut, http.MethodDelete, http.MethodPatch:
+ if c.Request.ContentLength > 0 {
+ if err := c.ShouldBindJSON(&in); err != nil {
+ Fail(c, ErrBadRequest.With(HanddleJSONErr(err).Error()))
+ return
+ }
+ }
+ }
+ }
+ out, err := fn(c, &in)
+ if err != nil {
+ Fail(c, err)
+ return
+ }
+ Success(c, out)
+ }
+}
+```
+
+Key points of this implementation:
+1. Using generics to support arbitrary input and output types
+2. Automatically selecting parameter binding methods based on request method
+3. Unified error handling and response formatting
+4. Skipping binding for zero-value parameters
+
+### Usage Example
+
+With `web.WrapH`, the code becomes exceptionally concise:
+
+```go
+func getUser(ctx *gin.Context, in *UserInput) (*UserOutput, error) {
+ return userService.GetUser(in.ID)
+}
+```
+
+### Advantage Analysis
+
+`web.WrapH` allows developers to focus on business logic implementation rather than repetitive boilerplate code. Through generics and unified error handling mechanisms, it achieves code conciseness and maintainability. At compile time, generics ensure type safety, prevent runtime type errors, and provide better IDE support.
+
+## Practical Application Scenarios
+
+### Scenario 1: Query Single User
+
+```go
+// Route definition
+router.GET("/users/:id", web.WrapH(getUser))
+
+// Handler function, using `*struct{}` empty value to avoid parameter binding
+func getUser(ctx *gin.Context, in *struct{}) (*UserOutput, error) {
+ id := ctx.Param("id")
+ return userService.GetUser(id)
+}
+```
+
+This scenario demonstrates how to handle GET requests without request parameters, using `ctx.Param` to get path parameters.
+
+### Scenario 2: Query User List
+
+```go
+// Route definition
+router.GET("/users", web.WrapH(listUsers))
+
+// Request parameters (defined in user package)
+type FindUsersInput struct {
+ Page int `form:"page"`
+ Size int `form:"size"`
+ Name string `form:"name"`
+}
+
+// Handler function
+func findUsers(ctx *gin.Context, in *FindUsersInput) (*FindUsersOutput, error) {
+ return userService.ListUsers(in)
+}
+```
+
+This scenario demonstrates how to use `form` tags to handle query parameters.
+
+### Scenario 3: Update User Information
+
+```go
+// Route definition
+router.PUT("/users/:id", web.WrapH(updateUser))
+
+// Request parameters
+type UpdateUserInput struct {
+ Name string `json:"name"`
+ Email string `json:"email"`
+ Age int `json:"age"`
+}
+
+// Handler function
+func updateUser(ctx *gin.Context, in *UpdateUserInput) (*UserOutput, error) {
+ id := ctx.Param("id")
+ return userService.UpdateUser(in, id)
+}
+```
+
+This scenario demonstrates how to handle PUT requests with both path parameters and request body parameters.
+
+### Scenario 4: Delete User
+
+```go
+// Route definition
+router.DELETE("/users/:id", web.WrapH(deleteUser))
+
+// Handler function
+func deleteUser(ctx *gin.Context, in *struct{}) (any, error) {
+ id := ctx.Param("id")
+ return userService.DeleteUser(id)
+}
+```
+
+This scenario demonstrates how to handle DELETE requests and handle cases with no return value.
+
+For complex scenarios like file downloads or non-CRUD operations, you can use `gin.HandlerFunc` instead of `web.WrapH`.
+
+## Best Practices
+
+- Use structs to define clear input and output parameters, improving code readability
+- Use tags appropriately, such as `json`, `form`, etc.
+- Add necessary parameter validation
+- Use pointer types to avoid unnecessary memory allocation
+- Design APIs following RESTful style
+
+## Summary
+
+By using `web.WrapH`, we can:
+1. Significantly reduce repetitive code
+2. Improve code maintainability
+3. Unify error handling
+4. Enhance development efficiency
+
+This encapsulation approach is particularly suitable for team collaboration, helping teams quickly build high-quality API services.
+
+## About goddd
+
+The `web.WrapH` introduced in this article is a core component of the [goddd](https://github.com/ixugo/goddd) project. goddd is a Go project based on DDD (Domain-Driven Design) principles, providing a series of tools and best practices to help developers build maintainable and extensible applications.
+
+If you're interested in the content introduced in this article, welcome to visit the [goddd project](https://github.com/ixugo/goddd) for more details. The project provides complete example code and detailed documentation to help you get started quickly.
+
+## Related Resources
+
+- [Gin Framework Official Documentation](https://gin-gonic.com/docs/)
+- [Go Generics Tutorial](https://go.dev/doc/tutorial/generics)
+- [goddd Project Source Code](https://github.com/ixugo/goddd)
\ No newline at end of file
diff --git a/docs/3_error.md b/docs/3_error.md
new file mode 100644
index 0000000..d797e4b
--- /dev/null
+++ b/docs/3_error.md
@@ -0,0 +1,215 @@
+# 用 HTTP 状态码还是自定义状态码? Go 错误处理的优雅解决方案
+
+在开发 REST API 时,你是否遇到过这样的问题:
+
+- 错误信息杂乱无章,难以定位问题
+- 用户看到的错误提示晦涩难懂
+- 生产环境暴露了敏感信息
+- 错误处理代码重复且难以维护
+
+本文将介绍一种优雅的错误处理解决方案,让你的代码更加清晰、可维护,同时提供更好的用户体验。
+
+## 错误处理的痛点
+
+在传统的错误处理方式中,我们常常会遇到以下问题:
+
+1. 错误信息不统一
+ - 有的使用数字状态码
+ - 有的使用字符串描述
+ - 有的直接返回底层错误
+
+2. 错误处理代码重复
+ - 每个接口都要写错误处理逻辑
+ - 日志记录分散在各处
+ - HTTP 状态码映射混乱
+
+3. 用户体验差
+ - 错误提示不友好
+ - 缺乏上下文信息
+ - 难以定位问题
+
+## 传统错误处理方案
+
+让我们先看看传统的错误处理方式:
+
+```go
+// 方式一:直接返回错误
+func findUser(ctx *gin.Context) {
+ user, err := db.FindUser()
+ if err != nil {
+ ctx.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+ ctx.JSON(200, user)
+}
+
+// 方式二:使用状态码
+func findUser(ctx *gin.Context) {
+ user, err := db.FindUser()
+ if err != nil {
+ ctx.JSON(500, gin.H{
+ "code": 1001,
+ "msg": "数据库查询失败"
+ })
+ return
+ }
+ ctx.JSON(200, user)
+}
+```
+
+这些方案虽然简单直接,但存在一些明显的缺陷:
+
+1. 安全性问题
+ - 方式一直接将底层错误暴露给用户,可能泄露敏感信息
+ - 数据库错误可能包含表结构、SQL 语句等内部信息
+
+2. 沟通复杂性
+ - 错误状态码 400 是 http 状态码还是自定义状态码?
+ - 必须对响应体反序列化,才能知道状态
+ - 重复定义,http 状态码 200 与自定义状态码 0 都表示成功
+
+3. 可维护性差
+ - 数字错误码(如 1001)缺乏语义,难以理解
+ - 开发者需要查阅文档才能理解错误含义
+ - 错误码定义者也可能忘记具体含义
+
+## 优雅的错误处理方案
+
+让我们先看看优雅的错误处理方案是如何使用的:
+
+```go
+var ErrBadRequest = reason.NewError("ErrBadRequest", "请求参数有误")
+
+func (u *UserAPI) getUser(ctx *gin.Context, _ *struct{}) (*user.UserOutput, error) {
+ return u.core.GetUser(in.ID)
+}
+
+// package user
+func (u *Core) GetUser(id int64) (*UserOutput, error) {
+ // 参数校验
+ if err != nil {
+ return nil, ErrBadRequest.With(err.Error(), "xx 参数应在 10~100 之间")
+ }
+ // 正确处理逻辑...
+}
+```
+
+还记得上一篇文章提到的 `web.WrapH` 函数吗? 其响应错误实际是调用的 `web.Fail(err)`,此方法会判断错误是否是 `reason.Error` 类型,如果是,则按照其定义的 http 状态码,reason, msg 等信息返回给客户端。
+
+类似
+
+HTTP Status Code: 400 (默认所有错误都是 400)
+```json
+{
+ "reason": "用于程序识别的错误",
+ "msg": "告诉用户的错误信息描述",
+ "details":[
+ "某字段传输有误",
+ "你可以这样修复",
+ "查看文档获取更多信息"
+ ]
+}
+```
+
+### 错误码设计思考
+
+传统方案中使用数字错误码(如1001表示数据库错误)存在明显缺点:缺乏语义性,需查阅文档理解,定义者也易忘记含义。
+
+因此,我们采用字符串作为错误码,优势明显:
+
+1. 自解释性强
+ - `ErrBadRequest`比`1001`直观明了
+ - 错误码即文档
+ - 便于代码审查和调试
+
+2. 扩展性好
+ - 可用模块前缀区分
+ - 避免错误码冲突
+ - 快速定位问题源
+
+在和前端的对接过程中,某些接口出现错误了,前端会一头雾水,找服务端排查,有些可能只是参数问题,如果在响应的错误中有帮助解决的方案呢?能否简化对接复杂度?
+
+为避免用户看到技术性错误,或者开发者缺乏上下文信息。我们将错误信息分为四个属性:
+
+```go
+type Error struct {
+ Reason string
+ Msg string
+ Details []string
+ HTTPStatus int
+}
+```
+
+每个字段的作用:
+
+1. `reason` 字段
+ - 使用大驼峰英语描述错误原因
+ - 用于程序内部判断错误类型
+ - 支持错误码映射到 HTTP 状态码
+
+2. `msg` 字段
+ - 使用开发者母语描述错误
+ - 面向用户,提供友好的提示
+
+3. `details` 字段
+ - 提供错误扩展信息
+ - 面向开发者,帮助调试
+ - 可在生产环境调用 `web.SetRelease()` 隐藏,避免泄露敏感信息
+
+4. `HTTPStatus`
+ - http 响应状态码
+ - 默认为 400
+ - 常用状态码 200,400,401 基本就够了,保持简单,少即是多
+
+## 使用文档
+
+1. 使用预定义的错误类型
+ - `reason.ErrBadRequest`: 请求参数错误
+ - `reason.ErrStore`: 数据库错误
+ - `reason.ErrServer`: 服务器错误
+
+2. 错误信息处理
+ - 使用 `SetMsg()` 方法修改用户友好提示
+ - 使用 `Withf()` 方法添加开发者帮助,增加错误上下文
+
+采用一个 reason 原则,即 reason 相同则是同一个错误。
+
+```go
+ // e1 和 e2 是不是同一个错误!
+ e1 = NewError("e1", "e1")
+ e2 = NewError("e2", "e1")
+```
+
+```go
+// e3 与 e2 是相同的错误
+e2 := NewError("e2", "e2").SetHTTPStatus(200).With("e2-1")
+e3 := fmt.Errorf("e3:%w", e2)
+if !errors.Is(e3, e2) {
+ t.Fatal("expect e3 is e2, but not")
+}
+```
+
+```go
+// 将错误转换为 *reason.Error 结构体
+ var e5 *reason.Error
+ if !errors.As(e4, &e5) {
+ t.Fatal("expect e4 as e5, but not")
+ }
+```
+
+## 总结
+
+通过合理的分层和封装,我们实现了:
+
+1. 统一的错误处理流程
+2. 友好的用户提示
+3. 详细的开发者信息
+4. 安全的错误暴露
+
+这种设计既保证了开发效率,又提升了用户体验。如果你正在寻找一个优雅的错误处理解决方案,不妨试试这个方案。
+
+## 关于 goddd
+
+本文介绍的错误处理是 [goddd](https://github.com/ixugo/goddd) 项目中的一个核心组件。goddd 是一个基于 DDD(领域驱动设计)理念的 Go 项目目标,它提供了一系列工具和最佳实践,帮助开发者构建可维护、可扩展的应用程序。
+
+如果你对本文介绍的内容感兴趣,欢迎访问 [goddd 项目](https://github.com/ixugo/goddd) 了解更多细节。项目提供了完整的示例代码和详细的文档,可以帮助你快速上手。
diff --git a/docs/3_error_en.md b/docs/3_error_en.md
new file mode 100644
index 0000000..c6615c5
--- /dev/null
+++ b/docs/3_error_en.md
@@ -0,0 +1,215 @@
+# HTTP Status Code or Custom Status Code? An Elegant Error Handling Solution in Go
+
+When developing REST APIs, have you ever encountered these problems:
+
+- Error messages are disorganized and hard to locate
+- Error prompts shown to users are obscure and difficult to understand
+- Sensitive information is exposed in production environment
+- Error handling code is repetitive and hard to maintain
+
+This article introduces an elegant error handling solution that makes your code clearer, more maintainable, and provides a better user experience.
+
+## Pain Points in Error Handling
+
+In traditional error handling approaches, we often face the following issues:
+
+1. Inconsistent error messages
+ - Some use numeric status codes
+ - Some use string descriptions
+ - Some directly return underlying errors
+
+2. Repetitive error handling code
+ - Each interface requires error handling logic
+ - Logging is scattered everywhere
+ - HTTP status code mapping is confusing
+
+3. Poor user experience
+ - Error prompts are not user-friendly
+ - Lack of contextual information
+ - Difficult to locate problems
+
+## Traditional Error Handling Solutions
+
+Let's first look at traditional error handling approaches:
+
+```go
+// Approach 1: Direct error return
+func findUser(ctx *gin.Context) {
+ user, err := db.FindUser()
+ if err != nil {
+ ctx.JSON(500, gin.H{"error": err.Error()})
+ return
+ }
+ ctx.JSON(200, user)
+}
+
+// Approach 2: Using status codes
+func findUser(ctx *gin.Context) {
+ user, err := db.FindUser()
+ if err != nil {
+ ctx.JSON(500, gin.H{
+ "code": 1001,
+ "msg": "Database query failed"
+ })
+ return
+ }
+ ctx.JSON(200, user)
+}
+```
+
+These approaches, while simple and direct, have some obvious drawbacks:
+
+1. Security issues
+ - Approach 1 directly exposes underlying errors to users, potentially leaking sensitive information
+ - Database errors may contain internal information like table structure, SQL statements
+
+2. Communication complexity
+ - Is status code 400 an HTTP status code or a custom status code?
+ - Must deserialize response body to know the status
+ - Duplicate definitions, both HTTP status code 200 and custom status code 0 indicate success
+
+3. Poor maintainability
+ - Numeric error codes (like 1001) lack semantics and are hard to understand
+ - Developers need to consult documentation to understand error meanings
+ - Even error code definers may forget their specific meanings
+
+## An Elegant Error Handling Solution
+
+Let's first see how the elegant error handling solution is used:
+
+```go
+var ErrBadRequest = reason.NewError("ErrBadRequest", "Invalid request parameters")
+
+func (u *UserAPI) getUser(ctx *gin.Context, _ *struct{}) (*user.UserOutput, error) {
+ return u.core.GetUser(in.ID)
+}
+
+// package user
+func (u *Core) GetUser(id int64) (*UserOutput, error) {
+ // Parameter validation
+ if err != nil {
+ return nil, ErrBadRequest.With(err.Error(), "xx parameter should be between 10~100")
+ }
+ // Correct processing logic...
+}
+```
+
+Remember the `web.WrapH` function mentioned in the previous article? Its error response actually calls `web.Fail(err)`, which checks if the error is of type `reason.Error`. If it is, it returns the defined HTTP status code, reason, and msg to the client.
+
+Similar to:
+
+HTTP Status Code: 400 (default for all errors)
+```json
+{
+ "reason": "Error for program recognition",
+ "msg": "Error message description for users",
+ "details": [
+ "Field transmission error",
+ "You can fix it this way",
+ "Check documentation for more information"
+ ]
+}
+```
+
+### Error Code Design Considerations
+
+Traditional solutions using numeric error codes (like 1001 for database errors) have obvious disadvantages: lack of semantics, requiring documentation consultation, and definers easily forgetting meanings.
+
+Therefore, we use strings as error codes, with clear advantages:
+
+1. Strong self-explanatory nature
+ - `ErrBadRequest` is more intuitive than `1001`
+ - Error codes serve as documentation
+ - Facilitates code review and debugging
+
+2. Good extensibility
+ - Can use module prefixes for distinction
+ - Avoids error code conflicts
+ - Quickly locates problem sources
+
+During frontend integration, when certain interfaces encounter errors, frontend developers might be confused and need to consult backend developers. Some issues might just be parameter problems. What if the error response included solutions? Could this simplify integration complexity?
+
+To prevent users from seeing technical errors or developers lacking contextual information, we divide error information into four attributes:
+
+```go
+type Error struct {
+ Reason string
+ Msg string
+ Details []string
+ HTTPStatus int
+}
+```
+
+The role of each field:
+
+1. `reason` field
+ - Uses PascalCase English to describe error reasons
+ - Used for internal program error type determination
+ - Supports error code mapping to HTTP status codes
+
+2. `msg` field
+ - Uses developer's native language to describe errors
+ - User-oriented, provides friendly prompts
+
+3. `details` field
+ - Provides extended error information
+ - Developer-oriented, aids debugging
+ - Can be hidden in production environment by calling `web.SetRelease()` to avoid leaking sensitive information
+
+4. `HTTPStatus`
+ - HTTP response status code
+ - Defaults to 400
+ - Common status codes 200, 400, 401 are usually sufficient, keeping it simple
+
+## Usage Documentation
+
+1. Using predefined error types
+ - `reason.ErrBadRequest`: Request parameter error
+ - `reason.ErrStore`: Database error
+ - `reason.ErrServer`: Server error
+
+2. Error message handling
+ - Use `SetMsg()` method to modify user-friendly prompts
+ - Use `Withf()` method to add developer assistance, increasing error context
+
+Adopt a single reason principle, meaning errors with the same reason are considered the same error.
+
+```go
+// Are e1 and e2 the same error?
+e1 = NewError("e1", "e1")
+e2 = NewError("e2", "e1")
+```
+
+```go
+// e3 and e2 are the same error
+e2 := NewError("e2", "e2").SetHTTPStatus(200).With("e2-1")
+e3 := fmt.Errorf("e3:%w", e2)
+if !errors.Is(e3, e2) {
+ t.Fatal("expect e3 is e2, but not")
+}
+```
+
+```go
+// Convert error to *reason.Error struct
+var e5 *reason.Error
+if !errors.As(e4, &e5) {
+ t.Fatal("expect e4 as e5, but not")
+}
+```
+
+## Summary
+
+Through reasonable layering and encapsulation, we have achieved:
+
+1. Unified error handling process
+2. Friendly user prompts
+3. Detailed developer information
+4. Secure error exposure
+
+This design ensures both development efficiency and user experience. If you're looking for an elegant error handling solution, why not try this approach?
+
+## About goddd
+
+The error handling introduced in this article is a core component of the [goddd](https://github.com/ixugo/goddd) project. goddd is a Go project based on DDD (Domain-Driven Design) principles, providing a series of tools and best practices to help developers build maintainable and extensible applications.
+
+If you're interested in the content introduced in this article, welcome to visit the [goddd project](https://github.com/ixugo/goddd) to learn more. The project provides complete example code and detailed documentation to help you get started quickly.
\ No newline at end of file
diff --git a/docs/4.别在反向优化 md5.md b/docs/4.别在反向优化 md5.md
new file mode 100644
index 0000000..108fddf
--- /dev/null
+++ b/docs/4.别在反向优化 md5.md
@@ -0,0 +1,174 @@
+# MD5优化误区:性能不是关键,内存才重要
+
+在 Go 语言开发中,MD5计算是一个常见的操作。再过去的使用中,我总会想方设法优化 MD5 的计算性能,然而,通过深入的性能测试和源码分析,发现:**MD5计算本身并不需要性能优化,真正的优化点在于调用者的内存使用**。
+
+## 性能测试结果对比
+
+让我们先来看一组 benchmark 测试结果,测试对象是1MB的字符串数据:
+
+```
+BenchmarkMD5/segment_md5-8 81 42623578 ns/op 1120 B/op 4 allocs/op
+BenchmarkMD5/md5-8 84 43422871 ns/op 32 B/op 1 allocs/op
+BenchmarkMD5/io_md5-8 85 41750913 ns/op 192 B/op 4 allocs/op
+```
+
+从结果可以看出几个关键点:
+
+1. **性能差异微乎其微**:三种方式的执行时间都在4千万纳秒左右,差异不到5%
+2. **内存分配差异显著**:
+ - `md5(直接调用基础库)`: 仅32字节分配,1次分配
+ - `io_md5(使用 io.Copy)`: 92字节分配,4次分配
+ - `segment_md5(对字节数组分片)`: 1120字节分配,4次分配
+
+## 测试的三种MD5实现方式
+
+让我们看看这三种不同的实现方式:
+
+```go
+// 方式1:直接计算
+func MD5(s []byte) string {
+ b := md5.Sum(s)
+ return hex.EncodeToString(b[:])
+}
+
+// 方式2:分段读取
+func SegmentMD5(r io.Reader) (string, error) {
+ h := md5.New()
+ buf := make([]byte, 1*1024) // 1KB缓冲区
+ for {
+ n, err := r.Read(buf)
+ if n > 0 {
+ if _, err := h.Write(buf[:n]); err != nil {
+ return "", err
+ }
+ }
+ if err != nil {
+ if errors.Is(err, io.EOF) {
+ break
+ }
+ return "", err
+ }
+ }
+ return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+// 方式3:使用io.Copy
+func IOMD5(r io.Reader) (string, error) {
+ h := md5.New()
+ if _,err := io.Copy(h, r); err != nil {
+ return "",err
+ }
+ return hex.EncodeToString(h.Sum(nil)), nil
+}
+```
+
+## 为什么MD5计算不需要性能优化?
+
+### 底层算法的真正优化
+
+Go标准库的MD5实现已经经过高度优化。让我们看看真正的`Write`函数实现:
+
+```go
+func (d *digest) Write(p []byte) (nn int, err error) {
+ // ....
+ if len(p) >= BlockSize {
+ n := len(p) &^ (BlockSize - 1)
+ if haveAsm {
+ for n > maxAsmSize {
+ block(d, p[:maxAsmSize])
+ p = p[maxAsmSize:]
+ n -= maxAsmSize
+ }
+ block(d, p[:n])
+ } else {
+ blockGeneric(d, p[:n])
+ }
+ p = p[n:]
+ }
+ if len(p) > 0 {
+ d.nx = copy(d.x[:], p)
+ }
+ return
+}
+```
+
+### 优化点详解
+
+
+**大数据处理优化**:
+```go
+for n > maxAsmSize {
+ block(d, p[:maxAsmSize])
+ p = p[maxAsmSize:]
+ n -= maxAsmSize
+}
+```
+
+对超过64KB(maxAsmSize)的数据进行分批处理,这是因为汇编实现是非抢占式的,不能被中断。
+
+**汇编优化**:
+```go
+if haveAsm {
+ block(d, d.x[:]) // 优化的汇编版本
+} else {
+ blockGeneric(d, d.x[:]) // 通用Go版本
+}
+```
+
+在支持的平台(如amd64)上,使用专门优化的汇编实现`block`,在其他平台使用通用版本`blockGeneric`。
+
+在amd64平台上,`block`函数使用汇编实现:
+
+```asm
+TEXT ·block(SB), NOSPLIT, $8-32
+ MOVQ dig+0(FP), BP
+ MOVQ p_base+8(FP), SI
+ MOVQ p_len+16(FP), DX
+ SHRQ $0x06, DX // 除以64,计算块数
+ SHLQ $0x06, DX // 乘以64,计算总字节数
+ // ... 优化的MD5算法实现
+```
+
+汇编版本通过直接操作CPU寄存器,避免了Go函数调用的开销,并且使用了SIMD指令等底层优化。
+
+## 上层优化的正确姿势
+
+既然 MD5 计算本身不需要优化,我们应该将注意力放在上层调用上:
+
+### 选择合适的读取方式
+
+```go
+// 推荐:处理大文件时使用流式处理
+func ProcessLargeFile(filename string) (string, error) {
+ file, err := os.Open(filename)
+ if err != nil {
+ return "", err
+ }
+ defer file.Close()
+
+ h := md5.New()
+ if _, err := io.Copy(h, file); err != nil {
+ return "", err
+ }
+
+ return hex.EncodeToString(h.Sum(nil)), nil
+}
+
+// 不推荐:一次性读取大文件
+func ProcessLargeFileBad(filename string) (string, error) {
+ data, err := os.ReadFile(filename) // 可能消耗大量内存
+ if err != nil {
+ return "", err
+ }
+
+ return MD5(data), nil
+}
+```
+
+在处理大文件时,流式处理相比一次性读取的优势在于不必一次性分配大量内存。
+
+## 总结
+
+对于 MD5 计算,标准库的实现已经是最佳选择。我们要做的是在上层调用上做出明智的选择,避免不必要的内存开销。
+
+想要了解更多Go语言中的优化实践,欢迎访问 [goddd](https://github.com/ixugo/goddd) web 框架模板,这里有更多关于Go语言开发的最佳实践。
diff --git a/docs/version.md b/docs/version.md
new file mode 100644
index 0000000..dbfbbf3
--- /dev/null
+++ b/docs/version.md
@@ -0,0 +1,40 @@
+# 关于 GORM 控制 AutoMigrate 节省启动时间
+
+## 什么是 AutoMigrate?
+
+在 Go 开发中,我们经常需要修改数据库表结构。传统方式是手动编写 SQL 语句,既繁琐又容易出错。GORM 的 AutoMigrate 功能应运而生,它能自动同步代码和数据库结构,让开发更高效。
+
+## 为什么需要控制 AutoMigrate?
+
+让我们看看一个真实场景:项目初期只有 7 个表,AutoMigrate 执行只需 1 秒。随着功能增加,表数量增长到 30 个,启动时间延长到 10 秒。更糟的是,即使表结构没有变化,每次启动都要重新检查所有表,这显然不合理。
+
+## 解决方案:版本控制
+
+goddd 引入版本控制机制,就像软件更新一样,只有在新版本发布时才需要更新。具体实现:
+
+1. 数据库记录当前版本号
+2. 程序启动时检查代码版本号
+3. 仅当代码版本号大于数据库版本号时执行 AutoMigrate
+
+## 如何修改版本?代码示例
+
+```go
+// 设置数据库版本号(0.0.2 或 v0.0.2 都可以)
+versionapi.DBVersion = "0.0.2"
+// 添加版本说明,记录这次更新了什么
+versionapi.DBRemark = "添加了用户头像字段"
+```
+
+通过 `orm.SetEnabledAutoMigrate()` 可以全局控制 AutoMigrate 的启用状态。
+
+## 这样做有什么好处?
+
+1. 程序启动更快了,不用每次都检查所有表
+2. 避免不必要的数据库操作,减少数据库压力
+3. 可以清楚地知道每次数据库结构变更的历史
+
+## 关于 goddd
+
+[github.com/ixugo/goddd](https://github.com/ixugo/goddd) 是一个基于领域驱动设计(DDD)的 Go 语言项目模板,提供了完整的项目结构和最佳实践。它集成了 GORM、Gin 等常用组件,特别适合中小项目快速开始。
+
+想了解更多?欢迎查看 goddd 的源码。
diff --git a/docs/version_en.md b/docs/version_en.md
new file mode 100644
index 0000000..03a9b8c
--- /dev/null
+++ b/docs/version_en.md
@@ -0,0 +1,40 @@
+# About GORM AutoMigrate Control for Faster Startup
+
+## What is AutoMigrate?
+
+In Go development, we often need to modify database table structures. The traditional approach involves manually writing SQL statements, which is both tedious and error-prone. GORM's AutoMigrate feature was created to solve this problem, automatically synchronizing code and database structures to make development more efficient.
+
+## Why Control AutoMigrate?
+
+Let's look at a real scenario: In the early stages of a project with just 7 tables, AutoMigrate execution takes only 1 second. As features are added and the number of tables grows to 30, startup time extends to 10 seconds. Even worse, every startup requires checking all tables, even when no structural changes have been made - this is clearly inefficient.
+
+## Solution: Version Control
+
+goddd introduces a version control mechanism, similar to software updates, where updates are only needed when a new version is released. Implementation details:
+
+1. Database records the current version number
+2. Program checks code version number at startup
+3. AutoMigrate is only executed when code version number is greater than database version number
+
+## How to Modify Version? Code Example
+
+```go
+// Set database version (both 0.0.2 and v0.0.2 are acceptable)
+versionapi.DBVersion = "0.0.2"
+// Add version description to record what was updated
+versionapi.DBRemark = "Added user avatar field"
+```
+
+The `orm.SetEnabledAutoMigrate` variable can be used to globally control AutoMigrate's enabled state.
+
+## What are the Benefits?
+
+1. Faster program startup by avoiding unnecessary table checks
+2. Reduced database pressure by eliminating redundant operations
+3. Clear tracking of database structure change history
+
+## About goddd
+
+[github.com/ixugo/goddd](https://github.com/ixugo/goddd) is a Go language project template based on Domain-Driven Design (DDD), providing a complete project structure and best practices. It integrates commonly used components like GORM and Gin, making it particularly suitable for quickly starting small to medium-sized projects.
+
+Want to learn more? Check out the goddd source code.
\ No newline at end of file
diff --git a/domain/uniqueid/bz.go b/domain/uniqueid/bz.go
new file mode 100644
index 0000000..54b8727
--- /dev/null
+++ b/domain/uniqueid/bz.go
@@ -0,0 +1,83 @@
+// uniqueid
+// 的设计是用于生成全局唯一的 ID,避免重复。
+// 此库不考虑分布式,仅通过数据库主键索引来实现。
+// 当 id 重复时,由业务端抛出错误即可
+
+package uniqueid
+
+import (
+ "context"
+ "crypto/rand"
+ "log/slog"
+ "math/big"
+ "time"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "github.com/ixugo/goddd/pkg/hook"
+)
+
+const (
+ // 删除 o 和 i 的字符集,避免视觉混淆
+ LetterBytes36NoOI = "abcdefghjklmnpqrstuvwxyz0123456789"
+
+ LetterBytes72 = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+ LetterBytes36 = "abcdefghijklmnopqrstuvwxyz0123456789" // default
+ LetterBytes36Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
+)
+
+type IDManager struct {
+ store UniqueIDStorer
+ // TODO: 可以初始化时读取数据库内的数量,判断重复因子,从而减少尝试或更换策略
+
+ letterBytes string // 随机字符串字符集
+}
+
+func NewIDManager(store UniqueIDStorer) *IDManager {
+ return &IDManager{
+ store: store,
+ letterBytes: LetterBytes36,
+ }
+}
+
+// SetLetterBytes 设置随机字符串字符集
+func (m *IDManager) SetLetterBytes(letterBytes string) {
+ m.letterBytes = letterBytes
+}
+
+// UniqueID 获取唯一 id
+func (m *IDManager) UniqueID(prefix string, length int) string {
+ cost := hook.UseTiming(time.Second)
+ defer cost()
+
+ // 如果在最低长度中,碰撞比较频繁,增加 1 位长度再试一次
+ for i := range 10 {
+ // 生成自定义长度随机数,通过数据库主键来防止碰撞,碰撞后再次尝试
+ for range 36 {
+ id := prefix + GenerateRandomString(m.letterBytes, length+i)
+ if err := m.store.Add(context.Background(), &UniqueID{ID: id}); err != nil {
+ slog.Error("UniqueID", "err", err)
+ continue
+ }
+ return id
+ }
+ }
+ slog.Error("UniqueID", "err", "超过最大循环次数,未获取到唯一 id")
+ return "unknown"
+}
+
+// UndoUniqueID 删除唯一 id
+func (m *IDManager) UndoUniqueID(id string) error {
+ var uni UniqueID
+ return m.store.Del(context.Background(), &uni, orm.Where("id=?", id))
+}
+
+// GenerateRandomString 生成随机字符串
+func GenerateRandomString(letterBytes string, length int) string {
+ lettersLength := big.NewInt(int64(len(letterBytes)))
+ result := make([]byte, length)
+ for i := 0; i < length; i++ {
+ idx, _ := rand.Int(rand.Reader, lettersLength)
+ result[i] = letterBytes[idx.Int64()]
+ }
+ return string(result)
+}
diff --git a/domain/uniqueid/core.go b/domain/uniqueid/core.go
new file mode 100644
index 0000000..389bcdf
--- /dev/null
+++ b/domain/uniqueid/core.go
@@ -0,0 +1,39 @@
+// Code generated by godddx, DO AVOID EDIT.
+package uniqueid
+
+// Storer data persistence
+type Storer interface {
+ UniqueID() UniqueIDStorer
+}
+
+// Core business domain
+type Core struct {
+ store Storer
+ m *IDManager
+ length int
+}
+
+// NewCore create business domain
+func NewCore(store Storer, length int) Core {
+ return Core{
+ store: store,
+ length: length,
+ m: NewIDManager(store.UniqueID()),
+ }
+}
+
+// UniqueID 获取全局唯一 ID
+// 当创建的 id 并未使用,或允许下次再次使用,请执行 UndoUniqueID
+func (c Core) UniqueID(prefix string) string {
+ return c.m.UniqueID(prefix, c.length)
+}
+
+// UniqueIDByCustomLen 获取自定义长度的全局 id
+func (c Core) UniqueIDWithCustomLen(prefix string, length int) string {
+ return c.m.UniqueID(prefix, length)
+}
+
+// UndoUniqueID 撤销唯一 id,如果 UniqueID 获取的某个 id 并未使用,或者随着数据源的删除可以调用此函数撤销
+func (c Core) UndoUniqueID(id string) error {
+ return c.m.UndoUniqueID(id)
+}
diff --git a/domain/uniqueid/store/uniqueiddb/db.go b/domain/uniqueid/store/uniqueiddb/db.go
new file mode 100644
index 0000000..2215de3
--- /dev/null
+++ b/domain/uniqueid/store/uniqueiddb/db.go
@@ -0,0 +1,38 @@
+// Code generated by godddx, DO AVOID EDIT.
+package uniqueiddb
+
+import (
+ "easyvqd/domain/uniqueid"
+
+ "gorm.io/gorm"
+)
+
+var _ uniqueid.Storer = DB{}
+
+// DB Related business namespaces
+type DB struct {
+ db *gorm.DB
+}
+
+// NewDB instance object
+func NewDB(db *gorm.DB) DB {
+ return DB{db: db}
+}
+
+// UniqueID Get business instance
+func (d DB) UniqueID() uniqueid.UniqueIDStorer {
+ return UniqueID(d)
+}
+
+// AutoMigrate sync database
+func (d DB) AutoMigrate(ok bool) DB {
+ if !ok {
+ return d
+ }
+ if err := d.db.AutoMigrate(
+ new(uniqueid.UniqueID),
+ ); err != nil {
+ panic(err)
+ }
+ return d
+}
diff --git a/domain/uniqueid/store/uniqueiddb/unique_id.go b/domain/uniqueid/store/uniqueiddb/unique_id.go
new file mode 100644
index 0000000..36680bc
--- /dev/null
+++ b/domain/uniqueid/store/uniqueiddb/unique_id.go
@@ -0,0 +1,61 @@
+// Code generated by godddx, DO AVOID EDIT.
+package uniqueiddb
+
+import (
+ "context"
+
+ "easyvqd/domain/uniqueid"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "gorm.io/gorm"
+)
+
+var _ uniqueid.UniqueIDStorer = UniqueID{}
+
+// UniqueID Related business namespaces
+type UniqueID DB
+
+// NewUniqueID instance object
+func NewUniqueID(db *gorm.DB) UniqueID {
+ return UniqueID{db: db}
+}
+
+// Find implements uniqueid.UniqueIDStorer.
+func (d UniqueID) Find(ctx context.Context, bs *[]*uniqueid.UniqueID, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
+ return orm.FindWithContext(ctx, d.db, bs, page, opts...)
+}
+
+// Get implements uniqueid.UniqueIDStorer.
+func (d UniqueID) Get(ctx context.Context, model *uniqueid.UniqueID, opts ...orm.QueryOption) error {
+ return orm.FirstWithContext(ctx, d.db, model, opts...)
+}
+
+// Add implements uniqueid.UniqueIDStorer.
+func (d UniqueID) Add(ctx context.Context, model *uniqueid.UniqueID) error {
+ return d.db.WithContext(ctx).Create(model).Error
+}
+
+// Edit implements uniqueid.UniqueIDStorer.
+func (d UniqueID) Edit(ctx context.Context, model *uniqueid.UniqueID, changeFn func(*uniqueid.UniqueID), opts ...orm.QueryOption) error {
+ return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
+}
+
+// Del implements uniqueid.UniqueIDStorer.
+func (d UniqueID) Del(ctx context.Context, model *uniqueid.UniqueID, opts ...orm.QueryOption) error {
+ return orm.DeleteWithContext(ctx, d.db, model, opts...)
+}
+
+func (d UniqueID) Session(ctx context.Context, changeFns ...func(*gorm.DB) error) error {
+ return d.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
+ for _, fn := range changeFns {
+ if err := fn(tx); err != nil {
+ return err
+ }
+ }
+ return nil
+ })
+}
+
+func (d UniqueID) EditWithSession(tx *gorm.DB, model *uniqueid.UniqueID, changeFn func(b *uniqueid.UniqueID) error, opts ...orm.QueryOption) error {
+ return orm.UpdateWithSession(tx, model, changeFn, opts...)
+}
diff --git a/domain/uniqueid/unique_id.go b/domain/uniqueid/unique_id.go
new file mode 100644
index 0000000..931a1be
--- /dev/null
+++ b/domain/uniqueid/unique_id.go
@@ -0,0 +1,17 @@
+// Code generated by godddx, DO AVOID EDIT.
+package uniqueid
+
+import (
+ "context"
+
+ "git.lnton.com/lnton/pkg/orm"
+)
+
+// UniqueIDStorer Instantiation interface
+type UniqueIDStorer interface {
+ Find(context.Context, *[]*UniqueID, orm.Pager, ...orm.QueryOption) (int64, error)
+ Get(context.Context, *UniqueID, ...orm.QueryOption) error
+ Add(context.Context, *UniqueID) error
+ Edit(context.Context, *UniqueID, func(*UniqueID), ...orm.QueryOption) error
+ Del(context.Context, *UniqueID, ...orm.QueryOption) error
+}
diff --git a/domain/uniqueid/unique_id.model.go b/domain/uniqueid/unique_id.model.go
new file mode 100644
index 0000000..b8eeb5e
--- /dev/null
+++ b/domain/uniqueid/unique_id.model.go
@@ -0,0 +1,15 @@
+// Code generated by godddx, DO AVOID EDIT.
+package uniqueid
+
+import "git.lnton.com/lnton/pkg/orm"
+
+// UniqueID domain model
+type UniqueID struct {
+ ID string `gorm:"primaryKey" json:"id"` // 唯一 id
+ CreatedAt orm.Time `gorm:"column:created_at;notNull;default:CURRENT_TIMESTAMP;comment:创建时间" json:"created_at"` // 创建时间
+}
+
+// TableName database table name
+func (*UniqueID) TableName() string {
+ return "unique_ids"
+}
diff --git a/domain/version/core.go b/domain/version/core.go
new file mode 100644
index 0000000..f880579
--- /dev/null
+++ b/domain/version/core.go
@@ -0,0 +1,72 @@
+package version
+
+import (
+ "fmt"
+ "strings"
+)
+
+// Storer ...
+type Storer interface {
+ First(*Version) error
+ Add(*Version) error
+}
+
+// Core 控制程序启动时是否执行表迁移
+// 每次都执行启动太慢了
+type Core struct {
+ store Storer
+ IsMigrate *bool
+}
+
+// NewCore ...
+func NewCore(store Storer) Core {
+ var isMigrate bool
+ return Core{
+ store: store,
+ IsMigrate: &isMigrate,
+ }
+}
+
+// IsAutoMigrate 是否需要进行表迁移?
+func (c Core) IsAutoMigrate(currentVer string) bool {
+ var ver Version
+ if err := c.store.First(&ver); err != nil {
+ isMigrate := true
+ *c.IsMigrate = isMigrate
+ return isMigrate
+ }
+ isMigrate := compareVersionFunc(currentVer, ver.Version, func(a, b string) bool {
+ return a > b
+ })
+ *c.IsMigrate = isMigrate
+ return isMigrate
+}
+
+// RecordVersion 记录当前版本号
+func (c Core) RecordVersion(currentVer, remark string) error {
+ var ver Version
+ ver.Version = currentVer
+ ver.Remark = remark
+ return c.store.Add(&ver)
+}
+
+func compareVersionFunc(a, b string, f func(a, b string) bool) bool {
+ s1 := versionToStr(a)
+ s2 := versionToStr(b)
+ if len(s1) != len(s2) {
+ return true
+ }
+ return f(s1, s2)
+}
+
+func versionToStr(str string) string {
+ var result strings.Builder
+ arr := strings.Split(strings.TrimPrefix(str, "v"), ".")
+ for _, item := range arr {
+ if idx := strings.Index(item, "-"); idx != -1 {
+ item = item[0:idx]
+ }
+ result.WriteString(fmt.Sprintf("%03s", item))
+ }
+ return result.String()
+}
diff --git a/domain/version/model.go b/domain/version/model.go
new file mode 100644
index 0000000..b4010f0
--- /dev/null
+++ b/domain/version/model.go
@@ -0,0 +1,17 @@
+package version
+
+import "git.lnton.com/lnton/pkg/orm"
+
+// Version 数据库版本记录
+// 每次迁移,则创建一条记录
+// 每次程序启动,则按 id 倒序获取最后一条记录,当小于硬编码的版本号时,进行数据库迁移
+type Version struct {
+ orm.Model
+ Version string // 版本
+ Remark string // 迁移说明
+}
+
+// TableName ...
+func (*Version) TableName() string {
+ return "versions"
+}
diff --git a/domain/version/store/versiondb/versiondb.go b/domain/version/store/versiondb/versiondb.go
new file mode 100644
index 0000000..be4115a
--- /dev/null
+++ b/domain/version/store/versiondb/versiondb.go
@@ -0,0 +1,40 @@
+package versiondb
+
+import (
+ "easyvqd/domain/version"
+
+ "gorm.io/gorm"
+)
+
+// DB ...
+type DB struct {
+ db *gorm.DB
+}
+
+// NewDB ...
+func NewDB(db *gorm.DB) DB {
+ return DB{db: db}
+}
+
+// AutoMigrate ...
+func (d DB) AutoMigrate(ok bool) DB {
+ if !ok {
+ return d
+ }
+ if err := d.db.AutoMigrate(
+ new(version.Version),
+ ); err != nil {
+ panic(err)
+ }
+ return d
+}
+
+// First ...
+func (d DB) First(v *version.Version) error {
+ return d.db.Order("id DESC").First(v).Error
+}
+
+// Add ...
+func (d DB) Add(v *version.Version) error {
+ return d.db.Create(v).Error
+}
diff --git a/domain/version/versionapi/api.go b/domain/version/versionapi/api.go
new file mode 100644
index 0000000..367fa84
--- /dev/null
+++ b/domain/version/versionapi/api.go
@@ -0,0 +1,30 @@
+package versionapi
+
+import (
+ "log/slog"
+
+ "easyvqd/domain/version"
+ "easyvqd/domain/version/store/versiondb"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "gorm.io/gorm"
+)
+
+// 通过修改版本号,来控制是否执行表迁移
+var (
+ DBVersion = "0.0.20"
+ DBRemark = "增加同步记录表"
+)
+
+// NewVersionCore ...
+func NewVersionCore(db *gorm.DB) version.Core {
+ vdb := versiondb.NewDB(db)
+ core := version.NewCore(vdb)
+ isOK := core.IsAutoMigrate(DBVersion)
+ vdb.AutoMigrate(isOK)
+ if isOK {
+ slog.Info("更新数据库表结构")
+ }
+ orm.SetEnabledAutoMigrate(isOK)
+ return core
+}
diff --git a/domain/version/versionapi/version.go b/domain/version/versionapi/version.go
new file mode 100644
index 0000000..dfa8814
--- /dev/null
+++ b/domain/version/versionapi/version.go
@@ -0,0 +1,40 @@
+package versionapi
+
+import (
+ "easyvqd/domain/version"
+ "log/slog"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "git.lnton.com/lnton/pkg/web"
+ "github.com/gin-gonic/gin"
+)
+
+type API struct {
+ versionCore version.Core
+}
+
+func New(ver version.Core) API {
+ return API{versionCore: ver}
+}
+
+func Register(r gin.IRouter, verAPI API, handler ...gin.HandlerFunc) {
+ {
+ group := r.Group("/version", handler...)
+ group.GET("", web.WrapH(verAPI.getVersion))
+ }
+}
+
+func (v API) getVersion(_ *gin.Context, _ *struct{}) (any, error) {
+ return gin.H{"version": DBVersion, "remark": DBRemark}, nil
+}
+
+// RecordVersion 更新版本号,错误仅记录日志,不建议上层处理
+func (v API) RecordVersion() {
+ // 如果没有执行表迁移,则不需要更新版本号
+ if !orm.GetEnabledAutoMigrate() {
+ return
+ }
+ if err := v.versionCore.RecordVersion(DBVersion, DBRemark); err != nil {
+ slog.Error("RecordVersion", "err", err)
+ }
+}
diff --git a/extensions.json b/extensions.json
new file mode 100644
index 0000000..45da842
--- /dev/null
+++ b/extensions.json
@@ -0,0 +1,37 @@
+{
+ "name": "sync_record",
+ "display_name": "SyncRecord",
+ "icon": "http://212.64.34.165/SyncRecord.ico",
+ "version": "0.1.0",
+ "updated_at": "2025-10-29",
+ "engines": {
+ "easynvr": "v3.7.78"
+ },
+ "description": "支持按通道按天数进行文件备份,支持SMB、NFS、网盘等挂载目录备份,即将开放S3协议备份",
+ "author": "TSINGSEE",
+ "website": "https://www.tsingsee.com",
+ "package": {
+ "windows_amd64": {
+ "url": "https://easy.xxrb.com.cn:18443/EasyNVR/easyntd_windows_amd64.zip",
+ "size": 7969177
+ },
+ "linux_amd64": {
+ "url": "http://212.64.34.165/syncrecord-linux-amd64-v0.0.50-a88817a-251029.zip",
+ "size": 7864320
+ },
+ "linux_arm64": {
+ "url": "https://easy.xxrb.com.cn:18443/EasyNVR/easyntd_linux_arm64.zip",
+ "size": 6606028
+ }
+ },
+ "features": [
+ ],
+ "auto_update": false,
+ "homepage": "",
+ "activation": "platform",
+ "is_free": true,
+ "free_start_ms": 0,
+ "free_end_ms": 0,
+ "free_days": 10,
+ "offer": []
+}
\ No newline at end of file
diff --git a/go.mod b/go.mod
new file mode 100644
index 0000000..99b7ed3
--- /dev/null
+++ b/go.mod
@@ -0,0 +1,97 @@
+module easyvqd
+
+go 1.24.0
+
+require (
+ git.lnton.com/lnton/pkg v1.5.27
+ github.com/gin-contrib/static v1.1.5
+ github.com/gin-gonic/gin v1.11.0
+ github.com/google/uuid v1.6.0
+ github.com/google/wire v0.7.0
+ github.com/gorilla/websocket v1.5.3
+ github.com/ixugo/goddd v1.4.0
+ github.com/jinzhu/copier v0.4.0
+ github.com/lib/pq v1.10.9
+ github.com/pelletier/go-toml/v2 v2.2.4
+ github.com/pion/rtp v1.9.0
+ github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300
+ github.com/youpy/go-wav v0.3.2
+ gorm.io/gorm v1.31.0
+)
+
+require (
+ github.com/bytedance/gopkg v0.1.3 // indirect
+ github.com/bytedance/sonic v1.14.1 // indirect
+ github.com/bytedance/sonic/loader v0.3.0 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/ebitengine/purego v0.8.4 // indirect
+ github.com/glebarez/go-sqlite v1.22.0 // indirect
+ github.com/go-ole/go-ole v1.2.6 // indirect
+ github.com/goccy/go-yaml v1.18.0 // indirect
+ github.com/golang-jwt/jwt/v4 v4.5.2 // indirect
+ github.com/ixugo/netpulse v0.1.1 // indirect
+ github.com/jackc/pgpassfile v1.0.0 // indirect
+ github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
+ github.com/jackc/pgx/v5 v5.7.2 // indirect
+ github.com/jackc/puddle/v2 v2.2.2 // indirect
+ github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
+ github.com/panjf2000/ants/v2 v2.11.3 // indirect
+ github.com/pion/randutil v0.1.0 // indirect
+ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
+ github.com/quic-go/qpack v0.5.1 // indirect
+ github.com/quic-go/quic-go v0.55.0 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/shirou/gopsutil/v4 v4.25.7 // indirect
+ github.com/youpy/go-riff v0.1.0 // indirect
+ github.com/yusufpapurcu/wmi v1.2.4 // indirect
+ github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b // indirect
+ go.uber.org/zap v1.27.0 // indirect
+ go.uber.org/zap/exp v0.3.0 // indirect
+ golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b // indirect
+ golang.org/x/mod v0.29.0 // indirect
+ golang.org/x/net v0.46.0 // indirect
+ golang.org/x/sync v0.17.0 // indirect
+ golang.org/x/text v0.30.0 // indirect
+ golang.org/x/time v0.14.0 // indirect
+ golang.org/x/tools v0.38.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 // indirect
+ google.golang.org/grpc v1.75.1 // indirect
+ gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
+ modernc.org/libc v1.61.5 // indirect
+ modernc.org/mathutil v1.7.1 // indirect
+ modernc.org/memory v1.8.0 // indirect
+ modernc.org/sqlite v1.34.4 // indirect
+)
+
+require (
+ git.lnton.com/lnton/pluginsdk v0.0.0-20251009073251-48c71dab3f4a
+ github.com/cloudwego/base64x v0.1.6 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.10 // indirect
+ github.com/gin-contrib/sse v1.1.0 // indirect
+ github.com/glebarez/sqlite v1.11.0
+ github.com/go-playground/locales v0.14.1 // indirect
+ github.com/go-playground/universal-translator v0.18.1 // indirect
+ github.com/go-playground/validator/v10 v10.28.0 // indirect
+ github.com/goccy/go-json v0.10.5 // indirect
+ github.com/jinzhu/inflection v1.0.0 // indirect
+ github.com/jinzhu/now v1.1.5 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/klauspost/cpuid/v2 v2.3.0 // indirect
+ github.com/leodido/go-urn v1.4.0 // indirect
+ github.com/lestrrat-go/strftime v1.1.1 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pkg/errors v0.9.1 // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.3.0 // indirect
+ go.uber.org/multierr v1.11.0 // indirect
+ golang.org/x/arch v0.22.0 // indirect
+ golang.org/x/crypto v0.43.0 // indirect
+ golang.org/x/sys v0.37.0 // indirect
+ google.golang.org/protobuf v1.36.10 // indirect
+ gorm.io/driver/postgres v1.5.11
+)
+
+replace git.lnton.com/lnton/pluginsdk => ../pluginsdk
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..4b7af0b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,243 @@
+git.lnton.com/lnton/pkg v1.5.27 h1:pf4QqL00/yrGtVUdaHo7eZ941D6B9q5oX4ffnjAYWI4=
+git.lnton.com/lnton/pkg v1.5.27/go.mod h1:+xvqNpqlxuRthZHiuaMg5Spf5yIgRE63KrmOFLmlp3E=
+github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
+github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
+github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
+github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
+github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
+github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
+github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
+github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
+github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
+github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
+github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
+github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
+github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
+github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4=
+github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM=
+github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
+github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
+github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
+github.com/glebarez/go-sqlite v1.22.0/go.mod h1:PlBIdHe0+aUEFn+r2/uthrWq4FxbzugL0L8Li6yQJbc=
+github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GMw=
+github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
+github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
+github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
+github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
+github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
+github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
+github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
+github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
+github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
+github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
+github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
+github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
+github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
+github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
+github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
+github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
+github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
+github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
+github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
+github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI=
+github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0=
+github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
+github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
+github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
+github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/google/wire v0.7.0 h1:JxUKI6+CVBgCO2WToKy/nQk0sS+amI9z9EjVmdaocj4=
+github.com/google/wire v0.7.0/go.mod h1:n6YbUQD9cPKTnHXEBN2DXlOp/mVADhVErcMFb0v3J18=
+github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
+github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/ixugo/goddd v1.4.0 h1:2SMO7wfqVYuZu5KwdvN6aLk1cOY9MgxcJAhClH1yhMQ=
+github.com/ixugo/goddd v1.4.0/go.mod h1:FzEjEd6uWEWan1XWTh8VXdqGtyjMYGow/URNtBY8X7w=
+github.com/ixugo/netpulse v0.1.1 h1:M7pdwJhpSDuwFdjEgCcanR5lLZgd+4akOstgbyRZOgw=
+github.com/ixugo/netpulse v0.1.1/go.mod h1:vH0zFyVMxDkz8jVHtI9/2oEb7npi/5+eSIx5RzHkN4g=
+github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
+github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo=
+github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM=
+github.com/jackc/pgx/v5 v5.7.2 h1:mLoDLV6sonKlvjIEsV56SkWNCnuNv531l94GaIzO+XI=
+github.com/jackc/pgx/v5 v5.7.2/go.mod h1:ncY89UGWxg82EykZUwSpUKEfccBGGYq1xjrOpsbsfGQ=
+github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
+github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
+github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
+github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
+github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
+github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
+github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
+github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
+github.com/jonboulle/clockwork v0.5.0 h1:Hyh9A8u51kptdkR+cqRpT1EebBwTn1oK9YfGYbdFz6I=
+github.com/jonboulle/clockwork v0.5.0/go.mod h1:3mZlmanh0g2NDKO5TWZVJAfofYk64M7XN3SzBPjZF60=
+github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
+github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
+github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
+github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
+github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
+github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc h1:RKf14vYWi2ttpEmkA4aQ3j4u9dStX2t4M8UM6qqNsG8=
+github.com/lestrrat-go/envload v0.0.0-20180220234015-a3eb8ddeffcc/go.mod h1:kopuH9ugFRkIXf3YoqHKyrJ9YfUFsckUU9S7B+XP+is=
+github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible h1:Y6sqxHMyB1D2YSzWkLibYKgg+SwmyFU9dF2hn6MdTj4=
+github.com/lestrrat-go/file-rotatelogs v2.4.0+incompatible/go.mod h1:ZQnN8lSECaebrkQytbHj4xNgtg8CR7RYXnPok8e0EHA=
+github.com/lestrrat-go/strftime v1.1.1 h1:zgf8QCsgj27GlKBy3SU9/8MMgegZ8UCzlCyHYrUF0QU=
+github.com/lestrrat-go/strftime v1.1.1/go.mod h1:YDrzHJAODYQ+xxvrn5SG01uFIQAeDTzpxNVppCz7Nmw=
+github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
+github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
+github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
+github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
+github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/panjf2000/ants/v2 v2.11.3 h1:AfI0ngBoXJmYOpDh9m516vjqoUu2sLrIVgppI9TZVpg=
+github.com/panjf2000/ants/v2 v2.11.3/go.mod h1:8u92CYMUc6gyvTIw8Ru7Mt7+/ESnJahz5EVtqfrilek=
+github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
+github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
+github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
+github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
+github.com/pion/rtp v1.9.0 h1:NL2nGZPXhjnTQGRgsDZRv0ZTo0Or5fkjCy9o9PtBHBU=
+github.com/pion/rtp v1.9.0/go.mod h1:rF5nS1GqbR7H/TCpKwylzeq6yDM+MM6k+On5EgeThEM=
+github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
+github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
+github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
+github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
+github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
+github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
+github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
+github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
+github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
+github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300 h1:XQdibLKagjdevRB6vAjVY4qbSr8rQ610YzTkWcxzxSI=
+github.com/tcolgate/mp3 v0.0.0-20170426193717-e79c5a46d300/go.mod h1:FNa/dfN95vAYCNFrIKRrlRo+MBLbwmR9Asa5f2ljmBI=
+github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
+github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
+github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
+github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
+github.com/youpy/go-riff v0.1.0 h1:vZO/37nI4tIET8tQI0Qn0Y79qQh99aEpponTPiPut7k=
+github.com/youpy/go-riff v0.1.0/go.mod h1:83nxdDV4Z9RzrTut9losK7ve4hUnxUR8ASSz4BsKXwQ=
+github.com/youpy/go-wav v0.3.2 h1:NLM8L/7yZ0Bntadw/0h95OyUsen+DQIVf9gay+SUsMU=
+github.com/youpy/go-wav v0.3.2/go.mod h1:0FCieAXAeSdcxFfwLpRuEo0PFmAoc+8NU34h7TUvk50=
+github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
+github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
+github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b h1:QqixIpc5WFIqTLxB3Hq8qs0qImAgBdq0p6rq2Qdl634=
+github.com/zaf/g711 v0.0.0-20190814101024-76a4a538f52b/go.mod h1:T2h1zV50R/q0CVYnsQOQ6L7P4a2ZxH47ixWcMXFGyx8=
+go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
+go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
+go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
+go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
+go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
+go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
+go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
+go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
+go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
+go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
+go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
+go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
+go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
+go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
+go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
+go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
+go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
+go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
+go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8=
+go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
+go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
+go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
+golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
+golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
+golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
+golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
+golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b h1:18qgiDvlvH7kk8Ioa8Ov+K6xCi0GMvmGfGW0sgd/SYA=
+golang.org/x/exp v0.0.0-20251009144603-d2f985daa21b/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70=
+golang.org/x/mod v0.29.0 h1:HV8lRxZC4l2cr3Zq1LvtOsi/ThTgWnUk/y64QSs8GwA=
+golang.org/x/mod v0.29.0/go.mod h1:NyhrlYXJ2H4eJiRy/WDBO6HMqZQ6q9nk4JzS3NuCK+w=
+golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4=
+golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210=
+golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
+golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
+golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
+golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
+golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k=
+golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM=
+golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
+golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
+golang.org/x/tools v0.38.0 h1:Hx2Xv8hISq8Lm16jvBZ2VQf+RLmbd7wVUsALibYI/IQ=
+golang.org/x/tools v0.38.0/go.mod h1:yEsQ/d/YK8cjh0L6rZlY8tgtlKiBNTL14pGDJPJpYQs=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
+gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7 h1:pFyd6EwwL2TqFf8emdthzeX+gZE1ElRq3iM8pui4KBY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250707201910-8d1bb00bc6a7/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
+google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
+google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
+google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
+google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
+gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
+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/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gorm.io/driver/postgres v1.5.11 h1:ubBVAfbKEUld/twyKZ0IYn9rSQh448EdelLYk9Mv314=
+gorm.io/driver/postgres v1.5.11/go.mod h1:DX3GReXH+3FPWGrrgffdvCk3DQ1dwDPdmbenSkweRGI=
+gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
+gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
+gotest.tools v2.2.0+incompatible h1:VsBPFP1AI068pPrMxtb/S8Zkgf9xEmTLJjfM+P5UIEo=
+gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
+modernc.org/cc/v4 v4.24.1 h1:mLykA8iIlZ/SZbwI2JgYIURXQMSgmOb/+5jaielxPi4=
+modernc.org/cc/v4 v4.24.1/go.mod h1:T1lKJZhXIi2VSqGBiB4LIbKs9NsKTbUXj4IDrmGqtTI=
+modernc.org/ccgo/v4 v4.23.5 h1:6uAwu8u3pnla3l/+UVUrDDO1HIGxHTYmFH6w+X9nsyw=
+modernc.org/ccgo/v4 v4.23.5/go.mod h1:FogrWfBdzqLWm1ku6cfr4IzEFouq2fSAPf6aSAHdAJQ=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.6.0 h1:Tiw3pezQj7PfV8k4Dzyu/vhRHR2e92kOXtTFU8pbCl4=
+modernc.org/gc/v2 v2.6.0/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.61.5 h1:WzsPUvWl2CvsRmk2foyWWHUEUmQ2iW4oFyWOVR0O5ho=
+modernc.org/libc v1.61.5/go.mod h1:llBdEGIywhnRgAFuTF+CWaKV8/2bFgACcQZTXhkAuAM=
+modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
+modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.34.4 h1:sjdARozcL5KJBvYQvLlZEmctRgW9xqIZc2ncN7PU0P8=
+modernc.org/sqlite v1.34.4/go.mod h1:3QQFCG2SEMtc2nv+Wq4cQCH7Hjcg+p/RMlS1XK+zwbk=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/internal/app/app.go b/internal/app/app.go
new file mode 100644
index 0000000..cc15063
--- /dev/null
+++ b/internal/app/app.go
@@ -0,0 +1,90 @@
+package app
+
+import (
+ "easyvqd/internal/core/host"
+ "fmt"
+ "log/slog"
+ "net"
+ "os"
+ "os/signal"
+ "path/filepath"
+ "strconv"
+ "syscall"
+
+ "easyvqd/internal/conf"
+
+ "git.lnton.com/lnton/pkg/logger"
+ "git.lnton.com/lnton/pkg/server"
+ "git.lnton.com/lnton/pkg/system"
+)
+
+func Run(bc *conf.Bootstrap) {
+ // 以可执行文件所在目录为工作目录,防止以服务方式运行时,工作目录切换到其它位置
+ bin, _ := os.Executable()
+ if err := os.Chdir(filepath.Dir(bin)); err != nil {
+ slog.Error("change work dir fail", "err", err)
+ }
+
+ log, clean := SetupLog(bc)
+ defer clean()
+
+ handler, cleanUp, err := WireApp(bc, log)
+ if err != nil {
+ slog.Error("程序构建失败", "err", err.Error())
+ panic(err)
+ }
+ defer cleanUp()
+
+ var svc *server.Server
+ if bc.Server.HTTP.Port != 0 {
+ svc = server.New(handler,
+ server.Port(strconv.Itoa(bc.Server.HTTP.Port)),
+ server.ReadTimeout(bc.Server.HTTP.Timeout.Duration()),
+ server.WriteTimeout(bc.Server.HTTP.Timeout.Duration()),
+ )
+ host.StartOk <- struct{}{} // http端口已监听,开始心跳保活
+ go svc.Start()
+ } else {
+ svc = server.New(handler,
+ server.ReadTimeout(bc.Server.HTTP.Timeout.Duration()),
+ server.WriteTimeout(bc.Server.HTTP.Timeout.Duration()),
+ )
+ lis, err := net.Listen("tcp", ":8089")
+ if err != nil {
+ fmt.Printf("创建监听器失败: %v\n", err)
+ return
+ }
+ //bc.Server.HTTP.Port = lis.Addr().(*net.TCPAddr).Port
+ bc.Server.HTTP.Port = lis.Addr().(*net.TCPAddr).Port
+ host.StartOk <- struct{}{} // http端口已监听,开始心跳保活
+ go svc.StartWithListen(lis)
+ }
+
+ interrupt := make(chan os.Signal, 1)
+ signal.Notify(interrupt, syscall.SIGINT, syscall.SIGTERM)
+ fmt.Println("服务启动成功 port:", bc.Server.HTTP.Port)
+
+ select {
+ case s := <-interrupt:
+ slog.Info(`<-interrupt`, "signal", s.String())
+ case err := <-svc.Notify():
+ system.ErrPrintf("err: %s\n", err.Error())
+ slog.Error(`<-server.Notify()`, "err", err)
+ }
+ if err := svc.Shutdown(); err != nil {
+ slog.Error(`server.Shutdown()`, "err", err)
+ }
+}
+
+// SetupLog 初始化日志
+func SetupLog(bc *conf.Bootstrap) (*slog.Logger, func()) {
+ logDir := filepath.Join(system.Getwd(), bc.Log.Dir)
+ return logger.SetupSlog(logger.Config{
+ Dir: logDir, // 日志地址
+ Debug: bc.Debug, // 服务级别Debug/Release
+ MaxAge: bc.Log.MaxAge.Duration(), // 日志存储时间
+ RotationTime: bc.Log.RotationTime.Duration(), // 循环时间
+ RotationSize: bc.Log.RotationSize * 1024 * 1024, // 循环大小
+ Level: bc.Log.Level, // 日志级别
+ })
+}
diff --git a/internal/app/wire.go b/internal/app/wire.go
new file mode 100644
index 0000000..6503540
--- /dev/null
+++ b/internal/app/wire.go
@@ -0,0 +1,18 @@
+//go:build wireinject
+// +build wireinject
+
+package app
+
+import (
+ "log/slog"
+ "net/http"
+
+ "easyvqd/internal/conf"
+ "easyvqd/internal/data"
+ "easyvqd/internal/web/api"
+ "github.com/google/wire"
+)
+
+func WireApp(bc *conf.Bootstrap, log *slog.Logger) (http.Handler, func(), error) {
+ panic(wire.Build(data.ProviderSet, api.ProviderVersionSet, api.ProviderSet))
+}
diff --git a/internal/app/wire_gen.go b/internal/app/wire_gen.go
new file mode 100644
index 0000000..a070da9
--- /dev/null
+++ b/internal/app/wire_gen.go
@@ -0,0 +1,50 @@
+// Code generated by Wire. DO NOT EDIT.
+
+//go:generate go run -mod=mod github.com/google/wire/cmd/wire
+//go:build !wireinject
+// +build !wireinject
+
+package app
+
+import (
+ "easyvqd/domain/version/versionapi"
+ "easyvqd/internal/conf"
+ "easyvqd/internal/core/host"
+ "easyvqd/internal/core/media"
+ "easyvqd/internal/core/vqdsdk"
+ "easyvqd/internal/data"
+ "easyvqd/internal/web/api"
+ "log/slog"
+ "net/http"
+)
+
+// Injectors from wire.go:
+
+func WireApp(bc *conf.Bootstrap, log *slog.Logger) (http.Handler, func(), error) {
+ db, err := data.SetupDB(bc, log)
+ if err != nil {
+ return nil, nil, err
+ }
+ core := versionapi.NewVersionCore(db)
+ versionapiAPI := versionapi.New(core)
+ vqdTaskCore := api.NewVqdTaskCore(db)
+ hostCore := host.NewCore(bc)
+ mediaCore := media.NewCore(hostCore)
+ vqdSdkCore := vqdsdk.NewCore(hostCore, vqdTaskCore)
+ vqdTaskAPI := api.NewVqdTaskAPI(vqdTaskCore, mediaCore,vqdSdkCore,hostCore, bc)
+
+ usecase := &api.Usecase{
+ Conf: bc,
+ DB: db,
+ Version: versionapiAPI,
+ VqdTaskCore: vqdTaskCore,
+ VqdTaskAPI: vqdTaskAPI,
+ HostCore: hostCore,
+ MediaCore: mediaCore,
+ VqdSdkCore: vqdSdkCore,
+ }
+ handler := api.NewHTTPHandler(usecase)
+ return handler, func() {
+ //mediaCore.Close()
+ }, nil
+}
diff --git a/internal/conf/config.go b/internal/conf/config.go
new file mode 100644
index 0000000..69ed3f3
--- /dev/null
+++ b/internal/conf/config.go
@@ -0,0 +1,179 @@
+package conf
+
+import (
+ "path/filepath"
+ "time"
+)
+
+type Bootstrap struct {
+ Debug bool `toml:"-" json:"-"`
+ BuildVersion string `toml:"-" json:"-"`
+ ConfigDir string `toml:"-" json:"-"`
+ ConfigPath string `toml:"-" json:"-"`
+
+ Server Server // 服务器
+ Data Data // 数据
+ Plugin Plugin
+ VqdConfig VqdConfig // 诊断基础配置
+ VqdLgtDark VqdLgtDark // 亮度检测
+ VqdBlue VqdBlue // 蓝屏检查
+ VqdClarity VqdClarity // 清晰度检查
+ VqdShark VqdShark // 抖动检查
+ VqdFreeze VqdFreeze // 冻结检测
+ VqdColor VqdColor // 偏色检测
+ VqdOcclusion VqdOcclusion // 遮挡检测
+ VqdNoise VqdNoise // 噪声检测
+ VqdContrast VqdContrast // 对比度检测
+ VqdMosaic VqdMosaic // 马赛克检测
+ VqdFlower VqdFlower // 花屏检测
+ Log Log // 日志
+}
+
+type Plugin struct {
+ HttpAPI string `json:"http_api" comment:"http 地址"`
+ // Enable bool `json:"enable" comment:"是否开启"`
+ GrpcPort int `json:"port" comment:"通信端口"`
+ AllDebug bool `json:"all_debug" comment:"是否开启"`
+}
+
+type Server struct {
+ HTTP ServerHTTP `comment:"对外提供的服务,建议由 nginx 代理"` // HTTP服务器
+}
+
+type ServerHTTP struct {
+ Port int `comment:"http 端口"` // 服务器端口号
+ Timeout Duration `comment:"请求超时时间"` // 请求超时时间
+ JwtSecret string `comment:"jwt 秘钥,空串时,每次启动程序将随机赋值"` // JWT密钥
+ PProf ServerPPROF // Pprof配置
+}
+
+// ServerPPROF 结构体,包含 Enabled 和 AccessIps 两个字段
+type ServerPPROF struct {
+ Enabled bool `comment:"是否启用 pprof, 建议设置为 true"` // 是否启用
+ AccessIps []string `comment:"访问白名单" json:"access_ips"` // 允许访问的IP地址列表
+}
+
+// Data 结构体,包含 Database 和 Redis 两个字段
+type Data struct {
+ // Database 数据库
+ Database Database `comment:"数据库支持 sqlite 和 postgres 两种,使用 sqlite 时 dsn 应当填写文件存储路径"`
+ // Redis Redis数据库
+ // Redis DataRedis
+}
+
+// Database 结构体,包含 Dsn、MaxIdleConns、MaxOpenConns、ConnMaxLifetime 和 SlowThreshold 五个字段
+type Database struct {
+ Dsn string // 数据源名称
+ MaxIdleConns int32 // 最大空闲连接数
+ MaxOpenConns int32 // 最大打开连接数
+ ConnMaxLifetime Duration // 连接最大生命周期
+ SlowThreshold Duration // 慢查询阈值
+}
+
+// Log 结构体,包含 Dir、Level、MaxAge、RotationTime 和 RotationSize 五个字段
+type Log struct {
+ Dir string `comment:"日志存储目录,不能使用特殊符号"`
+ Level string `comment:"记录级别 debug/info/warn/error"`
+ MaxAge Duration `comment:"保留日志多久,超过时间自动删除"`
+ RotationTime Duration `comment:"多久时间,分割一个新的日志文件"`
+ RotationSize int64 `comment:"多大文件,分割一个新的日志文件(MB)"`
+}
+
+func (s *Bootstrap) ConfigDirPath() string {
+ return filepath.Join(s.ConfigDir, "config.toml")
+}
+
+type Duration time.Duration
+
+func (d *Duration) UnmarshalText(b []byte) error {
+ x, err := time.ParseDuration(string(b))
+ if err != nil {
+ return err
+ }
+ *d = Duration(x)
+ return nil
+}
+
+func (d Duration) MarshalText() ([]byte, error) {
+ return []byte(d.Duration().String()), nil
+}
+
+func (d *Duration) Duration() time.Duration {
+ return time.Duration(*d)
+}
+
+// 基础配置
+type VqdConfig struct {
+ SaveDay int32 `json:"save_day" comment:"数据保存天数"`
+ FrmNum int32 `json:"frm_num" comment:"连续分析帧数(2-64), 默认为10, 最大为 64"`
+ IsDeepLearn bool `json:"is_deep_learn" comment:"是否使用深度学习版本, 默认使用深度学习版本"`
+}
+
+// 亮度检测
+type VqdLgtDark struct {
+ DarkThr float64 `json:"dark_thr" comment:"默认 0.4, 取值范围: 0~1, 建议范围: 0.2~0.6"`
+ LgtThr float64 `json:"lgt_thr" comment:"默认 0.1, 取值范围: 0~1, 建议范围: 0.1~0.5"`
+ LgtDarkAbnNumRatio float64 `json:"lgt_dark_abn_num_ratio" comment:"默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9"`
+}
+
+// 蓝屏检查
+type VqdBlue struct {
+ BlueThr float64 `json:"blue_thr" comment:"默认为 0.6, 取值范围: 0~1, 建议范围 0.4~0.9"`
+ BlueAbnNumRatio float64 `json:"blue_abn_num_ratio" comment:"默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9"`
+}
+
+// 清晰度检查
+type VqdClarity struct {
+ ClarityThr float64 `json:"clarity_thr" comment:"默认为0.4, 取值范围: 0~1, 建议范围: 0.3~0.99"`
+ ClarityAbnNumRatio float64 `json:"clarity_abn_num_ratio" comment:"默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9"`
+}
+
+// 抖动检查
+type VqdShark struct {
+ SharkThr float64 `json:"shark_thr" comment:"默认为 0.2, 取值范围: 0~1, 建议范围: 0.1~0.8"`
+ SharkAbnNumRatio float64 `json:"shark_abn_num_ratio" comment:"默认为0.2, 取值范围: 0~1, 建议范围: 0.1~0.6"`
+}
+
+// 冻结检测
+type VqdFreeze struct {
+ FreezeThr float64 `json:"freeze_thr" comment:"默认 0.4, 取值范围: 0~1, 建议范围: 0.2~0.6"`
+ FreezeAbnNumRatio float64 `json:"freeze_abn_num_ratio" comment:"默认为0.99, 取值范围: 0.8~1, 建议范围: 0.95~1"`
+}
+
+// 偏色检测
+type VqdColor struct {
+ ColorThr float64 `json:"color_thr" comment:"默认为0.18, 取值范围: 0~1, 建议范围: 0.1~0.5"`
+ ColorAbnNumRatio float64 `json:"color_abn_num_ratio" comment:"默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9"`
+}
+
+// 遮挡检测
+type VqdOcclusion struct {
+ OcclusionThr float64 `json:"occlusion_thr" comment:"默认为0.1, 取值范围: 0~1, 建议范围: 0.05~0.5"`
+ OcclusionAbnNumRatio float64 `json:"occlusion_abn_num_ratio" comment:"默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9"`
+}
+
+// 噪声检测
+type VqdNoise struct {
+ NoiseThr float64 `json:"noise_thr" comment:"默认为 0.3, 取值范围: 0~1, 建议范围: 0.2~0.8"`
+ NoiseAbnNumRatio float64 `json:"noise_abn_num_ratio" comment:"默认为0.6, 取值范围: 0~1, 建议范围: 0.3~0.9"`
+}
+
+// 对比度检测
+type VqdContrast struct {
+ CtraLowThr float64 `json:"ctra_low_thr" comment:"默认为 0.2, 取值范围: 0~1, 建议范围: 0.1~0.3"`
+ CtraHighThr float64 `json:"ctra_high_thr" comment:"默认为 0.8, 取值范围: 0~1, 建议范围: 0.7~0.9"`
+ CtraAbnNumRatio float64 `json:"ctra_abn_num_ratio" comment:"默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9"`
+}
+
+// 马赛克检测
+type VqdMosaic struct {
+ MosaicThr float64 `json:"mosaic_thr" comment:"默认为 0.1 取值范围: 0~1, 建议范围: 0.1~0.9"`
+ MosaicAbnNumRatio float64 `json:"mosaic_abn_num_ratio" comment:"默认为0.5,取值范围: 0~1, 建议范围: 0.3"`
+}
+
+// 花屏检测
+type VqdFlower struct {
+ FlowerThr float64 `json:"flower_thr" comment:"默认为 0.3 取值范围: 0~1, 建议范围: 0.1~0.9"`
+ FlowerAbnNumRatio float64 `json:"flower_abn_num_ratio" comment:"默认为0.6, 取值范围: 0~1, 建议范围: 0.3"`
+ MosaicThr float64 `json:"mosaic_thr" comment:"默认为 0.3 取值范围: 0~1, 建议范围: 0.1~0.9"`
+}
diff --git a/internal/conf/config_test.go b/internal/conf/config_test.go
new file mode 100644
index 0000000..86c8cec
--- /dev/null
+++ b/internal/conf/config_test.go
@@ -0,0 +1,23 @@
+package conf
+
+import (
+ "testing"
+)
+
+func TestWriteConfig(t *testing.T) {
+ if err := WriteConfig(Bootstrap{Server: Server{
+ HTTP: ServerHTTP{
+ Port: 8080,
+ },
+ }}, "test.toml"); err != nil {
+ t.Fatal(err)
+ }
+
+ if err := WriteConfig(Bootstrap{Server: Server{
+ HTTP: ServerHTTP{
+ Port: 8081,
+ },
+ }}, "test.toml"); err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/internal/conf/default.go b/internal/conf/default.go
new file mode 100644
index 0000000..f333b90
--- /dev/null
+++ b/internal/conf/default.go
@@ -0,0 +1,44 @@
+package conf
+
+import (
+ "time"
+
+ "git.lnton.com/lnton/pkg/orm"
+)
+
+func DefaultConfig() Bootstrap {
+ return Bootstrap{
+ Server: Server{
+ HTTP: ServerHTTP{
+ Port: 8080,
+ Timeout: Duration(30 * time.Second),
+ JwtSecret: orm.GenerateRandomString(32),
+ PProf: ServerPPROF{
+ Enabled: true,
+ AccessIps: []string{"::1", "127.0.0.1"},
+ },
+ },
+ },
+ Data: Data{
+ Database: Database{
+ Dsn: "./configs/data.db",
+ MaxIdleConns: 10,
+ MaxOpenConns: 50,
+ ConnMaxLifetime: Duration(6 * time.Hour),
+ SlowThreshold: Duration(200 * time.Millisecond),
+ },
+ },
+ Plugin: Plugin{
+ HttpAPI: "http://127.0.0.1:10000",
+ GrpcPort: 50051,
+ AllDebug: false,
+ },
+ Log: Log{
+ Dir: "./logs",
+ Level: "debug",
+ MaxAge: Duration(7 * 24 * time.Hour),
+ RotationTime: Duration(8 * time.Hour),
+ RotationSize: 50,
+ },
+ }
+}
diff --git a/internal/conf/unmarshal.go b/internal/conf/unmarshal.go
new file mode 100644
index 0000000..a576f6f
--- /dev/null
+++ b/internal/conf/unmarshal.go
@@ -0,0 +1,35 @@
+package conf
+
+import (
+ "os"
+
+ "github.com/pelletier/go-toml/v2"
+)
+
+// SetupConfig 从文件读取内容初始化配置
+func SetupConfig(v any, path string) error {
+ b, err := os.ReadFile(path)
+ if err != nil {
+ return err
+ }
+ return toml.Unmarshal(b, v)
+}
+
+// WriteConfig 将配置写回文件
+func WriteConfig(v any, path string) error {
+ tmp := path + ".tmp"
+ _ = os.RemoveAll(tmp)
+
+ f, err := os.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
+ if err != nil {
+ return err
+ }
+ defer f.Close()
+ if err := toml.NewEncoder(f).SetIndentTables(true).Encode(v); err != nil {
+ return err
+ }
+ if err := f.Close(); err != nil {
+ return err
+ }
+ return os.Rename(tmp, path)
+}
diff --git a/internal/core/host/channel.go b/internal/core/host/channel.go
new file mode 100644
index 0000000..73d2b41
--- /dev/null
+++ b/internal/core/host/channel.go
@@ -0,0 +1,24 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+)
+
+func (c Core) FindChannels(ctx context.Context, in *FindChannelsInput) (*FindChannelsOutput, error) {
+ marshal, err := json.Marshal(in)
+ if err != nil {
+ return nil, err
+ }
+ result, err := c.Plugin.CallHost("findChannels", marshal)
+
+ if err != nil {
+ return nil, err
+ }
+
+ out := FindChannelsOutput{}
+ if err = json.Unmarshal(result, &out); err != nil {
+ return nil, err
+ }
+ return &out, nil
+}
diff --git a/internal/core/host/channel.model.go b/internal/core/host/channel.model.go
new file mode 100644
index 0000000..ecea8f8
--- /dev/null
+++ b/internal/core/host/channel.model.go
@@ -0,0 +1,172 @@
+package host
+
+import (
+ "database/sql/driver"
+ "encoding"
+ "encoding/json"
+ "fmt"
+ "strings"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "github.com/lib/pq"
+ "gorm.io/gorm"
+)
+
+// Channel 通道
+type Channel struct {
+ ID string `gorm:"primaryKey;column:id" json:"id"` // ID
+ CreatedAt orm.Time `gorm:"type:timestamptz;notNull;default:CURRENT_TIMESTAMP;index;comment:创建时间" json:"created_at"`
+ UpdatedAt orm.Time `gorm:"type:timestamptz;notNull;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
+ Enabled bool `gorm:"column:enabled;notNull;default:true;comment:是否启用" json:"enabled"` // 是否启用
+ Name string `gorm:"column:name;notNull;default:'';comment:通道名称" json:"name"` // 通道名称
+ DeviceID string `gorm:"column:device_id;notNull;default:'';index;comment:设备ID" json:"device_id"` // 设备 id
+ Protocol string `gorm:"column:protocol;notNull;default:'';comment:通道协议" json:"protocol"` // 通道协议
+ PTZType int `gorm:"column:ptz_type;notNull;default:0;comment:云台类型" json:"ptz_type"` // 云台类型
+ Remark string `gorm:"column:remark;notNull;default:'';comment:备注" json:"remark"` // 备注描述
+ Transport string `gorm:"column:transport;notNull;default:'TCP';comment:传输协议" json:"transport"` // TCP/UDP
+ IP string `gorm:"column:ip;notNull;default:'';comment:IP" json:"ip"` // ip 地址
+ Port int `gorm:"column:port;notNull;default:0;comment:端口号" json:"port"` // 端口号
+ Username string `gorm:"column:username;notNull;default:'';comment:用户名" json:"-"` // 用户名
+ Password string `gorm:"column:password;notNull;default:'';comment:密码" json:"-"` // 密码
+ BID string `gorm:"column:bid;notNull;default:'';comment:协议专属 id" json:"bid"`
+ PTZ bool `gorm:"column:ptz;notNull;default:FALSE;comment:是否支持 ptz" json:"ptz"` // 是否支持 ptz
+ Talk bool `gorm:"column:talk;notNull;default:FALSE;comment:是否支持对讲" json:"talk"` // 是否支持语音对讲
+ PID string `gorm:"column:pid;notNull;index;default:'';comment:父通道 ID" json:"pid"`
+ Groups pq.StringArray `gorm:"column:groups;type:text[];default:'{}';comment:虚拟组织" json:"-"`
+ Ext ChannelExt `gorm:"column:ext;type:jsonb;notNull;default:'{}';comment:扩展字段" json:"ext"`
+ ChildCount int `gorm:"column:child_count;notNull;default:0" json:"child_count"` // 子通道数量(不包含子孙通道)
+ Status bool `gorm:"column:status;notNull;default:false;comment:通道状态" json:"status"`
+ LastPushedAt orm.Time `gorm:"column:last_pushed_at;notNull;index;default:'1970-01-01 00:00:00';comment:最后推送时间" json:"last_pushed_at"`
+ // CustomGBID string `gorm:"column:custom_gb_id;notNull;default:'';comment:自定义国标ID" json:"custom_gb_id"`
+ DefaultGBID string `gorm:"column:default_gb_id;notNull;default:'';comment:默认国标ID" json:"default_gb_id"` // 默认国标ID
+ CustomName string `gorm:"column:custom_name;notNull;default:'';comment:自定义名称 " json:"custom_name"`
+
+ URL string `gorm:"-" json:"url"` //
+ Playing bool `gorm:"-" json:"playing"` // 是否播放中
+ PlayingStatus string `gorm:"-" json:"playing_status"`
+ IsDir bool `gorm:"-" json:"is_dir"` // 是否目录
+ RecordPlanEnabled bool `gorm:"-" json:"record_plan_enabled"` // 是否启用了录像计划
+ CascadeShareEnabled bool `gorm:"-" json:"cascade_share_enabled"` // 是否启用了级联共享
+ CustomID string `gorm:"-" json:"custom_id"`
+ IsRecording bool `gorm:"-" json:"is_recording"` // 是否配置录像计划
+ PlanID int `gorm:"-" json:"plan_id"` // 绑定的录像计划
+ DeviceName string `gorm:"-" json:"device_name"` // 设备名称;单独给前端用的
+
+ // 长沙地铁项目
+ ServerID string `gorm:"column:server_id;notNull;default:'';comment:平台ID" json:"server_id" `
+ ServerName string `json:"column:server_name;notNull;default:'';comment:平台名称" json:"server_name"`
+
+ // Latitude float32
+ // Longitude float32
+}
+
+func (a ChannelExt) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+
+func (c *Channel) GetTransport() string {
+ switch strings.ToUpper(c.Transport) {
+ case "UDP":
+ return "UDP"
+ case "TCPA":
+ return "TCPA"
+ default:
+ return "TCP"
+ }
+}
+
+func (d *Channel) BeforeCreate(tx *gorm.DB) error {
+ d.CreatedAt = orm.Now()
+ d.UpdatedAt = orm.Now()
+ return nil
+}
+
+func (d *Channel) BeforeUpdate(tx *gorm.DB) error {
+ d.UpdatedAt = orm.Now()
+ return nil
+}
+
+// type sortByChannel []*device.SyncReply_Channel
+
+// func (a sortByChannel) Len() int { return len(a) }
+// func (a sortByChannel) Swap(i, j int) { a[i], a[j] = a[j], a[i] }
+// func (a sortByChannel) Less(i, j int) bool { return a[i].GetBid() < a[j].GetBid() }
+
+// ChannelExt 通道的扩展内容,主要集中在国标上
+type ChannelExt struct {
+ Parental int `json:"parental"` // 是否有子设备(1:有;0:没有)
+ ParentID string `json:"parent_id"` // 父设备/区域/系统ID
+}
+
+// SetIsDir 设置是否为目录
+func (i *Channel) SetIsDir() {
+ i.IsDir = i.ChildCount > 0 || i.Ext.Parental == 1
+}
+
+// Scan implements orm.Scaner.
+func (i *ChannelExt) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// UnmarshalBinary implements encoding.BinaryUnmarshaler.
+func (c *Channel) UnmarshalBinary(data []byte) error {
+ return orm.JsonUnmarshal(data, c)
+}
+
+// MarshalBinary implements encoding.BinaryMarshaler.
+func (c *Channel) MarshalBinary() (data []byte, err error) {
+ return json.Marshal(c)
+}
+
+var (
+ _ encoding.BinaryMarshaler = (*Channel)(nil)
+ _ encoding.BinaryUnmarshaler = (*Channel)(nil)
+)
+
+// GetID 根据不同的设备类型返回 id
+// func (c *Channel) GetID() string {
+// switch c.Protocol {
+// case DeviceTypeSD, DeviceTypeGB, DeviceTypeOnvif, DeviceTypePull:
+// return c.BID
+// }
+// return c.ID
+// }
+
+// GetBID 获取设备BID
+// BID是通道的实际ID
+// GB28181: 由设备提供20位纯数字
+// ONVIF: 由设备提供可能带有下划线
+// PULL: 由平台提供,例如: 01,02(目前只支持单通道)
+func (c *Channel) GetBID() string {
+ // switch c.Protocol {
+ // case DeviceTypeSD, DeviceTypeGB, DeviceTypeOnvif, DeviceTypePull,DeviceTypePush:
+ // return c.BID
+ // }
+ // return c.ID
+ return c.BID
+}
+
+// GetStreamName 获取流 id
+func (c *Channel) GetStreamName() string {
+ return fmt.Sprintf("%s_%s", c.DeviceID, c.GetBID())
+}
+
+// TableName 指定表名
+// func (Channel) TableName() string {
+// return "channels"
+// }
+//
+// // IsGB28181 ...
+// func (c Channel) IsGB28181() bool {
+// return c.Protocol == DeviceTypeGB
+// }
+//
+// // IsSaida ...
+// func (c Channel) IsSaida() bool {
+// return c.Protocol == DeviceTypeSD
+// }
+//
+// // IsSupportLocalRecord 该协议是否支持本地录像
+// func (c *Channel) IsSupportLocalRecord() bool {
+// return c.Protocol == DeviceTypeSD
+// }
diff --git a/internal/core/host/channel.param.go b/internal/core/host/channel.param.go
new file mode 100644
index 0000000..ef8e2a2
--- /dev/null
+++ b/internal/core/host/channel.param.go
@@ -0,0 +1,35 @@
+package host
+
+import (
+ "git.lnton.com/lnton/pkg/web"
+)
+
+type FindChannelsInput struct {
+ web.PagerFilter
+ DeviceID string `form:"device_id"`
+ // Etag string `form:"etag"`
+ Protocol string `form:"protocol"`
+ PID string `form:"pid"` // 父级目录
+ PlanID int `form:"plan_id"` // 录像计划的 ID
+ CascadeID string `form:"cascade_id"` // 上级级联的 ID
+
+ Status string `form:"status"` // 过滤状态
+ Name string `form:"name"`
+ // ID string `form:"id"`
+ BID string `form:"bid"`
+
+ RequestIP string `form:"-"`
+
+ // 用于角色控制
+ UserName string `form:"-"`
+ Level int `form:"-"`
+
+ // 长沙地铁
+ ServerID string `form:"server_id"`
+ ServerName string `form:"server_name"`
+}
+
+type FindChannelsOutput struct {
+ Items []Channel `json:"items"`
+ Total int64 `json:"total"`
+}
diff --git a/internal/core/host/configs.go b/internal/core/host/configs.go
new file mode 100644
index 0000000..9c02009
--- /dev/null
+++ b/internal/core/host/configs.go
@@ -0,0 +1,24 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+)
+
+func (c Core) GetBaseConfig(ctx context.Context, in *ConfigBaseInput) (*ConfigBaseOutput, error) {
+ marshal, err := json.Marshal(in)
+ if err != nil {
+ return nil, err
+ }
+ result, err := c.Plugin.CallHost("getBaseConfig", marshal)
+
+ if err != nil {
+ return nil, err
+ }
+
+ out := ConfigBaseOutput{}
+ if err = json.Unmarshal(result, &out); err != nil {
+ return nil, err
+ }
+ return &out, nil
+}
diff --git a/internal/core/host/configs.model.go b/internal/core/host/configs.model.go
new file mode 100644
index 0000000..cd147b7
--- /dev/null
+++ b/internal/core/host/configs.model.go
@@ -0,0 +1 @@
+package host
diff --git a/internal/core/host/configs.param.go b/internal/core/host/configs.param.go
new file mode 100644
index 0000000..f65ddc8
--- /dev/null
+++ b/internal/core/host/configs.param.go
@@ -0,0 +1,10 @@
+package host
+
+type ConfigBaseInput struct {
+ Mode string `form:"mode"`
+}
+type ConfigBaseOutput struct {
+ HostIp string `json:"host_ip"`
+ RtspEnable bool `json:"rtsp_enable"`
+ RtspPort string `json:"rtsp_port"`
+}
diff --git a/internal/core/host/core.go b/internal/core/host/core.go
new file mode 100644
index 0000000..bcaaee1
--- /dev/null
+++ b/internal/core/host/core.go
@@ -0,0 +1,98 @@
+package host
+
+import (
+ "context"
+ "easyvqd/internal/conf"
+ "easyvqd/pkg/pluginheart"
+ "encoding/json"
+ "fmt"
+ "log/slog"
+
+ "git.lnton.com/lnton/pluginsdk/pkg/data"
+ "git.lnton.com/lnton/pluginsdk/pkg/plugin"
+)
+
+type Core struct {
+ Plugin *plugin.PluginSDK // 用于业务GRPC通信
+ HttpPlugin *pluginheart.Engine // 用于插件管理HTTP通信
+ RtspConfig *ConfigBaseOutput
+}
+
+var StartOk chan struct{}
+
+func NewCore(cfg *conf.Bootstrap) *Core {
+ cb := plugin.PluginCallback{
+ OnShutdown: func() { slog.Info("Plugin shutting down") },
+ }
+ sdk, err := plugin.NewPlugin(data.Plugin{
+ ID: "EasyVQD",
+ Name: "EasyVQD",
+ Version: "1.0.0",
+ ServerAddr: fmt.Sprintf("%s:%d", "localhost", cfg.Plugin.GrpcPort),
+ }, cb)
+ if err != nil {
+ panic(err)
+ }
+
+ engine := pluginheart.NewEngine("easyvqd", cfg.Plugin.HttpAPI)
+
+ // 后台等待启动完成信号,然后触发心跳(不阻塞主流程)
+ go func() {
+ StartOk = make(chan struct{})
+ <-StartOk
+ engine.AutoHeart(pluginheart.HeartInput{
+ Port: cfg.Server.HTTP.Port,
+ }, false)
+ }()
+
+ c := &Core{
+ Plugin: sdk,
+ HttpPlugin: engine,
+ }
+
+ sdk.AddResponseHandler("stop", c.stop)
+ sdk.AddResponseHandler("ping", c.ping)
+
+ // 这部分都是收到响应后的回调
+ sdk.AddResponseHandler("findDevices", c.findDevicesRespH)
+ sdk.AddResponseHandler("findChannels", c.findChannelsRespH)
+ sdk.AddResponseHandler("getBaseConfig", c.getBaseConfigRespH)
+ sdk.AddResponseHandler("findTalkUrl", c.findTalkUrlRespH)
+
+ config, err := c.GetBaseConfig(context.TODO(), &ConfigBaseInput{Mode: "rtsp"})
+ if err != nil {
+ panic(err)
+ }
+ // defer sdk.Close()
+ return &Core{
+ Plugin: sdk,
+ RtspConfig: config,
+ }
+}
+
+func (c Core) stop(requestID string, args json.RawMessage) (interface{}, error) {
+ slog.Info("Received 'stop' from host", "request_id", requestID)
+ return map[string]interface{}{"status": "stopped"}, nil
+}
+
+func (c Core) ping(requestID string, args json.RawMessage) (interface{}, error) {
+ return nil, nil
+}
+
+func (c Core) findDevicesRespH(requestID string, args json.RawMessage) (interface{}, error) {
+ slog.Debug("Received 'findDeviceList' from host", "request_id", requestID, "args", args)
+ return nil, nil
+}
+
+func (c Core) findChannelsRespH(requestID string, args json.RawMessage) (interface{}, error) {
+ slog.Debug("Received 'findChannels' from host", "request_id", requestID, "args", args)
+ return nil, nil
+}
+func (c Core) getBaseConfigRespH(requestID string, args json.RawMessage) (interface{}, error) {
+ slog.Debug("Received 'getBaseConfig' from host", "request_id", requestID, "args", args)
+ return nil, nil
+}
+func (c Core) findTalkUrlRespH(requestID string, args json.RawMessage) (interface{}, error) {
+ slog.Debug("Received 'findTalkUrl' from host", "request_id", requestID, "args", args)
+ return nil, nil
+}
diff --git a/internal/core/host/device.go b/internal/core/host/device.go
new file mode 100644
index 0000000..464d3b3
--- /dev/null
+++ b/internal/core/host/device.go
@@ -0,0 +1,24 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+)
+
+func (c Core) FindDevices(ctx context.Context, in *FindDevicesInput) (*FindDeviceOutput, error) {
+ marshal, err := json.Marshal(in)
+ if err != nil {
+ return nil, err
+ }
+ result, err := c.Plugin.CallHost("findDevices", marshal)
+
+ if err != nil {
+ return nil, err
+ }
+
+ out := FindDeviceOutput{}
+ if err = json.Unmarshal(result, &out); err != nil {
+ return nil, err
+ }
+ return &out, nil
+}
diff --git a/internal/core/host/device.model.go b/internal/core/host/device.model.go
new file mode 100644
index 0000000..7600d43
--- /dev/null
+++ b/internal/core/host/device.model.go
@@ -0,0 +1,171 @@
+package host
+
+import (
+ "database/sql/driver"
+ "encoding"
+ "encoding/json"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "gorm.io/gorm"
+)
+
+var (
+ _ encoding.BinaryMarshaler = (*Device)(nil)
+ _ encoding.BinaryUnmarshaler = (*Device)(nil)
+)
+
+// Ability 设备能力
+type Ability struct {
+ PTZ bool `json:"ptz"` // 云台控制
+ WIFI bool `json:"wifi"` // WIFI配网
+ OTA bool `json:"ota"` // OTA升级
+ CloudBroadcast bool `json:"cloud_broadcast"` // 云广播
+ AI bool `json:"ai"` // 智能检测
+ GAT1400 bool `json:"gat_1400"` // GA/T1400
+ Talk bool `json:"talk"` // P2P对讲
+}
+
+// Device 设备
+type Device struct {
+ ID string `gorm:"primaryKey" json:"id"`
+ CreatedAt orm.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;index;comment:创建时间" json:"created_at"`
+ UpdatedAt orm.Time `gorm:"notNull;default:CURRENT_TIMESTAMP;comment:更新时间" json:"updated_at"`
+ Name string `gorm:"column:name;notNull;default:'';comment:设备名称" json:"name"` // 设备名称
+ Protocol string `gorm:"column:protocol;notNull;default:'';comment:设备协议" json:"protocol"` // 设备协议 rtsp/rtmp
+ IP string `gorm:"column:ip;notNull;default:'';comment:设备IP" json:"ip"` // ip 地址
+ Port int `gorm:"column:port;notNull;default:0;comment:端口" json:"port"` // 端口号
+ Addr string `gorm:"column:addr;notNull;default:'';comment:ip 物理地址" json:"addr"`
+ Remark string `gorm:"column:remark;notNull;default:'';comment:备注" json:"remark"` // 备注描述
+ Username string `gorm:"column:username;notNull;default:'';comment:账号" json:"username"` // 账号
+ Password string `gorm:"column:password;notNull;default:'';comment:密码" json:"password"` // 密码
+ MediaTransport string `json:"media_transport,omitempty"` // TCP/UDP 服务端配置,空表示 UDP
+ GBCode string `gorm:"column:gb_code;notNull;default:'';comment:国标设备编码" json:"gb_code"` // 设备编码
+ UID int `gorm:"column:uid;notNull;default:0;comment:创建者" json:"uid"` // 创建者
+ Version string `gorm:"column:version;notNull;default:'';comment:版本" json:"version"` // 固件版本号
+ Ability Ability `gorm:"column:ability;type:jsonb;notNull;default:'{}';comment:设备能力" json:"ability"` // 能力
+ Status bool `gorm:"column:status;notNull;default:FALSE;comment:在线状态" json:"status"` // true:在线;false;离线
+ Model string `gorm:"column:model;notNull;default:'';comment:设备型号" json:"model"` // 状态
+ ChannelCount int `gorm:"column:channel_count;notNull;default:0;comment:通道数量" json:"channel_count"` // 通道数量
+ Ext DeviceExt `gorm:"type:jsonb;notNull;default:'{}';comment:设备扩展信息" json:"ext"`
+ GB GBExt `gorm:"type:jsonb;notNull;default:'{}';comment:国标扩展信息" json:"gb"`
+ ONVIF ONVIFExt `gorm:"type:jsonb;notNull;default:'{}';comment:ONVIF 扩展信息" json:"onvif"`
+ Network string `gorm:"column:network;notNull;default:'WAN';comment:网络类型" json:"network"` // LAN:局域网;WAN:公网
+ // CustomRTPIP string `gorm:"column:custom_rtp_ip;notNull;default:'';comment:自定义 rtp ip" json:"custom_rtp_ip"` // 自定义 rtp ip
+ RTPID string `gorm:"column:rtp_id;notNull;default:''" json:"rtp_id"`
+ ISPlatform bool `gorm:"column:is_platform;notNull;default:FALSE;comment:设备或平台" json:"is_platform"` // 设备/平台
+ URL string `gorm:"column:url;notNull;default:'';comment:拉流地址" json:"url"` // 拉流地址
+ // LastUpdatedAt orm.Time `gorm:"column:last_updated_at;notNull;default:'1970-01-01 00:00:00';index;comment:最后更新时间" json:"last_updated_at"`
+ LastPushedAt orm.Time `gorm:"column:last_pushed_at;notNull;index;default:'1970-01-01 00:00:00';comment:最后推送时间" json:"last_pushed_at"`
+ FilterList string `gorm:"column:filter_list;notNull;default:'';comment:过滤列表" json:"filter_list"`
+
+ // 示例数据 1,2,3
+ // 应用场景
+ // - 播放的时候,提供相关线路连接,默认取第一个
+ Routes string `gorm:"column:routes;notNull;default:'';comment:线路" json:"routes"`
+ DefaultGBID string `gorm:"-" json:"default_gb_id"`
+ PullMode int `gorm:"column:pull_mode;notNull;default:0;comment:拉流模式" json:"pull_mode"` // 拉流模式 默认(0) 拉流库(1)
+ Audio string `gorm:"column:audio;notNull;default:'0';comment:音频控制" json:"audio"` // 音频控制 0 关闭 1 启用 *** 自定义音频地址
+
+ // 长沙地铁项目
+ ServerID string `gorm:"column:server_id;notNull;default:'';comment:平台ID" json:"server_id" `
+ ServerName string `json:"column:server_name;notNull;default:'';comment:平台名称" json:"server_name"`
+}
+
+func (a DeviceExt) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+
+func (d *Device) BeforeCreate(tx *gorm.DB) error {
+ d.CreatedAt = orm.Now()
+ d.UpdatedAt = orm.Now()
+ return nil
+}
+
+func (d *Device) BeforeUpdate(tx *gorm.DB) error {
+ d.UpdatedAt = orm.Now()
+ return nil
+}
+
+func (d *Device) NetworkIP(iip, eip string) string {
+ if d.Network == "LAN" {
+ return iip
+ }
+ return eip
+}
+
+type ONVIFExt struct {
+ AuthType string `gorm:"column:auth_type;notNull;default:'';comment:认证类型" json:"auth_type"` // 认证类型
+}
+
+// Scan implements orm.Scaner.
+func (o *ONVIFExt) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, o)
+}
+
+func (o ONVIFExt) Value() (driver.Value, error) {
+ return json.Marshal(o)
+}
+
+// GBExt 国标扩展信息
+// 实际在使用过程中,发现部分字段存在支持其它协议的情况
+type GBExt struct {
+ SIPTransport string `json:"sip_transport,omitempty"` // TCP/UDP 由设备主动推送
+ SIPVersion string `json:"sip_version,omitempty"` // 国标 sip 版本号 2011/2016/2022
+
+ CatalogPeriod int `json:"catalog_period"` // 目录刷新周期
+ CatelogSubscribe bool `json:"catelog_subscribe,omitempty"` // 目录订阅,仅支持 gb28181
+ AlarmSubscribe bool `json:"alarm_subscribe,omitempty"` // 报警订阅,支持 gb28181/sdsdk
+ PositionSubscribe bool `json:"position_subscribe,omitempty"` // 位置订阅,仅支持 gb28181
+ NotifyURL string `json:"notify_url,omitempty"` // 通知地址
+
+ StorageID int `json:"storage_id,string,omitempty"`
+ StrategyID int `json:"strategy_id,string,omitempty"`
+}
+
+// Scan implements orm.Scaner.
+func (d *GBExt) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, d)
+}
+
+func (d GBExt) Value() (driver.Value, error) {
+ return json.Marshal(d)
+}
+
+// DeviceExt 设备信息
+type DeviceExt struct {
+ WanIP string `json:"wan_ip"`
+ LanIP string `json:"lan_ip"`
+ Mac string `json:"mac,omitempty"`
+ SDKVersion string `json:"sdk_version,omitempty"`
+ UID string `json:"uid,omitempty"` // 没啥用,等同于 sn,甲方设计必须有这个参数
+ Manufacturer string `json:"manufacturer,omitempty"` // 厂商
+ RemoteIP string `json:"remote_ip,omitempty"`
+}
+
+// Scan implements orm.Scaner.
+func (d *DeviceExt) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, d)
+}
+
+// UnmarshalBinary implements encoding.BinaryUnmarshaler.
+func (d *Device) UnmarshalBinary(data []byte) error {
+ return json.Unmarshal(data, d)
+}
+
+// MarshalBinary implements encoding.BinaryMarshaler.
+func (d Device) MarshalBinary() ([]byte, error) {
+ return json.Marshal(d)
+}
+
+// func (d *Device) GetRoutes() []Route {
+// routes := make([]Route, 0, 2)
+// routeIDs := strings.Split(d.Routes, ",")
+// for _, id := range routeIDs {
+// if id == "" {
+// continue
+// }
+// intID, _ := strconv.Atoi(id)
+// routes = append(routes, Route{ID: intID})
+// }
+// return routes
+// }
diff --git a/internal/core/host/device.param.go b/internal/core/host/device.param.go
new file mode 100644
index 0000000..5416cf3
--- /dev/null
+++ b/internal/core/host/device.param.go
@@ -0,0 +1,16 @@
+package host
+
+import "git.lnton.com/lnton/pkg/web"
+
+type FindDevicesInput struct {
+ web.PagerFilter
+ ID string `form:"id"`
+ Name string `form:"name"`
+ Status string `form:"status"`
+ ISPlatform string `form:"is_platform"`
+ Protocol string `form:"protocol"` // 协议类型:GB28181, ONVIF,RTSP,RTMP
+}
+type FindDeviceOutput struct {
+ Items []Device `json:"items"`
+ Total int64 `json:"total"`
+}
diff --git a/internal/core/host/talk.go b/internal/core/host/talk.go
new file mode 100644
index 0000000..d315a26
--- /dev/null
+++ b/internal/core/host/talk.go
@@ -0,0 +1,24 @@
+package host
+
+import (
+ "context"
+ "encoding/json"
+)
+
+func (c Core) FindTalkUrl(ctx context.Context, in *FindTalkInput) (*FinTalkOutput, error) {
+ marshal, err := json.Marshal(in)
+ if err != nil {
+ return nil, err
+ }
+ result, err := c.Plugin.CallHost("findTalkUrl", marshal)
+
+ if err != nil {
+ return nil, err
+ }
+
+ out := FinTalkOutput{}
+ if err = json.Unmarshal(result, &out); err != nil {
+ return nil, err
+ }
+ return &out, nil
+}
diff --git a/internal/core/host/talk.param.go b/internal/core/host/talk.param.go
new file mode 100644
index 0000000..421d053
--- /dev/null
+++ b/internal/core/host/talk.param.go
@@ -0,0 +1,9 @@
+package host
+
+type FindTalkInput struct {
+ ChannelID string `form:"channel_id"`
+}
+type FinTalkOutput struct {
+ TalkUrl string `json:"talk_url"`
+ Transport string `json:"transport"`
+}
diff --git a/internal/core/media/core.go b/internal/core/media/core.go
new file mode 100644
index 0000000..13a19dc
--- /dev/null
+++ b/internal/core/media/core.go
@@ -0,0 +1,18 @@
+package media
+
+import (
+ "easyvqd/internal/core/host"
+ "git.lnton.com/lnton/pluginsdk/pkg/plugin"
+)
+
+// var _ protocol.RecordReader = (*Core)(nil)
+
+type Core struct {
+ Plugin *plugin.PluginSDK
+}
+
+func NewCore(HostCore *host.Core) *Core {
+ return &Core{
+ Plugin: HostCore.Plugin,
+ }
+}
diff --git a/internal/core/vqd/core.go b/internal/core/vqd/core.go
new file mode 100644
index 0000000..a74a08d
--- /dev/null
+++ b/internal/core/vqd/core.go
@@ -0,0 +1,19 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+// Storer data
+type Storer interface {
+ VqdTask() VqdTaskStorer
+ VqdAlarm() VqdAlarmStorer
+ VqdTaskTemplate() VqdTaskTemplateStorer
+}
+
+// Core
+type Core struct {
+ store Storer
+}
+
+// NewCore create
+func NewCore(store Storer) *Core {
+ return &Core{store: store}
+}
diff --git a/internal/core/vqd/model.go b/internal/core/vqd/model.go
new file mode 100644
index 0000000..f598ca8
--- /dev/null
+++ b/internal/core/vqd/model.go
@@ -0,0 +1,260 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "database/sql/driver"
+ "encoding/json"
+ "git.lnton.com/lnton/pkg/orm"
+)
+
+type EncodeStatus int
+
+const (
+ EncodeStatusEmpty EncodeStatus = iota
+ EncodeStatusSuccess
+ EncodeStatusPing
+ EncodeStatusFailed
+)
+
+type VqdTask struct {
+ orm.Model
+ Name string `gorm:"column:name;notNull;default:'';comment:名称" json:"name"` // 名称
+ ChannelID string `gorm:"column:channel_id;notNull;default:'';comment:关联通道" json:"channel_id"` // 关联通道
+ ChannelName string `gorm:"column:channel_name;notNull;default:'';comment:关联通道名称" json:"channel_name"` // 关联通道名称
+ TaskTemplateID int64 `gorm:"column:task_template_id;notNull;default:0;comment:关联模板" json:"task_template_id"` //关联模板
+ TaskTemplateName string `gorm:"column:task_template_name;notNull;default:0;comment:关联模板名称" json:"task_template_name"` //关联模板名称
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ Des string `gorm:"column:des;notNull;default:'';comment:描述" json:"des"` // 描述
+}
+
+// TableName database table name
+func (*VqdTask) TableName() string {
+ return "vqd_task"
+}
+
+type JsonField[T any] struct {
+ Val T
+}
+
+func (j JsonField[T]) Value() (driver.Value, error) {
+ return json.Marshal(j.Val)
+}
+
+func (j *JsonField[T]) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, &j.Val)
+}
+
+type VqdConfig struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ FrmNum int32 `gorm:"column:frm_num;notNull;default:0;comment:过暗阈值" json:"frm_num"` // 连续分析帧数(2-64), 默认为10, 最大为 64
+ IsDeepLearn bool `gorm:"column:is_deep_learn;notNull;default:FALSE;comment:启用" json:"is_deep_learn"` // 是否使用深度学习版本, 默认使用深度学习版本
+}
+
+func (a VqdConfig) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdConfig) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 亮度检测
+type VqdLgtDark struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ DarkThr float64 `gorm:"column:dark_thr;notNull;default:0;comment:过暗阈值" json:"dark_thr"` // 默认 0.4, 取值范围: 0~1, 建议范围: 0.2~0.6
+ LgtThr float64 `gorm:"column:lgt_thr;notNull;default:0;comment:过亮阈值" json:"lgt_thr"` // 默认 0.1, 取值范围: 0~1, 建议范围: 0.1~0.5
+ LgtDarkAbnNumRatio float64 `gorm:"column:lgt_dark_abn_num_ratio;notNull;default:0;comment:偏暗或者偏亮次数比例" json:"lgt_dark_abn_num_ratio"` // 默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9
+}
+
+func (a VqdLgtDark) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdLgtDark) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 蓝屏检查
+type VqdBlue struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ BlueThr float64 `gorm:"column:blue_thr;notNull;default:0;comment:蓝屏判断阈值" json:"blue_thr"` // 默认为 0.6, 取值范围: 0~1, 建议范围 0.4~0.9
+ BlueAbnNumRatio float64 `gorm:"column:blue_abn_num_ratio;notNull;default:0;comment:蓝屏次数比例" json:"blue_abn_num_ratio"` // 默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9
+}
+
+func (a VqdBlue) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdBlue) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 清晰度检查
+type VqdClarity struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ ClarityThr float64 `gorm:"column:clarity_thr;notNull;default:0;comment:清晰度判断阈值" json:"clarity_thr"` // 默认为0.4, 取值范围: 0~1, 建议范围: 0.3~0.99
+ ClarityAbnNumRatio float64 `gorm:"column:clarity_abn_num_ratio;notNull;default:0;comment:清晰度异常次数比例" json:"clarity_abn_num_ratio"` // 默认为0.5, 取值范围: 0~1, 建议范围: 0.1~0.9
+}
+
+func (a VqdClarity) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdClarity) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 抖动检查
+type VqdShark struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ SharkThr float64 `gorm:"column:shark_thr;notNull;default:0;comment:抖动阈值参数" json:"shark_thr"` // 默认为 0.2, 取值范围: 0~1, 建议范围: 0.1~0.8
+ SharkAbnNumRatio float64 `gorm:"column:shark_abn_num_ratio;notNull;default:0;comment:出现抖动次数的比例" json:"shark_abn_num_ratio"` // 默认为0.2, 取值范围: 0~1, 建议范围: 0.1~0.6
+}
+
+func (a VqdShark) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdShark) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 冻结检测
+type VqdFreeze struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ FreezeThr float64 `gorm:"column:freeze_thr;notNull;default:0;comment:冻结阈值参数" json:"freeze_thr"` // 默认 0.4, 取值范围: 0~1, 建议范围: 0.2~0.6
+ FreezeAbnNumRatio float64 `gorm:"column:freeze_abn_num_ratio;notNull;default:0;comment:冻结帧数占得比例" json:"freeze_abn_num_ratio"` // 默认为0.99, 取值范围: 0.8~1, 建议范围: 0.95~1
+}
+
+func (a VqdFreeze) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdFreeze) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 偏色检测
+type VqdColor struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ ColorThr float64 `gorm:"column:color_thr;notNull;default:0;comment:偏色判断值" json:"color_thr"` // 默认为0.18, 取值范围: 0~1, 建议范围: 0.1~0.5
+ ColorAbnNumRatio float64 `gorm:"column:color_abn_num_ratio;notNull;default:0;comment:偏色次数比例" json:"color_abn_num_ratio"` // 默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9
+}
+
+func (a VqdColor) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdColor) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 遮挡检测
+type VqdOcclusion struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ OcclusionThr float64 `gorm:"column:occlusion_thr;notNull;default:0;comment:遮挡判断阈值" json:"occlusion_thr"` // 默认为0.1, 取值范围: 0~1, 建议范围: 0.05~0.5
+ OcclusionAbnNumRatio float64 `gorm:"column:occlusion_abn_num_ratio;notNull;default:0;comment:遮挡次数比例" json:"occlusion_abn_num_ratio"` // 默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9
+}
+
+func (a VqdOcclusion) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdOcclusion) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 噪声检测
+type VqdNoise struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ NoiseThr float64 `gorm:"column:noise_thr;notNull;default:0;comment:噪声判断阈值" json:"noise_thr"` // 默认为 0.3, 取值范围: 0~1, 建议范围: 0.2~0.8
+ NoiseAbnNumRatio float64 `gorm:"column:noise_abn_num_ratio;notNull;default:0;comment:噪声次数比例" json:"noise_abn_num_ratio"` // 默认为0.6, 取值范围: 0~1, 建议范围: 0.3~0.9
+}
+
+func (a VqdNoise) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdNoise) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 对比度检测
+type VqdContrast struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ CtraLowThr float64 `gorm:"column:ctra_low_thr;notNull;default:0;comment:低对比度判断阈值" json:"ctra_low_thr"` // 默认为 0.2, 取值范围: 0~1, 建议范围: 0.1~0.3
+ CtraHighThr float64 `gorm:"column:ctra_high_thr;notNull;default:0;comment:高对比度判断阈值" json:"ctra_high_thr"` // 默认为 0.8, 取值范围: 0~1, 建议范围: 0.7~0.9
+ CtraAbnNumRatio float64 `gorm:"column:ctra_abn_num_ratio;notNull;default:0;comment:对比度异常次数比例" json:"ctra_abn_num_ratio"` // 默认为0.5, 取值范围: 0~1, 建议范围: 0.3~0.9
+}
+
+func (a VqdContrast) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdContrast) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 马赛克检测
+type VqdMosaic struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ MosaicThr float64 `gorm:"column:mosaic_thr;notNull;default:0;comment:马赛克阈值参数" json:"mosaic_thr"` // 默认为 0.1 取值范围: 0~1, 建议范围: 0.1~0.9
+ MosaicAbnNumRatio float64 `gorm:"column:mosaic_abn_num_ratio;notNull;default:0;comment:马赛克次数比例" json:"mosaic_abn_num_ratio"` // 默认为0.5,取值范围: 0~1, 建议范围: 0.3
+}
+
+func (a VqdMosaic) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdMosaic) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+// 花屏检测
+type VqdFlower struct {
+ Enable bool `gorm:"column:enable;notNull;default:FALSE;comment:启用" json:"enable"` // 启用
+ FlowerThr float64 `gorm:"column:flower_thr;notNull;default:0;comment:花屏阈值参数" json:"flower_thr"` // 默认为 0.3 取值范围: 0~1, 建议范围: 0.1~0.9
+ FlowerAbnNumRatio float64 `gorm:"column:flower_abn_num_ratio;notNull;default:0;comment:花屏次数比例" json:"flower_abn_num_ratio"` // 默认为0.6, 取值范围: 0~1, 建议范围: 0.3
+ MosaicThr float64 `gorm:"column:mosaic_thr;notNull;default:0;comment:阈值" json:"mosaic_thr"` // 默认为 0.3 取值范围: 0~1, 建议范围: 0.1~0.9
+}
+
+func (a VqdFlower) Value() (driver.Value, error) {
+ return json.Marshal(a)
+}
+func (i *VqdFlower) Scan(input interface{}) error {
+ return orm.JsonUnmarshal(input, i)
+}
+
+type VqdTaskTemplate struct {
+ orm.Model
+ Name string `gorm:"column:name;notNull;default:'';comment:名称" json:"name"`
+ Plans string `gorm:"column:plans;notNull;default:'';comment:计划" json:"plans"`
+ Enable bool `gorm:"column:enable;notNull;default:TRUE;comment:是否启用" json:"enable"`
+ IsDefault bool `gorm:"column:is_default;notNull;default:FALSE;comment:是否默认" json:"is_default"`
+ VqdConfig VqdConfig `gorm:"column:vqd_config;type:jsonb;notNull;default:'{}';comment:诊断基础配置" json:"vqd_config"` // 诊断基础配置
+ VqdLgtDark VqdLgtDark `gorm:"column:vqd_lgt_dark;type:jsonb;notNull;default:'{}';comment:亮度检测" json:"vqd_lgt_dark"` // 亮度检测
+ VqdBlue VqdBlue `gorm:"column:vqd_blue;type:jsonb;notNull;default:'{}';comment:蓝屏检查" json:"vqd_blue"` // 蓝屏检查
+ VqdClarity VqdClarity `gorm:"column:vqd_clarity;type:jsonb;notNull;default:'{}';comment:清晰度检查" json:"vqd_clarity"` // 清晰度检查
+ VqdShark VqdShark `gorm:"column:vqd_shark;type:jsonb;notNull;default:'{}';comment:抖动检查" json:"vqd_shark"` // 抖动检查
+ VqdFreeze VqdFreeze `gorm:"column:vqd_freeze;type:jsonb;notNull;default:'{}';comment:冻结检测" json:"vqd_freeze"` // 冻结检测
+ VqdColor VqdColor `gorm:"column:vqd_color;type:jsonb;notNull;default:'{}';comment:偏色检测" json:"vqd_color"` // 偏色检测
+ VqdOcclusion VqdOcclusion `gorm:"column:vqd_occlusion;type:jsonb;notNull;default:'{}';comment:遮挡检测" json:"vqd_occlusion"` // 遮挡检测
+ VqdNoise VqdNoise `gorm:"column:vqd_noise;type:jsonb;notNull;default:'{}';comment:噪声检测" json:"vqd_noise"` // 噪声检测
+ VqdContrast VqdContrast `gorm:"column:vqd_contrast;type:jsonb;notNull;default:'{}';comment:对比度检测" json:"vqd_contrast"` // 对比度检测
+ VqdMosaic VqdMosaic `gorm:"column:vqd_mosaic;type:jsonb;notNull;default:'{}';comment:马赛克检测" json:"vqd_mosaic"` // 马赛克检测
+ VqdFlower VqdFlower `gorm:"column:vqd_flower;type:jsonb;notNull;default:'{}';comment:花屏检测" json:"vqd_flower"` // 花屏检测
+ Des string `gorm:"column:des;notNull;default:'';comment:描述" json:"des"` // 描述
+}
+
+// TableName database table name
+func (*VqdTaskTemplate) TableName() string {
+ return "vqd_task_template"
+}
+
+type VqdAlarm struct {
+ orm.Model
+ AlarmName string `gorm:"column:alarm_name;notNull;default:'';comment:告警名称" json:"alarm_name"` // 告警名称
+ AlarmValue string `gorm:"column:alarm_value;notNull;default:'';comment:告警参数" json:"alarm_value"` // 告警参数
+ ChannelID string `gorm:"column:channel_id;notNull;default:'';comment:关联通道" json:"channel_id"` // 关联通道
+ ChannelName string `gorm:"column:channel_name;notNull;default:'';comment:关联通道名称" json:"channel_name"` // 关联通道名称
+ TaskTemplateID int64 `gorm:"column:task_template_id;notNull;default:0;comment:关联模板" json:"task_template_id"` //关联模板
+ TaskTemplateName string `gorm:"column:task_template_name;notNull;default:0;comment:关联模板名称" json:"task_template_name"` //关联模板名称
+ TaskID int64 `gorm:"column:task_id;notNull;default:0;comment:关联任务" json:"task_id"` // 关联任务名称
+ TaskName string `gorm:"column:task_name;notNull;default:'';comment:关联任务名称" json:"task_name"` // 任务名称
+ FilePath string `gorm:"column:file_path;notNull;default:'';comment:文件路径" json:"file_path"` // 文件路径
+
+}
+
+// TableName database table name
+func (*VqdAlarm) TableName() string {
+ return "vqd_alarm"
+}
diff --git a/internal/core/vqd/store/audioencodedb/db.go b/internal/core/vqd/store/audioencodedb/db.go
new file mode 100644
index 0000000..e641f34
--- /dev/null
+++ b/internal/core/vqd/store/audioencodedb/db.go
@@ -0,0 +1,44 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package audioencodedb
+
+import (
+ "easyvqd/internal/core/vqd"
+ "gorm.io/gorm"
+)
+
+var _ vqd.Storer = DB{}
+
+// DB Related business namespaces
+type DB struct {
+ db *gorm.DB
+}
+
+// NewDB instance object
+func NewDB(db *gorm.DB) DB {
+ return DB{db: db}
+}
+
+func (d DB) VqdTask() vqd.VqdTaskStorer {
+ return VqdTask(d)
+}
+func (d DB) VqdTaskTemplate() vqd.VqdTaskTemplateStorer {
+ return VqdTaskTemplate(d)
+}
+func (d DB) VqdAlarm() vqd.VqdAlarmStorer {
+ return VqdAlarm(d)
+}
+
+// AutoMigrate sync database
+func (d DB) AutoMigrate(ok bool) DB {
+ if !ok {
+ return d
+ }
+ if err := d.db.AutoMigrate(
+ new(vqd.VqdTask),
+ new(vqd.VqdTaskTemplate),
+ new(vqd.VqdAlarm),
+ ); err != nil {
+ panic(err)
+ }
+ return d
+}
diff --git a/internal/core/vqd/store/audioencodedb/vqdalarm.go b/internal/core/vqd/store/audioencodedb/vqdalarm.go
new file mode 100644
index 0000000..a68f4e4
--- /dev/null
+++ b/internal/core/vqd/store/audioencodedb/vqdalarm.go
@@ -0,0 +1,68 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package audioencodedb
+
+import (
+ "context"
+ "easyvqd/internal/core/vqd"
+ "git.lnton.com/lnton/pkg/orm"
+)
+
+var _ vqd.VqdAlarmStorer = VqdAlarm{}
+
+// VqdAlarm Related business namespaces
+type VqdAlarm DB
+
+// FindAll implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) FindAll(bs *[]*vqd.VqdAlarm) (int64, error) {
+ db := d.db.Model(&vqd.VqdAlarm{})
+ var total int64
+ if err := db.Count(&total).Error; err != nil || total <= 0 {
+ // 如果统计失败或者数量为0,则返回错误
+ return 0, err
+ }
+ return total, db.Find(bs).Error
+}
+
+// Find implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) Find(ctx context.Context, bs *[]*vqd.VqdAlarm, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
+ return orm.FindWithContext(ctx, d.db, bs, page, opts...)
+}
+
+// Get implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) Get(ctx context.Context, model *vqd.VqdAlarm, opts ...orm.QueryOption) error {
+ return orm.FirstWithContext(ctx, d.db, model, opts...)
+}
+
+// Add implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) Add(ctx context.Context, model *vqd.VqdAlarm) error {
+ return d.db.WithContext(ctx).Create(model).Error
+}
+
+// Edit implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) Edit(ctx context.Context, model *vqd.VqdAlarm, changeFn func(*vqd.VqdAlarm), opts ...orm.QueryOption) error {
+ return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
+}
+
+// Del implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) Del(ctx context.Context, model *vqd.VqdAlarm, opts ...orm.QueryOption) error {
+ return orm.DeleteWithContext(ctx, d.db, model, opts...)
+}
+
+// EditStatus implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) EditStatus(status int, id int) error {
+ if err := d.db.Model(&vqd.VqdAlarm{}).Where(`id = ?`, id).Update("task_status", status).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+// EditStatusError implements vqd.VqdAlarmStorer.
+func (d VqdAlarm) EditStatusError(id, status int, s string) error {
+ if err := d.db.Model(&vqd.VqdAlarm{}).Where(`id = ?`, id).Updates(map[string]any{
+ "task_status": status,
+ "error_msg": s,
+ }).Error; err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/internal/core/vqd/store/audioencodedb/vqdtask.go b/internal/core/vqd/store/audioencodedb/vqdtask.go
new file mode 100644
index 0000000..c303049
--- /dev/null
+++ b/internal/core/vqd/store/audioencodedb/vqdtask.go
@@ -0,0 +1,68 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package audioencodedb
+
+import (
+ "context"
+ "easyvqd/internal/core/vqd"
+ "git.lnton.com/lnton/pkg/orm"
+)
+
+var _ vqd.VqdTaskStorer = VqdTask{}
+
+// VqdTask Related business namespaces
+type VqdTask DB
+
+// FindAll implements vqd.VqdTaskStorer.
+func (d VqdTask) FindAll(bs *[]*vqd.VqdTask) (int64, error) {
+ db := d.db.Model(&vqd.VqdTask{})
+ var total int64
+ if err := db.Count(&total).Error; err != nil || total <= 0 {
+ // 如果统计失败或者数量为0,则返回错误
+ return 0, err
+ }
+ return total, db.Find(bs).Error
+}
+
+// Find implements vqd.VqdTaskStorer.
+func (d VqdTask) Find(ctx context.Context, bs *[]*vqd.VqdTask, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
+ return orm.FindWithContext(ctx, d.db, bs, page, opts...)
+}
+
+// Get implements vqd.VqdTaskStorer.
+func (d VqdTask) Get(ctx context.Context, model *vqd.VqdTask, opts ...orm.QueryOption) error {
+ return orm.FirstWithContext(ctx, d.db, model, opts...)
+}
+
+// Add implements vqd.VqdTaskStorer.
+func (d VqdTask) Add(ctx context.Context, model *vqd.VqdTask) error {
+ return d.db.WithContext(ctx).Create(model).Error
+}
+
+// Edit implements vqd.VqdTaskStorer.
+func (d VqdTask) Edit(ctx context.Context, model *vqd.VqdTask, changeFn func(*vqd.VqdTask), opts ...orm.QueryOption) error {
+ return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
+}
+
+// Del implements vqd.VqdTaskStorer.
+func (d VqdTask) Del(ctx context.Context, model *vqd.VqdTask, opts ...orm.QueryOption) error {
+ return orm.DeleteWithContext(ctx, d.db, model, opts...)
+}
+
+// EditStatus implements vqd.VqdTaskStorer.
+func (d VqdTask) EditStatus(status int, id int) error {
+ if err := d.db.Model(&vqd.VqdTask{}).Where(`id = ?`, id).Update("task_status", status).Error; err != nil {
+ return err
+ }
+ return nil
+}
+
+// EditStatusError implements vqd.VqdTaskStorer.
+func (d VqdTask) EditStatusError(id, status int, s string) error {
+ if err := d.db.Model(&vqd.VqdTask{}).Where(`id = ?`, id).Updates(map[string]any{
+ "task_status": status,
+ "error_msg": s,
+ }).Error; err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/internal/core/vqd/store/audioencodedb/vqdtasktemplate.go b/internal/core/vqd/store/audioencodedb/vqdtasktemplate.go
new file mode 100644
index 0000000..86e92e5
--- /dev/null
+++ b/internal/core/vqd/store/audioencodedb/vqdtasktemplate.go
@@ -0,0 +1,57 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package audioencodedb
+
+import (
+ "context"
+ "easyvqd/internal/core/vqd"
+ "git.lnton.com/lnton/pkg/orm"
+)
+
+var _ vqd.VqdTaskTemplateStorer = VqdTaskTemplate{}
+
+// VqdTaskTemplate Related business namespaces
+type VqdTaskTemplate DB
+
+// FindAll implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) FindAll(bs *[]*vqd.VqdTaskTemplate) (int64, error) {
+ db := d.db.Model(&vqd.VqdTaskTemplate{})
+ var total int64
+ if err := db.Count(&total).Error; err != nil || total <= 0 {
+ // 如果统计失败或者数量为0,则返回错误
+ return 0, err
+ }
+ return total, db.Find(bs).Error
+}
+
+// Find implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) Find(ctx context.Context, bs *[]*vqd.VqdTaskTemplate, page orm.Pager, opts ...orm.QueryOption) (int64, error) {
+ return orm.FindWithContext(ctx, d.db, bs, page, opts...)
+}
+
+// Get implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) Get(ctx context.Context, model *vqd.VqdTaskTemplate, opts ...orm.QueryOption) error {
+ return orm.FirstWithContext(ctx, d.db, model, opts...)
+}
+
+// Add implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) Add(ctx context.Context, model *vqd.VqdTaskTemplate) error {
+ return d.db.WithContext(ctx).Create(model).Error
+}
+
+// Edit implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) Edit(ctx context.Context, model *vqd.VqdTaskTemplate, changeFn func(*vqd.VqdTaskTemplate), opts ...orm.QueryOption) error {
+ return orm.UpdateWithContext(ctx, d.db, model, changeFn, opts...)
+}
+
+// Del implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) Del(ctx context.Context, model *vqd.VqdTaskTemplate, opts ...orm.QueryOption) error {
+ return orm.DeleteWithContext(ctx, d.db, model, opts...)
+}
+
+// EditStatus implements vqd.VqdTaskTemplateStorer.
+func (d VqdTaskTemplate) EditStatus(status vqd.EncodeStatus, id int) error {
+ if err := d.db.Model(&vqd.VqdTaskTemplate{}).Where(`id = ?`, id).Update("encode_status", status).Error; err != nil {
+ return err
+ }
+ return nil
+}
diff --git a/internal/core/vqd/vqdalarm.go b/internal/core/vqd/vqdalarm.go
new file mode 100644
index 0000000..be8c3da
--- /dev/null
+++ b/internal/core/vqd/vqdalarm.go
@@ -0,0 +1,115 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "context"
+ "git.lnton.com/lnton/pkg/orm"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/jinzhu/copier"
+ "log/slog"
+)
+
+// VqdAlarmStorer Instantiation interface
+type VqdAlarmStorer interface {
+ Find(context.Context, *[]*VqdAlarm, orm.Pager, ...orm.QueryOption) (int64, error)
+ FindAll(dp *[]*VqdAlarm) (int64, error)
+ Get(context.Context, *VqdAlarm, ...orm.QueryOption) error
+ Add(context.Context, *VqdAlarm) error
+ Edit(context.Context, *VqdAlarm, func(*VqdAlarm), ...orm.QueryOption) error
+ Del(context.Context, *VqdAlarm, ...orm.QueryOption) error
+}
+
+// FindVqdAlarmAll Paginated search
+func (c Core) FindVqdAlarmAll() ([]*VqdAlarm, int64, error) {
+ items := make([]*VqdAlarm, 0)
+ total, err := c.store.VqdAlarm().FindAll(&items)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+}
+
+// FindVqdAlarm Paginated search
+func (c Core) FindVqdAlarm(ctx context.Context, in *FindVqdAlarmInput) ([]*VqdAlarm, int64, error) {
+ items := make([]*VqdAlarm, 0)
+ if in.AlarmName != "" {
+ query := orm.NewQuery(8).
+ Where("audio_name like ? OR channel_id like ? OR channel_name like ?", "%"+in.AlarmName+"%", "%"+in.AlarmName+"%", "%"+in.AlarmName+"%").OrderBy("created_at DESC")
+ total, err := c.store.VqdAlarm().Find(ctx, &items, in, query.Encode()...)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+ } else {
+ query := orm.NewQuery(2).OrderBy("created_at DESC")
+ total, err := c.store.VqdAlarm().Find(ctx, &items, in, query.Encode()...)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+ }
+}
+
+// GetVqdAlarm Query a single object
+func (c Core) GetVqdAlarm(ctx context.Context, id int) (*VqdAlarm, error) {
+ var out VqdAlarm
+ if err := c.store.VqdAlarm().Get(ctx, &out, orm.Where("id=?", id)); err != nil {
+ if orm.IsErrRecordNotFound(err) {
+ return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
+ }
+ return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+func (c Core) GetNameVqdAlarm(ctx context.Context, name string) (*VqdAlarm, error) {
+ var out VqdAlarm
+ if err := c.store.VqdAlarm().Get(ctx, &out, orm.Where("name=?", name)); err != nil {
+ if orm.IsErrRecordNotFound(err) {
+ return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
+ }
+ return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// AddVqdAlarm Insert into database
+func (c Core) AddVqdAlarm(ctx context.Context, in *AddVqdAlarmInput) (*VqdAlarm, error) {
+ var out VqdAlarm
+ if err := copier.Copy(&out, in); err != nil {
+ slog.Error("Copy", "err", err)
+ }
+ if err := c.store.VqdAlarm().Add(ctx, &out); err != nil {
+ return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// EditVqdAlarm Update object information
+func (c Core) EditVqdAlarm(ctx context.Context, in *EditVqdAlarmInput, id int) (*VqdAlarm, error) {
+ var out VqdAlarm
+ if err := c.store.VqdAlarm().Edit(ctx, &out, func(b *VqdAlarm) {
+ if err := copier.Copy(b, in); err != nil {
+ slog.Error("Copy", "err", err)
+ }
+ }, orm.Where("id=?", id)); err != nil {
+ return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// DelVqdAlarm Delete object
+func (c Core) DelVqdAlarm(ctx context.Context, id int) (*VqdAlarm, error) {
+ var out VqdAlarm
+ if err := c.store.VqdAlarm().Del(ctx, &out, orm.Where("id=?", id)); err != nil {
+ return nil, reason.ErrDB.Withf(`Del err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+func (c Core) DelVqdAlarmAll(ctx context.Context, ids []int) (*VqdAlarm, error) {
+ var out VqdAlarm
+ if err := c.store.VqdAlarm().Del(ctx, &out, orm.Where("id in (?)", ids)); err != nil {
+ return nil, reason.ErrDB.Withf(`Del ids err[%s]`, err.Error())
+ }
+ return &out, nil
+}
diff --git a/internal/core/vqd/vqdalarm.param.go b/internal/core/vqd/vqdalarm.param.go
new file mode 100644
index 0000000..568c3ae
--- /dev/null
+++ b/internal/core/vqd/vqdalarm.param.go
@@ -0,0 +1,30 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "git.lnton.com/lnton/pkg/web"
+)
+
+type FindVqdAlarmInput struct {
+ web.PagerFilter
+ AlarmName string `form:"alarm_name"` // 名称
+}
+
+type EditVqdAlarmInput struct {
+ AlarmName string `json:"alarm_name"` // 名称
+}
+
+type AddVqdAlarmInput struct {
+ AlarmName string `json:"alarm_name"` // 告警名称
+ AlarmValue string `json:"alarm_value"` // 告警参数
+ ChannelID string `json:"channel_id"` // 关联通道
+ ChannelName string `json:"channel_name"` // 关联通道名称
+ TaskTemplateID int64 `json:"task_template_id"` // 关联模板
+ TaskTemplateName string `json:"task_template_name"` // 关联模板名称
+ TaskID int64 `json:"task_id"` // 关联任务名称
+ TaskName string `json:"task_name"` // 任务名称
+ FilePath string `json:"file_path"` // 文件路径
+}
+type DelVqdAlarmInput struct {
+ IDs []int `json:"ids"` // id
+}
diff --git a/internal/core/vqd/vqdtask.go b/internal/core/vqd/vqdtask.go
new file mode 100644
index 0000000..032bf1a
--- /dev/null
+++ b/internal/core/vqd/vqdtask.go
@@ -0,0 +1,133 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "context"
+ "git.lnton.com/lnton/pkg/orm"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/jinzhu/copier"
+ "log/slog"
+)
+
+// VqdTaskStorer Instantiation interface
+type VqdTaskStorer interface {
+ Find(context.Context, *[]*VqdTask, orm.Pager, ...orm.QueryOption) (int64, error)
+ FindAll(dp *[]*VqdTask) (int64, error)
+ Get(context.Context, *VqdTask, ...orm.QueryOption) error
+ Add(context.Context, *VqdTask) error
+ Edit(context.Context, *VqdTask, func(*VqdTask), ...orm.QueryOption) error
+ Del(context.Context, *VqdTask, ...orm.QueryOption) error
+ EditStatus(status int, id int) error
+ EditStatusError(id, status int, s string) error
+}
+
+// FindVqdTaskAll Paginated search
+func (c Core) FindVqdTaskAll() ([]*VqdTask, int64, error) {
+ items := make([]*VqdTask, 0)
+ total, err := c.store.VqdTask().FindAll(&items)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+}
+
+// FindVqdTask Paginated search
+func (c Core) FindVqdTask(ctx context.Context, in *FindVqdTaskInput) ([]*VqdTask, int64, error) {
+ items := make([]*VqdTask, 0)
+ if in.Name != "" {
+ query := orm.NewQuery(8).
+ Where("name like ?", "%"+in.Name+"%").OrderBy("created_at DESC")
+ total, err := c.store.VqdTask().Find(ctx, &items, in, query.Encode()...)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+ } else {
+ query := orm.NewQuery(2).OrderBy("created_at DESC")
+ total, err := c.store.VqdTask().Find(ctx, &items, in, query.Encode()...)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+ }
+}
+
+// GetVqdTask Query a single object
+func (c Core) GetVqdTask(ctx context.Context, id int) (*VqdTask, error) {
+ var out VqdTask
+ if err := c.store.VqdTask().Get(ctx, &out, orm.Where("id=?", id)); err != nil {
+ if orm.IsErrRecordNotFound(err) {
+ return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
+ }
+ return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+func (c Core) GetNameVqdTask(ctx context.Context, name string) (*VqdTask, error) {
+ var out VqdTask
+ if err := c.store.VqdTask().Get(ctx, &out, orm.Where("name=?", name)); err != nil {
+ if orm.IsErrRecordNotFound(err) {
+ return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
+ }
+ return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// AddVqdTask Insert into database
+func (c Core) AddVqdTask(ctx context.Context, in *AddVqdTaskInput) (*VqdTask, error) {
+ var out VqdTask
+ if err := copier.Copy(&out, in); err != nil {
+ slog.Error("Copy", "err", err)
+ }
+ if err := c.store.VqdTask().Add(ctx, &out); err != nil {
+ return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// EditVqdTask Update object information
+func (c Core) EditVqdTask(ctx context.Context, in *EditVqdTaskInput, id int) (*VqdTask, error) {
+ var out VqdTask
+ if err := c.store.VqdTask().Edit(ctx, &out, func(b *VqdTask) {
+ if err := copier.Copy(b, in); err != nil {
+ slog.Error("Copy", "err", err)
+ }
+ }, orm.Where("id=?", id)); err != nil {
+ return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// DelVqdTask Delete object
+func (c Core) DelVqdTask(ctx context.Context, id int) (*VqdTask, error) {
+ var out VqdTask
+ if err := c.store.VqdTask().Del(ctx, &out, orm.Where("id=?", id)); err != nil {
+ return nil, reason.ErrDB.Withf(`Del err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+func (c Core) DelVqdTaskAll(ctx context.Context, ids []int) (*VqdTask, error) {
+ var out VqdTask
+ if err := c.store.VqdTask().Del(ctx, &out, orm.Where("id in (?)", ids)); err != nil {
+ return nil, reason.ErrDB.Withf(`Del ids err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// VqdTaskStatus Update
+func (c Core) VqdTaskStatus(status int, id int) error {
+ if err := c.store.VqdTask().EditStatus(status, id); err != nil {
+ return reason.ErrDB.Withf(`Status err[%s]`, err.Error())
+ }
+ return nil
+}
+
+// VqdTaskStatus Update
+func (c Core) VqdTaskStatusError(id, status int, s string) error {
+ if err := c.store.VqdTask().EditStatusError(id, status, s); err != nil {
+ return reason.ErrDB.Withf(`StatusError err[%s]`, err.Error())
+ }
+ return nil
+}
diff --git a/internal/core/vqd/vqdtask.param.go b/internal/core/vqd/vqdtask.param.go
new file mode 100644
index 0000000..c0d13e6
--- /dev/null
+++ b/internal/core/vqd/vqdtask.param.go
@@ -0,0 +1,31 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "git.lnton.com/lnton/pkg/web"
+)
+
+type FindVqdTaskInput struct {
+ web.PagerFilter
+ Name string `form:"name"` // 名称
+}
+
+type EditVqdTaskInput struct {
+ Name string `json:"name"` // 名称
+ ChannelID string `json:"channel_id"` // 关联通道
+ ChannelName string `json:"channel_name"` // 通道名称
+ TaskTemplateID string `json:"task_template_id"` // 关联模板
+ TaskTemplateName string `json:"task_template_name"` // 模板名称
+ Enable bool `form:"enable"` // 启用
+ Des string `json:"des"` // 描述
+}
+
+type AddVqdTaskInput struct {
+ Name string `json:"name"` // 名称
+ ChannelID string `json:"channel_id"` // 关联通道
+ ChannelName string `json:"channel_name"` // 通道名称
+ TaskTemplateID string `json:"task_template_id"` // 关联模板
+ TaskTemplateName string `json:"task_template_name"` // 模板名称
+ Enable bool `form:"enable"` // 启用
+ Des string `json:"des"` // 描述
+}
diff --git a/internal/core/vqd/vqdtasktemplate.go b/internal/core/vqd/vqdtasktemplate.go
new file mode 100644
index 0000000..cb6e32d
--- /dev/null
+++ b/internal/core/vqd/vqdtasktemplate.go
@@ -0,0 +1,107 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "context"
+ "git.lnton.com/lnton/pkg/orm"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/jinzhu/copier"
+ "log/slog"
+)
+
+// VqdTaskTemplateStorer Instantiation interface
+type VqdTaskTemplateStorer interface {
+ Find(context.Context, *[]*VqdTaskTemplate, orm.Pager, ...orm.QueryOption) (int64, error)
+ FindAll(dp *[]*VqdTaskTemplate) (int64, error)
+ Get(context.Context, *VqdTaskTemplate, ...orm.QueryOption) error
+ Add(context.Context, *VqdTaskTemplate) error
+ Edit(context.Context, *VqdTaskTemplate, func(*VqdTaskTemplate), ...orm.QueryOption) error
+ Del(context.Context, *VqdTaskTemplate, ...orm.QueryOption) error
+}
+
+// FindVqdTaskTemplateAll Paginated search
+func (c Core) FindVqdTaskTemplateAll() ([]*VqdTaskTemplate, int64, error) {
+ items := make([]*VqdTaskTemplate, 0)
+ total, err := c.store.VqdTaskTemplate().FindAll(&items)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+}
+
+// FindVqdTaskTemplate Paginated search
+func (c Core) FindVqdTaskTemplate(ctx context.Context, in *FindVqdTaskTemplateInput) ([]*VqdTaskTemplate, int64, error) {
+ items := make([]*VqdTaskTemplate, 0)
+ if in.Name != "" {
+ query := orm.NewQuery(8).
+ Where("name like ? ", "%"+in.Name+"%").OrderBy("created_at DESC")
+ total, err := c.store.VqdTaskTemplate().Find(ctx, &items, in, query.Encode()...)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+ } else {
+ query := orm.NewQuery(2).OrderBy("created_at DESC")
+ total, err := c.store.VqdTaskTemplate().Find(ctx, &items, in, query.Encode()...)
+ if err != nil {
+ return nil, 0, reason.ErrDB.Withf(`Find err[%s]`, err.Error())
+ }
+ return items, total, nil
+ }
+}
+
+// GetVqdTaskTemplate Query a single object
+func (c Core) GetVqdTaskTemplate(ctx context.Context, id int) (*VqdTaskTemplate, error) {
+ var out VqdTaskTemplate
+ if err := c.store.VqdTaskTemplate().Get(ctx, &out, orm.Where("id=?", id)); err != nil {
+ if orm.IsErrRecordNotFound(err) {
+ return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
+ }
+ return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+func (c Core) GetNameVqdTaskTemplate(ctx context.Context, name string) (*VqdTaskTemplate, error) {
+ var out VqdTaskTemplate
+ if err := c.store.VqdTaskTemplate().Get(ctx, &out, orm.Where("name=?", name)); err != nil {
+ if orm.IsErrRecordNotFound(err) {
+ return nil, reason.ErrNotFound.Withf(`Get err[%s]`, err.Error())
+ }
+ return nil, reason.ErrDB.Withf(`Get err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// AddVqdTaskTemplate Insert into database
+func (c Core) AddVqdTaskTemplate(ctx context.Context, in *AddVqdTaskTemplateInput) (*VqdTaskTemplate, error) {
+ var out VqdTaskTemplate
+ if err := copier.Copy(&out, in); err != nil {
+ slog.Error("Copy", "err", err)
+ }
+ if err := c.store.VqdTaskTemplate().Add(ctx, &out); err != nil {
+ return nil, reason.ErrDB.Withf(`Add err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// EditVqdTaskTemplate Update object information
+func (c Core) EditVqdTaskTemplate(ctx context.Context, in *EditVqdTaskTemplateInput, id int) (*VqdTaskTemplate, error) {
+ var out VqdTaskTemplate
+ if err := c.store.VqdTaskTemplate().Edit(ctx, &out, func(b *VqdTaskTemplate) {
+ if err := copier.Copy(b, in); err != nil {
+ slog.Error("Copy", "err", err)
+ }
+ }, orm.Where("id=?", id)); err != nil {
+ return nil, reason.ErrDB.Withf(`Edit err[%s]`, err.Error())
+ }
+ return &out, nil
+}
+
+// DelVqdTaskTemplate Delete object
+func (c Core) DelVqdTaskTemplate(ctx context.Context, id int) (*VqdTaskTemplate, error) {
+ var out VqdTaskTemplate
+ if err := c.store.VqdTaskTemplate().Del(ctx, &out, orm.Where("id=?", id)); err != nil {
+ return nil, reason.ErrDB.Withf(`Del err[%s]`, err.Error())
+ }
+ return &out, nil
+}
diff --git a/internal/core/vqd/vqdtasktemplate.param.go b/internal/core/vqd/vqdtasktemplate.param.go
new file mode 100644
index 0000000..0541bef
--- /dev/null
+++ b/internal/core/vqd/vqdtasktemplate.param.go
@@ -0,0 +1,49 @@
+// Code generated by gowebx, DO AVOID EDIT.
+package vqd
+
+import (
+ "git.lnton.com/lnton/pkg/web"
+)
+
+type FindVqdTaskTemplateInput struct {
+ web.PagerFilter
+ Name string `form:"name"` // 名称
+}
+
+type EditVqdTaskTemplateInput struct {
+ Name string `json:"name"`
+ Plans string `json:"plans"`
+ Enable bool `json:"enable"`
+ //VqdConfig VqdConfig `json:"vqd_config"` // 诊断基础配置
+ //VqdLgtDark VqdLgtDark `json:"vqd_lgt_dark"` // 亮度检测
+ //VqdBlue VqdBlue `json:"vqd_blue"` // 蓝屏检查
+ //VqdClarity VqdClarity `json:"vqd_clarity"` // 清晰度检查
+ //VqdShark VqdShark `json:"vqd_shark"` // 抖动检查
+ //VqdFreeze VqdFreeze `json:"vqd_freeze"` // 冻结检测
+ //VqdColor VqdColor `json:"vqd_color"` // 偏色检测
+ //VqdOcclusion VqdOcclusion `json:"vqd_occlusion"` // 遮挡检测
+ //VqdNoise VqdNoise `json:"vqd_noise"` // 噪声检测
+ //VqdContrast VqdContrast `json:"vqd_contrast"` // 对比度检测
+ //VqdMosaic VqdMosaic `json:"vqd_mosaic"` // 马赛克检测
+ //VqdFlower VqdFlower `json:"vqd_flower"` // 花屏检测
+ Des string ` json:"des"` // 描述
+}
+
+type AddVqdTaskTemplateInput struct {
+ Name string `json:"name"`
+ Plans string `json:"plans"`
+ Enable bool `json:"enable"`
+ //VqdConfig VqdConfig `json:"vqd_config"` // 诊断基础配置
+ //VqdLgtDark VqdLgtDark `json:"vqd_lgt_dark"` // 亮度检测
+ //VqdBlue VqdBlue `json:"vqd_blue"` // 蓝屏检查
+ //VqdClarity VqdClarity `json:"vqd_clarity"` // 清晰度检查
+ //VqdShark VqdShark `json:"vqd_shark"` // 抖动检查
+ //VqdFreeze VqdFreeze `json:"vqd_freeze"` // 冻结检测
+ //VqdColor VqdColor `json:"vqd_color"` // 偏色检测
+ //VqdOcclusion VqdOcclusion `json:"vqd_occlusion"` // 遮挡检测
+ //VqdNoise VqdNoise `json:"vqd_noise"` // 噪声检测
+ //VqdContrast VqdContrast `json:"vqd_contrast"` // 对比度检测
+ //VqdMosaic VqdMosaic `json:"vqd_mosaic"` // 马赛克检测
+ //VqdFlower VqdFlower `json:"vqd_flower"` // 花屏检测
+ Des string ` json:"des"` // 描述
+}
diff --git a/internal/core/vqdsdk/core.go b/internal/core/vqdsdk/core.go
new file mode 100644
index 0000000..a5132cb
--- /dev/null
+++ b/internal/core/vqdsdk/core.go
@@ -0,0 +1,52 @@
+package vqdsdk
+
+import (
+ "context"
+ "easyvqd/internal/core/host"
+ "easyvqd/internal/core/vqd"
+ "time"
+)
+
+type Core struct {
+ HostCore *host.Core
+ VqdTaskCore *vqd.Core
+ //WorkflowCore *Workflow
+}
+
+func NewCore(HostCore *host.Core, VqdTaskCore *vqd.Core) *Core {
+ core := &Core{
+ HostCore: HostCore,
+ VqdTaskCore: VqdTaskCore,
+ //WorkflowCore: OpenVqdTask(VqdTaskCore),
+ }
+ time.AfterFunc(time.Duration(5)*time.Second, func() {
+ in := &vqd.AddVqdAlarmInput{
+ AlarmName: "遮挡告警",
+ AlarmValue: "",
+ ChannelID: "",
+ ChannelName: "",
+ TaskTemplateID: 0,
+ TaskTemplateName: "",
+ TaskID: 0,
+ TaskName: "",
+ FilePath: "",
+ }
+ core.VqdTaskCore.AddVqdAlarm(context.TODO(), in)
+ core.VqdTaskCore.AddVqdAlarm(context.TODO(), in)
+
+ })
+ // 启用任务管理器
+ return core
+}
+
+//
+//func OpenVqdTask(VqdTaskCore *vqd.Core) *Workflow {
+// wf := NewWorkflow(WorkflowConfig{
+// MaxConcurrency: 100, // 并发
+// CleanupInterval: 30 * time.Second, // 每30秒清理一次
+// MaxTaskHistory: 500, // 最多保留500个任务历史
+// RetentionTime: 60 * time.Second, // 任务保留1分钟
+// })
+//
+// return wf
+//}
diff --git a/internal/core/vqdsdk/mode.go b/internal/core/vqdsdk/mode.go
new file mode 100644
index 0000000..c703511
--- /dev/null
+++ b/internal/core/vqdsdk/mode.go
@@ -0,0 +1 @@
+package vqdsdk
diff --git a/internal/web/api/api.go b/internal/web/api/api.go
new file mode 100644
index 0000000..2b4f28e
--- /dev/null
+++ b/internal/web/api/api.go
@@ -0,0 +1,172 @@
+package api
+
+import (
+ "easyvqd/internal/web/api/static"
+ "expvar"
+ statics "github.com/gin-contrib/static"
+ "log/slog"
+ "net/http"
+ "os"
+ "path/filepath"
+ "runtime"
+ "runtime/debug"
+ "sort"
+ "strings"
+ "time"
+
+ "easyvqd/domain/version/versionapi"
+
+ localweb "easyvqd/pkg/web"
+
+ "git.lnton.com/lnton/pkg/web"
+ "github.com/gin-gonic/gin"
+)
+
+var startRuntime = time.Now()
+
+func setupRouter(r *gin.Engine, uc *Usecase) {
+ r.Use(
+ // 格式化输出到控制台,然后记录到日志
+ // 此处不做 recover,底层 http.server 也会 recover,但不会输出方便查看的格式
+ gin.CustomRecovery(func(c *gin.Context, err any) {
+ slog.Error("panic", "err", err, "stack", string(debug.Stack()))
+ c.AbortWithStatus(http.StatusInternalServerError)
+ }),
+ web.Metrics(),
+ web.Logger(),
+ // debug 环境中配合 debug 日志级别,记录请求体与响应体
+ web.LoggerWithBody(web.DefaultBodyLimit, func(_ *gin.Context) bool {
+ // true: 表示忽略记录日志
+ // !debug 表示非调试环境不记录
+ return !uc.Conf.Debug
+ }),
+ )
+ go web.CountGoroutines(10*time.Minute, 20)
+
+ auth := localweb.AuthMiddleware(uc.Conf.Server.HTTP.JwtSecret, uc.Conf.Plugin.HttpAPI+"/extensions/auth", "")
+ r.Any("/health", web.WrapH(uc.getHealth))
+ r.GET("/app/metrics/api", web.WrapH(uc.getMetricsAPI))
+ //快照
+ dir, _ := os.Getwd()
+ uploadsDir := filepath.Join(dir, "uploads")
+ r.Use(statics.Serve("/uploads", statics.LocalFile(uploadsDir, true)))
+
+ versionapi.Register(r, uc.Version, auth)
+ registerConfig(r, ConfigAPI{uc: uc, cfg: uc.Conf})
+ RegisterHostAPI(r, uc)
+ RegisterVqdTask(r, uc.VqdTaskAPI)
+
+ r.NoRoute(func(ctx *gin.Context) {
+ p := ctx.Request.URL.Path
+ if strings.HasPrefix(p, "/web/") {
+ q := ctx.Request.URL.RawQuery
+ target := "/web/"
+ if q != "" {
+ target = target + "?" + q
+ }
+ ctx.Redirect(http.StatusTemporaryRedirect, target)
+ return
+ }
+ if strings.HasPrefix(p, "/uploads/") {
+ q := ctx.Request.URL.RawQuery
+ target := "/uploads/"
+ if q != "" {
+ target = target + "?" + q
+ }
+ ctx.Redirect(http.StatusTemporaryRedirect, target)
+ return
+ }
+
+ if strings.HasPrefix(p, "/extensions/easyvqd") {
+ // 改为前缀替换并在当前请求内重新分发,而不是重定向
+ newPath := strings.TrimPrefix(p, "/extensions/easyvqd")
+ ctx.Request.URL.Path = newPath
+ r.HandleContext(ctx)
+ return
+ }
+ ctx.AbortWithStatus(http.StatusNotFound)
+ })
+
+ // 直接返回静态文件系统中的入口页
+ r.StaticFS("/web/", static.FileSystem())
+
+}
+
+type getHealthOutput struct {
+ Version string `json:"version"`
+ StartAt time.Time `json:"start_at"`
+ GitBranch string `json:"git_branch"`
+ GitHash string `json:"git_hash"`
+}
+
+func (uc *Usecase) getHealth(_ *gin.Context, _ *struct{}) (getHealthOutput, error) {
+ return getHealthOutput{
+ Version: uc.Conf.BuildVersion,
+ GitBranch: strings.Trim(expvar.Get("git_branch").String(), `"`),
+ GitHash: strings.Trim(expvar.Get("git_hash").String(), `"`),
+ StartAt: startRuntime,
+ }, nil
+}
+
+type getMetricsAPIOutput struct {
+ RealTimeRequests int64 `json:"real_time_requests"` // 实时请求数
+ TotalRequests int64 `json:"total_requests"` // 总请求数
+ TotalResponses int64 `json:"total_responses"` // 总响应数
+ RequestTop []KV `json:"request_top"` // 请求TOP
+ StatusCodeTop []KV `json:"status_code_top"` // 状态码TOP
+ Goroutines any `json:"goroutines"` // 协程数量
+ NumGC uint32 `json:"num_gc"` // gc 次数
+ SysAlloc uint64 `json:"sys_alloc"` // 内存占用
+ StartAt string `json:"start_at"` // 运行时间
+}
+
+func (uc *Usecase) getMetricsAPI(_ *gin.Context, _ *struct{}) (*getMetricsAPIOutput, error) {
+ req := expvar.Get("request").(*expvar.Int).Value()
+ reqs := expvar.Get("requests").(*expvar.Int).Value()
+ resps := expvar.Get("responses").(*expvar.Int).Value()
+ urls := expvar.Get(`requestURLs`).(*expvar.Map)
+ status := expvar.Get(`statusCodes`).(*expvar.Map)
+ u := sortExpvarMap(urls, 15)
+ s := sortExpvarMap(status, 15)
+ g := expvar.Get("goroutine_num").(expvar.Func)
+
+ var stats runtime.MemStats
+ runtime.ReadMemStats(&stats)
+
+ return &getMetricsAPIOutput{
+ RealTimeRequests: req,
+ TotalRequests: reqs,
+ TotalResponses: resps,
+ RequestTop: u,
+ StatusCodeTop: s,
+ Goroutines: g(),
+ NumGC: stats.NumGC,
+ SysAlloc: stats.Sys,
+ StartAt: startRuntime.Format(time.DateTime),
+ }, nil
+}
+
+type KV struct {
+ Key string
+ Value int64
+}
+
+func sortExpvarMap(data *expvar.Map, top int) []KV {
+ kvs := make([]KV, 0, 8)
+ data.Do(func(kv expvar.KeyValue) {
+ kvs = append(kvs, KV{
+ Key: kv.Key,
+ Value: kv.Value.(*expvar.Int).Value(),
+ })
+ })
+
+ sort.Slice(kvs, func(i, j int) bool {
+ return kvs[i].Value > kvs[j].Value
+ })
+
+ idx := top
+ if l := len(kvs); l < top {
+ idx = len(kvs)
+ }
+ return kvs[:idx]
+}
diff --git a/internal/web/api/config.go b/internal/web/api/config.go
new file mode 100644
index 0000000..c475be5
--- /dev/null
+++ b/internal/web/api/config.go
@@ -0,0 +1,94 @@
+package api
+
+import (
+ "easyvqd/internal/conf"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/gin-gonic/gin"
+ "github.com/ixugo/goddd/pkg/web"
+ "github.com/jinzhu/copier"
+ "github.com/pelletier/go-toml/v2"
+ "io"
+ "log/slog"
+ "strconv"
+ "sync"
+)
+
+var confMutex sync.Mutex
+
+type ConfigAPI struct {
+ cfg *conf.Bootstrap
+ uc *Usecase
+}
+
+func registerConfig(g gin.IRouter, api ConfigAPI, handler ...gin.HandlerFunc) {
+ group := g.Group("/api/configs", handler...)
+ //group.GET("", api.getToml)
+ //group.PUT("", api.editToml)
+
+ group.GET("/base", web.WarpH(api.getBase))
+ group.PUT("/base", web.WarpH(api.editBase))
+
+}
+
+type getBaseOutput conf.VqdConfig
+type editBaseInput conf.VqdConfig
+
+func (uc *ConfigAPI) editBase(c *gin.Context, in *editBaseInput) (any, error) {
+ uc.cfg.VqdConfig.FrmNum = in.FrmNum
+ uc.cfg.VqdConfig.SaveDay = in.SaveDay
+ uc.cfg.VqdConfig.IsDeepLearn = in.IsDeepLearn
+ conf.WriteConfig(uc.cfg, uc.cfg.ConfigDirPath())
+ return in, nil
+}
+func (uc *ConfigAPI) getBase(_ *gin.Context, _ *struct{}) (getBaseOutput, error) {
+ confMutex.Lock()
+ defer confMutex.Unlock()
+
+ return getBaseOutput{
+ FrmNum: uc.cfg.VqdConfig.FrmNum,
+ IsDeepLearn: uc.cfg.VqdConfig.IsDeepLearn,
+ SaveDay: uc.cfg.VqdConfig.SaveDay,
+ }, nil
+}
+func (uc *ConfigAPI) getToml(c *gin.Context) {
+ c.Header("Content-Type", "application/toml")
+ if err := toml.NewEncoder(c.Writer).Encode(uc.uc.Conf); err != nil {
+ slog.Error("获取配置失败", "err", err)
+ return
+ }
+}
+
+func (uc *ConfigAPI) editToml(c *gin.Context) {
+ b, err := io.ReadAll(io.LimitReader(c.Request.Body, 1024*20))
+ if err != nil {
+ web.Fail(c, reason.ErrBadRequest.SetMsg(err.Error()))
+ return
+ }
+ if len(b) <= 10 {
+ web.Fail(c, reason.ErrBadRequest.SetMsg("错误的文件"))
+ return
+ }
+
+ data, err := strconv.Unquote(string(b))
+ if err != nil {
+ web.Fail(c, reason.ErrBadRequest.SetMsg(err.Error()))
+ return
+ }
+
+ var cfg conf.Bootstrap
+ if err := toml.Unmarshal([]byte(data), &cfg); err != nil {
+ web.Fail(c, reason.ErrBadRequest.SetMsg(err.Error()))
+ return
+ }
+
+ if err := copier.Copy(uc.uc.Conf, cfg); err != nil {
+ web.Fail(c, reason.ErrServer.SetMsg(err.Error()))
+ return
+ }
+ if err := conf.WriteConfig(uc.uc.Conf, uc.uc.Conf.ConfigDirPath()); err != nil {
+ web.Fail(c, reason.ErrServer.SetMsg(err.Error()))
+ return
+ }
+
+ web.Success(c, gin.H{"msg": "ok"})
+}
diff --git a/internal/web/api/grpc_plugin.go b/internal/web/api/grpc_plugin.go
new file mode 100644
index 0000000..21abbb3
--- /dev/null
+++ b/internal/web/api/grpc_plugin.go
@@ -0,0 +1,40 @@
+package api
+
+import (
+ "easyvqd/internal/core/host"
+ "easyvqd/internal/core/media"
+ "encoding/json"
+ "log/slog"
+)
+
+type PluginGRPC struct {
+ Core *host.Core
+ Media *media.Core
+ uc *Usecase
+}
+
+func NewPluginGRPC(core *host.Core, mediaCore *media.Core, uc *Usecase) *PluginGRPC {
+ return &PluginGRPC{
+ Core: core,
+ Media: mediaCore,
+ uc: uc,
+ }
+}
+
+// 与Host刚建立连接就会通信,如果回调函数放在了API层,来不及注册回调,影响通信
+
+func RegisterPluginGRPC(hostCore *host.Core, mediaCore *media.Core, uc *Usecase) {
+ hostGRPC := NewPluginGRPC(hostCore, mediaCore, uc)
+ plugin := hostCore.Plugin
+ plugin.AddResponseHandler("start", hostGRPC.start)
+ // 这部分是主动处理Host的请求
+
+}
+
+func (pg PluginGRPC) start(requestID string, args json.RawMessage) (interface{}, error) {
+ slog.Info("Received 'start' from host", "request_id", requestID, "args", args)
+ return map[string]interface{}{
+ "status": "started",
+ "task": "task",
+ }, nil
+}
diff --git a/internal/web/api/host.go b/internal/web/api/host.go
new file mode 100644
index 0000000..1c533d0
--- /dev/null
+++ b/internal/web/api/host.go
@@ -0,0 +1,84 @@
+package api
+
+import (
+ "easyvqd/internal/core/host"
+ "net/http"
+ "net/http/httputil"
+ "net/url"
+
+ "git.lnton.com/lnton/pkg/web"
+ "github.com/gin-gonic/gin"
+)
+
+type HostAPI struct {
+ uc *Usecase
+}
+
+func NewHostAPI(uc *Usecase) *HostAPI {
+ return &HostAPI{
+ uc: uc,
+ }
+}
+
+func RegisterHostAPI(base gin.IRouter, uc *Usecase, handler ...gin.HandlerFunc) {
+ api := NewHostAPI(uc)
+ g := base.Group("/api", handler...)
+
+ g.GET("/devices", web.WrapH(api.findDevices))
+ g.GET("/channels", web.WrapH(api.findChannels))
+ base.POST("/api/login", api.login)
+}
+
+// login 代理登录
+//
+// 登录会代理到Host的登录接口,鉴权也会使用Host的接口
+func (ha *HostAPI) login(c *gin.Context) {
+ target, _ := url.Parse(ha.uc.Conf.Plugin.HttpAPI + "/login")
+ proxy := httputil.NewSingleHostReverseProxy(target)
+
+ origDirector := proxy.Director
+ proxy.Director = func(req *http.Request) {
+ origDirector(req)
+ // 保持原始方法、头、查询、体
+ req.Method = c.Request.Method
+ req.URL.Path = "/login"
+ req.URL.RawQuery = c.Request.URL.RawQuery
+ req.Header = c.Request.Header.Clone()
+ req.Body = c.Request.Body
+ req.Host = target.Host
+ }
+
+ proxy.ErrorHandler = func(rw http.ResponseWriter, r *http.Request, err error) {
+ http.Error(rw, err.Error(), http.StatusBadGateway)
+ }
+
+ proxy.ServeHTTP(c.Writer, c.Request)
+}
+
+func (ha *HostAPI) findDevices(c *gin.Context, in *host.FindDevicesInput) (any, error) {
+ // // 用于角色控制
+ // in.Level = web.GetLevel(c)
+ // in.Username = web.GetUsername(c)
+
+ out, err := ha.uc.HostCore.FindDevices(c, in)
+ if err != nil {
+ return nil, err
+ }
+
+ // 构造返回的body
+ return out, nil
+}
+
+func (ha *HostAPI) findChannels(c *gin.Context, in *host.FindChannelsInput) (any, error) {
+ // // 用于角色控制
+ // in.Level = web.GetLevel(c)
+ // in.Username = web.GetUsername(c)
+
+ out, err := ha.uc.HostCore.FindChannels(c, in)
+ if err != nil {
+ return nil, err
+ }
+
+ // 构造返回的body
+ return out, nil
+}
diff --git a/internal/web/api/provider.go b/internal/web/api/provider.go
new file mode 100644
index 0000000..ae39f1e
--- /dev/null
+++ b/internal/web/api/provider.go
@@ -0,0 +1,83 @@
+package api
+
+import (
+ "easyvqd/internal/core/host"
+ "easyvqd/internal/core/media"
+ "easyvqd/internal/core/vqd"
+ "easyvqd/internal/core/vqd/store/audioencodedb"
+ "easyvqd/internal/core/vqdsdk"
+ "net/http"
+
+ "easyvqd/domain/uniqueid"
+ "easyvqd/domain/uniqueid/store/uniqueiddb"
+ "easyvqd/domain/version/versionapi"
+ "easyvqd/internal/conf"
+
+ "git.lnton.com/lnton/pkg/orm"
+ "git.lnton.com/lnton/pkg/web"
+ "github.com/gin-gonic/gin"
+ "github.com/google/wire"
+ "gorm.io/gorm"
+)
+
+var (
+ ProviderVersionSet = wire.NewSet(versionapi.NewVersionCore)
+ ProviderSet = wire.NewSet(
+ wire.Struct(new(Usecase), "*"),
+ NewHTTPHandler,
+ NewVqdTaskCore,
+ NewVqdTaskAPI,
+ versionapi.New,
+ host.NewCore,
+ media.NewCore,
+ )
+)
+
+type Usecase struct {
+ Conf *conf.Bootstrap
+ DB *gorm.DB
+ Version versionapi.API
+ VqdTaskCore *vqd.Core
+ VqdTaskAPI VqdTaskAPI
+ HostCore *host.Core
+ MediaCore *media.Core
+ VqdSdkCore *vqdsdk.Core
+}
+
+// NewHTTPHandler 生成Gin框架路由内容
+func NewHTTPHandler(uc *Usecase) http.Handler {
+ cfg := uc.Conf
+ // 检查是否设置了 JWT 密钥,如果未设置,则生成一个长度为 32 的随机字符串作为密钥
+ if cfg.Server.HTTP.JwtSecret == "" {
+ uc.Conf.Server.HTTP.JwtSecret = orm.GenerateRandomString(32)
+ }
+ // 如果不处于调试模式,将 Gin 设置为发布模式
+ if !uc.Conf.Debug {
+ gin.SetMode(gin.ReleaseMode)
+ }
+ g := gin.New()
+ // 处理未找到路由的情况,返回 JSON 格式的 404 错误信息
+
+ // 如果启用了 Pprof,设置 Pprof 监控
+ if cfg.Server.HTTP.PProf.Enabled {
+ web.SetupPProf(g, &cfg.Server.HTTP.PProf.AccessIps)
+ }
+
+ setupRouter(g, uc)
+ RegisterPluginGRPC(uc.HostCore, uc.MediaCore, uc)
+ // 这个一定要在最后执行,避免在迁移过程中发生panic导致迁移一半
+ uc.Version.RecordVersion()
+ return g
+}
+
+// NewUniqueID 生成唯一 id
+func NewUniqueID(db *gorm.DB) uniqueid.Core {
+ store := uniqueiddb.NewDB(db).AutoMigrate(orm.GetEnabledAutoMigrate())
+ return uniqueid.NewCore(store, 6)
+}
+
+// NewVqdTaskCore 推流任务
+func NewVqdTaskCore(db *gorm.DB) *vqd.Core {
+ store := audioencodedb.NewDB(db).AutoMigrate(orm.EnabledAutoMigrate)
+ return vqd.NewCore(store)
+}
diff --git a/internal/web/api/static/static.go b/internal/web/api/static/static.go
new file mode 100644
index 0000000..4cfdd35
--- /dev/null
+++ b/internal/web/api/static/static.go
@@ -0,0 +1,21 @@
+package static
+
+import (
+ "embed"
+ "io/fs"
+ "net/http"
+)
+
+//go:embed www/*
+var www embed.FS
+
+func FileSystem() http.FileSystem {
+ // 将嵌入的根目录定位到 www 子目录,便于通过 /ui 直接访问 index.html
+ // 例如:c.FileFromFS("index.html", FileSystem()) 实际读取的是 www/index.html
+ sub, err := fs.Sub(www, "www")
+ if err != nil {
+ // 子目录创建失败则回退为原始嵌入根(不推荐,但避免崩溃)
+ return http.FS(www)
+ }
+ return http.FS(sub)
+}
diff --git a/internal/web/api/vqdalarm.go b/internal/web/api/vqdalarm.go
new file mode 100644
index 0000000..f018ade
--- /dev/null
+++ b/internal/web/api/vqdalarm.go
@@ -0,0 +1,78 @@
+package api
+
+import (
+ "easyvqd/internal/core/vqd"
+ "fmt"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/gin-gonic/gin"
+ "strconv"
+)
+
+func (a VqdTaskAPI) findVqdAlarm(c *gin.Context, in *vqd.FindVqdAlarmInput) (any, error) {
+ items, total, err := a.core.FindVqdAlarm(c.Request.Context(), in)
+ rows := make([]map[string]interface{}, 0)
+
+ for _, item := range items {
+ //row := structs.Map(item)
+ row := make(map[string]interface{})
+ row["id"] = item.ID
+ row["alarm_name"] = item.AlarmName
+ row["alarm_value"] = item.AlarmValue
+ row["channel_id"] = item.ChannelID
+ row["channel_name"] = item.ChannelName
+ row["task_template_id"] = item.TaskTemplateID
+ row["task_template_name"] = item.TaskTemplateName
+ row["task_id"] = item.TaskID
+ row["task_name"] = item.TaskName
+ row["file_path"] = item.FilePath
+ row["created_at"] = item.CreatedAt
+ row["updated_at"] = item.UpdatedAt
+
+ rows = append(rows, row)
+ }
+
+ return gin.H{"items": rows, "total": total}, err
+}
+func (a VqdTaskAPI) getVqdAlarm(c *gin.Context, _ *struct{}) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ item, err := a.core.GetVqdAlarm(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find vqd [%d] err [%s]`, ID, err.Error()))
+ }
+ row := make(map[string]interface{})
+
+ row["id"] = item.ID
+ row["alarm_name"] = item.AlarmName
+ row["alarm_value"] = item.AlarmValue
+ row["channel_id"] = item.ChannelID
+ row["channel_name"] = item.ChannelName
+ row["task_template_id"] = item.TaskTemplateID
+ row["task_template_name"] = item.TaskTemplateName
+ row["task_id"] = item.TaskID
+ row["task_name"] = item.TaskName
+ row["file_path"] = item.FilePath
+ row["created_at"] = item.CreatedAt
+ row["updated_at"] = item.UpdatedAt
+
+ return gin.H{"data": row}, err
+}
+
+func (a VqdTaskAPI) delVqdAlarm(c *gin.Context, _ *struct{}) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ _, err := a.core.DelVqdAlarm(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del vqd [%d] err [%s]`, ID, err.Error()))
+ }
+
+ return gin.H{"data": "OK!"}, err
+}
+func (a VqdTaskAPI) delVqdAlarmAll(c *gin.Context, in *vqd.DelVqdAlarmInput) (any, error) {
+ if len(in.IDs) == 0 {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del vqd ids is empty`))
+ }
+ _, err := a.core.DelVqdAlarmAll(c.Request.Context(), in.IDs)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del vqd [%d] err [%s]`, in.IDs, err.Error()))
+ }
+ return gin.H{"data": "OK!"}, err
+}
diff --git a/internal/web/api/vqdtask.go b/internal/web/api/vqdtask.go
new file mode 100644
index 0000000..38f73c7
--- /dev/null
+++ b/internal/web/api/vqdtask.go
@@ -0,0 +1,126 @@
+package api
+
+import (
+ "easyvqd/internal/conf"
+ "easyvqd/internal/core/host"
+ "easyvqd/internal/core/media"
+ "easyvqd/internal/core/vqd"
+ "easyvqd/internal/core/vqdsdk"
+ "fmt"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/gin-gonic/gin"
+ "github.com/ixugo/goddd/pkg/web"
+ "strconv"
+)
+
+type VqdTaskAPI struct {
+ core *vqd.Core
+ meidaCore *media.Core
+ vqdSdkCore *vqdsdk.Core
+ cfg *conf.Bootstrap
+ HostCore *host.Core
+}
+
+func NewVqdTaskAPI(core *vqd.Core, meidaCore *media.Core, vqdSdkCore *vqdsdk.Core, HostCore *host.Core, cfg *conf.Bootstrap) VqdTaskAPI {
+ return VqdTaskAPI{core: core, meidaCore: meidaCore, vqdSdkCore: vqdSdkCore, HostCore: HostCore, cfg: cfg}
+}
+
+func RegisterVqdTask(g gin.IRouter, api VqdTaskAPI, handler ...gin.HandlerFunc) {
+ {
+
+ groupTask := g.Group("/api/task", handler...)
+ groupTask.GET("", web.WarpH(api.findVqdTask))
+ groupTask.GET("/:id", web.WarpH(api.getVqdTask))
+ groupTask.PUT("/:id", web.WarpH(api.editVqdTask))
+ groupTask.POST("", web.WarpH(api.addVqdTask))
+ groupTask.DELETE("/:id", web.WarpH(api.delVqdTask))
+
+ groupTemplate := g.Group("/api/template", handler...)
+ groupTemplate.GET("", web.WarpH(api.findVqdTaskTemplate))
+ groupTemplate.GET("/:id", web.WarpH(api.getVqdTaskTemplate))
+ groupTemplate.PUT("/:id", web.WarpH(api.editVqdTaskTemplate))
+ groupTemplate.POST("", web.WarpH(api.addVqdTaskTemplate))
+ groupTemplate.DELETE("/:id", web.WarpH(api.delVqdTaskTemplate))
+
+ groupAlarm := g.Group("/api/alarm", handler...)
+ groupAlarm.GET("", web.WarpH(api.findVqdAlarm))
+ groupAlarm.GET("/:id", web.WarpH(api.getVqdAlarm))
+ groupAlarm.DELETE("/:id", web.WarpH(api.delVqdAlarm))
+ groupAlarm.DELETE("", web.WarpH(api.delVqdAlarmAll))
+
+ }
+}
+
+func (a VqdTaskAPI) findVqdTask(c *gin.Context, in *vqd.FindVqdTaskInput) (any, error) {
+ items, total, err := a.core.FindVqdTask(c.Request.Context(), in)
+ rows := make([]map[string]interface{}, 0)
+
+ for _, item := range items {
+ //row := structs.Map(item)
+ row := make(map[string]interface{})
+ row["name"] = item.Name
+ row["id"] = item.ID
+ row["channel_id"] = item.ChannelID
+ row["channel_name"] = item.ChannelName
+ row["task_template_id"] = item.TaskTemplateID
+ row["task_template_name"] = item.TaskTemplateName
+ row["created_at"] = item.CreatedAt
+ row["updated_at"] = item.UpdatedAt
+
+ rows = append(rows, row)
+ }
+
+ return gin.H{"items": rows, "total": total}, err
+}
+func (a VqdTaskAPI) getVqdTask(c *gin.Context, _ *struct{}) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ item, err := a.core.GetVqdTask(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find vqd [%d] err [%s]`, ID, err.Error()))
+ }
+ row := make(map[string]interface{})
+
+ row["name"] = item.Name
+ row["id"] = item.ID
+ row["channel_id"] = item.ChannelID
+ row["channel_name"] = item.ChannelName
+ row["task_template_id"] = item.TaskTemplateID
+ row["task_template_name"] = item.TaskTemplateName
+ row["created_at"] = item.CreatedAt
+ row["updated_at"] = item.UpdatedAt
+
+ row["des"] = item.Des
+ return gin.H{"data": row}, err
+}
+func (a VqdTaskAPI) addVqdTask(c *gin.Context, in *vqd.AddVqdTaskInput) (any, error) {
+ _, err := a.core.AddVqdTask(c.Request.Context(), in)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add vqd err [%s]`, err.Error()))
+ }
+
+ return gin.H{"data": "OK!"}, err
+}
+
+func (a VqdTaskAPI) editVqdTask(c *gin.Context, in *vqd.EditVqdTaskInput) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ _, err := a.core.GetVqdTask(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find vqd [%d] err [%s]`, ID, err.Error()))
+ }
+
+ _, err = a.core.EditVqdTask(c.Request.Context(), in, ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`edit vqd err [%s]`, err.Error()))
+ }
+ return gin.H{"data": "OK!"}, err
+}
+
+func (a VqdTaskAPI) delVqdTask(c *gin.Context, _ *struct{}) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ _, err := a.core.DelVqdTask(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del vqd [%d] err [%s]`, ID, err.Error()))
+ }
+
+ return gin.H{"data": "OK!"}, err
+}
diff --git a/internal/web/api/vqdtasktemplate.go b/internal/web/api/vqdtasktemplate.go
new file mode 100644
index 0000000..cec3f91
--- /dev/null
+++ b/internal/web/api/vqdtasktemplate.go
@@ -0,0 +1,80 @@
+package api
+
+import (
+ "easyvqd/internal/core/vqd"
+ "fmt"
+ "git.lnton.com/lnton/pkg/reason"
+ "github.com/gin-gonic/gin"
+ "strconv"
+)
+
+func (a VqdTaskAPI) findVqdTaskTemplate(c *gin.Context, in *vqd.FindVqdTaskTemplateInput) (any, error) {
+ items, total, err := a.core.FindVqdTaskTemplate(c.Request.Context(), in)
+ rows := make([]map[string]interface{}, 0)
+
+ for _, item := range items {
+ //row := structs.Map(item)
+ row := make(map[string]interface{})
+ row["id"] = item.ID
+ row["name"] = item.Name
+ row["des"] = item.Des
+ row["plans"] = item.Plans
+ row["enable"] = item.Enable
+ row["created_at"] = item.CreatedAt
+ row["updated_at"] = item.UpdatedAt
+
+ rows = append(rows, row)
+ }
+
+ return gin.H{"items": rows, "total": total}, err
+}
+func (a VqdTaskAPI) getVqdTaskTemplate(c *gin.Context, _ *struct{}) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ item, err := a.core.GetVqdTaskTemplate(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find vqd [%d] err [%s]`, ID, err.Error()))
+ }
+ row := make(map[string]interface{})
+
+ row["id"] = item.ID
+ row["name"] = item.Name
+ row["des"] = item.Des
+ row["plans"] = item.Plans
+ row["enable"] = item.Enable
+ row["created_at"] = item.CreatedAt
+ row["updated_at"] = item.UpdatedAt
+
+ return gin.H{"data": row}, err
+}
+func (a VqdTaskAPI) addVqdTaskTemplate(c *gin.Context, in *vqd.AddVqdTaskTemplateInput) (any, error) {
+ _, err := a.core.AddVqdTaskTemplate(c.Request.Context(), in)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`add vqd err [%s]`, err.Error()))
+ }
+
+ return gin.H{"data": "OK!"}, err
+}
+
+func (a VqdTaskAPI) editVqdTaskTemplate(c *gin.Context, in *vqd.EditVqdTaskTemplateInput) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ _, err := a.core.GetVqdTaskTemplate(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`find vqd [%d] err [%s]`, ID, err.Error()))
+ }
+
+ _, err = a.core.EditVqdTaskTemplate(c.Request.Context(), in, ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`edit vqd err [%s]`, err.Error()))
+ }
+ return gin.H{"data": "OK!"}, err
+}
+
+func (a VqdTaskAPI) delVqdTaskTemplate(c *gin.Context, _ *struct{}) (any, error) {
+ ID, _ := strconv.Atoi(c.Param("id"))
+ _, err := a.core.DelVqdTaskTemplate(c.Request.Context(), ID)
+ if err != nil {
+ return nil, reason.ErrServer.SetMsg(fmt.Sprintf(`del vqd [%d] err [%s]`, ID, err.Error()))
+ }
+
+ return gin.H{"data": "OK!"}, err
+}
diff --git a/logo.png b/logo.png
new file mode 100644
index 0000000..3369e51
Binary files /dev/null and b/logo.png differ
diff --git a/logo_dark.png b/logo_dark.png
new file mode 100644
index 0000000..c79d8eb
Binary files /dev/null and b/logo_dark.png differ
diff --git a/main.go b/main.go
new file mode 100644
index 0000000..b868ee7
--- /dev/null
+++ b/main.go
@@ -0,0 +1,111 @@
+package main
+
+import (
+ "encoding/json"
+ "expvar"
+ "flag"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strconv"
+ "time"
+
+ "easyvqd/internal/app"
+ "easyvqd/internal/conf"
+
+ "git.lnton.com/lnton/pkg/system"
+)
+
+var (
+ buildVersion = "0.0.1" // 构建版本号
+ gitBranch = "dev" // git 分支
+ gitHash = "debug" // git 提交点哈希值
+ release string // 发布模式 true/false
+ buildTime string // 构建时间戳
+)
+
+// 自定义配置目录
+var configDir = flag.String("conf", "./configs", "config directory, eg: -conf /configs/")
+
+// 默认使用随机端口
+// 可使用-http-port指定端口
+// 可医用-http-port = -1 使用配置文件的端口
+var httpPort = flag.Int("http-port", 0, "HTTP server port; 0 uses random")
+
+func getBuildRelease() bool {
+ v, _ := strconv.ParseBool(release)
+ return v
+}
+
+func main() {
+ flag.Parse()
+
+ // 初始化配置
+ var bc conf.Bootstrap
+ fileDir, _ := system.Abs(*configDir)
+ _ = os.MkdirAll(fileDir, 0o755)
+ filePath := filepath.Join(fileDir, "config.toml")
+ configIsNotExistWrite(filePath)
+ if err := conf.SetupConfig(&bc, filePath); err != nil {
+ panic(err)
+ }
+ LoadEnvConfig(&bc)
+
+ // 根据启动参数设置端口
+ if *httpPort >= 0 {
+ bc.Server.HTTP.Port = *httpPort
+ }
+ bc.Debug = !getBuildRelease()
+ bc.BuildVersion = buildVersion
+ bc.ConfigDir = fileDir
+ bc.ConfigPath = filePath
+
+ {
+ expvar.NewString("version").Set(buildVersion)
+ expvar.NewString("git_branch").Set(gitBranch)
+ expvar.NewString("git_hash").Set(gitHash)
+ expvar.NewString("build_time").Set(buildTime)
+ expvar.Publish("timestamp", expvar.Func(func() any {
+ return time.Now().Format(time.DateTime)
+ }))
+ }
+
+ app.Run(&bc)
+}
+
+// configIsNotExistWrite 配置文件不存在时,回写配置
+func configIsNotExistWrite(path string) {
+ if _, err := os.Stat(path); os.IsNotExist(err) {
+ if err := conf.WriteConfig(conf.DefaultConfig(), path); err != nil {
+ system.ErrPrintf("WriteConfig", "err", err)
+ }
+ }
+}
+
+type EnvPlugin struct {
+ HostGrpcPort int `json:"host_grpc_port"`
+ EasyVQDPort int `json:"easy_vqd_port"`
+}
+
+func LoadEnvConfig(bc *conf.Bootstrap) {
+ easygbsAPI := os.Getenv("EASYGBS_API")
+ fmt.Printf("环境变量EASYGBS_API='%s'", easygbsAPI)
+ if easygbsAPI != "" {
+ bc.Plugin.HttpAPI = easygbsAPI
+ }
+
+ pluginConfigs := os.Getenv("PLUGIN_CONFIGS")
+ cp := EnvPlugin{}
+ if err := json.Unmarshal([]byte(pluginConfigs), &cp); err != nil {
+ return
+ }
+ fmt.Printf("环境变量PLUGIN_CONFIGS='%s'", pluginConfigs)
+
+ // 将环境变量的值更新到全局
+ if cp.HostGrpcPort > 0 {
+ bc.Plugin.GrpcPort = cp.HostGrpcPort
+ }
+ if cp.EasyVQDPort > 0 {
+ bc.Server.HTTP.Port = cp.EasyVQDPort
+ }
+}
diff --git a/pkg/ffmpeg/core.go b/pkg/ffmpeg/core.go
new file mode 100644
index 0000000..8edcc5d
--- /dev/null
+++ b/pkg/ffmpeg/core.go
@@ -0,0 +1,197 @@
+package ffmpeg
+
+import (
+ "fmt"
+ "github.com/tcolgate/mp3"
+ "github.com/youpy/go-wav"
+ "io"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "time"
+)
+
+// 音频转码配置
+type TranscodeConfig struct {
+ InputPath string // 输入文件路径(MP3/WAV)
+ OutputPath string // 输出文件路径(G711A)
+ SampleRate int // 采样率(默认 8000 Hz,G711A 标准采样率)
+ Channels int // 声道数(默认 1,单声道)
+}
+
+// 默认转码配置
+func defaultTranscodeConfig(input, output string) TranscodeConfig {
+ return TranscodeConfig{
+ InputPath: input,
+ OutputPath: output,
+ SampleRate: 8000, // G711A 标准采样率
+ Channels: 1, // 单声道(电话/语音常用)
+ }
+}
+
+// 校验输入文件类型(仅允许 MP3/WAV)
+func validateInputFile(inputPath string) error {
+ // 检查文件是否存在
+ if _, err := os.Stat(inputPath); os.IsNotExist(err) {
+ return fmt.Errorf("输入文件不存在: %s", inputPath)
+ }
+
+ // 校验文件扩展名
+ ext := strings.ToLower(filepath.Ext(inputPath))
+ if ext != ".mp3" && ext != ".wav" {
+ return fmt.Errorf("仅支持 MP3/WAV 格式,当前文件扩展名: %s", ext)
+ }
+
+ return nil
+}
+
+// MP3/WAV 转 G711A(PCMA)
+func TranscodeToG711A(config TranscodeConfig) error {
+ // 1. 校验输入文件
+ if err := validateInputFile(config.InputPath); err != nil {
+ return err
+ }
+
+ // 2. 补全默认配置
+ if config.SampleRate <= 0 {
+ config.SampleRate = 8000
+ }
+ if config.Channels <= 0 {
+ config.Channels = 1
+ }
+
+ // 3. 确保输出目录存在
+ outputDir := filepath.Dir(config.OutputPath)
+ if err := os.MkdirAll(outputDir, 0755); err != nil {
+ return fmt.Errorf("创建输出目录失败: %v", err)
+ }
+
+ // 4. 构建 FFmpeg 命令
+ // 核心参数说明:
+ // -i: 输入文件
+ // -ar: 采样率
+ // -ac: 声道数
+ // -f: 输出格式(alaw 即 G711A)
+ // -y: 覆盖已存在的输出文件
+ cmdArgs := []string{
+ "-i", config.InputPath,
+ "-ar", fmt.Sprintf("%d", config.SampleRate),
+ "-ac", fmt.Sprintf("%d", config.Channels),
+ "-f", "alaw", // 指定输出格式为 G711A(PCMA)
+ "-y", // 覆盖输出文件(无需确认)
+ config.OutputPath,
+ }
+ dir, _ := os.Getwd() // Windows 路径用反斜杠,或双正斜杠
+ ffmpegPath := ""
+ switch runtime.GOOS {
+ case "linux":
+ ffmpegPath = filepath.Join(dir, "ffmpeg")
+ case "windows":
+ ffmpegPath = filepath.Join(dir, "ffmpeg.exe")
+ }
+
+ // 执行 FFmpeg 命令
+ cmd := exec.Command(ffmpegPath, cmdArgs...)
+ // 捕获 FFmpeg 输出(便于调试)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ return fmt.Errorf("转码失败: %v, FFmpeg 输出: %s", err, string(output))
+ }
+
+ // 校验输出文件是否生成
+ if _, err := os.Stat(config.OutputPath); os.IsNotExist(err) {
+ return fmt.Errorf("转码后输出文件未生成: %s", config.OutputPath)
+ }
+
+ fmt.Printf("转码成功!输入: %s → 输出: %s\n", config.InputPath, config.OutputPath)
+ return nil
+}
+func TranscodeToG711AFile(inputFile, outputFile string) error {
+ // 也可自定义配置(比如调整采样率)
+ /*
+ customConfig := TranscodeConfig{
+ InputPath: "./uploads/test.wav",
+ OutputPath: "./uploads/test_custom.g711a",
+ SampleRate: 16000, // 自定义采样率
+ Channels: 1,
+ }
+ err = TranscodeToG711A(customConfig)
+ */
+ // 使用默认配置转码
+ err := TranscodeToG711A(defaultTranscodeConfig(inputFile, outputFile))
+ if err != nil {
+ fmt.Printf("转码失败: %v\n", err)
+ return fmt.Errorf("转码失败: %v\n", err)
+ }
+ return nil
+
+}
+
+// GetMP3Duration 获取MP3文件时长
+func GetMP3Duration(filePath string) (time.Duration, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return 0, err
+ }
+ defer file.Close()
+
+ decoder := mp3.NewDecoder(file)
+ var frame mp3.Frame
+ var totalDuration float64
+ skipped := 0
+
+ for {
+ if err := decoder.Decode(&frame, &skipped); err != nil {
+ if err == io.EOF {
+ break
+ }
+ return 0, err
+ }
+ totalDuration += frame.Duration().Seconds()
+ }
+
+ duration := time.Duration(totalDuration * float64(time.Second))
+ return duration, nil
+}
+
+// GetWAVDurationOptimized 优化的WAV文件时长获取方法
+func GetWAVDurationOptimized(filePath string) (time.Duration, error) {
+ file, err := os.Open(filePath)
+ if err != nil {
+ return 0, err
+ }
+ defer file.Close()
+
+ reader := wav.NewReader(file)
+
+ // 使用库提供的Duration方法
+ duration, err := reader.Duration()
+ if err != nil {
+ return 0, err
+ }
+
+ return duration, nil
+}
+
+// GetAudioDuration 获取音频文件时长(支持MP3和WAV)
+func GetAudioDuration(filePath string) (time.Duration, string, error) {
+ // 根据文件扩展名判断文件类型
+ if len(filePath) > 4 {
+ ext := filePath[len(filePath)-4:]
+ switch ext {
+ case ".mp3":
+ duration, err := GetMP3Duration(filePath)
+ return duration, "MP3", err
+ case ".wav":
+ duration, err := GetWAVDurationOptimized(filePath)
+ return duration, "WAV", err
+ default:
+ return 0, "", fmt.Errorf("不支持的音频格式: %s", ext)
+ }
+ }
+
+ // 如果无法从扩展名判断,可以尝试根据文件头部信息判断
+ return 0, "", fmt.Errorf("无法识别的音频格式")
+}
diff --git a/pkg/pluginheart/model.go b/pkg/pluginheart/model.go
new file mode 100644
index 0000000..3738773
--- /dev/null
+++ b/pkg/pluginheart/model.go
@@ -0,0 +1,12 @@
+package pluginheart
+
+type HeartInput struct {
+ API string `json:"api"` // 当前服务端的 api 地址
+
+ PID int `json:"pid"` // 免填
+ Port int `json:"port"`
+}
+
+type ConsoleInput struct {
+ Msg string `json:"msg"`
+}
diff --git a/pkg/pluginheart/plugin.go b/pkg/pluginheart/plugin.go
new file mode 100644
index 0000000..53e4e11
--- /dev/null
+++ b/pkg/pluginheart/plugin.go
@@ -0,0 +1,95 @@
+package pluginheart
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "io"
+ "log/slog"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+)
+
+type Engine struct {
+ api string
+ cli *http.Client
+ name string
+}
+
+func NewEngine(name, api string) *Engine {
+ e := Engine{
+ api: api,
+ name: name,
+ cli: &http.Client{
+ Timeout: 5 * time.Second,
+ },
+ }
+ return &e
+}
+
+func (e *Engine) AutoHeart(in HeartInput, isService bool) {
+ ticker := time.NewTicker(5 * time.Second)
+ defer ticker.Stop()
+
+ maxFaildCount := 3
+ if isService {
+ maxFaildCount = 17280
+ }
+
+ var faildCount int
+ for {
+ if faildCount > maxFaildCount {
+ fmt.Println("heart failed")
+ os.Exit(0)
+ }
+
+ resp, err := e.Heart(in)
+ <-ticker.C
+
+ if err != nil {
+ slog.Error("heart failed", "error", err)
+ e.api = strings.ReplaceAll(e.api, "127.0.0.1", "localhost")
+ faildCount++
+ continue
+ }
+ body, _ := io.ReadAll(resp.Body)
+ resp.Body.Close()
+ if resp.StatusCode != 200 {
+ slog.Error("heart failed", "error", resp.StatusCode, "body", string(body))
+ faildCount++
+ continue
+ }
+ faildCount = 0
+ }
+}
+
+func (e *Engine) Heart(in HeartInput) (*http.Response, error) {
+ in.PID = os.Getpid()
+ b, _ := json.Marshal(in)
+ const path = `/extensions/heartbeat`
+
+ req, _ := http.NewRequest(http.MethodPost, e.api+path, bytes.NewReader(b))
+ SetExtensionName(req, e.name)
+ return e.cli.Do(req)
+}
+
+func (e *Engine) Console(in ConsoleInput) (*http.Response, error) {
+ b, _ := json.Marshal(in)
+ const path = `/extensions/console`
+
+ req, _ := http.NewRequest(http.MethodPost, e.api+path, bytes.NewReader(b))
+ SetExtensionName(req, e.name)
+ return e.cli.Do(req)
+}
+
+// GetExtensionName 获取扩展名称
+func GetExtensionName(req *http.Request) string {
+ return req.Header.Get("X-Extension-Name")
+}
+
+func SetExtensionName(req *http.Request, name string) {
+ req.Header.Set("Content-Type", "application/json")
+ req.Header.Set("X-Extension-Name", name)
+}
diff --git a/pkg/pluginheart/plugin_test.go b/pkg/pluginheart/plugin_test.go
new file mode 100644
index 0000000..0daa37f
--- /dev/null
+++ b/pkg/pluginheart/plugin_test.go
@@ -0,0 +1,27 @@
+package pluginheart
+
+import (
+ "fmt"
+ "testing"
+ "time"
+)
+
+func TestHeart(t *testing.T) {
+ e := NewEngine("easyntd", "http://localhost:10000")
+ _, err := e.Heart(HeartInput{
+ API: "http://localhost:10000",
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
+
+func TestConsole(t *testing.T) {
+ e := NewEngine("easyntd", "http://localhost:10000")
+ _, err := e.Console(ConsoleInput{
+ Msg: fmt.Sprintf("%s 发生了什么 name=%s", time.Now().Format(time.DateTime), "easyntd"),
+ })
+ if err != nil {
+ t.Fatal(err)
+ }
+}
diff --git a/pkg/web/jwt.go b/pkg/web/jwt.go
new file mode 100644
index 0000000..aef566c
--- /dev/null
+++ b/pkg/web/jwt.go
@@ -0,0 +1,141 @@
+package web
+
+import (
+ "fmt"
+ "log/slog"
+ "net/http"
+ "strings"
+
+ "git.lnton.com/lnton/pkg/reason"
+ "git.lnton.com/lnton/pkg/web"
+ "github.com/gin-gonic/gin"
+)
+
+const (
+ uid = "uid"
+ token = "token"
+ username = "username"
+ groupLevel = "group_level"
+ level = "level"
+ role = "role"
+)
+
+// AuthMiddleware 鉴权
+func AuthMiddleware(secret string,
+ authAddr string, /*第三方鉴权地址*/
+ failedRedirect string, /*鉴权失败后重定向地址*/
+) gin.HandlerFunc {
+ // var errResp = gin.H{
+ // "msg": "身份验证失败",
+ // }
+ return func(c *gin.Context) {
+
+ // 创建不进行鉴权
+ // err := systemAuth(c, secret)
+ // if err == nil {
+ // c.Next()
+ // return
+ // }
+ // slog.DebugContext(c.Request.Context(), "systemAuth鉴权失败", "err", err)
+
+ // 使用第三方鉴权
+ if authAddr != "" {
+ err := AuthenticateWithOAuth(c, authAddr)
+ if err == nil {
+ c.Next()
+ return
+ }
+ slog.DebugContext(c.Request.Context(), "AuthenticateWithOAuth鉴权失败", "err", err)
+ }
+ // 鉴权失败进行重定向
+ if failedRedirect != "" {
+ c.Writer.Header().Set("X-Redirect", failedRedirect)
+ }
+ web.AbortWithStatusJSON(c, reason.ErrUnauthorizedToken.SetMsg("身份验证失败"))
+ }
+}
+
+func systemAuth(c *gin.Context, secret string) error {
+ auth := c.Request.Header.Get(" Authorization")
+ const prefix = "Bearer "
+ if len(auth) <= len(prefix) || !strings.EqualFold(auth[:len(prefix)], prefix) {
+ return reason.ErrUnauthorizedToken.SetMsg("鉴权失败")
+ }
+ claims, err := web.ParseToken(auth[len(prefix):], secret)
+ if err != nil {
+ return reason.ErrUnauthorizedToken.SetMsg("身份已过期,请重新登录")
+ }
+ if err := claims.Valid(); err != nil {
+ return reason.ErrUnauthorizedToken.SetMsg("身份已过期,请重新登录")
+ }
+
+ c.Set(uid, claims.UID)
+ c.Set(username, claims.Username)
+ c.Set(token, auth)
+ c.Set(groupLevel, claims.GroupLevel)
+ return nil
+}
+
+// AuthenticateWithOAuth 第三方鉴权逻辑
+func AuthenticateWithOAuth(c *gin.Context, authAddr string) error {
+ req, err := http.NewRequest(http.MethodPost, authAddr, nil)
+ if err != nil {
+ // c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create request"})
+ return reason.ErrBadRequest.SetMsg("failed to create request")
+ }
+
+ // 转发请求头
+ for key, values := range c.Request.Header {
+ // 如果包含 origin 头,跳过
+ if strings.EqualFold(strings.ToLower(key), "origin") {
+ continue
+ }
+ for _, value := range values {
+ req.Header.Add(key, value)
+ }
+ }
+ clientIP := c.ClientIP() // 获取客户端 IP 地址
+ // 获取现有的 X-Forwarded-For 头
+ existingIPs := c.Request.Header.Get("X-Forwarded-For")
+
+ if existingIPs == "" {
+ // 如果没有 X-Forwarded-For 头,设置客户端 IP
+ req.Header.Set("X-Forwarded-For", clientIP)
+ } else {
+ // 如果已经存在 X-Forwarded-For 头,追加客户端 IP
+ req.Header.Set("X-Forwarded-For", fmt.Sprintf("%s, %s", existingIPs, clientIP))
+ }
+ // 发送请求
+ client := &http.Client{} // TODO:超时控制
+ resp, err := client.Do(req)
+ if err != nil {
+ return reason.ErrBadRequest.SetMsg("request to backend failed")
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return reason.ErrUnauthorizedToken.SetMsg("身份验证失败")
+ }
+
+ // 读取响应
+ // responseBody, err := ioutil.ReadAll(resp.Body)
+ // if err != nil {
+ // return reason.ErrBadRequest.SetMsg("failed to read response body")
+ // }
+ // respStruct := struct {
+ // Code int `json:"code"`
+ // Msg string `json:"msg"`
+ // }{}
+
+ // if err := json.Unmarshal(responseBody, &respStruct); err != nil || respStruct.Code != 200 {
+ // return reason.ErrUnauthorizedToken.SetMsg("身份验证失败")
+ // }
+ c.Set(uid, 224)
+ c.Set("groupID", 111)
+ c.Set(username, "api_admin")
+ c.Set(role, "admin")
+ c.Set(groupLevel, 1)
+ c.Set(level, 1)
+ c.Set(web.KeyRoleID, 1)
+ // c.Set(token, auth)
+ return nil
+}
diff --git a/web/.env.development b/web/.env.development
new file mode 100644
index 0000000..7e8c06b
--- /dev/null
+++ b/web/.env.development
@@ -0,0 +1,3 @@
+VITE_API_BASE_URL=/api
+VITE_WEB_BASE_URL=/
+VITE_AUDIO_BASE_URL=/
\ No newline at end of file
diff --git a/web/.env.production b/web/.env.production
new file mode 100644
index 0000000..6eeaaf5
--- /dev/null
+++ b/web/.env.production
@@ -0,0 +1,3 @@
+VITE_API_BASE_URL=/extensions/easyvqd/api
+VITE_WEB_BASE_URL=/extensions/easyvqd/web
+VITE_AUDIO_BASE_URL=/extensions/easyvqd/
\ No newline at end of file
diff --git a/web/.gitignore b/web/.gitignore
new file mode 100644
index 0000000..16a4c58
--- /dev/null
+++ b/web/.gitignore
@@ -0,0 +1,26 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+dish*
\ No newline at end of file
diff --git a/web/.npmrc b/web/.npmrc
new file mode 100644
index 0000000..c249c6d
--- /dev/null
+++ b/web/.npmrc
@@ -0,0 +1,2 @@
+registry=https://registry.npmmirror.com/
+# registry=https://registry.npmjs.org/
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..fd75b22
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,9 @@
+## 运行项目
+安装依赖
+yarn install
+
+运行项目
+yarn run dev
+
+运行tailwind
+yarn run tailwind
diff --git a/web/eslint.config.js b/web/eslint.config.js
new file mode 100644
index 0000000..1dafa56
--- /dev/null
+++ b/web/eslint.config.js
@@ -0,0 +1,33 @@
+import js from '@eslint/js'
+import globals from 'globals'
+import reactHooks from 'eslint-plugin-react-hooks'
+import reactRefresh from 'eslint-plugin-react-refresh'
+import tseslint from 'typescript-eslint'
+import { defineConfig, globalIgnores } from 'eslint/config'
+
+export default defineConfig([
+ globalIgnores(['dist']),
+ {
+ files: ['**/*.{ts,tsx}'],
+ extends: [
+ js.configs.recommended,
+ tseslint.configs.recommended,
+ reactHooks.configs['recommended-latest'],
+ reactRefresh.configs.vite,
+ ],
+ languageOptions: {
+ ecmaVersion: 2020,
+ globals: globals.browser,
+ },
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'off',
+ '@typescript-eslint/no-unsafe-assignment': 'off',
+ '@typescript-eslint/no-unsafe-member-access': 'off',
+ '@typescript-eslint/no-unsafe-call': 'off',
+ '@typescript-eslint/no-unsafe-return': 'off',
+ '@typescript-eslint/no-unsafe-argument': 'off',
+ "@typescript-eslint/no-unused-vars": "off",
+ "react-refresh/only-export-components": "off",
+ },
+ },
+])
diff --git a/web/index.html b/web/index.html
new file mode 100644
index 0000000..f847d27
--- /dev/null
+++ b/web/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ EasyVQD
+
+
+
+
+
+
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..f61a1ac
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,40 @@
+{
+ "name": "easyvqd",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host",
+ "build": "vite build",
+ "lint": "eslint .",
+ "tailwind": "npx tailwindcss -i ./src/input.css -o ./src/output.css --watch",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@tanstack/react-query": "^5.90.3",
+ "antd": "^5.27.5",
+ "axios": "^1.12.2",
+ "crypto-js": "^4.2.0",
+ "react": "^18.2.0",
+ "react-audio-player": "^0.17.0",
+ "react-audio-player-pro": "^1.3.3",
+ "react-dom": "^18.2.0",
+ "react-router-dom": "^7.9.4"
+ },
+ "devDependencies": {
+ "@eslint/js": "^9.36.0",
+ "@types/node": "^24.6.0",
+ "@types/react": "^19.1.16",
+ "@types/react-dom": "^19.1.9",
+ "@types/react-router-dom": "^5.3.3",
+ "@vitejs/plugin-react-swc": "^4.1.0",
+ "eslint": "^9.36.0",
+ "eslint-plugin-react-hooks": "^5.2.0",
+ "eslint-plugin-react-refresh": "^0.4.22",
+ "globals": "^16.4.0",
+ "tailwindcss": "3.4.17",
+ "typescript": "~5.9.3",
+ "typescript-eslint": "^8.45.0",
+ "vite": "^7.1.7"
+ }
+}
diff --git a/web/public/assets/img/noImg.png b/web/public/assets/img/noImg.png
new file mode 100644
index 0000000..c64066a
Binary files /dev/null and b/web/public/assets/img/noImg.png differ
diff --git a/web/src/App.tsx b/web/src/App.tsx
new file mode 100644
index 0000000..f52b931
--- /dev/null
+++ b/web/src/App.tsx
@@ -0,0 +1,44 @@
+import { useEffect, useState } from "react";
+import { Routes, Route, Navigate } from "react-router-dom";
+import Login from "./pages/Login";
+import { getLocalStorage, setLocalStorage } from "./utils/local";
+import Home from "./pages/Home";
+import { useNavigate } from "react-router-dom";
+
+function App() {
+ const navigate = useNavigate();
+ const [isLoading, setIsLoading] = useState(true);
+ useEffect(() => {
+ const urlParams = new URLSearchParams(window.location.search);
+ const urlToken = urlParams.get("token");
+ const localToken = getLocalStorage("token");
+ // 当前路由是否是登录路由
+ const isLoginRoute = window.location.pathname === "/web/login";
+
+ if (urlToken) {
+ setLocalStorage("token", urlToken);
+ }
+
+ if (localToken || urlToken) {
+ if (isLoginRoute) {
+ navigate("/home");
+ }
+ }
+
+ setIsLoading(false);
+ }, [navigate]);
+
+ if (isLoading) {
+ return Loading...
; // Or a proper loading component
+ }
+
+ return (
+
+ } />
+ } />
+ } />
+
+ );
+}
+
+export default App;
diff --git a/web/src/Context.tsx b/web/src/Context.tsx
new file mode 100644
index 0000000..d157dbd
--- /dev/null
+++ b/web/src/Context.tsx
@@ -0,0 +1,72 @@
+import { message } from "antd";
+import { MessageInstance } from "antd/es/message/interface";
+import { AxiosError } from "axios";
+import React, { createContext, useContext, useState } from "react";
+
+interface IGlobalContext {
+ children: React.ReactNode;
+ messageApi: MessageInstance;
+ contextHolder: React.ReactNode;
+ ErrorHandle: (error: any) => void;
+}
+
+export const GlobalContext = createContext(null);
+
+// ErrorHandle 仅处理 400 错误,此错误为业务逻辑相关错误
+type Error = {
+ reason: string;
+ msg: string;
+ details?: any;
+};
+
+export const GlobalContextProvider = ({
+ children,
+}: {
+ children: React.ReactNode;
+}) => {
+ const [messageApi, contextHolder] = message.useMessage();
+
+ function ErrorHandle(error: any) {
+ const err = error as AxiosError;
+
+ if (!err.response || !err.response.data) {
+ return;
+ }
+ const data = err.response.data as Error;
+
+ const key = Date.now().toString();
+ messageApi.error({
+ content: `${data.msg} ${data.details?.length > 0 ? "😦" : ""}`,
+ duration: 4,
+ key: key,
+ onClick(e) {
+ messageApi.destroy(key);
+ data.details?.map((v: string) => {
+ if (v) {
+ message.error({
+ content: v,
+ duration: 4,
+ });
+ }
+ });
+ },
+ });
+ }
+
+ return (
+
+ {contextHolder}
+ {children}
+
+ );
+};
+
+export const useGlobal = () => {
+ const context = useContext(GlobalContext);
+ if (!context) {
+ throw new Error("useGlobal must be used within an GlobalContextProvider");
+ }
+ return context;
+};
diff --git a/web/src/api/config.ts b/web/src/api/config.ts
new file mode 100644
index 0000000..f0379f6
--- /dev/null
+++ b/web/src/api/config.ts
@@ -0,0 +1,17 @@
+import { GET, PUT } from "./http";
+import type { UpdateConfigBaseReq, VqdConfigBaseDetailRes } from "../types/config";
+
+/**
+ * 获取详情
+ */
+export async function GetVqdConfigBase() {
+ return await GET(`/configs/base`);
+}
+
+/**
+ * 更新
+ * @param data 更新参数
+ */
+export async function UpdateConfigBase(data: UpdateConfigBaseReq) {
+ return await PUT(`/configs/base`, data);
+}
diff --git a/web/src/api/devices.ts b/web/src/api/devices.ts
new file mode 100644
index 0000000..530699b
--- /dev/null
+++ b/web/src/api/devices.ts
@@ -0,0 +1,14 @@
+import { ChannelReq, ChannelRes, DeviceReq, DeviceRes } from "../types/device";
+import { GET } from "./http";
+
+// 获取设备列表
+export const getDevices = "GetDevices";
+export async function GetDevices(data: DeviceReq) {
+ return await GET(`/devices`, data);
+}
+
+// 获取通道列表
+export const getChannels = "GetChannels";
+export async function GetChannels(data: ChannelReq) {
+ return await GET(`/channels`, data);
+}
diff --git a/web/src/api/http.ts b/web/src/api/http.ts
new file mode 100644
index 0000000..4d93597
--- /dev/null
+++ b/web/src/api/http.ts
@@ -0,0 +1,161 @@
+import { message } from "antd";
+import axios, { AxiosProgressEvent, GenericAbortSignal } from "axios";
+import { getLocalStorage, removeLocalStorage } from "../utils/local";
+const apiBaseUrl = import.meta.env.VITE_API_BASE_URL;
+const codeMessage: { [key: number]: string } = {
+ 200: "服务器成功返回请求的数据。",
+ 201: "新建或修改数据成功。",
+ 202: "一个请求已经进入后台排队(异步任务)。",
+ 204: "删除数据成功。",
+ // 400: "发出的请求有错误,服务器没有进行新建或修改数据的操作。",
+ 401: "用户没有权限(令牌、用户名、密码错误)。",
+ 403: "用户得到授权,但是访问是被禁止的。",
+ 404: "发出的请求针对的是不存在的记录,服务器没有进行操作。",
+ 406: "请求的格式不可得。",
+ 410: "请求的资源被永久删除,且不会再得到的。",
+ 422: "当创建一个对象时,发生一个验证错误。",
+ 500: "服务器发生错误,请检查服务器日志。",
+ 502: "网关错误。",
+ 503: "服务不可用,服务器暂时过载或维护。",
+ 504: "网关超时。",
+ 511: "没有权限 , 非法操作",
+};
+
+// 忽略错误处理的url
+const neglectUrl = ["/configs/info/web", "/stats", "/online/log"];
+
+const headers = {
+ "Content-Type": "application/json",
+};
+
+const baseURL = `${window.location.origin}${apiBaseUrl}`;
+
+export const service = axios.create({
+ baseURL,
+ timeout: 120000,
+ headers: headers,
+ responseType: "json",
+});
+
+service.interceptors.request.use((config) => {
+ const token = getLocalStorage("token");
+ if (!token) return config;
+ config.headers["authorization"] = `Bearer ${token}`;
+ return config;
+});
+
+service.interceptors.response.use(
+ (resp) => resp,
+ (error) => {
+ if (!error) {
+ message.error("Network Error");
+ return Promise.reject(error);
+ }
+
+ const resp = error.response;
+ const errTips = resp?.data["msg"];
+
+ let errorText = "";
+ if (resp?.status) {
+ errorText = codeMessage[resp?.status] || resp.statusText;
+ }
+
+ if (neglectUrl.includes(error.config.url)) {
+ return Promise.reject(error);
+ }
+ switch (resp?.status) {
+ case 401:
+ removeLocalStorage("token");
+ window.location.href = "/extensions/easyvqd/web/login";
+ break;
+ case 500:
+ message.error(errorText ?? "Server Error");
+ break;
+ case 504:
+ message.error(errorText ?? errTips ?? "Network Error");
+ break;
+ default:
+ console.log(
+ "🚀 ~ file: http.ts ~ line 50 ~ service.interceptors.response.use",
+ errorText
+ );
+ break;
+ }
+
+ return Promise.reject(error);
+ }
+);
+
+service.interceptors.request.use((config) => {
+ return config;
+});
+
+async function request(
+ url: string,
+ method: string,
+ data?: object,
+ signal?: GenericAbortSignal,
+ timeOut?: number,
+ responseType?: "json" | "blob" | "arraybuffer",
+ headers?: { [key: string]: string },
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
+) {
+ return await service.request({
+ url,
+ method,
+ data: method == "GET" ? {} : data,
+ params: method == "GET" ? data : {},
+ signal: signal,
+ timeout: timeOut,
+ responseType: responseType || "json",
+ headers: headers,
+ onUploadProgress: (progressEvent) => {
+ if (onUploadProgress) {
+ onUploadProgress(progressEvent);
+ }
+ },
+ });
+}
+// 查询
+export async function GET(
+ url: string,
+ params?: any,
+ signal?: GenericAbortSignal,
+ timeOut?: number,
+ responseType?: "json" | "blob" | "arraybuffer",
+ headers?: { [key: string]: string }
+) {
+ return request(url, "GET", params, signal, timeOut, responseType, headers);
+}
+
+// 添加
+export async function POST(
+ url: string,
+ params?: any,
+ signal?: GenericAbortSignal,
+ timeOut?: number,
+ responseType?: "json" | "blob" | "arraybuffer",
+ headers?: { [key: string]: string },
+ onUploadProgress?: (progressEvent: AxiosProgressEvent) => void
+) {
+ return request(
+ url,
+ "POST",
+ params,
+ signal,
+ timeOut,
+ responseType,
+ headers,
+ onUploadProgress
+ );
+}
+
+// 更新
+export async function PUT(url: string, params?: any) {
+ return request(url, "PUT", params);
+}
+
+// 删除
+export async function DELETE(url: string, params?: any) {
+ return request(url, "DELETE", params);
+}
diff --git a/web/src/api/login.ts b/web/src/api/login.ts
new file mode 100644
index 0000000..f1dd278
--- /dev/null
+++ b/web/src/api/login.ts
@@ -0,0 +1,28 @@
+import { CaptchaRes, LoginReq, LoginRes } from "../types/login";
+import { POST } from "./http";
+import CryptoJS from "crypto-js";
+
+export async function Login(data: LoginReq) {
+ return await POST(`/login`, {
+ ...data,
+ password: Sha256(data.password),
+ });
+}
+
+// //获取验证码
+// export const getCaptcha = "GetCaptcha";
+// export async function GetCaptcha() {
+// return await POST(`/captcha`, { username: "" });
+// }
+
+// 退出登录
+export async function Logout() {
+ return await POST<{ url: string }>(`/logout`, {});
+}
+
+function Sha256(password: string) {
+ const hash = CryptoJS.SHA256(password);
+ // 将散列值转换为字符串表示
+ const hashInString = hash.toString(CryptoJS.enc.Hex);
+ return hashInString;
+}
diff --git a/web/src/api/vqdalarm.ts b/web/src/api/vqdalarm.ts
new file mode 100644
index 0000000..bf7f3af
--- /dev/null
+++ b/web/src/api/vqdalarm.ts
@@ -0,0 +1,35 @@
+import { GET, POST, PUT, DELETE } from "./http";
+import type { VqdAlarmRes, VqdAlarmDetailRes, VqdAlarmReq, DelVqdAlarmReq, VqdAlarmBaseRes } from "../types/vqdalarm";
+
+/**
+ * 获取列表
+ * @returns 列表
+ */
+export async function GetVqdAlarm(data: VqdAlarmReq) {
+ return await GET(`/alarm`, data);
+}
+
+/**
+ * 获取详情
+ * @param id ID
+ */
+export async function GetVqdAlarmById(id: string) {
+ return await GET(`/alarm/${id}`);
+}
+
+
+/**
+ * 删除
+ * @param id ID
+ */
+export async function DeleteVqdAlarm(id: number) {
+ return await DELETE(`/alarm/${id}`);
+}
+
+/**
+ * 批量删除
+ * @param ids ID
+ */
+export async function DeleteVqdAlarmAll(data: DelVqdAlarmReq) {
+ return await DELETE(`/alarm`, data);
+}
diff --git a/web/src/api/vqdtask.ts b/web/src/api/vqdtask.ts
new file mode 100644
index 0000000..1ce6b62
--- /dev/null
+++ b/web/src/api/vqdtask.ts
@@ -0,0 +1,43 @@
+import { GET, POST, PUT, DELETE } from "./http";
+import type { VqdTaskBaseRes, VqdTaskRes, CreateVqdTaskReq, UpdateVqdTaskReq, VqdTaskDetailRes, VqdTaskReq } from "../types/vqdtask";
+
+/**
+ * 获取列表
+ * @returns 列表
+ */
+export async function GetVqdTask(data: VqdTaskReq) {
+ return await GET(`/task`, data);
+}
+
+/**
+ * 创建
+ * @param data 创建参数
+ */
+export async function CreateVqdTask(data: CreateVqdTaskReq) {
+ return await POST(`/task`, data);
+}
+
+/**
+ * 获取详情
+ * @param id ID
+ */
+export async function GetVqdTaskById(id: string) {
+ return await GET(`/task/${id}`);
+}
+
+/**
+ * 更新
+ * @param data 更新参数(需包含 id)
+ */
+export async function UpdateVqdTask(data: UpdateVqdTaskReq) {
+ const { id, ...payload } = data;
+ return await PUT(`/task/${id}`, payload);
+}
+
+/**
+ * 删除
+ * @param id ID
+ */
+export async function DeleteVqdTask(id: number) {
+ return await DELETE(`/task/${id}`);
+}
diff --git a/web/src/api/vqdtasktemplate.ts b/web/src/api/vqdtasktemplate.ts
new file mode 100644
index 0000000..5b8a37f
--- /dev/null
+++ b/web/src/api/vqdtasktemplate.ts
@@ -0,0 +1,43 @@
+import { GET, POST, PUT, DELETE } from "./http";
+import type { VqdTaskTemplateBaseRes, VqdTaskTemplateRes, CreateVqdTaskTemplateReq, UpdateVqdTaskTemplateReq, VqdTaskTemplateDetailRes, VqdTaskTemplateReq } from "../types/vqdtasktemplate";
+
+/**
+ * 获取列表
+ * @returns 列表
+ */
+export async function GetVqdTaskTemplate(data: VqdTaskTemplateReq) {
+ return await GET(`/template`, data);
+}
+
+/**
+ * 创建
+ * @param data 创建参数
+ */
+export async function CreateVqdTaskTemplate(data: CreateVqdTaskTemplateReq) {
+ return await POST(`/template`, data);
+}
+
+/**
+ * 获取详情
+ * @param id ID
+ */
+export async function GetVqdTaskTemplateById(id: string) {
+ return await GET(`/template/${id}`);
+}
+
+/**
+ * 更新
+ * @param data 更新参数(需包含 id)
+ */
+export async function UpdateVqdTaskTemplate(data: UpdateVqdTaskTemplateReq) {
+ const { id, ...payload } = data;
+ return await PUT(`/template/${id}`, payload);
+}
+
+/**
+ * 删除
+ * @param id ID
+ */
+export async function DeleteVqdTaskTemplate(id: number) {
+ return await DELETE(`/template/${id}`);
+}
diff --git a/web/src/components/AddVqdTask.tsx b/web/src/components/AddVqdTask.tsx
new file mode 100644
index 0000000..5f1a5f8
--- /dev/null
+++ b/web/src/components/AddVqdTask.tsx
@@ -0,0 +1,155 @@
+import { forwardRef, useImperativeHandle, useState, useRef } from "react";
+import { Modal, Form, Input, Radio, Button, message, Space } from "antd";
+import { useMutation } from "@tanstack/react-query";
+import { CreateVqdTask, UpdateVqdTask } from "../api/vqdtask";
+import type { CreateVqdTaskReq, VqdTaskItem } from "../types/vqdtask";
+import { useGlobal } from "../Context";
+
+
+interface AddVqdTaskProps {
+ title: string;
+ onSuccess: () => void;
+}
+
+export interface AddVqdTaskRef {
+ open: (task?: VqdTaskItem) => void;
+}
+
+const AddVqdTask = forwardRef(
+ ({ title, onSuccess }, ref) => {
+ const [open, setOpen] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [form] = Form.useForm();
+ const { ErrorHandle } = useGlobal();
+ useImperativeHandle(ref, () => ({
+ open: (task?: VqdTaskItem) => {
+ if (task) {
+ setEditing(true);
+ const formValues = {
+ name: task.name,
+ id: task.id,
+ channel_id: task.channel_id,
+ channel_name: task.channel_name,
+ task_template_id: task.task_template_id,
+ task_template_name: task.task_template_name,
+ enable: task.enable,
+ };
+ form.setFieldsValue(formValues);
+ } else {
+ setEditing(false);
+ form.resetFields();
+ // form.setFieldsValue({
+ // bid: "2",
+ // });
+ }
+ setOpen(true);
+ },
+ }));
+
+ const { mutate: createMutate, isPending: creating } = useMutation({
+ mutationFn: CreateVqdTask,
+ onSuccess: () => {
+ message.success("创建任务成功");
+ handleClose();
+ onSuccess?.();
+ },
+ onError: ErrorHandle,
+ });
+
+ const { mutate: updateMutate, isPending: updating } = useMutation({
+ mutationFn: UpdateVqdTask,
+ onSuccess: () => {
+ message.success("更新任务成功");
+ handleClose();
+ onSuccess?.();
+ },
+ onError: ErrorHandle,
+ });
+
+ const handleClose = () => {
+ setOpen(false);
+ setEditing(false);
+ form.resetFields();
+ };
+
+ return (
+ form.submit()}
+ confirmLoading={creating || updating}
+ >
+
+
+
+
+
+
+
+ {editing && (
+
+
+
+ )}
+
+
+ );
+ }
+);
+
+export default AddVqdTask;
diff --git a/web/src/components/AddVqdTaskTemplate.tsx b/web/src/components/AddVqdTaskTemplate.tsx
new file mode 100644
index 0000000..0cd8876
--- /dev/null
+++ b/web/src/components/AddVqdTaskTemplate.tsx
@@ -0,0 +1,144 @@
+import { forwardRef, useImperativeHandle, useState, useRef } from "react";
+import { Modal, Form, Input, Radio, Button, message, Space } from "antd";
+import { useMutation } from "@tanstack/react-query";
+import { CreateVqdTaskTemplate, UpdateVqdTaskTemplate } from "../api/vqdtasktemplate";
+import type { CreateVqdTaskTemplateReq, VqdTaskTemplateItem } from "../types/vqdtasktemplate";
+import { useGlobal } from "../Context";
+
+
+interface AddVqdTaskTemplateProps {
+ title: string;
+ onSuccess: () => void;
+}
+
+export interface AddVqdTaskTemplateRef {
+ open: (task?: VqdTaskTemplateItem) => void;
+}
+
+const AddVqdTaskTemplate = forwardRef(
+ ({ title, onSuccess }, ref) => {
+ const [open, setOpen] = useState(false);
+ const [editing, setEditing] = useState(false);
+ const [form] = Form.useForm();
+ const { ErrorHandle } = useGlobal();
+ useImperativeHandle(ref, () => ({
+ open: (task?: VqdTaskTemplateItem) => {
+ if (task) {
+ setEditing(true);
+ const formValues = {
+ name: task.name,
+ id: task.id,
+ plans: task.plans,
+ enable: task.enable,
+ };
+ form.setFieldsValue(formValues);
+ } else {
+ setEditing(false);
+ form.resetFields();
+ // form.setFieldsValue({
+ // bid: "2",
+ // });
+ }
+ setOpen(true);
+ },
+ }));
+
+ const { mutate: createMutate, isPending: creating } = useMutation({
+ mutationFn: CreateVqdTaskTemplate,
+ onSuccess: () => {
+ message.success("创建任务成功");
+ handleClose();
+ onSuccess?.();
+ },
+ onError: ErrorHandle,
+ });
+
+ const { mutate: updateMutate, isPending: updating } = useMutation({
+ mutationFn: UpdateVqdTaskTemplate,
+ onSuccess: () => {
+ message.success("更新任务成功");
+ handleClose();
+ onSuccess?.();
+ },
+ onError: ErrorHandle,
+ });
+
+ const handleClose = () => {
+ setOpen(false);
+ setEditing(false);
+ form.resetFields();
+ };
+
+ return (
+ form.submit()}
+ confirmLoading={creating || updating}
+ >
+
+
+
+
+
+
+
+ {editing && (
+
+
+
+ )}
+
+
+ );
+ }
+);
+
+export default AddVqdTaskTemplate;
diff --git a/web/src/components/Box.tsx b/web/src/components/Box.tsx
new file mode 100644
index 0000000..41dafd7
--- /dev/null
+++ b/web/src/components/Box.tsx
@@ -0,0 +1,34 @@
+import React from "react";
+
+const Box: React.FC<{
+ children: React.ReactNode;
+ style?: React.CSSProperties;
+ className?: string; // 添加className属性
+}> = ({ children, style, className }) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default Box;
+
+// 定义一个 react 函数,返回一个 p 标签,其内容是 children 的值
+// 用于 form 表单的 tips
+export function Tips({
+ children,
+ style,
+}: {
+ children: React.ReactNode;
+ style?: React.CSSProperties;
+}) {
+ return (
+ {children}
+ );
+}
diff --git a/web/src/components/Filter.tsx b/web/src/components/Filter.tsx
new file mode 100644
index 0000000..6a5d2a8
--- /dev/null
+++ b/web/src/components/Filter.tsx
@@ -0,0 +1,77 @@
+import { Select, Space, Input } from "antd";
+import { LoadingOutlined, SearchOutlined } from "@ant-design/icons";
+import React from "react";
+
+// 自动搜索输入框(带防抖)
+interface AutoSearchProps {
+ onSearch: (value: string) => void;
+ loading: boolean;
+ placeholder?: string;
+ delay?: number; // 防抖延迟毫秒
+}
+
+const AutoSearch: React.FC = ({
+ onSearch,
+ loading,
+ placeholder,
+ delay = 400,
+}) => {
+ const [value, setValue] = React.useState("");
+ const timerRef = React.useRef(null);
+
+ const triggerSearch = React.useCallback(
+ (val: string) => {
+ onSearch?.(val.trim());
+ },
+ [onSearch]
+ );
+
+ React.useEffect(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ timerRef.current = window.setTimeout(() => {
+ triggerSearch(value);
+ }, delay);
+ return () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ };
+ }, [value, delay, triggerSearch]);
+
+ return (
+ setValue(e.target.value)}
+ onPressEnter={() => triggerSearch(value)}
+ placeholder={placeholder}
+ suffix={loading ? : }
+ />
+ );
+};
+
+interface IFilterProps {
+ searchLoading: boolean;
+ onSearchChange: (value: string) => void;
+}
+
+const Filter: React.FC = ({
+ searchLoading,
+ onSearchChange,
+}) => {
+ return (
+
+
+
+ );
+};
+
+export default Filter;
diff --git a/web/src/components/VqdAlarm.tsx b/web/src/components/VqdAlarm.tsx
new file mode 100644
index 0000000..de6cac7
--- /dev/null
+++ b/web/src/components/VqdAlarm.tsx
@@ -0,0 +1,201 @@
+import { useRef, useState, useMemo } from "react";
+import { Table, Button, Space, Popconfirm, Flex, message, Tooltip } from "antd";
+import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { GetVqdAlarm, DeleteVqdAlarm, DeleteVqdAlarmAll } from "../api/vqdalarm";
+import type { VqdAlarmItem } from "../types/vqdalarm";
+import type { ColumnsType } from "antd/es/table";
+import ChannelModel, { IChannelModelFunc } from "./channel/Channel";
+import { useGlobal } from "../Context";
+import { FormatFileSizeToString } from "../utils/rate";
+import { formatSecondsToHMS } from "../utils/time";
+import Filter from "./Filter";
+export default function VqdAlarmPage() {
+ const { ErrorHandle } = useGlobal();
+ const [pagination, setPagination] = useState({
+ page: 1,
+ size: 10,
+ name: ""
+ });
+
+ // 获取任务列表
+ const {
+ data: storageResponse,
+ isLoading,
+ refetch,
+ } = useQuery({
+ queryKey: ["storage", pagination],
+ queryFn: () =>
+ GetVqdAlarm({ ...pagination })
+ .then((res) => res.data)
+ .catch((err) => {
+ ErrorHandle(err);
+ throw err;
+ }),
+ // refetchInterval: 4000,
+ retry: 2,
+ });
+
+ // 删除任务
+ const [delLoadings, setDelLoadings] = useState([]);
+ const { mutate: deleteMutation } = useMutation({
+ mutationFn: DeleteVqdAlarm,
+ onMutate: (id: number) => {
+ setDelLoadings((prev) => [...prev, id]);
+ },
+ onSuccess: (_, ctx) => {
+ setDelLoadings((prev) => prev.filter((item) => item !== ctx));
+ message.success("删除成功");
+ refetch();
+ },
+ onError: (error: Error, ctx) => {
+ setDelLoadings((prev) => prev.filter((item) => item !== ctx));
+ ErrorHandle(error);
+ },
+ });
+
+ // 处理分页变化
+ const handleTableChange = (page: number, pageSize?: number) => {
+ setPagination((prev) => ({
+ ...prev,
+ page: page,
+ size: pageSize || prev.size,
+ }));
+ };
+
+ // 客户端分页数据
+ const dataSource = useMemo(() => {
+ const items = storageResponse?.items || [];
+ const start = (pagination.page - 1) * pagination.size;
+ const end = start + pagination.size;
+ return items.slice(start, end);
+ }, [storageResponse, pagination]);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const rowSelection = {
+ selectedRowKeys,
+ onChange: (
+ newSelectedRowKeys: React.Key[],
+ selectedRows: VqdAlarmItem[]
+ ) => {
+ setSelectedRowKeys([...newSelectedRowKeys]);
+ },
+ };
+ // 批量删除任务
+ const { mutate: deleteMutationAll, isPending: delAllLoadings } = useMutation({
+ mutationFn: DeleteVqdAlarmAll,
+ onSuccess: () => {
+ message.success("批量删除成功");
+ refetch()
+ setSelectedRowKeys([])
+ },
+ onError: ErrorHandle,
+ retry: 0,
+ });
+ // 表格列定义
+ const columns: ColumnsType = [
+ {
+ title: "名称",
+ dataIndex: "alarm_name",
+ align: "center",
+ },
+ {
+ title: "文件名称",
+ dataIndex: "file_path",
+ align: "center",
+ render: (text, record) => (
+
+ {text}
+
+ ),
+ },
+ {
+ title: "描述",
+ dataIndex: "des",
+ align: "center",
+ },
+ {
+ title: "创建日期",
+ dataIndex: "created_at",
+ align: "center",
+ render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
+ },
+ {
+ title: "操作",
+ align: "center",
+ width: 120,
+ fixed: "right",
+ render: (_, record) => (
+
+
+
+ {
+ if (record.id) {
+ deleteMutation(record.id);
+ }
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+ }
+ />
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+
+ {
+ deleteMutationAll({ ids: selectedRowKeys as number[] })
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+
+ 批量删除
+
+
+
+
+
+ {
+ setPagination({ ...pagination, name: value });
+ }}
+ />
+
+ {/* 表格 */}
+
`共 ${total} 条`,
+ onChange: handleTableChange,
+ onShowSizeChange: handleTableChange,
+ }}
+ />
+
+ );
+}
diff --git a/web/src/components/VqdConfig.tsx b/web/src/components/VqdConfig.tsx
new file mode 100644
index 0000000..d398027
--- /dev/null
+++ b/web/src/components/VqdConfig.tsx
@@ -0,0 +1,86 @@
+import { forwardRef, useImperativeHandle, useState, useRef } from "react";
+import { Form, Switch, Button, message, InputNumber } from "antd";
+import { useMutation, useQuery } from "@tanstack/react-query";
+import { GetVqdConfigBase, UpdateConfigBase } from "../api/config";
+import type { UpdateConfigBaseReq, VqdConfigBaseDetailRes } from "../types/config";
+import { useGlobal } from "../Context";
+export default function VqdTaskPage() {
+
+ const [form] = Form.useForm();
+ const { ErrorHandle } = useGlobal();
+
+ const { mutate: updateMutate, isPending: updating } = useMutation({
+ mutationFn: UpdateConfigBase,
+ onSuccess: () => {
+ message.success("更新成功");
+ },
+ onError: ErrorHandle,
+ });
+ const handleSave = () => {
+ form.submit()
+ }
+ // 获取存储列表
+ const { refetch } = useQuery({
+ queryKey: ["config"],
+ queryFn: () =>
+ GetVqdConfigBase()
+ .then((res) => {
+ console.log(res.data);
+ const formValues = {
+ frm_num: res.data.frm_num,
+ is_deep_learn: res.data.is_deep_learn,
+ save_day: res.data.save_day,
+ };
+ form.setFieldsValue(formValues);
+ })
+ .catch((err) => {
+ ErrorHandle(err);
+ throw err;
+ }),
+ retry: 2,
+ });
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ 保存
+
+
+
+ );
+}
diff --git a/web/src/components/VqdTask.tsx b/web/src/components/VqdTask.tsx
new file mode 100644
index 0000000..c9ec787
--- /dev/null
+++ b/web/src/components/VqdTask.tsx
@@ -0,0 +1,212 @@
+import { useRef, useState, useMemo } from "react";
+import { Table, Button, Space, Popconfirm, Flex, message, Tooltip } from "antd";
+import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { GetVqdTask, DeleteVqdTask } from "../api/vqdtask";
+import type { VqdTaskItem } from "../types/vqdtask";
+import type { ColumnsType } from "antd/es/table";
+import AddVqdTask, { AddVqdTaskRef } from "./AddVqdTask";
+import ChannelModel, { IChannelModelFunc } from "./channel/Channel";
+import { useGlobal } from "../Context";
+import { FormatFileSizeToString } from "../utils/rate";
+import { formatSecondsToHMS } from "../utils/time";
+import Filter from "./Filter";
+export default function VqdTaskPage() {
+ const { ErrorHandle } = useGlobal();
+ const dialogRef = useRef(null);
+ const [pagination, setPagination] = useState({
+ page: 1,
+ size: 10,
+ name: ""
+ });
+
+ // 获取任务列表
+ const {
+ data: storageResponse,
+ isLoading,
+ refetch,
+ } = useQuery({
+ queryKey: ["storage", pagination],
+ queryFn: () =>
+ GetVqdTask({ ...pagination })
+ .then((res) => res.data)
+ .catch((err) => {
+ ErrorHandle(err);
+ throw err;
+ }),
+ // refetchInterval: 4000,
+ retry: 2,
+ });
+
+ // 删除任务
+ const [delLoadings, setDelLoadings] = useState([]);
+ const { mutate: deleteMutation } = useMutation({
+ mutationFn: DeleteVqdTask,
+ onMutate: (id: number) => {
+ setDelLoadings((prev) => [...prev, id]);
+ },
+ onSuccess: (_, ctx) => {
+ setDelLoadings((prev) => prev.filter((item) => item !== ctx));
+ message.success("删除成功");
+ refetch();
+ },
+ onError: (error: Error, ctx) => {
+ setDelLoadings((prev) => prev.filter((item) => item !== ctx));
+ ErrorHandle(error);
+ },
+ });
+
+ // 打开新增模态框
+ const handleAdd = () => {
+ dialogRef.current?.open();
+ };
+
+ // 打开编辑模态框
+ const handleEdit = (disk: VqdTaskItem) => {
+ dialogRef.current?.open(disk);
+ };
+
+ // 处理分页变化
+ const handleTableChange = (page: number, pageSize?: number) => {
+ setPagination((prev) => ({
+ ...prev,
+ page: page,
+ size: pageSize || prev.size,
+ }));
+ };
+
+ // 客户端分页数据
+ const dataSource = useMemo(() => {
+ const items = storageResponse?.items || [];
+ const start = (pagination.page - 1) * pagination.size;
+ const end = start + pagination.size;
+ return items.slice(start, end);
+ }, [storageResponse, pagination]);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const rowSelection = {
+ selectedRowKeys,
+ onChange: (
+ newSelectedRowKeys: React.Key[],
+ selectedRows: VqdTaskItem[]
+ ) => {
+ setSelectedRowKeys([...newSelectedRowKeys]);
+ },
+ };
+ // 表格列定义
+ const columns: ColumnsType = [
+ {
+ title: "名称",
+ dataIndex: "name",
+ align: "center",
+ },
+ {
+ title: "文件名称",
+ dataIndex: "file_name",
+ align: "center",
+ render: (text, record) => (
+
+ {text}
+
+ ),
+ },
+ {
+ title: "描述",
+ dataIndex: "des",
+ align: "center",
+ },
+ {
+ title: "创建日期",
+ dataIndex: "created_at",
+ align: "center",
+ render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
+ },
+ {
+ title: "操作",
+ align: "center",
+ width: 120,
+ fixed: "right",
+ render: (_, record) => (
+
+
+
+ } onClick={() => handleEdit(record)} />
+
+ {
+ if (record.id) {
+ deleteMutation(record.id);
+ }
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+ }
+ />
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ } onClick={handleAdd}>
+ 新增任务
+
+ {/* {
+
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+
+ 批量删除
+
+ */}
+
+
+
+ {
+ setPagination({ ...pagination, name: value });
+ }}
+ />
+
+ {/* 表格 */}
+
`共 ${total} 条`,
+ onChange: handleTableChange,
+ onShowSizeChange: handleTableChange,
+ }}
+ />
+
+ {/* 编辑模态框 */}
+ refetch()}
+ />
+
+ );
+}
diff --git a/web/src/components/VqdTaskTemplate.tsx b/web/src/components/VqdTaskTemplate.tsx
new file mode 100644
index 0000000..6a06a20
--- /dev/null
+++ b/web/src/components/VqdTaskTemplate.tsx
@@ -0,0 +1,211 @@
+import { useRef, useState, useMemo } from "react";
+import { Table, Button, Space, Popconfirm, Flex, message, Tooltip } from "antd";
+import { EditOutlined, DeleteOutlined, PlusOutlined } from "@ant-design/icons";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { GetVqdTaskTemplate, DeleteVqdTaskTemplate } from "../api/vqdtasktemplate";
+import type { VqdTaskTemplateItem } from "../types/vqdtasktemplate";
+import type { ColumnsType } from "antd/es/table";
+import AddVqdTaskTemplate, { AddVqdTaskTemplateRef } from "./AddVqdTaskTemplate";
+import { useGlobal } from "../Context";
+import { FormatFileSizeToString } from "../utils/rate";
+import { formatSecondsToHMS } from "../utils/time";
+import Filter from "./Filter";
+export default function VqdTaskTemplatePage() {
+ const { ErrorHandle } = useGlobal();
+ const dialogRef = useRef(null);
+ const [pagination, setPagination] = useState({
+ page: 1,
+ size: 10,
+ name: ""
+ });
+
+ // 获取任务列表
+ const {
+ data: storageResponse,
+ isLoading,
+ refetch,
+ } = useQuery({
+ queryKey: ["storage", pagination],
+ queryFn: () =>
+ GetVqdTaskTemplate({ ...pagination })
+ .then((res) => res.data)
+ .catch((err) => {
+ ErrorHandle(err);
+ throw err;
+ }),
+ // refetchInterval: 4000,
+ retry: 2,
+ });
+
+ // 删除任务
+ const [delLoadings, setDelLoadings] = useState([]);
+ const { mutate: deleteMutation } = useMutation({
+ mutationFn: DeleteVqdTaskTemplate,
+ onMutate: (id: number) => {
+ setDelLoadings((prev) => [...prev, id]);
+ },
+ onSuccess: (_, ctx) => {
+ setDelLoadings((prev) => prev.filter((item) => item !== ctx));
+ message.success("删除成功");
+ refetch();
+ },
+ onError: (error: Error, ctx) => {
+ setDelLoadings((prev) => prev.filter((item) => item !== ctx));
+ ErrorHandle(error);
+ },
+ });
+
+ // 打开新增模态框
+ const handleAdd = () => {
+ dialogRef.current?.open();
+ };
+
+ // 打开编辑模态框
+ const handleEdit = (disk: VqdTaskTemplateItem) => {
+ dialogRef.current?.open(disk);
+ };
+
+ // 处理分页变化
+ const handleTableChange = (page: number, pageSize?: number) => {
+ setPagination((prev) => ({
+ ...prev,
+ page: page,
+ size: pageSize || prev.size,
+ }));
+ };
+
+ // 客户端分页数据
+ const dataSource = useMemo(() => {
+ const items = storageResponse?.items || [];
+ const start = (pagination.page - 1) * pagination.size;
+ const end = start + pagination.size;
+ return items.slice(start, end);
+ }, [storageResponse, pagination]);
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const rowSelection = {
+ selectedRowKeys,
+ onChange: (
+ newSelectedRowKeys: React.Key[],
+ selectedRows: VqdTaskTemplateItem[]
+ ) => {
+ setSelectedRowKeys([...newSelectedRowKeys]);
+ },
+ };
+ // 表格列定义
+ const columns: ColumnsType = [
+ {
+ title: "名称",
+ dataIndex: "name",
+ align: "center",
+ },
+ {
+ title: "文件名称",
+ dataIndex: "file_name",
+ align: "center",
+ render: (text, record) => (
+
+ {text}
+
+ ),
+ },
+ {
+ title: "描述",
+ dataIndex: "des",
+ align: "center",
+ },
+ {
+ title: "创建日期",
+ dataIndex: "created_at",
+ align: "center",
+ render: (text: string) => (text ? new Date(text).toLocaleString() : "-"),
+ },
+ {
+ title: "操作",
+ align: "center",
+ width: 120,
+ fixed: "right",
+ render: (_, record) => (
+
+
+
+ } onClick={() => handleEdit(record)} />
+
+ {
+ if (record.id) {
+ deleteMutation(record.id);
+ }
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+ }
+ />
+
+
+ ),
+ },
+ ];
+
+ return (
+
+
+
+ } onClick={handleAdd}>
+ 新增模板
+
+ {/* {
+
+ }}
+ okText="确定"
+ cancelText="取消"
+ >
+
+ 批量删除
+
+ */}
+
+
+
+ {
+ setPagination({ ...pagination, name: value });
+ }}
+ />
+
+ {/* 表格 */}
+
`共 ${total} 条`,
+ onChange: handleTableChange,
+ onShowSizeChange: handleTableChange,
+ }}
+ />
+
+ {/* 编辑模态框 */}
+ refetch()}
+ />
+
+ );
+}
diff --git a/web/src/components/channel/Channel.tsx b/web/src/components/channel/Channel.tsx
new file mode 100644
index 0000000..d602e0e
--- /dev/null
+++ b/web/src/components/channel/Channel.tsx
@@ -0,0 +1,237 @@
+import { Space, ConfigProvider, Modal, Tag, Tooltip, Button } from "antd";
+import { DeliveredProcedureOutlined } from "@ant-design/icons";
+import Table, { ColumnsType } from "antd/es/table";
+import React, {
+ forwardRef,
+ useImperativeHandle,
+ useRef,
+ useState,
+ useEffect,
+} from "react";
+import { useQuery, useMutation } from "@tanstack/react-query";
+import { ChannelItem, ChannelReq } from "../../types/device";
+import Filter from "./Filter";
+import { GetChannels } from "../../api/devices";
+import { useGlobal } from "../../Context";
+import type { AddTaskItem } from "../../types/audiotask";
+export interface IChannelModelFunc {
+ openModal: (id: number, name: string) => void;
+}
+
+interface IChannelModel {
+ ref: any;
+ onCallback: (data: AddTaskItem[]) => void;
+}
+
+const ChannelModel: React.FC = forwardRef(({ onCallback }, ref) => {
+ useImperativeHandle(ref, () => ({
+ openModal: (id: number, name: string) => {
+ setOpen(true);
+ // if (id != 0) {
+ // setSelectedRowKeys([id])
+ // }
+ audioName.current = name
+ pid.current = id;
+ },
+ }));
+ const [open, setOpen] = useState(false);
+ const pid = useRef(0);
+ const audioName = useRef('');
+ const { ErrorHandle } = useGlobal();
+
+ const columns: ColumnsType = [
+ {
+ title: "ID",
+ align: "center",
+ dataIndex: "id",
+ },
+ {
+ title: "设备名称",
+ align: "center",
+ dataIndex: "device_name",
+ ellipsis: true,
+ render: (text: string) => text || "-",
+ },
+ {
+ title: "通道名称",
+ align: "center",
+ dataIndex: "name",
+ ellipsis: true,
+ render: (text: string) => text || "-",
+ },
+ {
+ title: "状态",
+ dataIndex: "status",
+ align: "center",
+ render: (text: any, record) => {
+ return record.is_dir ? (
+ "-"
+ ) : (
+ {text ? "在线" : "离线"}
+ );
+ },
+ },
+ {
+ title: "接入方式",
+ align: "center",
+ dataIndex: "protocol",
+ render: (text: string) => text || "-",
+ },
+ {
+ title: "操作",
+ align: "center",
+ width: 120,
+ fixed: "right",
+ render: (_, record) => (
+
+
+ } onClick={() => {
+ onCallback([{
+ audio_id: pid.current,
+ channel_id: record.id,
+ channel_name: record.name,
+ }])
+ }} />
+
+
+ ),
+ },
+ ];
+
+ // 获取通道列表
+ const [pagination, setPagination] = useState({
+ page: 1,
+ size: 10, // 通道一般 < 10 个,客户端不做分页,一次性全查
+ device_id: "",
+ pid: "ROOT",
+ status: true,
+ name: "",
+ bid: "",
+ protocol: "GB28181"
+ });
+
+ const { data, isLoading } = useQuery({
+ queryKey: ["channels", pagination, pid.current],
+ queryFn: () =>
+ GetChannels({ ...pagination })
+ .then((res) => res.data)
+ .catch((err) => {
+ ErrorHandle(err);
+ }),
+ retry: 2,
+ enabled: open,
+ });
+ const [selectedRowKeys, setSelectedRowKeys] = useState([]);
+ const [selectedRowsAll, setSelectedRows] = useState([]);
+ const rowSelection = {
+ selectedRowKeys,
+ getCheckboxProps: (record: ChannelItem) => ({
+ disabled: !!record.is_dir,
+ }),
+ onChange: (
+ newSelectedRowKeys: React.Key[],
+ selectedRows: ChannelItem[]
+ ) => {
+ setSelectedRowKeys(newSelectedRowKeys);
+ setSelectedRows(selectedRows)
+ // if (newSelectedRowKeys.length > 0) {
+ // setSelectedRowKeys([newSelectedRowKeys[newSelectedRowKeys.length - 1]]);
+ // }
+ },
+ };
+ const onAll = () => {
+ let dataItem: AddTaskItem[] = []
+ selectedRowsAll.forEach(record => {
+ let list: AddTaskItem = {
+ audio_id: pid.current,
+ channel_id: record.id,
+ channel_name: record.name,
+ }
+ dataItem.push(list)
+ });
+ onCallback(dataItem)
+ setOpen(false);
+ setSelectedRows([])
+ setSelectedRowKeys([])
+ }
+ const onCancel = () => {
+ setOpen(false);
+ // if (selectedRowKeys.length>0) {
+ // onCallback(selectedRowKeys[0], pid.current)
+ // }
+ setSelectedRows([])
+ setSelectedRowKeys([])
+ };
+
+ const modalStyles = {
+ content: {
+ padding: "20px 24px 12px 24px",
+ },
+ };
+
+ return (
+
+
+ onCancel()} >关 闭
+ onAll()} className="mr-6">批量下发
+ >
+ }
+ onCancel={onCancel}
+ // onOk={onCancel}
+ >
+
+
+ {
+ setPagination({ ...pagination, name: value, bid: value });
+ }}
+ onSelectChange={(value: any) => {
+ setPagination({ ...pagination, status: value });
+ }}
+ />
+
+
+ setPagination({ ...pagination, page, size }),
+ showTotal: (total) => `共 ${total} 条`,
+ showSizeChanger: true,
+ pageSizeOptions: [5, 10, 20, 30],
+ }}
+ />
+
+
+
+ );
+});
+
+export default ChannelModel;
diff --git a/web/src/components/channel/Filter.tsx b/web/src/components/channel/Filter.tsx
new file mode 100644
index 0000000..6aa9e6e
--- /dev/null
+++ b/web/src/components/channel/Filter.tsx
@@ -0,0 +1,97 @@
+import { Select, Space, Input } from "antd";
+import { LoadingOutlined, SearchOutlined } from "@ant-design/icons";
+import React from "react";
+
+// 自动搜索输入框(带防抖)
+interface AutoSearchProps {
+ onSearch: (value: string) => void;
+ loading: boolean;
+ placeholder?: string;
+ delay?: number; // 防抖延迟毫秒
+}
+
+const AutoSearch: React.FC = ({
+ onSearch,
+ loading,
+ placeholder,
+ delay = 400,
+}) => {
+ const [value, setValue] = React.useState("");
+ const timerRef = React.useRef(null);
+
+ const triggerSearch = React.useCallback(
+ (val: string) => {
+ onSearch?.(val.trim());
+ },
+ [onSearch]
+ );
+
+ React.useEffect(() => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ timerRef.current = window.setTimeout(() => {
+ triggerSearch(value);
+ }, delay);
+ return () => {
+ if (timerRef.current) {
+ clearTimeout(timerRef.current);
+ }
+ };
+ }, [value, delay, triggerSearch]);
+
+ return (
+ setValue(e.target.value)}
+ onPressEnter={() => triggerSearch(value)}
+ placeholder={placeholder}
+ suffix={loading ? : }
+ />
+ );
+};
+
+interface IFilterProps {
+ searchLoading: boolean;
+ stateValue: any;
+ onSelectChange: (value: any) => void;
+ onSearchChange: (value: string) => void;
+}
+
+const Filter: React.FC = ({
+ searchLoading,
+ stateValue,
+ onSelectChange,
+ onSearchChange,
+}) => {
+ return (
+
+
+ 状态
+ {
+ onSelectChange(val);
+ }}
+ options={[
+ { value: "", label: "全部" },
+ { value: true, label: "在线" },
+ { value: false, label: "离线" },
+ ]}
+ />
+
+
+
+ );
+};
+
+export default Filter;
diff --git a/web/src/input.css b/web/src/input.css
new file mode 100644
index 0000000..c179657
--- /dev/null
+++ b/web/src/input.css
@@ -0,0 +1,8 @@
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+
+body {
+ padding: 24px;
+ background-color: #f5f5f5;
+}
diff --git a/web/src/main.tsx b/web/src/main.tsx
new file mode 100644
index 0000000..a9ff875
--- /dev/null
+++ b/web/src/main.tsx
@@ -0,0 +1,42 @@
+import { StrictMode } from "react";
+import { createRoot } from "react-dom/client";
+import "./output.css";
+import App from "./App.tsx";
+import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
+import { GlobalContextProvider } from "./Context.tsx";
+import { BrowserRouter } from "react-router-dom";
+import { ConfigProvider } from "antd";
+import zhCN from "antd/locale/zh_CN";
+const webBaseUrl = import.meta.env.VITE_WEB_BASE_URL;
+// 创建 QueryClient 实例
+const queryClient = new QueryClient({
+ defaultOptions: {
+ queries: {
+ gcTime: 10 * 60 * 1000,
+ retry: 1,
+ refetchOnWindowFocus: false,
+ },
+ mutations: {
+ retry: 1,
+ },
+ },
+});
+const theme = {
+ token: {
+ colorPrimary: '#52c41a', // 这是 antd 默认的蓝色,你可以改成任何你想要的颜色,比如 '#52c41a' (绿色)
+ },
+};
+
+createRoot(document.getElementById("root")!).render(
+
+
+
+
+
+
+
+
+
+
+
+);
diff --git a/web/src/output.css b/web/src/output.css
new file mode 100644
index 0000000..d76d477
--- /dev/null
+++ b/web/src/output.css
@@ -0,0 +1,555 @@
+*, ::before, ::after {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+ --tw-contain-size: ;
+ --tw-contain-layout: ;
+ --tw-contain-paint: ;
+ --tw-contain-style: ;
+}
+
+::backdrop {
+ --tw-border-spacing-x: 0;
+ --tw-border-spacing-y: 0;
+ --tw-translate-x: 0;
+ --tw-translate-y: 0;
+ --tw-rotate: 0;
+ --tw-skew-x: 0;
+ --tw-skew-y: 0;
+ --tw-scale-x: 1;
+ --tw-scale-y: 1;
+ --tw-pan-x: ;
+ --tw-pan-y: ;
+ --tw-pinch-zoom: ;
+ --tw-scroll-snap-strictness: proximity;
+ --tw-gradient-from-position: ;
+ --tw-gradient-via-position: ;
+ --tw-gradient-to-position: ;
+ --tw-ordinal: ;
+ --tw-slashed-zero: ;
+ --tw-numeric-figure: ;
+ --tw-numeric-spacing: ;
+ --tw-numeric-fraction: ;
+ --tw-ring-inset: ;
+ --tw-ring-offset-width: 0px;
+ --tw-ring-offset-color: #fff;
+ --tw-ring-color: rgb(59 130 246 / 0.5);
+ --tw-ring-offset-shadow: 0 0 #0000;
+ --tw-ring-shadow: 0 0 #0000;
+ --tw-shadow: 0 0 #0000;
+ --tw-shadow-colored: 0 0 #0000;
+ --tw-blur: ;
+ --tw-brightness: ;
+ --tw-contrast: ;
+ --tw-grayscale: ;
+ --tw-hue-rotate: ;
+ --tw-invert: ;
+ --tw-saturate: ;
+ --tw-sepia: ;
+ --tw-drop-shadow: ;
+ --tw-backdrop-blur: ;
+ --tw-backdrop-brightness: ;
+ --tw-backdrop-contrast: ;
+ --tw-backdrop-grayscale: ;
+ --tw-backdrop-hue-rotate: ;
+ --tw-backdrop-invert: ;
+ --tw-backdrop-opacity: ;
+ --tw-backdrop-saturate: ;
+ --tw-backdrop-sepia: ;
+ --tw-contain-size: ;
+ --tw-contain-layout: ;
+ --tw-contain-paint: ;
+ --tw-contain-style: ;
+}
+
+.visible {
+ visibility: visible;
+}
+
+.fixed {
+ position: fixed;
+}
+
+.absolute {
+ position: absolute;
+}
+
+.relative {
+ position: relative;
+}
+
+.left-1\/2 {
+ left: 50%;
+}
+.top-3 {
+ top: 30px;
+}
+.top-\[30\%\] {
+ top: 30%;
+}
+
+.col-span-3 {
+ grid-column: span 3 / span 3;
+}
+
+.col-span-1 {
+ grid-column: span 1 / span 1;
+}
+
+.mx-3 {
+ margin-left: 0.75rem;
+ margin-right: 0.75rem;
+}
+
+.mb-1 {
+ margin-bottom: 0.25rem;
+}
+
+.mb-2 {
+ margin-bottom: 0.5rem;
+}
+
+.mb-3 {
+ margin-bottom: 0.75rem;
+}
+
+.mb-4 {
+ margin-bottom: 1rem;
+}
+
+.mb-7 {
+ margin-bottom: 1.75rem;
+}
+
+.mr-0 {
+ margin-right: 0px;
+}
+
+.mt-4 {
+ margin-top: 1rem;
+}
+
+.block {
+ display: block;
+}
+
+.flex {
+ display: flex;
+}
+
+.grid {
+ display: grid;
+}
+
+.hidden {
+ display: none;
+}
+
+.h-16 {
+ height: 4rem;
+}
+
+.h-20 {
+ height: 5rem;
+}
+
+.h-\[calc\(100vh-200px\)\] {
+ height: calc(100vh - 200px);
+}
+
+.h-full {
+ height: 100%;
+}
+
+.h-screen {
+ height: 100vh;
+}
+
+.max-h-\[300px\] {
+ max-height: 300px;
+}
+
+.min-h-32 {
+ min-height: 8rem;
+}
+
+.w-32 {
+ width: 8rem;
+}
+
+.w-\[400px\] {
+ width: 400px;
+}
+
+.w-full {
+ width: 100%;
+}
+
+.w-52 {
+ width: 13rem;
+}
+
+.min-w-\[1000px\] {
+ min-width: 1000px;
+}
+
+.min-w-0 {
+ min-width: 0px;
+}
+
+.max-w-\[540px\] {
+ max-width: 540px;
+}
+
+.flex-1 {
+ flex: 1 1 0%;
+}
+
+.flex-shrink-0 {
+ flex-shrink: 0;
+}
+
+.-translate-x-1\/2 {
+ --tw-translate-x: -50%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.-translate-y-1\/2 {
+ --tw-translate-y: -50%;
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.transform {
+ transform: translate(var(--tw-translate-x), var(--tw-translate-y)) rotate(var(--tw-rotate)) skewX(var(--tw-skew-x)) skewY(var(--tw-skew-y)) scaleX(var(--tw-scale-x)) scaleY(var(--tw-scale-y));
+}
+
+.cursor-pointer {
+ cursor: pointer;
+}
+
+.select-none {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+ user-select: none;
+}
+
+.grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+}
+
+.grid-cols-4 {
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+}
+
+.grid-cols-1 {
+ grid-template-columns: repeat(1, minmax(0, 1fr));
+}
+
+.flex-col {
+ flex-direction: column;
+}
+
+.items-start {
+ align-items: flex-start;
+}
+
+.items-center {
+ align-items: center;
+}
+
+.justify-end {
+ justify-content: flex-end;
+}
+
+.justify-center {
+ justify-content: center;
+}
+
+.justify-between {
+ justify-content: space-between;
+}
+
+.gap-2 {
+ gap: 0.5rem;
+}
+
+.gap-3 {
+ gap: 0.75rem;
+}
+
+.gap-4 {
+ gap: 1rem;
+}
+
+.overflow-auto {
+ overflow: auto;
+}
+
+.overflow-y-auto {
+ overflow-y: auto;
+}
+
+.truncate {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+}
+
+.whitespace-nowrap {
+ white-space: nowrap;
+}
+
+.rounded-lg {
+ border-radius: 0.5rem;
+}
+
+.rounded-xl {
+ border-radius: 0.75rem;
+}
+
+.border {
+ border-width: 1px;
+}
+
+.border-2 {
+ border-width: 2px;
+}
+
+.border-solid {
+ border-style: solid;
+}
+
+.border-blue-500 {
+ --tw-border-opacity: 1;
+ border-color: rgb(59 130 246 / var(--tw-border-opacity, 1));
+}
+
+.border-transparent {
+ border-color: transparent;
+}
+
+.bg-blue-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(239 246 255 / var(--tw-bg-opacity, 1));
+}
+
+.bg-gray-50 {
+ --tw-bg-opacity: 1;
+ background-color: rgb(249 250 251 / var(--tw-bg-opacity, 1));
+}
+
+.bg-white {
+ --tw-bg-opacity: 1;
+ background-color: rgb(255 255 255 / var(--tw-bg-opacity, 1));
+}
+
+.bg-\[url\(\'https\:\/\/mdn\.alipayobjects\.com\/yuyan_qk0oxh\/afts\/img\/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr\'\)\] {
+ background-image: url('https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/V-_oS6r-i7wAAAAAAAAAAAAAFl94AQBr');
+}
+
+.bg-cover {
+ background-size: cover;
+}
+
+.p-3 {
+ padding: 0.75rem;
+}
+
+.p-4 {
+ padding: 1rem;
+}
+
+.px-0 {
+ padding-left: 0px;
+ padding-right: 0px;
+}
+
+.px-2 {
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+
+.px-3 {
+ padding-left: 0.75rem;
+ padding-right: 0.75rem;
+}
+
+.py-1\.5 {
+ padding-top: 0.375rem;
+ padding-bottom: 0.375rem;
+}
+
+.py-2 {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+}
+
+.py-4 {
+ padding-top: 1rem;
+ padding-bottom: 1rem;
+}
+
+.py-8 {
+ padding-top: 2rem;
+ padding-bottom: 2rem;
+}
+
+.pb-2 {
+ padding-bottom: 0.5rem;
+}
+
+.pb-3 {
+ padding-bottom: 0.75rem;
+}
+
+.pr-4 {
+ padding-right: 1rem;
+}
+
+.pr-6 {
+ padding-right: 1.5rem;
+}
+
+
+.text-left {
+ text-align: left;
+}
+
+.text-center {
+ text-align: center;
+}
+
+.text-right {
+ text-align: right;
+}
+
+.text-base {
+ font-size: 1rem;
+ line-height: 1.5rem;
+}
+
+.text-sm {
+ font-size: 0.875rem;
+ line-height: 1.25rem;
+}
+
+.text-xs {
+ font-size: 0.75rem;
+ line-height: 1rem;
+}
+
+.font-medium {
+ font-weight: 500;
+}
+
+.text-gray-500 {
+ --tw-text-opacity: 1;
+ color: rgb(107 114 128 / var(--tw-text-opacity, 1));
+}
+
+.text-gray-600 {
+ --tw-text-opacity: 1;
+ color: rgb(75 85 99 / var(--tw-text-opacity, 1));
+}
+
+.text-gray-700 {
+ --tw-text-opacity: 1;
+ color: rgb(55 65 81 / var(--tw-text-opacity, 1));
+}
+
+.text-gray-900 {
+ --tw-text-opacity: 1;
+ color: rgb(17 24 39 / var(--tw-text-opacity, 1));
+}
+
+.filter {
+ filter: var(--tw-blur) var(--tw-brightness) var(--tw-contrast) var(--tw-grayscale) var(--tw-hue-rotate) var(--tw-invert) var(--tw-saturate) var(--tw-sepia) var(--tw-drop-shadow);
+}
+
+.transition-all {
+ transition-property: all;
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+ transition-duration: 150ms;
+}
+
+.duration-200 {
+ transition-duration: 200ms;
+}
+
+.ease-in-out {
+ transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+}
+
+body {
+ padding: 24px;
+ background-color: #f5f5f5;
+}
+
+.hover\:border-gray-300:hover {
+ --tw-border-opacity: 1;
+ border-color: rgb(209 213 219 / var(--tw-border-opacity, 1));
+}
+
+@media (min-width: 1024px) {
+ .lg\:block {
+ display: block;
+ }
+
+ .lg\:hidden {
+ display: none;
+ }
+
+ .lg\:grid-cols-2 {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+}
+
+@media (min-width: 1280px) {
+ .xl\:grid-cols-3 {
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ }
+}
diff --git a/web/src/pages/Home.tsx b/web/src/pages/Home.tsx
new file mode 100644
index 0000000..10038a5
--- /dev/null
+++ b/web/src/pages/Home.tsx
@@ -0,0 +1,101 @@
+import { Affix, Col, Menu, Row, type MenuProps } from "antd";
+import {
+ FileSearchOutlined,
+ AlertOutlined,
+ FileTextOutlined,
+ SettingOutlined
+} from "@ant-design/icons";
+import Box from "../components/Box";
+import { useState } from "react";
+import VqdTaskPage from "../components/VqdTask";
+import VqdTaskTemplatePage from "../components/VqdTaskTemplate";
+import VqdAlarmPage from "../components/VqdAlarm";
+import VqdConfigPage from "../components/VqdConfig";
+
+type MenuItem = Required["items"][number];
+export default function Home() {
+ const [currentMenu, setCurrentMenu] = useState("sub0");
+
+ const items: MenuItem[] = [
+ {
+ key: "sub0",
+ label: "任务管理",
+ icon: ,
+ },
+ {
+ key: "sub1",
+ label: "任务模板",
+ icon: ,
+ },
+ {
+ key: "sub2",
+ label: "诊断告警",
+ icon: ,
+ },
+ {
+ key: "sub3",
+ label: "基础配置",
+ icon: ,
+ },
+ ];
+
+ const onClickMenu = (item: any) => {
+ setCurrentMenu(item?.key as string);
+ };
+
+ return (
+
+
+
+ {/* */}
+ {currentMenu == "sub0" && (
+
+
+
+ )}
+ {currentMenu == "sub1" && (
+
+
+
+ )}
+ {currentMenu == "sub2" && (
+
+
+
+ )}
+ {currentMenu == "sub3" && (
+
+
+
+ )}
+
+
+ );
+}
diff --git a/web/src/pages/Login.tsx b/web/src/pages/Login.tsx
new file mode 100644
index 0000000..9c1ffd9
--- /dev/null
+++ b/web/src/pages/Login.tsx
@@ -0,0 +1,166 @@
+import { Login } from "../api/login";
+import { setLocalStorage } from "../utils/local";
+import { LockOutlined, UserOutlined } from "@ant-design/icons";
+import { Form, Space, Typography, Button, Input } from "antd";
+// import GoCaptcha from "go-captcha-react";
+import React, { useEffect } from "react";
+import { LoginReq } from "../types/login";
+import { useMutation } from "@tanstack/react-query";
+import { useNavigate, useSearchParams } from "react-router-dom";
+
+const View: React.FC = () => {
+ const navigate = useNavigate();
+ const [searchParams] = useSearchParams();
+
+ // 从URL参数中获取值,如果不存在则使用空字符串
+ const isAutoLogin = searchParams.get("isAutoLogin") || "";
+ const username = searchParams.get("username") || "";
+ const password = searchParams.get("password") || "";
+ const [form] = Form.useForm();
+ const title = "录像同步";
+
+ useEffect(() => {
+ document.title = title;
+ }, []);
+
+ useEffect(() => {
+ // 只有当username或password有值时,才更新表单
+ if (username || password) {
+ form.setFieldsValue({
+ username: username || undefined,
+ password: password || undefined,
+ });
+
+ // 如果启用了自动登录且有用户名和密码,则自动提交
+ if (isAutoLogin && username && password) {
+ form.submit();
+ }
+ }
+ }, [username, password, isAutoLogin, form]);
+
+ const { mutate, isPending } = useMutation({
+ mutationFn: Login,
+ onSuccess: (res) => {
+ setLocalStorage("token", res.data.token);
+ navigate("/home");
+ },
+ onError: (err) => {
+ form.setFieldsValue({ captcha: "" });
+ ErrorHandle(err);
+ },
+ });
+
+ // const { data: captchaData, refetch } = useQuery({
+ // queryKey: [getCaptcha],
+ // queryFn: () =>
+ // GetCaptcha()
+ // .then((res) => res.data)
+ // .catch((err) => {
+ // ErrorHandle(err);
+ // throw err;
+ // }),
+ // refetchInterval: 280000,
+ // });
+
+ // const [captchaEnabled, setCaptchaEnabled] = useState(false);
+ return (
+
+ );
+};
+
+export default View;
+function ErrorHandle(err: Error) {
+ throw new Error("Function not implemented.");
+}
diff --git a/web/src/types/config.ts b/web/src/types/config.ts
new file mode 100644
index 0000000..f97b3c7
--- /dev/null
+++ b/web/src/types/config.ts
@@ -0,0 +1,19 @@
+
+/**
+ * 基础
+ */
+export type VqdConfigBaseRes = {
+ data: string;
+};
+
+export type UpdateConfigBaseReq = {
+ save_day: number;
+ frm_num: number;
+ is_deep_learn: boolean;
+};
+
+export type VqdConfigBaseDetailRes = {
+ save_day: number;
+ frm_num: number;
+ is_deep_learn: boolean;
+}
\ No newline at end of file
diff --git a/web/src/types/device.d.ts b/web/src/types/device.d.ts
new file mode 100644
index 0000000..125142d
--- /dev/null
+++ b/web/src/types/device.d.ts
@@ -0,0 +1,287 @@
+export type DeviceReq = {
+ /**
+ * 模糊搜索
+ */
+ id?: string;
+ /**
+ * 模糊搜索
+ */
+ name?: string;
+ /**
+ * 页码(1~N)
+ */
+ page: number;
+ /**
+ * 单页元素数量(10~100)
+ */
+ size: number;
+ /**
+ * true:在线过滤; false:离线过滤
+ */
+ status?: string;
+};
+
+export type DeviceRes = {
+ items: DeviceItem[];
+ /**
+ * 总数,设备总数
+ */
+ total: number;
+};
+
+export type DeviceItem = {
+ /**
+ * 能力
+ */
+ ability?: Ability;
+ addr?: string;
+ /**
+ * 音频控制,关闭:0、启用:1
+ */
+ audio?: string;
+ /**
+ * 通道数量
+ */
+ channel_count?: number;
+ /**
+ * 创建时间
+ */
+ created_at?: string;
+ /**
+ * 私有协议扩展字段
+ */
+ ext?: Ext;
+ gb?: Gb;
+ gb_code?: string;
+ id?: string;
+ /**
+ * IP地址
+ */
+ ip?: string;
+ is_platform?: boolean;
+ last_pushed_at?: string;
+ media_transport?: string;
+ /**
+ * 设备型号
+ */
+ model?: string;
+ /**
+ * 设备名称
+ */
+ name?: string;
+ /**
+ * LAN/WAN
+ */
+ network?: string;
+ /**
+ * 密码
+ */
+ password?: string;
+ /**
+ * 端口
+ */
+ port?: number;
+ /**
+ * 设备协议,HIKSDK,GB等
+ */
+ protocol?: string;
+ /**
+ * 取流模式,native:0、ffmpeg:1
+ */
+ pull_mode?: number;
+ /**
+ * 备注
+ */
+ remark?: string;
+ routes?: string;
+ rtp_id?: string;
+ /**
+ * 设备状态
+ */
+ status?: boolean;
+ /**
+ * 用户id
+ */
+ uid?: number;
+ /**
+ * 修改时间
+ */
+ updated_at?: string;
+ url?: string;
+ /**
+ * 用户名
+ */
+ username?: string;
+ /**
+ * 版本号
+ */
+ version?: string;
+ [property: string]: any;
+};
+
+/**
+ * 能力
+ */
+export type Ability = {
+ ai: boolean;
+ cloud_broadcast: boolean;
+ gat_1400: boolean;
+ ota: boolean;
+ ptz: boolean;
+ talk: boolean;
+ wifi: boolean;
+ [property: string]: any;
+};
+
+/**
+ * 私有协议扩展字段
+ */
+export type Ext = {
+ lan_ip: string;
+ manufacturer: string;
+ remote_ip: string;
+ wan_ip: string;
+ [property: string]: any;
+};
+
+export type Gb = {
+ sip_transport: string;
+ [property: string]: any;
+};
+
+type ChannelReq = {
+ /**
+ * 设备通道 id 模糊搜索
+ */
+ bid?: string;
+ /**
+ * 返回级联共享状态
+ */
+ cascade_id?: string;
+ /**
+ * 设备唯一标识,过滤属于哪个设备的通道
+ */
+ device_id?: string;
+ /**
+ * 名称模糊搜索
+ */
+ name?: string;
+ /**
+ * 页码(1~N)
+ */
+ page: number;
+ /**
+ * 父级目录 ID,如果按组织结构查询,顶层传 root
+ */
+ pid?: string;
+ /**
+ * 返回的信息中包含录像计划状态
+ */
+ plan_id?: number;
+ /**
+ * 协议类型过滤
+ */
+ protocol?: string;
+ /**
+ * 单页元素数量(10~100)
+ */
+ size: number;
+ /**
+ * true:在线; false:离线;
+ */
+ status?: boolean;
+};
+
+export type ChannelRes = {
+ items: ChannelItem[];
+ /**
+ * 总数,通道总数
+ */
+ total: number;
+ [property: string]: any;
+}
+
+export type ChannelItem = {
+ /**
+ * 设备真实ID
+ */
+ bid: string;
+ /**
+ * 子设备数量
+ */
+ child_count: number;
+ /**
+ * 创建时间
+ */
+ created_at: string;
+ /**
+ * 设备ID
+ */
+ device_id: string;
+ /**
+ * 启用状态,是否启用
+ */
+ enabled: boolean;
+ ext: Ext;
+ /**
+ * 唯一ID,设备ID
+ */
+ id: string;
+ /**
+ * IP地址
+ */
+ ip: string;
+ /**
+ * 是否目录
+ */
+ is_dir: boolean;
+ /**
+ * 名称,通道名称
+ */
+ name: string;
+ pid: string;
+ /**
+ * 是否正在播放
+ */
+ playing: boolean;
+ /**
+ * 端口
+ */
+ port: number;
+ /**
+ * 设备协议,HIKSDK、GB等
+ */
+ protocol: string;
+ ptz: boolean;
+ /**
+ * 云台类型
+ */
+ ptz_type: number;
+ /**
+ * 备注
+ */
+ remark: string;
+ /**
+ * 在线状态,在线或者离线
+ */
+ status: boolean;
+ /**
+ * 语音对讲
+ */
+ talk: boolean;
+ /**
+ * 传输协议,TCP、UDP等
+ */
+ transport: string;
+ /**
+ * 修改时间
+ */
+ updated_at: string;
+ [property: string]: any;
+}
+
+export type Ext = {
+ parent_id: string;
+ parental: number;
+ [property: string]: any;
+}
\ No newline at end of file
diff --git a/web/src/types/login.d.ts b/web/src/types/login.d.ts
new file mode 100644
index 0000000..c911f48
--- /dev/null
+++ b/web/src/types/login.d.ts
@@ -0,0 +1,78 @@
+export type LoginReq = {
+ username: string;
+ password: string;
+ captcha: string;
+ captcha_id: number;
+};
+
+export type CaptchaRes = {
+ master: string;
+ tile: string;
+ w: number;
+ h: number;
+ x: number;
+ y: number;
+
+ /**
+ * 图片
+ */
+ // base64: string;
+ /**
+ * 验证码 id,登录时携带参数
+ */
+ captcha_id: number;
+ /**
+ * 过期时间(秒)
+ */
+ expired: number;
+};
+
+export type LoginReq = {
+ /**
+ * 验证码答案,验证码的值
+ */
+ captcha?: string;
+ /**
+ * 验证码唯一标识符,验证码 id
+ */
+ captcha_id: number;
+ /**
+ * 加密的密码,密码(sha256)
+ */
+ password: string;
+ /**
+ * 用户名,用户名
+ */
+ username: string;
+};
+
+export type LoginRes = {
+ /**
+ * token过期时间戳(秒)
+ */
+ expired_at: number;
+ timeout_s: number;
+ /**
+ * 是否重置密码
+ */
+ is_reset_account: boolean;
+ /**
+ * 认证令牌,示例:1adjd1i2jnqaw.12312fainofasdasdas.12312fasdasd
+ */
+ token: string;
+ user: User;
+};
+
+export type User = {
+ id: number;
+ name: string;
+ /**
+ * 域(顶级用户组)
+ */
+ rgroup_id: number;
+ /**
+ * 用户名
+ */
+ username: string;
+ level: number;
+};
diff --git a/web/src/types/vqdalarm.ts b/web/src/types/vqdalarm.ts
new file mode 100644
index 0000000..9432afb
--- /dev/null
+++ b/web/src/types/vqdalarm.ts
@@ -0,0 +1,82 @@
+
+/**
+ * 基础
+ */
+export type VqdAlarmBaseRes = {
+ data: string;
+};
+
+/**
+ * 查询列表响应
+ */
+export type VqdAlarmRes = {
+ items: VqdAlarmItem[];
+ /**
+ * 总数
+ */
+ total: number;
+};
+
+
+/**
+ * 项
+ */
+export type VqdAlarmItem = {
+ id: number;
+ alarm_name: string;
+ alarm_value: string;
+ channel_id: string;
+ channel_name: string;
+ task_template_id: number;
+ task_template_name: string;
+ task_id: number;
+ task_name: string;
+ file_path: string;
+ created_at?: string;
+ updated_at?: string;
+
+};
+/**
+ * 创建请求
+ */
+export type VqdAlarmReq = {
+ /**
+ * 名称模糊搜索
+ */
+ name?: string;
+ /**
+ * 页码(1~N)
+ */
+ page: number;
+ /**
+ * 单页元素数量(10~100)
+ */
+ size: number;
+
+}
+
+/**
+ * 详情响应(Apifox 未提供具体结构,这里用通用对象)
+ */
+export type VqdAlarmDetailRes = {
+ id: number;
+ alarm_name: string;
+ alarm_value: string;
+ channel_id: string;
+ channel_name: string;
+ task_template_id: number;
+ task_template_name: string;
+ task_id: number;
+ task_name: string;
+ file_path: string;
+ created_at?: string;
+ updated_at?: string;
+
+}
+
+export type DelVqdAlarmReq = {
+ /**
+ * IDs
+ */
+ ids: number[];
+};
\ No newline at end of file
diff --git a/web/src/types/vqdtask.ts b/web/src/types/vqdtask.ts
new file mode 100644
index 0000000..b242ec6
--- /dev/null
+++ b/web/src/types/vqdtask.ts
@@ -0,0 +1,110 @@
+
+/**
+ * 基础
+ */
+export type VqdTaskBaseRes = {
+ data: string;
+};
+
+/**
+ * 查询列表响应
+ */
+export type VqdTaskRes = {
+ items: VqdTaskItem[];
+ /**
+ * 总数
+ */
+ total: number;
+};
+
+
+/**
+ * 项
+ */
+export type VqdTaskItem = {
+ id: number;
+ name: string;
+ channel_id: string;
+ channel_name: string;
+ task_template_id: number;
+ task_template_name: string;
+ enable: boolean;
+ created_at?: string;
+ updated_at?: string;
+ des: string;
+
+};
+/**
+ * 创建请求
+ */
+export type VqdTaskReq = {
+ /**
+ * 名称模糊搜索
+ */
+ name?: string;
+ /**
+ * 页码(1~N)
+ */
+ page: number;
+ /**
+ * 单页元素数量(10~100)
+ */
+ size: number;
+
+}
+export type CreateVqdTaskReq = {
+ /**
+ * 名称
+ */
+ name: string;
+ /**
+ * 关联通道
+ */
+ channel_id: string;
+ /**
+ * 通道名称
+ */
+ channel_name: string;
+ /**
+ * 关联模板
+ */
+ task_template_id: number;
+ /**
+ * 模板名称
+ */
+ task_template_name: string;
+ /**
+ * 启用
+ */
+ enable: boolean;
+ /**
+ * 描述
+ */
+ des: string;
+};
+
+/**
+ * 更新请求
+ */
+export type UpdateVqdTaskReq = Partial & {
+ /**
+ * ID
+ */
+ id: string;
+};
+
+/**
+ * 详情响应(Apifox 未提供具体结构,这里用通用对象)
+ */
+export type VqdTaskDetailRes = {
+ id: number;
+ name: string;
+ size: number;
+ mode: string;
+ encode_status: number;
+ file_name: string;
+ source_url: string;
+ created_at?: string;
+ updated_at?: string;
+ des?: string;
+}
\ No newline at end of file
diff --git a/web/src/types/vqdtasktemplate.ts b/web/src/types/vqdtasktemplate.ts
new file mode 100644
index 0000000..6185f4b
--- /dev/null
+++ b/web/src/types/vqdtasktemplate.ts
@@ -0,0 +1,92 @@
+
+/**
+ * 基础
+ */
+export type VqdTaskTemplateBaseRes = {
+ data: string;
+};
+
+/**
+ * 查询列表响应
+ */
+export type VqdTaskTemplateRes = {
+ items: VqdTaskTemplateItem[];
+ /**
+ * 总数
+ */
+ total: number;
+};
+
+
+/**
+ * 项
+ */
+export type VqdTaskTemplateItem = {
+ id: number;
+ name: string;
+ plans: string;
+ enable: boolean;
+ created_at?: string;
+ updated_at?: string;
+ des: string;
+
+};
+/**
+ * 创建请求
+ */
+export type VqdTaskTemplateReq = {
+ /**
+ * 名称模糊搜索
+ */
+ name?: string;
+ /**
+ * 页码(1~N)
+ */
+ page: number;
+ /**
+ * 单页元素数量(10~100)
+ */
+ size: number;
+
+}
+export type CreateVqdTaskTemplateReq = {
+ /**
+ * 名称
+ */
+ name: string;
+ /**
+ * 计划
+ */
+ plans: string;
+ /**
+ * 启用
+ */
+ enable: boolean;
+ /**
+ * 描述
+ */
+ des: string;
+};
+
+/**
+ * 更新请求
+ */
+export type UpdateVqdTaskTemplateReq = Partial & {
+ /**
+ * ID
+ */
+ id: string;
+};
+
+/**
+ * 详情响应(Apifox 未提供具体结构,这里用通用对象)
+ */
+export type VqdTaskTemplateDetailRes = {
+ id: number;
+ name: string;
+ plans: string;
+ enable: boolean;
+ created_at?: string;
+ updated_at?: string;
+ des: string;
+}
\ No newline at end of file
diff --git a/web/src/utils/local.ts b/web/src/utils/local.ts
new file mode 100644
index 0000000..45d4edd
--- /dev/null
+++ b/web/src/utils/local.ts
@@ -0,0 +1,44 @@
+export const getLocalStorage = (key: string): T | null => {
+ let data = localStorage.getItem(key);
+ try {
+ data = JSON.parse(data ?? "");
+ } catch (err) {
+ data = localStorage.getItem(key);
+ }
+
+ return data as T;
+};
+
+export const setLocalStorage = (key: string, value: any) => {
+ let data = value;
+ if (Array.isArray(value) || typeof value === "object") {
+ data = JSON.stringify(value);
+ }
+ localStorage.setItem(key, data);
+};
+
+export const getSessionStorage = (key: string): T | null => {
+ let data = sessionStorage.getItem(key);
+ try {
+ data = JSON.parse(data ?? "");
+ } catch (err) {
+ data = sessionStorage.getItem(key);
+ }
+ return data as T;
+};
+
+export const setSessionStorage = (key: string, value: any) => {
+ let data = value;
+ if (Array.isArray(value) || typeof value === "object") {
+ data = JSON.stringify(value);
+ }
+ sessionStorage.setItem(key, data);
+};
+
+export const removeLocalStorage = (key: string) => {
+ localStorage.removeItem(key);
+};
+
+export const removeSessionStorage = (key: string) => {
+ sessionStorage.removeItem(key);
+};
diff --git a/web/src/utils/rate.ts b/web/src/utils/rate.ts
new file mode 100644
index 0000000..27c2269
--- /dev/null
+++ b/web/src/utils/rate.ts
@@ -0,0 +1,67 @@
+// 格式化比特率
+export function FormatBitrate(bitsPerSecond: number) {
+ if (bitsPerSecond >= 1_000_000) {
+ // 大于等于1,000,000 bps时,用 Mbps 表示
+ return (bitsPerSecond / 1_000_000).toFixed(2) + ' Mbps';
+ } else if (bitsPerSecond >= 1_000) {
+ // 大于等于1,000 bps时,用 Kbps 表示
+ return (bitsPerSecond / 1_000).toFixed(2) + ' Kbps';
+ } else {
+ // 小于1,000 bps时,用 bps 表示
+ return bitsPerSecond + ' bps';
+ }
+}
+
+// 将bit 转 Mbps
+export function ConvertBitsToMbps(bits: number) {
+ return Math.round((bits / 1_000_000) * 100) / 100;
+}
+
+//将 bit 转成 GB
+export function ConvertBitsToGB(bits: number) {
+ const GB = bits / (1024 * 1024 * 1024);
+ const data = Number(GB.toFixed(1));
+ return data;
+}
+
+/**
+ * 根据1024的倍数,返回对应的单位 FormatFileSize
+ * @param sizeInBytes number 文件大小默认是byte
+ */
+export function FormatFileSize(sizeInBytes: number, isSlice = false) {
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ let size = sizeInBytes;
+ let unitIndex = 0;
+
+ while (size >= 1024 && unitIndex < units.length - 1) {
+ size /= 1024;
+ unitIndex++;
+ }
+ return { size: size.toFixed(2), unit: units[unitIndex] };
+}
+
+export function FormatFileSizeToString(sizeInBytes: number, isSlice = false) {
+ const units = ['B', 'KB', 'MB', 'GB', 'TB'];
+ let size = sizeInBytes;
+ let unitIndex = 0;
+
+ while (size >= 1024 && unitIndex < units.length - 1) {
+ size /= 1024;
+ unitIndex++;
+ }
+ return `${size.toFixed(2)} ${units[unitIndex]}`;
+}
+
+/**
+ * 计算使用百分比
+ * @param used 已使用量
+ * @param total 总量
+ * @param fixed 保留小数位,默认 1
+ * @returns number 百分比 (0 ~ 100)
+ */
+export function GetUsagePercent(used: number, total: number, fixed: number = 1): number {
+ if (total <= 0) return 0;
+ const percent = (used / total) * 100;
+ return Number(percent.toFixed(fixed));
+}
+
diff --git a/web/src/utils/time.ts b/web/src/utils/time.ts
new file mode 100644
index 0000000..2845360
--- /dev/null
+++ b/web/src/utils/time.ts
@@ -0,0 +1,19 @@
+/**
+ * 将秒数格式化为 "HH:mm:ss" 格式
+ * @param {number} totalSeconds - 总秒数
+ * @returns {string} - 格式化后的时间字符串
+ */
+export function formatSecondsToHMS(totalSeconds: number) :string{
+ if (totalSeconds <= 0) return "";
+ // 计算小时、分钟和秒
+ const hours = Math.floor(totalSeconds / 3600);
+ const minutes = Math.floor((totalSeconds - (hours * 3600)) / 60);
+ const seconds = totalSeconds - (hours * 3600) - (minutes * 60);
+
+ // 使用 padStart(2, '0') 来确保每个部分都是两位数,不足的用 '0' 填充
+ const formattedHours = hours.toString().padStart(2, '0');
+ const formattedMinutes = minutes.toString().padStart(2, '0');
+ const formattedSeconds = seconds.toString().padStart(2, '0');
+
+ return `${formattedHours}:${formattedMinutes}:${formattedSeconds}`;
+}
\ No newline at end of file
diff --git a/web/tailwind.config.js b/web/tailwind.config.js
new file mode 100644
index 0000000..ac0ba75
--- /dev/null
+++ b/web/tailwind.config.js
@@ -0,0 +1,11 @@
+/** @type {import('tailwindcss').Config} */
+export default {
+ content: ["./src/**/*.{html,js,tsx,jsx,ts}"],
+ theme: {
+ extend: {},
+ },
+ plugins: [],
+ corePlugins: {
+ preflight: false, // 防止tailwindcss 和 ant 样式冲突
+ },
+};
diff --git a/web/tsconfig.app.json b/web/tsconfig.app.json
new file mode 100644
index 0000000..36f3266
--- /dev/null
+++ b/web/tsconfig.app.json
@@ -0,0 +1,29 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
+ "target": "ES2022",
+ "useDefineForClassFields": true,
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "types": ["vite/client"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": false,
+ "moduleDetection": "force",
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noImplicitAny": false,
+ "noUnusedLocals": false,
+ "noUnusedParameters": false,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["src"]
+}
diff --git a/web/tsconfig.json b/web/tsconfig.json
new file mode 100644
index 0000000..1ffef60
--- /dev/null
+++ b/web/tsconfig.json
@@ -0,0 +1,7 @@
+{
+ "files": [],
+ "references": [
+ { "path": "./tsconfig.app.json" },
+ { "path": "./tsconfig.node.json" }
+ ]
+}
diff --git a/web/tsconfig.node.json b/web/tsconfig.node.json
new file mode 100644
index 0000000..8a67f62
--- /dev/null
+++ b/web/tsconfig.node.json
@@ -0,0 +1,26 @@
+{
+ "compilerOptions": {
+ "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
+ "target": "ES2023",
+ "lib": ["ES2023"],
+ "module": "ESNext",
+ "types": ["node"],
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
+ "allowImportingTsExtensions": true,
+ "verbatimModuleSyntax": true,
+ "moduleDetection": "force",
+ "noEmit": true,
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "erasableSyntaxOnly": true,
+ "noFallthroughCasesInSwitch": true,
+ "noUncheckedSideEffectImports": true
+ },
+ "include": ["vite.config.ts"]
+}
diff --git a/web/vite.config.ts b/web/vite.config.ts
new file mode 100644
index 0000000..16b5876
--- /dev/null
+++ b/web/vite.config.ts
@@ -0,0 +1,34 @@
+import { defineConfig, loadEnv } from 'vite'
+import react from '@vitejs/plugin-react-swc'
+import path from 'path'
+// https://vite.dev/config/
+export default defineConfig(({ mode }) => {
+ const env = loadEnv(mode, path.resolve(process.cwd(), './'), '')
+ // console.log('当前环境:', mode)
+ // console.log('读取的.env变量:', env.VITE_WEB_BASE_URL)
+ return {
+ plugins: [react()],
+ base: env.VITE_WEB_BASE_URL,
+ build: { outDir: "../internal/web/api/static/www" },
+ server: {
+ port: 5174,
+ proxy: {
+ // 代理所有 /api 请求到后端服务器
+ '/api': {
+ target: 'http://127.0.0.1:8089',
+ changeOrigin: true,
+ secure: false,
+ // 可选:重写路径
+ // rewrite: (path) => path.replace(/^\/api/, '')
+ },
+ '/uploads': {
+ target: 'http://127.0.0.1:8089',
+ changeOrigin: true,
+ secure: false,
+ // 可选:重写路径
+ // rewrite: (path) => path.replace(/^\/api/, '')
+ },
+ }
+ }
+ }
+})
diff --git a/web/yarn.lock b/web/yarn.lock
new file mode 100644
index 0000000..c85a070
--- /dev/null
+++ b/web/yarn.lock
@@ -0,0 +1,2722 @@
+# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
+# yarn lockfile v1
+
+
+"@alloc/quick-lru@^5.2.0":
+ version "5.2.0"
+ resolved "https://registry.npmmirror.com/@alloc/quick-lru/-/quick-lru-5.2.0.tgz"
+ integrity sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==
+
+"@ant-design/colors@^7.0.0", "@ant-design/colors@^7.2.1":
+ version "7.2.1"
+ resolved "https://registry.npmmirror.com/@ant-design/colors/-/colors-7.2.1.tgz"
+ integrity sha512-lCHDcEzieu4GA3n8ELeZ5VQ8pKQAWcGGLRTQ50aQM2iqPpq2evTxER84jfdPvsPAtEcZ7m44NI45edFMo8oOYQ==
+ dependencies:
+ "@ant-design/fast-color" "^2.0.6"
+
+"@ant-design/cssinjs-utils@^1.1.3":
+ version "1.1.3"
+ resolved "https://registry.npmmirror.com/@ant-design/cssinjs-utils/-/cssinjs-utils-1.1.3.tgz"
+ integrity sha512-nOoQMLW1l+xR1Co8NFVYiP8pZp3VjIIzqV6D6ShYF2ljtdwWJn5WSsH+7kvCktXL/yhEtWURKOfH5Xz/gzlwsg==
+ dependencies:
+ "@ant-design/cssinjs" "^1.21.0"
+ "@babel/runtime" "^7.23.2"
+ rc-util "^5.38.0"
+
+"@ant-design/cssinjs@^1.21.0", "@ant-design/cssinjs@^1.23.0":
+ version "1.24.0"
+ resolved "https://registry.npmmirror.com/@ant-design/cssinjs/-/cssinjs-1.24.0.tgz"
+ integrity sha512-K4cYrJBsgvL+IoozUXYjbT6LHHNt+19a9zkvpBPxLjFHas1UpPM2A5MlhROb0BT8N8WoavM5VsP9MeSeNK/3mg==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ "@emotion/hash" "^0.8.0"
+ "@emotion/unitless" "^0.7.5"
+ classnames "^2.3.1"
+ csstype "^3.1.3"
+ rc-util "^5.35.0"
+ stylis "^4.3.4"
+
+"@ant-design/fast-color@^2.0.6":
+ version "2.0.6"
+ resolved "https://registry.npmmirror.com/@ant-design/fast-color/-/fast-color-2.0.6.tgz"
+ integrity sha512-y2217gk4NqL35giHl72o6Zzqji9O7vHh9YmhUVkPtAOpoTCH4uWxo/pr4VE8t0+ChEPs0qo4eJRC5Q1eXWo3vA==
+ dependencies:
+ "@babel/runtime" "^7.24.7"
+
+"@ant-design/icons-svg@^4.4.0":
+ version "4.4.2"
+ resolved "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
+ integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
+
+"@ant-design/icons@^5.6.1":
+ version "5.6.1"
+ resolved "https://registry.npmmirror.com/@ant-design/icons/-/icons-5.6.1.tgz"
+ integrity sha512-0/xS39c91WjPAZOWsvi1//zjx6kAp4kxWwctR6kuU6p133w8RU0D2dSCvZC19uQyharg/sAvYxGYWl01BbZZfg==
+ dependencies:
+ "@ant-design/colors" "^7.0.0"
+ "@ant-design/icons-svg" "^4.4.0"
+ "@babel/runtime" "^7.24.8"
+ classnames "^2.2.6"
+ rc-util "^5.31.1"
+
+"@ant-design/react-slick@~1.1.2":
+ version "1.1.2"
+ resolved "https://registry.npmmirror.com/@ant-design/react-slick/-/react-slick-1.1.2.tgz"
+ integrity sha512-EzlvzE6xQUBrZuuhSAFTdsr4P2bBBHGZwKFemEfq8gIGyIQCxalYfZW/T2ORbtQx5rU69o+WycP3exY/7T1hGA==
+ dependencies:
+ "@babel/runtime" "^7.10.4"
+ classnames "^2.2.5"
+ json2mq "^0.2.0"
+ resize-observer-polyfill "^1.5.1"
+ throttle-debounce "^5.0.0"
+
+"@babel/runtime@^7.10.1", "@babel/runtime@^7.10.4", "@babel/runtime@^7.11.1", "@babel/runtime@^7.11.2", "@babel/runtime@^7.16.7", "@babel/runtime@^7.18.0", "@babel/runtime@^7.18.3", "@babel/runtime@^7.20.0", "@babel/runtime@^7.20.7", "@babel/runtime@^7.21.0", "@babel/runtime@^7.22.5", "@babel/runtime@^7.23.2", "@babel/runtime@^7.23.6", "@babel/runtime@^7.23.9", "@babel/runtime@^7.24.4", "@babel/runtime@^7.24.7", "@babel/runtime@^7.24.8", "@babel/runtime@^7.25.7", "@babel/runtime@^7.26.0":
+ version "7.28.4"
+ resolved "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.28.4.tgz"
+ integrity sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==
+
+"@emotion/hash@^0.8.0":
+ version "0.8.0"
+ resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz"
+ integrity sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==
+
+"@emotion/unitless@^0.7.5":
+ version "0.7.5"
+ resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.7.5.tgz"
+ integrity sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==
+
+"@esbuild/aix-ppc64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49"
+ integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg==
+
+"@esbuild/android-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03"
+ integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ==
+
+"@esbuild/android-arm@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae"
+ integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg==
+
+"@esbuild/android-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6"
+ integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g==
+
+"@esbuild/darwin-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz#0a678c4ac4bf8717e67481e1a797e6c152f93c84"
+ integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w==
+
+"@esbuild/darwin-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe"
+ integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ==
+
+"@esbuild/freebsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a"
+ integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA==
+
+"@esbuild/freebsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb"
+ integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw==
+
+"@esbuild/linux-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5"
+ integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA==
+
+"@esbuild/linux-arm@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f"
+ integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw==
+
+"@esbuild/linux-ia32@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b"
+ integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw==
+
+"@esbuild/linux-loong64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb"
+ integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw==
+
+"@esbuild/linux-mips64el@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5"
+ integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ==
+
+"@esbuild/linux-ppc64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74"
+ integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw==
+
+"@esbuild/linux-riscv64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273"
+ integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww==
+
+"@esbuild/linux-s390x@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263"
+ integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw==
+
+"@esbuild/linux-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910"
+ integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ==
+
+"@esbuild/netbsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077"
+ integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg==
+
+"@esbuild/netbsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034"
+ integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A==
+
+"@esbuild/openbsd-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad"
+ integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg==
+
+"@esbuild/openbsd-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2"
+ integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw==
+
+"@esbuild/openharmony-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1"
+ integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ==
+
+"@esbuild/sunos-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244"
+ integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA==
+
+"@esbuild/win32-arm64@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935"
+ integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q==
+
+"@esbuild/win32-ia32@0.25.11":
+ version "0.25.11"
+ resolved "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343"
+ integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA==
+
+"@esbuild/win32-x64@0.25.11":
+ version "0.25.11"
+ resolved "https://r.cnpmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz"
+ integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA==
+
+"@eslint-community/eslint-utils@^4.7.0", "@eslint-community/eslint-utils@^4.8.0":
+ version "4.9.0"
+ resolved "https://r.cnpmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz"
+ integrity sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==
+ dependencies:
+ eslint-visitor-keys "^3.4.3"
+
+"@eslint-community/regexpp@^4.10.0", "@eslint-community/regexpp@^4.12.1":
+ version "4.12.1"
+ resolved "https://r.cnpmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz"
+ integrity sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==
+
+"@eslint/config-array@^0.21.0":
+ version "0.21.0"
+ resolved "https://r.cnpmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz"
+ integrity sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==
+ dependencies:
+ "@eslint/object-schema" "^2.1.6"
+ debug "^4.3.1"
+ minimatch "^3.1.2"
+
+"@eslint/config-helpers@^0.4.0":
+ version "0.4.0"
+ resolved "https://r.cnpmjs.org/@eslint/config-helpers/-/config-helpers-0.4.0.tgz"
+ integrity sha512-WUFvV4WoIwW8Bv0KeKCIIEgdSiFOsulyN0xrMu+7z43q/hkOLXjvb5u7UC9jDxvRzcrbEmuZBX5yJZz1741jog==
+ dependencies:
+ "@eslint/core" "^0.16.0"
+
+"@eslint/core@^0.16.0":
+ version "0.16.0"
+ resolved "https://r.cnpmjs.org/@eslint/core/-/core-0.16.0.tgz"
+ integrity sha512-nmC8/totwobIiFcGkDza3GIKfAw1+hLiYVrh3I1nIomQ8PEr5cxg34jnkmGawul/ep52wGRAcyeDCNtWKSOj4Q==
+ dependencies:
+ "@types/json-schema" "^7.0.15"
+
+"@eslint/eslintrc@^3.3.1":
+ version "3.3.1"
+ resolved "https://r.cnpmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz"
+ integrity sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==
+ dependencies:
+ ajv "^6.12.4"
+ debug "^4.3.2"
+ espree "^10.0.1"
+ globals "^14.0.0"
+ ignore "^5.2.0"
+ import-fresh "^3.2.1"
+ js-yaml "^4.1.0"
+ minimatch "^3.1.2"
+ strip-json-comments "^3.1.1"
+
+"@eslint/js@9.37.0", "@eslint/js@^9.36.0":
+ version "9.37.0"
+ resolved "https://r.cnpmjs.org/@eslint/js/-/js-9.37.0.tgz"
+ integrity sha512-jaS+NJ+hximswBG6pjNX0uEJZkrT0zwpVi3BA3vX22aFGjJjmgSTSmPpZCRKmoBL5VY/M6p0xsSJx7rk7sy5gg==
+
+"@eslint/object-schema@^2.1.6":
+ version "2.1.6"
+ resolved "https://r.cnpmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz"
+ integrity sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==
+
+"@eslint/plugin-kit@^0.4.0":
+ version "0.4.0"
+ resolved "https://r.cnpmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.0.tgz"
+ integrity sha512-sB5uyeq+dwCWyPi31B2gQlVlo+j5brPlWx4yZBrEaRo/nhdDE8Xke1gsGgtiBdaBTxuTkceLVuVt/pclrasb0A==
+ dependencies:
+ "@eslint/core" "^0.16.0"
+ levn "^0.4.1"
+
+"@humanfs/core@^0.19.1":
+ version "0.19.1"
+ resolved "https://r.cnpmjs.org/@humanfs/core/-/core-0.19.1.tgz"
+ integrity sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==
+
+"@humanfs/node@^0.16.6":
+ version "0.16.7"
+ resolved "https://r.cnpmjs.org/@humanfs/node/-/node-0.16.7.tgz"
+ integrity sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==
+ dependencies:
+ "@humanfs/core" "^0.19.1"
+ "@humanwhocodes/retry" "^0.4.0"
+
+"@humanwhocodes/module-importer@^1.0.1":
+ version "1.0.1"
+ resolved "https://r.cnpmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz"
+ integrity sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==
+
+"@humanwhocodes/retry@^0.4.0", "@humanwhocodes/retry@^0.4.2":
+ version "0.4.3"
+ resolved "https://r.cnpmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz"
+ integrity sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==
+
+"@isaacs/cliui@^8.0.2":
+ version "8.0.2"
+ resolved "https://registry.npmmirror.com/@isaacs/cliui/-/cliui-8.0.2.tgz"
+ integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==
+ 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"
+
+"@jridgewell/gen-mapping@^0.3.2":
+ version "0.3.13"
+ resolved "https://registry.npmmirror.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz"
+ integrity sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==
+ dependencies:
+ "@jridgewell/sourcemap-codec" "^1.5.0"
+ "@jridgewell/trace-mapping" "^0.3.24"
+
+"@jridgewell/resolve-uri@^3.1.0":
+ version "3.1.2"
+ resolved "https://registry.npmmirror.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz"
+ integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==
+
+"@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0":
+ version "1.5.5"
+ resolved "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz"
+ integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==
+
+"@jridgewell/trace-mapping@^0.3.24":
+ version "0.3.31"
+ resolved "https://registry.npmmirror.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz"
+ integrity sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==
+ dependencies:
+ "@jridgewell/resolve-uri" "^3.1.0"
+ "@jridgewell/sourcemap-codec" "^1.4.14"
+
+"@nodelib/fs.scandir@2.1.5":
+ version "2.1.5"
+ resolved "https://r2.cnpmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz"
+ integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==
+ dependencies:
+ "@nodelib/fs.stat" "2.0.5"
+ run-parallel "^1.1.9"
+
+"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2":
+ version "2.0.5"
+ resolved "https://r2.cnpmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz"
+ integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==
+
+"@nodelib/fs.walk@^1.2.3":
+ version "1.2.8"
+ resolved "https://r2.cnpmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz"
+ integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==
+ dependencies:
+ "@nodelib/fs.scandir" "2.1.5"
+ fastq "^1.6.0"
+
+"@pkgjs/parseargs@^0.11.0":
+ version "0.11.0"
+ resolved "https://registry.npmmirror.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
+ integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
+
+"@rc-component/async-validator@^5.0.3":
+ version "5.0.4"
+ resolved "https://registry.npmmirror.com/@rc-component/async-validator/-/async-validator-5.0.4.tgz"
+ integrity sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==
+ dependencies:
+ "@babel/runtime" "^7.24.4"
+
+"@rc-component/color-picker@~2.0.1":
+ version "2.0.1"
+ resolved "https://registry.npmmirror.com/@rc-component/color-picker/-/color-picker-2.0.1.tgz"
+ integrity sha512-WcZYwAThV/b2GISQ8F+7650r5ZZJ043E57aVBFkQ+kSY4C6wdofXgB0hBx+GPGpIU0Z81eETNoDUJMr7oy/P8Q==
+ dependencies:
+ "@ant-design/fast-color" "^2.0.6"
+ "@babel/runtime" "^7.23.6"
+ classnames "^2.2.6"
+ rc-util "^5.38.1"
+
+"@rc-component/context@^1.4.0":
+ version "1.4.0"
+ resolved "https://registry.npmmirror.com/@rc-component/context/-/context-1.4.0.tgz"
+ integrity sha512-kFcNxg9oLRMoL3qki0OMxK+7g5mypjgaaJp/pkOis/6rVxma9nJBF/8kCIuTYHUQNr0ii7MxqE33wirPZLJQ2w==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ rc-util "^5.27.0"
+
+"@rc-component/mini-decimal@^1.0.1":
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/@rc-component/mini-decimal/-/mini-decimal-1.1.0.tgz"
+ integrity sha512-jS4E7T9Li2GuYwI6PyiVXmxTiM6b07rlD9Ge8uGZSCz3WlzcG5ZK7g5bbuKNeZ9pgUuPK/5guV781ujdVpm4HQ==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+
+"@rc-component/mutate-observer@^1.1.0":
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/@rc-component/mutate-observer/-/mutate-observer-1.1.0.tgz"
+ integrity sha512-QjrOsDXQusNwGZPf4/qRQasg7UFEj06XiCJ8iuiq/Io7CrHrgVi6Uuetw60WAMG1799v+aM8kyc+1L/GBbHSlw==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ classnames "^2.3.2"
+ rc-util "^5.24.4"
+
+"@rc-component/portal@^1.0.0-8", "@rc-component/portal@^1.0.0-9", "@rc-component/portal@^1.0.2", "@rc-component/portal@^1.1.0", "@rc-component/portal@^1.1.1":
+ version "1.1.2"
+ resolved "https://registry.npmmirror.com/@rc-component/portal/-/portal-1.1.2.tgz"
+ integrity sha512-6f813C0IsasTZms08kfA8kPAGxbbkYToa8ALaiDIGGECU4i9hj8Plgbx0sNJDrey3EtHO30hmdaxtT0138xZcg==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ classnames "^2.3.2"
+ rc-util "^5.24.4"
+
+"@rc-component/qrcode@~1.0.1":
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/@rc-component/qrcode/-/qrcode-1.0.1.tgz"
+ integrity sha512-g8eeeaMyFXVlq8cZUeaxCDhfIYjpao0l9cvm5gFwKXy/Vm1yDWV7h2sjH5jHYzdFedlVKBpATFB1VKMrHzwaWQ==
+ dependencies:
+ "@babel/runtime" "^7.24.7"
+ classnames "^2.3.2"
+
+"@rc-component/tour@~1.15.1":
+ version "1.15.1"
+ resolved "https://registry.npmmirror.com/@rc-component/tour/-/tour-1.15.1.tgz"
+ integrity sha512-Tr2t7J1DKZUpfJuDZWHxyxWpfmj8EZrqSgyMZ+BCdvKZ6r1UDsfU46M/iWAAFBy961Ssfom2kv5f3UcjIL2CmQ==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ "@rc-component/portal" "^1.0.0-9"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.3.2"
+ rc-util "^5.24.4"
+
+"@rc-component/trigger@^2.0.0", "@rc-component/trigger@^2.1.1", "@rc-component/trigger@^2.3.0":
+ version "2.3.0"
+ resolved "https://registry.npmmirror.com/@rc-component/trigger/-/trigger-2.3.0.tgz"
+ integrity sha512-iwaxZyzOuK0D7lS+0AQEtW52zUWxoGqTGkke3dRyb8pYiShmRpCjB/8TzPI4R6YySCH7Vm9BZj/31VPiiQTLBg==
+ dependencies:
+ "@babel/runtime" "^7.23.2"
+ "@rc-component/portal" "^1.1.0"
+ classnames "^2.3.2"
+ rc-motion "^2.0.0"
+ rc-resize-observer "^1.3.1"
+ rc-util "^5.44.0"
+
+"@rolldown/pluginutils@1.0.0-beta.35":
+ version "1.0.0-beta.35"
+ resolved "https://r.cnpmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.35.tgz"
+ integrity sha512-slYrCpoxJUqzFDDNlvrOYRazQUNRvWPjXA17dAOISY3rDMxX6k8K4cj2H+hEYMHF81HO3uNd5rHVigAWRM5dSg==
+
+"@rollup/rollup-android-arm-eabi@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.4.tgz#59e7478d310f7e6a7c72453978f562483828112f"
+ integrity sha512-BTm2qKNnWIQ5auf4deoetINJm2JzvihvGb9R6K/ETwKLql/Bb3Eg2H1FBp1gUb4YGbydMA3jcmQTR73q7J+GAA==
+
+"@rollup/rollup-android-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.4.tgz#a825192a0b1b2f27a5c950c439e7e37a33c5d056"
+ integrity sha512-P9LDQiC5vpgGFgz7GSM6dKPCiqR3XYN1WwJKA4/BUVDjHpYsf3iBEmVz62uyq20NGYbiGPR5cNHI7T1HqxNs2w==
+
+"@rollup/rollup-darwin-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.4.tgz#4ee37078bccd725ae3c5f30ef92efc8e1bf886f3"
+ integrity sha512-QRWSW+bVccAvZF6cbNZBJwAehmvG9NwfWHwMy4GbWi/BQIA/laTIktebT2ipVjNncqE6GLPxOok5hsECgAxGZg==
+
+"@rollup/rollup-darwin-x64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.4.tgz#43cc08bd05bf9f388f125e7210a544e62d368d90"
+ integrity sha512-hZgP05pResAkRJxL1b+7yxCnXPGsXU0fG9Yfd6dUaoGk+FhdPKCJ5L1Sumyxn8kvw8Qi5PvQ8ulenUbRjzeCTw==
+
+"@rollup/rollup-freebsd-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.4.tgz#bc8e640e28abe52450baf3fc80d9b26d9bb6587d"
+ integrity sha512-xmc30VshuBNUd58Xk4TKAEcRZHaXlV+tCxIXELiE9sQuK3kG8ZFgSPi57UBJt8/ogfhAF5Oz4ZSUBN77weM+mQ==
+
+"@rollup/rollup-freebsd-x64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.4.tgz#e981a22e057cc8c65bb523019d344d3a66b15bbc"
+ integrity sha512-WdSLpZFjOEqNZGmHflxyifolwAiZmDQzuOzIq9L27ButpCVpD7KzTRtEG1I0wMPFyiyUdOO+4t8GvrnBLQSwpw==
+
+"@rollup/rollup-linux-arm-gnueabihf@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.4.tgz#4036b68904f392a20f3499d63b33e055b67eb274"
+ integrity sha512-xRiOu9Of1FZ4SxVbB0iEDXc4ddIcjCv2aj03dmW8UrZIW7aIQ9jVJdLBIhxBI+MaTnGAKyvMwPwQnoOEvP7FgQ==
+
+"@rollup/rollup-linux-arm-musleabihf@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.4.tgz#d3b1b9589606e0ff916801c855b1ace9e733427a"
+ integrity sha512-FbhM2p9TJAmEIEhIgzR4soUcsW49e9veAQCziwbR+XWB2zqJ12b4i/+hel9yLiD8pLncDH4fKIPIbt5238341Q==
+
+"@rollup/rollup-linux-arm64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.4.tgz#cbf0943c477e3b96340136dd3448eaf144378cf2"
+ integrity sha512-4n4gVwhPHR9q/g8lKCyz0yuaD0MvDf7dV4f9tHt0C73Mp8h38UCtSCSE6R9iBlTbXlmA8CjpsZoujhszefqueg==
+
+"@rollup/rollup-linux-arm64-musl@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.4.tgz#837f5a428020d5dce1c3b4cc049876075402cf78"
+ integrity sha512-u0n17nGA0nvi/11gcZKsjkLj1QIpAuPFQbR48Subo7SmZJnGxDpspyw2kbpuoQnyK+9pwf3pAoEXerJs/8Mi9g==
+
+"@rollup/rollup-linux-loong64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.4.tgz#532c214ababb32ab4bc21b4054278b9a8979e516"
+ integrity sha512-0G2c2lpYtbTuXo8KEJkDkClE/+/2AFPdPAbmaHoE870foRFs4pBrDehilMcrSScrN/fB/1HTaWO4bqw+ewBzMQ==
+
+"@rollup/rollup-linux-ppc64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.4.tgz#93900163b61b49cee666d10ee38257a8b1dd161a"
+ integrity sha512-teSACug1GyZHmPDv14VNbvZFX779UqWTsd7KtTM9JIZRDI5NUwYSIS30kzI8m06gOPB//jtpqlhmraQ68b5X2g==
+
+"@rollup/rollup-linux-riscv64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.4.tgz#f0ffdcc7066ca04bc972370c74289f35c7a7dc42"
+ integrity sha512-/MOEW3aHjjs1p4Pw1Xk4+3egRevx8Ji9N6HUIA1Ifh8Q+cg9dremvFCUbOX2Zebz80BwJIgCBUemjqhU5XI5Eg==
+
+"@rollup/rollup-linux-riscv64-musl@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.4.tgz#361695c39dbe96773509745d77a870a32a9f8e48"
+ integrity sha512-1HHmsRyh845QDpEWzOFtMCph5Ts+9+yllCrREuBR/vg2RogAQGGBRC8lDPrPOMnrdOJ+mt1WLMOC2Kao/UwcvA==
+
+"@rollup/rollup-linux-s390x-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.4.tgz#09fc6cc2e266a2324e366486ae5d1bca48c43a6a"
+ integrity sha512-seoeZp4L/6D1MUyjWkOMRU6/iLmCU2EjbMTyAG4oIOs1/I82Y5lTeaxW0KBfkUdHAWN7j25bpkt0rjnOgAcQcA==
+
+"@rollup/rollup-linux-x64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.4.tgz#aa9d5b307c08f05d3454225bb0a2b4cc87eeb2e1"
+ integrity sha512-Wi6AXf0k0L7E2gteNsNHUs7UMwCIhsCTs6+tqQ5GPwVRWMaflqGec4Sd8n6+FNFDw9vGcReqk2KzBDhCa1DLYg==
+
+"@rollup/rollup-linux-x64-musl@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.4.tgz#26949e5b4645502a61daba2f7a8416bd17cb5382"
+ integrity sha512-dtBZYjDmCQ9hW+WgEkaffvRRCKm767wWhxsFW3Lw86VXz/uJRuD438/XvbZT//B96Vs8oTA8Q4A0AfHbrxP9zw==
+
+"@rollup/rollup-openharmony-arm64@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.4.tgz#ef493c072f9dac7e0edb6c72d63366846b6ffcd9"
+ integrity sha512-1ox+GqgRWqaB1RnyZXL8PD6E5f7YyRUJYnCqKpNzxzP0TkaUh112NDrR9Tt+C8rJ4x5G9Mk8PQR3o7Ku2RKqKA==
+
+"@rollup/rollup-win32-arm64-msvc@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.4.tgz#56e1aaa6a630d2202ee7ec0adddd05cf384ffd44"
+ integrity sha512-8GKr640PdFNXwzIE0IrkMWUNUomILLkfeHjXBi/nUvFlpZP+FA8BKGKpacjW6OUUHaNI6sUURxR2U2g78FOHWQ==
+
+"@rollup/rollup-win32-ia32-msvc@4.52.4":
+ version "4.52.4"
+ resolved "https://registry.npmmirror.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.4.tgz#0a44bbf933a9651c7da2b8569fa448dec0de7480"
+ integrity sha512-AIy/jdJ7WtJ/F6EcfOb2GjR9UweO0n43jNObQMb6oGxkYTfLcnN7vYYpG+CN3lLxrQkzWnMOoNSHTW54pgbVxw==
+
+"@rollup/rollup-win32-x64-gnu@4.52.4":
+ version "4.52.4"
+ resolved "https://r.cnpmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.4.tgz"
+ integrity sha512-UF9KfsH9yEam0UjTwAgdK0anlQ7c8/pWPU2yVjyWcF1I1thABt6WXE47cI71pGiZ8wGvxohBoLnxM04L/wj8mQ==
+
+"@rollup/rollup-win32-x64-msvc@4.52.4":
+ version "4.52.4"
+ resolved "https://r.cnpmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.4.tgz"
+ integrity sha512-bf9PtUa0u8IXDVxzRToFQKsNCRz9qLYfR/MpECxl4mRoWYjAeFjgxj1XdZr2M/GNVpT05p+LgQOHopYDlUu6/w==
+
+"@swc/core-darwin-arm64@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-darwin-arm64/-/core-darwin-arm64-1.13.5.tgz#7638c073946f9297753ed9a2eb198d07b2336a24"
+ integrity sha512-lKNv7SujeXvKn16gvQqUQI5DdyY8v7xcoO3k06/FJbHJS90zEwZdQiMNRiqpYw/orU543tPaWgz7cIYWhbopiQ==
+
+"@swc/core-darwin-x64@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-darwin-x64/-/core-darwin-x64-1.13.5.tgz#18061167378f0fb285e17818494bc6c89dd07551"
+ integrity sha512-ILd38Fg/w23vHb0yVjlWvQBoE37ZJTdlLHa8LRCFDdX4WKfnVBiblsCU9ar4QTMNdeTBEX9iUF4IrbNWhaF1Ng==
+
+"@swc/core-linux-arm-gnueabihf@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-linux-arm-gnueabihf/-/core-linux-arm-gnueabihf-1.13.5.tgz#4c8062bd598049b5b9b0beb762e075e76b4c23c3"
+ integrity sha512-Q6eS3Pt8GLkXxqz9TAw+AUk9HpVJt8Uzm54MvPsqp2yuGmY0/sNaPPNVqctCX9fu/Nu8eaWUen0si6iEiCsazQ==
+
+"@swc/core-linux-arm64-gnu@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-linux-arm64-gnu/-/core-linux-arm64-gnu-1.13.5.tgz#7222d321197ea9304e387933e87d775849fc1ae6"
+ integrity sha512-aNDfeN+9af+y+M2MYfxCzCy/VDq7Z5YIbMqRI739o8Ganz6ST+27kjQFd8Y/57JN/hcnUEa9xqdS3XY7WaVtSw==
+
+"@swc/core-linux-arm64-musl@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-linux-arm64-musl/-/core-linux-arm64-musl-1.13.5.tgz#51e7958deaf37edc212bd9dc0ea1476f151d2bea"
+ integrity sha512-9+ZxFN5GJag4CnYnq6apKTnnezpfJhCumyz0504/JbHLo+Ue+ZtJnf3RhyA9W9TINtLE0bC4hKpWi8ZKoETyOQ==
+
+"@swc/core-linux-x64-gnu@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-linux-x64-gnu/-/core-linux-x64-gnu-1.13.5.tgz#3476beab93ab03e92844d955ca9d9289aa4a5993"
+ integrity sha512-WD530qvHrki8Ywt/PloKUjaRKgstQqNGvmZl54g06kA+hqtSE2FTG9gngXr3UJxYu/cNAjJYiBifm7+w4nbHbA==
+
+"@swc/core-linux-x64-musl@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-linux-x64-musl/-/core-linux-x64-musl-1.13.5.tgz#f4934b1e77e2a297909bb3ab977836205c36e5e0"
+ integrity sha512-Luj8y4OFYx4DHNQTWjdIuKTq2f5k6uSXICqx+FSabnXptaOBAbJHNbHT/06JZh6NRUouaf0mYXN0mcsqvkhd7Q==
+
+"@swc/core-win32-arm64-msvc@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-win32-arm64-msvc/-/core-win32-arm64-msvc-1.13.5.tgz#5084c107435cfc82d4d901bfb388dc319d38a236"
+ integrity sha512-cZ6UpumhF9SDJvv4DA2fo9WIzlNFuKSkZpZmPG1c+4PFSEMy5DFOjBSllCvnqihCabzXzpn6ykCwBmHpy31vQw==
+
+"@swc/core-win32-ia32-msvc@1.13.5":
+ version "1.13.5"
+ resolved "https://registry.npmmirror.com/@swc/core-win32-ia32-msvc/-/core-win32-ia32-msvc-1.13.5.tgz#f8b2e28bc51b30467e316ed736a130c1324b9880"
+ integrity sha512-C5Yi/xIikrFUzZcyGj9L3RpKljFvKiDMtyDzPKzlsDrKIw2EYY+bF88gB6oGY5RGmv4DAX8dbnpRAqgFD0FMEw==
+
+"@swc/core-win32-x64-msvc@1.13.5":
+ version "1.13.5"
+ resolved "https://r.cnpmjs.org/@swc/core-win32-x64-msvc/-/core-win32-x64-msvc-1.13.5.tgz"
+ integrity sha512-YrKdMVxbYmlfybCSbRtrilc6UA8GF5aPmGKBdPvjrarvsmf4i7ZHGCEnLtfOMd3Lwbs2WUZq3WdMbozYeLU93Q==
+
+"@swc/core@^1.13.5":
+ version "1.13.5"
+ resolved "https://r.cnpmjs.org/@swc/core/-/core-1.13.5.tgz"
+ integrity sha512-WezcBo8a0Dg2rnR82zhwoR6aRNxeTGfK5QCD6TQ+kg3xx/zNT02s/0o+81h/3zhvFSB24NtqEr8FTw88O5W/JQ==
+ dependencies:
+ "@swc/counter" "^0.1.3"
+ "@swc/types" "^0.1.24"
+ optionalDependencies:
+ "@swc/core-darwin-arm64" "1.13.5"
+ "@swc/core-darwin-x64" "1.13.5"
+ "@swc/core-linux-arm-gnueabihf" "1.13.5"
+ "@swc/core-linux-arm64-gnu" "1.13.5"
+ "@swc/core-linux-arm64-musl" "1.13.5"
+ "@swc/core-linux-x64-gnu" "1.13.5"
+ "@swc/core-linux-x64-musl" "1.13.5"
+ "@swc/core-win32-arm64-msvc" "1.13.5"
+ "@swc/core-win32-ia32-msvc" "1.13.5"
+ "@swc/core-win32-x64-msvc" "1.13.5"
+
+"@swc/counter@^0.1.3":
+ version "0.1.3"
+ resolved "https://r.cnpmjs.org/@swc/counter/-/counter-0.1.3.tgz"
+ integrity sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==
+
+"@swc/types@^0.1.24":
+ version "0.1.25"
+ resolved "https://r.cnpmjs.org/@swc/types/-/types-0.1.25.tgz"
+ integrity sha512-iAoY/qRhNH8a/hBvm3zKj9qQ4oc2+3w1unPJa2XvTK3XjeLXtzcCingVPw/9e5mn1+0yPqxcBGp9Jf0pkfMb1g==
+ dependencies:
+ "@swc/counter" "^0.1.3"
+
+"@tanstack/query-core@5.90.3":
+ version "5.90.3"
+ resolved "https://registry.npmmirror.com/@tanstack/query-core/-/query-core-5.90.3.tgz"
+ integrity sha512-HtPOnCwmx4dd35PfXU8jjkhwYrsHfuqgC8RCJIwWglmhIUIlzPP0ZcEkDAc+UtAWCiLm7T8rxeEfHZlz3hYMCA==
+
+"@tanstack/react-query@^5.90.3":
+ version "5.90.3"
+ resolved "https://registry.npmmirror.com/@tanstack/react-query/-/react-query-5.90.3.tgz"
+ integrity sha512-i/LRL6DtuhG6bjGzavIMIVuKKPWx2AnEBIsBfuMm3YoHne0a20nWmsatOCBcVSaT0/8/5YFjNkebHAPLVUSi0Q==
+ dependencies:
+ "@tanstack/query-core" "5.90.3"
+
+"@types/estree@1.0.8", "@types/estree@^1.0.6":
+ version "1.0.8"
+ resolved "https://r.cnpmjs.org/@types/estree/-/estree-1.0.8.tgz"
+ integrity sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==
+
+"@types/history@^4.7.11":
+ version "4.7.11"
+ resolved "https://registry.npmmirror.com/@types/history/-/history-4.7.11.tgz"
+ integrity sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==
+
+"@types/json-schema@^7.0.15":
+ version "7.0.15"
+ resolved "https://r.cnpmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz"
+ integrity sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==
+
+"@types/node@^24.6.0":
+ version "24.7.2"
+ resolved "https://r.cnpmjs.org/@types/node/-/node-24.7.2.tgz"
+ integrity sha512-/NbVmcGTP+lj5oa4yiYxxeBjRivKQ5Ns1eSZeB99ExsEQ6rX5XYU1Zy/gGxY/ilqtD4Etx9mKyrPxZRetiahhA==
+ dependencies:
+ undici-types "~7.14.0"
+
+"@types/react-dom@^19.1.9":
+ version "19.2.2"
+ resolved "https://r.cnpmjs.org/@types/react-dom/-/react-dom-19.2.2.tgz"
+ integrity sha512-9KQPoO6mZCi7jcIStSnlOWn2nEF3mNmyr3rIAsGnAbQKYbRLyqmeSc39EVgtxXVia+LMT8j3knZLAZAh+xLmrw==
+
+"@types/react-router-dom@^5.3.3":
+ version "5.3.3"
+ resolved "https://registry.npmmirror.com/@types/react-router-dom/-/react-router-dom-5.3.3.tgz"
+ integrity sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==
+ dependencies:
+ "@types/history" "^4.7.11"
+ "@types/react" "*"
+ "@types/react-router" "*"
+
+"@types/react-router@*":
+ version "5.1.20"
+ resolved "https://registry.npmmirror.com/@types/react-router/-/react-router-5.1.20.tgz"
+ integrity sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==
+ dependencies:
+ "@types/history" "^4.7.11"
+ "@types/react" "*"
+
+"@types/react@*", "@types/react@^19.1.16":
+ version "19.2.2"
+ resolved "https://r.cnpmjs.org/@types/react/-/react-19.2.2.tgz"
+ integrity sha512-6mDvHUFSjyT2B2yeNx2nUgMxh9LtOWvkhIU3uePn2I2oyNymUAX1NIsdgviM4CH+JSrp2D2hsMvJOkxY+0wNRA==
+ dependencies:
+ csstype "^3.0.2"
+
+"@typescript-eslint/eslint-plugin@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.46.1.tgz"
+ integrity sha512-rUsLh8PXmBjdiPY+Emjz9NX2yHvhS11v0SR6xNJkm5GM1MO9ea/1GoDKlHHZGrOJclL/cZ2i/vRUYVtjRhrHVQ==
+ dependencies:
+ "@eslint-community/regexpp" "^4.10.0"
+ "@typescript-eslint/scope-manager" "8.46.1"
+ "@typescript-eslint/type-utils" "8.46.1"
+ "@typescript-eslint/utils" "8.46.1"
+ "@typescript-eslint/visitor-keys" "8.46.1"
+ graphemer "^1.4.0"
+ ignore "^7.0.0"
+ natural-compare "^1.4.0"
+ ts-api-utils "^2.1.0"
+
+"@typescript-eslint/parser@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/parser/-/parser-8.46.1.tgz"
+ integrity sha512-6JSSaBZmsKvEkbRUkf7Zj7dru/8ZCrJxAqArcLaVMee5907JdtEbKGsZ7zNiIm/UAkpGUkaSMZEXShnN2D1HZA==
+ dependencies:
+ "@typescript-eslint/scope-manager" "8.46.1"
+ "@typescript-eslint/types" "8.46.1"
+ "@typescript-eslint/typescript-estree" "8.46.1"
+ "@typescript-eslint/visitor-keys" "8.46.1"
+ debug "^4.3.4"
+
+"@typescript-eslint/project-service@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/project-service/-/project-service-8.46.1.tgz"
+ integrity sha512-FOIaFVMHzRskXr5J4Jp8lFVV0gz5ngv3RHmn+E4HYxSJ3DgDzU7fVI1/M7Ijh1zf6S7HIoaIOtln1H5y8V+9Zg==
+ dependencies:
+ "@typescript-eslint/tsconfig-utils" "^8.46.1"
+ "@typescript-eslint/types" "^8.46.1"
+ debug "^4.3.4"
+
+"@typescript-eslint/scope-manager@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.46.1.tgz"
+ integrity sha512-weL9Gg3/5F0pVQKiF8eOXFZp8emqWzZsOJuWRUNtHT+UNV2xSJegmpCNQHy37aEQIbToTq7RHKhWvOsmbM680A==
+ dependencies:
+ "@typescript-eslint/types" "8.46.1"
+ "@typescript-eslint/visitor-keys" "8.46.1"
+
+"@typescript-eslint/tsconfig-utils@8.46.1", "@typescript-eslint/tsconfig-utils@^8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.46.1.tgz"
+ integrity sha512-X88+J/CwFvlJB+mK09VFqx5FE4H5cXD+H/Bdza2aEWkSb8hnWIQorNcscRl4IEo1Cz9VI/+/r/jnGWkbWPx54g==
+
+"@typescript-eslint/type-utils@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/type-utils/-/type-utils-8.46.1.tgz"
+ integrity sha512-+BlmiHIiqufBxkVnOtFwjah/vrkF4MtKKvpXrKSPLCkCtAp8H01/VV43sfqA98Od7nJpDcFnkwgyfQbOG0AMvw==
+ dependencies:
+ "@typescript-eslint/types" "8.46.1"
+ "@typescript-eslint/typescript-estree" "8.46.1"
+ "@typescript-eslint/utils" "8.46.1"
+ debug "^4.3.4"
+ ts-api-utils "^2.1.0"
+
+"@typescript-eslint/types@8.46.1", "@typescript-eslint/types@^8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/types/-/types-8.46.1.tgz"
+ integrity sha512-C+soprGBHwWBdkDpbaRC4paGBrkIXxVlNohadL5o0kfhsXqOC6GYH2S/Obmig+I0HTDl8wMaRySwrfrXVP8/pQ==
+
+"@typescript-eslint/typescript-estree@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.46.1.tgz"
+ integrity sha512-uIifjT4s8cQKFQ8ZBXXyoUODtRoAd7F7+G8MKmtzj17+1UbdzFl52AzRyZRyKqPHhgzvXunnSckVu36flGy8cg==
+ dependencies:
+ "@typescript-eslint/project-service" "8.46.1"
+ "@typescript-eslint/tsconfig-utils" "8.46.1"
+ "@typescript-eslint/types" "8.46.1"
+ "@typescript-eslint/visitor-keys" "8.46.1"
+ debug "^4.3.4"
+ fast-glob "^3.3.2"
+ is-glob "^4.0.3"
+ minimatch "^9.0.4"
+ semver "^7.6.0"
+ ts-api-utils "^2.1.0"
+
+"@typescript-eslint/utils@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/utils/-/utils-8.46.1.tgz"
+ integrity sha512-vkYUy6LdZS7q1v/Gxb2Zs7zziuXN0wxqsetJdeZdRe/f5dwJFglmuvZBfTUivCtjH725C1jWCDfpadadD95EDQ==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.7.0"
+ "@typescript-eslint/scope-manager" "8.46.1"
+ "@typescript-eslint/types" "8.46.1"
+ "@typescript-eslint/typescript-estree" "8.46.1"
+
+"@typescript-eslint/visitor-keys@8.46.1":
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.46.1.tgz"
+ integrity sha512-ptkmIf2iDkNUjdeu2bQqhFPV1m6qTnFFjg7PPDjxKWaMaP0Z6I9l30Jr3g5QqbZGdw8YdYvLp+XnqnWWZOg/NA==
+ dependencies:
+ "@typescript-eslint/types" "8.46.1"
+ eslint-visitor-keys "^4.2.1"
+
+"@vitejs/plugin-react-swc@^4.1.0":
+ version "4.1.0"
+ resolved "https://r.cnpmjs.org/@vitejs/plugin-react-swc/-/plugin-react-swc-4.1.0.tgz"
+ integrity sha512-Ff690TUck0Anlh7wdIcnsVMhofeEVgm44Y4OYdeeEEPSKyZHzDI9gfVBvySEhDfXtBp8tLCbfsVKPWEMEjq8/g==
+ dependencies:
+ "@rolldown/pluginutils" "1.0.0-beta.35"
+ "@swc/core" "^1.13.5"
+
+acorn-jsx@^5.3.2:
+ version "5.3.2"
+ resolved "https://r2.cnpmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz"
+ integrity sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==
+
+acorn@^8.15.0:
+ version "8.15.0"
+ resolved "https://r.cnpmjs.org/acorn/-/acorn-8.15.0.tgz"
+ integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==
+
+ajv@^6.12.4:
+ version "6.12.6"
+ resolved "https://r2.cnpmjs.org/ajv/-/ajv-6.12.6.tgz"
+ integrity sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==
+ 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"
+
+ansi-regex@^5.0.1:
+ version "5.0.1"
+ resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-5.0.1.tgz"
+ integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-regex@^6.0.1:
+ version "6.2.2"
+ resolved "https://registry.npmmirror.com/ansi-regex/-/ansi-regex-6.2.2.tgz"
+ integrity sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
+ version "4.3.0"
+ resolved "https://r2.cnpmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz"
+ integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
+ dependencies:
+ color-convert "^2.0.1"
+
+ansi-styles@^6.1.0:
+ version "6.2.3"
+ resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-6.2.3.tgz"
+ integrity sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==
+
+antd@^5.27.5:
+ version "5.27.5"
+ resolved "https://registry.npmmirror.com/antd/-/antd-5.27.5.tgz"
+ integrity sha512-Ehd9mqtHvJ1clon1yJ/1BTV6eX/3SH2YXZZPTHUk8XdzXFwUioI+Lht47s+MaHIUBY77RnZrmtKwwR+VVu0l7A==
+ dependencies:
+ "@ant-design/colors" "^7.2.1"
+ "@ant-design/cssinjs" "^1.23.0"
+ "@ant-design/cssinjs-utils" "^1.1.3"
+ "@ant-design/fast-color" "^2.0.6"
+ "@ant-design/icons" "^5.6.1"
+ "@ant-design/react-slick" "~1.1.2"
+ "@babel/runtime" "^7.26.0"
+ "@rc-component/color-picker" "~2.0.1"
+ "@rc-component/mutate-observer" "^1.1.0"
+ "@rc-component/qrcode" "~1.0.1"
+ "@rc-component/tour" "~1.15.1"
+ "@rc-component/trigger" "^2.3.0"
+ classnames "^2.5.1"
+ copy-to-clipboard "^3.3.3"
+ dayjs "^1.11.11"
+ rc-cascader "~3.34.0"
+ rc-checkbox "~3.5.0"
+ rc-collapse "~3.9.0"
+ rc-dialog "~9.6.0"
+ rc-drawer "~7.3.0"
+ rc-dropdown "~4.2.1"
+ rc-field-form "~2.7.0"
+ rc-image "~7.12.0"
+ rc-input "~1.8.0"
+ rc-input-number "~9.5.0"
+ rc-mentions "~2.20.0"
+ rc-menu "~9.16.1"
+ rc-motion "^2.9.5"
+ rc-notification "~5.6.4"
+ rc-pagination "~5.1.0"
+ rc-picker "~4.11.3"
+ rc-progress "~4.0.0"
+ rc-rate "~2.13.1"
+ rc-resize-observer "^1.4.3"
+ rc-segmented "~2.7.0"
+ rc-select "~14.16.8"
+ rc-slider "~11.1.9"
+ rc-steps "~6.0.1"
+ rc-switch "~4.1.0"
+ rc-table "~7.54.0"
+ rc-tabs "~15.7.0"
+ rc-textarea "~1.10.2"
+ rc-tooltip "~6.4.0"
+ rc-tree "~5.13.1"
+ rc-tree-select "~5.27.0"
+ rc-upload "~4.9.2"
+ rc-util "^5.44.4"
+ scroll-into-view-if-needed "^3.1.0"
+ throttle-debounce "^5.0.2"
+
+any-promise@^1.0.0:
+ version "1.3.0"
+ resolved "https://registry.npmmirror.com/any-promise/-/any-promise-1.3.0.tgz"
+ integrity sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==
+
+anymatch@~3.1.2:
+ version "3.1.3"
+ resolved "https://registry.npmmirror.com/anymatch/-/anymatch-3.1.3.tgz"
+ integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==
+ dependencies:
+ normalize-path "^3.0.0"
+ picomatch "^2.0.4"
+
+arg@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.npmmirror.com/arg/-/arg-5.0.2.tgz"
+ integrity sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==
+
+argparse@^2.0.1:
+ version "2.0.1"
+ resolved "https://r2.cnpmjs.org/argparse/-/argparse-2.0.1.tgz"
+ integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==
+
+asynckit@^0.4.0:
+ version "0.4.0"
+ resolved "https://registry.npmmirror.com/asynckit/-/asynckit-0.4.0.tgz#c79ed97f7f34cb8f2ba1bc9790bcc366474b4b79"
+ integrity sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==
+
+axios@^1.12.2:
+ version "1.12.2"
+ resolved "https://registry.npmmirror.com/axios/-/axios-1.12.2.tgz#6c307390136cf7a2278d09cec63b136dfc6e6da7"
+ integrity sha512-vMJzPewAlRyOgxV2dU0Cuz2O8zzzx9VYtbJOaBgXFeLc4IV/Eg50n4LowmehOOR61S8ZMpc2K5Sa7g6A4jfkUw==
+ dependencies:
+ follow-redirects "^1.15.6"
+ form-data "^4.0.4"
+ proxy-from-env "^1.1.0"
+
+balanced-match@^1.0.0:
+ version "1.0.2"
+ resolved "https://r2.cnpmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
+ integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
+
+binary-extensions@^2.0.0:
+ version "2.3.0"
+ resolved "https://registry.npmmirror.com/binary-extensions/-/binary-extensions-2.3.0.tgz"
+ integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==
+
+brace-expansion@^1.1.7:
+ version "1.1.12"
+ resolved "https://r.cnpmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz"
+ integrity sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==
+ dependencies:
+ balanced-match "^1.0.0"
+ concat-map "0.0.1"
+
+brace-expansion@^2.0.1:
+ version "2.0.2"
+ resolved "https://r.cnpmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz"
+ integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
+ dependencies:
+ balanced-match "^1.0.0"
+
+braces@^3.0.3, braces@~3.0.2:
+ version "3.0.3"
+ resolved "https://r.cnpmjs.org/braces/-/braces-3.0.3.tgz"
+ integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==
+ dependencies:
+ fill-range "^7.1.1"
+
+call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6"
+ integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==
+ dependencies:
+ es-errors "^1.3.0"
+ function-bind "^1.1.2"
+
+callsites@^3.0.0:
+ version "3.1.0"
+ resolved "https://r2.cnpmjs.org/callsites/-/callsites-3.1.0.tgz"
+ integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
+
+camelcase-css@^2.0.1:
+ version "2.0.1"
+ resolved "https://registry.npmmirror.com/camelcase-css/-/camelcase-css-2.0.1.tgz"
+ integrity sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==
+
+chalk@^4.0.0:
+ version "4.1.2"
+ resolved "https://r2.cnpmjs.org/chalk/-/chalk-4.1.2.tgz"
+ integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==
+ dependencies:
+ ansi-styles "^4.1.0"
+ supports-color "^7.1.0"
+
+chokidar@^3.6.0:
+ version "3.6.0"
+ resolved "https://registry.npmmirror.com/chokidar/-/chokidar-3.6.0.tgz"
+ integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==
+ dependencies:
+ anymatch "~3.1.2"
+ braces "~3.0.2"
+ glob-parent "~5.1.2"
+ is-binary-path "~2.1.0"
+ is-glob "~4.0.1"
+ normalize-path "~3.0.0"
+ readdirp "~3.6.0"
+ optionalDependencies:
+ fsevents "~2.3.2"
+
+classnames@2.x, classnames@^2.2.1, classnames@^2.2.3, classnames@^2.2.5, classnames@^2.2.6, classnames@^2.3.1, classnames@^2.3.2, classnames@^2.5.1:
+ version "2.5.1"
+ resolved "https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz"
+ integrity sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==
+
+color-convert@^2.0.1:
+ version "2.0.1"
+ resolved "https://r2.cnpmjs.org/color-convert/-/color-convert-2.0.1.tgz"
+ integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==
+ dependencies:
+ color-name "~1.1.4"
+
+color-name@~1.1.4:
+ version "1.1.4"
+ resolved "https://r2.cnpmjs.org/color-name/-/color-name-1.1.4.tgz"
+ integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==
+
+combined-stream@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.npmmirror.com/combined-stream/-/combined-stream-1.0.8.tgz#c3d45a8b34fd730631a110a8a2520682b31d5a7f"
+ integrity sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==
+ dependencies:
+ delayed-stream "~1.0.0"
+
+commander@^4.0.0:
+ version "4.1.1"
+ resolved "https://registry.npmmirror.com/commander/-/commander-4.1.1.tgz"
+ integrity sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==
+
+compute-scroll-into-view@^3.0.2:
+ version "3.1.1"
+ resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-3.1.1.tgz"
+ integrity sha512-VRhuHOLoKYOy4UbilLbUzbYg93XLjv2PncJC50EuTWPA3gaja1UjBsUP/D/9/juV3vQFr6XBEzn9KCAHdUvOHw==
+
+concat-map@0.0.1:
+ version "0.0.1"
+ resolved "https://r2.cnpmjs.org/concat-map/-/concat-map-0.0.1.tgz"
+ integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==
+
+cookie@^1.0.1:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/cookie/-/cookie-1.0.2.tgz"
+ integrity sha512-9Kr/j4O16ISv8zBBhJoi4bXOYNTkFLOqSL3UDB0njXxCXNezjeyVrJyGOWtgfs/q2km1gwBcfH8q1yEGoMYunA==
+
+copy-to-clipboard@^3.3.3:
+ version "3.3.3"
+ resolved "https://registry.npmmirror.com/copy-to-clipboard/-/copy-to-clipboard-3.3.3.tgz"
+ integrity sha512-2KV8NhB5JqC3ky0r9PMCAZKbUHSwtEo4CwCs0KXgruG43gX5PMqDEBbVU4OUzw2MuAWUfsuFmWvEKG5QRfSnJA==
+ dependencies:
+ toggle-selection "^1.0.6"
+
+cross-spawn@^7.0.6:
+ version "7.0.6"
+ resolved "https://r.cnpmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz"
+ integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==
+ dependencies:
+ path-key "^3.1.0"
+ shebang-command "^2.0.0"
+ which "^2.0.1"
+
+crypto-js@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.npmmirror.com/crypto-js/-/crypto-js-4.2.0.tgz#4d931639ecdfd12ff80e8186dba6af2c2e856631"
+ integrity sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==
+
+cssesc@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/cssesc/-/cssesc-3.0.0.tgz"
+ integrity sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==
+
+csstype@^3.0.2, csstype@^3.1.3:
+ version "3.1.3"
+ resolved "https://r.cnpmjs.org/csstype/-/csstype-3.1.3.tgz"
+ integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
+
+dayjs@^1.11.11:
+ version "1.11.18"
+ resolved "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.18.tgz"
+ integrity sha512-zFBQ7WFRvVRhKcWoUh+ZA1g2HVgUbsZm9sbddh8EC5iv93sui8DVVz1Npvz+r6meo9VKfa8NyLWBsQK1VvIKPA==
+
+debug@^4.3.1, debug@^4.3.2, debug@^4.3.4:
+ version "4.4.3"
+ resolved "https://r.cnpmjs.org/debug/-/debug-4.4.3.tgz"
+ integrity sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==
+ dependencies:
+ ms "^2.1.3"
+
+deep-is@^0.1.3:
+ version "0.1.4"
+ resolved "https://r2.cnpmjs.org/deep-is/-/deep-is-0.1.4.tgz"
+ integrity sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==
+
+delayed-stream@~1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/delayed-stream/-/delayed-stream-1.0.0.tgz#df3ae199acadfb7d440aaae0b29e2272b24ec619"
+ integrity sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==
+
+didyoumean@^1.2.2:
+ version "1.2.2"
+ resolved "https://registry.npmmirror.com/didyoumean/-/didyoumean-1.2.2.tgz"
+ integrity sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==
+
+dlv@^1.1.3:
+ version "1.1.3"
+ resolved "https://registry.npmmirror.com/dlv/-/dlv-1.1.3.tgz"
+ integrity sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==
+
+dunder-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a"
+ integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==
+ dependencies:
+ call-bind-apply-helpers "^1.0.1"
+ es-errors "^1.3.0"
+ gopd "^1.2.0"
+
+eastasianwidth@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmmirror.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
+ integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
+
+emoji-regex@^8.0.0:
+ version "8.0.0"
+ resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-8.0.0.tgz"
+ integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
+emoji-regex@^9.2.2:
+ version "9.2.2"
+ resolved "https://registry.npmmirror.com/emoji-regex/-/emoji-regex-9.2.2.tgz"
+ integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
+
+es-define-property@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa"
+ integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==
+
+es-errors@^1.3.0:
+ version "1.3.0"
+ resolved "https://registry.npmmirror.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f"
+ integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==
+
+es-object-atoms@^1.0.0, es-object-atoms@^1.1.1:
+ version "1.1.1"
+ resolved "https://registry.npmmirror.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1"
+ integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==
+ dependencies:
+ es-errors "^1.3.0"
+
+es-set-tostringtag@^2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d"
+ integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==
+ dependencies:
+ es-errors "^1.3.0"
+ get-intrinsic "^1.2.6"
+ has-tostringtag "^1.0.2"
+ hasown "^2.0.2"
+
+esbuild@^0.25.0:
+ version "0.25.11"
+ resolved "https://r.cnpmjs.org/esbuild/-/esbuild-0.25.11.tgz"
+ integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q==
+ optionalDependencies:
+ "@esbuild/aix-ppc64" "0.25.11"
+ "@esbuild/android-arm" "0.25.11"
+ "@esbuild/android-arm64" "0.25.11"
+ "@esbuild/android-x64" "0.25.11"
+ "@esbuild/darwin-arm64" "0.25.11"
+ "@esbuild/darwin-x64" "0.25.11"
+ "@esbuild/freebsd-arm64" "0.25.11"
+ "@esbuild/freebsd-x64" "0.25.11"
+ "@esbuild/linux-arm" "0.25.11"
+ "@esbuild/linux-arm64" "0.25.11"
+ "@esbuild/linux-ia32" "0.25.11"
+ "@esbuild/linux-loong64" "0.25.11"
+ "@esbuild/linux-mips64el" "0.25.11"
+ "@esbuild/linux-ppc64" "0.25.11"
+ "@esbuild/linux-riscv64" "0.25.11"
+ "@esbuild/linux-s390x" "0.25.11"
+ "@esbuild/linux-x64" "0.25.11"
+ "@esbuild/netbsd-arm64" "0.25.11"
+ "@esbuild/netbsd-x64" "0.25.11"
+ "@esbuild/openbsd-arm64" "0.25.11"
+ "@esbuild/openbsd-x64" "0.25.11"
+ "@esbuild/openharmony-arm64" "0.25.11"
+ "@esbuild/sunos-x64" "0.25.11"
+ "@esbuild/win32-arm64" "0.25.11"
+ "@esbuild/win32-ia32" "0.25.11"
+ "@esbuild/win32-x64" "0.25.11"
+
+escape-string-regexp@^4.0.0:
+ version "4.0.0"
+ resolved "https://r2.cnpmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz"
+ integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==
+
+eslint-plugin-react-hooks@^5.2.0:
+ version "5.2.0"
+ resolved "https://r.cnpmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-5.2.0.tgz"
+ integrity sha512-+f15FfK64YQwZdJNELETdn5ibXEUQmW1DZL6KXhNnc2heoy/sg9VJJeT7n8TlMWouzWqSWavFkIhHyIbIAEapg==
+
+eslint-plugin-react-refresh@^0.4.22:
+ version "0.4.24"
+ resolved "https://r.cnpmjs.org/eslint-plugin-react-refresh/-/eslint-plugin-react-refresh-0.4.24.tgz"
+ integrity sha512-nLHIW7TEq3aLrEYWpVaJ1dRgFR+wLDPN8e8FpYAql/bMV2oBEfC37K0gLEGgv9fy66juNShSMV8OkTqzltcG/w==
+
+eslint-scope@^8.4.0:
+ version "8.4.0"
+ resolved "https://r.cnpmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz"
+ integrity sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==
+ dependencies:
+ esrecurse "^4.3.0"
+ estraverse "^5.2.0"
+
+eslint-visitor-keys@^3.4.3:
+ version "3.4.3"
+ resolved "https://r.cnpmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz"
+ integrity sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==
+
+eslint-visitor-keys@^4.2.1:
+ version "4.2.1"
+ resolved "https://r.cnpmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz"
+ integrity sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==
+
+eslint@^9.36.0:
+ version "9.37.0"
+ resolved "https://r.cnpmjs.org/eslint/-/eslint-9.37.0.tgz"
+ integrity sha512-XyLmROnACWqSxiGYArdef1fItQd47weqB7iwtfr9JHwRrqIXZdcFMvvEcL9xHCmL0SNsOvF0c42lWyM1U5dgig==
+ dependencies:
+ "@eslint-community/eslint-utils" "^4.8.0"
+ "@eslint-community/regexpp" "^4.12.1"
+ "@eslint/config-array" "^0.21.0"
+ "@eslint/config-helpers" "^0.4.0"
+ "@eslint/core" "^0.16.0"
+ "@eslint/eslintrc" "^3.3.1"
+ "@eslint/js" "9.37.0"
+ "@eslint/plugin-kit" "^0.4.0"
+ "@humanfs/node" "^0.16.6"
+ "@humanwhocodes/module-importer" "^1.0.1"
+ "@humanwhocodes/retry" "^0.4.2"
+ "@types/estree" "^1.0.6"
+ "@types/json-schema" "^7.0.15"
+ ajv "^6.12.4"
+ chalk "^4.0.0"
+ cross-spawn "^7.0.6"
+ debug "^4.3.2"
+ escape-string-regexp "^4.0.0"
+ eslint-scope "^8.4.0"
+ eslint-visitor-keys "^4.2.1"
+ espree "^10.4.0"
+ esquery "^1.5.0"
+ esutils "^2.0.2"
+ fast-deep-equal "^3.1.3"
+ file-entry-cache "^8.0.0"
+ find-up "^5.0.0"
+ glob-parent "^6.0.2"
+ ignore "^5.2.0"
+ imurmurhash "^0.1.4"
+ is-glob "^4.0.0"
+ json-stable-stringify-without-jsonify "^1.0.1"
+ lodash.merge "^4.6.2"
+ minimatch "^3.1.2"
+ natural-compare "^1.4.0"
+ optionator "^0.9.3"
+
+espree@^10.0.1, espree@^10.4.0:
+ version "10.4.0"
+ resolved "https://r.cnpmjs.org/espree/-/espree-10.4.0.tgz"
+ integrity sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==
+ dependencies:
+ acorn "^8.15.0"
+ acorn-jsx "^5.3.2"
+ eslint-visitor-keys "^4.2.1"
+
+esquery@^1.5.0:
+ version "1.6.0"
+ resolved "https://r.cnpmjs.org/esquery/-/esquery-1.6.0.tgz"
+ integrity sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==
+ dependencies:
+ estraverse "^5.1.0"
+
+esrecurse@^4.3.0:
+ version "4.3.0"
+ resolved "https://r2.cnpmjs.org/esrecurse/-/esrecurse-4.3.0.tgz"
+ integrity sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==
+ dependencies:
+ estraverse "^5.2.0"
+
+estraverse@^5.1.0, estraverse@^5.2.0:
+ version "5.3.0"
+ resolved "https://r2.cnpmjs.org/estraverse/-/estraverse-5.3.0.tgz"
+ integrity sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==
+
+esutils@^2.0.2:
+ version "2.0.3"
+ resolved "https://r2.cnpmjs.org/esutils/-/esutils-2.0.3.tgz"
+ integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==
+
+fast-deep-equal@^3.1.1, fast-deep-equal@^3.1.3:
+ version "3.1.3"
+ resolved "https://r2.cnpmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz"
+ integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==
+
+fast-glob@^3.3.2:
+ version "3.3.3"
+ resolved "https://r.cnpmjs.org/fast-glob/-/fast-glob-3.3.3.tgz"
+ integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==
+ dependencies:
+ "@nodelib/fs.stat" "^2.0.2"
+ "@nodelib/fs.walk" "^1.2.3"
+ glob-parent "^5.1.2"
+ merge2 "^1.3.0"
+ micromatch "^4.0.8"
+
+fast-json-stable-stringify@^2.0.0:
+ version "2.1.0"
+ resolved "https://r2.cnpmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz"
+ integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==
+
+fast-levenshtein@^2.0.6:
+ version "2.0.6"
+ resolved "https://r2.cnpmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz"
+ integrity sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==
+
+fastq@^1.6.0:
+ version "1.19.1"
+ resolved "https://r.cnpmjs.org/fastq/-/fastq-1.19.1.tgz"
+ integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==
+ dependencies:
+ reusify "^1.0.4"
+
+fdir@^6.5.0:
+ version "6.5.0"
+ resolved "https://r.cnpmjs.org/fdir/-/fdir-6.5.0.tgz"
+ integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==
+
+file-entry-cache@^8.0.0:
+ version "8.0.0"
+ resolved "https://r.cnpmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz"
+ integrity sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==
+ dependencies:
+ flat-cache "^4.0.0"
+
+fill-range@^7.1.1:
+ version "7.1.1"
+ resolved "https://r.cnpmjs.org/fill-range/-/fill-range-7.1.1.tgz"
+ integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==
+ dependencies:
+ to-regex-range "^5.0.1"
+
+find-up@^5.0.0:
+ version "5.0.0"
+ resolved "https://r2.cnpmjs.org/find-up/-/find-up-5.0.0.tgz"
+ integrity sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==
+ dependencies:
+ locate-path "^6.0.0"
+ path-exists "^4.0.0"
+
+flat-cache@^4.0.0:
+ version "4.0.1"
+ resolved "https://r.cnpmjs.org/flat-cache/-/flat-cache-4.0.1.tgz"
+ integrity sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==
+ dependencies:
+ flatted "^3.2.9"
+ keyv "^4.5.4"
+
+flatted@^3.2.9:
+ version "3.3.3"
+ resolved "https://r.cnpmjs.org/flatted/-/flatted-3.3.3.tgz"
+ integrity sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==
+
+follow-redirects@^1.15.6:
+ version "1.15.11"
+ resolved "https://registry.npmmirror.com/follow-redirects/-/follow-redirects-1.15.11.tgz#777d73d72a92f8ec4d2e410eb47352a56b8e8340"
+ integrity sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==
+
+foreground-child@^3.1.0:
+ version "3.3.1"
+ resolved "https://registry.npmmirror.com/foreground-child/-/foreground-child-3.3.1.tgz"
+ integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==
+ dependencies:
+ cross-spawn "^7.0.6"
+ signal-exit "^4.0.1"
+
+form-data@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.npmmirror.com/form-data/-/form-data-4.0.4.tgz#784cdcce0669a9d68e94d11ac4eea98088edd2c4"
+ integrity sha512-KrGhL9Q4zjj0kiUt5OO4Mr/A/jlI2jDYs5eHBpYHPcBEVSiipAvn2Ko2HnPe20rmcuuvMHNdZFp+4IlGTMF0Ow==
+ dependencies:
+ asynckit "^0.4.0"
+ combined-stream "^1.0.8"
+ es-set-tostringtag "^2.1.0"
+ hasown "^2.0.2"
+ mime-types "^2.1.12"
+
+fsevents@~2.3.2, fsevents@~2.3.3:
+ version "2.3.3"
+ resolved "https://registry.npmmirror.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6"
+ integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
+
+function-bind@^1.1.2:
+ version "1.1.2"
+ resolved "https://registry.npmmirror.com/function-bind/-/function-bind-1.1.2.tgz"
+ integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==
+
+get-intrinsic@^1.2.6:
+ version "1.3.0"
+ resolved "https://registry.npmmirror.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01"
+ integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==
+ dependencies:
+ call-bind-apply-helpers "^1.0.2"
+ es-define-property "^1.0.1"
+ es-errors "^1.3.0"
+ es-object-atoms "^1.1.1"
+ function-bind "^1.1.2"
+ get-proto "^1.0.1"
+ gopd "^1.2.0"
+ has-symbols "^1.1.0"
+ hasown "^2.0.2"
+ math-intrinsics "^1.1.0"
+
+get-proto@^1.0.1:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1"
+ integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==
+ dependencies:
+ dunder-proto "^1.0.1"
+ es-object-atoms "^1.0.0"
+
+glob-parent@^5.1.2, glob-parent@~5.1.2:
+ version "5.1.2"
+ resolved "https://r2.cnpmjs.org/glob-parent/-/glob-parent-5.1.2.tgz"
+ integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==
+ dependencies:
+ is-glob "^4.0.1"
+
+glob-parent@^6.0.2:
+ version "6.0.2"
+ resolved "https://r2.cnpmjs.org/glob-parent/-/glob-parent-6.0.2.tgz"
+ integrity sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==
+ dependencies:
+ is-glob "^4.0.3"
+
+glob@^10.3.10:
+ version "10.4.5"
+ resolved "https://registry.npmmirror.com/glob/-/glob-10.4.5.tgz"
+ integrity sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==
+ dependencies:
+ foreground-child "^3.1.0"
+ jackspeak "^3.1.2"
+ minimatch "^9.0.4"
+ minipass "^7.1.2"
+ package-json-from-dist "^1.0.0"
+ path-scurry "^1.11.1"
+
+globals@^14.0.0:
+ version "14.0.0"
+ resolved "https://r.cnpmjs.org/globals/-/globals-14.0.0.tgz"
+ integrity sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==
+
+globals@^16.4.0:
+ version "16.4.0"
+ resolved "https://r.cnpmjs.org/globals/-/globals-16.4.0.tgz"
+ integrity sha512-ob/2LcVVaVGCYN+r14cnwnoDPUufjiYgSqRhiFD0Q1iI4Odora5RE8Iv1D24hAz5oMophRGkGz+yuvQmmUMnMw==
+
+gopd@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.npmmirror.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1"
+ integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==
+
+graphemer@^1.4.0:
+ version "1.4.0"
+ resolved "https://r.cnpmjs.org/graphemer/-/graphemer-1.4.0.tgz"
+ integrity sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==
+
+has-flag@^4.0.0:
+ version "4.0.0"
+ resolved "https://r2.cnpmjs.org/has-flag/-/has-flag-4.0.0.tgz"
+ integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==
+
+has-symbols@^1.0.3, has-symbols@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338"
+ integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==
+
+has-tostringtag@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc"
+ integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==
+ dependencies:
+ has-symbols "^1.0.3"
+
+hasown@^2.0.2:
+ version "2.0.2"
+ resolved "https://registry.npmmirror.com/hasown/-/hasown-2.0.2.tgz"
+ integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==
+ dependencies:
+ function-bind "^1.1.2"
+
+ignore@^5.2.0:
+ version "5.3.2"
+ resolved "https://r.cnpmjs.org/ignore/-/ignore-5.3.2.tgz"
+ integrity sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==
+
+ignore@^7.0.0:
+ version "7.0.5"
+ resolved "https://r.cnpmjs.org/ignore/-/ignore-7.0.5.tgz"
+ integrity sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==
+
+import-fresh@^3.2.1:
+ version "3.3.1"
+ resolved "https://r.cnpmjs.org/import-fresh/-/import-fresh-3.3.1.tgz"
+ integrity sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==
+ dependencies:
+ parent-module "^1.0.0"
+ resolve-from "^4.0.0"
+
+imurmurhash@^0.1.4:
+ version "0.1.4"
+ resolved "https://r2.cnpmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz"
+ integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
+
+is-binary-path@~2.1.0:
+ version "2.1.0"
+ resolved "https://registry.npmmirror.com/is-binary-path/-/is-binary-path-2.1.0.tgz"
+ integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==
+ dependencies:
+ binary-extensions "^2.0.0"
+
+is-core-module@^2.16.0:
+ version "2.16.1"
+ resolved "https://registry.npmmirror.com/is-core-module/-/is-core-module-2.16.1.tgz"
+ integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==
+ dependencies:
+ hasown "^2.0.2"
+
+is-extglob@^2.1.1:
+ version "2.1.1"
+ resolved "https://r2.cnpmjs.org/is-extglob/-/is-extglob-2.1.1.tgz"
+ integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
+
+is-fullwidth-code-point@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz"
+ integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
+is-glob@^4.0.0, is-glob@^4.0.1, is-glob@^4.0.3, is-glob@~4.0.1:
+ version "4.0.3"
+ resolved "https://r2.cnpmjs.org/is-glob/-/is-glob-4.0.3.tgz"
+ integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==
+ dependencies:
+ is-extglob "^2.1.1"
+
+is-number@^7.0.0:
+ version "7.0.0"
+ resolved "https://r2.cnpmjs.org/is-number/-/is-number-7.0.0.tgz"
+ integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==
+
+isexe@^2.0.0:
+ version "2.0.0"
+ resolved "https://r2.cnpmjs.org/isexe/-/isexe-2.0.0.tgz"
+ integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==
+
+jackspeak@^3.1.2:
+ version "3.4.3"
+ resolved "https://registry.npmmirror.com/jackspeak/-/jackspeak-3.4.3.tgz"
+ integrity sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==
+ dependencies:
+ "@isaacs/cliui" "^8.0.2"
+ optionalDependencies:
+ "@pkgjs/parseargs" "^0.11.0"
+
+jiti@^1.21.6:
+ version "1.21.7"
+ resolved "https://registry.npmmirror.com/jiti/-/jiti-1.21.7.tgz"
+ integrity sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==
+
+"js-tokens@^3.0.0 || ^4.0.0":
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499"
+ integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
+
+js-yaml@^4.1.0:
+ version "4.1.0"
+ resolved "https://r2.cnpmjs.org/js-yaml/-/js-yaml-4.1.0.tgz"
+ integrity sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==
+ dependencies:
+ argparse "^2.0.1"
+
+json-buffer@3.0.1:
+ version "3.0.1"
+ resolved "https://r2.cnpmjs.org/json-buffer/-/json-buffer-3.0.1.tgz"
+ integrity sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==
+
+json-schema-traverse@^0.4.1:
+ version "0.4.1"
+ resolved "https://r2.cnpmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz"
+ integrity sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==
+
+json-stable-stringify-without-jsonify@^1.0.1:
+ version "1.0.1"
+ resolved "https://r2.cnpmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz"
+ integrity sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==
+
+json2mq@^0.2.0:
+ version "0.2.0"
+ resolved "https://registry.npmmirror.com/json2mq/-/json2mq-0.2.0.tgz"
+ integrity sha512-SzoRg7ux5DWTII9J2qkrZrqV1gt+rTaoufMxEzXbS26Uid0NwaJd123HcoB80TgubEppxxIGdNxCx50fEoEWQA==
+ dependencies:
+ string-convert "^0.2.0"
+
+keyv@^4.5.4:
+ version "4.5.4"
+ resolved "https://r.cnpmjs.org/keyv/-/keyv-4.5.4.tgz"
+ integrity sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==
+ dependencies:
+ json-buffer "3.0.1"
+
+levn@^0.4.1:
+ version "0.4.1"
+ resolved "https://r2.cnpmjs.org/levn/-/levn-0.4.1.tgz"
+ integrity sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==
+ dependencies:
+ prelude-ls "^1.2.1"
+ type-check "~0.4.0"
+
+lilconfig@^3.0.0, lilconfig@^3.1.3:
+ version "3.1.3"
+ resolved "https://registry.npmmirror.com/lilconfig/-/lilconfig-3.1.3.tgz"
+ integrity sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==
+
+lines-and-columns@^1.1.6:
+ version "1.2.4"
+ resolved "https://registry.npmmirror.com/lines-and-columns/-/lines-and-columns-1.2.4.tgz"
+ integrity sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==
+
+locate-path@^6.0.0:
+ version "6.0.0"
+ resolved "https://r2.cnpmjs.org/locate-path/-/locate-path-6.0.0.tgz"
+ integrity sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==
+ dependencies:
+ p-locate "^5.0.0"
+
+lodash.merge@^4.6.2:
+ version "4.6.2"
+ resolved "https://r2.cnpmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz"
+ integrity sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==
+
+loose-envify@^1.1.0, loose-envify@^1.4.0:
+ version "1.4.0"
+ resolved "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz#71ee51fa7be4caec1a63839f7e682d8132d30caf"
+ integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
+ dependencies:
+ js-tokens "^3.0.0 || ^4.0.0"
+
+lru-cache@^10.2.0:
+ version "10.4.3"
+ resolved "https://registry.npmmirror.com/lru-cache/-/lru-cache-10.4.3.tgz"
+ integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==
+
+math-intrinsics@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9"
+ integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
+
+merge2@^1.3.0:
+ version "1.4.1"
+ resolved "https://r2.cnpmjs.org/merge2/-/merge2-1.4.1.tgz"
+ integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==
+
+micromatch@^4.0.8:
+ version "4.0.8"
+ resolved "https://r.cnpmjs.org/micromatch/-/micromatch-4.0.8.tgz"
+ integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==
+ dependencies:
+ braces "^3.0.3"
+ picomatch "^2.3.1"
+
+mime-db@1.52.0:
+ version "1.52.0"
+ resolved "https://registry.npmmirror.com/mime-db/-/mime-db-1.52.0.tgz#bbabcdc02859f4987301c856e3387ce5ec43bf70"
+ integrity sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==
+
+mime-types@^2.1.12:
+ version "2.1.35"
+ resolved "https://registry.npmmirror.com/mime-types/-/mime-types-2.1.35.tgz#381a871b62a734450660ae3deee44813f70d959a"
+ integrity sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==
+ dependencies:
+ mime-db "1.52.0"
+
+minimatch@^3.1.2:
+ version "3.1.2"
+ resolved "https://r.cnpmjs.org/minimatch/-/minimatch-3.1.2.tgz"
+ integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
+ dependencies:
+ brace-expansion "^1.1.7"
+
+minimatch@^9.0.4:
+ version "9.0.5"
+ resolved "https://r.cnpmjs.org/minimatch/-/minimatch-9.0.5.tgz"
+ integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
+ dependencies:
+ brace-expansion "^2.0.1"
+
+"minipass@^5.0.0 || ^6.0.2 || ^7.0.0", minipass@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.npmmirror.com/minipass/-/minipass-7.1.2.tgz"
+ integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
+
+ms@^2.1.3:
+ version "2.1.3"
+ resolved "https://r2.cnpmjs.org/ms/-/ms-2.1.3.tgz"
+ integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
+
+mz@^2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmmirror.com/mz/-/mz-2.7.0.tgz"
+ integrity sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==
+ dependencies:
+ any-promise "^1.0.0"
+ object-assign "^4.0.1"
+ thenify-all "^1.0.0"
+
+nanoid@^3.3.11:
+ version "3.3.11"
+ resolved "https://r.cnpmjs.org/nanoid/-/nanoid-3.3.11.tgz"
+ integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
+
+natural-compare@^1.4.0:
+ version "1.4.0"
+ resolved "https://r2.cnpmjs.org/natural-compare/-/natural-compare-1.4.0.tgz"
+ integrity sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==
+
+normalize-path@^3.0.0, normalize-path@~3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/normalize-path/-/normalize-path-3.0.0.tgz"
+ integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==
+
+object-assign@^4.0.1, object-assign@^4.1.1:
+ version "4.1.1"
+ resolved "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz"
+ integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
+
+object-hash@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.npmmirror.com/object-hash/-/object-hash-3.0.0.tgz"
+ integrity sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==
+
+optionator@^0.9.3:
+ version "0.9.4"
+ resolved "https://r.cnpmjs.org/optionator/-/optionator-0.9.4.tgz"
+ integrity sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==
+ dependencies:
+ deep-is "^0.1.3"
+ fast-levenshtein "^2.0.6"
+ levn "^0.4.1"
+ prelude-ls "^1.2.1"
+ type-check "^0.4.0"
+ word-wrap "^1.2.5"
+
+p-limit@^3.0.2:
+ version "3.1.0"
+ resolved "https://r2.cnpmjs.org/p-limit/-/p-limit-3.1.0.tgz"
+ integrity sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==
+ dependencies:
+ yocto-queue "^0.1.0"
+
+p-locate@^5.0.0:
+ version "5.0.0"
+ resolved "https://r2.cnpmjs.org/p-locate/-/p-locate-5.0.0.tgz"
+ integrity sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==
+ dependencies:
+ p-limit "^3.0.2"
+
+package-json-from-dist@^1.0.0:
+ version "1.0.1"
+ resolved "https://registry.npmmirror.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz"
+ integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==
+
+parent-module@^1.0.0:
+ version "1.0.1"
+ resolved "https://r2.cnpmjs.org/parent-module/-/parent-module-1.0.1.tgz"
+ integrity sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==
+ dependencies:
+ callsites "^3.0.0"
+
+path-exists@^4.0.0:
+ version "4.0.0"
+ resolved "https://r2.cnpmjs.org/path-exists/-/path-exists-4.0.0.tgz"
+ integrity sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==
+
+path-key@^3.1.0:
+ version "3.1.1"
+ resolved "https://r2.cnpmjs.org/path-key/-/path-key-3.1.1.tgz"
+ integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==
+
+path-parse@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.npmmirror.com/path-parse/-/path-parse-1.0.7.tgz"
+ integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==
+
+path-scurry@^1.11.1:
+ version "1.11.1"
+ resolved "https://registry.npmmirror.com/path-scurry/-/path-scurry-1.11.1.tgz"
+ integrity sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==
+ dependencies:
+ lru-cache "^10.2.0"
+ minipass "^5.0.0 || ^6.0.2 || ^7.0.0"
+
+picocolors@^1.1.1:
+ version "1.1.1"
+ resolved "https://r.cnpmjs.org/picocolors/-/picocolors-1.1.1.tgz"
+ integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==
+
+picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.3.1:
+ version "2.3.1"
+ resolved "https://r.cnpmjs.org/picomatch/-/picomatch-2.3.1.tgz"
+ integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
+
+picomatch@^4.0.3:
+ version "4.0.3"
+ resolved "https://r.cnpmjs.org/picomatch/-/picomatch-4.0.3.tgz"
+ integrity sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==
+
+pify@^2.3.0:
+ version "2.3.0"
+ resolved "https://registry.npmmirror.com/pify/-/pify-2.3.0.tgz"
+ integrity sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==
+
+pirates@^4.0.1:
+ version "4.0.7"
+ resolved "https://registry.npmmirror.com/pirates/-/pirates-4.0.7.tgz"
+ integrity sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==
+
+postcss-import@^15.1.0:
+ version "15.1.0"
+ resolved "https://registry.npmmirror.com/postcss-import/-/postcss-import-15.1.0.tgz"
+ integrity sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==
+ dependencies:
+ postcss-value-parser "^4.0.0"
+ read-cache "^1.0.0"
+ resolve "^1.1.7"
+
+postcss-js@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.npmmirror.com/postcss-js/-/postcss-js-4.1.0.tgz"
+ integrity sha512-oIAOTqgIo7q2EOwbhb8UalYePMvYoIeRY2YKntdpFQXNosSu3vLrniGgmH9OKs/qAkfoj5oB3le/7mINW1LCfw==
+ dependencies:
+ camelcase-css "^2.0.1"
+
+postcss-load-config@^4.0.2:
+ version "4.0.2"
+ resolved "https://registry.npmmirror.com/postcss-load-config/-/postcss-load-config-4.0.2.tgz"
+ integrity sha512-bSVhyJGL00wMVoPUzAVAnbEoWyqRxkjv64tUl427SKnPrENtq6hJwUojroMz2VB+Q1edmi4IfrAPpami5VVgMQ==
+ dependencies:
+ lilconfig "^3.0.0"
+ yaml "^2.3.4"
+
+postcss-nested@^6.2.0:
+ version "6.2.0"
+ resolved "https://registry.npmmirror.com/postcss-nested/-/postcss-nested-6.2.0.tgz"
+ integrity sha512-HQbt28KulC5AJzG+cZtj9kvKB93CFCdLvog1WFLf1D+xmMvPGlBstkpTEZfK5+AN9hfJocyBFCNiqyS48bpgzQ==
+ dependencies:
+ postcss-selector-parser "^6.1.1"
+
+postcss-selector-parser@^6.1.1, postcss-selector-parser@^6.1.2:
+ version "6.1.2"
+ resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz"
+ integrity sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==
+ dependencies:
+ cssesc "^3.0.0"
+ util-deprecate "^1.0.2"
+
+postcss-value-parser@^4.0.0:
+ version "4.2.0"
+ resolved "https://registry.npmmirror.com/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz"
+ integrity sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==
+
+postcss@^8.4.47, postcss@^8.5.6:
+ version "8.5.6"
+ resolved "https://r.cnpmjs.org/postcss/-/postcss-8.5.6.tgz"
+ integrity sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==
+ dependencies:
+ nanoid "^3.3.11"
+ picocolors "^1.1.1"
+ source-map-js "^1.2.1"
+
+prelude-ls@^1.2.1:
+ version "1.2.1"
+ resolved "https://r2.cnpmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz"
+ integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==
+
+prop-types@^15.7.2:
+ version "15.8.1"
+ resolved "https://registry.npmmirror.com/prop-types/-/prop-types-15.8.1.tgz#67d87bf1a694f48435cf332c24af10214a3140b5"
+ integrity sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==
+ dependencies:
+ loose-envify "^1.4.0"
+ object-assign "^4.1.1"
+ react-is "^16.13.1"
+
+proxy-from-env@^1.1.0:
+ version "1.1.0"
+ resolved "https://registry.npmmirror.com/proxy-from-env/-/proxy-from-env-1.1.0.tgz#e102f16ca355424865755d2c9e8ea4f24d58c3e2"
+ integrity sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==
+
+punycode@^2.1.0:
+ version "2.3.1"
+ resolved "https://r.cnpmjs.org/punycode/-/punycode-2.3.1.tgz"
+ integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
+
+queue-microtask@^1.2.2:
+ version "1.2.3"
+ resolved "https://r2.cnpmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz"
+ integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==
+
+rc-cascader@~3.34.0:
+ version "3.34.0"
+ resolved "https://registry.npmmirror.com/rc-cascader/-/rc-cascader-3.34.0.tgz"
+ integrity sha512-KpXypcvju9ptjW9FaN2NFcA2QH9E9LHKq169Y0eWtH4e/wHQ5Wh5qZakAgvb8EKZ736WZ3B0zLLOBsrsja5Dag==
+ dependencies:
+ "@babel/runtime" "^7.25.7"
+ classnames "^2.3.1"
+ rc-select "~14.16.2"
+ rc-tree "~5.13.0"
+ rc-util "^5.43.0"
+
+rc-checkbox@~3.5.0:
+ version "3.5.0"
+ resolved "https://registry.npmmirror.com/rc-checkbox/-/rc-checkbox-3.5.0.tgz"
+ integrity sha512-aOAQc3E98HteIIsSqm6Xk2FPKIER6+5vyEFMZfo73TqM+VVAIqOkHoPjgKLqSNtVLWScoaM7vY2ZrGEheI79yg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.3.2"
+ rc-util "^5.25.2"
+
+rc-collapse@~3.9.0:
+ version "3.9.0"
+ resolved "https://registry.npmmirror.com/rc-collapse/-/rc-collapse-3.9.0.tgz"
+ integrity sha512-swDdz4QZ4dFTo4RAUMLL50qP0EY62N2kvmk2We5xYdRwcRn8WcYtuetCJpwpaCbUfUt5+huLpVxhvmnK+PHrkA==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ rc-motion "^2.3.4"
+ rc-util "^5.27.0"
+
+rc-dialog@~9.6.0:
+ version "9.6.0"
+ resolved "https://registry.npmmirror.com/rc-dialog/-/rc-dialog-9.6.0.tgz"
+ integrity sha512-ApoVi9Z8PaCQg6FsUzS8yvBEQy0ZL2PkuvAgrmohPkN3okps5WZ5WQWPc1RNuiOKaAYv8B97ACdsFU5LizzCqg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/portal" "^1.0.0-8"
+ classnames "^2.2.6"
+ rc-motion "^2.3.0"
+ rc-util "^5.21.0"
+
+rc-drawer@~7.3.0:
+ version "7.3.0"
+ resolved "https://registry.npmmirror.com/rc-drawer/-/rc-drawer-7.3.0.tgz"
+ integrity sha512-DX6CIgiBWNpJIMGFO8BAISFkxiuKitoizooj4BDyee8/SnBn0zwO2FHrNDpqqepj0E/TFTDpmEBCyFuTgC7MOg==
+ dependencies:
+ "@babel/runtime" "^7.23.9"
+ "@rc-component/portal" "^1.1.1"
+ classnames "^2.2.6"
+ rc-motion "^2.6.1"
+ rc-util "^5.38.1"
+
+rc-dropdown@~4.2.0, rc-dropdown@~4.2.1:
+ version "4.2.1"
+ resolved "https://registry.npmmirror.com/rc-dropdown/-/rc-dropdown-4.2.1.tgz"
+ integrity sha512-YDAlXsPv3I1n42dv1JpdM7wJ+gSUBfeyPK59ZpBD9jQhK9jVuxpjj3NmWQHOBceA1zEPVX84T2wbdb2SD0UjmA==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.2.6"
+ rc-util "^5.44.1"
+
+rc-field-form@~2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmmirror.com/rc-field-form/-/rc-field-form-2.7.0.tgz"
+ integrity sha512-hgKsCay2taxzVnBPZl+1n4ZondsV78G++XVsMIJCAoioMjlMQR9YwAp7JZDIECzIu2Z66R+f4SFIRrO2DjDNAA==
+ dependencies:
+ "@babel/runtime" "^7.18.0"
+ "@rc-component/async-validator" "^5.0.3"
+ rc-util "^5.32.2"
+
+rc-image@~7.12.0:
+ version "7.12.0"
+ resolved "https://registry.npmmirror.com/rc-image/-/rc-image-7.12.0.tgz"
+ integrity sha512-cZ3HTyyckPnNnUb9/DRqduqzLfrQRyi+CdHjdqgsyDpI3Ln5UX1kXnAhPBSJj9pVRzwRFgqkN7p9b6HBDjmu/Q==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ "@rc-component/portal" "^1.0.2"
+ classnames "^2.2.6"
+ rc-dialog "~9.6.0"
+ rc-motion "^2.6.2"
+ rc-util "^5.34.1"
+
+rc-input-number@~9.5.0:
+ version "9.5.0"
+ resolved "https://registry.npmmirror.com/rc-input-number/-/rc-input-number-9.5.0.tgz"
+ integrity sha512-bKaEvB5tHebUURAEXw35LDcnRZLq3x1k7GxfAqBMzmpHkDGzjAtnUL8y4y5N15rIFIg5IJgwr211jInl3cipag==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/mini-decimal" "^1.0.1"
+ classnames "^2.2.5"
+ rc-input "~1.8.0"
+ rc-util "^5.40.1"
+
+rc-input@~1.8.0:
+ version "1.8.0"
+ resolved "https://registry.npmmirror.com/rc-input/-/rc-input-1.8.0.tgz"
+ integrity sha512-KXvaTbX+7ha8a/k+eg6SYRVERK0NddX8QX7a7AnRvUa/rEH0CNMlpcBzBkhI0wp2C8C4HlMoYl8TImSN+fuHKA==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-util "^5.18.1"
+
+rc-mentions@~2.20.0:
+ version "2.20.0"
+ resolved "https://registry.npmmirror.com/rc-mentions/-/rc-mentions-2.20.0.tgz"
+ integrity sha512-w8HCMZEh3f0nR8ZEd466ATqmXFCMGMN5UFCzEUL0bM/nGw/wOS2GgRzKBcm19K++jDyuWCOJOdgcKGXU3fXfbQ==
+ dependencies:
+ "@babel/runtime" "^7.22.5"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.2.6"
+ rc-input "~1.8.0"
+ rc-menu "~9.16.0"
+ rc-textarea "~1.10.0"
+ rc-util "^5.34.1"
+
+rc-menu@~9.16.0, rc-menu@~9.16.1:
+ version "9.16.1"
+ resolved "https://registry.npmmirror.com/rc-menu/-/rc-menu-9.16.1.tgz"
+ integrity sha512-ghHx6/6Dvp+fw8CJhDUHFHDJ84hJE3BXNCzSgLdmNiFErWSOaZNsihDAsKq9ByTALo/xkNIwtDFGIl6r+RPXBg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "2.x"
+ rc-motion "^2.4.3"
+ rc-overflow "^1.3.1"
+ rc-util "^5.27.0"
+
+rc-motion@^2.0.0, rc-motion@^2.0.1, rc-motion@^2.3.0, rc-motion@^2.3.4, rc-motion@^2.4.3, rc-motion@^2.4.4, rc-motion@^2.6.1, rc-motion@^2.6.2, rc-motion@^2.9.0, rc-motion@^2.9.5:
+ version "2.9.5"
+ resolved "https://registry.npmmirror.com/rc-motion/-/rc-motion-2.9.5.tgz"
+ integrity sha512-w+XTUrfh7ArbYEd2582uDrEhmBHwK1ZENJiSJVb7uRxdE7qJSYjbO2eksRXmndqyKqKoYPc9ClpPh5242mV1vA==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-util "^5.44.0"
+
+rc-notification@~5.6.4:
+ version "5.6.4"
+ resolved "https://registry.npmmirror.com/rc-notification/-/rc-notification-5.6.4.tgz"
+ integrity sha512-KcS4O6B4qzM3KH7lkwOB7ooLPZ4b6J+VMmQgT51VZCeEcmghdeR4IrMcFq0LG+RPdnbe/ArT086tGM8Snimgiw==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ rc-motion "^2.9.0"
+ rc-util "^5.20.1"
+
+rc-overflow@^1.3.1, rc-overflow@^1.3.2:
+ version "1.4.1"
+ resolved "https://registry.npmmirror.com/rc-overflow/-/rc-overflow-1.4.1.tgz"
+ integrity sha512-3MoPQQPV1uKyOMVNd6SZfONi+f3st0r8PksexIdBTeIYbMX0Jr+k7pHEDvsXtR4BpCv90/Pv2MovVNhktKrwvw==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.37.0"
+
+rc-pagination@~5.1.0:
+ version "5.1.0"
+ resolved "https://registry.npmmirror.com/rc-pagination/-/rc-pagination-5.1.0.tgz"
+ integrity sha512-8416Yip/+eclTFdHXLKTxZvn70duYVGTvUUWbckCCZoIl3jagqke3GLsFrMs0bsQBikiYpZLD9206Ej4SOdOXQ==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.3.2"
+ rc-util "^5.38.0"
+
+rc-picker@~4.11.3:
+ version "4.11.3"
+ resolved "https://registry.npmmirror.com/rc-picker/-/rc-picker-4.11.3.tgz"
+ integrity sha512-MJ5teb7FlNE0NFHTncxXQ62Y5lytq6sh5nUw0iH8OkHL/TjARSEvSHpr940pWgjGANpjCwyMdvsEV55l5tYNSg==
+ dependencies:
+ "@babel/runtime" "^7.24.7"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.2.1"
+ rc-overflow "^1.3.2"
+ rc-resize-observer "^1.4.0"
+ rc-util "^5.43.0"
+
+rc-progress@~4.0.0:
+ version "4.0.0"
+ resolved "https://registry.npmmirror.com/rc-progress/-/rc-progress-4.0.0.tgz"
+ integrity sha512-oofVMMafOCokIUIBnZLNcOZFsABaUw8PPrf1/y0ZBvKZNpOiu5h4AO9vv11Sw0p4Hb3D0yGWuEattcQGtNJ/aw==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.6"
+ rc-util "^5.16.1"
+
+rc-rate@~2.13.1:
+ version "2.13.1"
+ resolved "https://registry.npmmirror.com/rc-rate/-/rc-rate-2.13.1.tgz"
+ integrity sha512-QUhQ9ivQ8Gy7mtMZPAjLbxBt5y9GRp65VcUyGUMF3N3fhiftivPHdpuDIaWIMOTEprAjZPC08bls1dQB+I1F2Q==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.5"
+ rc-util "^5.0.1"
+
+rc-resize-observer@^1.0.0, rc-resize-observer@^1.1.0, rc-resize-observer@^1.3.1, rc-resize-observer@^1.4.0, rc-resize-observer@^1.4.3:
+ version "1.4.3"
+ resolved "https://registry.npmmirror.com/rc-resize-observer/-/rc-resize-observer-1.4.3.tgz"
+ integrity sha512-YZLjUbyIWox8E9i9C3Tm7ia+W7euPItNWSPX5sCcQTYbnwDb5uNpnLHQCG1f22oZWUhLw4Mv2tFmeWe68CDQRQ==
+ dependencies:
+ "@babel/runtime" "^7.20.7"
+ classnames "^2.2.1"
+ rc-util "^5.44.1"
+ resize-observer-polyfill "^1.5.1"
+
+rc-segmented@~2.7.0:
+ version "2.7.0"
+ resolved "https://registry.npmmirror.com/rc-segmented/-/rc-segmented-2.7.0.tgz"
+ integrity sha512-liijAjXz+KnTRVnxxXG2sYDGd6iLL7VpGGdR8gwoxAXy2KglviKCxLWZdjKYJzYzGSUwKDSTdYk8brj54Bn5BA==
+ dependencies:
+ "@babel/runtime" "^7.11.1"
+ classnames "^2.2.1"
+ rc-motion "^2.4.4"
+ rc-util "^5.17.0"
+
+rc-select@~14.16.2, rc-select@~14.16.8:
+ version "14.16.8"
+ resolved "https://registry.npmmirror.com/rc-select/-/rc-select-14.16.8.tgz"
+ integrity sha512-NOV5BZa1wZrsdkKaiK7LHRuo5ZjZYMDxPP6/1+09+FB4KoNi8jcG1ZqLE3AVCxEsYMBe65OBx71wFoHRTP3LRg==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/trigger" "^2.1.1"
+ classnames "2.x"
+ rc-motion "^2.0.1"
+ rc-overflow "^1.3.1"
+ rc-util "^5.16.1"
+ rc-virtual-list "^3.5.2"
+
+rc-slider@~11.1.9:
+ version "11.1.9"
+ resolved "https://registry.npmmirror.com/rc-slider/-/rc-slider-11.1.9.tgz"
+ integrity sha512-h8IknhzSh3FEM9u8ivkskh+Ef4Yo4JRIY2nj7MrH6GQmrwV6mcpJf5/4KgH5JaVI1H3E52yCdpOlVyGZIeph5A==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.5"
+ rc-util "^5.36.0"
+
+rc-steps@~6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmmirror.com/rc-steps/-/rc-steps-6.0.1.tgz"
+ integrity sha512-lKHL+Sny0SeHkQKKDJlAjV5oZ8DwCdS2hFhAkIjuQt1/pB81M0cA0ErVFdHq9+jmPmFw1vJB2F5NBzFXLJxV+g==
+ dependencies:
+ "@babel/runtime" "^7.16.7"
+ classnames "^2.2.3"
+ rc-util "^5.16.1"
+
+rc-switch@~4.1.0:
+ version "4.1.0"
+ resolved "https://registry.npmmirror.com/rc-switch/-/rc-switch-4.1.0.tgz"
+ integrity sha512-TI8ufP2Az9oEbvyCeVE4+90PDSljGyuwix3fV58p7HV2o4wBnVToEyomJRVyTaZeqNPAp+vqeo4Wnj5u0ZZQBg==
+ dependencies:
+ "@babel/runtime" "^7.21.0"
+ classnames "^2.2.1"
+ rc-util "^5.30.0"
+
+rc-table@~7.54.0:
+ version "7.54.0"
+ resolved "https://registry.npmmirror.com/rc-table/-/rc-table-7.54.0.tgz"
+ integrity sha512-/wDTkki6wBTjwylwAGjpLKYklKo9YgjZwAU77+7ME5mBoS32Q4nAwoqhA2lSge6fobLW3Tap6uc5xfwaL2p0Sw==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ "@rc-component/context" "^1.4.0"
+ classnames "^2.2.5"
+ rc-resize-observer "^1.1.0"
+ rc-util "^5.44.3"
+ rc-virtual-list "^3.14.2"
+
+rc-tabs@~15.7.0:
+ version "15.7.0"
+ resolved "https://registry.npmmirror.com/rc-tabs/-/rc-tabs-15.7.0.tgz"
+ integrity sha512-ZepiE+6fmozYdWf/9gVp7k56PKHB1YYoDsKeQA1CBlJ/POIhjkcYiv0AGP0w2Jhzftd3AVvZP/K+V+Lpi2ankA==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ classnames "2.x"
+ rc-dropdown "~4.2.0"
+ rc-menu "~9.16.0"
+ rc-motion "^2.6.2"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.34.1"
+
+rc-textarea@~1.10.0, rc-textarea@~1.10.2:
+ version "1.10.2"
+ resolved "https://registry.npmmirror.com/rc-textarea/-/rc-textarea-1.10.2.tgz"
+ integrity sha512-HfaeXiaSlpiSp0I/pvWpecFEHpVysZ9tpDLNkxQbMvMz6gsr7aVZ7FpWP9kt4t7DB+jJXesYS0us1uPZnlRnwQ==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "^2.2.1"
+ rc-input "~1.8.0"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.27.0"
+
+rc-tooltip@~6.4.0:
+ version "6.4.0"
+ resolved "https://registry.npmmirror.com/rc-tooltip/-/rc-tooltip-6.4.0.tgz"
+ integrity sha512-kqyivim5cp8I5RkHmpsp1Nn/Wk+1oeloMv9c7LXNgDxUpGm+RbXJGL+OPvDlcRnx9DBeOe4wyOIl4OKUERyH1g==
+ dependencies:
+ "@babel/runtime" "^7.11.2"
+ "@rc-component/trigger" "^2.0.0"
+ classnames "^2.3.1"
+ rc-util "^5.44.3"
+
+rc-tree-select@~5.27.0:
+ version "5.27.0"
+ resolved "https://registry.npmmirror.com/rc-tree-select/-/rc-tree-select-5.27.0.tgz"
+ integrity sha512-2qTBTzwIT7LRI1o7zLyrCzmo5tQanmyGbSaGTIf7sYimCklAToVVfpMC6OAldSKolcnjorBYPNSKQqJmN3TCww==
+ dependencies:
+ "@babel/runtime" "^7.25.7"
+ classnames "2.x"
+ rc-select "~14.16.2"
+ rc-tree "~5.13.0"
+ rc-util "^5.43.0"
+
+rc-tree@~5.13.0, rc-tree@~5.13.1:
+ version "5.13.1"
+ resolved "https://registry.npmmirror.com/rc-tree/-/rc-tree-5.13.1.tgz"
+ integrity sha512-FNhIefhftobCdUJshO7M8uZTA9F4OPGVXqGfZkkD/5soDeOhwO06T/aKTrg0WD8gRg/pyfq+ql3aMymLHCTC4A==
+ dependencies:
+ "@babel/runtime" "^7.10.1"
+ classnames "2.x"
+ rc-motion "^2.0.1"
+ rc-util "^5.16.1"
+ rc-virtual-list "^3.5.1"
+
+rc-upload@~4.9.2:
+ version "4.9.2"
+ resolved "https://registry.npmmirror.com/rc-upload/-/rc-upload-4.9.2.tgz"
+ integrity sha512-nHx+9rbd1FKMiMRYsqQ3NkXUv7COHPBo3X1Obwq9SWS6/diF/A0aJ5OHubvwUAIDs+4RMleljV0pcrNUc823GQ==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ classnames "^2.2.5"
+ rc-util "^5.2.0"
+
+rc-util@^5.0.1, rc-util@^5.16.1, rc-util@^5.17.0, rc-util@^5.18.1, rc-util@^5.2.0, rc-util@^5.20.1, rc-util@^5.21.0, rc-util@^5.24.4, rc-util@^5.25.2, rc-util@^5.27.0, rc-util@^5.30.0, rc-util@^5.31.1, rc-util@^5.32.2, rc-util@^5.34.1, rc-util@^5.35.0, rc-util@^5.36.0, rc-util@^5.37.0, rc-util@^5.38.0, rc-util@^5.38.1, rc-util@^5.40.1, rc-util@^5.43.0, rc-util@^5.44.0, rc-util@^5.44.1, rc-util@^5.44.3, rc-util@^5.44.4:
+ version "5.44.4"
+ resolved "https://registry.npmmirror.com/rc-util/-/rc-util-5.44.4.tgz"
+ integrity sha512-resueRJzmHG9Q6rI/DfK6Kdv9/Lfls05vzMs1Sk3M2P+3cJa+MakaZyWY8IPfehVuhPJFKrIY1IK4GqbiaiY5w==
+ dependencies:
+ "@babel/runtime" "^7.18.3"
+ react-is "^18.2.0"
+
+rc-virtual-list@^3.14.2, rc-virtual-list@^3.5.1, rc-virtual-list@^3.5.2:
+ version "3.19.2"
+ resolved "https://registry.npmmirror.com/rc-virtual-list/-/rc-virtual-list-3.19.2.tgz"
+ integrity sha512-Ys6NcjwGkuwkeaWBDqfI3xWuZ7rDiQXlH1o2zLfFzATfEgXcqpk8CkgMfbJD81McqjcJVez25a3kPxCR807evA==
+ dependencies:
+ "@babel/runtime" "^7.20.0"
+ classnames "^2.2.6"
+ rc-resize-observer "^1.0.0"
+ rc-util "^5.36.0"
+
+react-audio-player-pro@^1.3.3:
+ version "1.3.3"
+ resolved "https://registry.npmmirror.com/react-audio-player-pro/-/react-audio-player-pro-1.3.3.tgz#6c8e53faf5ed54a2c361afc9374cb72dbdc975bb"
+ integrity sha512-7+Esj6VaXVeznfuaLUmTV2bwORZNZjvB5sx1AsP7amb79nJE3gRj+a9ZyxYlyhAZgLazUF18930m80wvC/lZWQ==
+
+react-audio-player@^0.17.0:
+ version "0.17.0"
+ resolved "https://registry.npmmirror.com/react-audio-player/-/react-audio-player-0.17.0.tgz#4be7b1952512801f36ba0865a9c98f7b108f991e"
+ integrity sha512-aCZgusPxA9HK7rLZcTdhTbBH9l6do9vn3NorgoDZRxRxJlOy9uZWzPaKjd7QdcuP2vXpxGA/61JMnnOEY7NXeA==
+ dependencies:
+ prop-types "^15.7.2"
+
+react-dom@^18.2.0:
+ version "18.3.1"
+ resolved "https://registry.npmmirror.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4"
+ integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
+ dependencies:
+ loose-envify "^1.1.0"
+ scheduler "^0.23.2"
+
+react-is@^16.13.1:
+ version "16.13.1"
+ resolved "https://registry.npmmirror.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
+ integrity sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==
+
+react-is@^18.2.0:
+ version "18.3.1"
+ resolved "https://registry.npmmirror.com/react-is/-/react-is-18.3.1.tgz"
+ integrity sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==
+
+react-router-dom@^7.9.4:
+ version "7.9.4"
+ resolved "https://registry.npmmirror.com/react-router-dom/-/react-router-dom-7.9.4.tgz"
+ integrity sha512-f30P6bIkmYvnHHa5Gcu65deIXoA2+r3Eb6PJIAddvsT9aGlchMatJ51GgpU470aSqRRbFX22T70yQNUGuW3DfA==
+ dependencies:
+ react-router "7.9.4"
+
+react-router@7.9.4:
+ version "7.9.4"
+ resolved "https://registry.npmmirror.com/react-router/-/react-router-7.9.4.tgz"
+ integrity sha512-SD3G8HKviFHg9xj7dNODUKDFgpG4xqD5nhyd0mYoB5iISepuZAvzSr8ywxgxKJ52yRzf/HWtVHc9AWwoTbljvA==
+ dependencies:
+ cookie "^1.0.1"
+ set-cookie-parser "^2.6.0"
+
+react@^18.2.0:
+ version "18.3.1"
+ resolved "https://registry.npmmirror.com/react/-/react-18.3.1.tgz#49ab892009c53933625bd16b2533fc754cab2891"
+ integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
+read-cache@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/read-cache/-/read-cache-1.0.0.tgz"
+ integrity sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==
+ dependencies:
+ pify "^2.3.0"
+
+readdirp@~3.6.0:
+ version "3.6.0"
+ resolved "https://registry.npmmirror.com/readdirp/-/readdirp-3.6.0.tgz"
+ integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==
+ dependencies:
+ picomatch "^2.2.1"
+
+resize-observer-polyfill@^1.5.1:
+ version "1.5.1"
+ resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
+ integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
+
+resolve-from@^4.0.0:
+ version "4.0.0"
+ resolved "https://r2.cnpmjs.org/resolve-from/-/resolve-from-4.0.0.tgz"
+ integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==
+
+resolve@^1.1.7, resolve@^1.22.8:
+ version "1.22.10"
+ resolved "https://registry.npmmirror.com/resolve/-/resolve-1.22.10.tgz"
+ integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==
+ dependencies:
+ is-core-module "^2.16.0"
+ path-parse "^1.0.7"
+ supports-preserve-symlinks-flag "^1.0.0"
+
+reusify@^1.0.4:
+ version "1.1.0"
+ resolved "https://r.cnpmjs.org/reusify/-/reusify-1.1.0.tgz"
+ integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==
+
+rollup@^4.43.0:
+ version "4.52.4"
+ resolved "https://r.cnpmjs.org/rollup/-/rollup-4.52.4.tgz"
+ integrity sha512-CLEVl+MnPAiKh5pl4dEWSyMTpuflgNQiLGhMv8ezD5W/qP8AKvmYpCOKRRNOh7oRKnauBZ4SyeYkMS+1VSyKwQ==
+ dependencies:
+ "@types/estree" "1.0.8"
+ optionalDependencies:
+ "@rollup/rollup-android-arm-eabi" "4.52.4"
+ "@rollup/rollup-android-arm64" "4.52.4"
+ "@rollup/rollup-darwin-arm64" "4.52.4"
+ "@rollup/rollup-darwin-x64" "4.52.4"
+ "@rollup/rollup-freebsd-arm64" "4.52.4"
+ "@rollup/rollup-freebsd-x64" "4.52.4"
+ "@rollup/rollup-linux-arm-gnueabihf" "4.52.4"
+ "@rollup/rollup-linux-arm-musleabihf" "4.52.4"
+ "@rollup/rollup-linux-arm64-gnu" "4.52.4"
+ "@rollup/rollup-linux-arm64-musl" "4.52.4"
+ "@rollup/rollup-linux-loong64-gnu" "4.52.4"
+ "@rollup/rollup-linux-ppc64-gnu" "4.52.4"
+ "@rollup/rollup-linux-riscv64-gnu" "4.52.4"
+ "@rollup/rollup-linux-riscv64-musl" "4.52.4"
+ "@rollup/rollup-linux-s390x-gnu" "4.52.4"
+ "@rollup/rollup-linux-x64-gnu" "4.52.4"
+ "@rollup/rollup-linux-x64-musl" "4.52.4"
+ "@rollup/rollup-openharmony-arm64" "4.52.4"
+ "@rollup/rollup-win32-arm64-msvc" "4.52.4"
+ "@rollup/rollup-win32-ia32-msvc" "4.52.4"
+ "@rollup/rollup-win32-x64-gnu" "4.52.4"
+ "@rollup/rollup-win32-x64-msvc" "4.52.4"
+ fsevents "~2.3.2"
+
+run-parallel@^1.1.9:
+ version "1.2.0"
+ resolved "https://r2.cnpmjs.org/run-parallel/-/run-parallel-1.2.0.tgz"
+ integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==
+ dependencies:
+ queue-microtask "^1.2.2"
+
+scheduler@^0.23.2:
+ version "0.23.2"
+ resolved "https://registry.npmmirror.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3"
+ integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==
+ dependencies:
+ loose-envify "^1.1.0"
+
+scroll-into-view-if-needed@^3.1.0:
+ version "3.1.0"
+ resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-3.1.0.tgz"
+ integrity sha512-49oNpRjWRvnU8NyGVmUaYG4jtTkNonFZI86MmGRDqBphEK2EXT9gdEUoQPZhuBM8yWHxCWbobltqYO5M4XrUvQ==
+ dependencies:
+ compute-scroll-into-view "^3.0.2"
+
+semver@^7.6.0:
+ version "7.7.3"
+ resolved "https://r.cnpmjs.org/semver/-/semver-7.7.3.tgz"
+ integrity sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==
+
+set-cookie-parser@^2.6.0:
+ version "2.7.1"
+ resolved "https://registry.npmmirror.com/set-cookie-parser/-/set-cookie-parser-2.7.1.tgz"
+ integrity sha512-IOc8uWeOZgnb3ptbCURJWNjWUPcO3ZnTTdzsurqERrP6nPyv+paC55vJM0LpOlT2ne+Ix+9+CRG1MNLlyZ4GjQ==
+
+shebang-command@^2.0.0:
+ version "2.0.0"
+ resolved "https://r2.cnpmjs.org/shebang-command/-/shebang-command-2.0.0.tgz"
+ integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==
+ dependencies:
+ shebang-regex "^3.0.0"
+
+shebang-regex@^3.0.0:
+ version "3.0.0"
+ resolved "https://r2.cnpmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz"
+ integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==
+
+signal-exit@^4.0.1:
+ version "4.1.0"
+ resolved "https://registry.npmmirror.com/signal-exit/-/signal-exit-4.1.0.tgz"
+ integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==
+
+source-map-js@^1.2.1:
+ version "1.2.1"
+ resolved "https://r.cnpmjs.org/source-map-js/-/source-map-js-1.2.1.tgz"
+ integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==
+
+string-convert@^0.2.0:
+ version "0.2.1"
+ resolved "https://registry.npmmirror.com/string-convert/-/string-convert-0.2.1.tgz"
+ integrity sha512-u/1tdPl4yQnPBjnVrmdLo9gtuLvELKsAoRapekWggdiQNvvvum+jYF329d84NAa660KQw7pB2n36KrIKVoXa3A==
+
+"string-width-cjs@npm:string-width@^4.2.0":
+ version "4.2.3"
+ resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^4.1.0:
+ version "4.2.3"
+ resolved "https://registry.npmmirror.com/string-width/-/string-width-4.2.3.tgz"
+ integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+ dependencies:
+ emoji-regex "^8.0.0"
+ is-fullwidth-code-point "^3.0.0"
+ strip-ansi "^6.0.1"
+
+string-width@^5.0.1, string-width@^5.1.2:
+ version "5.1.2"
+ resolved "https://registry.npmmirror.com/string-width/-/string-width-5.1.2.tgz"
+ integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==
+ dependencies:
+ eastasianwidth "^0.2.0"
+ emoji-regex "^9.2.2"
+ strip-ansi "^7.0.1"
+
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+ version "6.0.1"
+ resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+ version "6.0.1"
+ resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-6.0.1.tgz"
+ integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+ dependencies:
+ ansi-regex "^5.0.1"
+
+strip-ansi@^7.0.1:
+ version "7.1.2"
+ resolved "https://registry.npmmirror.com/strip-ansi/-/strip-ansi-7.1.2.tgz"
+ integrity sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==
+ dependencies:
+ ansi-regex "^6.0.1"
+
+strip-json-comments@^3.1.1:
+ version "3.1.1"
+ resolved "https://r2.cnpmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
+ integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
+
+stylis@^4.3.4:
+ version "4.3.6"
+ resolved "https://registry.npmmirror.com/stylis/-/stylis-4.3.6.tgz"
+ integrity sha512-yQ3rwFWRfwNUY7H5vpU0wfdkNSnvnJinhF9830Swlaxl03zsOjCfmX0ugac+3LtK0lYSgwL/KXc8oYL3mG4YFQ==
+
+sucrase@^3.35.0:
+ version "3.35.0"
+ resolved "https://registry.npmmirror.com/sucrase/-/sucrase-3.35.0.tgz"
+ integrity sha512-8EbVDiu9iN/nESwxeSxDKe0dunta1GOlHufmSSXxMD2z2/tMZpDMpvXQGsc+ajGo8y2uYUmixaSRUc/QPoQ0GA==
+ dependencies:
+ "@jridgewell/gen-mapping" "^0.3.2"
+ commander "^4.0.0"
+ glob "^10.3.10"
+ lines-and-columns "^1.1.6"
+ mz "^2.7.0"
+ pirates "^4.0.1"
+ ts-interface-checker "^0.1.9"
+
+supports-color@^7.1.0:
+ version "7.2.0"
+ resolved "https://r2.cnpmjs.org/supports-color/-/supports-color-7.2.0.tgz"
+ integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==
+ dependencies:
+ has-flag "^4.0.0"
+
+supports-preserve-symlinks-flag@^1.0.0:
+ version "1.0.0"
+ resolved "https://registry.npmmirror.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz"
+ integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==
+
+tailwindcss@3.4.17:
+ version "3.4.17"
+ resolved "https://registry.npmmirror.com/tailwindcss/-/tailwindcss-3.4.17.tgz"
+ integrity sha512-w33E2aCvSDP0tW9RZuNXadXlkHXqFzSkQew/aIa2i/Sj8fThxwovwlXHSPXTbAHwEIhBFXAedUhP2tueAKP8Og==
+ dependencies:
+ "@alloc/quick-lru" "^5.2.0"
+ arg "^5.0.2"
+ chokidar "^3.6.0"
+ didyoumean "^1.2.2"
+ dlv "^1.1.3"
+ fast-glob "^3.3.2"
+ glob-parent "^6.0.2"
+ is-glob "^4.0.3"
+ jiti "^1.21.6"
+ lilconfig "^3.1.3"
+ micromatch "^4.0.8"
+ normalize-path "^3.0.0"
+ object-hash "^3.0.0"
+ picocolors "^1.1.1"
+ postcss "^8.4.47"
+ postcss-import "^15.1.0"
+ postcss-js "^4.0.1"
+ postcss-load-config "^4.0.2"
+ postcss-nested "^6.2.0"
+ postcss-selector-parser "^6.1.2"
+ resolve "^1.22.8"
+ sucrase "^3.35.0"
+
+thenify-all@^1.0.0:
+ version "1.6.0"
+ resolved "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz"
+ integrity sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==
+ dependencies:
+ thenify ">= 3.1.0 < 4"
+
+"thenify@>= 3.1.0 < 4":
+ version "3.3.1"
+ resolved "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz"
+ integrity sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==
+ dependencies:
+ any-promise "^1.0.0"
+
+throttle-debounce@^5.0.0, throttle-debounce@^5.0.2:
+ version "5.0.2"
+ resolved "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz"
+ integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==
+
+tinyglobby@^0.2.15:
+ version "0.2.15"
+ resolved "https://r.cnpmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz"
+ integrity sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==
+ dependencies:
+ fdir "^6.5.0"
+ picomatch "^4.0.3"
+
+to-regex-range@^5.0.1:
+ version "5.0.1"
+ resolved "https://r2.cnpmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz"
+ integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==
+ dependencies:
+ is-number "^7.0.0"
+
+toggle-selection@^1.0.6:
+ version "1.0.6"
+ resolved "https://registry.npmmirror.com/toggle-selection/-/toggle-selection-1.0.6.tgz"
+ integrity sha512-BiZS+C1OS8g/q2RRbJmy59xpyghNBqrr6k5L/uKBGRsTfxmu3ffiRnd8mlGPUVayg8pvfi5urfnu8TU7DVOkLQ==
+
+ts-api-utils@^2.1.0:
+ version "2.1.0"
+ resolved "https://r.cnpmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz"
+ integrity sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==
+
+ts-interface-checker@^0.1.9:
+ version "0.1.13"
+ resolved "https://registry.npmmirror.com/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz"
+ integrity sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==
+
+type-check@^0.4.0, type-check@~0.4.0:
+ version "0.4.0"
+ resolved "https://r2.cnpmjs.org/type-check/-/type-check-0.4.0.tgz"
+ integrity sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==
+ dependencies:
+ prelude-ls "^1.2.1"
+
+typescript-eslint@^8.45.0:
+ version "8.46.1"
+ resolved "https://r.cnpmjs.org/typescript-eslint/-/typescript-eslint-8.46.1.tgz"
+ integrity sha512-VHgijW803JafdSsDO8I761r3SHrgk4T00IdyQ+/UsthtgPRsBWQLqoSxOolxTpxRKi1kGXK0bSz4CoAc9ObqJA==
+ dependencies:
+ "@typescript-eslint/eslint-plugin" "8.46.1"
+ "@typescript-eslint/parser" "8.46.1"
+ "@typescript-eslint/typescript-estree" "8.46.1"
+ "@typescript-eslint/utils" "8.46.1"
+
+typescript@~5.9.3:
+ version "5.9.3"
+ resolved "https://r.cnpmjs.org/typescript/-/typescript-5.9.3.tgz"
+ integrity sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==
+
+undici-types@~7.14.0:
+ version "7.14.0"
+ resolved "https://r.cnpmjs.org/undici-types/-/undici-types-7.14.0.tgz"
+ integrity sha512-QQiYxHuyZ9gQUIrmPo3IA+hUl4KYk8uSA7cHrcKd/l3p1OTpZcM0Tbp9x7FAtXdAYhlasd60ncPpgu6ihG6TOA==
+
+uri-js@^4.2.2:
+ version "4.4.1"
+ resolved "https://r2.cnpmjs.org/uri-js/-/uri-js-4.4.1.tgz"
+ integrity sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==
+ dependencies:
+ punycode "^2.1.0"
+
+util-deprecate@^1.0.2:
+ version "1.0.2"
+ resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz"
+ integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
+
+vite@^7.1.7:
+ version "7.1.10"
+ resolved "https://r.cnpmjs.org/vite/-/vite-7.1.10.tgz"
+ integrity sha512-CmuvUBzVJ/e3HGxhg6cYk88NGgTnBoOo7ogtfJJ0fefUWAxN/WDSUa50o+oVBxuIhO8FoEZW0j2eW7sfjs5EtA==
+ dependencies:
+ esbuild "^0.25.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"
+
+which@^2.0.1:
+ version "2.0.2"
+ resolved "https://r2.cnpmjs.org/which/-/which-2.0.2.tgz"
+ integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
+ dependencies:
+ isexe "^2.0.0"
+
+word-wrap@^1.2.5:
+ version "1.2.5"
+ resolved "https://r.cnpmjs.org/word-wrap/-/word-wrap-1.2.5.tgz"
+ integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
+
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+ version "7.0.0"
+ resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz"
+ integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+ dependencies:
+ ansi-styles "^4.0.0"
+ string-width "^4.1.0"
+ strip-ansi "^6.0.0"
+
+wrap-ansi@^8.1.0:
+ version "8.1.0"
+ resolved "https://registry.npmmirror.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz"
+ integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==
+ dependencies:
+ ansi-styles "^6.1.0"
+ string-width "^5.0.1"
+ strip-ansi "^7.0.1"
+
+yaml@^2.3.4:
+ version "2.8.1"
+ resolved "https://registry.npmmirror.com/yaml/-/yaml-2.8.1.tgz"
+ integrity sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==
+
+yocto-queue@^0.1.0:
+ version "0.1.0"
+ resolved "https://r2.cnpmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz"
+ integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==