diff --git a/.env.example b/.env.example
deleted file mode 100644
index 452c28a..0000000
--- a/.env.example
+++ /dev/null
@@ -1,10 +0,0 @@
-# Discourse SSO 配置
-# Discourse 网站地址
-DISCOURSE_URL=https://q58.pro
-
-# SSO 密钥 (必需)
-# 可以使用以下命令生成: openssl rand -hex 32
-DISCOURSE_SSO_SECRET=your_sso_secret_here
-
-# 服务器配置
-PORT=8000
\ No newline at end of file
diff --git a/.github/workflows/docker-build.yml b/.github/workflows/docker-build.yml
new file mode 100644
index 0000000..ed5e88d
--- /dev/null
+++ b/.github/workflows/docker-build.yml
@@ -0,0 +1,59 @@
+name: Docker Build and Push
+
+on:
+ push:
+ branches: [ "main" ]
+ tags: [ 'v*.*.*' ]
+ pull_request:
+ branches: [ "main" ]
+
+env:
+ DOCKER_HUB_USERNAME: ${{ secrets.DOCKER_HUB_USERNAME }}
+ IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/aimodels-prices
+
+jobs:
+ build:
+ runs-on: ubuntu-latest
+
+ steps:
+ - name: Checkout repository
+ uses: actions/checkout@v4
+
+ # 设置 QEMU 以支持多架构构建
+ - name: Set up QEMU
+ uses: docker/setup-qemu-action@v3
+
+ # 设置 Docker Buildx
+ - name: Set up Docker Buildx
+ uses: docker/setup-buildx-action@v3
+
+ # 登录到 Docker Hub
+ - name: Log into Docker Hub
+ if: github.event_name != 'pull_request'
+ uses: docker/login-action@v3
+ with:
+ username: ${{ secrets.DOCKER_HUB_USERNAME }}
+ password: ${{ secrets.DOCKER_HUB_TOKEN }}
+
+ # 提取版本信息
+ - name: Extract version
+ id: version
+ run: |
+ if [[ $GITHUB_REF == refs/tags/* ]]; then
+ echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
+ else
+ echo "VERSION=latest" >> $GITHUB_OUTPUT
+ fi
+
+ # 构建并推送 Docker 镜像
+ - name: Build and push Docker image
+ uses: docker/build-push-action@v5
+ with:
+ context: .
+ push: ${{ github.event_name != 'pull_request' }}
+ platforms: linux/amd64,linux/arm64
+ tags: |
+ ${{ env.IMAGE_NAME }}:${{ steps.version.outputs.VERSION }}
+ ${{ env.IMAGE_NAME }}:latest
+ cache-from: type=gha
+ cache-to: type=gha,mode=max
\ No newline at end of file
diff --git a/.gitignore b/.gitignore
index e83a6d2..d9ee314 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,6 +4,7 @@ kaifa.md
.env
.env.local
.env.*.local
+wrangler.toml
# 系统文件
.DS_Store
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite
new file mode 100644
index 0000000..0b58f0d
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite differ
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite-shm b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite-shm
new file mode 100644
index 0000000..d6fcf24
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite-shm differ
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite-wal b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite-wal
new file mode 100644
index 0000000..cf773dc
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/1fbe96c65917121592d4d49b8308bf00e2e4238540275494cd335eb76f5bfcda.sqlite-wal differ
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/38dd4d5a44c85be39743d2dd03425e2bc1680446bbb634117b50e659261268fc.sqlite b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/38dd4d5a44c85be39743d2dd03425e2bc1680446bbb634117b50e659261268fc.sqlite
new file mode 100644
index 0000000..d1a7064
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/38dd4d5a44c85be39743d2dd03425e2bc1680446bbb634117b50e659261268fc.sqlite differ
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite
new file mode 100644
index 0000000..d1dcec6
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite differ
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite-shm b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite-shm
new file mode 100644
index 0000000..8213601
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite-shm differ
diff --git a/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite-wal b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite-wal
new file mode 100644
index 0000000..a729bfa
Binary files /dev/null and b/.wrangler/state/v3/d1/miniflare-D1DatabaseObject/97ea68c203504371df2eaae8a072fd30e7ff10d9b1da886e5e58eb2fc78cb772.sqlite-wal differ
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..0a8940b
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,58 @@
+# 第一阶段:构建后端
+FROM golang:1.21-alpine AS backend-builder
+
+WORKDIR /app/backend
+
+# 安装依赖
+COPY backend/go.mod backend/go.sum ./
+RUN go mod download
+
+# 复制后端源代码
+COPY backend/ .
+
+# 编译后端
+RUN CGO_ENABLED=0 GOOS=linux go build -o main .
+
+# 第二阶段:构建前端
+FROM node:18-alpine AS frontend-builder
+
+WORKDIR /app/frontend
+
+# 安装依赖
+COPY frontend/package.json frontend/package-lock.json ./
+RUN npm ci
+
+# 复制前端源代码
+COPY frontend/ .
+
+# 构建前端
+RUN npm run build
+
+# 第三阶段:最终镜像
+FROM alpine:3.18
+
+WORKDIR /app
+
+# 安装 nginx
+RUN apk add --no-cache nginx
+
+# 创建数据目录
+RUN mkdir -p /app/data
+
+# 复制后端二进制文件
+COPY --from=backend-builder /app/backend/main ./
+COPY backend/config/nginx.conf /etc/nginx/nginx.conf
+
+# 复制前端构建产物
+COPY --from=frontend-builder /app/frontend/.next/static /app/frontend/static
+COPY --from=frontend-builder /app/frontend/public /app/frontend/public
+COPY --from=frontend-builder /app/frontend/.next/standalone /app/frontend
+
+# 复制启动脚本
+COPY scripts/start.sh ./
+RUN chmod +x start.sh
+
+EXPOSE 80
+
+# 启动服务
+CMD ["./start.sh"]
\ No newline at end of file
diff --git a/backend/config/config.go b/backend/config/config.go
new file mode 100644
index 0000000..d35e6e8
--- /dev/null
+++ b/backend/config/config.go
@@ -0,0 +1,47 @@
+package config
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/joho/godotenv"
+)
+
+type Config struct {
+ DBPath string
+ ServerPort string
+}
+
+func LoadConfig() (*Config, error) {
+ // 确保数据目录存在
+ dbDir := "./data"
+ if err := os.MkdirAll(dbDir, 0755); err != nil {
+ return nil, fmt.Errorf("failed to create data directory: %v", err)
+ }
+
+ // 尝试从 data 目录加载 .env 文件
+ envPath := filepath.Join(dbDir, ".env")
+ if err := godotenv.Load(envPath); err != nil {
+ fmt.Printf("Warning: .env file not found in data directory: %v\n", err)
+ // 如果 data/.env 不存在,尝试加载项目根目录的 .env
+ if err := godotenv.Load(); err != nil {
+ fmt.Printf("Warning: .env file not found in root directory: %v\n", err)
+ }
+ }
+
+ config := &Config{
+ DBPath: filepath.Join(dbDir, "aimodels.db"),
+ ServerPort: getEnv("PORT", "8080"),
+ }
+
+ return config, nil
+}
+
+func getEnv(key, defaultValue string) string {
+ value := os.Getenv(key)
+ if value == "" {
+ return defaultValue
+ }
+ return value
+}
diff --git a/backend/database/db.go b/backend/database/db.go
new file mode 100644
index 0000000..54b19bc
--- /dev/null
+++ b/backend/database/db.go
@@ -0,0 +1,67 @@
+package database
+
+import (
+ "database/sql"
+ "log"
+
+ _ "modernc.org/sqlite"
+
+ "aimodels-prices/models"
+)
+
+// DB 是数据库连接的全局实例
+var DB *sql.DB
+
+// InitDB 初始化数据库连接
+func InitDB(dbPath string) error {
+ var err error
+ DB, err = sql.Open("sqlite", dbPath)
+ if err != nil {
+ return err
+ }
+
+ // 测试连接
+ if err = DB.Ping(); err != nil {
+ return err
+ }
+
+ // 设置连接池参数
+ DB.SetMaxOpenConns(10)
+ DB.SetMaxIdleConns(5)
+
+ // 创建表结构
+ if err = createTables(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+// createTables 创建数据库表
+func createTables() error {
+ // 创建用户表
+ if _, err := DB.Exec(models.CreateUserTableSQL()); err != nil {
+ log.Printf("Failed to create user table: %v", err)
+ return err
+ }
+
+ // 创建会话表
+ if _, err := DB.Exec(models.CreateSessionTableSQL()); err != nil {
+ log.Printf("Failed to create session table: %v", err)
+ return err
+ }
+
+ // 创建供应商表
+ if _, err := DB.Exec(models.CreateProviderTableSQL()); err != nil {
+ log.Printf("Failed to create provider table: %v", err)
+ return err
+ }
+
+ // 创建价格表
+ if _, err := DB.Exec(models.CreatePriceTableSQL()); err != nil {
+ log.Printf("Failed to create price table: %v", err)
+ return err
+ }
+
+ return nil
+}
diff --git a/backend/go.mod b/backend/go.mod
new file mode 100644
index 0000000..c7064ed
--- /dev/null
+++ b/backend/go.mod
@@ -0,0 +1,51 @@
+module aimodels-prices
+
+go 1.21
+
+require (
+ github.com/gin-gonic/gin v1.9.1
+ github.com/joho/godotenv v1.5.1
+ modernc.org/sqlite v1.28.0
+)
+
+require (
+ github.com/bytedance/sonic v1.9.1 // indirect
+ github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/gabriel-vasile/mimetype v1.4.2 // indirect
+ github.com/gin-contrib/sse v0.1.0 // indirect
+ 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.14.0 // indirect
+ github.com/goccy/go-json v0.10.2 // indirect
+ github.com/google/uuid v1.3.0 // indirect
+ github.com/json-iterator/go v1.1.12 // indirect
+ github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
+ github.com/klauspost/cpuid/v2 v2.2.4 // indirect
+ github.com/leodido/go-urn v1.2.4 // indirect
+ github.com/mattn/go-isatty v0.0.19 // indirect
+ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+ github.com/modern-go/reflect2 v1.0.2 // indirect
+ github.com/pelletier/go-toml/v2 v2.0.8 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
+ github.com/ugorji/go/codec v1.2.11 // indirect
+ golang.org/x/arch v0.3.0 // indirect
+ golang.org/x/crypto v0.14.0 // indirect
+ golang.org/x/mod v0.8.0 // indirect
+ golang.org/x/net v0.10.0 // indirect
+ golang.org/x/sys v0.13.0 // indirect
+ golang.org/x/text v0.14.0 // indirect
+ golang.org/x/tools v0.6.0 // indirect
+ google.golang.org/protobuf v1.30.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+ lukechampine.com/uint128 v1.2.0 // indirect
+ modernc.org/cc/v3 v3.40.0 // indirect
+ modernc.org/ccgo/v3 v3.16.13 // indirect
+ modernc.org/libc v1.29.0 // indirect
+ modernc.org/mathutil v1.6.0 // indirect
+ modernc.org/memory v1.7.2 // indirect
+ modernc.org/opt v0.1.3 // indirect
+ modernc.org/strutil v1.1.3 // indirect
+ modernc.org/token v1.0.1 // indirect
+)
diff --git a/backend/go.sum b/backend/go.sum
new file mode 100644
index 0000000..7a1109b
--- /dev/null
+++ b/backend/go.sum
@@ -0,0 +1,134 @@
+github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
+github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s=
+github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
+github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
+github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
+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/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU=
+github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA=
+github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
+github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
+github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
+github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
+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.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js=
+github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU=
+github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
+github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
+github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26 h1:Xim43kblpZXfIBQsbuBVKCudVG457BR2GZFIz3uw3hQ=
+github.com/google/pprof v0.0.0-20221118152302-e6195bd50e26/go.mod h1:dDKJzRmX4S37WGHujM7tX//fmj1uioxKzKxz3lo4HJo=
+github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
+github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
+github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
+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/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
+github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
+github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
+github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
+github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
+github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q=
+github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4=
+github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA=
+github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
+github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
+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/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ=
+github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4=
+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/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+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.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.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY=
+github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+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.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU=
+github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
+golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k=
+golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
+golang.org/x/crypto v0.14.0 h1:wBqGXzWJW6m1XrIKlAH0Hs1JJ7+9KBwnIO8v66Q9cHc=
+golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4=
+golang.org/x/mod v0.8.0 h1:LUYupSeNrTNCGzR/hVBk2NHZO4hXcVaW1k4Qx7rjPx8=
+golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
+golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M=
+golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
+golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o=
+golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
+golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE=
+golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
+golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
+golang.org/x/tools v0.6.0 h1:BOw41kyTf3PuCW1pVQf8+Cyg8pMlkYB1oo9iJ6D/lKM=
+golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng=
+google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+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=
+lukechampine.com/uint128 v1.2.0 h1:mBi/5l91vocEN8otkC5bDLhi2KdCticRiwbdB0O+rjI=
+lukechampine.com/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk=
+modernc.org/cc/v3 v3.40.0 h1:P3g79IUS/93SYhtoeaHW+kRCIrYaxJ27MFPv+7kaTOw=
+modernc.org/cc/v3 v3.40.0/go.mod h1:/bTg4dnWkSXowUO6ssQKnOV0yMVxDYNIsIrzqTFDGH0=
+modernc.org/ccgo/v3 v3.16.13 h1:Mkgdzl46i5F/CNR/Kj80Ri59hC8TKAhZrYSaqvkwzUw=
+modernc.org/ccgo/v3 v3.16.13/go.mod h1:2Quk+5YgpImhPjv2Qsob1DnZ/4som1lJTodubIcoUkY=
+modernc.org/ccorpus v1.11.6 h1:J16RXiiqiCgua6+ZvQot4yUuUy8zxgqbqEEUuGPlISk=
+modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ=
+modernc.org/httpfs v1.0.6 h1:AAgIpFZRXuYnkjftxTAZwMIiwEqAfk8aVB2/oA6nAeM=
+modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM=
+modernc.org/libc v1.29.0 h1:tTFRFq69YKCF2QyGNuRUQxKBm1uZZLubf6Cjh/pVHXs=
+modernc.org/libc v1.29.0/go.mod h1:DaG/4Q3LRRdqpiLyP0C2m1B8ZMGkQ+cCgOIjEtQlYhQ=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.7.2 h1:Klh90S215mmH8c9gO98QxQFsY+W451E8AnzjoE2ee1E=
+modernc.org/memory v1.7.2/go.mod h1:NO4NVCQy0N7ln+T9ngWqOQfi7ley4vpwvARR+Hjw95E=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sqlite v1.28.0 h1:Zx+LyDDmXczNnEQdvPuEfcFVA2ZPyaD7UCZDjef3BHQ=
+modernc.org/sqlite v1.28.0/go.mod h1:Qxpazz0zH8Z1xCFyi5GSL3FzbtZ3fvbjmywNogldEW0=
+modernc.org/strutil v1.1.3 h1:fNMm+oJklMGYfU9Ylcywl0CO5O6nTfaowNsh2wpPjzY=
+modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw=
+modernc.org/tcl v1.15.2 h1:C4ybAYCGJw968e+Me18oW55kD/FexcHbqH2xak1ROSY=
+modernc.org/tcl v1.15.2/go.mod h1:3+k/ZaEbKrC8ePv8zJWPtBSW0V7Gg9g8rkmhI1Kfs3c=
+modernc.org/token v1.0.1 h1:A3qvTqOwexpfZZeyI0FeGPDlSWX5pjZu9hF4lU+EKWg=
+modernc.org/token v1.0.1/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
+modernc.org/z v1.7.3 h1:zDJf6iHjrnB+WRD88stbXokugjyc0/pB91ri1gO6LZY=
+modernc.org/z v1.7.3/go.mod h1:Ipv4tsdxZRbQyLq9Q1M6gdbkxYzdlrciF2Hi/lS7nWE=
+rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
diff --git a/backend/handlers/auth.go b/backend/handlers/auth.go
new file mode 100644
index 0000000..e76919e
--- /dev/null
+++ b/backend/handlers/auth.go
@@ -0,0 +1,291 @@
+package handlers
+
+import (
+ "crypto/hmac"
+ "crypto/rand"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/base64"
+ "encoding/hex"
+ "fmt"
+ "net/http"
+ "net/url"
+ "os"
+ "strings"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "aimodels-prices/models"
+)
+
+func generateSessionID() string {
+ b := make([]byte, 32)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func GetAuthStatus(c *gin.Context) {
+ cookie, err := c.Cookie("session")
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"})
+ return
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+ var session models.Session
+ err = db.QueryRow("SELECT id, user_id, expires_at, created_at, updated_at, deleted_at FROM session WHERE id = ?", cookie).Scan(
+ &session.ID, &session.UserID, &session.ExpiresAt, &session.CreatedAt, &session.UpdatedAt, &session.DeletedAt)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid session"})
+ return
+ }
+
+ if session.ExpiresAt.Before(time.Now()) {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"})
+ return
+ }
+
+ user, err := session.GetUser(db)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
+ return
+ }
+
+ c.Set("user", user)
+ c.JSON(http.StatusOK, gin.H{
+ "user": user,
+ })
+}
+
+func Login(c *gin.Context) {
+ // 开发环境下使用测试账号
+ if gin.Mode() != gin.ReleaseMode {
+ db := c.MustGet("db").(*sql.DB)
+
+ // 创建测试用户(如果不存在)
+ var count int
+ err := db.QueryRow("SELECT COUNT(*) FROM user WHERE username = 'admin'").Scan(&count)
+ if err != nil || count == 0 {
+ _, err = db.Exec("INSERT INTO user (username, email, role) VALUES (?, ?, ?)",
+ "admin", "admin@test.com", "admin")
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create test user"})
+ return
+ }
+ }
+
+ // 获取用户ID
+ var userID uint
+ err = db.QueryRow("SELECT id FROM user WHERE username = 'admin'").Scan(&userID)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
+ return
+ }
+
+ // 创建会话
+ sessionID := generateSessionID()
+ expiresAt := time.Now().Add(24 * time.Hour)
+ _, err = db.Exec("INSERT INTO session (id, user_id, expires_at) VALUES (?, ?, ?)",
+ sessionID, userID, expiresAt)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
+ return
+ }
+
+ // 设置cookie
+ c.SetCookie("session", sessionID, int(24*time.Hour.Seconds()), "/", "", false, true)
+ c.JSON(http.StatusOK, gin.H{"message": "Logged in successfully"})
+ return
+ }
+
+ // 生产环境使用 Discourse SSO
+ discourseURL := os.Getenv("DISCOURSE_URL")
+ if discourseURL == "" {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Discourse URL not configured"})
+ return
+ }
+
+ // 生成随机 nonce
+ nonce := make([]byte, 16)
+ if _, err := rand.Read(nonce); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to generate nonce"})
+ return
+ }
+ nonceStr := hex.EncodeToString(nonce)
+
+ // 构建 payload
+ payload := url.Values{}
+ payload.Set("nonce", nonceStr)
+ payload.Set("return_sso_url", fmt.Sprintf("%s/api/auth/callback", c.Request.Host))
+
+ // Base64 编码
+ payloadStr := base64.StdEncoding.EncodeToString([]byte(payload.Encode()))
+
+ // 计算签名
+ ssoSecret := os.Getenv("DISCOURSE_SSO_SECRET")
+ if ssoSecret == "" {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "SSO secret not configured"})
+ return
+ }
+
+ h := hmac.New(sha256.New, []byte(ssoSecret))
+ h.Write([]byte(payloadStr))
+ sig := hex.EncodeToString(h.Sum(nil))
+
+ // 构建重定向 URL
+ redirectURL := fmt.Sprintf("%s/session/sso_provider?sso=%s&sig=%s",
+ discourseURL, url.QueryEscape(payloadStr), sig)
+
+ c.Redirect(http.StatusTemporaryRedirect, redirectURL)
+}
+
+func Logout(c *gin.Context) {
+ cookie, err := c.Cookie("session")
+ if err == nil {
+ db := c.MustGet("db").(*sql.DB)
+ db.Exec("DELETE FROM session WHERE id = ?", cookie)
+ }
+
+ c.SetCookie("session", "", -1, "/", "", false, true)
+ c.JSON(http.StatusOK, gin.H{"message": "Logged out successfully"})
+}
+
+func GetUser(c *gin.Context) {
+ cookie, err := c.Cookie("session")
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"})
+ return
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+ var session models.Session
+ if err := db.QueryRow("SELECT id, user_id, expires_at, created_at, updated_at, deleted_at FROM session WHERE id = ?", cookie).Scan(
+ &session.ID, &session.UserID, &session.ExpiresAt, &session.CreatedAt, &session.UpdatedAt, &session.DeletedAt); err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid session"})
+ return
+ }
+
+ user, err := session.GetUser(db)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "user": user,
+ })
+}
+
+func AuthCallback(c *gin.Context) {
+ sso := c.Query("sso")
+ sig := c.Query("sig")
+
+ if sso == "" || sig == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Missing parameters"})
+ return
+ }
+
+ // 获取 SSO 密钥
+ ssoSecret := os.Getenv("DISCOURSE_SSO_SECRET")
+ if ssoSecret == "" {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "SSO secret not configured"})
+ return
+ }
+
+ // 验证签名
+ h := hmac.New(sha256.New, []byte(ssoSecret))
+ h.Write([]byte(sso))
+ computedSig := hex.EncodeToString(h.Sum(nil))
+ if computedSig != sig {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid signature"})
+ return
+ }
+
+ // 解码 SSO payload
+ payload, err := base64.StdEncoding.DecodeString(sso)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid SSO payload"})
+ return
+ }
+
+ // 解析 payload
+ values, err := url.ParseQuery(string(payload))
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid payload format"})
+ return
+ }
+
+ // 获取用户信息
+ username := values.Get("username")
+ email := values.Get("email")
+ groups := values.Get("groups")
+ admin := values.Get("admin") // Discourse 管理员标志
+ moderator := values.Get("moderator") // Discourse 版主标志
+ if username == "" || email == "" {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Missing user information"})
+ return
+ }
+
+ // 判断用户角色
+ role := "user"
+ // 如果是管理员、版主或属于 admins 组,都赋予管理权限
+ if admin == "true" || moderator == "true" || (groups != "" && strings.Contains(groups, "admins")) {
+ role = "admin"
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+
+ // 检查用户是否存在
+ var user models.User
+ err = db.QueryRow("SELECT id, username, email, role FROM user WHERE email = ?", email).Scan(
+ &user.ID, &user.Username, &user.Email, &user.Role)
+
+ if err == sql.ErrNoRows {
+ // 创建新用户
+ result, err := db.Exec(`
+ INSERT INTO user (username, email, role)
+ VALUES (?, ?, ?)`,
+ username, email, role)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create user"})
+ return
+ }
+ userID, _ := result.LastInsertId()
+ user = models.User{
+ ID: uint(userID),
+ Username: username,
+ Email: email,
+ Role: role,
+ }
+ } else if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Database error"})
+ return
+ } else {
+ // 更新现有用户的角色(如果需要)
+ if user.Role != role {
+ _, err = db.Exec("UPDATE user SET role = ? WHERE id = ?", role, user.ID)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update user role"})
+ return
+ }
+ user.Role = role
+ }
+ }
+
+ // 创建会话
+ sessionID := generateSessionID()
+ expiresAt := time.Now().Add(24 * time.Hour)
+ _, err = db.Exec("INSERT INTO session (id, user_id, expires_at) VALUES (?, ?, ?)",
+ sessionID, user.ID, expiresAt)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create session"})
+ return
+ }
+
+ // 设置 cookie
+ c.SetCookie("session", sessionID, int(24*time.Hour.Seconds()), "/", "", false, true)
+
+ // 重定向到前端
+ c.Redirect(http.StatusTemporaryRedirect, "/")
+}
diff --git a/backend/handlers/prices.go b/backend/handlers/prices.go
new file mode 100644
index 0000000..cc47db7
--- /dev/null
+++ b/backend/handlers/prices.go
@@ -0,0 +1,274 @@
+package handlers
+
+import (
+ "database/sql"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "aimodels-prices/models"
+)
+
+func GetPrices(c *gin.Context) {
+ db := c.MustGet("db").(*sql.DB)
+ rows, err := db.Query(`
+ SELECT id, model, billing_type, channel_type, currency, input_price, output_price,
+ price_source, status, created_at, updated_at, created_by,
+ temp_model, temp_billing_type, temp_channel_type, temp_currency,
+ temp_input_price, temp_output_price, temp_price_source, updated_by
+ FROM price ORDER BY created_at DESC`)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch prices"})
+ return
+ }
+ defer rows.Close()
+
+ var prices []models.Price
+ for rows.Next() {
+ var price models.Price
+ if err := rows.Scan(
+ &price.ID, &price.Model, &price.BillingType, &price.ChannelType, &price.Currency,
+ &price.InputPrice, &price.OutputPrice, &price.PriceSource, &price.Status,
+ &price.CreatedAt, &price.UpdatedAt, &price.CreatedBy,
+ &price.TempModel, &price.TempBillingType, &price.TempChannelType, &price.TempCurrency,
+ &price.TempInputPrice, &price.TempOutputPrice, &price.TempPriceSource, &price.UpdatedBy); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan price"})
+ return
+ }
+ prices = append(prices, price)
+ }
+
+ c.JSON(http.StatusOK, prices)
+}
+
+func CreatePrice(c *gin.Context) {
+ var price models.Price
+ if err := c.ShouldBindJSON(&price); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 验证供应商ID是否存在
+ db := c.MustGet("db").(*sql.DB)
+ var providerExists bool
+ err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM provider WHERE id = ?)", price.ChannelType).Scan(&providerExists)
+ if err != nil || !providerExists {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
+ return
+ }
+
+ now := time.Now()
+ result, err := db.Exec(`
+ INSERT INTO price (model, billing_type, channel_type, currency, input_price, output_price,
+ price_source, status, created_by, created_at, updated_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, 'pending', ?, ?, ?)`,
+ price.Model, price.BillingType, price.ChannelType, price.Currency,
+ price.InputPrice, price.OutputPrice, price.PriceSource, price.CreatedBy,
+ now, now)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create price"})
+ return
+ }
+
+ id, _ := result.LastInsertId()
+ price.ID = uint(id)
+ price.Status = "pending"
+ price.CreatedAt = now
+ price.UpdatedAt = now
+
+ c.JSON(http.StatusCreated, price)
+}
+
+func UpdatePriceStatus(c *gin.Context) {
+ id := c.Param("id")
+ var input struct {
+ Status string `json:"status" binding:"required,oneof=approved rejected"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+ now := time.Now()
+
+ if input.Status == "approved" {
+ // 如果是批准,将临时字段的值更新到正式字段
+ _, err := db.Exec(`
+ UPDATE price
+ SET model = COALESCE(temp_model, model),
+ billing_type = COALESCE(temp_billing_type, billing_type),
+ channel_type = COALESCE(temp_channel_type, channel_type),
+ currency = COALESCE(temp_currency, currency),
+ input_price = COALESCE(temp_input_price, input_price),
+ output_price = COALESCE(temp_output_price, output_price),
+ price_source = COALESCE(temp_price_source, price_source),
+ status = ?,
+ updated_at = ?,
+ temp_model = NULL,
+ temp_billing_type = NULL,
+ temp_channel_type = NULL,
+ temp_currency = NULL,
+ temp_input_price = NULL,
+ temp_output_price = NULL,
+ temp_price_source = NULL,
+ updated_by = NULL
+ WHERE id = ?`, input.Status, now, id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price status"})
+ return
+ }
+ } else {
+ // 如果是拒绝,清除临时字段
+ _, err := db.Exec(`
+ UPDATE price
+ SET status = ?,
+ updated_at = ?,
+ temp_model = NULL,
+ temp_billing_type = NULL,
+ temp_channel_type = NULL,
+ temp_currency = NULL,
+ temp_input_price = NULL,
+ temp_output_price = NULL,
+ temp_price_source = NULL,
+ updated_by = NULL
+ WHERE id = ?`, input.Status, now, id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price status"})
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "Status updated successfully",
+ "status": input.Status,
+ "updated_at": now,
+ })
+}
+
+// UpdatePrice 更新价格
+func UpdatePrice(c *gin.Context) {
+ id := c.Param("id")
+ var price models.Price
+ if err := c.ShouldBindJSON(&price); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 验证供应商ID是否存在
+ db := c.MustGet("db").(*sql.DB)
+ var providerExists bool
+ err := db.QueryRow("SELECT EXISTS(SELECT 1 FROM provider WHERE id = ?)", price.ChannelType).Scan(&providerExists)
+ if err != nil || !providerExists {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid provider ID"})
+ return
+ }
+
+ // 获取当前用户
+ user, exists := c.Get("user")
+ if !exists {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
+ return
+ }
+ currentUser := user.(*models.User)
+
+ now := time.Now()
+ // 将新的价格信息存储到临时字段
+ _, err = db.Exec(`
+ UPDATE price
+ SET temp_model = ?, temp_billing_type = ?, temp_channel_type = ?, temp_currency = ?,
+ temp_input_price = ?, temp_output_price = ?, temp_price_source = ?,
+ updated_by = ?, updated_at = ?, status = 'pending'
+ WHERE id = ?`,
+ price.Model, price.BillingType, price.ChannelType, price.Currency,
+ price.InputPrice, price.OutputPrice, price.PriceSource,
+ currentUser.Username, now, id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update price"})
+ return
+ }
+
+ // 获取更新后的价格信息
+ err = db.QueryRow(`
+ SELECT id, model, billing_type, channel_type, currency, input_price, output_price,
+ price_source, status, created_at, updated_at, created_by,
+ temp_model, temp_billing_type, temp_channel_type, temp_currency,
+ temp_input_price, temp_output_price, temp_price_source, updated_by
+ FROM price WHERE id = ?`, id).Scan(
+ &price.ID, &price.Model, &price.BillingType, &price.ChannelType, &price.Currency,
+ &price.InputPrice, &price.OutputPrice, &price.PriceSource, &price.Status,
+ &price.CreatedAt, &price.UpdatedAt, &price.CreatedBy,
+ &price.TempModel, &price.TempBillingType, &price.TempChannelType, &price.TempCurrency,
+ &price.TempInputPrice, &price.TempOutputPrice, &price.TempPriceSource, &price.UpdatedBy)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get updated price"})
+ return
+ }
+
+ c.JSON(http.StatusOK, price)
+}
+
+// DeletePrice 删除价格
+func DeletePrice(c *gin.Context) {
+ id := c.Param("id")
+ db := c.MustGet("db").(*sql.DB)
+
+ _, err := db.Exec("DELETE FROM price WHERE id = ?", id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete price"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Price deleted successfully"})
+}
+
+// PriceRate 价格倍率结构
+type PriceRate struct {
+ Model string `json:"model"`
+ Type string `json:"type"`
+ ChannelType uint `json:"channel_type"`
+ Input float64 `json:"input"`
+ Output float64 `json:"output"`
+}
+
+// GetPriceRates 获取价格倍率
+func GetPriceRates(c *gin.Context) {
+ db := c.MustGet("db").(*sql.DB)
+ rows, err := db.Query(`
+ SELECT model, billing_type, channel_type,
+ CASE
+ WHEN currency = 'USD' THEN input_price / 2
+ ELSE input_price / 14
+ END as input_rate,
+ CASE
+ WHEN currency = 'USD' THEN output_price / 2
+ ELSE output_price / 14
+ END as output_rate
+ FROM price
+ WHERE status = 'approved'
+ ORDER BY model, channel_type`)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch price rates"})
+ return
+ }
+ defer rows.Close()
+
+ var rates []PriceRate
+ for rows.Next() {
+ var rate PriceRate
+ if err := rows.Scan(
+ &rate.Model,
+ &rate.Type,
+ &rate.ChannelType,
+ &rate.Input,
+ &rate.Output); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan price rate"})
+ return
+ }
+ rates = append(rates, rate)
+ }
+
+ c.JSON(http.StatusOK, rates)
+}
diff --git a/backend/handlers/providers.go b/backend/handlers/providers.go
new file mode 100644
index 0000000..8b462c6
--- /dev/null
+++ b/backend/handlers/providers.go
@@ -0,0 +1,188 @@
+package handlers
+
+import (
+ "database/sql"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "aimodels-prices/models"
+)
+
+// GetProviders 获取所有供应商
+func GetProviders(c *gin.Context) {
+ db := c.MustGet("db").(*sql.DB)
+ rows, err := db.Query(`
+ SELECT id, name, icon, created_at, updated_at, created_by
+ FROM provider ORDER BY id`)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to fetch providers"})
+ return
+ }
+ defer rows.Close()
+
+ var providers []models.Provider
+ for rows.Next() {
+ var provider models.Provider
+ if err := rows.Scan(
+ &provider.ID, &provider.Name, &provider.Icon,
+ &provider.CreatedAt, &provider.UpdatedAt, &provider.CreatedBy); err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to scan provider"})
+ return
+ }
+ providers = append(providers, provider)
+ }
+
+ c.JSON(http.StatusOK, providers)
+}
+
+// CreateProvider 创建供应商
+func CreateProvider(c *gin.Context) {
+ var provider models.Provider
+ if err := c.ShouldBindJSON(&provider); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // 检查ID是否已存在
+ db := c.MustGet("db").(*sql.DB)
+ var existingID int
+ err := db.QueryRow("SELECT id FROM provider WHERE id = ?", provider.ID).Scan(&existingID)
+ if err != sql.ErrNoRows {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "ID already exists"})
+ return
+ }
+
+ // 获取当前用户
+ user, exists := c.Get("user")
+ if !exists {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "User not found"})
+ return
+ }
+ currentUser := user.(*models.User)
+
+ now := time.Now()
+ _, err = db.Exec(`
+ INSERT INTO provider (id, name, icon, created_at, updated_at, created_by)
+ VALUES (?, ?, ?, ?, ?, ?)`,
+ provider.ID, provider.Name, provider.Icon, now, now, currentUser.Username)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to create provider"})
+ return
+ }
+
+ provider.CreatedAt = now
+ provider.UpdatedAt = now
+ provider.CreatedBy = currentUser.Username
+
+ c.JSON(http.StatusCreated, provider)
+}
+
+// UpdateProvider 更新供应商
+func UpdateProvider(c *gin.Context) {
+ id, err := strconv.ParseUint(c.Param("id"), 10, 32)
+ if err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": "Invalid ID"})
+ return
+ }
+
+ var provider models.Provider
+ if err := c.ShouldBindJSON(&provider); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+ now := time.Now()
+ _, err = db.Exec(`
+ UPDATE provider
+ SET name = ?, icon = ?, updated_at = ?
+ WHERE id = ?`,
+ provider.Name, provider.Icon, now, id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update provider"})
+ return
+ }
+
+ // 获取更新后的供应商信息
+ err = db.QueryRow(`
+ SELECT id, name, icon, created_at, updated_at, created_by
+ FROM provider WHERE id = ?`, id).Scan(
+ &provider.ID, &provider.Name, &provider.Icon,
+ &provider.CreatedAt, &provider.UpdatedAt, &provider.CreatedBy)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get updated provider"})
+ return
+ }
+
+ c.JSON(http.StatusOK, provider)
+}
+
+// UpdateProviderStatus 更新供应商状态
+func UpdateProviderStatus(c *gin.Context) {
+ id := c.Param("id")
+ var input struct {
+ Status string `json:"status" binding:"required,oneof=approved rejected"`
+ }
+
+ if err := c.ShouldBindJSON(&input); err != nil {
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+ now := time.Now()
+
+ if input.Status == "approved" {
+ // 如果是批准,将临时字段的值更新到正式字段
+ _, err := db.Exec(`
+ UPDATE provider
+ SET name = COALESCE(temp_name, name),
+ icon = COALESCE(temp_icon, icon),
+ status = ?,
+ updated_at = ?,
+ temp_name = NULL,
+ temp_icon = NULL,
+ updated_by = NULL
+ WHERE id = ?`, input.Status, now, id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update provider status"})
+ return
+ }
+ } else {
+ // 如果是拒绝,清除临时字段
+ _, err := db.Exec(`
+ UPDATE provider
+ SET status = ?,
+ updated_at = ?,
+ temp_name = NULL,
+ temp_icon = NULL,
+ updated_by = NULL
+ WHERE id = ?`, input.Status, now, id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to update provider status"})
+ return
+ }
+ }
+
+ c.JSON(http.StatusOK, gin.H{
+ "message": "Status updated successfully",
+ "status": input.Status,
+ "updated_at": now,
+ })
+}
+
+// DeleteProvider 删除供应商
+func DeleteProvider(c *gin.Context) {
+ id := c.Param("id")
+ db := c.MustGet("db").(*sql.DB)
+ _, err := db.Exec("DELETE FROM provider WHERE id = ?", id)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to delete provider"})
+ return
+ }
+
+ c.JSON(http.StatusOK, gin.H{"message": "Provider deleted successfully"})
+}
diff --git a/backend/main.go b/backend/main.go
new file mode 100644
index 0000000..c75bc93
--- /dev/null
+++ b/backend/main.go
@@ -0,0 +1,98 @@
+package main
+
+import (
+ "fmt"
+ "log"
+
+ "github.com/gin-gonic/gin"
+
+ "aimodels-prices/config"
+ "aimodels-prices/database"
+ "aimodels-prices/handlers"
+ "aimodels-prices/middleware"
+)
+
+func main() {
+ // 加载配置
+ cfg, err := config.LoadConfig()
+ if err != nil {
+ log.Fatalf("Failed to load config: %v", err)
+ }
+
+ // 初始化数据库
+ if err := database.InitDB(cfg.DBPath); err != nil {
+ log.Fatalf("Failed to initialize database: %v", err)
+ }
+ defer database.DB.Close()
+
+ // 设置gin模式
+ if gin.Mode() == gin.ReleaseMode {
+ gin.SetMode(gin.ReleaseMode)
+ }
+
+ r := gin.Default()
+
+ // 注入数据库
+ r.Use(func(c *gin.Context) {
+ c.Set("db", database.DB)
+ c.Next()
+ })
+
+ // CORS中间件
+ r.Use(func(c *gin.Context) {
+ origin := c.Request.Header.Get("Origin")
+ if origin != "" {
+ c.Writer.Header().Set("Access-Control-Allow-Origin", origin)
+ c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
+ c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
+ c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
+ }
+
+ if c.Request.Method == "OPTIONS" {
+ c.AbortWithStatus(204)
+ return
+ }
+
+ c.Next()
+ })
+
+ // API路由组
+ api := r.Group("/api")
+ {
+ // 价格相关路由
+ prices := api.Group("/prices")
+ {
+ prices.GET("", handlers.GetPrices)
+ prices.GET("/rates", handlers.GetPriceRates)
+ prices.POST("", middleware.AuthRequired(), handlers.CreatePrice)
+ prices.PUT("/:id", middleware.AuthRequired(), handlers.UpdatePrice)
+ prices.DELETE("/:id", middleware.AuthRequired(), handlers.DeletePrice)
+ prices.PUT("/:id/status", middleware.AuthRequired(), middleware.AdminRequired(), handlers.UpdatePriceStatus)
+ }
+
+ // 供应商相关路由
+ providers := api.Group("/providers")
+ {
+ providers.GET("", handlers.GetProviders)
+ providers.POST("", middleware.AuthRequired(), handlers.CreateProvider)
+ providers.PUT("/:id", middleware.AuthRequired(), middleware.AdminRequired(), handlers.UpdateProvider)
+ providers.DELETE("/:id", middleware.AuthRequired(), middleware.AdminRequired(), handlers.DeleteProvider)
+ }
+
+ // 认证相关路由
+ auth := api.Group("/auth")
+ {
+ auth.GET("/status", handlers.GetAuthStatus)
+ auth.POST("/login", handlers.Login)
+ auth.POST("/logout", handlers.Logout)
+ auth.GET("/user", handlers.GetUser)
+ auth.GET("/callback", handlers.AuthCallback)
+ }
+ }
+
+ // 启动服务器
+ addr := fmt.Sprintf(":%s", cfg.ServerPort)
+ if err := r.Run(addr); err != nil {
+ log.Fatalf("Failed to start server: %v", err)
+ }
+}
diff --git a/backend/middleware/auth.go b/backend/middleware/auth.go
new file mode 100644
index 0000000..b5fc917
--- /dev/null
+++ b/backend/middleware/auth.go
@@ -0,0 +1,97 @@
+package middleware
+
+import (
+ "database/sql"
+ "net/http"
+ "time"
+
+ "github.com/gin-gonic/gin"
+
+ "aimodels-prices/models"
+)
+
+func AuthRequired() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ cookie, err := c.Cookie("session")
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"})
+ c.Abort()
+ return
+ }
+
+ db := c.MustGet("db").(*sql.DB)
+ var session models.Session
+ err = db.QueryRow("SELECT id, user_id, expires_at, created_at, updated_at, deleted_at FROM session WHERE id = ?", cookie).Scan(
+ &session.ID, &session.UserID, &session.ExpiresAt, &session.CreatedAt, &session.UpdatedAt, &session.DeletedAt)
+ if err != nil {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid session"})
+ c.Abort()
+ return
+ }
+
+ if session.ExpiresAt.Before(time.Now()) {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Session expired"})
+ c.Abort()
+ return
+ }
+
+ user, err := session.GetUser(db)
+ if err != nil {
+ c.JSON(http.StatusInternalServerError, gin.H{"error": "Failed to get user"})
+ c.Abort()
+ return
+ }
+
+ c.Set("user", user)
+ c.Next()
+ }
+}
+
+func AdminRequired() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ user, exists := c.Get("user")
+ if !exists {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Not logged in"})
+ c.Abort()
+ return
+ }
+
+ if u, ok := user.(*models.User); !ok || u.Role != "admin" {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
+ c.Abort()
+ return
+ }
+
+ c.Next()
+ }
+}
+
+func RequireAuth() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ _, exists := c.Get("user")
+ if !exists {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ c.Abort()
+ return
+ }
+ c.Next()
+ }
+}
+
+func RequireAdmin() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ user, exists := c.Get("user")
+ if !exists {
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "Authentication required"})
+ c.Abort()
+ return
+ }
+
+ if u, ok := user.(*models.User); !ok || u.Role != "admin" {
+ c.JSON(http.StatusForbidden, gin.H{"error": "Admin access required"})
+ c.Abort()
+ return
+ }
+ c.Next()
+ }
+}
diff --git a/backend/middleware/db.go b/backend/middleware/db.go
new file mode 100644
index 0000000..10fad7f
--- /dev/null
+++ b/backend/middleware/db.go
@@ -0,0 +1,14 @@
+package middleware
+
+import (
+ "aimodels-prices/database"
+
+ "github.com/gin-gonic/gin"
+)
+
+func Database() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ c.Set("db", database.DB)
+ c.Next()
+ }
+}
diff --git a/backend/models/price.go b/backend/models/price.go
new file mode 100644
index 0000000..8e46e61
--- /dev/null
+++ b/backend/models/price.go
@@ -0,0 +1,57 @@
+package models
+
+import (
+ "time"
+)
+
+type Price struct {
+ ID uint `json:"id"`
+ Model string `json:"model"`
+ BillingType string `json:"billing_type"` // tokens or times
+ ChannelType string `json:"channel_type"`
+ Currency string `json:"currency"` // USD or CNY
+ InputPrice float64 `json:"input_price"`
+ OutputPrice float64 `json:"output_price"`
+ PriceSource string `json:"price_source"`
+ Status string `json:"status"` // pending, approved, rejected
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ CreatedBy string `json:"created_by"`
+ // 临时字段,用于存储待审核的更新
+ TempModel *string `json:"temp_model,omitempty"`
+ TempBillingType *string `json:"temp_billing_type,omitempty"`
+ TempChannelType *string `json:"temp_channel_type,omitempty"`
+ TempCurrency *string `json:"temp_currency,omitempty"`
+ TempInputPrice *float64 `json:"temp_input_price,omitempty"`
+ TempOutputPrice *float64 `json:"temp_output_price,omitempty"`
+ TempPriceSource *string `json:"temp_price_source,omitempty"`
+ UpdatedBy *string `json:"updated_by,omitempty"`
+}
+
+// CreatePriceTableSQL 返回创建价格表的 SQL
+func CreatePriceTableSQL() string {
+ return `
+ CREATE TABLE IF NOT EXISTS price (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ model TEXT NOT NULL,
+ billing_type TEXT NOT NULL,
+ channel_type TEXT NOT NULL,
+ currency TEXT NOT NULL,
+ input_price REAL NOT NULL,
+ output_price REAL NOT NULL,
+ price_source TEXT NOT NULL,
+ status TEXT NOT NULL DEFAULT 'pending',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ created_by TEXT NOT NULL,
+ temp_model TEXT,
+ temp_billing_type TEXT,
+ temp_channel_type TEXT,
+ temp_currency TEXT,
+ temp_input_price REAL,
+ temp_output_price REAL,
+ temp_price_source TEXT,
+ updated_by TEXT,
+ FOREIGN KEY (channel_type) REFERENCES provider(id)
+ )`
+}
diff --git a/backend/models/provider.go b/backend/models/provider.go
new file mode 100644
index 0000000..93b29b7
--- /dev/null
+++ b/backend/models/provider.go
@@ -0,0 +1,25 @@
+package models
+
+import "time"
+
+type Provider struct {
+ ID uint `json:"id"`
+ Name string `json:"name"`
+ Icon string `json:"icon"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ CreatedBy string `json:"created_by"`
+}
+
+// CreateProviderTableSQL 返回创建供应商表的 SQL
+func CreateProviderTableSQL() string {
+ return `
+ CREATE TABLE IF NOT EXISTS provider (
+ id INTEGER PRIMARY KEY,
+ name TEXT NOT NULL,
+ icon TEXT,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ created_by TEXT NOT NULL
+ )`
+}
diff --git a/backend/models/user.go b/backend/models/user.go
new file mode 100644
index 0000000..7be300b
--- /dev/null
+++ b/backend/models/user.go
@@ -0,0 +1,64 @@
+package models
+
+import (
+ "database/sql"
+ "time"
+)
+
+type User struct {
+ ID uint `json:"id"`
+ Username string `json:"username"`
+ Email string `json:"email"`
+ Role string `json:"role"` // admin or user
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ DeletedAt *time.Time `json:"deleted_at,omitempty"`
+}
+
+type Session struct {
+ ID string `json:"id"`
+ UserID uint `json:"user_id"`
+ ExpiresAt time.Time `json:"expires_at"`
+ CreatedAt time.Time `json:"created_at"`
+ UpdatedAt time.Time `json:"updated_at"`
+ DeletedAt *time.Time `json:"deleted_at,omitempty"`
+}
+
+// CreateTableSQL 返回创建用户表的 SQL
+func CreateUserTableSQL() string {
+ return `
+ CREATE TABLE IF NOT EXISTS user (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ email TEXT UNIQUE NOT NULL,
+ role TEXT NOT NULL DEFAULT 'user',
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ deleted_at DATETIME
+ )`
+}
+
+// CreateSessionTableSQL 返回创建会话表的 SQL
+func CreateSessionTableSQL() string {
+ return `
+ CREATE TABLE IF NOT EXISTS session (
+ id TEXT PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ deleted_at DATETIME,
+ FOREIGN KEY (user_id) REFERENCES user(id)
+ )`
+}
+
+// GetUser 获取会话关联的用户
+func (s *Session) GetUser(db *sql.DB) (*User, error) {
+ var user User
+ err := db.QueryRow("SELECT id, username, email, role, created_at, updated_at, deleted_at FROM user WHERE id = ?", s.UserID).Scan(
+ &user.ID, &user.Username, &user.Email, &user.Role, &user.CreatedAt, &user.UpdatedAt, &user.DeletedAt)
+ if err != nil {
+ return nil, err
+ }
+ return &user, nil
+}
diff --git a/backend/router/router.go b/backend/router/router.go
new file mode 100644
index 0000000..23f8654
--- /dev/null
+++ b/backend/router/router.go
@@ -0,0 +1,37 @@
+package router
+
+import (
+ "github.com/gin-gonic/gin"
+
+ "aimodels-prices/handlers"
+ "aimodels-prices/middleware"
+)
+
+// SetupRouter 设置路由
+func SetupRouter() *gin.Engine {
+ r := gin.Default()
+
+ // 添加数据库中间件
+ r.Use(middleware.Database())
+
+ // 认证相关路由
+ auth := r.Group("/auth")
+ {
+ auth.GET("/status", handlers.GetAuthStatus)
+ auth.POST("/login", handlers.Login)
+ auth.POST("/logout", handlers.Logout)
+ }
+
+ // 供应商相关路由
+ providers := r.Group("/providers")
+ {
+ providers.GET("", handlers.GetProviders)
+ providers.Use(middleware.RequireAuth())
+ providers.Use(middleware.RequireAdmin())
+ providers.POST("", handlers.CreateProvider)
+ providers.PUT("/:id", handlers.UpdateProvider)
+ providers.DELETE("/:id", handlers.DeleteProvider)
+ }
+
+ return r
+}
diff --git a/deno.json b/deno.json
deleted file mode 100644
index e313f2a..0000000
--- a/deno.json
+++ /dev/null
@@ -1,11 +0,0 @@
-{
- "compilerOptions": {
- "allowJs": true,
- "lib": ["dom", "dom.iterable", "dom.asynciterable", "deno.ns"],
- "strict": true
- },
- "importMap": "./import_map.json",
- "tasks": {
- "start": "deno run --allow-net --allow-read --allow-env main.ts"
- }
-}
\ No newline at end of file
diff --git a/deno.jsonc b/deno.jsonc
deleted file mode 100644
index 29c06ef..0000000
--- a/deno.jsonc
+++ /dev/null
@@ -1,13 +0,0 @@
-{
- "compilerOptions": {
- "allowJs": true,
- "lib": ["dom", "dom.iterable", "deno.ns"],
- "strict": true,
- "types": ["https://deno.land/x/types/deno.ns.d.ts"]
- },
- "importMap": "import_map.json",
- "tasks": {
- "start": "deno run --allow-net --allow-read --allow-env main.ts",
- "dev": "deno run --watch --allow-net --allow-read --allow-env main.ts"
- }
-}
\ No newline at end of file
diff --git a/deps.ts b/deps.ts
deleted file mode 100644
index 6a7b5bb..0000000
--- a/deps.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-export { serve } from "https://deno.land/std@0.208.0/http/server.ts";
-export { crypto } from "https://deno.land/std@0.208.0/crypto/mod.ts";
-export { decode as base64Decode } from "https://deno.land/std@0.208.0/encoding/base64.ts";
\ No newline at end of file
diff --git a/favicon.ico b/favicon.ico
new file mode 100644
index 0000000..3caead1
Binary files /dev/null and b/favicon.ico differ
diff --git a/frontend/index.html b/frontend/index.html
new file mode 100644
index 0000000..b4b954d
--- /dev/null
+++ b/frontend/index.html
@@ -0,0 +1,26 @@
+
+
+
+
+
+
+
+
+
+
+ AI模型价格
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/package-lock.json b/frontend/package-lock.json
new file mode 100644
index 0000000..c9eb3ce
--- /dev/null
+++ b/frontend/package-lock.json
@@ -0,0 +1,1482 @@
+{
+ "name": "aimodels-prices-frontend",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "aimodels-prices-frontend",
+ "version": "0.1.0",
+ "dependencies": {
+ "axios": "^1.6.7",
+ "element-plus": "^2.5.3",
+ "vue": "^3.4.15",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.3",
+ "vite": "^5.0.12"
+ }
+ },
+ "node_modules/@babel/helper-string-parser": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz",
+ "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/helper-validator-identifier": {
+ "version": "7.25.9",
+ "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz",
+ "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@babel/parser": {
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz",
+ "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/types": "^7.26.7"
+ },
+ "bin": {
+ "parser": "bin/babel-parser.js"
+ },
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@babel/types": {
+ "version": "7.26.7",
+ "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz",
+ "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/helper-string-parser": "^7.25.9",
+ "@babel/helper-validator-identifier": "^7.25.9"
+ },
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@ctrl/tinycolor": {
+ "version": "3.6.1",
+ "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
+ "integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/@element-plus/icons-vue": {
+ "version": "2.3.1",
+ "resolved": "https://registry.npmjs.org/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
+ "integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/@esbuild/aix-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "aix"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz",
+ "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz",
+ "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/android-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz",
+ "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz",
+ "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/darwin-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz",
+ "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz",
+ "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/freebsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz",
+ "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz",
+ "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz",
+ "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz",
+ "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-loong64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz",
+ "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-mips64el": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz",
+ "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==",
+ "cpu": [
+ "mips64el"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-ppc64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz",
+ "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-riscv64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz",
+ "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-s390x": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz",
+ "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/linux-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz",
+ "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/netbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "netbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/openbsd-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz",
+ "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openbsd"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/sunos-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz",
+ "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "sunos"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-arm64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz",
+ "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-ia32": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz",
+ "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@esbuild/win32-x64": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz",
+ "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/@floating-ui/core": {
+ "version": "1.6.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
+ "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/dom": {
+ "version": "1.6.13",
+ "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz",
+ "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==",
+ "license": "MIT",
+ "dependencies": {
+ "@floating-ui/core": "^1.6.0",
+ "@floating-ui/utils": "^0.2.9"
+ }
+ },
+ "node_modules/@floating-ui/utils": {
+ "version": "0.2.9",
+ "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
+ "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz",
+ "integrity": "sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==",
+ "license": "MIT"
+ },
+ "node_modules/@popperjs/core": {
+ "name": "@sxzz/popperjs-es",
+ "version": "2.11.7",
+ "resolved": "https://registry.npmjs.org/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
+ "integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
+ "license": "MIT",
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/popperjs"
+ }
+ },
+ "node_modules/@rollup/rollup-android-arm-eabi": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.34.6.tgz",
+ "integrity": "sha512-+GcCXtOQoWuC7hhX1P00LqjjIiS/iOouHXhMdiDSnq/1DGTox4SpUvO52Xm+div6+106r+TcvOeo/cxvyEyTgg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-android-arm64": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.34.6.tgz",
+ "integrity": "sha512-E8+2qCIjciYUnCa1AiVF1BkRgqIGW9KzJeesQqVfyRITGQN+dFuoivO0hnro1DjT74wXLRZ7QF8MIbz+luGaJA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-arm64": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.34.6.tgz",
+ "integrity": "sha512-z9Ib+OzqN3DZEjX7PDQMHEhtF+t6Mi2z/ueChQPLS/qUMKY7Ybn5A2ggFoKRNRh1q1T03YTQfBTQCJZiepESAg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-darwin-x64": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.34.6.tgz",
+ "integrity": "sha512-PShKVY4u0FDAR7jskyFIYVyHEPCPnIQY8s5OcXkdU8mz3Y7eXDJPdyM/ZWjkYdR2m0izD9HHWA8sGcXn+Qrsyg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-arm64": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.34.6.tgz",
+ "integrity": "sha512-YSwyOqlDAdKqs0iKuqvRHLN4SrD2TiswfoLfvYXseKbL47ht1grQpq46MSiQAx6rQEN8o8URtpXARCpqabqxGQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-freebsd-x64": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.34.6.tgz",
+ "integrity": "sha512-HEP4CgPAY1RxXwwL5sPFv6BBM3tVeLnshF03HMhJYCNc6kvSqBgTMmsEjb72RkZBAWIqiPUyF1JpEBv5XT9wKQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.34.6.tgz",
+ "integrity": "sha512-88fSzjC5xeH9S2Vg3rPgXJULkHcLYMkh8faix8DX4h4TIAL65ekwuQMA/g2CXq8W+NJC43V6fUpYZNjaX3+IIg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm-musleabihf": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.34.6.tgz",
+ "integrity": "sha512-wM4ztnutBqYFyvNeR7Av+reWI/enK9tDOTKNF+6Kk2Q96k9bwhDDOlnCUNRPvromlVXo04riSliMBs/Z7RteEg==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-gnu": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.34.6.tgz",
+ "integrity": "sha512-9RyprECbRa9zEjXLtvvshhw4CMrRa3K+0wcp3KME0zmBe1ILmvcVHnypZ/aIDXpRyfhSYSuN4EPdCCj5Du8FIA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-arm64-musl": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.34.6.tgz",
+ "integrity": "sha512-qTmklhCTyaJSB05S+iSovfo++EwnIEZxHkzv5dep4qoszUMX5Ca4WM4zAVUMbfdviLgCSQOu5oU8YoGk1s6M9Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-loongarch64-gnu": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.34.6.tgz",
+ "integrity": "sha512-4Qmkaps9yqmpjY5pvpkfOerYgKNUGzQpFxV6rnS7c/JfYbDSU0y6WpbbredB5cCpLFGJEqYX40WUmxMkwhWCjw==",
+ "cpu": [
+ "loong64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-powerpc64le-gnu": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.34.6.tgz",
+ "integrity": "sha512-Zsrtux3PuaxuBTX/zHdLaFmcofWGzaWW1scwLU3ZbW/X+hSsFbz9wDIp6XvnT7pzYRl9MezWqEqKy7ssmDEnuQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-riscv64-gnu": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.34.6.tgz",
+ "integrity": "sha512-aK+Zp+CRM55iPrlyKiU3/zyhgzWBxLVrw2mwiQSYJRobCURb781+XstzvA8Gkjg/hbdQFuDw44aUOxVQFycrAg==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-s390x-gnu": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.34.6.tgz",
+ "integrity": "sha512-WoKLVrY9ogmaYPXwTH326+ErlCIgMmsoRSx6bO+l68YgJnlOXhygDYSZe/qbUJCSiCiZAQ+tKm88NcWuUXqOzw==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-gnu": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.34.6.tgz",
+ "integrity": "sha512-Sht4aFvmA4ToHd2vFzwMFaQCiYm2lDFho5rPcvPBT5pCdC+GwHG6CMch4GQfmWTQ1SwRKS0dhDYb54khSrjDWw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-linux-x64-musl": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.34.6.tgz",
+ "integrity": "sha512-zmmpOQh8vXc2QITsnCiODCDGXFC8LMi64+/oPpPx5qz3pqv0s6x46ps4xoycfUiVZps5PFn1gksZzo4RGTKT+A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-arm64-msvc": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.34.6.tgz",
+ "integrity": "sha512-3/q1qUsO/tLqGBaD4uXsB6coVGB3usxw3qyeVb59aArCgedSF66MPdgRStUd7vbZOsko/CgVaY5fo2vkvPLWiA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-ia32-msvc": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.34.6.tgz",
+ "integrity": "sha512-oLHxuyywc6efdKVTxvc0135zPrRdtYVjtVD5GUm55I3ODxhU/PwkQFD97z16Xzxa1Fz0AEe4W/2hzRtd+IfpOA==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@rollup/rollup-win32-x64-msvc": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.34.6.tgz",
+ "integrity": "sha512-0PVwmgzZ8+TZ9oGBmdZoQVXflbvuwzN/HRclujpl4N/q3i+y0lqLw8n1bXA8ru3sApDjlmONaNAuYr38y1Kr9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ]
+ },
+ "node_modules/@types/estree": {
+ "version": "1.0.6",
+ "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
+ "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash": {
+ "version": "4.17.15",
+ "resolved": "https://registry.npmjs.org/@types/lodash/-/lodash-4.17.15.tgz",
+ "integrity": "sha512-w/P33JFeySuhN6JLkysYUK2gEmy9kHHFN7E8ro0tkfmlDOgxBDzWEZ/J8cWA+fHqFevpswDTFZnDx+R9lbL6xw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/lodash-es": {
+ "version": "4.17.12",
+ "resolved": "https://registry.npmjs.org/@types/lodash-es/-/lodash-es-4.17.12.tgz",
+ "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/lodash": "*"
+ }
+ },
+ "node_modules/@types/web-bluetooth": {
+ "version": "0.0.16",
+ "resolved": "https://registry.npmjs.org/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
+ "integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vitejs/plugin-vue": {
+ "version": "5.2.1",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-vue/-/plugin-vue-5.2.1.tgz",
+ "integrity": "sha512-cxh314tzaWwOLqVes2gnnCtvBDcM1UMdn+iFR+UjAn411dPT3tOmqrJjbMd7koZpMAmBM/GqeV4n9ge7JSiJJQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "peerDependencies": {
+ "vite": "^5.0.0 || ^6.0.0",
+ "vue": "^3.2.25"
+ }
+ },
+ "node_modules/@vue/compiler-core": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-core/-/compiler-core-3.5.13.tgz",
+ "integrity": "sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.3",
+ "@vue/shared": "3.5.13",
+ "entities": "^4.5.0",
+ "estree-walker": "^2.0.2",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/compiler-dom": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz",
+ "integrity": "sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-core": "3.5.13",
+ "@vue/shared": "3.5.13"
+ }
+ },
+ "node_modules/@vue/compiler-sfc": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz",
+ "integrity": "sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/parser": "^7.25.3",
+ "@vue/compiler-core": "3.5.13",
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/compiler-ssr": "3.5.13",
+ "@vue/shared": "3.5.13",
+ "estree-walker": "^2.0.2",
+ "magic-string": "^0.30.11",
+ "postcss": "^8.4.48",
+ "source-map-js": "^1.2.0"
+ }
+ },
+ "node_modules/@vue/compiler-ssr": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz",
+ "integrity": "sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/shared": "3.5.13"
+ }
+ },
+ "node_modules/@vue/devtools-api": {
+ "version": "6.6.4",
+ "resolved": "https://registry.npmjs.org/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
+ "integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
+ "license": "MIT"
+ },
+ "node_modules/@vue/reactivity": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/reactivity/-/reactivity-3.5.13.tgz",
+ "integrity": "sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/shared": "3.5.13"
+ }
+ },
+ "node_modules/@vue/runtime-core": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-core/-/runtime-core-3.5.13.tgz",
+ "integrity": "sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.13",
+ "@vue/shared": "3.5.13"
+ }
+ },
+ "node_modules/@vue/runtime-dom": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz",
+ "integrity": "sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/reactivity": "3.5.13",
+ "@vue/runtime-core": "3.5.13",
+ "@vue/shared": "3.5.13",
+ "csstype": "^3.1.3"
+ }
+ },
+ "node_modules/@vue/server-renderer": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/server-renderer/-/server-renderer-3.5.13.tgz",
+ "integrity": "sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-ssr": "3.5.13",
+ "@vue/shared": "3.5.13"
+ },
+ "peerDependencies": {
+ "vue": "3.5.13"
+ }
+ },
+ "node_modules/@vue/shared": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/@vue/shared/-/shared-3.5.13.tgz",
+ "integrity": "sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ==",
+ "license": "MIT"
+ },
+ "node_modules/@vueuse/core": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/core/-/core-9.13.0.tgz",
+ "integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/web-bluetooth": "^0.0.16",
+ "@vueuse/metadata": "9.13.0",
+ "@vueuse/shared": "9.13.0",
+ "vue-demi": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/core/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@vueuse/metadata": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/metadata/-/metadata-9.13.0.tgz",
+ "integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared": {
+ "version": "9.13.0",
+ "resolved": "https://registry.npmjs.org/@vueuse/shared/-/shared-9.13.0.tgz",
+ "integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
+ "license": "MIT",
+ "dependencies": {
+ "vue-demi": "*"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ }
+ },
+ "node_modules/@vueuse/shared/node_modules/vue-demi": {
+ "version": "0.14.10",
+ "resolved": "https://registry.npmjs.org/vue-demi/-/vue-demi-0.14.10.tgz",
+ "integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "vue-demi-fix": "bin/vue-demi-fix.js",
+ "vue-demi-switch": "bin/vue-demi-switch.js"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/antfu"
+ },
+ "peerDependencies": {
+ "@vue/composition-api": "^1.0.0-rc.1",
+ "vue": "^3.0.0-0 || ^2.6.0"
+ },
+ "peerDependenciesMeta": {
+ "@vue/composition-api": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/async-validator": {
+ "version": "4.2.5",
+ "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
+ "integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
+ "license": "MIT"
+ },
+ "node_modules/asynckit": {
+ "version": "0.4.0",
+ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
+ "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
+ "license": "MIT"
+ },
+ "node_modules/axios": {
+ "version": "1.7.9",
+ "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz",
+ "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==",
+ "license": "MIT",
+ "dependencies": {
+ "follow-redirects": "^1.15.6",
+ "form-data": "^4.0.0",
+ "proxy-from-env": "^1.1.0"
+ }
+ },
+ "node_modules/combined-stream": {
+ "version": "1.0.8",
+ "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
+ "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
+ "license": "MIT",
+ "dependencies": {
+ "delayed-stream": "~1.0.0"
+ },
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.1.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz",
+ "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
+ "license": "MIT"
+ },
+ "node_modules/dayjs": {
+ "version": "1.11.13",
+ "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.13.tgz",
+ "integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
+ "license": "MIT"
+ },
+ "node_modules/delayed-stream": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
+ "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/element-plus": {
+ "version": "2.9.4",
+ "resolved": "https://registry.npmjs.org/element-plus/-/element-plus-2.9.4.tgz",
+ "integrity": "sha512-sGnW0wd9zf6lEGixXV2gfwx3X6VTMkP52qTkX7zbURJ2oariyslrKTBh2txt1sdn1pUvj2l0KY3OfSXoZGmDOw==",
+ "license": "MIT",
+ "dependencies": {
+ "@ctrl/tinycolor": "^3.4.1",
+ "@element-plus/icons-vue": "^2.3.1",
+ "@floating-ui/dom": "^1.0.1",
+ "@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
+ "@types/lodash": "^4.14.182",
+ "@types/lodash-es": "^4.17.6",
+ "@vueuse/core": "^9.1.0",
+ "async-validator": "^4.2.5",
+ "dayjs": "^1.11.13",
+ "escape-html": "^1.0.3",
+ "lodash": "^4.17.21",
+ "lodash-es": "^4.17.21",
+ "lodash-unified": "^1.0.2",
+ "memoize-one": "^6.0.0",
+ "normalize-wheel-es": "^1.2.0"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ },
+ "node_modules/entities": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz",
+ "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/esbuild": {
+ "version": "0.21.5",
+ "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz",
+ "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "bin": {
+ "esbuild": "bin/esbuild"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "optionalDependencies": {
+ "@esbuild/aix-ppc64": "0.21.5",
+ "@esbuild/android-arm": "0.21.5",
+ "@esbuild/android-arm64": "0.21.5",
+ "@esbuild/android-x64": "0.21.5",
+ "@esbuild/darwin-arm64": "0.21.5",
+ "@esbuild/darwin-x64": "0.21.5",
+ "@esbuild/freebsd-arm64": "0.21.5",
+ "@esbuild/freebsd-x64": "0.21.5",
+ "@esbuild/linux-arm": "0.21.5",
+ "@esbuild/linux-arm64": "0.21.5",
+ "@esbuild/linux-ia32": "0.21.5",
+ "@esbuild/linux-loong64": "0.21.5",
+ "@esbuild/linux-mips64el": "0.21.5",
+ "@esbuild/linux-ppc64": "0.21.5",
+ "@esbuild/linux-riscv64": "0.21.5",
+ "@esbuild/linux-s390x": "0.21.5",
+ "@esbuild/linux-x64": "0.21.5",
+ "@esbuild/netbsd-x64": "0.21.5",
+ "@esbuild/openbsd-x64": "0.21.5",
+ "@esbuild/sunos-x64": "0.21.5",
+ "@esbuild/win32-arm64": "0.21.5",
+ "@esbuild/win32-ia32": "0.21.5",
+ "@esbuild/win32-x64": "0.21.5"
+ }
+ },
+ "node_modules/escape-html": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
+ "license": "MIT"
+ },
+ "node_modules/estree-walker": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz",
+ "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
+ "license": "MIT"
+ },
+ "node_modules/follow-redirects": {
+ "version": "1.15.9",
+ "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz",
+ "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==",
+ "funding": [
+ {
+ "type": "individual",
+ "url": "https://github.com/sponsors/RubenVerborgh"
+ }
+ ],
+ "license": "MIT",
+ "engines": {
+ "node": ">=4.0"
+ },
+ "peerDependenciesMeta": {
+ "debug": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/form-data": {
+ "version": "4.0.1",
+ "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.1.tgz",
+ "integrity": "sha512-tzN8e4TX8+kkxGPK8D5u0FNmjPUjw3lwC9lSLxxoB/+GtsJG91CO8bSWy73APlgAZzZbXEYZJuxjkHH2w+Ezhw==",
+ "license": "MIT",
+ "dependencies": {
+ "asynckit": "^0.4.0",
+ "combined-stream": "^1.0.8",
+ "mime-types": "^2.1.12"
+ },
+ "engines": {
+ "node": ">= 6"
+ }
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/lodash": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
+ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-es": {
+ "version": "4.17.21",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz",
+ "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
+ "license": "MIT"
+ },
+ "node_modules/lodash-unified": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/lodash-unified/-/lodash-unified-1.0.3.tgz",
+ "integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/lodash-es": "*",
+ "lodash": "*",
+ "lodash-es": "*"
+ }
+ },
+ "node_modules/magic-string": {
+ "version": "0.30.17",
+ "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz",
+ "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==",
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0"
+ }
+ },
+ "node_modules/memoize-one": {
+ "version": "6.0.0",
+ "resolved": "https://registry.npmjs.org/memoize-one/-/memoize-one-6.0.0.tgz",
+ "integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
+ "license": "MIT"
+ },
+ "node_modules/mime-db": {
+ "version": "1.52.0",
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
+ "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/mime-types": {
+ "version": "2.1.35",
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
+ "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
+ "license": "MIT",
+ "dependencies": {
+ "mime-db": "1.52.0"
+ },
+ "engines": {
+ "node": ">= 0.6"
+ }
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.8",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.8.tgz",
+ "integrity": "sha512-WNLf5Sd8oZxOm+TzppcYk8gVOgP+l58xNy58D0nbUnOxOWRWvlcCV4kUF7ltmI6PsrLl/BgKEyS4mqsGChFN0w==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/normalize-wheel-es": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
+ "integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "license": "ISC"
+ },
+ "node_modules/postcss": {
+ "version": "8.5.1",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz",
+ "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==",
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.8",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/proxy-from-env": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
+ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==",
+ "license": "MIT"
+ },
+ "node_modules/rollup": {
+ "version": "4.34.6",
+ "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.34.6.tgz",
+ "integrity": "sha512-wc2cBWqJgkU3Iz5oztRkQbfVkbxoz5EhnCGOrnJvnLnQ7O0WhQUYyv18qQI79O8L7DdHrrlJNeCHd4VGpnaXKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/estree": "1.0.6"
+ },
+ "bin": {
+ "rollup": "dist/bin/rollup"
+ },
+ "engines": {
+ "node": ">=18.0.0",
+ "npm": ">=8.0.0"
+ },
+ "optionalDependencies": {
+ "@rollup/rollup-android-arm-eabi": "4.34.6",
+ "@rollup/rollup-android-arm64": "4.34.6",
+ "@rollup/rollup-darwin-arm64": "4.34.6",
+ "@rollup/rollup-darwin-x64": "4.34.6",
+ "@rollup/rollup-freebsd-arm64": "4.34.6",
+ "@rollup/rollup-freebsd-x64": "4.34.6",
+ "@rollup/rollup-linux-arm-gnueabihf": "4.34.6",
+ "@rollup/rollup-linux-arm-musleabihf": "4.34.6",
+ "@rollup/rollup-linux-arm64-gnu": "4.34.6",
+ "@rollup/rollup-linux-arm64-musl": "4.34.6",
+ "@rollup/rollup-linux-loongarch64-gnu": "4.34.6",
+ "@rollup/rollup-linux-powerpc64le-gnu": "4.34.6",
+ "@rollup/rollup-linux-riscv64-gnu": "4.34.6",
+ "@rollup/rollup-linux-s390x-gnu": "4.34.6",
+ "@rollup/rollup-linux-x64-gnu": "4.34.6",
+ "@rollup/rollup-linux-x64-musl": "4.34.6",
+ "@rollup/rollup-win32-arm64-msvc": "4.34.6",
+ "@rollup/rollup-win32-ia32-msvc": "4.34.6",
+ "@rollup/rollup-win32-x64-msvc": "4.34.6",
+ "fsevents": "~2.3.2"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/vite": {
+ "version": "5.4.14",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.14.tgz",
+ "integrity": "sha512-EK5cY7Q1D8JNhSaPKVK4pwBFvaTmZxEnoKXLG/U9gmdDcihQGNzFlgIvaxezFR4glP1LsuiedwMBqCXH3wZccA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "esbuild": "^0.21.3",
+ "postcss": "^8.4.43",
+ "rollup": "^4.20.0"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^18.0.0 || >=20.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^18.0.0 || >=20.0.0",
+ "less": "*",
+ "lightningcss": "^1.21.0",
+ "sass": "*",
+ "sass-embedded": "*",
+ "stylus": "*",
+ "sugarss": "*",
+ "terser": "^5.4.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "lightningcss": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue": {
+ "version": "3.5.13",
+ "resolved": "https://registry.npmjs.org/vue/-/vue-3.5.13.tgz",
+ "integrity": "sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/compiler-dom": "3.5.13",
+ "@vue/compiler-sfc": "3.5.13",
+ "@vue/runtime-dom": "3.5.13",
+ "@vue/server-renderer": "3.5.13",
+ "@vue/shared": "3.5.13"
+ },
+ "peerDependencies": {
+ "typescript": "*"
+ },
+ "peerDependenciesMeta": {
+ "typescript": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vue-router": {
+ "version": "4.5.0",
+ "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.5.0.tgz",
+ "integrity": "sha512-HDuk+PuH5monfNuY+ct49mNmkCRK4xJAV9Ts4z9UFc4rzdDnxQLyCMGGc8pKhZhHTVzfanpNwB/lwqevcBwI4w==",
+ "license": "MIT",
+ "dependencies": {
+ "@vue/devtools-api": "^6.6.4"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/posva"
+ },
+ "peerDependencies": {
+ "vue": "^3.2.0"
+ }
+ }
+ }
+}
diff --git a/frontend/package.json b/frontend/package.json
new file mode 100644
index 0000000..6d42add
--- /dev/null
+++ b/frontend/package.json
@@ -0,0 +1,20 @@
+{
+ "name": "aimodels-prices-frontend",
+ "version": "0.1.0",
+ "private": true,
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "axios": "^1.6.7",
+ "element-plus": "^2.5.3",
+ "vue": "^3.4.15",
+ "vue-router": "^4.2.5"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-vue": "^5.0.3",
+ "vite": "^5.0.12"
+ }
+}
\ No newline at end of file
diff --git a/frontend/public/favicon.ico b/frontend/public/favicon.ico
new file mode 100644
index 0000000..3caead1
Binary files /dev/null and b/frontend/public/favicon.ico differ
diff --git a/frontend/src/App.vue b/frontend/src/App.vue
new file mode 100644
index 0000000..26141e5
--- /dev/null
+++ b/frontend/src/App.vue
@@ -0,0 +1,171 @@
+
+
+
+
+
+
+ AI模型价格
+
+
+ 价格列表
+ 供应商列表
+
+
+
+
+
+
+ {{ globalUser.username }}
+ 管理员
+
+ 退出
+
+ 登录
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/main.js b/frontend/src/main.js
new file mode 100644
index 0000000..612f8d1
--- /dev/null
+++ b/frontend/src/main.js
@@ -0,0 +1,15 @@
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import App from './App.vue'
+import router from './router'
+import axios from 'axios'
+
+// 配置 axios
+axios.defaults.withCredentials = true
+
+const app = createApp(App)
+
+app.use(ElementPlus)
+app.use(router)
+app.mount('#app')
\ No newline at end of file
diff --git a/frontend/src/router/index.js b/frontend/src/router/index.js
new file mode 100644
index 0000000..745943e
--- /dev/null
+++ b/frontend/src/router/index.js
@@ -0,0 +1,33 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import Prices from '../views/Prices.vue'
+import Providers from '../views/Providers.vue'
+import Login from '../views/Login.vue'
+import Home from '../views/Home.vue'
+
+const router = createRouter({
+ history: createWebHistory(import.meta.env.BASE_URL),
+ routes: [
+ {
+ path: '/',
+ name: 'home',
+ component: Home
+ },
+ {
+ path: '/prices',
+ name: 'prices',
+ component: Prices
+ },
+ {
+ path: '/providers',
+ name: 'providers',
+ component: Providers
+ },
+ {
+ path: '/login',
+ name: 'login',
+ component: Login
+ }
+ ]
+})
+
+export default router
\ No newline at end of file
diff --git a/frontend/src/views/Home.vue b/frontend/src/views/Home.vue
new file mode 100644
index 0000000..c24eeac
--- /dev/null
+++ b/frontend/src/views/Home.vue
@@ -0,0 +1,236 @@
+
+
+
+
+
+
+
+
项目简介
+
这是一个专门用于管理AI模型价格的系统,支持多供应商、多币种的价格管理,并提供标准的API接口供其他系统调用。
+
+
主要功能
+
+ - 供应商管理:添加、编辑和删除AI模型供应商
+ - 价格管理:设置和更新各个模型的价格
+ - 多币种支持:支持USD和CNY两种货币
+ - 审核流程:价格变更需要管理员审核
+ - API接口:提供标准的REST API
+
+
+
API文档
+
+
+
+
+ GET
+
+
+ {{ origin }}/api/prices/rates
+
+
+
+
获取所有已审核通过的价格的倍率信息
+
响应示例:
+
+[
+ {
+ "model": "babbage-002",
+ "type": "tokens",
+ "channel_type": 1,
+ "input": 0.2,
+ "output": 0.2
+ }
+]
+
字段说明:
+
+ - model: 模型名称
+ - type: 计费类型(tokens/times)
+ - channel_type: 供应商ID
+ - input: 输入价格倍率
+ - output: 输出价格倍率
+
+
+
+
+
+
+
+ GET
+
+
+ {{ origin }}/api/prices
+
+
+
+
获取所有价格信息,包括待审核的价格
+
响应示例:
+
+[
+ {
+ "id": 1,
+ "model": "gpt-4",
+ "billing_type": "tokens",
+ "channel_type": "1",
+ "currency": "USD",
+ "input_price": 0.01,
+ "output_price": 0.03,
+ "price_source": "官方",
+ "status": "approved"
+ }
+]
+
+
+
+
+
+
+ GET
+
+
+ {{ origin }}/api/providers
+
+
+
+
获取所有供应商信息
+
响应示例:
+
+[
+ {
+ "id": 1,
+ "name": "OpenAI",
+ "icon": "https://example.com/openai.png",
+ "created_at": "2024-01-01T00:00:00Z",
+ "updated_at": "2024-01-01T00:00:00Z",
+ "created_by": "admin"
+ }
+]
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/Login.vue b/frontend/src/views/Login.vue
new file mode 100644
index 0000000..8eacecb
--- /dev/null
+++ b/frontend/src/views/Login.vue
@@ -0,0 +1,62 @@
+
+
+
+
+ 登录
+
+
+ {{ loading ? '登录中...' : '登录' }}
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/Prices.vue b/frontend/src/views/Prices.vue
new file mode 100644
index 0000000..24c50b5
--- /dev/null
+++ b/frontend/src/views/Prices.vue
@@ -0,0 +1,483 @@
+
+
+
+
+
+
+
+
+
厂商筛选:
+
+
全部
+
+
+
+ {{ provider.name }}
+
+
+
+
+
+
+
+
+
+
{{ row.model }}
+
+ 待审核: {{ row.temp_model }}
+
+
+
+
+
+
+
+
{{ getBillingType(row.billing_type) }}
+
+ 待审核: {{ getBillingType(row.temp_billing_type) }}
+
+
+
+
+
+
+
+
+
+ {{ getProvider(row.channel_type)?.name || row.channel_type }}
+
+
+ 待审核: {{ getProvider(row.temp_channel_type)?.name || row.temp_channel_type }}
+
+
+
+
+
+
+
+
{{ row.currency }}
+
+ 待审核: {{ row.temp_currency }}
+
+
+
+
+
+
+
+
{{ row.input_price }}
+
+ 待审核: {{ row.temp_input_price }}
+
+
+
+
+
+
+
+
{{ row.output_price }}
+
+ 待审核: {{ row.temp_output_price }}
+
+
+
+
+
+
+ {{ calculateRate(row.input_price, row.currency) }}
+
+
+
+
+ {{ calculateRate(row.output_price, row.currency) }}
+
+
+
+
+
+
{{ row.price_source }}
+
+ 待审核: {{ row.temp_price_source }}
+
+
+
+
+
+
+ {{ getStatus(row.status) }}
+
+
+
+
+
+
+ 编辑
+ 删除
+ 通过
+ 拒绝
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ provider.name }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/src/views/Providers.vue b/frontend/src/views/Providers.vue
new file mode 100644
index 0000000..de9a561
--- /dev/null
+++ b/frontend/src/views/Providers.vue
@@ -0,0 +1,217 @@
+
+
+
+
+
+
+
+
+
+
+
+ {{ row.name }}
+
+
+
+
+
+ -
+
+
+
+
+
+
+ 编辑
+ 删除
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/frontend/vite.config.js b/frontend/vite.config.js
new file mode 100644
index 0000000..d6da2d6
--- /dev/null
+++ b/frontend/vite.config.js
@@ -0,0 +1,15 @@
+import { defineConfig } from 'vite'
+import vue from '@vitejs/plugin-vue'
+
+export default defineConfig({
+ plugins: [vue()],
+ publicDir: 'public',
+ server: {
+ proxy: {
+ '/api': {
+ target: 'http://localhost:8080',
+ changeOrigin: true
+ }
+ }
+ }
+})
\ No newline at end of file
diff --git a/import_map.json b/import_map.json
deleted file mode 100644
index daf44f0..0000000
--- a/import_map.json
+++ /dev/null
@@ -1,6 +0,0 @@
-{
- "imports": {
- "std/": "https://deno.land/std@0.220.1/",
- "deno/": "https://deno.land/x/deno@v1.40.5/"
- }
-}
\ No newline at end of file
diff --git a/main.ts b/main.ts
deleted file mode 100644
index 797787e..0000000
--- a/main.ts
+++ /dev/null
@@ -1,1261 +0,0 @@
-///
-
-import { serve, crypto, base64Decode } from "./deps.ts";
-
-// 声明 Deno 命名空间
-declare namespace Deno {
- interface Kv {
- get(key: unknown[]): Promise<{ value: any }>;
- set(key: unknown[], value: unknown, options?: { expireIn?: number }): Promise;
- delete(key: unknown[]): Promise;
- }
-
- interface Env {
- get(key: string): string | undefined;
- }
-
- const env: Env;
- const exit: (code: number) => never;
- const openKv: () => Promise;
-}
-
-// 类型定义
-interface Vendor {
- id: number;
- name: string;
- icon: string;
-}
-
-interface VendorResponse {
- data: {
- [key: string]: Vendor;
- };
-}
-
-interface Price {
- id?: string;
- model: string;
- billing_type: 'tokens' | 'times';
- channel_type: number;
- currency: 'CNY' | 'USD';
- input_price: number;
- output_price: number;
- input_ratio: number;
- output_ratio: number;
- price_source: string;
- status: 'pending' | 'approved' | 'rejected';
- created_by: string;
- created_at: string;
- reviewed_by?: string;
- reviewed_at?: string;
-}
-
-// 声明全局变量
-declare const vendors: { [key: string]: Vendor };
-
-// 缓存供应商数据
-let vendorsCache: VendorResponse | null = null;
-let vendorsCacheTime: number = 0;
-const CACHE_DURATION = 1000 * 60 * 5; // 5分钟缓存
-
-// 初始化 KV 存储
-let kv: Deno.Kv;
-
-try {
- kv = await Deno.openKv();
-} catch (error) {
- console.error('初始化 KV 存储失败:', error);
- Deno.exit(1);
-}
-
-// 获取供应商数据
-async function getVendors(): Promise {
- const now = Date.now();
- if (vendorsCache && (now - vendorsCacheTime) < CACHE_DURATION) {
- return vendorsCache;
- }
-
- try {
- const response = await fetch('https://oapi.czl.net/api/ownedby');
- const data = await response.json() as VendorResponse;
- vendorsCache = data;
- vendorsCacheTime = now;
- return data;
- } catch (error) {
- console.error('获取供应商数据失败:', error);
- throw new Error('获取供应商数据失败');
- }
-}
-
-// 计算倍率
-function calculateRatio(price: number, currency: 'CNY' | 'USD'): number {
- return currency === 'USD' ? price / 2 : price / 14;
-}
-
-// 修改验证价格数据函数
-function validatePrice(data: any): string | null {
- console.log('验证数据:', data); // 添加日志
-
- if (!data.model || !data.billing_type || !data.channel_type ||
- !data.currency || data.input_price === undefined || data.output_price === undefined ||
- !data.price_source) {
- return "所有字段都是必需的";
- }
-
- if (data.billing_type !== 'tokens' && data.billing_type !== 'times') {
- return "计费类型必须是 tokens 或 times";
- }
-
- if (data.currency !== 'CNY' && data.currency !== 'USD') {
- return "币种必须是 CNY 或 USD";
- }
-
- const channel_type = Number(data.channel_type);
- const input_price = Number(data.input_price);
- const output_price = Number(data.output_price);
-
- if (isNaN(channel_type) || isNaN(input_price) || isNaN(output_price)) {
- return "价格和供应商ID必须是数字";
- }
-
- if (channel_type < 0 || input_price < 0 || output_price < 0) {
- return "价格和供应商ID不能为负数";
- }
-
- return null;
-}
-
-// 添加 Discourse SSO 配置
-const DISCOURSE_URL = Deno.env.get('DISCOURSE_URL') || 'https://discourse.czl.net';
-const DISCOURSE_SSO_SECRET = Deno.env.get('DISCOURSE_SSO_SECRET');
-
-// 验证必需的环境变量
-if (!DISCOURSE_SSO_SECRET) {
- console.error('错误: 必须设置 DISCOURSE_SSO_SECRET 环境变量');
- Deno.exit(1);
-}
-
-// 添加认证相关函数
-async function verifyDiscourseSSO(request: Request): Promise {
- const cookie = request.headers.get('cookie');
- if (!cookie) return null;
-
- const sessionMatch = cookie.match(/session=([^;]+)/);
- if (!sessionMatch) return null;
-
- const sessionId = sessionMatch[1];
- const session = await kv.get(['sessions', sessionId]);
-
- if (!session.value) return null;
- return session.value.username;
-}
-
-// 修改 generateSSO 函数
-async function generateSSO(returnUrl: string): Promise {
- const encoder = new TextEncoder();
- const nonce = crypto.randomUUID();
- const rawPayload = `nonce=${nonce}&return_sso_url=${encodeURIComponent(returnUrl)}`;
- const base64Payload = btoa(rawPayload);
- const key = await crypto.subtle.importKey(
- "raw",
- encoder.encode(DISCOURSE_SSO_SECRET),
- { name: "HMAC", hash: "SHA-256" },
- false,
- ["sign"]
- );
- const signature = await crypto.subtle.sign(
- "HMAC",
- key,
- encoder.encode(base64Payload)
- );
- const sig = Array.from(new Uint8Array(signature))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
-
- // 存储 nonce 用于验证回调
- await kv.set(['sso_nonce', nonce], {
- return_url: returnUrl,
- created_at: new Date().toISOString()
- }, { expireIn: 5 * 60 * 1000 }); // 5分钟过期
-
- return `${DISCOURSE_URL}/session/sso_provider?sso=${encodeURIComponent(base64Payload)}&sig=${sig}`;
-}
-
-// HTML 页面
-const html = `
-
-
- AI Models Price API
-
-
-
-
-
-
-
-
-
-
- 价格json:
- https://aimodels-price.deno.dev/api/prices
-
- 已复制
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- 模型名称 |
- 计费类型 |
- 供应商 |
- 币种 |
- 输入价格(M) |
- 输出价格(M) |
- 输入倍率 |
- 输出倍率 |
- 价格依据 |
- 状态 |
- 操作 |
-
-
-
-
-
-
- 加载中...
-
- |
-
-
-
-
-
-
-
状态说明:
-
- 待审核
- 新提交的价格记录
-
-
- 已通过
- 管理员审核通过
-
-
- 已拒绝
- 管理员拒绝
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-`;
-
-// 读取价格数据
-async function readPrices(): Promise {
- try {
- const prices = await kv.get(["prices"]);
- return prices.value || [];
- } catch (error) {
- console.error('读取价格数据失败:', error);
- return [];
- }
-}
-
-// 写入价格数据
-async function writePrices(prices: Price[]): Promise {
- try {
- await kv.set(["prices"], prices);
- } catch (error) {
- console.error('写入价格数据失败:', error);
- throw new Error('写入价格数据失败');
- }
-}
-
-// 修改验证函数
-function validateData(data: any): string | null {
- if (!data.model || !data.type || data.channel_type === undefined || data.input === undefined || data.output === undefined) {
- return "所有字段都是必需的";
- }
-
- // 确保数字字段是数字类型
- const channel_type = Number(data.channel_type);
- const input = Number(data.input);
- const output = Number(data.output);
-
- if (isNaN(channel_type) || isNaN(input) || isNaN(output)) {
- return "数字字段格式无效";
- }
-
- // 验证数字范围(允许等于0)
- if (channel_type < 0 || input < 0 || output < 0) {
- return "数字不能小于0";
- }
-
- return null;
-}
-
-// 修改处理函数
-async function handler(req: Request): Promise {
- const headers = {
- "Access-Control-Allow-Origin": req.headers.get("origin") || "*",
- "Access-Control-Allow-Methods": "GET, POST, OPTIONS, PUT, DELETE",
- "Access-Control-Allow-Headers": "Content-Type, Cookie, Authorization, Accept",
- "Access-Control-Allow-Credentials": "true",
- "Access-Control-Max-Age": "86400"
- };
-
- const jsonHeaders = {
- ...headers,
- "Content-Type": "application/json"
- };
-
- const htmlHeaders = {
- ...headers,
- "Content-Type": "text/html; charset=utf-8"
- };
-
- try {
- const url = new URL(req.url);
- console.log('Received request:', req.method, url.pathname);
-
- // 处理预检请求
- if (req.method === "OPTIONS") {
- return new Response(null, {
- status: 204,
- headers
- });
- }
-
- // 获取价格列表
- if (url.pathname === "/api/prices" && req.method === "GET") {
- try {
- console.log('Reading prices from KV store...');
- const prices = await readPrices();
- console.log('Prices read successfully:', prices.length);
- return new Response(JSON.stringify(prices), {
- headers: jsonHeaders
- });
- } catch (error) {
- console.error('获取价格列表失败:', error);
- return new Response(JSON.stringify({
- error: "获取价格列表失败",
- details: error.message
- }), {
- status: 500,
- headers: jsonHeaders
- });
- }
- }
-
- // 认证状态检查
- if (url.pathname === "/api/auth/status") {
- try {
- const username = await verifyDiscourseSSO(req);
- return new Response(JSON.stringify({
- authenticated: !!username,
- user: username
- }), { headers: jsonHeaders });
- } catch (error) {
- console.error('验证用户状态失败:', error);
- return new Response(JSON.stringify({
- error: "验证用户状态失败",
- details: error.message
- }), {
- status: 500,
- headers: jsonHeaders
- });
- }
- }
-
- // 登录处理
- if (url.pathname === "/api/auth/login") {
- const params = new URLSearchParams(url.search);
- const returnUrl = params.get('return_url');
- if (!returnUrl) {
- return new Response(JSON.stringify({ error: "缺少 return_url 参数" }), {
- status: 400,
- headers: jsonHeaders
- });
- }
-
- const ssoUrl = await generateSSO(returnUrl);
- return new Response(null, {
- status: 302,
- headers: {
- ...headers,
- "Location": ssoUrl
- }
- });
- }
-
- // SSO 回调处理
- if (url.pathname === "/auth/callback") {
- const params = new URLSearchParams(url.search);
- const sso = params.get('sso');
- const sig = params.get('sig');
-
- if (!sso || !sig) {
- return new Response("Invalid SSO parameters", {
- status: 400,
- headers: {
- "Content-Type": "text/plain",
- ...headers
- }
- });
- }
-
- try {
- // 验证签名
- const key = await crypto.subtle.importKey(
- "raw",
- new TextEncoder().encode(DISCOURSE_SSO_SECRET),
- { name: "HMAC", hash: "SHA-256" },
- false,
- ["sign"]
- );
- const signature = await crypto.subtle.sign(
- "HMAC",
- key,
- new TextEncoder().encode(sso)
- );
- const expectedSig = Array.from(new Uint8Array(signature))
- .map(b => b.toString(16).padStart(2, '0'))
- .join('');
-
- if (sig !== expectedSig) {
- throw new Error('Invalid signature');
- }
-
- // 解码 payload
- const payload = atob(sso);
- const payloadParams = new URLSearchParams(payload);
-
- // 验证 nonce
- const nonce = payloadParams.get('nonce');
- if (!nonce) {
- throw new Error('Missing nonce');
- }
-
- const nonceData = await kv.get(['sso_nonce', nonce]);
- if (!nonceData.value) {
- throw new Error('Invalid or expired nonce');
- }
-
- // 删除已使用的 nonce
- await kv.delete(['sso_nonce', nonce]);
-
- const username = payloadParams.get('username');
- if (!username) {
- throw new Error('Missing username');
- }
-
- // 设置 session cookie
- const sessionId = crypto.randomUUID();
- await kv.set(['sessions', sessionId], {
- username,
- created_at: new Date().toISOString()
- }, { expireIn: 24 * 60 * 60 * 1000 }); // 24小时过期
-
- return new Response(null, {
- status: 302,
- headers: {
- ...headers,
- "Location": "/",
- "Set-Cookie": `session=${sessionId}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400`
- }
- });
- } catch (error) {
- console.error('SSO 回调处理失败:', error);
- return new Response("SSO verification failed: " + error.message, {
- status: 400,
- headers: {
- "Content-Type": "text/plain",
- ...headers
- }
- });
- }
- }
-
- // 登出处理
- if (url.pathname === "/api/auth/logout" && req.method === "POST") {
- const cookie = req.headers.get('cookie');
- if (cookie) {
- const sessionMatch = cookie.match(/session=([^;]+)/);
- if (sessionMatch) {
- const sessionId = sessionMatch[1];
- await kv.delete(['sessions', sessionId]);
- }
- }
-
- return new Response(JSON.stringify({ success: true }), {
- headers: {
- ...jsonHeaders,
- "Set-Cookie": "session=; Path=/; HttpOnly; SameSite=Lax; Max-Age=0"
- }
- });
- }
-
- // 价格审核
- if (url.pathname.match(/^\/api\/prices\/\d+\/review$/)) {
- const username = await verifyDiscourseSSO(req);
- if (!username || username !== 'wood') {
- return new Response(JSON.stringify({ error: "未授权" }), {
- status: 403,
- headers: jsonHeaders
- });
- }
-
- if (req.method === "POST") {
- try {
- const id = url.pathname.split('/')[3];
- const { status } = await req.json();
-
- if (status !== 'approved' && status !== 'rejected') {
- throw new Error("无效的状态");
- }
-
- const prices = await readPrices();
- const priceIndex = prices.findIndex(p => p.id === id);
-
- if (priceIndex === -1) {
- throw new Error("价格记录不存在");
- }
-
- prices[priceIndex].status = status;
- prices[priceIndex].reviewed_by = username;
- prices[priceIndex].reviewed_at = new Date().toISOString();
-
- await writePrices(prices);
-
- return new Response(JSON.stringify({ success: true }), {
- headers: jsonHeaders
- });
- } catch (error) {
- return new Response(JSON.stringify({
- error: error.message || "审核失败"
- }), {
- status: 400,
- headers: jsonHeaders
- });
- }
- }
- }
-
- // 提交新价格
- if (url.pathname === "/api/prices" && req.method === "POST") {
- const username = await verifyDiscourseSSO(req);
- if (!username) {
- return new Response(JSON.stringify({ error: "请先登录" }), {
- status: 401,
- headers: jsonHeaders
- });
- }
-
- try {
- let rawData;
- const contentType = req.headers.get("content-type") || "";
-
- if (contentType.includes("application/json")) {
- rawData = await req.json();
- } else if (contentType.includes("application/x-www-form-urlencoded")) {
- const formData = await req.formData();
- rawData = {};
- for (const [key, value] of formData.entries()) {
- rawData[key] = value;
- }
- } else {
- throw new Error("不支持的内容类型");
- }
-
- console.log('接收到的数据:', rawData); // 添加日志
-
- // 处理数据
- const newPrice: Price = {
- model: String(rawData.model).trim(),
- billing_type: rawData.billing_type as 'tokens' | 'times',
- channel_type: Number(rawData.channel_type),
- currency: rawData.currency as 'CNY' | 'USD',
- input_price: Number(rawData.input_price),
- output_price: Number(rawData.output_price),
- input_ratio: calculateRatio(Number(rawData.input_price), rawData.currency as 'CNY' | 'USD'),
- output_ratio: calculateRatio(Number(rawData.output_price), rawData.currency as 'CNY' | 'USD'),
- price_source: String(rawData.price_source),
- status: 'pending',
- created_by: username,
- created_at: new Date().toISOString()
- };
-
- console.log('处理后的数据:', newPrice); // 添加日志
-
- // 验证数据
- const error = validatePrice(newPrice);
- if (error) {
- return new Response(JSON.stringify({ error }), {
- status: 400,
- headers: jsonHeaders
- });
- }
-
- // 读取现有数据
- const prices = await readPrices();
-
- // 生成唯一ID
- newPrice.id = Date.now().toString();
-
- // 添加新数据
- prices.push(newPrice);
-
- // 保存数据
- await writePrices(prices);
-
- return new Response(JSON.stringify({
- success: true,
- data: newPrice
- }), {
- headers: jsonHeaders
- });
- } catch (error) {
- console.error("处理价格提交失败:", error);
- return new Response(JSON.stringify({
- error: error.message,
- details: "数据处理失败,请检查输入格式"
- }), {
- status: 500,
- headers: jsonHeaders
- });
- }
- }
-
- // 提供静态页面
- if (url.pathname === "/" || url.pathname === "/index.html") {
- return new Response(html, {
- headers: htmlHeaders
- });
- }
-
- return new Response("Not Found", {
- status: 404,
- headers: {
- ...headers,
- "Content-Type": "text/plain"
- }
- });
- } catch (error) {
- console.error('处理请求失败:', error);
- return new Response(JSON.stringify({
- error: "处理请求失败",
- details: error.message
- }), {
- status: 500,
- headers: jsonHeaders
- });
- }
-}
-
-// 启动服务器
-serve(handler);
\ No newline at end of file
diff --git a/prices.json b/prices.json
index 3c8ac87..e6a9f8a 100644
--- a/prices.json
+++ b/prices.json
@@ -1,1374 +1 @@
-[
- {
- "model": "babbage-002",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.2,
- "output": 0.2
- },
- {
- "model": "dall-e-2",
- "type": "tokens",
- "channel_type": 1,
- "input": 8,
- "output": 8
- },
- {
- "model": "dall-e-3",
- "type": "tokens",
- "channel_type": 1,
- "input": 20,
- "output": 20
- },
- {
- "model": "davinci-002",
- "type": "tokens",
- "channel_type": 1,
- "input": 1,
- "output": 1
- },
- {
- "model": "gpt-3.5-turbo",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gpt-3.5-turbo-0125",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gpt-3.5-turbo-0301",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.75,
- "output": 1
- },
- {
- "model": "gpt-3.5-turbo-0613",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.75,
- "output": 1
- },
- {
- "model": "gpt-3.5-turbo-1106",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.5,
- "output": 1
- },
- {
- "model": "gpt-3.5-turbo-16k",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.5,
- "output": 2
- },
- {
- "model": "gpt-3.5-turbo-16k-0613",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.5,
- "output": 2
- },
- {
- "model": "gpt-3.5-turbo-instruct",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.75,
- "output": 1
- },
- {
- "model": "gpt-4",
- "type": "tokens",
- "channel_type": 1,
- "input": 15,
- "output": 30
- },
- {
- "model": "gpt-4-turbo",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "gpt-4-turbo-2024-04-09",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "o1-mini",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.5,
- "output": 6
- },
- {
- "model": "o1-mini-2024-09-12",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.5,
- "output": 6
- },
- {
- "model": "o1-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 7.5,
- "output": 30
- },
- {
- "model": "o1-preview-2024-09-12",
- "type": "tokens",
- "channel_type": 1,
- "input": 7.5,
- "output": 30
- },
- {
- "model": "chatgpt-4o-latest",
- "type": "tokens",
- "channel_type": 1,
- "input": 2.5,
- "output": 7.5
- },
- {
- "model": "gpt-4o",
- "type": "tokens",
- "channel_type": 1,
- "input": 2.5,
- "output": 7.5
- },
- {
- "model": "gpt-4o-2024-05-13",
- "type": "tokens",
- "channel_type": 1,
- "input": 2.5,
- "output": 7.5
- },
- {
- "model": "gpt-4o-2024-08-06",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.25,
- "output": 5
- },
- {
- "model": "gpt-4o-mini",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.075,
- "output": 0.3
- },
- {
- "model": "gpt-4o-mini-2024-07-18",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.075,
- "output": 0.3
- },
- {
- "model": "gpt-4o-realtime-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 2.5,
- "output": 10
- },
- {
- "model": "gpt-4o-realtime-preview-2024-10-01",
- "type": "tokens",
- "channel_type": 1,
- "input": 2.5,
- "output": 10
- },
- {
- "model": "gpt-4o-audio-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.25,
- "output": 5
- },
- {
- "model": "gpt-4o-audio-preview-2024-10-01",
- "type": "tokens",
- "channel_type": 1,
- "input": 1.25,
- "output": 5
- },
- {
- "model": "gpt-4-0125-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "gpt-4-0314",
- "type": "tokens",
- "channel_type": 1,
- "input": 15,
- "output": 30
- },
- {
- "model": "gpt-4-0613",
- "type": "tokens",
- "channel_type": 1,
- "input": 15,
- "output": 30
- },
- {
- "model": "gpt-4-1106-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "gpt-4-32k",
- "type": "tokens",
- "channel_type": 1,
- "input": 30,
- "output": 60
- },
- {
- "model": "gpt-4-32k-0314",
- "type": "tokens",
- "channel_type": 1,
- "input": 30,
- "output": 60
- },
- {
- "model": "gpt-4-32k-0613",
- "type": "tokens",
- "channel_type": 1,
- "input": 30,
- "output": 60
- },
- {
- "model": "gpt-4-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "gpt-4-turbo-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "gpt-4-vision-preview",
- "type": "tokens",
- "channel_type": 1,
- "input": 5,
- "output": 15
- },
- {
- "model": "text-embedding-3-large",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.065,
- "output": 0.065
- },
- {
- "model": "text-embedding-3-small",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.01,
- "output": 0.01
- },
- {
- "model": "text-embedding-ada-002",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.05,
- "output": 0.05
- },
- {
- "model": "text-moderation-latest",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.1,
- "output": 0.1
- },
- {
- "model": "text-moderation-stable",
- "type": "tokens",
- "channel_type": 1,
- "input": 0.1,
- "output": 0.1
- },
- {
- "model": "tts-1",
- "type": "tokens",
- "channel_type": 1,
- "input": 7.5,
- "output": 7.5
- },
- {
- "model": "tts-1-1106",
- "type": "tokens",
- "channel_type": 1,
- "input": 7.5,
- "output": 7.5
- },
- {
- "model": "tts-1-hd",
- "type": "tokens",
- "channel_type": 1,
- "input": 15,
- "output": 15
- },
- {
- "model": "tts-1-hd-1106",
- "type": "tokens",
- "channel_type": 1,
- "input": 15,
- "output": 15
- },
- {
- "model": "whisper-1",
- "type": "tokens",
- "channel_type": 1,
- "input": 15,
- "output": 15
- },
- {
- "model": "PaLM-2",
- "type": "tokens",
- "channel_type": 11,
- "input": 1,
- "output": 1
- },
- {
- "model": "claude-2.0",
- "type": "tokens",
- "channel_type": 14,
- "input": 4,
- "output": 12
- },
- {
- "model": "claude-2.1",
- "type": "tokens",
- "channel_type": 14,
- "input": 4,
- "output": 12
- },
- {
- "model": "claude-3-haiku-20240307",
- "type": "tokens",
- "channel_type": 14,
- "input": 0.125,
- "output": 0.625
- },
- {
- "model": "claude-3-5-haiku-20241022",
- "type": "tokens",
- "channel_type": 14,
- "input": 0.5,
- "output": 2.5
- },
- {
- "model": "claude-3-5-haiku-latest",
- "type": "tokens",
- "channel_type": 14,
- "input": 0.5,
- "output": 2.5
- },
- {
- "model": "claude-3-opus-20240229",
- "type": "tokens",
- "channel_type": 14,
- "input": 7.5,
- "output": 37.5
- },
- {
- "model": "claude-3-sonnet-20240229",
- "type": "tokens",
- "channel_type": 14,
- "input": 1.5,
- "output": 7.5
- },
- {
- "model": "claude-3-5-sonnet-20240620",
- "type": "tokens",
- "channel_type": 14,
- "input": 1.5,
- "output": 7.5
- },
- {
- "model": "claude-3-5-sonnet-20241022",
- "type": "tokens",
- "channel_type": 14,
- "input": 1.5,
- "output": 7.5
- },
- {
- "model": "claude-3-5-sonnet-latest",
- "type": "tokens",
- "channel_type": 14,
- "input": 1.5,
- "output": 7.5
- },
- {
- "model": "claude-instant-1.2",
- "type": "tokens",
- "channel_type": 14,
- "input": 0.4,
- "output": 1.2
- },
- {
- "model": "BLOOMZ-7B",
- "type": "tokens",
- "channel_type": 15,
- "input": 0.2857,
- "output": 0.2857
- },
- {
- "model": "ERNIE-3.5-8K",
- "type": "tokens",
- "channel_type": 15,
- "input": 0.8572,
- "output": 0.8572
- },
- {
- "model": "ERNIE-4.0",
- "type": "tokens",
- "channel_type": 15,
- "input": 8.572,
- "output": 8.572
- },
- {
- "model": "ERNIE-Bot",
- "type": "tokens",
- "channel_type": 15,
- "input": 0.8572,
- "output": 0.8572
- },
- {
- "model": "ERNIE-Bot-4",
- "type": "tokens",
- "channel_type": 15,
- "input": 8.572,
- "output": 8.572
- },
- {
- "model": "ERNIE-Bot-turbo",
- "type": "tokens",
- "channel_type": 15,
- "input": 0,
- "output": 0
- },
- {
- "model": "ERNIE-Speed",
- "type": "tokens",
- "channel_type": 15,
- "input": 0,
- "output": 0
- },
- {
- "model": "ERNIE-Speed-128K",
- "type": "tokens",
- "channel_type": 15,
- "input": 0,
- "output": 0
- },
- {
- "model": "ERNIE-Lite-8K",
- "type": "tokens",
- "channel_type": 15,
- "input": 0,
- "output": 0
- },
- {
- "model": "ERNIE-Tiny-8K",
- "type": "tokens",
- "channel_type": 15,
- "input": 0,
- "output": 0
- },
- {
- "model": "ERNIE-Functions-8K",
- "type": "tokens",
- "channel_type": 15,
- "input": 0.2857,
- "output": 0.5714
- },
- {
- "model": "Embedding-V1",
- "type": "tokens",
- "channel_type": 15,
- "input": 0.1429,
- "output": 0.1429
- },
- {
- "model": "cogview-3",
- "type": "tokens",
- "channel_type": 16,
- "input": 17.8571,
- "output": 17.8571
- },
- {
- "model": "embedding-2",
- "type": "tokens",
- "channel_type": 16,
- "input": 0.0357,
- "output": 0.0357
- },
- {
- "model": "glm-3-turbo",
- "type": "tokens",
- "channel_type": 16,
- "input": 0.0714,
- "output": 0.0714
- },
- {
- "model": "glm-4",
- "type": "tokens",
- "channel_type": 16,
- "input": 7.143,
- "output": 7.143
- },
- {
- "model": "glm-4v",
- "type": "tokens",
- "channel_type": 16,
- "input": 7.143,
- "output": 7.143
- },
- {
- "model": "glm-4-plus",
- "type": "tokens",
- "channel_type": 16,
- "input": 3.5714,
- "output": 3.5714
- },
- {
- "model": "glm-4-0520",
- "type": "tokens",
- "channel_type": 16,
- "input": 7.1429,
- "output": 7.1429
- },
- {
- "model": "glm-4-air",
- "type": "tokens",
- "channel_type": 16,
- "input": 0.0714,
- "output": 0.0714
- },
- {
- "model": "glm-4-airx",
- "type": "tokens",
- "channel_type": 16,
- "input": 0.7143,
- "output": 0.7143
- },
- {
- "model": "glm-4-long",
- "type": "tokens",
- "channel_type": 16,
- "input": 0.0714,
- "output": 0.0714
- },
- {
- "model": "glm-4-flash",
- "type": "tokens",
- "channel_type": 16,
- "input": 0,
- "output": 0
- },
- {
- "model": "glm-4-alltools",
- "type": "tokens",
- "channel_type": 16,
- "input": 7.1429,
- "output": 7.1429
- },
- {
- "model": "qwen-max",
- "type": "tokens",
- "channel_type": 17,
- "input": 1.4286,
- "output": 4.2857
- },
- {
- "model": "qwen-max-longcontext",
- "type": "tokens",
- "channel_type": 17,
- "input": 2.8571,
- "output": 8.5714
- },
- {
- "model": "qwen-plus",
- "type": "tokens",
- "channel_type": 17,
- "input": 0.0571,
- "output": 0.1429
- },
- {
- "model": "qwen-turbo",
- "type": "tokens",
- "channel_type": 17,
- "input": 0.0214,
- "output": 0.0429
- },
- {
- "model": "qwen-long",
- "type": "tokens",
- "channel_type": 17,
- "input": 0.0357,
- "output": 0.1429
- },
- {
- "model": "qwen-vl-max",
- "type": "tokens",
- "channel_type": 17,
- "input": 1.4286,
- "output": 1.4286
- },
- {
- "model": "qwen-vl-plus",
- "type": "tokens",
- "channel_type": 17,
- "input": 0.5715,
- "output": 0.5715
- },
- {
- "model": "text-embedding-v1",
- "type": "tokens",
- "channel_type": 17,
- "input": 0.05,
- "output": 0.05
- },
- {
- "model": "SparkDesk-v1.1",
- "type": "tokens",
- "channel_type": 18,
- "input": 0,
- "output": 0
- },
- {
- "model": "SparkDesk-v3.1",
- "type": "tokens",
- "channel_type": 18,
- "input": 2.1429,
- "output": 2.1429
- },
- {
- "model": "SparkDesk-v3.5",
- "type": "tokens",
- "channel_type": 18,
- "input": 2.1429,
- "output": 2.1429
- },
- {
- "model": "SparkDesk-v4.0",
- "type": "tokens",
- "channel_type": 18,
- "input": 7.1429,
- "output": 7.1429
- },
- {
- "model": "360GPT_S2_V9",
- "type": "tokens",
- "channel_type": 19,
- "input": 0.8572,
- "output": 0.8572
- },
- {
- "model": "embedding-bert-512-v1",
- "type": "tokens",
- "channel_type": 19,
- "input": 0.0715,
- "output": 0.0715
- },
- {
- "model": "embedding_s1_v1",
- "type": "tokens",
- "channel_type": 19,
- "input": 0.0715,
- "output": 0.0715
- },
- {
- "model": "semantic_similarity_s1_v1",
- "type": "tokens",
- "channel_type": 19,
- "input": 0.0715,
- "output": 0.0715
- },
- {
- "model": "ChatPro",
- "type": "tokens",
- "channel_type": 23,
- "input": 7.143,
- "output": 7.143
- },
- {
- "model": "ChatStd",
- "type": "tokens",
- "channel_type": 23,
- "input": 0.7143,
- "output": 0.7143
- },
- {
- "model": "hunyuan",
- "type": "tokens",
- "channel_type": 23,
- "input": 7.143,
- "output": 7.143
- },
- {
- "model": "gemini-1.0-pro",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gemini-1.0-pro-001",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gemini-1.0-pro-latest",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gemini-1.0-ultra-latest",
- "type": "tokens",
- "channel_type": 25,
- "input": 1,
- "output": 1
- },
- {
- "model": "gemini-1.5-pro",
- "type": "tokens",
- "channel_type": 25,
- "input": 1.75,
- "output": 5.25
- },
- {
- "model": "gemini-1.5-pro-latest",
- "type": "tokens",
- "channel_type": 25,
- "input": 1.75,
- "output": 5.25
- },
- {
- "model": "gemini-1.5-pro-001",
- "type": "tokens",
- "channel_type": 25,
- "input": 1.75,
- "output": 5.25
- },
- {
- "model": "gemini-1.5-pro-exp-0827",
- "type": "tokens",
- "channel_type": 25,
- "input": 1.75,
- "output": 5.25
- },
- {
- "model": "gemini-1.5-pro-exp-0801",
- "type": "tokens",
- "channel_type": 25,
- "input": 1.75,
- "output": 5.25
- },
- {
- "model": "gemini-1.5-flash-latest",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.0375,
- "output": 0.15
- },
- {
- "model": "gemini-1.5-flash",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.0375,
- "output": 0.15
- },
- {
- "model": "gemini-1.5-flash-001",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.0375,
- "output": 0.15
- },
- {
- "model": "gemini-1.5-flash-exp-0827",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.0375,
- "output": 0.15
- },
- {
- "model": "gemini-1.5-flash-8b-exp-0827",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.0375,
- "output": 0.15
- },
- {
- "model": "gemini-ultra",
- "type": "tokens",
- "channel_type": 25,
- "input": 1,
- "output": 1
- },
- {
- "model": "gemini-pro",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gemini-pro-vision",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "gemini-1.0-pro-vision-latest",
- "type": "tokens",
- "channel_type": 25,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "Baichuan-Text-Embedding",
- "type": "tokens",
- "channel_type": 26,
- "input": 0.0357,
- "output": 0.0357
- },
- {
- "model": "Baichuan2-53B",
- "type": "tokens",
- "channel_type": 26,
- "input": 1.4286,
- "output": 1.4286
- },
- {
- "model": "Baichuan2-Turbo",
- "type": "tokens",
- "channel_type": 26,
- "input": 0.5715,
- "output": 0.5715
- },
- {
- "model": "Baichuan2-Turbo-192k",
- "type": "tokens",
- "channel_type": 26,
- "input": 1.143,
- "output": 1.143
- },
- {
- "model": "abab5.5-chat",
- "type": "tokens",
- "channel_type": 27,
- "input": 1.0714,
- "output": 1.0714
- },
- {
- "model": "abab5.5s-chat",
- "type": "tokens",
- "channel_type": 27,
- "input": 0.3572,
- "output": 0.3572
- },
- {
- "model": "abab6-chat",
- "type": "tokens",
- "channel_type": 27,
- "input": 7.1429,
- "output": 7.1429
- },
- {
- "model": "abab6.5-chat",
- "type": "tokens",
- "channel_type": 27,
- "input": 2.1429,
- "output": 2.1429
- },
- {
- "model": "abab6.5s-chat",
- "type": "tokens",
- "channel_type": 27,
- "input": 0.7143,
- "output": 0.7143
- },
- {
- "model": "embo-01",
- "type": "tokens",
- "channel_type": 27,
- "input": 0.0357,
- "output": 0.0357
- },
- {
- "model": "deepseek-chat",
- "type": "tokens",
- "channel_type": 28,
- "input": 0.0714,
- "output": 0.1429
- },
- {
- "model": "deepseek-coder",
- "type": "tokens",
- "channel_type": 28,
- "input": 0.0714,
- "output": 0.1429
- },
- {
- "model": "moonshot-v1-128k",
- "type": "tokens",
- "channel_type": 29,
- "input": 4.2857,
- "output": 4.2857
- },
- {
- "model": "moonshot-v1-32k",
- "type": "tokens",
- "channel_type": 29,
- "input": 1.7143,
- "output": 1.7143
- },
- {
- "model": "moonshot-v1-8k",
- "type": "tokens",
- "channel_type": 29,
- "input": 0.8572,
- "output": 0.8572
- },
- {
- "model": "mistral-embed",
- "type": "tokens",
- "channel_type": 30,
- "input": 0.05,
- "output": 0.05
- },
- {
- "model": "mistral-large-latest",
- "type": "tokens",
- "channel_type": 30,
- "input": 4,
- "output": 12
- },
- {
- "model": "mistral-medium-latest",
- "type": "tokens",
- "channel_type": 30,
- "input": 1.35,
- "output": 4.05
- },
- {
- "model": "mistral-small-latest",
- "type": "tokens",
- "channel_type": 30,
- "input": 1,
- "output": 3
- },
- {
- "model": "open-mistral-7b",
- "type": "tokens",
- "channel_type": 30,
- "input": 0.125,
- "output": 0.125
- },
- {
- "model": "open-mixtral-8x7b",
- "type": "tokens",
- "channel_type": 30,
- "input": 0.35,
- "output": 0.35
- },
- {
- "model": "open-mixtral-8x22b",
- "type": "tokens",
- "channel_type": 30,
- "input": 4,
- "output": 12
- },
- {
- "model": "gemma-7b-it",
- "type": "times",
- "channel_type": 31,
- "input": 0,
- "output": 0
- },
- {
- "model": "llama2-70b-4096",
- "type": "times",
- "channel_type": 31,
- "input": 0,
- "output": 0
- },
- {
- "model": "mixtral-8x7b-32768",
- "type": "times",
- "channel_type": 31,
- "input": 0,
- "output": 0
- },
- {
- "model": "llama3-8b-8192",
- "type": "times",
- "channel_type": 31,
- "input": 0,
- "output": 0
- },
- {
- "model": "llama3-70b-8192",
- "type": "times",
- "channel_type": 31,
- "input": 0,
- "output": 0
- },
- {
- "model": "yi-34b-chat-0205",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.1786,
- "output": 0.1786
- },
- {
- "model": "yi-34b-chat-200k",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.8571,
- "output": 0.8571
- },
- {
- "model": "yi-vl-plus",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.4286,
- "output": 0.4286
- },
- {
- "model": "yi-lightning",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.0707,
- "output": 0.0707
- },
- {
- "model": "yi-large",
- "type": "tokens",
- "channel_type": 33,
- "input": 1.4286,
- "output": 1.4286
- },
- {
- "model": "yi-medium",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.1786,
- "output": 0.1786
- },
- {
- "model": "yi-vision",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.4286,
- "output": 0.4286
- },
- {
- "model": "yi-medium-200k",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.8571,
- "output": 0.8571
- },
- {
- "model": "yi-spark",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.0714,
- "output": 0.0714
- },
- {
- "model": "yi-large-rag",
- "type": "tokens",
- "channel_type": 33,
- "input": 1.7857,
- "output": 1.7857
- },
- {
- "model": "yi-large-fc",
- "type": "tokens",
- "channel_type": 33,
- "input": 1.4286,
- "output": 1.4286
- },
- {
- "model": "yi-large-turbo",
- "type": "tokens",
- "channel_type": 33,
- "input": 0.8571,
- "output": 0.8571
- },
- {
- "model": "mj_blend",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_custom_zoom",
- "type": "times",
- "channel_type": 34,
- "input": 0,
- "output": 0
- },
- {
- "model": "mj_describe",
- "type": "times",
- "channel_type": 34,
- "input": 25,
- "output": 25
- },
- {
- "model": "mj_high_variation",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_imagine",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_inpaint",
- "type": "times",
- "channel_type": 34,
- "input": 0,
- "output": 0
- },
- {
- "model": "mj_low_variation",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_modal",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_pan",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_reroll",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_shorten",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_upscale",
- "type": "times",
- "channel_type": 34,
- "input": 25,
- "output": 25
- },
- {
- "model": "mj_variation",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "mj_zoom",
- "type": "times",
- "channel_type": 34,
- "input": 50,
- "output": 50
- },
- {
- "model": "swap_face",
- "type": "times",
- "channel_type": 34,
- "input": 25,
- "output": 25
- },
- {
- "model": "@cf/bytedance/stable-diffusion-xl-lightning",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@cf/lykon/dreamshaper-8-lcm",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@cf/openai/whisper",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@cf/qwen/qwen1.5-14b-chat-awq",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@cf/qwen/qwen1.5-7b-chat-awq",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@cf/stabilityai/stable-diffusion-xl-base-1.0",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@hf/google/gemma-7b-it",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@hf/thebloke/deepseek-coder-6.7b-base-awq",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@hf/thebloke/llama-2-13b-chat-awq",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "@cf/meta/llama-3-8b-instruct",
- "type": "tokens",
- "channel_type": 35,
- "input": 0,
- "output": 0
- },
- {
- "model": "command-r",
- "type": "tokens",
- "channel_type": 36,
- "input": 0.25,
- "output": 0.75
- },
- {
- "model": "command-r-plus",
- "type": "tokens",
- "channel_type": 36,
- "input": 1.5,
- "output": 7.5
- },
- {
- "model": "sd3",
- "type": "tokens",
- "channel_type": 37,
- "input": 32.5,
- "output": 32.5
- },
- {
- "model": "sd3-turbo",
- "type": "tokens",
- "channel_type": 37,
- "input": 20,
- "output": 20
- },
- {
- "model": "stable-image-core",
- "type": "tokens",
- "channel_type": 37,
- "input": 15,
- "output": 15
- },
- {
- "model": "hunyuan-lite",
- "type": "tokens",
- "channel_type": 40,
- "input": 0,
- "output": 0
- },
- {
- "model": "hunyuan-standard",
- "type": "tokens",
- "channel_type": 40,
- "input": 0.3214,
- "output": 0.3571
- },
- {
- "model": "hunyuan-standard-256k",
- "type": "tokens",
- "channel_type": 40,
- "input": 1.0714,
- "output": 4.2857
- },
- {
- "model": "hunyuan-pro",
- "type": "tokens",
- "channel_type": 37,
- "input": 2.1429,
- "output": 7.1429
- },
- {
- "model": "coze-*",
- "type": "times",
- "channel_type": 38,
- "input": 0,
- "output": 0
- }
-]
\ No newline at end of file
+[{"model":"dall-e-2","type":"tokens","channel_type":1,"input":8,"output":8},{"model":"babbage-002","type":"tokens","channel_type":1,"input":0.2,"output":0.2},{"model":"babbage-002","type":"tokens","channel_type":1,"input":0.2,"output":0.2},{"model":"tts-1-hd-1106","type":"tokens","channel_type":1,"input":15,"output":15},{"model":"gpt-3.5-turbo-instruct-0914","type":"tokens","channel_type":1,"input":0.75,"output":1},{"model":"mj_reroll","type":"times","channel_type":34,"input":50,"output":50},{"model":"Baichuan2-Turbo-192k","type":"tokens","channel_type":26,"input":1.143,"output":1.143},{"model":"Baichuan-Text-Embedding","type":"tokens","channel_type":26,"input":0.0355,"output":0.0355},{"model":"gemma-7b-it","type":"times","channel_type":31,"input":0,"output":0},{"model":"gpt-4-1106-vision-preview","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"ChatStd","type":"tokens","channel_type":23,"input":0.7145,"output":0.7145},{"model":"@hf/thebloke/llama-2-13b-chat-awq","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"glm-4-plus","type":"tokens","channel_type":16,"input":3.5715,"output":3.5715},{"model":"dall-e-3","type":"tokens","channel_type":1,"input":20,"output":20},{"model":"gpt-3.5-turbo-0125","type":"tokens","channel_type":1,"input":0.25,"output":0.75},{"model":"claude-3-haiku-20240307","type":"tokens","channel_type":14,"input":0.125,"output":0.625},{"model":"claude-2.1","type":"tokens","channel_type":14,"input":4,"output":12},{"model":"o1-mini-2024-09-12","type":"tokens","channel_type":1,"input":1.5,"output":6},{"model":"text-embedding-ada-002","type":"tokens","channel_type":1,"input":0.05,"output":0.05},{"model":"coze-*","type":"times","channel_type":38,"input":0,"output":0},{"model":"gemini-1.5-pro-exp-0801","type":"tokens","channel_type":25,"input":1.75,"output":5.25},{"model":"stable-image-core","type":"tokens","channel_type":37,"input":15,"output":15},{"model":"moonshot-v1-auto","type":"tokens","channel_type":29,"input":4.2855,"output":4.2855},{"model":"gpt-4o-mini","type":"tokens","channel_type":1,"input":0.075,"output":0.3},{"model":"moonshot-v1-32k","type":"tokens","channel_type":29,"input":1.7145,"output":1.7145},{"model":"text-moderation-latest","type":"tokens","channel_type":1,"input":0.1,"output":0.1},{"model":"yi-large-rag","type":"tokens","channel_type":33,"input":1.7855,"output":1.7855},{"model":"gpt-4o-2024-08-06","type":"tokens","channel_type":1,"input":1.25,"output":5},{"model":"tts-1","type":"tokens","channel_type":1,"input":7.5,"output":7.5},{"model":"embedding-2","type":"tokens","channel_type":16,"input":0.0355,"output":0.0355},{"model":"ERNIE-3.5-8K","type":"tokens","channel_type":15,"input":0.857,"output":0.857},{"model":"abab5.5-chat","type":"tokens","channel_type":27,"input":1.0715,"output":1.0715},{"model":"embo-01","type":"tokens","channel_type":27,"input":0.0355,"output":0.0355},{"model":"sd3","type":"tokens","channel_type":37,"input":32.5,"output":32.5},{"model":"qwen-max-longcontext","type":"tokens","channel_type":17,"input":2.857,"output":8.5715},{"model":"glm-4-air","type":"tokens","channel_type":16,"input":0.0715,"output":0.0715},{"model":"gpt-3.5-turbo-0613","type":"tokens","channel_type":1,"input":0.75,"output":1},{"model":"gpt-4-0314","type":"tokens","channel_type":1,"input":15,"output":30},{"model":"gemini-1.5-pro-001","type":"tokens","channel_type":25,"input":1.75,"output":5.25},{"model":"abab6-chat","type":"tokens","channel_type":27,"input":7.143,"output":7.143},{"model":"@cf/meta/llama-3-8b-instruct","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"gpt-4-0613","type":"tokens","channel_type":1,"input":15,"output":30},{"model":"@hf/meta-llama/meta-llama-3-8b-instruct","type":"times","channel_type":35,"input":0.5,"output":0.5},{"model":"mj_describe","type":"times","channel_type":34,"input":25,"output":25},{"model":"llama-guard-3-8b","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"gpt-4-turbo","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"gpt-4-32k","type":"tokens","channel_type":1,"input":30,"output":60},{"model":"gpt-4o-audio-preview-2024-10-01","type":"tokens","channel_type":1,"input":1.25,"output":5},{"model":"llama3-groq-8b-8192-tool-use-preview","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"gpt-3.5-turbo-1106","type":"tokens","channel_type":1,"input":0.5,"output":1},{"model":"llava-v1.5-7b-4096-preview","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"gemini-1.0-pro-vision-latest","type":"tokens","channel_type":25,"input":0.25,"output":0.75},{"model":"@cf/bytedance/stable-diffusion-xl-lightning","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"gpt-3.5-turbo-16k","type":"tokens","channel_type":1,"input":1.5,"output":2},{"model":"mistral-medium-latest","type":"tokens","channel_type":30,"input":1.35,"output":4.05},{"model":"@cf/stabilityai/stable-diffusion-xl-base-1.0","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"whisper-1","type":"tokens","channel_type":1,"input":15,"output":15},{"model":"mj_blend","type":"times","channel_type":34,"input":50,"output":50},{"model":"gpt-4-32k-0314","type":"tokens","channel_type":1,"input":30,"output":60},{"model":"text-embedding-3-large","type":"tokens","channel_type":1,"input":0.065,"output":0.065},{"model":"gemini-ultra","type":"tokens","channel_type":25,"input":1,"output":1},{"model":"qwen-vl-max","type":"tokens","channel_type":17,"input":1.4285,"output":1.4285},{"model":"claude-3-5-sonnet-latest","type":"tokens","channel_type":14,"input":1.5,"output":7.5},{"model":"mj_variation","type":"times","channel_type":34,"input":50,"output":50},{"model":"open-mixtral-8x7b","type":"tokens","channel_type":30,"input":0.35,"output":0.35},{"model":"abab6.5s-chat","type":"tokens","channel_type":27,"input":0.7145,"output":0.7145},{"model":"mj_inpaint","type":"times","channel_type":34,"input":0,"output":0},{"model":"ERNIE-Tiny-8K","type":"tokens","channel_type":15,"input":0,"output":0},{"model":"gemini-1.0-pro","type":"tokens","channel_type":25,"input":0.25,"output":0.75},{"model":"gemini-1.5-pro","type":"tokens","channel_type":25,"input":1.75,"output":5.25},{"model":"semantic_similarity_s1_v1","type":"tokens","channel_type":19,"input":0.0715,"output":0.0715},{"model":"Baichuan2-Turbo","type":"tokens","channel_type":26,"input":0.5715,"output":0.5715},{"model":"llama3-8b-8192","type":"times","channel_type":31,"input":0,"output":0},{"model":"o1-preview-2024-09-12","type":"tokens","channel_type":1,"input":7.5,"output":30},{"model":"glm-4v","type":"tokens","channel_type":16,"input":7.143,"output":7.143},{"model":"o1-mini","type":"tokens","channel_type":1,"input":1.5,"output":6},{"model":"hunyuan-standard","type":"tokens","channel_type":40,"input":0.3215,"output":0.357},{"model":"yi-vision","type":"tokens","channel_type":33,"input":0.4285,"output":0.4285},{"model":"gemini-pro","type":"tokens","channel_type":25,"input":0.25,"output":0.75},{"model":"embedding-bert-512-v1","type":"tokens","channel_type":19,"input":0.0715,"output":0.0715},{"model":"mj_zoom","type":"times","channel_type":34,"input":50,"output":50},{"model":"@cf/meta/llama-3.1-8b-instruct","type":"times","channel_type":35,"input":0.5,"output":0.5},{"model":"@hf/google/gemma-7b-it","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"mixtral-8x7b-32768","type":"times","channel_type":31,"input":0,"output":0},{"model":"glm-4","type":"tokens","channel_type":16,"input":7.143,"output":7.143},{"model":"gpt-4o-realtime-preview","type":"tokens","channel_type":1,"input":2.5,"output":10},{"model":"claude-3-5-sonnet-20241022","type":"tokens","channel_type":14,"input":1.5,"output":7.5},{"model":"claude-3-5-haiku-20241022","type":"tokens","channel_type":14,"input":0.5,"output":2.5},{"model":"abab5.5s-chat","type":"tokens","channel_type":27,"input":0.357,"output":0.357},{"model":"mj_pan","type":"times","channel_type":34,"input":50,"output":50},{"model":"gpt-4o-2024-11-20","type":"tokens","channel_type":1,"input":2.5,"output":7.5},{"model":"qwen-plus","type":"tokens","channel_type":17,"input":0.057,"output":0.143},{"model":"SparkDesk-v3.1","type":"tokens","channel_type":18,"input":2.143,"output":2.143},{"model":"gpt-3.5-turbo-16k-0613","type":"tokens","channel_type":1,"input":1.5,"output":2},{"model":"SparkDesk-v4.0","type":"tokens","channel_type":18,"input":7.143,"output":7.143},{"model":"tts-1-1106","type":"tokens","channel_type":1,"input":7.5,"output":7.5},{"model":"chatgpt-4o-latest","type":"tokens","channel_type":1,"input":2.5,"output":7.5},{"model":"tts-1-hd","type":"tokens","channel_type":1,"input":15,"output":15},{"model":"qwen-turbo","type":"tokens","channel_type":17,"input":0.0215,"output":0.043},{"model":"ChatPro","type":"tokens","channel_type":23,"input":7.143,"output":7.143},{"model":"claude-3-5-sonnet-20240620","type":"tokens","channel_type":14,"input":1.5,"output":7.5},{"model":"text-embedding-v1","type":"tokens","channel_type":17,"input":0.05,"output":0.05},{"model":"Baichuan2-53B","type":"tokens","channel_type":26,"input":1.4285,"output":1.4285},{"model":"dall-e-2","type":"tokens","channel_type":1,"input":8,"output":8},{"model":"abab6.5-chat","type":"tokens","channel_type":27,"input":2.143,"output":2.143},{"model":"gemini-1.5-pro-latest","type":"tokens","channel_type":25,"input":1.75,"output":5.25},{"model":"ERNIE-4.0","type":"tokens","channel_type":15,"input":8.572,"output":8.572},{"model":"@cf/openai/whisper","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"open-mixtral-8x22b","type":"tokens","channel_type":30,"input":4,"output":12},{"model":"ERNIE-Speed-128K","type":"tokens","channel_type":15,"input":0,"output":0},{"model":"gpt-4o-realtime-preview-2024-10-01","type":"tokens","channel_type":1,"input":2.5,"output":10},{"model":"gpt-3.5-turbo-instruct","type":"tokens","channel_type":1,"input":0.75,"output":1},{"model":"yi-medium-200k","type":"tokens","channel_type":33,"input":0.857,"output":0.857},{"model":"gemini-pro-vision","type":"tokens","channel_type":25,"input":0.25,"output":0.75},{"model":"yi-large-turbo","type":"tokens","channel_type":33,"input":0.857,"output":0.857},{"model":"qwen-max","type":"tokens","channel_type":17,"input":1.4285,"output":4.2855},{"model":"@cf/qwen/qwen1.5-14b-chat-awq","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"o1-preview","type":"tokens","channel_type":1,"input":7.5,"output":30},{"model":"yi-medium","type":"tokens","channel_type":33,"input":0.1785,"output":0.1785},{"model":"command-r","type":"tokens","channel_type":36,"input":0.25,"output":0.75},{"model":"gpt-4-preview","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"gemini-1.0-pro-latest","type":"tokens","channel_type":25,"input":0.25,"output":0.75},{"model":"hunyuan","type":"tokens","channel_type":23,"input":7.143,"output":7.143},{"model":"ERNIE-Bot-4","type":"tokens","channel_type":15,"input":8.572,"output":8.572},{"model":"Embedding-V1","type":"tokens","channel_type":15,"input":0.143,"output":0.143},{"model":"SparkDesk-v3.5","type":"tokens","channel_type":18,"input":2.143,"output":2.143},{"model":"claude-2.0","type":"tokens","channel_type":14,"input":4,"output":12},{"model":"PaLM-2","type":"tokens","channel_type":11,"input":1,"output":1},{"model":"embedding_s1_v1","type":"tokens","channel_type":19,"input":0.0715,"output":0.0715},{"model":"gemma2-9b-it","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"gemini-1.5-flash-8b-exp-0827","type":"tokens","channel_type":25,"input":0.0375,"output":0.15},{"model":"gpt-4o-mini-2024-07-18","type":"tokens","channel_type":1,"input":0.075,"output":0.3},{"model":"BLOOMZ-7B","type":"tokens","channel_type":15,"input":0.2855,"output":0.2855},{"model":"moonshot-v1-128k","type":"tokens","channel_type":29,"input":4.2855,"output":4.2855},{"model":"gpt-4o-audio-preview","type":"tokens","channel_type":1,"input":1.25,"output":5},{"model":"gpt-4-vision-preview","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"gpt-4-turbo-preview","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"mistral-embed","type":"tokens","channel_type":30,"input":0.05,"output":0.05},{"model":"yi-spark","type":"tokens","channel_type":33,"input":0.0715,"output":0.0715},{"model":"mistral-small-latest","type":"tokens","channel_type":30,"input":1,"output":3},{"model":"ERNIE-Functions-8K","type":"tokens","channel_type":15,"input":0.2855,"output":0.5715},{"model":"cogview-3","type":"tokens","channel_type":16,"input":17.857,"output":17.857},{"model":"gpt-3.5-turbo","type":"tokens","channel_type":1,"input":0.25,"output":0.75},{"model":"glm-4-long","type":"tokens","channel_type":16,"input":0.0715,"output":0.0715},{"model":"text-moderation-stable","type":"tokens","channel_type":1,"input":0.1,"output":0.1},{"model":"@cf/lykon/dreamshaper-8-lcm","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"deepseek-coder","type":"tokens","channel_type":28,"input":0.0715,"output":0.143},{"model":"yi-large-fc","type":"tokens","channel_type":33,"input":1.4285,"output":1.4285},{"model":"claude-3-opus-20240229","type":"tokens","channel_type":14,"input":7.5,"output":37.5},{"model":"glm-4-0520","type":"tokens","channel_type":16,"input":7.143,"output":7.143},{"model":"SparkDesk-v1.1","type":"tokens","channel_type":18,"input":0,"output":0},{"model":"mj_custom_zoom","type":"times","channel_type":34,"input":0,"output":0},{"model":"llama-3.1-8b-instant","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"llama2-70b-4096","type":"times","channel_type":31,"input":0,"output":0},{"model":"gpt-4","type":"tokens","channel_type":1,"input":15,"output":30},{"model":"yi-large","type":"tokens","channel_type":33,"input":1.4285,"output":1.4285},{"model":"glm-4-alltools","type":"tokens","channel_type":16,"input":7.143,"output":7.143},{"model":"davinci-002","type":"tokens","channel_type":1,"input":1,"output":1},{"model":"ERNIE-Speed","type":"tokens","channel_type":15,"input":0,"output":0},{"model":"gpt-4-1106-preview","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"gemini-1.0-pro-001","type":"tokens","channel_type":25,"input":0.25,"output":0.75},{"model":"whisper-large-v3","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"hunyuan-standard-256k","type":"tokens","channel_type":40,"input":1.0715,"output":4.2855},{"model":"mj_low_variation","type":"times","channel_type":34,"input":50,"output":50},{"model":"gpt-3.5-turbo-0301","type":"tokens","channel_type":1,"input":0.75,"output":1},{"model":"mj_upscale","type":"times","channel_type":34,"input":25,"output":25},{"model":"mj_shorten","type":"times","channel_type":34,"input":50,"output":50},{"model":"deepseek-chat","type":"tokens","channel_type":28,"input":0.0715,"output":0.143},{"model":"gpt-4-0125-preview","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"yi-34b-chat-0205","type":"tokens","channel_type":33,"input":0.1785,"output":0.1785},{"model":"hunyuan-pro","type":"tokens","channel_type":37,"input":2.143,"output":7.143},{"model":"hunyuan-lite","type":"tokens","channel_type":40,"input":0,"output":0},{"model":"claude-instant-1.2","type":"tokens","channel_type":14,"input":0.4,"output":1.2},{"model":"mj_high_variation","type":"times","channel_type":34,"input":50,"output":50},{"model":"ERNIE-Lite-8K","type":"tokens","channel_type":15,"input":0,"output":0},{"model":"text-embedding-3-small","type":"tokens","channel_type":1,"input":0.01,"output":0.01},{"model":"open-mistral-7b","type":"tokens","channel_type":30,"input":0.125,"output":0.125},{"model":"swap_face","type":"times","channel_type":34,"input":25,"output":25},{"model":"llama-3.1-70b-versatile","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"gpt-4-turbo-2024-04-09","type":"tokens","channel_type":1,"input":5,"output":15},{"model":"@hf/thebloke/deepseek-coder-6.7b-base-awq","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"glm-4-airx","type":"tokens","channel_type":16,"input":0.7145,"output":0.7145},{"model":"@cf/deepseek-ai/deepseek-r1-distill-qwen-32b","type":"tokens","channel_type":35,"input":0.25,"output":2.44},{"model":"gemini-1.5-flash-latest","type":"tokens","channel_type":25,"input":0.0375,"output":0.15},{"model":"ERNIE-Bot-turbo","type":"tokens","channel_type":15,"input":0,"output":0},{"model":"glm-4-flash","type":"tokens","channel_type":16,"input":0,"output":0},{"model":"command-r-plus","type":"tokens","channel_type":36,"input":1.5,"output":7.5},{"model":"gemini-1.0-ultra-latest","type":"tokens","channel_type":25,"input":1,"output":1},{"model":"gemini-1.5-pro-exp-0827","type":"tokens","channel_type":25,"input":1.75,"output":5.25},{"model":"moonshot-v1-8k","type":"tokens","channel_type":29,"input":0.857,"output":0.857},{"model":"gpt-4o-2024-05-13","type":"tokens","channel_type":1,"input":2.5,"output":7.5},{"model":"llama3-70b-8192","type":"times","channel_type":31,"input":0,"output":0},{"model":"qwen-long","type":"tokens","channel_type":17,"input":0.0355,"output":0.143},{"model":"gpt-4-32k-0613","type":"tokens","channel_type":1,"input":30,"output":60},{"model":"360GPT_S2_V9","type":"tokens","channel_type":19,"input":0.857,"output":0.857},{"model":"sd3-turbo","type":"tokens","channel_type":37,"input":20,"output":20},{"model":"gpt-4o","type":"tokens","channel_type":1,"input":2.5,"output":7.5},{"model":"claude-3-5-haiku-latest","type":"tokens","channel_type":14,"input":0.5,"output":2.5},{"model":"claude-3-sonnet-20240229","type":"tokens","channel_type":14,"input":1.5,"output":7.5},{"model":"@cf/qwen/qwen1.5-7b-chat-awq","type":"tokens","channel_type":35,"input":0,"output":0},{"model":"distil-whisper-large-v3-en","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"qwen-vl-plus","type":"tokens","channel_type":17,"input":0.5715,"output":0.5715},{"model":"gemini-1.5-flash-001","type":"tokens","channel_type":25,"input":0.0375,"output":0.15},{"model":"gemini-1.5-flash","type":"tokens","channel_type":25,"input":0.0375,"output":0.15},{"model":"mj_modal","type":"times","channel_type":34,"input":50,"output":50},{"model":"yi-34b-chat-200k","type":"tokens","channel_type":33,"input":0.857,"output":0.857},{"model":"mj_imagine","type":"times","channel_type":34,"input":50,"output":50},{"model":"ERNIE-Bot","type":"tokens","channel_type":15,"input":0.857,"output":0.857},{"model":"glm-3-turbo","type":"tokens","channel_type":16,"input":0.0715,"output":0.0715},{"model":"yi-vl-plus","type":"tokens","channel_type":33,"input":0.4285,"output":0.4285},{"model":"llama3-groq-70b-8192-tool-use-preview","type":"times","channel_type":31,"input":0.5,"output":0.5},{"model":"yi-lightning","type":"tokens","channel_type":33,"input":0.0705,"output":0.0705},{"model":"mistral-large-latest","type":"tokens","channel_type":30,"input":4,"output":12},{"model":"gemini-1.5-flash-exp-0827","type":"tokens","channel_type":25,"input":0.0375,"output":0.15},{"model":"ERNIE-3.5-8K","type":"tokens","channel_type":15,"input":0.12242857142857143,"output":0.12242857142857143},{"model":"ERNIE-3.5-8K","type":"tokens","channel_type":15,"input":0.05714285714285715,"output":0.12242857142857143},{"model":"ERNIE-3.5-8K","type":"tokens","channel_type":15,"input":0.05714285714285715,"output":0.14285714285714285}]
\ No newline at end of file
diff --git a/scripts/start.sh b/scripts/start.sh
new file mode 100644
index 0000000..1078207
--- /dev/null
+++ b/scripts/start.sh
@@ -0,0 +1,10 @@
+#!/bin/sh
+
+# 启动后端服务
+./main &
+
+# 启动前端服务
+cd /app/frontend && node server.js &
+
+# 启动 nginx
+nginx -g 'daemon off;'
\ No newline at end of file