Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4a68ff6162 | ||
|
|
6b2d78fe56 | ||
|
|
9c604460b1 | ||
|
|
8d34f8d6fe | ||
|
|
6d1d0f3b6b | ||
|
|
24529189d9 | ||
|
|
000162b1b1 | ||
|
|
6662ea5e04 | ||
|
|
5789d50e9e | ||
|
|
ca3d89751d | ||
|
|
2bc857cf88 | ||
|
|
a9ff7e1c94 | ||
|
|
51d4651c6c | ||
|
|
e112f3af12 | ||
|
|
73e53c2333 | ||
|
|
470e49b850 | ||
|
|
d426781e47 | ||
|
|
b80f0759a5 | ||
|
|
b83c6d9786 | ||
|
|
fe0f2a7e88 | ||
|
|
ed42f3ded7 | ||
|
|
54246c542a | ||
|
|
ca305f4199 | ||
|
|
f8e539c9b4 | ||
|
|
e0b531c578 | ||
|
|
afb51b0a94 | ||
|
|
3c4619b98c | ||
|
|
efd38034ac | ||
|
|
d4bf227cd8 | ||
|
|
fca67cae40 | ||
|
|
20c3cda4a7 | ||
|
|
9045130778 | ||
|
|
5403ed0cba | ||
|
|
87778326ea | ||
|
|
a32a9543e2 | ||
|
|
9e0c33ad93 | ||
|
|
c8e565c6a4 | ||
|
|
9ed162ff0c | ||
|
|
b0d68ba338 |
@@ -1,32 +1,23 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"opencatd-open/middleware"
|
||||
"opencatd-open/internal/cli"
|
||||
"opencatd-open/internal/consts"
|
||||
"opencatd-open/pkg/config"
|
||||
"opencatd-open/pkg/store"
|
||||
"opencatd-open/wire"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
"opencatd-open/router"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
//go:embed dist/*
|
||||
var web embed.FS
|
||||
|
||||
func main() {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
|
||||
cfg, err := config.LoadConfig()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -36,160 +27,37 @@ func main() {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get underlying *sql.DB: %v", err)
|
||||
|
||||
rootCmd := &cobra.Command{
|
||||
Use: "openteam",
|
||||
Short: "openteam cli",
|
||||
Long: consts.Logo,
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
router.SetRouter(cfg, db, &web)
|
||||
},
|
||||
}
|
||||
|
||||
team, err := wire.InitTeamHandler(ctx, cfg, db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
rootCmd.AddCommand(cli.LoadCmd)
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
api, err := wire.InitAPIHandler(ctx, cfg, db)
|
||||
func printFilesAndDirs(fsys fs.FS, prefix string) error {
|
||||
return fs.WalkDir(fsys, ".", func(p string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
proxy, err := wire.InitProxyHandler(ctx, cfg, db, &wg)
|
||||
if d.IsDir() {
|
||||
fmt.Printf("%s[DIR] %s\n", prefix, p)
|
||||
} else {
|
||||
info, err := d.Info()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
return err
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
teamGroup := r.Group("/1")
|
||||
teamGroup.Use(team.AuthMiddleware())
|
||||
{
|
||||
teamGroup.POST("/users/init", team.InitAdmin)
|
||||
// 获取当前用户信息
|
||||
teamGroup.GET("/me", team.Me)
|
||||
// team.GET("/me/usages", team.HandleMeUsage)
|
||||
|
||||
teamGroup.POST("/keys", team.CreateKey)
|
||||
teamGroup.GET("/keys", team.ListKeys)
|
||||
teamGroup.POST("/keys/:id", team.UpdateKey)
|
||||
teamGroup.DELETE("/keys/:id", team.DeleteKey)
|
||||
|
||||
teamGroup.POST("/users", team.CreateUser)
|
||||
teamGroup.GET("/users", team.ListUsers)
|
||||
teamGroup.POST("/users/:id/reset", team.ResetUserToken)
|
||||
teamGroup.DELETE("/users/:id", team.DeleteUser)
|
||||
|
||||
teamGroup.GET("/1/usages", team.ListUsages)
|
||||
}
|
||||
|
||||
public := r.Group("/api/auth")
|
||||
{
|
||||
public.GET("/passkey/begin", api.PasskeyAuthBegin)
|
||||
public.POST("/passkey/finish", api.PasskeyAuthFinish)
|
||||
|
||||
public.POST("/register", api.Register)
|
||||
public.POST("/login", api.Login)
|
||||
}
|
||||
|
||||
apiGroup := r.Group("/api", middleware.Auth)
|
||||
{
|
||||
apiGroup.GET("/profile", api.Profile)
|
||||
apiGroup.POST("/profile/update", api.UpdateProfile)
|
||||
apiGroup.POST("/profile/update/password", api.UpdatePassword)
|
||||
// 绑定PassKey
|
||||
apiGroup.GET("/profile/passkey", api.PasskeyCreateBegin)
|
||||
apiGroup.POST("/profile/passkey", api.PasskeyCreateFinish)
|
||||
apiGroup.GET("/profile/passkeys", api.ListPasskey)
|
||||
apiGroup.DELETE("/profile/passkeys/:id", api.DeletePasskey)
|
||||
|
||||
userGroup := apiGroup.Group("/users")
|
||||
{
|
||||
userGroup.POST("", api.CreateUser)
|
||||
userGroup.GET("", api.ListUser)
|
||||
userGroup.GET("/:id", api.GetUser)
|
||||
userGroup.PUT("/:id", api.EditUser)
|
||||
userGroup.DELETE("/:id", api.DeleteUser)
|
||||
userGroup.POST("/batch/:option", api.UserOption)
|
||||
}
|
||||
|
||||
tokenGroup := apiGroup.Group("/tokens")
|
||||
tokenGroup.POST("", api.CreateToken)
|
||||
tokenGroup.GET("", api.ListToken)
|
||||
// tokenGroup.GET("/:id", api.GetToken)
|
||||
tokenGroup.POST("/reset/:id", api.ResetToken)
|
||||
tokenGroup.PUT("/:id", api.UpdateToken)
|
||||
tokenGroup.DELETE("/:id", api.DeleteToken)
|
||||
// tokenGroup.POST("/batch/:option", api.TokenOption)
|
||||
|
||||
apiGroup.POST("keys", api.CreateApiKey)
|
||||
apiGroup.GET("keys", api.ListApiKey)
|
||||
apiGroup.GET("keys/:id", api.GetApiKey)
|
||||
apiGroup.PUT("keys/:id", api.UpdateApiKey)
|
||||
apiGroup.DELETE("keys/:id", api.DeleteApiKey)
|
||||
apiGroup.POST("keys/batch/:option", api.ApiKeyOption)
|
||||
fmt.Printf("%s[FILE] %s (%d bytes)\n", prefix, p, info.Size())
|
||||
}
|
||||
|
||||
v1 := r.Group("/v1")
|
||||
v1.Use(middleware.AuthLLM(db))
|
||||
{
|
||||
// v1.POST("/v2/*proxypath", router.HandleProxy)
|
||||
v1.POST("/v1/*proxypath", proxy.HandleProxy)
|
||||
// v1.GET("/models", dashboard.HandleModels)
|
||||
}
|
||||
|
||||
idxFS, err := fs.Sub(web, "dist")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.GET("/", gin.WrapH(http.FileServer(http.FS(idxFS))))
|
||||
assetsFS, err := fs.Sub(web, "dist/assets")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.GET("/assets/*filepath", gin.WrapH(http.StripPrefix("/assets/", http.FileServer(http.FS(assetsFS)))))
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: ":8080",
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
// 服务启动
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号来优雅地关闭服务器
|
||||
quit := make(chan os.Signal, 1)
|
||||
// kill (no param) default send syscall.SIGTERM
|
||||
// kill -2 is syscall.SIGINT
|
||||
// kill -9 is syscall.SIGKILL but can't be catch
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
fmt.Println("\nShutdown Server ...")
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Fatalln("Server Shutdown:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
sqlDB.Close()
|
||||
|
||||
waitChan := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(waitChan)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitChan:
|
||||
fmt.Println("All goroutines have finished")
|
||||
case <-shutdownCtx.Done():
|
||||
fmt.Println("⚠️ Shutdown timeout")
|
||||
}
|
||||
|
||||
fmt.Println("Server exited")
|
||||
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,11 +2,12 @@ FROM node:20-alpine AS frontend
|
||||
WORKDIR /frontend-build
|
||||
COPY ./frontend .
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com && npm install -g pnpm --registry=https://registry.npmmirror.com && pnpm i && pnpm build
|
||||
RUN npm install -g pnpm && pnpm i && pnpm build
|
||||
|
||||
FROM golang:1.23-alpine as backend
|
||||
FROM golang:1.23-alpine AS backend
|
||||
LABEL anther="github.com/Sakurasan"
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk --no-cache add make cmake upx
|
||||
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk --no-cache add make cmake upx
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
COPY --from=frontend /frontend-build/dist /build/cmd/openteam/dist
|
||||
@@ -17,12 +18,13 @@ RUN make build
|
||||
|
||||
FROM alpine:latest AS runner
|
||||
# 设置alpine 时间为上海时间
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && apk update && apk --no-cache add tzdata ffmpeg && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
# RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk update && apk --no-cache add tzdata ffmpeg && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone
|
||||
# RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils
|
||||
WORKDIR /app
|
||||
COPY --from=backend /build/bin/openteam /app/openteam
|
||||
ENV GIN_MODE=release
|
||||
ENV PATH=$PATH:/app
|
||||
EXPOSE 80
|
||||
EXPOSE 80 443
|
||||
ENTRYPOINT ["/app/openteam"]
|
||||
@@ -0,0 +1,30 @@
|
||||
FROM node:20-alpine AS frontend
|
||||
WORKDIR /frontend-build
|
||||
COPY ./frontend .
|
||||
|
||||
RUN npm config set registry https://registry.npmmirror.com && npm install -g pnpm --registry=https://registry.npmmirror.com && pnpm i && pnpm build
|
||||
|
||||
FROM golang:1.23-alpine AS backend
|
||||
LABEL anther="github.com/Sakurasan"
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk --no-cache add make cmake upx
|
||||
WORKDIR /build
|
||||
COPY . .
|
||||
COPY --from=frontend /frontend-build/dist /build/cmd/openteam/dist
|
||||
ENV GO111MODULE=on
|
||||
ENV GOPROXY=https://goproxy.cn,direct
|
||||
CMD [ "go mod tidy","go mod download" ]
|
||||
RUN make build
|
||||
|
||||
FROM alpine:latest AS runner
|
||||
# 设置alpine 时间为上海时间
|
||||
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
|
||||
RUN apk update && apk --no-cache add tzdata ffmpeg && cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
|
||||
&& echo "Asia/Shanghai" > /etc/timezone
|
||||
# RUN apk update && apk --no-cache add openssl libgcc libstdc++ binutils
|
||||
WORKDIR /app
|
||||
COPY --from=backend /build/bin/openteam /app/openteam
|
||||
ENV GIN_MODE=release
|
||||
ENV PATH=$PATH:/app
|
||||
EXPOSE 80 443
|
||||
ENTRYPOINT ["/app/openteam"]
|
||||
@@ -0,0 +1,8 @@
|
||||
version: '3.9'
|
||||
|
||||
services:
|
||||
adminer:
|
||||
image: adminer
|
||||
restart: always
|
||||
ports:
|
||||
- 8080:8080
|
||||
@@ -4,6 +4,7 @@ services:
|
||||
mariadb:
|
||||
image: mariadb
|
||||
container_name: mysql
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "3306:3306"
|
||||
volumes:
|
||||
@@ -18,8 +19,3 @@ services:
|
||||
MYSQL_USER: openteam
|
||||
MYSQL_PASSWORD: openteam
|
||||
|
||||
# adminer:
|
||||
# image: adminer
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
|
||||
@@ -20,8 +20,3 @@ services:
|
||||
volumes:
|
||||
- $PWD/pgdata:/var/lib/postgresql/data
|
||||
|
||||
# adminer:
|
||||
# image: adminer
|
||||
# restart: always
|
||||
# ports:
|
||||
# - 8080:8080
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
version: '3.7'
|
||||
services:
|
||||
sqlite-web:
|
||||
image: vaalacat/sqlite-web
|
||||
ports:
|
||||
- 8800:8080
|
||||
volumes:
|
||||
- $PWD/db:/data
|
||||
environment:
|
||||
- SQLITE_DATABASE=openteam.db
|
||||
@@ -14,6 +14,7 @@
|
||||
"axios": "^1.8.4",
|
||||
"element-plus": "^2.9.7",
|
||||
"lucide-vue-next": "^0.479.0",
|
||||
"qrcode.vue": "^3.6.0",
|
||||
"vue": "^3.5.13",
|
||||
"vue-router": "^4.5.0"
|
||||
},
|
||||
|
||||
@@ -23,6 +23,9 @@ importers:
|
||||
lucide-vue-next:
|
||||
specifier: ^0.479.0
|
||||
version: 0.479.0(vue@3.5.13)
|
||||
qrcode.vue:
|
||||
specifier: ^3.6.0
|
||||
version: 3.6.0(vue@3.5.13)
|
||||
vue:
|
||||
specifier: ^3.5.13
|
||||
version: 3.5.13
|
||||
@@ -556,6 +559,10 @@ packages:
|
||||
resolution: {integrity: sha512-QOSvevhslijgYwRx6Rv7zKdMF8lbRmx+uQGx2+vDc+KI/eBnsy9kit5aj23AgGu3pa4t9AgwbnXWqS+iOY+2aA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
camelcase@5.3.1:
|
||||
resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
caniuse-lite@1.0.30001714:
|
||||
resolution: {integrity: sha512-mtgapdwDLSSBnCI3JokHM7oEQBLxiJKVRtg10AxM1AyeiKcM96f0Mkbqeq+1AbiCtvMcHRulAAEMu693JrSWqg==}
|
||||
|
||||
@@ -563,6 +570,9 @@ packages:
|
||||
resolution: {integrity: sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==}
|
||||
engines: {node: '>= 8.10.0'}
|
||||
|
||||
cliui@6.0.0:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
color-convert@2.0.1:
|
||||
resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==}
|
||||
engines: {node: '>=7.0.0'}
|
||||
@@ -604,6 +614,10 @@ packages:
|
||||
dayjs@1.11.13:
|
||||
resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==}
|
||||
|
||||
decamelize@1.2.0:
|
||||
resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
delayed-stream@1.0.0:
|
||||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
@@ -611,6 +625,9 @@ packages:
|
||||
didyoumean@1.2.2:
|
||||
resolution: {integrity: sha512-gxtyfqMg7GKyhQmb056K7M3xszy/myH8w+B4RT+QXBQsvAOdc3XymqDDPHx1BgPgsdAA5SIifona89YtRATDzw==}
|
||||
|
||||
dijkstrajs@1.0.3:
|
||||
resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
|
||||
|
||||
dlv@1.1.3:
|
||||
resolution: {integrity: sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==}
|
||||
|
||||
@@ -692,6 +709,10 @@ packages:
|
||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
find-up@4.1.0:
|
||||
resolution: {integrity: sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
follow-redirects@1.15.9:
|
||||
resolution: {integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==}
|
||||
engines: {node: '>=4.0'}
|
||||
@@ -720,6 +741,10 @@ packages:
|
||||
function-bind@1.1.2:
|
||||
resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==}
|
||||
|
||||
get-caller-file@2.0.5:
|
||||
resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==}
|
||||
engines: {node: 6.* || 8.* || >= 10.*}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -797,6 +822,10 @@ packages:
|
||||
lines-and-columns@1.2.4:
|
||||
resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==}
|
||||
|
||||
locate-path@5.0.0:
|
||||
resolution: {integrity: sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
lodash-es@4.17.21:
|
||||
resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==}
|
||||
|
||||
@@ -882,9 +911,25 @@ packages:
|
||||
resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
p-limit@2.3.0:
|
||||
resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
p-locate@4.1.0:
|
||||
resolution: {integrity: sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
p-try@2.2.0:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
package-json-from-dist@1.0.1:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
path-exists@4.0.0:
|
||||
resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
path-key@3.1.1:
|
||||
resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -924,6 +969,10 @@ packages:
|
||||
resolution: {integrity: sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==}
|
||||
engines: {node: '>= 6'}
|
||||
|
||||
pngjs@5.0.0:
|
||||
resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
|
||||
postcss-import@15.1.0:
|
||||
resolution: {integrity: sha512-hpr+J05B2FVYUAXHeK1YyI267J/dDDhMU6B6civm8hSY1jYJnBXxzKDKDswzJmtLHryrjhnDjqqp/49t8FALew==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@@ -968,6 +1017,16 @@ packages:
|
||||
proxy-from-env@1.1.0:
|
||||
resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==}
|
||||
|
||||
qrcode.vue@3.6.0:
|
||||
resolution: {integrity: sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==}
|
||||
peerDependencies:
|
||||
vue: ^3.0.0
|
||||
|
||||
qrcode@1.5.4:
|
||||
resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
|
||||
engines: {node: '>=10.13.0'}
|
||||
hasBin: true
|
||||
|
||||
queue-microtask@1.2.3:
|
||||
resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==}
|
||||
|
||||
@@ -978,6 +1037,13 @@ packages:
|
||||
resolution: {integrity: sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==}
|
||||
engines: {node: '>=8.10.0'}
|
||||
|
||||
require-directory@2.1.1:
|
||||
resolution: {integrity: sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
require-main-filename@2.0.0:
|
||||
resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
|
||||
|
||||
resolve@1.22.10:
|
||||
resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -995,6 +1061,9 @@ packages:
|
||||
run-parallel@1.2.0:
|
||||
resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==}
|
||||
|
||||
set-blocking@2.0.0:
|
||||
resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -1132,11 +1201,18 @@ packages:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
which-module@2.0.1:
|
||||
resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
|
||||
|
||||
which@2.0.2:
|
||||
resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==}
|
||||
engines: {node: '>= 8'}
|
||||
hasBin: true
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
|
||||
engines: {node: '>=10'}
|
||||
@@ -1145,11 +1221,22 @@ packages:
|
||||
resolution: {integrity: sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
y18n@4.0.3:
|
||||
resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
|
||||
|
||||
yaml@2.7.1:
|
||||
resolution: {integrity: sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==}
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
yargs@15.4.1:
|
||||
resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
snapshots:
|
||||
|
||||
'@alloc/quick-lru@5.2.0': {}
|
||||
@@ -1540,6 +1627,8 @@ snapshots:
|
||||
|
||||
camelcase-css@2.0.1: {}
|
||||
|
||||
camelcase@5.3.1: {}
|
||||
|
||||
caniuse-lite@1.0.30001714: {}
|
||||
|
||||
chokidar@3.6.0:
|
||||
@@ -1554,6 +1643,12 @@ snapshots:
|
||||
optionalDependencies:
|
||||
fsevents: 2.3.3
|
||||
|
||||
cliui@6.0.0:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
wrap-ansi: 6.2.0
|
||||
|
||||
color-convert@2.0.1:
|
||||
dependencies:
|
||||
color-name: 1.1.4
|
||||
@@ -1594,10 +1689,14 @@ snapshots:
|
||||
|
||||
dayjs@1.11.13: {}
|
||||
|
||||
decamelize@1.2.0: {}
|
||||
|
||||
delayed-stream@1.0.0: {}
|
||||
|
||||
didyoumean@1.2.2: {}
|
||||
|
||||
dijkstrajs@1.0.3: {}
|
||||
|
||||
dlv@1.1.3: {}
|
||||
|
||||
dunder-proto@1.0.1:
|
||||
@@ -1708,6 +1807,11 @@ snapshots:
|
||||
dependencies:
|
||||
to-regex-range: 5.0.1
|
||||
|
||||
find-up@4.1.0:
|
||||
dependencies:
|
||||
locate-path: 5.0.0
|
||||
path-exists: 4.0.0
|
||||
|
||||
follow-redirects@1.15.9: {}
|
||||
|
||||
foreground-child@3.3.1:
|
||||
@@ -1729,6 +1833,8 @@ snapshots:
|
||||
|
||||
function-bind@1.1.2: {}
|
||||
|
||||
get-caller-file@2.0.5: {}
|
||||
|
||||
get-intrinsic@1.3.0:
|
||||
dependencies:
|
||||
call-bind-apply-helpers: 1.0.2
|
||||
@@ -1808,6 +1914,10 @@ snapshots:
|
||||
|
||||
lines-and-columns@1.2.4: {}
|
||||
|
||||
locate-path@5.0.0:
|
||||
dependencies:
|
||||
p-locate: 4.1.0
|
||||
|
||||
lodash-es@4.17.21: {}
|
||||
|
||||
lodash-unified@1.0.3(@types/lodash-es@4.17.12)(lodash-es@4.17.21)(lodash@4.17.21):
|
||||
@@ -1871,8 +1981,20 @@ snapshots:
|
||||
|
||||
object-hash@3.0.0: {}
|
||||
|
||||
p-limit@2.3.0:
|
||||
dependencies:
|
||||
p-try: 2.2.0
|
||||
|
||||
p-locate@4.1.0:
|
||||
dependencies:
|
||||
p-limit: 2.3.0
|
||||
|
||||
p-try@2.2.0: {}
|
||||
|
||||
package-json-from-dist@1.0.1: {}
|
||||
|
||||
path-exists@4.0.0: {}
|
||||
|
||||
path-key@3.1.1: {}
|
||||
|
||||
path-parse@1.0.7: {}
|
||||
@@ -1900,6 +2022,8 @@ snapshots:
|
||||
|
||||
pirates@4.0.7: {}
|
||||
|
||||
pngjs@5.0.0: {}
|
||||
|
||||
postcss-import@15.1.0(postcss@8.5.3):
|
||||
dependencies:
|
||||
postcss: 8.5.3
|
||||
@@ -1939,6 +2063,16 @@ snapshots:
|
||||
|
||||
proxy-from-env@1.1.0: {}
|
||||
|
||||
qrcode.vue@3.6.0(vue@3.5.13):
|
||||
dependencies:
|
||||
vue: 3.5.13
|
||||
|
||||
qrcode@1.5.4:
|
||||
dependencies:
|
||||
dijkstrajs: 1.0.3
|
||||
pngjs: 5.0.0
|
||||
yargs: 15.4.1
|
||||
|
||||
queue-microtask@1.2.3: {}
|
||||
|
||||
read-cache@1.0.0:
|
||||
@@ -1949,6 +2083,10 @@ snapshots:
|
||||
dependencies:
|
||||
picomatch: 2.3.1
|
||||
|
||||
require-directory@2.1.1: {}
|
||||
|
||||
require-main-filename@2.0.0: {}
|
||||
|
||||
resolve@1.22.10:
|
||||
dependencies:
|
||||
is-core-module: 2.16.1
|
||||
@@ -1987,6 +2125,8 @@ snapshots:
|
||||
dependencies:
|
||||
queue-microtask: 1.2.3
|
||||
|
||||
set-blocking@2.0.0: {}
|
||||
|
||||
shebang-command@2.0.0:
|
||||
dependencies:
|
||||
shebang-regex: 3.0.0
|
||||
@@ -2113,10 +2253,18 @@ snapshots:
|
||||
'@vue/server-renderer': 3.5.13(vue@3.5.13)
|
||||
'@vue/shared': 3.5.13
|
||||
|
||||
which-module@2.0.1: {}
|
||||
|
||||
which@2.0.2:
|
||||
dependencies:
|
||||
isexe: 2.0.0
|
||||
|
||||
wrap-ansi@6.2.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
string-width: 4.2.3
|
||||
strip-ansi: 6.0.1
|
||||
|
||||
wrap-ansi@7.0.0:
|
||||
dependencies:
|
||||
ansi-styles: 4.3.0
|
||||
@@ -2129,4 +2277,25 @@ snapshots:
|
||||
string-width: 5.1.2
|
||||
strip-ansi: 7.1.0
|
||||
|
||||
y18n@4.0.3: {}
|
||||
|
||||
yaml@2.7.1: {}
|
||||
|
||||
yargs-parser@18.1.3:
|
||||
dependencies:
|
||||
camelcase: 5.3.1
|
||||
decamelize: 1.2.0
|
||||
|
||||
yargs@15.4.1:
|
||||
dependencies:
|
||||
cliui: 6.0.0
|
||||
decamelize: 1.2.0
|
||||
find-up: 4.1.0
|
||||
get-caller-file: 2.0.5
|
||||
require-directory: 2.1.1
|
||||
require-main-filename: 2.0.0
|
||||
set-blocking: 2.0.0
|
||||
string-width: 4.2.3
|
||||
which-module: 2.0.1
|
||||
y18n: 4.0.3
|
||||
yargs-parser: 18.1.3
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Anthropic</title><path d="M13.827 3.52h3.603L24 20h-3.603l-6.57-16.48zm-7.258 0h3.767L16.906 20h-3.674l-1.343-3.461H5.017l-1.344 3.46H0L6.57 3.522zm4.132 9.959L8.453 7.687 6.205 13.48H10.7z"></path></svg>
|
||||
|
After Width: | Height: | Size: 368 B |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Azure</title><path d="M7.242 1.613A1.11 1.11 0 018.295.857h6.977L8.03 22.316a1.11 1.11 0 01-1.052.755h-5.43a1.11 1.11 0 01-1.053-1.466L7.242 1.613z" fill="url(#lobe-icons-azure-fill-0)"></path><path d="M18.397 15.296H7.4a.51.51 0 00-.347.882l7.066 6.595c.206.192.477.298.758.298h6.226l-2.706-7.775z" fill="#0078D4"></path><path d="M15.272.857H7.497L0 23.071h7.775l1.596-4.73 5.068 4.73h6.665l-2.707-7.775h-7.998L15.272.857z" fill="url(#lobe-icons-azure-fill-1)"></path><path d="M17.193 1.613a1.11 1.11 0 00-1.052-.756h-7.81.035c.477 0 .9.304 1.052.756l6.748 19.992a1.11 1.11 0 01-1.052 1.466h-.12 7.895a1.11 1.11 0 001.052-1.466L17.193 1.613z" fill="url(#lobe-icons-azure-fill-2)"></path><defs><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-0" x1="8.247" x2="1.002" y1="1.626" y2="23.03"><stop stop-color="#114A8B"></stop><stop offset="1" stop-color="#0669BC"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-1" x1="14.042" x2="12.324" y1="15.302" y2="15.888"><stop stop-opacity=".3"></stop><stop offset=".071" stop-opacity=".2"></stop><stop offset=".321" stop-opacity=".1"></stop><stop offset=".623" stop-opacity=".05"></stop><stop offset="1" stop-opacity="0"></stop></linearGradient><linearGradient gradientUnits="userSpaceOnUse" id="lobe-icons-azure-fill-2" x1="12.841" x2="20.793" y1="1.626" y2="22.814"><stop stop-color="#3CCBF4"></stop><stop offset="1" stop-color="#2892DF"></stop></linearGradient></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Bedrock</title><defs><linearGradient id="lobe-icons-bedrock-fill" x1="80%" x2="20%" y1="20%" y2="80%"><stop offset="0%" stop-color="#6350FB"></stop><stop offset="50%" stop-color="#3D8FFF"></stop><stop offset="100%" stop-color="#9AD8F8"></stop></linearGradient></defs><path d="M13.05 15.513h3.08c.214 0 .389.177.389.394v1.82a1.704 1.704 0 011.296 1.661c0 .943-.755 1.708-1.685 1.708-.931 0-1.686-.765-1.686-1.708 0-.807.554-1.484 1.297-1.662v-1.425h-2.69v4.663a.395.395 0 01-.188.338l-2.69 1.641a.385.385 0 01-.405-.002l-4.926-3.086a.395.395 0 01-.185-.336V16.3L2.196 14.87A.395.395 0 012 14.555L2 14.528V9.406c0-.14.073-.27.192-.34l2.465-1.462V4.448c0-.129.062-.249.165-.322l.021-.014L9.77 1.058a.385.385 0 01.407 0l2.69 1.675a.395.395 0 01.185.336V7.6h3.856V5.683a1.704 1.704 0 01-1.296-1.662c0-.943.755-1.708 1.685-1.708.931 0 1.685.765 1.685 1.708 0 .807-.553 1.484-1.296 1.662v2.311a.391.391 0 01-.389.394h-4.245v1.806h6.624a1.69 1.69 0 011.64-1.313c.93 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708a1.69 1.69 0 01-1.64-1.314H13.05v1.937h4.953l.915 1.18a1.66 1.66 0 01.84-.227c.931 0 1.685.764 1.685 1.707 0 .943-.754 1.708-1.685 1.708-.93 0-1.685-.765-1.685-1.708 0-.346.102-.668.276-.937l-.724-.935H13.05v1.806zM9.973 1.856L7.93 3.122V6.09h-.778V3.604L5.435 4.669v2.945l2.11 1.36L9.712 7.61V5.334h.778V7.83c0 .136-.07.263-.184.335L7.963 9.638v2.081l1.422 1.009-.446.646-1.406-.998-1.53 1.005-.423-.66 1.605-1.055v-1.99L5.038 8.29l-2.26 1.34v1.676l1.972-1.189.398.677-2.37 1.429V14.3l2.166 1.258 2.27-1.368.397.677-2.176 1.311V19.3l1.876 1.175 2.365-1.426.398.678-2.017 1.216 1.918 1.201 2.298-1.403v-5.78l-4.758 2.893-.4-.675 5.158-3.136V3.289L9.972 1.856zM16.13 18.47a.913.913 0 00-.908.92c0 .507.406.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zm3.63-3.81a.913.913 0 00-.908.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92zm1.555-4.99a.913.913 0 00-.908.92c0 .507.407.918.908.918a.913.913 0 00.907-.919.913.913 0 00-.907-.92zM17.296 3.1a.913.913 0 00-.907.92c0 .508.406.92.907.92a.913.913 0 00.908-.92.913.913 0 00-.908-.92z" fill="url(#lobe-icons-bedrock-fill)" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 2.2 KiB |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Claude</title><path d="M4.709 15.955l4.72-2.647.08-.23-.08-.128H9.2l-.79-.048-2.698-.073-2.339-.097-2.266-.122-.571-.121L0 11.784l.055-.352.48-.321.686.06 1.52.103 2.278.158 1.652.097 2.449.255h.389l.055-.157-.134-.098-.103-.097-2.358-1.596-2.552-1.688-1.336-.972-.724-.491-.364-.462-.158-1.008.656-.722.881.06.225.061.893.686 1.908 1.476 2.491 1.833.365.304.145-.103.019-.073-.164-.274-1.355-2.446-1.446-2.49-.644-1.032-.17-.619a2.97 2.97 0 01-.104-.729L6.283.134 6.696 0l.996.134.42.364.62 1.414 1.002 2.229 1.555 3.03.456.898.243.832.091.255h.158V9.01l.128-1.706.237-2.095.23-2.695.08-.76.376-.91.747-.492.584.28.48.685-.067.444-.286 1.851-.559 2.903-.364 1.942h.212l.243-.242.985-1.306 1.652-2.064.73-.82.85-.904.547-.431h1.033l.76 1.129-.34 1.166-1.064 1.347-.881 1.142-1.264 1.7-.79 1.36.073.11.188-.02 2.856-.606 1.543-.28 1.841-.315.833.388.091.395-.328.807-1.969.486-2.309.462-3.439.813-.042.03.049.061 1.549.146.662.036h1.622l3.02.225.79.522.474.638-.079.485-1.215.62-1.64-.389-3.829-.91-1.312-.329h-.182v.11l1.093 1.068 2.006 1.81 2.509 2.33.127.578-.322.455-.34-.049-2.205-1.657-.851-.747-1.926-1.62h-.128v.17l.444.649 2.345 3.521.122 1.08-.17.353-.608.213-.668-.122-1.374-1.925-1.415-2.167-1.143-1.943-.14.08-.674 7.254-.316.37-.729.28-.607-.461-.322-.747.322-1.476.389-1.924.315-1.53.286-1.9.17-.632-.012-.042-.14.018-1.434 1.967-2.18 2.945-1.726 1.845-.414.164-.717-.37.067-.662.401-.589 2.388-3.036 1.44-1.882.93-1.086-.006-.158h-.055L4.132 18.56l-1.13.146-.487-.456.061-.746.231-.243 1.908-1.312-.006.006z" fill="#D97757" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
@@ -0,0 +1 @@
|
||||
<svg height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Gemini</title><defs><linearGradient id="lobe-icons-gemini-fill" x1="0%" x2="68.73%" y1="100%" y2="30.395%"><stop offset="0%" stop-color="#1C7DFF"></stop><stop offset="52.021%" stop-color="#1C69FF"></stop><stop offset="100%" stop-color="#F0DCD6"></stop></linearGradient></defs><path d="M12 24A14.304 14.304 0 000 12 14.304 14.304 0 0012 0a14.305 14.305 0 0012 12 14.305 14.305 0 00-12 12" fill="url(#lobe-icons-gemini-fill)" fill-rule="nonzero"></path></svg>
|
||||
|
After Width: | Height: | Size: 581 B |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="56" viewBox="0 0 24 24" width="56" xmlns="http://www.w3.org/2000/svg" style="flex: 0 0 auto; line-height: 1;"><title>Github</title><path d="M12 0c6.63 0 12 5.276 12 11.79-.001 5.067-3.29 9.567-8.175 11.187-.6.118-.825-.25-.825-.56 0-.398.015-1.665.015-3.242 0-1.105-.375-1.813-.81-2.181 2.67-.295 5.475-1.297 5.475-5.822 0-1.297-.465-2.344-1.23-3.169.12-.295.54-1.503-.12-3.125 0 0-1.005-.324-3.3 1.209a11.32 11.32 0 00-3-.398c-1.02 0-2.04.133-3 .398-2.295-1.518-3.3-1.209-3.3-1.209-.66 1.622-.24 2.83-.12 3.125-.765.825-1.23 1.887-1.23 3.169 0 4.51 2.79 5.527 5.46 5.822-.345.294-.66.81-.765 1.577-.69.31-2.415.81-3.495-.973-.225-.354-.9-1.223-1.845-1.209-1.005.015-.405.56.015.781.51.28 1.095 1.327 1.23 1.666.24.663 1.02 1.93 4.035 1.385 0 .988.015 1.916.015 2.196 0 .31-.225.664-.825.56C3.303 21.374-.003 16.867 0 11.791 0 5.276 5.37 0 12 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 913 B |
|
After Width: | Height: | Size: 35 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>OpenAI</title><path d="M21.55 10.004a5.416 5.416 0 00-.478-4.501c-1.217-2.09-3.662-3.166-6.05-2.66A5.59 5.59 0 0010.831 1C8.39.995 6.224 2.546 5.473 4.838A5.553 5.553 0 001.76 7.496a5.487 5.487 0 00.691 6.5 5.416 5.416 0 00.477 4.502c1.217 2.09 3.662 3.165 6.05 2.66A5.586 5.586 0 0013.168 23c2.443.006 4.61-1.546 5.361-3.84a5.553 5.553 0 003.715-2.66 5.488 5.488 0 00-.693-6.497v.001zm-8.381 11.558a4.199 4.199 0 01-2.675-.954c.034-.018.093-.05.132-.074l4.44-2.53a.71.71 0 00.364-.623v-6.176l1.877 1.069c.02.01.033.029.036.05v5.115c-.003 2.274-1.87 4.118-4.174 4.123zM4.192 17.78a4.059 4.059 0 01-.498-2.763c.032.02.09.055.131.078l4.44 2.53c.225.13.504.13.73 0l5.42-3.088v2.138a.068.068 0 01-.027.057L9.9 19.288c-1.999 1.136-4.552.46-5.707-1.51h-.001zM3.023 8.216A4.15 4.15 0 015.198 6.41l-.002.151v5.06a.711.711 0 00.364.624l5.42 3.087-1.876 1.07a.067.067 0 01-.063.005l-4.489-2.559c-1.995-1.14-2.679-3.658-1.53-5.63h.001zm15.417 3.54l-5.42-3.088L14.896 7.6a.067.067 0 01.063-.006l4.489 2.557c1.998 1.14 2.683 3.662 1.529 5.633a4.163 4.163 0 01-2.174 1.807V12.38a.71.71 0 00-.363-.623zm1.867-2.773a6.04 6.04 0 00-.132-.078l-4.44-2.53a.731.731 0 00-.729 0l-5.42 3.088V7.325a.068.068 0 01.027-.057L14.1 4.713c2-1.137 4.555-.46 5.707 1.513.487.833.664 1.809.499 2.757h.001zm-11.741 3.81l-1.877-1.068a.065.065 0 01-.036-.051V6.559c.001-2.277 1.873-4.122 4.181-4.12.976 0 1.92.338 2.671.954-.034.018-.092.05-.131.073l-4.44 2.53a.71.71 0 00-.365.623l-.003 6.173v.002zm1.02-2.168L12 9.25l2.414 1.375v2.75L12 14.75l-2.415-1.375v-2.75z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
|
After Width: | Height: | Size: 10 KiB |
|
After Width: | Height: | Size: 35 KiB |
|
After Width: | Height: | Size: 15 KiB |
|
After Width: | Height: | Size: 19 KiB |
|
After Width: | Height: | Size: 328 KiB |
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="currentColor" fill-rule="evenodd" height="56" viewBox="0 0 24 24" width="56" xmlns="http://www.w3.org/2000/svg" style="flex: 0 0 auto; line-height: 1;"><title>Github</title><path d="M12 0c6.63 0 12 5.276 12 11.79-.001 5.067-3.29 9.567-8.175 11.187-.6.118-.825-.25-.825-.56 0-.398.015-1.665.015-3.242 0-1.105-.375-1.813-.81-2.181 2.67-.295 5.475-1.297 5.475-5.822 0-1.297-.465-2.344-1.23-3.169.12-.295.54-1.503-.12-3.125 0 0-1.005-.324-3.3 1.209a11.32 11.32 0 00-3-.398c-1.02 0-2.04.133-3 .398-2.295-1.518-3.3-1.209-3.3-1.209-.66 1.622-.24 2.83-.12 3.125-.765.825-1.23 1.887-1.23 3.169 0 4.51 2.79 5.527 5.46 5.822-.345.294-.66.81-.765 1.577-.69.31-2.415.81-3.495-.973-.225-.354-.9-1.223-1.845-1.209-1.005.015-.405.56.015.781.51.28 1.095 1.327 1.23 1.666.24.663 1.02 1.93 4.035 1.385 0 .988.015 1.916.015 2.196 0 .31-.225.664-.825.56C3.303 21.374-.003 16.867 0 11.791 0 5.276 5.37 0 12 0z"></path></svg>
|
||||
|
After Width: | Height: | Size: 913 B |
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<div class="flex flex-col items-center space-y-4 p-6 bg-base-100 rounded-xl shadow-lg mt-2 max-w-sm mx-auto backdrop-blur-xl glass">
|
||||
|
||||
<div class="p-3 bg-white rounded-lg shadow-inner cursor-pointer" @click="toggleQRCode">
|
||||
<qrcode-vue :value="currentValue" :size="size" level="H" />
|
||||
</div>
|
||||
|
||||
<div class="relative w-full p-4 ">
|
||||
<p
|
||||
class="text-sm break-all whitespace-pre-wrap m-1 p-1 pr-5 text-base-content border border-dashed border-base-content rounded-lg">
|
||||
{{ currentValue }}
|
||||
</p>
|
||||
<button @click="copyValue" class="absolute top-2 right-2 btn btn-xs btn-ghost bg-gray-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24"
|
||||
stroke="currentColor">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2M16 8h2a2 2 0 012 2v8a2 2 0 01-2-2h-8a2 2 0 01-2-2v-2" />
|
||||
</svg>
|
||||
</button>
|
||||
<div v-if="showCopied"
|
||||
class="absolute -top-5 -right-2 bg-neutral text-neutral-content px-2 py-1 rounded text-xs opacity-100 transition-opacity duration-300">
|
||||
Copied
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 gap-2 w-full">
|
||||
<button v-for="app in applist" :key="app.name"
|
||||
class="btn btn-sm btn-outline btn-ghost flex items-center justify-center"
|
||||
@click="applyPrefix(app.name)">
|
||||
<img :src="app.url" alt="" class="w-5 h-5 mr-1">
|
||||
<span>{{ app.name }}</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="w-full">
|
||||
<button class="btn btn-outline btn-sm w-full" @click="resetValue">
|
||||
Reset
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, reactive, watch } from 'vue';
|
||||
import QrcodeVue from 'qrcode.vue';
|
||||
|
||||
// 定义组件接收的 props
|
||||
const props = defineProps({
|
||||
value: { // 二维码的原始值
|
||||
type: String,
|
||||
required: true
|
||||
},
|
||||
size: { // 二维码的尺寸 (像素)
|
||||
type: Number,
|
||||
default: 160 // 默认大小
|
||||
}
|
||||
});
|
||||
|
||||
// 使用 ref 创建一个响应式变量,用于存储当前显示的二维码值
|
||||
// 初始值是来自 props 的 value
|
||||
const currentValue = ref(props.value);
|
||||
|
||||
// 监听 props.value 的变化,如果外部传入的 value 改变,更新 currentValue
|
||||
watch(() => props.value, (newValue) => {
|
||||
currentValue.value = newValue;
|
||||
});
|
||||
|
||||
|
||||
const showCopied = ref(false);
|
||||
|
||||
const copyValue = async () => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(currentValue.value);
|
||||
|
||||
showCopied.value = true;
|
||||
setTimeout(() => {
|
||||
showCopied.value = false;
|
||||
}, 1500); // 1.5 秒后恢复
|
||||
} catch (err) {
|
||||
console.error('Failed to copy: ', err);
|
||||
// 可以在这里添加错误提示
|
||||
}
|
||||
};
|
||||
|
||||
const applist = reactive([
|
||||
// { name: 'openteam', url: '/assets/logo.svg' },
|
||||
{ name: 'botgem', url: 'https://botgem.com/favicon.ico' },
|
||||
{ name: 'opencat', url: 'https://opencat.app/favicon.ico' },
|
||||
])
|
||||
|
||||
const applyPrefix = (name) => {
|
||||
let origin = window.location.origin;
|
||||
|
||||
switch (name) {
|
||||
case 'botgem':
|
||||
currentValue.value = `ama://set-api-key?server=${origin}&key=${props.value}`;
|
||||
break;
|
||||
case 'opencat':
|
||||
currentValue.value = `opencat://team/join?domain=${origin}&token=${props.value}`;
|
||||
break;
|
||||
default:
|
||||
currentValue.value = name + props.value;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleQRCode = () => {
|
||||
if (currentValue.value.startsWith('sk-')) {
|
||||
return
|
||||
} else {
|
||||
window.open(currentValue.value, '_blank')
|
||||
}
|
||||
}
|
||||
|
||||
// 清除前缀,恢复到原始值
|
||||
const resetValue = () => {
|
||||
currentValue.value = props.value;
|
||||
};
|
||||
</script>
|
||||
@@ -1,8 +1,8 @@
|
||||
<template>
|
||||
<aside class="w-60 bg-base-100 border-r border-base-200">
|
||||
<div class="flex items-center p-4 outline-none select-none">
|
||||
<aside class="w-60 bg-base-100 border-r border-base-200 glass">
|
||||
<div class="flex items-center p-4 outline-none select-none backdrop-blur-lg">
|
||||
<div class="w-12 h-12 rounded-full">
|
||||
<img src='@/assets/logo.svg' class="">
|
||||
<img src='@/assets/logo.svg' class="" @click="$router.push('/')" >
|
||||
</div>
|
||||
<div class="p-0 text-2xl font-bold text-center">OpenTeam</div>
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
try {
|
||||
// 1. 从后端获取注册选项 (Creation Options)
|
||||
const res = await request.get("/profile/passkey");
|
||||
console.log("begin:", res.data.data.publicKey);
|
||||
// console.log("begin:", res.data.data.publicKey);
|
||||
const options = res.data.data.publicKey;
|
||||
|
||||
// 调用 Web Authentication API 进行注册
|
||||
@@ -44,7 +44,7 @@ export const useWebAuthStore = defineStore("webauth", () => {
|
||||
|
||||
// 3. 将注册结果 (Attestation) 发送到后端进行验证和保存
|
||||
const res2 = await request.post("/profile/passkey", attestation);
|
||||
console.log("end:", res2);
|
||||
// console.log("end:", res2);
|
||||
return res2;
|
||||
} catch (err) {
|
||||
error.value =err.response?.data?.error || "添加 Passkey 失败,请稍后重试。";
|
||||
|
||||
@@ -11,7 +11,7 @@ if (import.meta.env.DEV) { // Vite 的方式判断开发环境
|
||||
|
||||
const service = axios.create({
|
||||
baseURL: baseURL,
|
||||
timeout: 5000,
|
||||
timeout: 6000,
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
|
||||
@@ -73,7 +73,7 @@
|
||||
</header>
|
||||
|
||||
<!-- Main Content Area -->
|
||||
<main class="flex-1 overflow-y-auto p-6 bg-base-200">
|
||||
<main class="flex-1 overflow-y-auto p-6 bg-base-200 px-1 sm:px-6">
|
||||
<router-view></router-view>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
@@ -115,7 +115,7 @@ const copyUrl = async () => {
|
||||
|
||||
const star = ref(0);
|
||||
const getGithubStars = async () => {
|
||||
const res = await fetch('https://ungh.cc/repos/mirrors2/opencatd-open', { next: { revalidate: 3600 } });
|
||||
const res = await fetch('https://ungh.cc/repos/mirrors2/openteam', { next: { revalidate: 3600 } });
|
||||
const data = await res.json();
|
||||
return data.repo.stars;
|
||||
};
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader />
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
@@ -12,19 +12,8 @@
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-if="error" role="alert" class="alert shadow-lg bg-rose-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<span>Error! {{ error }}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm" @click="error = null">X</button>
|
||||
</div>
|
||||
|
||||
<div class="card border border-base-300/40 shadow-sm">
|
||||
<form @submit.prevent="createApiKey" class="card-body space-y-5">
|
||||
<form @submit.prevent="createApiKey" class="card-body space-y-5 p-3 sm:p-8">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-base font-medium text-base-content border-b border-base-300/30 pb-2 mb-4">
|
||||
Basic Information
|
||||
@@ -53,23 +42,14 @@
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="azure">Azure</option>
|
||||
<option value="github">Github</option>
|
||||
<option value="openai-compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
<button type="button" @click="togglePasswordVisibility" tabindex="-1"
|
||||
class="absolute inset-y-0 left-0 px-3 flex items-center text-base-content/60 hover:text-base-content/80 focus:outline-none focus:ring-0 rounded-r-md"
|
||||
id="password-visibility-toggle">
|
||||
<template v-if="newApiKey.type === 'openai'">
|
||||
<img src="../../assets/openai.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else-if="newApiKey.type === 'claude'">
|
||||
<img src="../../assets/claude.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else-if="newApiKey.type === 'gemini'">
|
||||
<img src="../../assets/gemini.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else="newApiKey.type">
|
||||
<img src="../../assets/logo.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<img :src="apiKeyImageUrl(newApiKey.type)" class="w-5 h-5" alt="">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -109,13 +89,13 @@
|
||||
class="input input-sm input-bordered w-full" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<!-- <div class="form-control">
|
||||
<label for="deployment_name" class="label">
|
||||
<span class="label-text">Deployment Name</span>
|
||||
</label>
|
||||
<input id="deployment_name" type="text" v-model="newApiKey.deployment_name"
|
||||
placeholder="Deployment Name" class="input input-sm input-bordered w-full" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="form-control">
|
||||
<label for="api_secret" class="label">
|
||||
@@ -156,7 +136,7 @@
|
||||
<!-- <textarea id="support_models" v-model="newApiKey.support_models_text"
|
||||
placeholder='["model1", "model2"]' class="textarea textarea-sm textarea-bordered w-full"></textarea> -->
|
||||
<el-input-tag v-model="newApiKey.support_models_array" :trigger="'Enter'" clearable
|
||||
placeholder="Please input" @change="onchange_supportmodel"/>
|
||||
placeholder="Please input" @change="onchange_supportmodel" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
@@ -175,9 +155,22 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="error" role="alert" class="alert shadow-lg bg-rose-100">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6 shrink-0 stroke-current" fill="none"
|
||||
viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
||||
d="M10 14l2-2m0 0l2-2m-2 2l-2-2m2 2l2 2m7-2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
</svg>
|
||||
<div>
|
||||
<span>{{ error }}</span>
|
||||
</div>
|
||||
<button class="btn btn-sm" @click="error = null">X</button>
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end pt-4 items-center gap-2">
|
||||
<button @click="cancel" class="btn btn-sm btn-outline">Cancel</button>
|
||||
<button type="submit" class="btn btn-outline btn-sm px-4 text-sm font-medium btn-success" :disabled="!isFormValid">
|
||||
<button type="submit" class="btn btn-outline btn-sm px-4 text-sm font-medium btn-success"
|
||||
:disabled="!isFormValid">
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
@@ -209,12 +202,12 @@ const newApiKey = ref({
|
||||
active: true,
|
||||
endpoint: '',
|
||||
resource_name: '',
|
||||
deployment_name: '',
|
||||
// deployment_name: '',
|
||||
api_secret: '',
|
||||
model_prefix: '',
|
||||
model_alias: '',
|
||||
parameters: '{}',
|
||||
support_models: '',
|
||||
support_models: '[]',
|
||||
support_models_array: [],
|
||||
})
|
||||
|
||||
@@ -226,12 +219,12 @@ const resetNewApiKey = () => {
|
||||
active: true,
|
||||
endpoint: '',
|
||||
resource_name: '',
|
||||
deployment_name: '',
|
||||
// deployment_name: '',
|
||||
api_secret: '',
|
||||
model_prefix: '',
|
||||
model_alias: '',
|
||||
parameters: '{}',
|
||||
support_models: '',
|
||||
support_models: '[]',
|
||||
support_models_array: [],
|
||||
}
|
||||
}
|
||||
@@ -252,6 +245,19 @@ const cancel = () => {
|
||||
emit('closeModal', true)
|
||||
}
|
||||
|
||||
const apiKeyImageMap = {
|
||||
'openai': '/assets/openai.svg',
|
||||
'claude': '/assets/claude.svg',
|
||||
'gemini': '/assets/gemini.svg',
|
||||
'azure': '/assets/azure.svg',
|
||||
'github': '/assets/github.svg'
|
||||
|
||||
};
|
||||
|
||||
const apiKeyImageUrl = (keytype) => {
|
||||
return apiKeyImageMap[keytype] || '/assets/logo.svg';
|
||||
};
|
||||
|
||||
const createApiKey = async () => {
|
||||
if (!isFormValid.value) {
|
||||
setToast('Please fill in all required fields (Name, Type, API Key).', 'error')
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader title="Key Details" />
|
||||
|
||||
<div class="divider mt-1 mb-0"></div>
|
||||
<div class="card border border-base-300/40 shadow-sm"v-if="key" >
|
||||
<form @submit.prevent="updateKey" class="card-body space-y-5">
|
||||
<form @submit.prevent="updateKey" class="card-body space-y-5 p-3 sm:p-8">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-base font-medium text-base-content border-b border-base-300/30 pb-2 mb-4">
|
||||
Basic Information
|
||||
@@ -33,22 +33,13 @@
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="claude">Claude</option>
|
||||
<option value="gemini">Gemini</option>
|
||||
<option value="azure">Azure</option>
|
||||
<option value="github">Github</option>
|
||||
<option value="openai-compatible">OpenAI Compatible</option>
|
||||
</select>
|
||||
<button type="button" class="absolute inset-y-0 left-0 px-3 flex items-center text-base-content/60 hover:text-base-content/80 focus:outline-none focus:ring-0 rounded-r-md"
|
||||
id="password-visibility-toggle">
|
||||
<template v-if="key.type === 'openai'">
|
||||
<img src="../../assets/openai.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else-if="key.type === 'claude'">
|
||||
<img src="../../assets/claude.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else-if="key.type === 'gemini'">
|
||||
<img src="../../assets/gemini.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<template v-else="key.type">
|
||||
<img src="../../assets/logo.svg" class="w-5 h-5" alt="">
|
||||
</template>
|
||||
<img :src="apiKeyImageUrl(key.type)" class="w-5 h-5" alt="">
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -88,13 +79,13 @@
|
||||
class="input input-sm input-bordered w-full" />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<!-- <div class="form-control">
|
||||
<label for="deployment_name" class="label">
|
||||
<span class="label-text">Deployment Name</span>
|
||||
</label>
|
||||
<input id="deployment_name" type="text" v-model="key.deployment_name"
|
||||
placeholder="Deployment Name" class="input input-sm input-bordered w-full" />
|
||||
</div>
|
||||
</div> -->
|
||||
|
||||
<div class="form-control">
|
||||
<label for="api_secret" class="label">
|
||||
@@ -167,7 +158,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, onMounted, inject } from 'vue';
|
||||
import { ref, computed, onMounted, inject, reactive } from 'vue';
|
||||
import { useRoute,useRouter } from 'vue-router';
|
||||
import { Eye, EyeOff, BadgeCheck, Send, CircleX, CircleCheckBig, TrashIcon, Infinity } from 'lucide-vue-next';
|
||||
import { useKeyStore } from '../../stores/key';
|
||||
@@ -194,6 +185,27 @@ onMounted(async () => {
|
||||
}
|
||||
});
|
||||
|
||||
const keyOption = reactive([
|
||||
{name: 'openai', label: 'OpenAI'},
|
||||
{name: 'claude', label: 'Claude'},
|
||||
{name: 'gemini', label: 'Gemini'},
|
||||
{name: 'azure', label: 'Azure'},
|
||||
{name: 'github', label: 'Github'},
|
||||
{name: 'openai-compatible', label: 'OpenAI Compatible'}
|
||||
])
|
||||
|
||||
const apiKeyImageMap = {
|
||||
'openai': '/assets/openai.svg',
|
||||
'claude': '/assets/claude.svg',
|
||||
'gemini': '/assets/gemini.svg',
|
||||
'azure': '/assets/azure.svg',
|
||||
'github': '/assets/github.svg'
|
||||
};
|
||||
|
||||
const apiKeyImageUrl = (keytype) => {
|
||||
return apiKeyImageMap[keytype] || '/assets/logo.svg';
|
||||
};
|
||||
|
||||
const onchange_supportmodel = () => {
|
||||
key.value.support_models = JSON.stringify(key.value.support_models_array)
|
||||
}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<div class="flex flex-wrap gap-2 mb-4" v-if="keys">
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<input
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[150px] lg:w-[250px]"
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[120px] lg:w-[250px]"
|
||||
placeholder="Filter" value="">
|
||||
|
||||
<div class="dropdown">
|
||||
@@ -33,11 +33,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center justify-end space-x-2">
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
</button>
|
||||
<dialog id="myModal" class="modal" ref="modalRef">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
@@ -48,6 +49,7 @@
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm p-1 h-8 w-8">
|
||||
@@ -85,13 +87,15 @@
|
||||
<thead>
|
||||
<tr>
|
||||
<th>
|
||||
<div class="flex gap-1 items-center">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="selectAll" @change="toggleSelectAll" />
|
||||
</div>
|
||||
</th>
|
||||
<th>Type</th>
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th>Key</th>
|
||||
<th>Endpoint</th>
|
||||
<!-- <th>Key</th> -->
|
||||
<!-- <th>Endpoint</th> -->
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -100,27 +104,38 @@
|
||||
<tr v-for="key in keys" :key="key.id"
|
||||
class="hover:bg-gray-500/50 dark:hover:bg-neutral-600 transition-colors">
|
||||
<td>
|
||||
<div class="flex gap-1 items-center">
|
||||
<input type="checkbox" class="checkbox checkbox-xs" v-model="key.selected"
|
||||
@change="toggleUserSelection(key)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">
|
||||
<div class="flex gap-1 items-center">
|
||||
<span class="backdrop-blur-lg glass rounded-full border-none"> <img :src="displayIcon(key.type)"
|
||||
class="w-5 h-5" alt=""></span>
|
||||
{{ key.type }}
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">{{ key.type }}</td>
|
||||
<td class="text-xs dark:text-white">{{ key.name }}</td>
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="key.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="key.active" @change="updateStatus(key)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">{{ key.apikey }}</td>
|
||||
<td class="text-xs dark:text-white">{{ key.endpoint }}</td>
|
||||
<!-- <td class="text-xs dark:text-white">{{ key.apikey }}</td> -->
|
||||
<!-- <td class="text-xs dark:text-white">{{ key.endpoint }}</td> -->
|
||||
<td>
|
||||
<div class="flex gap-1">
|
||||
<div class="flex gap-2">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="预览">
|
||||
<button class="btn btn-ghost btn-xs btn-square " @click="viewKey(key)">
|
||||
<EyeIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
<EyeIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30" @click="confirmDeleteKey(key)">
|
||||
<TrashIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30"
|
||||
@click="confirmDeleteKey(key)">
|
||||
<TrashIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -133,7 +148,7 @@
|
||||
|
||||
<!-- Pagination -->
|
||||
<Pagination :currentPage="currentPage" :totalItems="totalItems" :pageSize="pageSize"
|
||||
:pageSizeOptions="[ 10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
||||
:pageSizeOptions="[10, 20, 50, 100]" @changePage="changePage" @changePageSize="changePageSize" />
|
||||
|
||||
</div>
|
||||
</template>
|
||||
@@ -305,6 +320,24 @@ const deleteKey = async (key) => {
|
||||
}
|
||||
};
|
||||
|
||||
const displayIcon = (apitype) => {
|
||||
switch (apitype) {
|
||||
case 'openai':
|
||||
return '/assets/openai.svg';
|
||||
case 'claude':
|
||||
return '/assets/claude.svg';
|
||||
case 'gemini':
|
||||
return '/assets/gemini.svg'
|
||||
case 'azure':
|
||||
return '/assets/azure.svg';
|
||||
case 'github':
|
||||
return '/assets/github.svg';
|
||||
default:
|
||||
return '/assets/logo.svg';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// 关闭模态框
|
||||
const modalRef = ref(null);
|
||||
const closeModal = async () => {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader title="User Details" />
|
||||
|
||||
<div class="max-w-3xl mx-auto space-y-4">
|
||||
<div class="card card-bordered bg-base-100 shadow-sm" v-if="user">
|
||||
<div class="card-body">
|
||||
<div class="card-body px-2 sm:px-8">
|
||||
<div class="flex flex-col md:flex-row items-start md:items-center gap-6">
|
||||
<div class="avatar placeholder" v-if="!user?.avatar_url">
|
||||
<div class="glass bg-neutral text-neutral-content rounded-full w-16 sm:w-20">
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="text-right md:text-left">
|
||||
<div class="text-right sm:text-left">
|
||||
<div class="flex items-center gap-2">
|
||||
<span class="font-medium text-base-content/90">Status:</span>
|
||||
<span class="badge badge-outline"
|
||||
@@ -119,7 +119,7 @@
|
||||
</div>
|
||||
<div v-else class="max-w-3xl mx-auto">
|
||||
<div class="card card-bordered bg-base-100 shadow-sm">
|
||||
<div class="card-body">
|
||||
<div class="card-body space-y-4 space-y-4 px-2 sm:px-8">
|
||||
<div class="flex justify-center items-center py-10">
|
||||
<span class="loading loading-spinner loading-lg"></span>
|
||||
</div>
|
||||
@@ -128,7 +128,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card card-bordered bg-base-100 shadow-sm" v-if="user">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="card-body space-y-4 space-y-4 px-2 sm:px-8">
|
||||
<h3 class="text-base font-medium text-base-content mb-3 flex items-center gap-2">
|
||||
<Bookmark class="h-5 w-5 text-base-content/80" /> Password
|
||||
</h3>
|
||||
@@ -195,14 +195,14 @@
|
||||
</div>
|
||||
|
||||
<div class="card card-bordered bg-base-100 shadow-sm" v-if="user">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="card-body space-y-4 px-2 sm:px-8">
|
||||
<h3 class="text-base font-medium text-base-content mb-3 flex items-center gap-2">
|
||||
<Bookmark class="h-5 w-5 text-base-content/80" /> Passkeys
|
||||
</h3>
|
||||
|
||||
<p class="text-sm text-base-content/80">Manage your passkeys for secure and passwordless login.</p>
|
||||
<div class="card card-bordered bg-base-100 shadow-sm mt-6" v-if="user">
|
||||
<div class="card-body">
|
||||
<div class="card-body px-2 sm:px-8">
|
||||
<div v-if="passkeys" class="overflow-x-auto -mx-6">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
@@ -243,7 +243,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card card-bordered bg-base-100 shadow-sm" v-if="user">
|
||||
<div class="card-body space-y-4">
|
||||
<div class="card-body space-y-4 px-2 sm:px-8">
|
||||
<h3 class="text-base font-medium text-base-content mb-3 flex items-center gap-2">
|
||||
<Bookmark class="h-5 w-5 text-base-content/80" /> Linked Accounts (todo)
|
||||
</h3>
|
||||
@@ -421,7 +421,10 @@ const toggleTelegramConnection = () => {
|
||||
|
||||
const newpasskey = async () => {
|
||||
try {
|
||||
await webAuthStore.addPasskey();
|
||||
let res = await webAuthStore.addPasskey();
|
||||
if (res.data?.code == 200) {
|
||||
await getPasskeys();
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('err', err);
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader title="User Details" />
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="card card-bordered bg-base-100 shadow-sm" v-if="user">
|
||||
<div class="card-body space-y-5">
|
||||
<div class="card-body space-y-5 p-3 sm:p-8">
|
||||
<div class="flex flex-col md:flex-row items-center gap-6">
|
||||
<div class="avatar placeholder" v-if="!user?.avatar_url">
|
||||
<div class="glass bg-neutral text-neutral-content rounded-full w-16 sm:w-20">
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader />
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card border border-base-300/40 shadow-sm">
|
||||
<form @submit.prevent="createToken" class="card-body space-y-5">
|
||||
<form @submit.prevent="createToken" class="card-body space-y-5 p-3 sm:p-8">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-base font-medium text-base-content border-b border-base-300/30 pb-2 mb-4">
|
||||
Basic Information
|
||||
@@ -36,33 +36,11 @@
|
||||
Name <span class="text-red-500">*</span>
|
||||
</span>
|
||||
</label>
|
||||
<input id="username" type="text" v-model="newToken.name" placeholder="Select a username"
|
||||
<input id="username" type="text" v-model="newToken.name" placeholder="New Token Name"
|
||||
class="input input-sm input-bordered w-full" required />
|
||||
</div>
|
||||
|
||||
<div class="form-control">
|
||||
<label for="password" class="label">
|
||||
<span class="label-text">
|
||||
Key
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input id="password" :type="isPasswordVisible ? 'text' : 'password'" v-model="newToken.key"
|
||||
class="input input-sm input-bordered w-full" />
|
||||
<button type="button" @click="togglePasswordVisibility"
|
||||
class="absolute inset-y-0 right-0 px-3 flex items-center text-base-content/60 hover:text-base-content/80 focus:outline-none focus:ring-0 rounded-r-md"
|
||||
id="password-visibility-toggle">
|
||||
|
||||
<template v-if="!isPasswordVisible">
|
||||
<EyeOff class="w-5 h-5" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Eye class="w-5 h-5" />
|
||||
</template>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -74,11 +52,34 @@
|
||||
</div>
|
||||
<div class="collapse-content px-0">
|
||||
<div class="grid grid-cols-1 md:grid-cols-2 gap-x-4 gap-y-4 pt-3">
|
||||
<div class="form-control">
|
||||
<label for="password" class="label">
|
||||
<span class="label-text">
|
||||
Key
|
||||
</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input id="token" :type="isTokenVisible ? 'text' : 'password'" v-model="newToken.key"
|
||||
class="input input-sm input-bordered w-full" />
|
||||
<button type="button" @click="toggleTokenVisibility"
|
||||
class="absolute inset-y-0 right-0 px-3 flex items-center text-base-content/60 hover:text-base-content/80 focus:outline-none focus:ring-0 rounded-r-md"
|
||||
id="token-visibility-toggle">
|
||||
|
||||
<template v-if="!isTokenVisible">
|
||||
<EyeOff class="w-5 h-5" />
|
||||
</template>
|
||||
<template v-else>
|
||||
<Eye class="w-5 h-5" />
|
||||
</template>
|
||||
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-control">
|
||||
<label class="label label-text text-xs">Expired at</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input type="date" v-model="newToken.format_expired_at" class="input input-bordered input-sm w-full"
|
||||
placeholder="年/月/日" :disabled="newToken.never_expired"/>
|
||||
placeholder="年/月/日" :disabled="newToken.never_expired" />
|
||||
<label class="flex items-center space-x-2 cursor-pointer whitespace-nowrap">
|
||||
<input type="checkbox" v-model="newToken.never_expired" class="checkbox checkbox-sm" />
|
||||
<span class="text-sm text-base-content/90">Never</span>
|
||||
@@ -92,7 +93,7 @@
|
||||
</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input id="quota" type="number" v-model="newToken.quota" placeholder="Enter quota amount"
|
||||
class="input input-sm input-bordered flex-grow" :disabled="newToken.unlimited_quota" />
|
||||
class="input input-sm input-bordered w-1/2 flex-grow" :disabled="newToken.unlimited_quota" />
|
||||
<label class="flex items-center space-x-2 cursor-pointer whitespace-nowrap">
|
||||
<input type="checkbox" v-model="newToken.unlimited_quota" class="checkbox checkbox-sm" />
|
||||
<span class="text-sm text-base-content/90">Unlimited</span>
|
||||
@@ -133,7 +134,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, inject ,watch } from 'vue'
|
||||
import { ref, computed, inject, watch } from 'vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
import { useAuthStore } from '@/stores/auth'
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
@@ -144,7 +145,7 @@ const router = useRouter()
|
||||
const authStore = useAuthStore()
|
||||
const { setToast } = inject('toast')
|
||||
const error = ref(null)
|
||||
const user = computed(() =>authStore.user);
|
||||
const user = computed(() => authStore.user);
|
||||
|
||||
const showAdvancedOptions = ref(false)
|
||||
|
||||
@@ -158,7 +159,7 @@ const newToken = ref({
|
||||
unlimited_quota: true,
|
||||
expired_at: 0,
|
||||
format_expired_at: '',
|
||||
never_expired:true,
|
||||
never_expired: true,
|
||||
})
|
||||
|
||||
const resetnewToken = () => {
|
||||
@@ -171,7 +172,7 @@ const resetnewToken = () => {
|
||||
unlimited_quota: true,
|
||||
expired_at: 0,
|
||||
format_expired_at: '',
|
||||
never_expired:true,
|
||||
never_expired: true,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,11 +185,11 @@ watch(
|
||||
}
|
||||
);
|
||||
watch(
|
||||
() =>newToken.value.format_expired_at,
|
||||
(format_expired_at)=> {
|
||||
if(!newToken.value.never_expired && format_expired_at) {
|
||||
() => newToken.value.format_expired_at,
|
||||
(format_expired_at) => {
|
||||
if (!newToken.value.never_expired && format_expired_at) {
|
||||
newToken.value.expired_at = dateToUnix(format_expired_at);
|
||||
}else{
|
||||
} else {
|
||||
newToken.value.expired_at = 0;
|
||||
}
|
||||
}
|
||||
@@ -231,10 +232,10 @@ const deleteToken = async (id) => {
|
||||
}
|
||||
|
||||
// 显示密码
|
||||
const isPasswordVisible = ref(false);
|
||||
const isTokenVisible = ref(false);
|
||||
|
||||
function togglePasswordVisibility() {
|
||||
isPasswordVisible.value = !isPasswordVisible.value;
|
||||
function toggleTokenVisibility() {
|
||||
isTokenVisible.value = !isTokenVisible.value;
|
||||
}
|
||||
|
||||
const emit = defineEmits(['closeModal'])
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
</button>
|
||||
<dialog id="myModal" class="modal" ref="modalRef">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
@@ -24,20 +24,18 @@
|
||||
</div>
|
||||
|
||||
<!-- Table -->
|
||||
<div class="card card-bordered bg-base-100 shadow-sm mt-6" v-if="user">
|
||||
<div class="card card-bordered bg-base-100 shadow-sm mt-6">
|
||||
|
||||
<div class="card-body">
|
||||
<h3 class="card-title text-lg">Tokens</h3>
|
||||
<div v-if="user.tokens && user.tokens.length" class="overflow-x-auto -mx-6">
|
||||
<div class="card bg-base-100 shadow-xs overflow-x-auto dark:bg-base-200" v-if="user">
|
||||
<table class="table table-sm w-full">
|
||||
<thead>
|
||||
<tr class="text-xs text-base-content/70 uppercase bg-base-200">
|
||||
<th class="px-2 py-3">Token Name</th>
|
||||
<th class="px-2 py-3">Token</th>
|
||||
<th class="px-2 py-3">Status</th>
|
||||
<th class="px-2 py-3">Key</th>
|
||||
<th class="px-2 py-3">Expired At</th>
|
||||
<!-- <th class="px-2 py-3">Key</th> -->
|
||||
<th class="px-2 py-3">Expired</th>
|
||||
<th class="px-2 py-3">Quota</th>
|
||||
<th class="px-2 py-3">Used Quota</th>
|
||||
<th class="px-2 py-3">Used</th>
|
||||
<th class="text-right px-2 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -49,7 +47,7 @@
|
||||
:class="token.active ? 'toggle-success' : 'toggle-error'" v-model="token.active"
|
||||
@change="updateStatus(token)" />
|
||||
</td>
|
||||
<td class="font-mono text-xs px-2 py-3">{{ token.key }}</td>
|
||||
<!-- <td class="font-mono text-xs px-2 py-3">{{ token.key }}</td> -->
|
||||
<td class="px-2 py-3">{{ token.expired_at == 0 ? 'Never' : unixToDate(token.expired_at) }}</td>
|
||||
<td class="px-2 py-3">
|
||||
<template v-if="token.unlimited_quota">
|
||||
@@ -58,35 +56,58 @@
|
||||
<template v-else>{{ token.quota }}</template>
|
||||
</td>
|
||||
<td class="px-2 py-3">{{ token.used_quota }}</td>
|
||||
<td class="text-right px-2 py-3 flex justify-between items-center gap-1">
|
||||
<div class="md:tooltip" data-tip="clean usedquota">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-sky-300" @click="cleanUsedToken(token)"
|
||||
aria-label="Revoke token">
|
||||
<Eraser class="w-4 h-4" />
|
||||
<td class="text-right px-1 py-3">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open pt-1" data-tip="预览">
|
||||
<button class="btn btn-ghost btn-xs btn-square" onclick="" @click="viewToken(token)">
|
||||
<EyeIcon class="w-5 h-5 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button v-if="token.name !== 'default'" class="btn btn-ghost btn-xs btn-square text-error"
|
||||
@click="confirmRevokeToken(token)" aria-label="Revoke token">
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
<div class="md:tooltip" data-tip="clean used">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-sky-300 mt-1" @click="cleanUsedToken(token)"
|
||||
aria-label="Revoke token">
|
||||
<Eraser class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button v-if="token.name !== 'default'"
|
||||
class="btn btn-ghost btn-xs btn-square text-error items-center" @click="confirmRevokeToken(token)"
|
||||
aria-label="Revoke token">
|
||||
<TrashIcon class="w-5 h-5" />
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<dialog id="myToken" class="modal" ref="tokenRef">
|
||||
<div class="modal-box px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
<QRCodeCard :value="qrCodeValue" :size="120" />
|
||||
</div>
|
||||
<form method="dialog" class="modal-backdrop">
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<p v-else class="text-center text-base-content/70 py-4">No tokens found</p>
|
||||
</div>
|
||||
</div>
|
||||
<!-- </div> -->
|
||||
|
||||
|
||||
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, inject, computed,watch } from 'vue';
|
||||
import { ref, onMounted, inject, computed, watch } from 'vue';
|
||||
import { useRouter } from 'vue-router';
|
||||
import BreadcrumbHeader from '@/components/dashboard/BreadcrumbHeader.vue';
|
||||
import QRCodeCard from '@/components/QRCodeCard.vue';
|
||||
import TokenNew from '@/views/dashboard/TokenNew.vue';
|
||||
import { useAuthStore } from '@/stores/auth';
|
||||
import {
|
||||
@@ -104,7 +125,7 @@ onMounted(async () => {
|
||||
})
|
||||
|
||||
watch(() => authStore.user, async (newUser) => {
|
||||
if (newUser.expired_at>0) {
|
||||
if (newUser.expired_at > 0) {
|
||||
newUser.format_expired_at = unixToDate(newUser.expired_at);
|
||||
}
|
||||
})
|
||||
@@ -160,6 +181,24 @@ const cleanUsedToken = async (token) => {
|
||||
}
|
||||
}
|
||||
|
||||
const showTokenModel = ref(false);
|
||||
const tokenRef = ref(null);
|
||||
const viewToken = (token) => {
|
||||
const dialog = tokenRef.value;
|
||||
if (dialog) {
|
||||
if (!dialog.hasAttribute('open')) {
|
||||
qrCodeValue.value = token.key;
|
||||
dialog.showModal();
|
||||
} else {
|
||||
if (dialog.hasAttribute('open')) {
|
||||
dialog.close();
|
||||
}
|
||||
}
|
||||
}
|
||||
showTokenModel.value = !showTokenModel.value
|
||||
}
|
||||
|
||||
const qrCodeValue = ref('');
|
||||
|
||||
|
||||
// 关闭模态框
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
<div class="flex flex-wrap gap-2 mb-4">
|
||||
<div class="flex flex-1 items-center space-x-2">
|
||||
<input
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[150px] lg:w-[250px]"
|
||||
class="flex rounded-md border border-input bg-transparent px-3 py-1 text-sm shadow-sm transition-colors placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 h-8 w-[120px] lg:w-[250px]"
|
||||
placeholder="Filter" value="">
|
||||
|
||||
<div class="dropdown">
|
||||
@@ -34,11 +34,12 @@
|
||||
|
||||
</div>
|
||||
|
||||
<div class="flex flex-1 items-center justify-end space-x-1">
|
||||
<button class="btn btn-outline btn-success btn-sm gap-1" onclick="myModal.showModal()">
|
||||
<PlusIcon class="w-3.5 h-3.5" />New
|
||||
</button>
|
||||
<dialog id="myModal" class="modal" ref="modalRef">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen">
|
||||
<div class="modal-box w-11/12 max-w-5xl h-screen px-0 sm:px-8">
|
||||
<form method="dialog">
|
||||
<button class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">✕</button>
|
||||
</form>
|
||||
@@ -49,6 +50,7 @@
|
||||
<button>关闭</button>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<div class="dropdown dropdown-end dropdown-hover">
|
||||
<div tabindex="0" role="button" class="btn btn-ghost btn-sm p-1 h-8 w-8">
|
||||
@@ -92,7 +94,7 @@
|
||||
<th>Name</th>
|
||||
<th>Active</th>
|
||||
<th>Quota</th>
|
||||
<th>UsedQuota</th>
|
||||
<th>Used</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -107,8 +109,10 @@
|
||||
<td class="text-xs dark:text-white">{{ user.id }}</td>
|
||||
<td class="text-xs dark:text-white">{{ user.username }}</td>
|
||||
<td>
|
||||
<div class="flex items-center gap-1">
|
||||
<input type="checkbox" class="toggle toggle-xs" :class="user.active ? 'toggle-success' : 'toggle-error'"
|
||||
v-model="user.active" @change="updateStatus(user)" />
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-xs dark:text-white">
|
||||
<template v-if="user.unlimited_quota">
|
||||
@@ -121,13 +125,13 @@
|
||||
<div class="flex gap-1">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="预览">
|
||||
<button class="btn btn-ghost btn-xs btn-square " @click="viewUser(user)">
|
||||
<EyeIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
<EyeIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除" v-if="user.role<20">
|
||||
<div class="lg:tooltip lg:tooltip-top lg:tooltip-open" data-tip="删除" v-if="user.role < 20">
|
||||
<button class="btn btn-ghost btn-xs btn-square text-error hover:bg-error/30"
|
||||
@click="confirmDeleteUser(user)">
|
||||
<TrashIcon class="w-3.5 h-3.5 dark:text-white" />
|
||||
<TrashIcon class="w-4 h-4 dark:text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,7 +172,7 @@ const pageSize = ref(10);
|
||||
const totalItems = computed(() => userStore.totalUsers);
|
||||
|
||||
// 封装公共的用户列表获取方法
|
||||
const listUsers = async (size = pageSize.value, page = currentPage.value, active=selectedStatuses.map(status => status.value)) => {
|
||||
const listUsers = async (size = pageSize.value, page = currentPage.value, active = selectedStatuses.map(status => status.value)) => {
|
||||
currentPage.value = page || currentPage.value;
|
||||
// console.log('pagesize', pageSize.value, 'page', currentPage.value, 'active', selectedStatuses.map(status => status.value));
|
||||
await userStore.listUser(size, page, active);
|
||||
@@ -231,7 +235,7 @@ const toggleStatusFilter = async (status) => {
|
||||
selectedStatuses.push({ status, value: statusValue });
|
||||
}
|
||||
|
||||
await listUsers(undefined,1,undefined);
|
||||
await listUsers(undefined, 1, undefined);
|
||||
};
|
||||
|
||||
// 处理批量操作
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader />
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
@@ -24,7 +24,7 @@
|
||||
</div>
|
||||
|
||||
<div class="card border border-base-300/40 shadow-sm">
|
||||
<form @submit.prevent="createUser" class="card-body space-y-5">
|
||||
<form @submit.prevent="createUser" class="card-body space-y-5 p-3 sm:p-8">
|
||||
<div class="space-y-4">
|
||||
<h2 class="text-base font-medium text-base-content border-b border-base-300/30 pb-2 mb-4">
|
||||
Basic Information
|
||||
|
||||
@@ -1,14 +1,13 @@
|
||||
<template>
|
||||
<div class="min-h-screen bg-base-100 p-4 md:p-6">
|
||||
<div class="min-h-screen bg-base-100 p-2 md:p-6">
|
||||
<BreadcrumbHeader title="User Details" />
|
||||
|
||||
<div class="max-w-3xl mx-auto">
|
||||
<div class="card card-bordered bg-base-100 shadow-sm" v-if="user">
|
||||
<div class="card-body space-y-5">
|
||||
<div class="card-body space-y-5 p-3 sm:p-8">
|
||||
<div class="flex flex-col md:flex-row items-center gap-6">
|
||||
<div class="avatar placeholder" v-if="!user?.avatar_url">
|
||||
<div
|
||||
class="glass bg-neutral text-neutral-content rounded-full w-16 sm:w-20">
|
||||
<div class="glass bg-neutral text-neutral-content rounded-full w-16 sm:w-20">
|
||||
<span class="text-2xl sm:text-3xl">{{ user?.username?.[0]?.toUpperCase() }}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -118,7 +117,7 @@
|
||||
<select id="role" v-model="user.role" class="select select-bordered select-sm w-full">
|
||||
<option :value="0">User</option>
|
||||
<option :value="10">Admin</option>
|
||||
<option v-if="user.role>10" :value="20">Root</option>
|
||||
<option v-if="user.role > 10" :value="20">Root</option>
|
||||
|
||||
</select>
|
||||
</div>
|
||||
@@ -139,7 +138,7 @@
|
||||
</label>
|
||||
<div class="flex items-center space-x-3">
|
||||
<input id="quota" type="number" v-model="user.quota" placeholder="Enter quota amount"
|
||||
class="input input-bordered input-sm flex-grow" :disabled="user.unlimited_quota" />
|
||||
class="input input-bordered input-sm w-1/2" :disabled="user.unlimited_quota" />
|
||||
<label class="label cursor-pointer space-x-2 p-0">
|
||||
<input type="checkbox" v-model="user.unlimited_quota" class="checkbox checkbox-sm" />
|
||||
<span class="label-text text-sm text-base-content/90">Unlimited</span>
|
||||
@@ -181,7 +180,7 @@
|
||||
<th class="px-2 py-3">Key</th>
|
||||
<th class="px-2 py-3">Expired At</th>
|
||||
<th class="px-2 py-3">Quota</th>
|
||||
<th class="px-2 py-3">Used Quota</th>
|
||||
<th class="px-2 py-3">Used</th>
|
||||
<th class="text-right px-2 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -198,8 +197,8 @@
|
||||
</td>
|
||||
<td class="px-2 py-3">{{ token.used_quota }}</td>
|
||||
<td class="text-right px-2 py-3">
|
||||
<button v-if="token.name!=='default'" class="btn btn-ghost btn-xs btn-square text-error" @click="revokeToken(token.id)"
|
||||
aria-label="Revoke token">
|
||||
<button v-if="token.name !== 'default'" class="btn btn-ghost btn-xs btn-square text-error"
|
||||
@click="revokeToken(token.id)" aria-label="Revoke token">
|
||||
<TrashIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</td>
|
||||
@@ -272,7 +271,7 @@ const updateUser = async () => {
|
||||
payload.password = user.value.password;
|
||||
}
|
||||
const res = await userStore.editUser(userId.value, payload);
|
||||
console.log('updateUser',res)
|
||||
console.log('updateUser', res)
|
||||
if (res.data?.code == 200) {
|
||||
setToast(`User ${userId.value} updated`, 'success');
|
||||
}
|
||||
@@ -293,9 +292,9 @@ const togglePasswordVisibility = () => {
|
||||
// 格式化角色
|
||||
const formatRole = (role) => {
|
||||
switch (true) {
|
||||
case role>10:
|
||||
case role > 10:
|
||||
return 'Root';
|
||||
case role>0:
|
||||
case role > 0:
|
||||
return 'Admin';
|
||||
default:
|
||||
return 'U';
|
||||
|
||||
@@ -27,6 +27,7 @@ require (
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkoukk/tiktoken-go v0.1.7
|
||||
github.com/sashabaranov/go-openai v1.32.2
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/exp v0.0.0-20241009180824-f66d83c29e7c
|
||||
@@ -50,25 +51,24 @@ require (
|
||||
cloud.google.com/go/iam v1.4.0 // indirect
|
||||
cloud.google.com/go/longrunning v0.6.4 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.12.3 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.13.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.4 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // 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.22.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
||||
github.com/go-sql-driver/mysql v1.8.1 // indirect
|
||||
github.com/go-webauthn/x v0.1.20 // indirect
|
||||
github.com/goccy/go-json v0.10.3 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
github.com/google/pprof v0.0.0-20240827171923-fa2c70bbbfe5 // indirect
|
||||
@@ -76,6 +76,7 @@ require (
|
||||
github.com/googleapis/enterprise-certificate-proxy v0.3.6 // indirect
|
||||
github.com/googleapis/gax-go/v2 v2.14.1 // indirect
|
||||
github.com/hajimehoshi/go-mp3 v0.3.4 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect
|
||||
github.com/jackc/pgx/v5 v5.5.5 // indirect
|
||||
@@ -83,7 +84,7 @@ require (
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
@@ -93,6 +94,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/spf13/pflag v1.0.6 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
@@ -104,7 +106,7 @@ require (
|
||||
go.opentelemetry.io/otel v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.35.0 // indirect
|
||||
golang.org/x/arch v0.11.0 // indirect
|
||||
golang.org/x/arch v0.16.0 // indirect
|
||||
golang.org/x/net v0.39.0 // indirect
|
||||
golang.org/x/oauth2 v0.28.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
|
||||
@@ -25,17 +25,17 @@ github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496 h1:zV3ejI06
|
||||
github.com/asaskevich/govalidator v0.0.0-20200108200545-475eaeb16496/go.mod h1:oGkLhpf+kjZl6xBf758TQhh5XrAeiJv/7FRz/2spLIg=
|
||||
github.com/bluele/gcache v0.0.2 h1:WcbfdXICg7G/DGBh1PFfcirkWOQV+v077yF1pSy3DGw=
|
||||
github.com/bluele/gcache v0.0.2/go.mod h1:m15KV+ECjptwSPxKhOhQoAFQVtUFjTVkc3H8o0t/fp0=
|
||||
github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
|
||||
github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
|
||||
github.com/bytedance/sonic v1.13.2 h1:8/H1FempDZqC4VqjptGo14QQlJx8VdZJegxs6wwfqpQ=
|
||||
github.com/bytedance/sonic v1.13.2/go.mod h1:o68xyaF9u2gvVBuGHPlUVCy+ZfmNNO5ETf1+KgkJhz4=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
|
||||
github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/bytedance/sonic/loader v0.2.4 h1:ZWCw4stuXUsn1/+zQDqeE7JKP+QO47tz7QCNan80NzY=
|
||||
github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coder/websocket v1.8.12 h1:5bUXkEPPIbewrnkU8LTCLVaxi4N4J8ahufH2vlo4NAo=
|
||||
github.com/coder/websocket v1.8.12/go.mod h1:LNVeNrXQZfe5qhS9ALED3uA+l5pPqvwXg3CKoDBB2gs=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/d4l3k/messagediff v1.2.2-0.20190829033028-7e0a312ae40b/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo=
|
||||
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=
|
||||
@@ -52,14 +52,14 @@ github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2
|
||||
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0 h1:fFtUGXUzXPHTIUdne5+zzMPTfffl3RD5qYnkY40vtxU=
|
||||
github.com/fxamacker/cbor/v2 v2.8.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||
github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
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-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/glebarez/go-sqlite v1.22.0 h1:uAcMJhaA6r3LHMTFgP0SifzgXg46yJkgxqyuyec+ruQ=
|
||||
@@ -82,8 +82,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
||||
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.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
|
||||
github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-sql-driver/mysql v1.7.0/go.mod h1:OXbVy3sEdcQ2Doequ6Z5BW6fXNQTmx+9S1MCJN5yJMI=
|
||||
github.com/go-sql-driver/mysql v1.8.1 h1:LedoTUt/eveggdHS9qUFC1EFSa8bU2+1pZjSRpvNJ1Y=
|
||||
github.com/go-sql-driver/mysql v1.8.1/go.mod h1:wEBSXgmK//2ZFJyE+qWnIsVGmvmEKlqwuVSjsCm7DZg=
|
||||
@@ -91,8 +91,8 @@ github.com/go-webauthn/webauthn v0.12.3 h1:hHQl1xkUuabUU9uS+ISNCMLs9z50p9mDUZI/F
|
||||
github.com/go-webauthn/webauthn v0.12.3/go.mod h1:4JRe8Z3W7HIw8NGEWn2fnUwecoDzkkeach/NnvhkqGY=
|
||||
github.com/go-webauthn/x v0.1.20 h1:brEBDqfiPtNNCdS/peu8gARtq8fIPsHz0VzpPjGvgiw=
|
||||
github.com/go-webauthn/x v0.1.20/go.mod h1:n/gAc8ssZJGATM0qThE+W+vfgXiMedsWi3wf/C4lld0=
|
||||
github.com/goccy/go-json v0.10.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
|
||||
github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible h1:IfV12K8xAKAnZqdXVzCZ+TOjboZ2keLg81eXfW3O+oY=
|
||||
github.com/golang-jwt/jwt v3.2.2+incompatible/go.mod h1:8pz2t5EyA70fFQQSrl6XZXzqecmYZeUEB8OUGHkxJ+I=
|
||||
github.com/golang-jwt/jwt/v5 v5.2.2 h1:Rl4B7itRWVtYIHFrSNd7vhTiz9UpLdi6gZhZ3wEeDy8=
|
||||
@@ -130,6 +130,8 @@ github.com/hajimehoshi/oto v0.7.1/go.mod h1:wovJ8WWMfFKvP587mhHgot/MBr4DnNy9m6Ee
|
||||
github.com/hajimehoshi/oto/v2 v2.3.1/go.mod h1:seWLbgHH7AyUMYKfKYT9pg7PhUu9/SisyJvNTT+ASQo=
|
||||
github.com/icza/bitio v1.0.0/go.mod h1:0jGnlLAx8MKMr9VGnn/4YrvZiprkvBelsVIbA9Jjr9A=
|
||||
github.com/icza/mighty v0.0.0-20180919140131-cfd07d671de6/go.mod h1:xQig96I1VNBDIWGCdTt54nHt6EeI639SmHycLYL7FkA=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
|
||||
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
|
||||
github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk=
|
||||
@@ -149,8 +151,8 @@ github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwA
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10 h1:tBs3QSyvjDyFTq3uoc/9xFpCuOsJQFNPiAhYdw2skhE=
|
||||
github.com/klauspost/cpuid/v2 v2.2.10/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
@@ -194,17 +196,24 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sashabaranov/go-openai v1.32.2 h1:8z9PfYaLPbRzmJIYpwcWu6z3XU8F+RwVMF1QRSeSF2M=
|
||||
github.com/sashabaranov/go-openai v1.32.2/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg=
|
||||
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
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/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
@@ -236,8 +245,8 @@ go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5J
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
|
||||
golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package cli
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/pkg/store"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/duke-git/lancet/v2/fileutil"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var LoadCmd = &cobra.Command{
|
||||
Use: "load",
|
||||
Short: "import user.json -> db",
|
||||
Long: "\nimport user.json -> db",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
db := store.GetDB()
|
||||
var cont int64
|
||||
if err := db.Model(model.User{}).Count(&cont).Error; err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
if cont == 0 {
|
||||
fmt.Println("创建管理员之后再操作")
|
||||
}
|
||||
if !fileutil.IsExist("./db/user.json") {
|
||||
log.Fatalln("404! user.json is not found.")
|
||||
return
|
||||
}
|
||||
file, err := os.Open("./db/user.json")
|
||||
if err != nil {
|
||||
fmt.Println("Error opening file:", err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
var usermap []map[string]string
|
||||
|
||||
if err := json.NewDecoder(file).Decode(&usermap); err != nil {
|
||||
fmt.Println("解析文件失败:", err)
|
||||
return
|
||||
}
|
||||
for _, um := range usermap {
|
||||
var name string
|
||||
if um["username"] != "" {
|
||||
name = um["name"]
|
||||
} else if um["name"] == "" {
|
||||
name = um["username"]
|
||||
} else {
|
||||
fmt.Println("获取不到数据")
|
||||
continue
|
||||
}
|
||||
var user = model.User{
|
||||
Username: name,
|
||||
Name: name,
|
||||
Tokens: []model.Token{
|
||||
{
|
||||
Name: "default",
|
||||
Key: "sk-team-" + strings.ReplaceAll(uuid.New().String(), "-", ""),
|
||||
},
|
||||
{
|
||||
Name: name,
|
||||
Key: um["token"],
|
||||
},
|
||||
},
|
||||
}
|
||||
if err := db.Create(&user).Error; err != nil {
|
||||
fmt.Printf("\nCreate User %s Error:%s", user.Username, err)
|
||||
}
|
||||
}
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
var SaveCmd = &cobra.Command{
|
||||
Use: "save",
|
||||
Short: "backup user info -> user.json",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// SaveCmd.Flags().StringP("user", "u", "", "Save User")
|
||||
}
|
||||
@@ -2,6 +2,20 @@ package consts
|
||||
|
||||
import "gorm.io/gorm"
|
||||
|
||||
const Logo = `
|
||||
____ _____
|
||||
/ __ \ |_ _|
|
||||
| | | |_ __ ___ _ __ | | ___ __ _ _ __ ___
|
||||
| | | | '_ \ / _ \ '_ \ | | / _ \/ _' | '_ ' _ \
|
||||
| |__| | |_) | __/ | | | | || __/ (_| | | | | | |
|
||||
\____/| .__/ \___|_| |_| \_/ \___|\__,_|_| |_| |_|
|
||||
| |
|
||||
|_|
|
||||
|
||||
https://github.com/mirrors2/openteam
|
||||
---------------------------------------------------
|
||||
`
|
||||
|
||||
const SecretKey = "openteam"
|
||||
|
||||
const Day = 24 * 60 * 60 // day := 86400
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"opencatd-open/internal/consts"
|
||||
"opencatd-open/internal/dto"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/internal/utils"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -18,13 +21,22 @@ func (a Api) CreateApiKey(c *gin.Context) {
|
||||
dto.Fail(c, 403, "Permission denied")
|
||||
return
|
||||
}
|
||||
req := new(model.ApiKey)
|
||||
err := c.ShouldBind(&req)
|
||||
newkey := new(model.ApiKey)
|
||||
err := c.ShouldBind(newkey)
|
||||
if err != nil {
|
||||
dto.Fail(c, 400, err.Error())
|
||||
}
|
||||
if slice.Contain([]string{"openai", "azure", "claude"}, *newkey.ApiType) {
|
||||
sma, err := utils.FetchKeyModel(a.db, newkey)
|
||||
if err == nil && len(sma) > 0 {
|
||||
newkey.SupportModelsArray = sma
|
||||
var buf = new(bytes.Buffer)
|
||||
json.NewEncoder(buf).Encode(sma) //nolint:errcheck
|
||||
newkey.SupportModels = utils.ToPtr(buf.String())
|
||||
}
|
||||
}
|
||||
|
||||
err = a.keyService.CreateApiKey(c, req)
|
||||
err = a.keyService.CreateApiKey(c, newkey)
|
||||
if err != nil {
|
||||
dto.Fail(c, 400, err.Error())
|
||||
} else {
|
||||
@@ -71,6 +83,19 @@ func (a Api) ListApiKey(c *gin.Context) {
|
||||
if err != nil {
|
||||
dto.Fail(c, 500, err.Error())
|
||||
} else {
|
||||
for _, key := range keys {
|
||||
str := *key.ApiKey
|
||||
slen := len(str)
|
||||
if slen > 20 {
|
||||
slen = 20
|
||||
}
|
||||
str = str[:slen]
|
||||
key.ApiKey = &str
|
||||
|
||||
var sma []string
|
||||
json.NewDecoder(strings.NewReader(*key.SupportModels)).Decode(&sma) //nolint:errcheck
|
||||
key.SupportModelsArray = sma
|
||||
}
|
||||
dto.Success(c, gin.H{
|
||||
"total": total,
|
||||
"keys": keys,
|
||||
|
||||
@@ -2,11 +2,13 @@ package controller
|
||||
|
||||
import (
|
||||
"opencatd-open/internal/service"
|
||||
"opencatd-open/pkg/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Api struct {
|
||||
cfg *config.Config
|
||||
db *gorm.DB
|
||||
userService *service.UserServiceImpl
|
||||
tokenService *service.TokenServiceImpl
|
||||
@@ -15,8 +17,9 @@ type Api struct {
|
||||
usageService *service.UsageService
|
||||
}
|
||||
|
||||
func NewApi(db *gorm.DB, userService *service.UserServiceImpl, tokenService *service.TokenServiceImpl, keyService *service.ApiKeyServiceImpl, webAuthService *service.WebAuthnService, usageService *service.UsageService) *Api {
|
||||
func NewApi(cfg *config.Config, db *gorm.DB, userService *service.UserServiceImpl, tokenService *service.TokenServiceImpl, keyService *service.ApiKeyServiceImpl, webAuthService *service.WebAuthnService, usageService *service.UsageService) *Api {
|
||||
return &Api{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
userService: userService,
|
||||
tokenService: tokenService,
|
||||
|
||||
@@ -4,15 +4,23 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"opencatd-open/internal/dto"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/llm"
|
||||
"opencatd-open/llm/claude/v2"
|
||||
"opencatd-open/llm/google/v2"
|
||||
"opencatd-open/llm/openai_compatible"
|
||||
"opencatd-open/pkg/tokenizer"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (h *Proxy) ChatHandler(c *gin.Context) {
|
||||
user := c.MustGet("user").(*model.User)
|
||||
if user == nil {
|
||||
dto.WrapErrorAsOpenAI(c, 401, "Unauthorized")
|
||||
return
|
||||
}
|
||||
|
||||
var chatreq llm.ChatRequest
|
||||
if err := c.ShouldBindJSON(&chatreq); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
@@ -35,11 +43,11 @@ func (h *Proxy) ChatHandler(c *gin.Context) {
|
||||
fallthrough
|
||||
default:
|
||||
llm, err = openai_compatible.NewOpenAICompatible(h.apikey)
|
||||
}
|
||||
if err != nil {
|
||||
dto.WrapErrorAsOpenAI(c, 500, fmt.Errorf("create llm client error: %w", err).Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if !chatreq.Stream {
|
||||
resp, err := llm.Chat(c, chatreq)
|
||||
@@ -57,4 +65,13 @@ func (h *Proxy) ChatHandler(c *gin.Context) {
|
||||
c.SSEvent("", data)
|
||||
}
|
||||
}
|
||||
|
||||
llmusage := llm.GetTokenUsage()
|
||||
llmusage.User = user
|
||||
llmusage.TokenID = c.GetInt64("token_id")
|
||||
cost := tokenizer.Cost(llmusage.Model, llmusage.PromptTokens+llmusage.ToolsTokens, llmusage.CompletionTokens)
|
||||
|
||||
h.SendUsage(llmusage)
|
||||
defer fmt.Println("cost:", cost, "prompt_tokens:", llmusage.PromptTokens, "completion_tokens:", llmusage.CompletionTokens, "total_tokens:", llmusage.TotalTokens)
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"opencatd-open/internal/dto"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (p *Proxy) HandleModels(c *gin.Context) {
|
||||
models, err := p.getModelCache()
|
||||
if err != nil {
|
||||
dto.Fail(c, http.StatusBadGateway, err.Error())
|
||||
return
|
||||
}
|
||||
type _model struct {
|
||||
ID string `json:"id"`
|
||||
}
|
||||
var ms []_model
|
||||
for _, model := range models {
|
||||
ms = append(ms, _model{ID: model})
|
||||
}
|
||||
dto.Success(c, ms)
|
||||
}
|
||||
|
||||
func (p *Proxy) setModelCache() error {
|
||||
apikeys, err := p.apiKeyDao.FindKeys(nil)
|
||||
models := make(map[string]bool)
|
||||
if err == nil && len(apikeys) > 0 {
|
||||
for _, k := range apikeys {
|
||||
if len(k.SupportModelsArray) > 0 {
|
||||
for _, sm := range k.SupportModelsArray {
|
||||
models[sm] = true
|
||||
}
|
||||
} else {
|
||||
var sma []string
|
||||
json.Unmarshal([]byte(*k.SupportModels), &sma) // nolint:errCheck
|
||||
for _, sm := range sma {
|
||||
models[sm] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return fmt.Errorf("empty data")
|
||||
}
|
||||
var support_models []string
|
||||
for m, _ := range models {
|
||||
support_models = append(support_models, m)
|
||||
}
|
||||
return p.cache.Set("models", support_models)
|
||||
}
|
||||
|
||||
func (p *Proxy) getModelCache() ([]string, error) {
|
||||
models, err := p.cache.Get("models")
|
||||
return models.([]string), err
|
||||
}
|
||||
@@ -12,17 +12,20 @@ import (
|
||||
"net/url"
|
||||
"opencatd-open/internal/dao"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/internal/utils"
|
||||
"opencatd-open/llm"
|
||||
"opencatd-open/pkg/config"
|
||||
"opencatd-open/pkg/tokenizer"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/bluele/gcache"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/lib/pq"
|
||||
"github.com/tidwall/gjson"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type Proxy struct {
|
||||
@@ -30,9 +33,10 @@ type Proxy struct {
|
||||
cfg *config.Config
|
||||
db *gorm.DB
|
||||
wg *sync.WaitGroup
|
||||
usageChan chan *model.Usage // 用于异步处理的channel
|
||||
usageChan chan *llm.TokenUsage // 用于异步处理的channel
|
||||
apikey *model.ApiKey
|
||||
httpClient *http.Client
|
||||
cache gcache.Cache
|
||||
|
||||
userDAO *dao.UserDAO
|
||||
apiKeyDao *dao.ApiKeyDAO
|
||||
@@ -52,13 +56,15 @@ func NewProxy(ctx context.Context, cfg *config.Config, db *gorm.DB, wg *sync.Wai
|
||||
client.Transport = tr
|
||||
}
|
||||
}
|
||||
|
||||
np := &Proxy{
|
||||
ctx: ctx,
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
wg: wg,
|
||||
httpClient: client,
|
||||
usageChan: make(chan *model.Usage, cfg.UsageChanSize),
|
||||
cache: gcache.New(1).Build(),
|
||||
usageChan: make(chan *llm.TokenUsage, cfg.UsageChanSize),
|
||||
userDAO: userDAO,
|
||||
apiKeyDao: apiKeyDAO,
|
||||
tokenDAO: tokenDAO,
|
||||
@@ -68,7 +74,7 @@ func NewProxy(ctx context.Context, cfg *config.Config, db *gorm.DB, wg *sync.Wai
|
||||
|
||||
go np.ProcessUsage()
|
||||
go np.ScheduleTask()
|
||||
|
||||
np.setModelCache()
|
||||
return np
|
||||
}
|
||||
|
||||
@@ -77,9 +83,13 @@ func (p *Proxy) HandleProxy(c *gin.Context) {
|
||||
p.ChatHandler(c)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(c.Request.URL.Path, "/v1/messages") {
|
||||
p.ProxyClaude(c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) SendUsage(usage *model.Usage) {
|
||||
func (p *Proxy) SendUsage(usage *llm.TokenUsage) {
|
||||
select {
|
||||
case p.usageChan <- usage:
|
||||
default:
|
||||
@@ -135,47 +145,91 @@ func (p *Proxy) ProcessUsage() {
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Proxy) Do(usage *model.Usage) error {
|
||||
func (p *Proxy) Do(llmusage *llm.TokenUsage) error {
|
||||
err := p.db.Transaction(func(tx *gorm.DB) error {
|
||||
now := time.Now()
|
||||
today, _ := time.Parse("2006-01-02", now.Format("2006-01-02"))
|
||||
|
||||
cost := tokenizer.Cost(llmusage.Model, llmusage.PromptTokens, llmusage.CompletionTokens)
|
||||
token, err := p.tokenDAO.GetByID(p.ctx, llmusage.TokenID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
usage := &model.Usage{
|
||||
UserID: llmusage.User.ID,
|
||||
TokenID: llmusage.TokenID,
|
||||
Date: now,
|
||||
Model: llmusage.Model,
|
||||
Stream: llmusage.Stream,
|
||||
PromptTokens: llmusage.PromptTokens,
|
||||
CompletionTokens: llmusage.CompletionTokens,
|
||||
TotalTokens: llmusage.TotalTokens,
|
||||
Cost: fmt.Sprintf("%.8f", cost),
|
||||
}
|
||||
// 1. 记录使用记录
|
||||
if err := tx.WithContext(p.ctx).Create(usage).Error; err != nil {
|
||||
return fmt.Errorf("create usage error: %w", err)
|
||||
}
|
||||
|
||||
// 2. 更新每日统计(upsert 操作)
|
||||
dailyUsage := model.DailyUsage{
|
||||
UserID: usage.UserID,
|
||||
TokenID: usage.TokenID,
|
||||
Capability: usage.Capability,
|
||||
Date: time.Date(usage.Date.Year(), usage.Date.Month(), usage.Date.Day(), 0, 0, 0, 0, usage.Date.Location()),
|
||||
Model: usage.Model,
|
||||
Stream: usage.Stream,
|
||||
PromptTokens: usage.PromptTokens,
|
||||
CompletionTokens: usage.CompletionTokens,
|
||||
TotalTokens: usage.TotalTokens,
|
||||
Cost: usage.Cost,
|
||||
// 2. 更新每日统计
|
||||
var dailyUsage model.DailyUsage
|
||||
result := tx.WithContext(p.ctx).Where("user_id = ? and date = ?", llmusage.User.ID, today).First(&dailyUsage)
|
||||
if result.RowsAffected == 0 {
|
||||
dailyUsage.UserID = llmusage.User.ID
|
||||
dailyUsage.TokenID = llmusage.TokenID
|
||||
dailyUsage.Date = today
|
||||
dailyUsage.Model = llmusage.Model
|
||||
dailyUsage.Stream = llmusage.Stream
|
||||
dailyUsage.PromptTokens = llmusage.PromptTokens
|
||||
dailyUsage.CompletionTokens = llmusage.CompletionTokens
|
||||
dailyUsage.TotalTokens = llmusage.TotalTokens
|
||||
dailyUsage.Cost = fmt.Sprintf("%.8f", cost)
|
||||
if err := tx.WithContext(p.ctx).Create(&dailyUsage).Error; err != nil {
|
||||
return fmt.Errorf("create daily usage error: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.WithContext(p.ctx).Model(&model.DailyUsage{}).Where("user_id = ? and date = ?", llmusage.User.ID, today).
|
||||
Updates(map[string]interface{}{
|
||||
"prompt_tokens": gorm.Expr("prompt_tokens + ?", llmusage.PromptTokens),
|
||||
"completion_tokens": gorm.Expr("completion_tokens + ?", llmusage.CompletionTokens),
|
||||
"total_tokens": gorm.Expr("total_tokens + ?", llmusage.TotalTokens),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update daily usage error: %w", err)
|
||||
}
|
||||
|
||||
// 使用 OnConflict 实现 upsert
|
||||
if err := tx.WithContext(p.ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "user_id"}, {Name: "token_id"}, {Name: "capability"}, {Name: "date"}}, // 唯一键
|
||||
DoUpdates: clause.Assignments(map[string]interface{}{
|
||||
"prompt_tokens": gorm.Expr("prompt_tokens + ?", usage.PromptTokens),
|
||||
"completion_tokens": gorm.Expr("completion_tokens + ?", usage.CompletionTokens),
|
||||
"total_tokens": gorm.Expr("total_tokens + ?", usage.TotalTokens),
|
||||
"cost": gorm.Expr("cost + ?", usage.Cost),
|
||||
}),
|
||||
}).Create(&dailyUsage).Error; err != nil {
|
||||
return fmt.Errorf("upsert daily usage error: %w", err)
|
||||
}
|
||||
|
||||
// 3. 更新用户额度
|
||||
if err := tx.WithContext(p.ctx).Model(&model.User{}).Where("id = ?", usage.UserID).Updates(map[string]interface{}{
|
||||
"quota": gorm.Expr("quota - ?", usage.Cost),
|
||||
"used_quota": gorm.Expr("used_quota + ?", usage.Cost),
|
||||
if *llmusage.User.UnlimitedQuota {
|
||||
if err := tx.WithContext(p.ctx).Model(&model.User{}).Where("id = ?", llmusage.User.ID).Updates(map[string]interface{}{
|
||||
"used_quota": gorm.Expr("used_quota + ?", fmt.Sprintf("%.8f", cost)),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update user quota and used_quota error: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.WithContext(p.ctx).Model(&model.User{}).Where("id = ?", llmusage.User.ID).Updates(map[string]interface{}{
|
||||
"quota": gorm.Expr("quota - ?", fmt.Sprintf("%.8f", cost)),
|
||||
"used_quota": gorm.Expr("used_quota + ?", fmt.Sprintf("%.8f", cost)),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update user quota and used_quota error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
//4 . 更新token额度
|
||||
if *token.UnlimitedQuota {
|
||||
if err := tx.WithContext(p.ctx).Model(&model.Token{}).Where("id = ?", llmusage.TokenID).Updates(map[string]interface{}{
|
||||
"used_quota": gorm.Expr("used_quota + ?", fmt.Sprintf("%.8f", cost)),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update token quota and used_quota error: %w", err)
|
||||
}
|
||||
} else {
|
||||
if err := tx.WithContext(p.ctx).Model(&model.Token{}).Where("id = ?", llmusage.TokenID).Updates(map[string]interface{}{
|
||||
"quota": gorm.Expr("quota - ?", fmt.Sprintf("%.8f", cost)),
|
||||
"used_quota": gorm.Expr("used_quota + ?", fmt.Sprintf("%.8f", cost)),
|
||||
}).Error; err != nil {
|
||||
return fmt.Errorf("update token quota and used_quota error: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
@@ -184,14 +238,9 @@ func (p *Proxy) Do(usage *model.Usage) error {
|
||||
|
||||
func (p *Proxy) SelectApiKey(model string) error {
|
||||
akpikeys, err := p.apiKeyDao.FindApiKeysBySupportModel(p.db, model)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(akpikeys) == 0 {
|
||||
return errors.New("no available apikey")
|
||||
} else {
|
||||
if strings.HasPrefix(model, "gpt") {
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]any{"type = ?": "openai"})
|
||||
if err != nil || len(akpikeys) == 0 {
|
||||
if strings.HasPrefix(model, "gpt") || strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3") || strings.HasPrefix(model, "o4") {
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]any{"active = ?": true, "apitype = ?": "openai"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -199,7 +248,7 @@ func (p *Proxy) SelectApiKey(model string) error {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(model, "gemini") {
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]any{"type = ?": "gemini"})
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]any{"active = ?": true, "apitype = ?": "gemini"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -207,7 +256,7 @@ func (p *Proxy) SelectApiKey(model string) error {
|
||||
}
|
||||
|
||||
if strings.HasPrefix(model, "claude") {
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]any{"type = ?": "claude"})
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]any{"active = ?": true, "apitype = ?": "claude"})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -216,7 +265,6 @@ func (p *Proxy) SelectApiKey(model string) error {
|
||||
}
|
||||
if len(akpikeys) == 0 {
|
||||
return errors.New("no available apikey")
|
||||
|
||||
}
|
||||
|
||||
if len(akpikeys) == 1 {
|
||||
@@ -232,7 +280,7 @@ func (p *Proxy) SelectApiKey(model string) error {
|
||||
|
||||
func (p *Proxy) updateSupportModel() {
|
||||
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]interface{}{"type in ?": "openai,azure,claude"})
|
||||
keys, err := p.apiKeyDao.FindKeys(map[string]interface{}{"apitype in ?": []string{"openai", "azure", "claude"}})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
@@ -275,7 +323,10 @@ func (p *Proxy) ScheduleTask() {
|
||||
select {
|
||||
case <-time.After(time.Duration(p.cfg.TaskTimeInterval) * time.Minute):
|
||||
p.updateSupportModel()
|
||||
|
||||
case <-time.After(time.Hour * 12):
|
||||
if err := p.setModelCache(); err != nil {
|
||||
fmt.Println("refrash model cache err:", err)
|
||||
}
|
||||
case <-p.ctx.Done():
|
||||
fmt.Println("schedule task done")
|
||||
return
|
||||
@@ -292,6 +343,9 @@ func (p *Proxy) getOpenAISupportModels(apikey model.ApiKey) ([]string, error) {
|
||||
var supportModels []string
|
||||
var req *http.Request
|
||||
if *apikey.ApiType == "azure" {
|
||||
if strings.HasSuffix(*apikey.Endpoint, "/") {
|
||||
apikey.Endpoint = utils.ToPtr(strings.TrimSuffix(*apikey.Endpoint, "/"))
|
||||
}
|
||||
req, _ = http.NewRequest("GET", *apikey.Endpoint+azureModelsUrl, nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("api-key", *apikey.ApiKey)
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (p *Proxy) ProxyClaude(c *gin.Context) {
|
||||
fmt.Println(c.Request.URL.String())
|
||||
data, _ := io.ReadAll(c.Request.Body)
|
||||
fmt.Println(string(data))
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"opencatd-open/internal/dto"
|
||||
"opencatd-open/internal/model"
|
||||
@@ -142,7 +141,7 @@ func (a Api) CreateUser(c *gin.Context) {
|
||||
dto.Fail(c, 400, err.Error())
|
||||
return
|
||||
}
|
||||
fmt.Printf("user:%+v\n", user)
|
||||
|
||||
err = a.userService.Create(c, &user)
|
||||
if err != nil {
|
||||
dto.Fail(c, http.StatusInternalServerError, err.Error())
|
||||
|
||||
@@ -90,7 +90,7 @@ func (a Api) ResetToken(c *gin.Context) {
|
||||
dto.Fail(c, http.StatusNotFound, "token not found")
|
||||
return
|
||||
}
|
||||
token.UsedQuota = utils.ToPtr(int64(0))
|
||||
token.UsedQuota = utils.ToPtr(float64(0))
|
||||
|
||||
err = a.tokenService.UpdateToken(c, token)
|
||||
if err != nil {
|
||||
|
||||
@@ -3,6 +3,7 @@ package dao
|
||||
import (
|
||||
"errors"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/internal/utils"
|
||||
"opencatd-open/pkg/config"
|
||||
|
||||
"gorm.io/gorm"
|
||||
@@ -29,8 +30,8 @@ type ApiKeyDAO struct {
|
||||
db *gorm.DB
|
||||
}
|
||||
|
||||
func NewApiKeyDAO(db *gorm.DB) *ApiKeyDAO {
|
||||
return &ApiKeyDAO{db: db}
|
||||
func NewApiKeyDAO(cfg *config.Config, db *gorm.DB) *ApiKeyDAO {
|
||||
return &ApiKeyDAO{cfg: cfg, db: db}
|
||||
}
|
||||
|
||||
// CreateApiKey 创建ApiKey
|
||||
@@ -38,6 +39,9 @@ func (dao *ApiKeyDAO) Create(apiKey *model.ApiKey) error {
|
||||
if apiKey == nil {
|
||||
return errors.New("apiKey is nil")
|
||||
}
|
||||
if len(*apiKey.SupportModels) < 2 {
|
||||
apiKey.SupportModels = utils.ToPtr("[]")
|
||||
}
|
||||
return dao.db.Create(apiKey).Error
|
||||
}
|
||||
|
||||
@@ -87,14 +91,21 @@ func (dao *ApiKeyDAO) FindApiKeysBySupportModel(db *gorm.DB, modelName string) (
|
||||
var apiKeys []model.ApiKey
|
||||
switch dao.cfg.DB_Type {
|
||||
case "mysql":
|
||||
return nil, errors.New("not support")
|
||||
err := db.Raw(`
|
||||
SELECT *
|
||||
FROM apikeys
|
||||
WHERE active = true
|
||||
AND JSON_CONTAINS(support_models, ?, '$')`, modelName).
|
||||
Scan(&apiKeys).Error
|
||||
return apiKeys, err
|
||||
case "postgres":
|
||||
return nil, errors.New("not support")
|
||||
}
|
||||
err := db.Model(&model.ApiKey{}).
|
||||
Joins("CROSS JOIN JSON_EACH(apikeys.support_models)").
|
||||
Where("value = ?", modelName).
|
||||
Find(&apiKeys).Error
|
||||
err := db.Raw(`
|
||||
SELECT a.*
|
||||
FROM apikeys a
|
||||
JOIN json_each(a.support_models) AS je ON je.value = ?
|
||||
WHERE a.active = true`, modelName).Scan(&apiKeys).Error
|
||||
return apiKeys, err
|
||||
}
|
||||
|
||||
|
||||
@@ -212,11 +212,7 @@ func (d *DailyUsageDAO) UpsertDailyUsage(ctx context.Context, usage *model.Usage
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "user_id"},
|
||||
{Name: "token_id"},
|
||||
{Name: "capability"},
|
||||
{Name: "date"},
|
||||
{Name: "model"},
|
||||
{Name: "stream"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(updateColumns),
|
||||
}).Create(dailyUsage).Error
|
||||
@@ -231,11 +227,7 @@ func (d *DailyUsageDAO) UpsertDailyUsage(ctx context.Context, usage *model.Usage
|
||||
return db.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{
|
||||
{Name: "user_id"},
|
||||
{Name: "token_id"},
|
||||
{Name: "capability"},
|
||||
{Name: "date"},
|
||||
{Name: "model"},
|
||||
{Name: "stream"},
|
||||
},
|
||||
DoUpdates: clause.Assignments(updateColumns),
|
||||
}).Create(dailyUsage).Error
|
||||
@@ -244,8 +236,8 @@ func (d *DailyUsageDAO) UpsertDailyUsage(ctx context.Context, usage *model.Usage
|
||||
default:
|
||||
return db.Transaction(func(tx *gorm.DB) error {
|
||||
var existing model.DailyUsage
|
||||
err := tx.Where("user_id = ? AND token_id = ? AND capability = ? AND date = ? AND model = ? AND stream = ?",
|
||||
usage.UserID, usage.TokenID, usage.Capability, date, usage.Model, usage.Stream).
|
||||
err := tx.Where("user_id = ? AND date = ?",
|
||||
usage.UserID, date).
|
||||
First(&existing).Error
|
||||
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
|
||||
@@ -10,7 +10,7 @@ type ApiKey_PG struct {
|
||||
Active *bool `gorm:"column:active;default:true" json:"active,omitempty"`
|
||||
Endpoint *string `gorm:"column:endpoint" json:"endpoint,omitempty"`
|
||||
ResourceNmae *string `gorm:"column:resource_name" json:"resource_name,omitempty"`
|
||||
DeploymentName *string `gorm:"column:deployment_name" json:"deployment_name,omitempty"`
|
||||
// DeploymentName *string `gorm:"column:deployment_name" json:"deployment_name,omitempty"`
|
||||
ApiSecret *string `gorm:"column:api_secret" json:"api_secret,omitempty"`
|
||||
ModelPrefix *string `gorm:"column:model_prefix" json:"model_prefix,omitempty"`
|
||||
ModelAlias *string `gorm:"column:model_alias" json:"model_alias,omitempty"`
|
||||
@@ -33,7 +33,7 @@ type ApiKey struct {
|
||||
Active *bool `gorm:"column:active;default:true" json:"active,omitempty"`
|
||||
Endpoint *string `gorm:"column:endpoint" json:"endpoint,omitempty"`
|
||||
ResourceNmae *string `gorm:"column:resource_name" json:"resource_name,omitempty"`
|
||||
DeploymentName *string `gorm:"column:deployment_name" json:"deployment_name,omitempty"`
|
||||
// DeploymentName *string `gorm:"column:deployment_name" json:"deployment_name,omitempty"`
|
||||
AccessKey *string `gorm:"column:access_key" json:"access_key,omitempty"`
|
||||
SecretKey *string `gorm:"column:secret_key" json:"secret_key,omitempty"`
|
||||
ModelPrefix *string `gorm:"column:model_prefix" json:"model_prefix,omitempty"`
|
||||
|
||||
@@ -7,9 +7,9 @@ type Token struct {
|
||||
Name string `gorm:"column:name;not null;index:idx_token_name" json:"name,omitempty" binding:"required,min=1,max=20"`
|
||||
Key string `gorm:"column:key;not null;uniqueIndex:idx_token_key;comment:token key" json:"key,omitempty"`
|
||||
Active *bool `gorm:"column:active;default:true" json:"active,omitempty"` //
|
||||
Quota *int64 `gorm:"column:quota;type:bigint;default:0" json:"quota,omitempty"` // default 0
|
||||
Quota *float64 `gorm:"column:quota;type:bigint;default:0" json:"quota,omitempty"` // default 0
|
||||
UnlimitedQuota *bool `gorm:"column:unlimited_quota;default:true" json:"unlimited_quota,omitempty"` // set Quota 1 unlimited
|
||||
UsedQuota *int64 `gorm:"column:used_quota;type:bigint;default:0" json:"used_quota,omitempty"`
|
||||
UsedQuota *float64 `gorm:"column:used_quota;type:bigint;default:0" json:"used_quota,omitempty"`
|
||||
ExpiredAt *int64 `gorm:"column:expired_at;type:bigint;default:0" json:"expired_at,omitempty"`
|
||||
NeverExpired *bool `gorm:"column:never_expires;type:bigint;" json:"never_expires,omitempty"`
|
||||
CreatedAt int64 `gorm:"column:created_at;type:bigint;autoCreateTime" json:"created_at,omitempty"`
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"opencatd-open/store"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type Usage struct {
|
||||
@@ -16,9 +12,9 @@ type Usage struct {
|
||||
Date time.Time `gorm:"column:date;autoCreateTime;index:idx_date"`
|
||||
Model string `gorm:"column:model"`
|
||||
Stream bool `gorm:"column:stream"`
|
||||
PromptTokens float64 `gorm:"column:prompt_tokens"`
|
||||
CompletionTokens float64 `gorm:"column:completion_tokens"`
|
||||
TotalTokens float64 `gorm:"column:total_tokens"`
|
||||
PromptTokens int `gorm:"column:prompt_tokens"`
|
||||
CompletionTokens int `gorm:"column:completion_tokens"`
|
||||
TotalTokens int `gorm:"column:total_tokens"`
|
||||
Cost string `gorm:"column:cost"`
|
||||
}
|
||||
|
||||
@@ -28,47 +24,18 @@ func (Usage) TableName() string {
|
||||
|
||||
type DailyUsage struct {
|
||||
ID int64 `gorm:"column:id;primaryKey;autoIncrement"`
|
||||
UserID int64 `gorm:"column:user_id;uniqueIndex:idx_daily_unique,priority:1"`
|
||||
TokenID int64 `gorm:"column:token_id;index:idx_daily_token_id"`
|
||||
Capability string `gorm:"column:capability;uniqueIndex:idx_daily_unique,priority:2;comment:模型能力"`
|
||||
UserID int64 `gorm:"column:user_id;uniqueIndex:idx_daily_unique,priority:1"` // uniqueIndex:idx_daily_unique,priority:1
|
||||
TokenID int64 `gorm:"column:token_id;uniqueIndex:idx_daily_unique,priority:2"`
|
||||
Capability string `gorm:"column:capability;index:idx_daily_usage_capability;comment:模型能力"`
|
||||
Date time.Time `gorm:"column:date;autoCreateTime;uniqueIndex:idx_daily_unique,priority:3"`
|
||||
Model string `gorm:"column:model"`
|
||||
Stream bool `gorm:"column:stream"`
|
||||
PromptTokens float64 `gorm:"column:prompt_tokens"`
|
||||
CompletionTokens float64 `gorm:"column:completion_tokens"`
|
||||
TotalTokens float64 `gorm:"column:total_tokens"`
|
||||
PromptTokens int `gorm:"column:prompt_tokens"`
|
||||
CompletionTokens int `gorm:"column:completion_tokens"`
|
||||
TotalTokens int `gorm:"column:total_tokens"`
|
||||
Cost string `gorm:"column:cost"`
|
||||
}
|
||||
|
||||
func (DailyUsage) TableName() string {
|
||||
return "daily_usages"
|
||||
}
|
||||
|
||||
func HandleUsage(c *gin.Context) {
|
||||
fromStr := c.Query("from")
|
||||
toStr := c.Query("to")
|
||||
getMonthStartAndEnd := func() (start, end string) {
|
||||
loc, _ := time.LoadLocation("Local")
|
||||
now := time.Now().In(loc)
|
||||
|
||||
year, month, _ := now.Date()
|
||||
|
||||
startOfMonth := time.Date(year, month, 1, 0, 0, 0, 0, loc)
|
||||
endOfMonth := startOfMonth.AddDate(0, 1, 0)
|
||||
|
||||
start = startOfMonth.Format("2006-01-02")
|
||||
end = endOfMonth.Format("2006-01-02")
|
||||
return
|
||||
}
|
||||
if fromStr == "" || toStr == "" {
|
||||
fromStr, toStr = getMonthStartAndEnd()
|
||||
}
|
||||
|
||||
usage, err := store.QueryUsage(fromStr, toStr)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, usage)
|
||||
}
|
||||
|
||||
@@ -52,9 +52,6 @@ func (s *ApiKeyServiceImpl) UpdateApiKey(ctx context.Context, apikey *model.ApiK
|
||||
if apikey.ResourceNmae != nil {
|
||||
_key.ResourceNmae = apikey.ResourceNmae
|
||||
}
|
||||
if apikey.DeploymentName != nil {
|
||||
_key.DeploymentName = apikey.DeploymentName
|
||||
}
|
||||
if apikey.AccessKey != nil {
|
||||
_key.AccessKey = apikey.AccessKey
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
"opencatd-open/internal/dto"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/internal/utils"
|
||||
"opencatd-open/pkg/config"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,12 +18,14 @@ import (
|
||||
)
|
||||
|
||||
type UserServiceImpl struct {
|
||||
cfg *config.Config
|
||||
db *gorm.DB
|
||||
userRepo dao.UserRepository
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, userRepo dao.UserRepository) *UserServiceImpl {
|
||||
func NewUserService(cfg *config.Config, db *gorm.DB, userRepo dao.UserRepository) *UserServiceImpl {
|
||||
return &UserServiceImpl{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
userRepo: userRepo,
|
||||
}
|
||||
@@ -40,11 +43,19 @@ func (s *UserServiceImpl) Register(ctx context.Context, req *model.User) error {
|
||||
_user.Role = utils.ToPtr(consts.RoleRoot)
|
||||
_user.Active = utils.ToPtr(true)
|
||||
_user.UnlimitedQuota = utils.ToPtr(true)
|
||||
} else {
|
||||
if !s.cfg.AllowRegister {
|
||||
return fmt.Errorf("register is not allowed")
|
||||
}
|
||||
}
|
||||
|
||||
_user.Password, err = utils.HashPassword(req.Password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_user.Active = &s.cfg.DefaultActive
|
||||
_user.UnlimitedQuota = &s.cfg.UnlimitedQuota
|
||||
|
||||
_user.Username = req.Username
|
||||
_user.Email = req.Email
|
||||
_user.Tokens = []model.Token{
|
||||
|
||||
@@ -62,6 +62,7 @@ func (u *WebAuthnUser) WebAuthnCredentialDescriptors() (descriptors []protocol.C
|
||||
|
||||
// WebAuthnService 提供WebAuthn相关功能
|
||||
type WebAuthnService struct {
|
||||
cfg *config.Config
|
||||
DB *gorm.DB
|
||||
WebAuthn *webauthn.WebAuthn
|
||||
// Sessions map[string]webauthn.SessionData // 用于存储注册和认证过程中的会话数据
|
||||
@@ -69,12 +70,12 @@ type WebAuthnService struct {
|
||||
}
|
||||
|
||||
// NewWebAuthnService 创建新的WebAuthn服务
|
||||
func NewWebAuthnService(db *gorm.DB, cfg *config.Config) (*WebAuthnService, error) {
|
||||
func NewWebAuthnService(cfg *config.Config, db *gorm.DB) (*WebAuthnService, error) {
|
||||
// 创建WebAuthn配置
|
||||
wconfig := &webauthn.Config{
|
||||
RPDisplayName: config.Cfg.AppName, // 依赖方(Relying Party)显示名称
|
||||
RPID: config.Cfg.Domain, // 依赖方ID(通常为域名)
|
||||
RPOrigins: []string{config.Cfg.AppURL}, // 依赖方源(URL)
|
||||
RPDisplayName: cfg.AppName, // 依赖方(Relying Party)显示名称
|
||||
RPID: cfg.RPID, // 依赖方ID(通常为域名)
|
||||
RPOrigins: cfg.RPOrigins, // 依赖方源(URL)
|
||||
AuthenticatorSelection: protocol.AuthenticatorSelection{
|
||||
RequireResidentKey: protocol.ResidentKeyRequired(), // 要求认证器存储用户 ID (resident key)
|
||||
ResidentKey: protocol.ResidentKeyRequirementRequired, // 使用 Discoverable 模式
|
||||
@@ -90,6 +91,7 @@ func NewWebAuthnService(db *gorm.DB, cfg *config.Config) (*WebAuthnService, erro
|
||||
}
|
||||
|
||||
return &WebAuthnService{
|
||||
cfg: cfg,
|
||||
DB: db,
|
||||
WebAuthn: wa,
|
||||
// Sessions: make(map[string]webauthn.SessionData),
|
||||
@@ -270,11 +272,22 @@ func (s *WebAuthnService) FinishLogin(challenge string, response *http.Request)
|
||||
// return nil, err
|
||||
// }
|
||||
var user *WebAuthnUser
|
||||
_, err = s.WebAuthn.FinishDiscoverableLogin(s.GetWebAuthnUser(&user), *sessionData, response)
|
||||
wc, err := s.WebAuthn.FinishDiscoverableLogin(s.GetWebAuthnUser(&user), *sessionData, response)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 更新Passkey的LastUsedAt
|
||||
// 更新Passkey 这里SignCount应该是由验证器上传,但可能为0,手动+1
|
||||
var pk model.Passkey
|
||||
if err := s.DB.Model(&model.Passkey{}).Where("credential_id = ?", base64.StdEncoding.EncodeToString(wc.ID)).First(&pk).Error; err == nil {
|
||||
if err := s.DB.Model(&model.Passkey{}).Where("id = ?", pk.ID, time.Now().Unix()).Updates(map[string]interface{}{
|
||||
"sign_count": pk.SignCount + 1,
|
||||
"last_used_at": time.Now().Unix(),
|
||||
}).Error; err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"opencatd-open/internal/model"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/tidwall/gjson"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var client = &http.Client{Timeout: 2 * time.Second}
|
||||
|
||||
func init() {
|
||||
if os.Getenv("LOCAL_PROXY") != "" {
|
||||
if proxyUrl, err := url.Parse(os.Getenv("LOCAL_PROXY")); err == nil {
|
||||
client.Transport = &http.Transport{Proxy: http.ProxyURL(proxyUrl)}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func FetchKeyModel(db *gorm.DB, key *model.ApiKey) ([]string, error) {
|
||||
|
||||
var supportModels []string
|
||||
var err error
|
||||
if *key.ApiType == "openai" || *key.ApiType == "azure" {
|
||||
supportModels, err = FetchOpenAISupportModels(db, key)
|
||||
}
|
||||
if *key.ApiType == "claude" {
|
||||
supportModels, err = FetchClaudeSupportModels(db, key)
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
}
|
||||
return supportModels, err
|
||||
}
|
||||
|
||||
func FetchOpenAISupportModels(db *gorm.DB, apikey *model.ApiKey) ([]string, error) {
|
||||
openaiModelsUrl := "https://api.openai.com/v1/models"
|
||||
// https://learn.microsoft.com/zh-cn/rest/api/azureopenai/models/list?view=rest-azureopenai-2025-02-01-preview&tabs=HTTP
|
||||
azureModelsUrl := "/openai/deployments?api-version=2022-12-01"
|
||||
|
||||
var supportModels []string
|
||||
var req *http.Request
|
||||
if *apikey.ApiType == "azure" {
|
||||
if strings.HasSuffix(*apikey.Endpoint, "/") {
|
||||
apikey.Endpoint = ToPtr(strings.TrimSuffix(*apikey.Endpoint, "/"))
|
||||
}
|
||||
req, _ = http.NewRequest("GET", *apikey.Endpoint+azureModelsUrl, nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("api-key", *apikey.ApiKey)
|
||||
} else {
|
||||
req, _ = http.NewRequest("GET", openaiModelsUrl, nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("Authorization", "Bearer "+*apikey.ApiKey)
|
||||
}
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
bytesbody, _ := io.ReadAll(resp.Body)
|
||||
result := gjson.GetBytes(bytesbody, "data.#.id").Array()
|
||||
for _, v := range result {
|
||||
model := v.Str
|
||||
model = strings.Replace(model, "-35-", "-3.5-", -1)
|
||||
model = strings.Replace(model, "-41-", "-4.1-", -1)
|
||||
supportModels = append(supportModels, model)
|
||||
}
|
||||
}
|
||||
return supportModels, nil
|
||||
}
|
||||
|
||||
func FetchClaudeSupportModels(db *gorm.DB, apikey *model.ApiKey) ([]string, error) {
|
||||
// https://docs.anthropic.com/en/api/models-list
|
||||
claudemodelsUrl := "https://api.anthropic.com/v1/models"
|
||||
var supportModels []string
|
||||
|
||||
req, _ := http.NewRequest("GET", claudemodelsUrl, nil)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
req.Header.Set("x-api-key", *apikey.ApiKey)
|
||||
req.Header.Set("anthropic-version", "2023-06-01")
|
||||
|
||||
resp, err := client.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusOK {
|
||||
bytesbody, _ := io.ReadAll(resp.Body)
|
||||
result := gjson.GetBytes(bytesbody, "data.#.id").Array()
|
||||
for _, v := range result {
|
||||
supportModels = append(supportModels, v.Str)
|
||||
}
|
||||
}
|
||||
return supportModels, nil
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models/all-models
|
||||
package claude
|
||||
|
||||
import (
|
||||
@@ -93,7 +94,9 @@ func (c *Claude) Chat(ctx context.Context, chatReq llm.ChatRequest) (*llm.ChatRe
|
||||
if chatReq.MaxTokens > 0 {
|
||||
maxTokens = chatReq.MaxTokens
|
||||
} else {
|
||||
if strings.Contains(chatReq.Model, "sonnet") || strings.Contains(chatReq.Model, "haiku") {
|
||||
if strings.Contains(chatReq.Model, "3-7") {
|
||||
maxTokens = 64000
|
||||
} else if strings.Contains(chatReq.Model, "3-5") {
|
||||
maxTokens = 8192
|
||||
} else {
|
||||
maxTokens = 4096
|
||||
@@ -110,6 +113,9 @@ func (c *Claude) Chat(ctx context.Context, chatReq llm.ChatRequest) (*llm.ChatRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if c.tokenUsage.Model == "" && resp.Model != "" {
|
||||
c.tokenUsage.Model = string(resp.Model)
|
||||
}
|
||||
c.tokenUsage.PromptTokens += resp.Usage.InputTokens
|
||||
c.tokenUsage.CompletionTokens += resp.Usage.OutputTokens
|
||||
c.tokenUsage.TotalTokens += resp.Usage.InputTokens + resp.Usage.OutputTokens
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
// https://github.com/google-gemini/api-examples/
|
||||
// https://ai.google.dev/gemini-api/docs/models?hl=zh-cn
|
||||
|
||||
package google
|
||||
|
||||
@@ -109,6 +110,9 @@ func (g *Gemini) Chat(ctx context.Context, chatReq llm.ChatRequest) (*llm.ChatRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if g.tokenUsage.Model == "" && response.ModelVersion != "" {
|
||||
g.tokenUsage.Model = response.ModelVersion
|
||||
}
|
||||
if response.UsageMetadata != nil {
|
||||
g.tokenUsage.PromptTokens += int(response.UsageMetadata.PromptTokenCount)
|
||||
g.tokenUsage.CompletionTokens += int(response.UsageMetadata.CandidatesTokenCount)
|
||||
|
||||
@@ -13,7 +13,7 @@ type LLM interface {
|
||||
|
||||
type llm struct {
|
||||
ApiKey *model.ApiKey
|
||||
Usage *model.Usage
|
||||
Usage *TokenUsage
|
||||
tools any // TODO
|
||||
Messages []any // TODO
|
||||
llm LLM
|
||||
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
"opencatd-open/llm"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
// https://learn.microsoft.com/en-us/azure/ai-services/openai/api-version-deprecation#latest-preview-api-releases
|
||||
@@ -86,6 +88,9 @@ func (o *OpenAICompatible) Chat(ctx context.Context, chatReq llm.ChatRequest) (*
|
||||
}
|
||||
var buildurl string
|
||||
if *o.ApiKey.Endpoint != "" {
|
||||
if strings.HasSuffix(*o.ApiKey.Endpoint, "/") {
|
||||
o.ApiKey.ApiKey = utils.ToPtr(strings.TrimSuffix(*o.ApiKey.Endpoint, "/"))
|
||||
}
|
||||
buildurl = fmt.Sprintf("%s/openai/deployments/%s/chat/completions?api-version=%s", *o.ApiKey.Endpoint, formatModel(chatReq.Model), AzureApiVersion)
|
||||
} else {
|
||||
buildurl = fmt.Sprintf("https://%s.openai.azure.com/openai/deployments/%s/chat/completions?api-version=%s", *o.ApiKey.ResourceNmae, formatModel(chatReq.Model), AzureApiVersion)
|
||||
@@ -116,6 +121,9 @@ func (o *OpenAICompatible) Chat(ctx context.Context, chatReq llm.ChatRequest) (*
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if o.tokenUsage.Model == "" && chatResp.Model != "" {
|
||||
o.tokenUsage.Model = chatResp.Model
|
||||
}
|
||||
o.tokenUsage.PromptTokens = chatResp.Usage.PromptTokens
|
||||
o.tokenUsage.CompletionTokens = chatResp.Usage.CompletionTokens
|
||||
o.tokenUsage.TotalTokens = chatResp.Usage.TotalTokens
|
||||
@@ -124,6 +132,7 @@ func (o *OpenAICompatible) Chat(ctx context.Context, chatReq llm.ChatRequest) (*
|
||||
|
||||
func (o *OpenAICompatible) StreamChat(ctx context.Context, chatReq llm.ChatRequest) (chan *llm.StreamChatResponse, error) {
|
||||
chatReq.Stream = true
|
||||
chatReq.StreamOptions = &openai.StreamOptions{IncludeUsage: true}
|
||||
dst, err := utils.StructToMap(chatReq)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -197,6 +206,7 @@ func (o *OpenAICompatible) StreamChat(ctx context.Context, chatReq llm.ChatReque
|
||||
if err := json.Unmarshal(line, &streamResp); err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
if streamResp.Usage != nil {
|
||||
o.tokenUsage.PromptTokens += streamResp.Usage.PromptTokens
|
||||
o.tokenUsage.CompletionTokens += streamResp.Usage.CompletionTokens
|
||||
|
||||
@@ -2,6 +2,7 @@ package llm
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"opencatd-open/internal/model"
|
||||
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
@@ -15,9 +16,13 @@ type StreamChatResponse openai.ChatCompletionStreamResponse
|
||||
type ChatMessage openai.ChatCompletionMessage
|
||||
|
||||
type TokenUsage struct {
|
||||
User *model.User
|
||||
TokenID int64
|
||||
Model string `json:"model"`
|
||||
Stream bool
|
||||
PromptTokens int `json:"prompt_tokens"`
|
||||
CompletionTokens int `json:"completion_tokens"`
|
||||
ToolsTokens int `json:"total_tokens"`
|
||||
ToolsTokens int `json:"tools_tokens"`
|
||||
TotalTokens int `json:"total_tokens"`
|
||||
}
|
||||
|
||||
|
||||
@@ -63,7 +63,7 @@ func AuthLLM(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
auth_token = auth_token[7:]
|
||||
token := model.Token{}
|
||||
if err := db.Preload("Users").First(&token, "token = ?", auth_token).Error; err != nil {
|
||||
if err := db.Preload("User").Where("key = ?", auth_token).First(&token).Error; err != nil {
|
||||
dto.WrapErrorAsOpenAI(c, http.StatusUnauthorized, "invalid_api_key")
|
||||
c.Abort()
|
||||
return
|
||||
@@ -94,6 +94,8 @@ func AuthLLM(db *gorm.DB) gin.HandlerFunc {
|
||||
}
|
||||
|
||||
c.Set("user", token.User)
|
||||
c.Set("user_id", token.User.ID)
|
||||
c.Set("token_id", token.ID)
|
||||
c.Set("authed", true)
|
||||
// 可以在这里对 token 进行验证并检查权限
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/joho/godotenv/autoload"
|
||||
@@ -14,15 +15,14 @@ var Cfg *Config
|
||||
// Config 结构体存储应用配置
|
||||
type Config struct {
|
||||
// 服务器配置
|
||||
ServerPort int
|
||||
ServerHost string
|
||||
Port int
|
||||
ReadTimeout time.Duration
|
||||
WriteTimeout time.Duration
|
||||
|
||||
// PassKey配置
|
||||
AppName string
|
||||
Domain string
|
||||
AppURL string
|
||||
AppName string // 依赖方(Relying Party)显示名称
|
||||
RPID string // 依赖方ID(通常为域名)
|
||||
RPOrigins []string // 依赖方源(URL)
|
||||
WebAuthnTimeout time.Duration
|
||||
ChallengeExpiration time.Duration
|
||||
|
||||
@@ -71,11 +71,11 @@ func init() {
|
||||
func LoadConfig() (*Config, error) {
|
||||
cfg := &Config{
|
||||
AppName: "OpenTeam",
|
||||
Domain: "localhost",
|
||||
AppURL: "https://localhost:5173",
|
||||
RPID: "localhost", // 域名
|
||||
RPOrigins: []string{"https://localhost:5173"},
|
||||
// 默认值设置
|
||||
ServerPort: 8080,
|
||||
ServerHost: "0.0.0.0",
|
||||
Port: 80,
|
||||
|
||||
ReadTimeout: 10 * time.Second,
|
||||
WriteTimeout: 10 * time.Second,
|
||||
|
||||
@@ -91,8 +91,8 @@ func LoadConfig() (*Config, error) {
|
||||
|
||||
// 系统设置
|
||||
AllowRegister: false,
|
||||
UnlimitedQuota: false,
|
||||
DefaultActive: false,
|
||||
UnlimitedQuota: true,
|
||||
DefaultActive: true,
|
||||
|
||||
UsageWorker: 1,
|
||||
UsageChanSize: 1000,
|
||||
@@ -103,26 +103,30 @@ func LoadConfig() (*Config, error) {
|
||||
if appName := os.Getenv("APP_NAME"); appName != "" {
|
||||
cfg.AppName = appName
|
||||
}
|
||||
if domain := os.Getenv("DOMAIN"); domain != "" {
|
||||
cfg.Domain = domain
|
||||
if domain := os.Getenv("RPID"); domain != "" {
|
||||
cfg.RPID = domain
|
||||
}
|
||||
if appURL := os.Getenv("APP_URL"); appURL != "" {
|
||||
cfg.AppURL = appURL
|
||||
if origin := os.Getenv("RPORIGINS"); origin != "" {
|
||||
var rpos []string
|
||||
list := strings.Split(origin, ",")
|
||||
for _, l := range list {
|
||||
trimmedl := strings.TrimSpace(l)
|
||||
if trimmedl != "" {
|
||||
rpos = append(rpos, trimmedl)
|
||||
}
|
||||
}
|
||||
cfg.RPOrigins = rpos
|
||||
}
|
||||
|
||||
// 服务器配置
|
||||
if port := os.Getenv("SERVER_PORT"); port != "" {
|
||||
if port := os.Getenv("PORT"); port != "" {
|
||||
if p, err := strconv.Atoi(port); err == nil {
|
||||
cfg.ServerPort = p
|
||||
cfg.Port = p
|
||||
} else {
|
||||
return nil, fmt.Errorf("无效的SERVER_PORT: %s", port)
|
||||
return nil, fmt.Errorf("PORT: %s", port)
|
||||
}
|
||||
}
|
||||
|
||||
if host := os.Getenv("SERVER_HOST"); host != "" {
|
||||
cfg.ServerHost = host
|
||||
}
|
||||
|
||||
if timeout := os.Getenv("READ_TIMEOUT"); timeout != "" {
|
||||
if t, err := strconv.Atoi(timeout); err == nil {
|
||||
cfg.ReadTimeout = time.Duration(t) * time.Second
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"log"
|
||||
"opencatd-open/internal/model"
|
||||
"opencatd-open/pkg/config"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
// "gocloud.dev/mysql"
|
||||
@@ -77,7 +78,13 @@ func InitDB(cfg *config.Config) (*gorm.DB, error) {
|
||||
|
||||
// initSQLite 初始化 SQLite 数据库
|
||||
func initSQLite() (*gorm.DB, error) {
|
||||
db, err := gorm.Open(sqlite.Open("openteam.db"), &gorm.Config{})
|
||||
if _, err := os.Stat("db"); os.IsNotExist(err) {
|
||||
errDir := os.MkdirAll("db", 0755)
|
||||
if errDir != nil {
|
||||
log.Fatalln("Error creating directory:", err)
|
||||
}
|
||||
}
|
||||
db, err := gorm.Open(sqlite.Open("./db/openteam.db"), &gorm.Config{})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to SQLite: %v", err)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,193 @@
|
||||
package router
|
||||
|
||||
import (
|
||||
"context"
|
||||
"embed"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"opencatd-open/middleware"
|
||||
"opencatd-open/pkg/config"
|
||||
"opencatd-open/wire"
|
||||
"os"
|
||||
"os/signal"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SetRouter(cfg *config.Config, db *gorm.DB, web *embed.FS) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
var wg sync.WaitGroup
|
||||
|
||||
if cfg == nil || db == nil {
|
||||
panic("cfg or db is nil")
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
log.Fatalf("Failed to get underlying *sql.DB: %v", err)
|
||||
}
|
||||
|
||||
team, err := wire.InitTeamHandler(ctx, cfg, db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
api, err := wire.InitAPIHandler(ctx, cfg, db)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
proxy, err := wire.InitProxyHandler(ctx, cfg, db, &wg)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
r := gin.Default()
|
||||
r.Use(middleware.CORS())
|
||||
|
||||
teamGroup := r.Group("/1")
|
||||
teamGroup.Use(team.AuthMiddleware())
|
||||
{
|
||||
teamGroup.POST("/users/init", team.InitAdmin)
|
||||
// 获取当前用户信息
|
||||
teamGroup.GET("/me", team.Me)
|
||||
// team.GET("/me/usages", team.HandleMeUsage)
|
||||
|
||||
teamGroup.POST("/keys", team.CreateKey)
|
||||
teamGroup.GET("/keys", team.ListKeys)
|
||||
teamGroup.POST("/keys/:id", team.UpdateKey)
|
||||
teamGroup.DELETE("/keys/:id", team.DeleteKey)
|
||||
|
||||
teamGroup.POST("/users", team.CreateUser)
|
||||
teamGroup.GET("/users", team.ListUsers)
|
||||
teamGroup.POST("/users/:id/reset", team.ResetUserToken)
|
||||
teamGroup.DELETE("/users/:id", team.DeleteUser)
|
||||
|
||||
teamGroup.GET("/1/usages", team.ListUsages)
|
||||
}
|
||||
|
||||
public := r.Group("/api/auth")
|
||||
{
|
||||
public.GET("/passkey/begin", api.PasskeyAuthBegin)
|
||||
public.POST("/passkey/finish", api.PasskeyAuthFinish)
|
||||
|
||||
public.POST("/register", api.Register)
|
||||
public.POST("/login", api.Login)
|
||||
}
|
||||
|
||||
apiGroup := r.Group("/api", middleware.Auth)
|
||||
{
|
||||
apiGroup.GET("/profile", api.Profile)
|
||||
apiGroup.POST("/profile/update", api.UpdateProfile)
|
||||
apiGroup.POST("/profile/update/password", api.UpdatePassword)
|
||||
// 绑定PassKey
|
||||
apiGroup.GET("/profile/passkey", api.PasskeyCreateBegin)
|
||||
apiGroup.POST("/profile/passkey", api.PasskeyCreateFinish)
|
||||
apiGroup.GET("/profile/passkeys", api.ListPasskey)
|
||||
apiGroup.DELETE("/profile/passkeys/:id", api.DeletePasskey)
|
||||
|
||||
userGroup := apiGroup.Group("/users")
|
||||
{
|
||||
userGroup.POST("", api.CreateUser)
|
||||
userGroup.GET("", api.ListUser)
|
||||
userGroup.GET("/:id", api.GetUser)
|
||||
userGroup.PUT("/:id", api.EditUser)
|
||||
userGroup.DELETE("/:id", api.DeleteUser)
|
||||
userGroup.POST("/batch/:option", api.UserOption)
|
||||
}
|
||||
|
||||
tokenGroup := apiGroup.Group("/tokens")
|
||||
tokenGroup.POST("", api.CreateToken)
|
||||
tokenGroup.GET("", api.ListToken)
|
||||
// tokenGroup.GET("/:id", api.GetToken)
|
||||
tokenGroup.POST("/reset/:id", api.ResetToken)
|
||||
tokenGroup.PUT("/:id", api.UpdateToken)
|
||||
tokenGroup.DELETE("/:id", api.DeleteToken)
|
||||
// tokenGroup.POST("/batch/:option", api.TokenOption)
|
||||
|
||||
apiGroup.POST("keys", api.CreateApiKey)
|
||||
apiGroup.GET("keys", api.ListApiKey)
|
||||
apiGroup.GET("keys/:id", api.GetApiKey)
|
||||
apiGroup.PUT("keys/:id", api.UpdateApiKey)
|
||||
apiGroup.DELETE("keys/:id", api.DeleteApiKey)
|
||||
apiGroup.POST("keys/batch/:option", api.ApiKeyOption)
|
||||
}
|
||||
|
||||
v1 := r.Group("/v1")
|
||||
v1.Use(middleware.AuthLLM(db))
|
||||
{
|
||||
// v1.POST("/v2/*proxypath", router.HandleProxy)
|
||||
v1.POST("/*proxypath", proxy.HandleProxy)
|
||||
v1.GET("/models", proxy.HandleModels)
|
||||
}
|
||||
|
||||
idxFS, err := fs.Sub(web, "dist")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
assetsFS, err := fs.Sub(web, "dist/assets")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
r.StaticFS("/assets", http.FS(assetsFS))
|
||||
|
||||
r.NoRoute(func(c *gin.Context) {
|
||||
if c.Writer.Status() == http.StatusNotFound {
|
||||
c.FileFromFS("/", http.FS(idxFS))
|
||||
}
|
||||
})
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", cfg.Port),
|
||||
Handler: r,
|
||||
}
|
||||
|
||||
go func() {
|
||||
fmt.Println("Starting server at port:", cfg.Port)
|
||||
// 服务启动
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
log.Fatalf("listen: %s\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 等待中断信号来优雅地关闭服务器
|
||||
quit := make(chan os.Signal, 1)
|
||||
// kill (no param) default send syscall.SIGTERM
|
||||
// kill -2 is syscall.SIGINT
|
||||
// kill -9 is syscall.SIGKILL but can't be catch
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
fmt.Println("\nShutdown Server ...")
|
||||
shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer shutdownCancel()
|
||||
|
||||
if err := srv.Shutdown(shutdownCtx); err != nil {
|
||||
log.Fatalln("Server Shutdown:", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
|
||||
sqlDB.Close()
|
||||
|
||||
waitChan := make(chan struct{})
|
||||
go func() {
|
||||
wg.Wait()
|
||||
close(waitChan)
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-waitChan:
|
||||
fmt.Println("All goroutines have finished")
|
||||
case <-shutdownCtx.Done():
|
||||
fmt.Println("⚠️ Shutdown timeout")
|
||||
}
|
||||
|
||||
fmt.Println("Server exited")
|
||||
}
|
||||
@@ -27,7 +27,7 @@ func InitTeamHandler(ctx context.Context, cfg *config.Config, db *gorm.DB) (*con
|
||||
userService := service.NewUserService(db, userDAO)
|
||||
tokenDAO := dao.NewTokenDAO(db)
|
||||
tokenService := service.NewTokenService(tokenDAO)
|
||||
apiKeyDAO := dao.NewApiKeyDAO(db)
|
||||
apiKeyDAO := dao.NewApiKeyDAO(cfg, db)
|
||||
apiKeyService := service.NewApiKeyService(db, apiKeyDAO)
|
||||
usageDAO := dao.NewUsageDAO(cfg, db)
|
||||
dailyUsageDAO := dao.NewDailyUsageDAO(cfg, db)
|
||||
@@ -38,23 +38,23 @@ func InitTeamHandler(ctx context.Context, cfg *config.Config, db *gorm.DB) (*con
|
||||
|
||||
func InitAPIHandler(ctx context.Context, cfg *config.Config, db *gorm.DB) (*controller2.Api, error) {
|
||||
userDAO := dao.NewUserDAO(db)
|
||||
userServiceImpl := service2.NewUserService(db, userDAO)
|
||||
userServiceImpl := service2.NewUserService(cfg, db, userDAO)
|
||||
tokenDAO := dao.NewTokenDAO(db)
|
||||
tokenServiceImpl := service2.NewTokenService(db, tokenDAO)
|
||||
apiKeyDAO := dao.NewApiKeyDAO(db)
|
||||
apiKeyDAO := dao.NewApiKeyDAO(cfg, db)
|
||||
apiKeyServiceImpl := service2.NewApiKeyService(db, apiKeyDAO)
|
||||
webAuthnService, err := service2.NewWebAuthnService(db, cfg)
|
||||
webAuthnService, err := service2.NewWebAuthnService(cfg, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
usageService := service2.NewUsageService(ctx, cfg, db)
|
||||
api := controller2.NewApi(db, userServiceImpl, tokenServiceImpl, apiKeyServiceImpl, webAuthnService, usageService)
|
||||
api := controller2.NewApi(cfg, db, userServiceImpl, tokenServiceImpl, apiKeyServiceImpl, webAuthnService, usageService)
|
||||
return api, nil
|
||||
}
|
||||
|
||||
func InitProxyHandler(ctx context.Context, cfg *config.Config, db *gorm.DB, wg *sync.WaitGroup) (*controller3.Proxy, error) {
|
||||
userDAO := dao.NewUserDAO(db)
|
||||
apiKeyDAO := dao.NewApiKeyDAO(db)
|
||||
apiKeyDAO := dao.NewApiKeyDAO(cfg, db)
|
||||
tokenDAO := dao.NewTokenDAO(db)
|
||||
usageDAO := dao.NewUsageDAO(cfg, db)
|
||||
dailyUsageDAO := dao.NewDailyUsageDAO(cfg, db)
|
||||
|
||||