diff --git a/.github/workflows/docker-image-amd64.yml b/.github/workflows/docker-image-amd64.yml index 36236df2..a823151c 100644 --- a/.github/workflows/docker-image-amd64.yml +++ b/.github/workflows/docker-image-amd64.yml @@ -18,20 +18,20 @@ jobs: contents: read steps: - name: Check out the repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Save version info run: | git describe --tags > VERSION - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -39,14 +39,14 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | calciumion/new-api ghcr.io/${{ github.repository }} - name: Build and push Docker images - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . push: true diff --git a/.github/workflows/docker-image-arm64.yml b/.github/workflows/docker-image-arm64.yml index 44aec807..d7468c8e 100644 --- a/.github/workflows/docker-image-arm64.yml +++ b/.github/workflows/docker-image-arm64.yml @@ -4,7 +4,6 @@ on: push: tags: - '*' - - '!*-alpha*' workflow_dispatch: inputs: name: @@ -19,26 +18,26 @@ jobs: contents: read steps: - name: Check out the repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Save version info run: | git describe --tags > VERSION - name: Set up QEMU - uses: docker/setup-qemu-action@v2 + uses: docker/setup-qemu-action@v3 - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 + uses: docker/setup-buildx-action@v3 - name: Log in to Docker Hub - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Log in to the Container registry - uses: docker/login-action@v2 + uses: docker/login-action@v3 with: registry: ghcr.io username: ${{ github.actor }} @@ -46,14 +45,14 @@ jobs: - name: Extract metadata (tags, labels) for Docker id: meta - uses: docker/metadata-action@v4 + uses: docker/metadata-action@v5 with: images: | calciumion/new-api ghcr.io/${{ github.repository }} - name: Build and push Docker images - uses: docker/build-push-action@v3 + uses: docker/build-push-action@v5 with: context: . platforms: linux/amd64,linux/arm64 diff --git a/.gitignore b/.gitignore index 28106a24..6a23f89e 100644 --- a/.gitignore +++ b/.gitignore @@ -9,4 +9,5 @@ logs web/dist .env one-api -.DS_Store \ No newline at end of file +.DS_Store +tiktoken_cache \ No newline at end of file diff --git a/README.md b/README.md index 0a0ff71b..6ac8839b 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,6 @@ # New API - 🍥新一代大模型网关与AI资产管理系统 Calcium-Ion%2Fnew-api | Trendshift @@ -37,199 +36,154 @@ > 本项目为开源项目,在[One API](https://github.com/songquanpeng/one-api)的基础上进行二次开发 > [!IMPORTANT] -> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 > - 本项目仅供个人学习使用,不保证稳定性,且不提供任何技术支持。 +> - 使用者必须在遵循 OpenAI 的[使用条款](https://openai.com/policies/terms-of-use)以及**法律法规**的情况下使用,不得用于非法用途。 > - 根据[《生成式人工智能服务管理暂行办法》](http://www.cac.gov.cn/2023-07/13/c_1690898327029107.htm)的要求,请勿对中国地区公众提供一切未经备案的生成式人工智能服务。 +## 📚 文档 + +详细文档请访问我们的官方Wiki:[https://docs.newapi.pro/](https://docs.newapi.pro/) + ## ✨ 主要特性 -1. 🎨 全新的UI界面(部分界面还待更新) -2. 🌍 多语言支持(待完善) -3. 🎨 添加[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口支持,[对接文档](Midjourney.md) -4. 💰 支持在线充值功能,可在系统设置中设置: - - [x] 易支付 -5. 🔍 支持用key查询使用额度: - - 配合项目[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)可实现用key查询使用 -6. 📑 分页支持选择每页显示数量 -7. 🔄 兼容原版One API的数据库,可直接使用原版数据库(one-api.db) -8. 💵 支持模型按次数收费,可在 系统设置-运营设置 中设置 -9. ⚖️ 支持渠道**加权随机** -10. 📈 数据看板(控制台) -11. 🔒 可设置令牌能调用的模型 -12. 🤖 支持Telegram授权登录: - 1. 系统设置-配置登录注册-允许通过Telegram登录 - 2. 对[@Botfather](https://t.me/botfather)输入指令/setdomain - 3. 选择你的bot,然后输入http(s)://你的网站地址/login - 4. Telegram Bot 名称是bot username 去掉@后的字符串 -13. 🎵 添加 [Suno API](https://github.com/Suno-API/Suno-API)接口支持,[对接文档](Suno.md) -14. 🔄 支持Rerank模型,目前兼容Cohere和Jina,可接入Dify,[对接文档](Rerank.md) -15. ⚡ **[OpenAI Realtime API](https://platform.openai.com/docs/guides/realtime/integration)** - 支持OpenAI的Realtime API,支持Azure渠道 -16. 支持使用路由/chat2link 进入聊天界面 -17. 🧠 支持通过模型名称后缀设置 reasoning effort: +New API提供了丰富的功能,详细特性请参考[特性说明](https://docs.newapi.pro/wiki/features-introduction): + +1. 🎨 全新的UI界面 +2. 🌍 多语言支持 +3. 💰 支持在线充值功能(易支付) +4. 🔍 支持用key查询使用额度(配合[neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool)) +5. 🔄 兼容原版One API的数据库 +6. 💵 支持模型按次数收费 +7. ⚖️ 支持渠道加权随机 +8. 📈 数据看板(控制台) +9. 🔒 令牌分组、模型限制 +10. 🤖 支持更多授权登陆方式(LinuxDO,Telegram、OIDC) +11. 🔄 支持Rerank模型(Cohere和Jina),[接口文档](https://docs.newapi.pro/api/jinaai-rerank) +12. ⚡ 支持OpenAI Realtime API(包括Azure渠道),[接口文档](https://docs.newapi.pro/api/openai-realtime) +13. ⚡ 支持Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat) +14. 支持使用路由/chat2link进入聊天界面 +15. 🧠 支持通过模型名称后缀设置 reasoning effort: 1. OpenAI o系列模型 - 添加后缀 `-high` 设置为 high reasoning effort (例如: `o3-mini-high`) - 添加后缀 `-medium` 设置为 medium reasoning effort (例如: `o3-mini-medium`) - 添加后缀 `-low` 设置为 low reasoning effort (例如: `o3-mini-low`) 2. Claude 思考模型 - 添加后缀 `-thinking` 启用思考模式 (例如: `claude-3-7-sonnet-20250219-thinking`) -18. 🔄 思考转内容,支持在 `渠道-编辑-渠道额外设置` 中设置 `thinking_to_content` 选项,默认`false`,开启后会将思考内容`reasoning_content`转换为``标签拼接到内容中返回。 -19. 🔄 模型限流,支持在 `系统设置-速率限制设置` 中设置模型限流,支持设置总请求数限制和成功请求数限制 -20. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费: +16. 🔄 思考转内容功能 +17. 🔄 针对用户的模型限流功能 +18. 💰 缓存计费支持,开启后可以在缓存命中时按照设定的比例计费: 1. 在 `系统设置-运营设置` 中设置 `提示缓存倍率` 选项 2. 在渠道中设置 `提示缓存倍率`,范围 0-1,例如设置为 0.5 表示缓存命中时按照 50% 计费 3. 支持的渠道: - [x] OpenAI - [x] Azure - [x] DeepSeek - - [ ] Claude + - [x] Claude ## 模型支持 -此版本额外支持以下模型: + +此版本支持多种模型,详情请参考[接口文档-中继接口](https://docs.newapi.pro/api): + 1. 第三方模型 **gpts** (gpt-4-gizmo-*) -2. [Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[对接文档](Midjourney.md) -3. 自定义渠道,支持填入完整调用地址 -4. [Suno API](https://github.com/Suno-API/Suno-API) 接口,[对接文档](Suno.md) -5. Rerank模型,目前支持[Cohere](https://cohere.ai/)和[Jina](https://jina.ai/),[对接文档](Rerank.md) -6. Dify +2. 第三方渠道[Midjourney-Proxy(Plus)](https://github.com/novicezk/midjourney-proxy)接口,[接口文档](https://docs.newapi.pro/api/midjourney-proxy-image) +3. 第三方渠道[Suno API](https://github.com/Suno-API/Suno-API)接口,[接口文档](https://docs.newapi.pro/api/suno-music) +4. 自定义渠道,支持填入完整调用地址 +5. Rerank模型([Cohere](https://cohere.ai/)和[Jina](https://jina.ai/)),[接口文档](https://docs.newapi.pro/api/jinaai-rerank) +6. Claude Messages 格式,[接口文档](https://docs.newapi.pro/api/anthropic-chat) +7. Dify,当前仅支持chatflow -您可以在渠道中添加自定义模型gpt-4-gizmo-*,此模型并非OpenAI官方模型,而是第三方模型,使用官方key无法调用。 +## 环境变量配置 -## 比原版One API多出的配置 -- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false`。 -- `STREAMING_TIMEOUT`:设置流式一次回复的超时时间,默认为 60 秒。 -- `DIFY_DEBUG`:设置 Dify 渠道是否输出工作流和节点信息到客户端,默认为 `true`。 -- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,请求上游返回流模式usage,默认为 `true`,建议开启,不影响客户端传入stream_options参数返回结果。 -- `GET_MEDIA_TOKEN`:是否统计图片token,默认为 `true`,关闭后将不再在本地计算图片token,可能会导致和上游计费不同,此项覆盖 `GET_MEDIA_TOKEN_NOT_STREAM` 选项作用。 -- `GET_MEDIA_TOKEN_NOT_STREAM`:是否在非流(`stream=false`)情况下统计图片token,默认为 `true`。 -- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认为 `true`,关闭后将不会更新任务进度。 -- `COHERE_SAFETY_SETTING`:Cohere模型[安全设置](https://docs.cohere.com/docs/safety-modes#overview),可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认为 `NONE`。 -- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认为 `16`,设置为 `-1` 则不限制。 -- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位 MB,默认为 `20`。 -- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容。 -- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,如果渠道设置中未指定API版本,则使用此版本,默认为 `2024-12-01-preview` -- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制的持续时间(分钟),默认为 `10`。 -- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认为 `2`。 +详细配置说明请参考[安装指南-环境变量配置](https://docs.newapi.pro/installation/environment-variables): -## 已废弃的环境变量 -- ~~`GEMINI_MODEL_MAP`(已废弃)~~:改为到`设置-模型相关设置`中设置 -- ~~`GEMINI_SAFETY_SETTING`(已废弃)~~:改为到`设置-模型相关设置`中设置 +- `GENERATE_DEFAULT_TOKEN`:是否为新注册用户生成初始令牌,默认为 `false` +- `STREAMING_TIMEOUT`:流式回复超时时间,默认60秒 +- `DIFY_DEBUG`:Dify渠道是否输出工作流和节点信息,默认 `true` +- `FORCE_STREAM_OPTION`:是否覆盖客户端stream_options参数,默认 `true` +- `GET_MEDIA_TOKEN`:是否统计图片token,默认 `true` +- `GET_MEDIA_TOKEN_NOT_STREAM`:非流情况下是否统计图片token,默认 `true` +- `UPDATE_TASK`:是否更新异步任务(Midjourney、Suno),默认 `true` +- `COHERE_SAFETY_SETTING`:Cohere模型安全设置,可选值为 `NONE`, `CONTEXTUAL`, `STRICT`,默认 `NONE` +- `GEMINI_VISION_MAX_IMAGE_NUM`:Gemini模型最大图片数量,默认 `16` +- `MAX_FILE_DOWNLOAD_MB`: 最大文件下载大小,单位MB,默认 `20` +- `CRYPTO_SECRET`:加密密钥,用于加密数据库内容 +- `AZURE_DEFAULT_API_VERSION`:Azure渠道默认API版本,默认 `2024-12-01-preview` +- `NOTIFICATION_LIMIT_DURATION_MINUTE`:通知限制持续时间,默认 `10`分钟 +- `NOTIFY_LIMIT_COUNT`:用户通知在指定持续时间内的最大数量,默认 `2` ## 部署 +详细部署指南请参考[安装指南-部署方式](https://docs.newapi.pro/installation): + > [!TIP] > 最新版Docker镜像:`calciumion/new-api:latest` -> 默认账号root 密码123456 -### 多机部署 -- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致。 -- 如果公用Redis,必须设置 `CRYPTO_SECRET`,否则会导致多机部署时Redis内容无法获取。 +### 多机部署注意事项 +- 必须设置环境变量 `SESSION_SECRET`,否则会导致多机部署时登录状态不一致 +- 如果公用Redis,必须设置 `CRYPTO_SECRET`,否则会导致多机部署时Redis内容无法获取 ### 部署要求 -- 本地数据库(默认):SQLite(Docker 部署默认使用 SQLite,必须挂载 `/data` 目录到宿主机) -- 远程数据库:MySQL 版本 >= 5.7.8,PgSQL 版本 >= 9.6 +- 本地数据库(默认):SQLite(Docker部署必须挂载`/data`目录) +- 远程数据库:MySQL版本 >= 5.7.8,PgSQL版本 >= 9.6 -### 使用宝塔面板Docker功能部署 -安装宝塔面板 (**9.2.0版本**及以上),前往 [宝塔面板](https://www.bt.cn/new/download.html) 官网,选择正式版的脚本下载安装 -安装后登录宝塔面板,在菜单栏中点击 Docker ,首次进入会提示安装 Docker 服务,点击立即安装,按提示完成安装 -安装完成后在应用商店中找到 **New-API** ,点击安装,配置基本选项 即可完成安装 +### 部署方式 + +#### 使用宝塔面板Docker功能部署 +安装宝塔面板(**9.2.0版本**及以上),在应用商店中找到**New-API**安装即可。 [图文教程](BT.md) -### 基于 Docker 进行部署 - -> [!TIP] -> 默认管理员账号root 密码123456 - -### 使用 Docker Compose 部署(推荐) +#### 使用Docker Compose部署(推荐) ```shell # 下载项目 git clone https://github.com/Calcium-Ion/new-api.git cd new-api -# 按需编辑 docker-compose.yml -# nano docker-compose.yml -# vim docker-compose.yml +# 按需编辑docker-compose.yml # 启动 docker-compose up -d ``` -#### 更新版本 +#### 直接使用Docker镜像 ```shell -docker-compose pull -docker-compose up -d -``` - -### 直接使用 Docker 镜像 -```shell -# 使用 SQLite 的部署命令: +# 使用SQLite docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest -# 使用 MySQL 的部署命令,在上面的基础上添加 `-e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi"`,请自行修改数据库连接参数。 -# 例如: +# 使用MySQL docker run --name new-api -d --restart always -p 3000:3000 -e SQL_DSN="root:123456@tcp(localhost:3306)/oneapi" -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest ``` -#### 更新版本 -```shell -# 拉取最新镜像 -docker pull calciumion/new-api:latest -# 停止并删除旧容器 -docker stop new-api -docker rm new-api -# 使用相同参数运行新容器 -docker run --name new-api -d --restart always -p 3000:3000 -e TZ=Asia/Shanghai -v /home/ubuntu/data/new-api:/data calciumion/new-api:latest -``` +## 渠道重试与缓存 +渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。 -或者使用 Watchtower 自动更新(不推荐,可能会导致数据库不兼容): -```shell -docker run --rm -v /var/run/docker.sock:/var/run/docker.sock containrrr/watchtower -cR -``` - -## 渠道重试 -渠道重试功能已经实现,可以在`设置->运营设置->通用设置`设置重试次数,**建议开启缓存**功能。 -如果开启了重试功能,重试使用下一个优先级,以此类推。 ### 缓存设置方法 -1. `REDIS_CONN_STRING`:设置之后将使用 Redis 作为缓存使用。 - + 例子:`REDIS_CONN_STRING=redis://default:redispw@localhost:49153` -2. `MEMORY_CACHE_ENABLED`:启用内存缓存(如果设置了`REDIS_CONN_STRING`,则无需手动设置),会导致用户额度的更新存在一定的延迟,可选值为 `true` 和 `false`,未设置则默认为 `false`。 - + 例子:`MEMORY_CACHE_ENABLED=true` -### 为什么有的时候没有重试 -这些错误码不会重试:400,504,524 -### 我想让400也重试 -在`渠道->编辑`中,将`状态码复写`改为 -```json -{ - "400": "500" -} -``` -可以实现400错误转为500错误,从而重试 +1. `REDIS_CONN_STRING`:设置Redis作为缓存 +2. `MEMORY_CACHE_ENABLED`:启用内存缓存(设置了Redis则无需手动设置) -## Midjourney接口设置文档 -[对接文档](Midjourney.md) +## 接口文档 -## Suno接口设置文档 -[对接文档](Suno.md) +详细接口文档请参考[接口文档](https://docs.newapi.pro/api): -## 界面截图 -![image](https://github.com/user-attachments/assets/a0dcd349-5df8-4dc8-9acf-ca272b239919) - - -![image](https://github.com/user-attachments/assets/c7d0f7e1-729c-43e2-ac7c-2cb73b0afc8e) - -![image](https://github.com/user-attachments/assets/29f81de5-33fc-4fc5-a5ff-f9b54b653c7c) - -![image](https://github.com/user-attachments/assets/4fa53e18-d2c5-477a-9b26-b86e44c71e35) - -## 交流群 - +- [聊天接口(Chat)](https://docs.newapi.pro/api/openai-chat) +- [图像接口(Image)](https://docs.newapi.pro/api/openai-image) +- [重排序接口(Rerank)](https://docs.newapi.pro/api/jinaai-rerank) +- [实时对话接口(Realtime)](https://docs.newapi.pro/api/openai-realtime) +- [Claude聊天接口(messages)](https://docs.newapi.pro/api/anthropic-chat) ## 相关项目 - [One API](https://github.com/songquanpeng/one-api):原版项目 - [Midjourney-Proxy](https://github.com/novicezk/midjourney-proxy):Midjourney接口支持 -- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代 AI 一站式 B/C 端解决方案 +- [chatnio](https://github.com/Deeptrain-Community/chatnio):下一代AI一站式B/C端解决方案 - [neko-api-key-tool](https://github.com/Calcium-Ion/neko-api-key-tool):用key查询使用额度 其他基于New API的项目: -- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版,专注于高并发优化,并支持Claude格式 -- [VoAPI](https://github.com/VoAPI/VoAPI):基于New API的前端美化版本,闭源免费 +- [new-api-horizon](https://github.com/Calcium-Ion/new-api-horizon):New API高性能优化版 +- [VoAPI](https://github.com/VoAPI/VoAPI):基于New API的前端美化版本 + +## 帮助支持 + +如有问题,请参考[帮助支持](https://docs.newapi.pro/support): +- [社区交流](https://docs.newapi.pro/support/community-interaction) +- [反馈问题](https://docs.newapi.pro/support/feedback-issues) +- [常见问题](https://docs.newapi.pro/support/faq) ## 🌟 Star History diff --git a/common/constants.go b/common/constants.go index 9611ed0e..ed50fae6 100644 --- a/common/constants.go +++ b/common/constants.go @@ -1,8 +1,8 @@ package common import ( - "os" - "strconv" + //"os" + //"strconv" "sync" "time" @@ -63,8 +63,8 @@ var EmailDomainWhitelist = []string{ "foxmail.com", } -var DebugEnabled = os.Getenv("DEBUG") == "true" -var MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true" +var DebugEnabled bool +var MemoryCacheEnabled bool var LogConsumeEnabled = true @@ -77,7 +77,6 @@ var SMTPToken = "" var GitHubClientId = "" var GitHubClientSecret = "" - var LinuxDOClientId = "" var LinuxDOClientSecret = "" @@ -104,22 +103,22 @@ var RetryTimes = 0 //var RootUserEmail = "" -var IsMasterNode = os.Getenv("NODE_TYPE") != "slave" +var IsMasterNode bool -var requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL")) -var RequestInterval = time.Duration(requestInterval) * time.Second +var requestInterval int +var RequestInterval time.Duration -var SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60) // unit is second +var SyncFrequency int // unit is second var BatchUpdateEnabled = false -var BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5) +var BatchUpdateInterval int -var RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0) // unit is second +var RelayTimeout int // unit is second -var GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE") +var GeminiSafetySetting string // https://docs.cohere.com/docs/safety-modes Type; NONE/CONTEXTUAL/STRICT -var CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE") +var CohereSafetySetting string const ( RequestIdKey = "X-Oneapi-Request-Id" @@ -146,13 +145,13 @@ var ( // All duration's unit is seconds // Shouldn't larger then RateLimitKeyExpirationDuration var ( - GlobalApiRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_API_RATE_LIMIT_ENABLE", true) - GlobalApiRateLimitNum = GetEnvOrDefault("GLOBAL_API_RATE_LIMIT", 180) - GlobalApiRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_API_RATE_LIMIT_DURATION", 180)) + GlobalApiRateLimitEnable bool + GlobalApiRateLimitNum int + GlobalApiRateLimitDuration int64 - GlobalWebRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_WEB_RATE_LIMIT_ENABLE", true) - GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60) - GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180)) + GlobalWebRateLimitEnable bool + GlobalWebRateLimitNum int + GlobalWebRateLimitDuration int64 UploadRateLimitNum = 10 UploadRateLimitDuration int64 = 60 @@ -235,6 +234,8 @@ const ( ChannelTypeMokaAI = 44 ChannelTypeVolcEngine = 45 ChannelTypeBaiduV2 = 46 + ChannelTypeXinference = 47 + ChannelTypeXai = 48 ChannelTypeDummy // this one is only for count, do not add any channel after this ) @@ -287,4 +288,6 @@ var ChannelBaseURLs = []string{ "https://api.moka.ai", //44 "https://ark.cn-beijing.volces.com", //45 "https://qianfan.baidubce.com", //46 + "", //47 + "https://api.x.ai", //48 } diff --git a/common/custom-event.go b/common/custom-event.go index 69da4bc4..d8f9ec9f 100644 --- a/common/custom-event.go +++ b/common/custom-event.go @@ -44,7 +44,7 @@ var fieldReplacer = strings.NewReplacer( "\r", "\\r") var dataReplacer = strings.NewReplacer( - "\n", "\ndata:", + "\n", "\n", "\r", "\\r") type CustomEvent struct { diff --git a/common/init.go b/common/init.go index 694e603e..c0caf0a1 100644 --- a/common/init.go +++ b/common/init.go @@ -6,6 +6,8 @@ import ( "log" "os" "path/filepath" + "strconv" + "time" ) var ( @@ -66,4 +68,31 @@ func LoadEnv() { } } } + + // Initialize variables from constants.go that were using environment variables + DebugEnabled = os.Getenv("DEBUG") == "true" + MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true" + IsMasterNode = os.Getenv("NODE_TYPE") != "slave" + + // Parse requestInterval and set RequestInterval + requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL")) + RequestInterval = time.Duration(requestInterval) * time.Second + + // Initialize variables with GetEnvOrDefault + SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60) + BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5) + RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0) + + // Initialize string variables with GetEnvOrDefaultString + GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE") + CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE") + + // Initialize rate limit variables + GlobalApiRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_API_RATE_LIMIT_ENABLE", true) + GlobalApiRateLimitNum = GetEnvOrDefault("GLOBAL_API_RATE_LIMIT", 180) + GlobalApiRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_API_RATE_LIMIT_DURATION", 180)) + + GlobalWebRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_WEB_RATE_LIMIT_ENABLE", true) + GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60) + GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180)) } diff --git a/common/json.go b/common/json.go new file mode 100644 index 00000000..cec8f16b --- /dev/null +++ b/common/json.go @@ -0,0 +1,18 @@ +package common + +import ( + "bytes" + "encoding/json" +) + +func DecodeJson(data []byte, v any) error { + return json.NewDecoder(bytes.NewReader(data)).Decode(v) +} + +func DecodeJsonStr(data string, v any) error { + return DecodeJson(StringToByteSlice(data), v) +} + +func EncodeJson(v any) ([]byte, error) { + return json.Marshal(v) +} diff --git a/common/limiter/limiter.go b/common/limiter/limiter.go new file mode 100644 index 00000000..ef5d1935 --- /dev/null +++ b/common/limiter/limiter.go @@ -0,0 +1,89 @@ +package limiter + +import ( + "context" + _ "embed" + "fmt" + "github.com/go-redis/redis/v8" + "one-api/common" + "sync" +) + +//go:embed lua/rate_limit.lua +var rateLimitScript string + +type RedisLimiter struct { + client *redis.Client + limitScriptSHA string +} + +var ( + instance *RedisLimiter + once sync.Once +) + +func New(ctx context.Context, r *redis.Client) *RedisLimiter { + once.Do(func() { + // 预加载脚本 + limitSHA, err := r.ScriptLoad(ctx, rateLimitScript).Result() + if err != nil { + common.SysLog(fmt.Sprintf("Failed to load rate limit script: %v", err)) + } + instance = &RedisLimiter{ + client: r, + limitScriptSHA: limitSHA, + } + }) + + return instance +} + +func (rl *RedisLimiter) Allow(ctx context.Context, key string, opts ...Option) (bool, error) { + // 默认配置 + config := &Config{ + Capacity: 10, + Rate: 1, + Requested: 1, + } + + // 应用选项模式 + for _, opt := range opts { + opt(config) + } + + // 执行限流 + result, err := rl.client.EvalSha( + ctx, + rl.limitScriptSHA, + []string{key}, + config.Requested, + config.Rate, + config.Capacity, + ).Int() + + if err != nil { + return false, fmt.Errorf("rate limit failed: %w", err) + } + return result == 1, nil +} + +// Config 配置选项模式 +type Config struct { + Capacity int64 + Rate int64 + Requested int64 +} + +type Option func(*Config) + +func WithCapacity(c int64) Option { + return func(cfg *Config) { cfg.Capacity = c } +} + +func WithRate(r int64) Option { + return func(cfg *Config) { cfg.Rate = r } +} + +func WithRequested(n int64) Option { + return func(cfg *Config) { cfg.Requested = n } +} diff --git a/common/limiter/lua/rate_limit.lua b/common/limiter/lua/rate_limit.lua new file mode 100644 index 00000000..c07fd3a8 --- /dev/null +++ b/common/limiter/lua/rate_limit.lua @@ -0,0 +1,44 @@ +-- 令牌桶限流器 +-- KEYS[1]: 限流器唯一标识 +-- ARGV[1]: 请求令牌数 (通常为1) +-- ARGV[2]: 令牌生成速率 (每秒) +-- ARGV[3]: 桶容量 + +local key = KEYS[1] +local requested = tonumber(ARGV[1]) +local rate = tonumber(ARGV[2]) +local capacity = tonumber(ARGV[3]) + +-- 获取当前时间(Redis服务器时间) +local now = redis.call('TIME') +local nowInSeconds = tonumber(now[1]) + +-- 获取桶状态 +local bucket = redis.call('HMGET', key, 'tokens', 'last_time') +local tokens = tonumber(bucket[1]) +local last_time = tonumber(bucket[2]) + +-- 初始化桶(首次请求或过期) +if not tokens or not last_time then + tokens = capacity + last_time = nowInSeconds +else + -- 计算新增令牌 + local elapsed = nowInSeconds - last_time + local add_tokens = elapsed * rate + tokens = math.min(capacity, tokens + add_tokens) + last_time = nowInSeconds +end + +-- 判断是否允许请求 +local allowed = false +if tokens >= requested then + tokens = tokens - requested + allowed = true +end + +---- 更新桶状态并设置过期时间 +redis.call('HMSET', key, 'tokens', tokens, 'last_time', last_time) +--redis.call('EXPIRE', key, math.ceil(capacity / rate) + 60) -- 适当延长过期时间 + +return allowed and 1 or 0 \ No newline at end of file diff --git a/constant/env.go b/constant/env.go index d2a1d04d..5a494852 100644 --- a/constant/env.go +++ b/constant/env.go @@ -4,32 +4,39 @@ import ( "one-api/common" ) -var StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60) -var DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true) - -var MaxFileDownloadMB = common.GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20) - -// ForceStreamOption 覆盖请求参数,强制返回usage信息 -var ForceStreamOption = common.GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true) - -var GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true) - -var GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true) - -var UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true) - -var AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2024-12-01-preview") +var StreamingTimeout int +var DifyDebug bool +var MaxFileDownloadMB int +var ForceStreamOption bool +var GetMediaToken bool +var GetMediaTokenNotStream bool +var UpdateTask bool +var AzureDefaultAPIVersion string +var GeminiVisionMaxImageNum int +var NotifyLimitCount int +var NotificationLimitDurationMinute int +var GenerateDefaultToken bool //var GeminiModelMap = map[string]string{ // "gemini-1.0-pro": "v1", //} -var GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16) - -var NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2) -var NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10) - func InitEnv() { + StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60) + DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true) + MaxFileDownloadMB = common.GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20) + // ForceStreamOption 覆盖请求参数,强制返回usage信息 + ForceStreamOption = common.GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true) + GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true) + GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true) + UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true) + AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2024-12-01-preview") + GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16) + NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2) + NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10) + // GenerateDefaultToken 是否生成初始令牌,默认关闭。 + GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false) + //modelVersionMapStr := strings.TrimSpace(os.Getenv("GEMINI_MODEL_MAP")) //if modelVersionMapStr == "" { // return @@ -43,6 +50,3 @@ func InitEnv() { // } //} } - -// GenerateDefaultToken 是否生成初始令牌,默认关闭。 -var GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false) diff --git a/constant/setup.go b/constant/setup.go new file mode 100644 index 00000000..26ecc883 --- /dev/null +++ b/constant/setup.go @@ -0,0 +1,3 @@ +package constant + +var Setup = false diff --git a/constant/user_setting.go b/constant/user_setting.go index a5b921b2..055884f7 100644 --- a/constant/user_setting.go +++ b/constant/user_setting.go @@ -1,11 +1,12 @@ package constant var ( - UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型 - UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值 - UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址 - UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥 - UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址 + UserSettingNotifyType = "notify_type" // QuotaWarningType 额度预警类型 + UserSettingQuotaWarningThreshold = "quota_warning_threshold" // QuotaWarningThreshold 额度预警阈值 + UserSettingWebhookUrl = "webhook_url" // WebhookUrl webhook地址 + UserSettingWebhookSecret = "webhook_secret" // WebhookSecret webhook密钥 + UserSettingNotificationEmail = "notification_email" // NotificationEmail 通知邮箱地址 + UserAcceptUnsetRatioModel = "accept_unset_model_ratio_model" // AcceptUnsetRatioModel 是否接受未设置价格的模型 ) var ( diff --git a/controller/channel-test.go b/controller/channel-test.go index 46fb6e9f..d1cb4093 100644 --- a/controller/channel-test.go +++ b/controller/channel-test.go @@ -103,11 +103,19 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr } request := buildTestRequest(testModel) - common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %v ", channel.Id, testModel, info)) + // 创建一个用于日志的 info 副本,移除 ApiKey + logInfo := *info + logInfo.ApiKey = "" + common.SysLog(fmt.Sprintf("testing channel %d with model %s , info %+v ", channel.Id, testModel, logInfo)) + + priceData, err := helper.ModelPriceHelper(c, info, 0, int(request.MaxTokens)) + if err != nil { + return err, nil + } adaptor.Init(info) - convertedRequest, err := adaptor.ConvertRequest(c, info, request) + convertedRequest, err := adaptor.ConvertOpenAIRequest(c, info, request) if err != nil { return err, nil } @@ -125,7 +133,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr if resp != nil { httpResp = resp.(*http.Response) if httpResp.StatusCode != http.StatusOK { - err := service.RelayErrorHandler(httpResp) + err := service.RelayErrorHandler(httpResp, true) return fmt.Errorf("status code %d: %s", httpResp.StatusCode, err.Error.Message), err } } @@ -143,10 +151,7 @@ func testChannel(channel *model.Channel, testModel string) (err error, openAIErr return err, nil } info.PromptTokens = usage.PromptTokens - priceData, err := helper.ModelPriceHelper(c, info, usage.PromptTokens, int(request.MaxTokens)) - if err != nil { - return err, nil - } + quota := 0 if !priceData.UsePrice { quota = usage.PromptTokens + int(math.Round(float64(usage.CompletionTokens)*priceData.CompletionRatio)) @@ -184,10 +189,14 @@ func buildTestRequest(model string) *dto.GeneralOpenAIRequest { return testRequest } // 并非Embedding 模型 - if strings.HasPrefix(model, "o1") || strings.HasPrefix(model, "o3") { + if strings.HasPrefix(model, "o") { testRequest.MaxCompletionTokens = 10 } else if strings.Contains(model, "thinking") { - testRequest.MaxTokens = 50 + if !strings.Contains(model, "claude") { + testRequest.MaxTokens = 50 + } + } else if strings.Contains(model, "gemini") { + testRequest.MaxTokens = 300 } else { testRequest.MaxTokens = 10 } diff --git a/controller/channel.go b/controller/channel.go index f3ec6b3a..ad85fe24 100644 --- a/controller/channel.go +++ b/controller/channel.go @@ -119,6 +119,9 @@ func FetchUpstreamModels(c *gin.Context) { baseURL = channel.GetBaseURL() } url := fmt.Sprintf("%s/v1/models", baseURL) + if channel.Type == common.ChannelTypeGemini { + url = fmt.Sprintf("%s/v1beta/openai/models", baseURL) + } body, err := GetResponseBody("GET", url, channel, GetAuthHeader(channel.Key)) if err != nil { c.JSON(http.StatusOK, gin.H{ @@ -139,7 +142,11 @@ func FetchUpstreamModels(c *gin.Context) { var ids []string for _, model := range result.Data { - ids = append(ids, model.ID) + id := model.ID + if channel.Type == common.ChannelTypeGemini { + id = strings.TrimPrefix(id, "models/") + } + ids = append(ids, id) } c.JSON(http.StatusOK, gin.H{ diff --git a/controller/image.go b/controller/image.go new file mode 100644 index 00000000..d6e8806a --- /dev/null +++ b/controller/image.go @@ -0,0 +1,9 @@ +package controller + +import ( + "github.com/gin-gonic/gin" +) + +func GetImage(c *gin.Context) { + +} diff --git a/controller/misc.go b/controller/misc.go index 0c2ed5e7..4d265c3f 100644 --- a/controller/misc.go +++ b/controller/misc.go @@ -5,9 +5,11 @@ import ( "fmt" "net/http" "one-api/common" + "one-api/constant" "one-api/model" "one-api/setting" "one-api/setting/operation_setting" + "one-api/setting/system_setting" "strings" "github.com/gin-gonic/gin" @@ -34,40 +36,44 @@ func GetStatus(c *gin.Context) { "success": true, "message": "", "data": gin.H{ - "version": common.Version, - "start_time": common.StartTime, - "email_verification": common.EmailVerificationEnabled, - "github_oauth": common.GitHubOAuthEnabled, - "github_client_id": common.GitHubClientId, - "linuxdo_oauth": common.LinuxDOOAuthEnabled, - "linuxdo_client_id": common.LinuxDOClientId, - "telegram_oauth": common.TelegramOAuthEnabled, - "telegram_bot_name": common.TelegramBotName, - "system_name": common.SystemName, - "logo": common.Logo, - "footer_html": common.Footer, - "wechat_qrcode": common.WeChatAccountQRCodeImageURL, - "wechat_login": common.WeChatAuthEnabled, - "server_address": setting.ServerAddress, - "price": setting.Price, - "min_topup": setting.MinTopUp, - "turnstile_check": common.TurnstileCheckEnabled, - "turnstile_site_key": common.TurnstileSiteKey, - "top_up_link": common.TopUpLink, - "docs_link": operation_setting.GetGeneralSetting().DocsLink, - "quota_per_unit": common.QuotaPerUnit, - "display_in_currency": common.DisplayInCurrencyEnabled, - "enable_batch_update": common.BatchUpdateEnabled, - "enable_drawing": common.DrawingEnabled, - "enable_task": common.TaskEnabled, - "enable_data_export": common.DataExportEnabled, - "data_export_default_time": common.DataExportDefaultTime, - "default_collapse_sidebar": common.DefaultCollapseSidebar, - "enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "", - "mj_notify_enabled": setting.MjNotifyEnabled, - "chats": setting.Chats, - "demo_site_enabled": operation_setting.DemoSiteEnabled, - "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, + "version": common.Version, + "start_time": common.StartTime, + "email_verification": common.EmailVerificationEnabled, + "github_oauth": common.GitHubOAuthEnabled, + "github_client_id": common.GitHubClientId, + "linuxdo_oauth": common.LinuxDOOAuthEnabled, + "linuxdo_client_id": common.LinuxDOClientId, + "telegram_oauth": common.TelegramOAuthEnabled, + "telegram_bot_name": common.TelegramBotName, + "system_name": common.SystemName, + "logo": common.Logo, + "footer_html": common.Footer, + "wechat_qrcode": common.WeChatAccountQRCodeImageURL, + "wechat_login": common.WeChatAuthEnabled, + "server_address": setting.ServerAddress, + "price": setting.Price, + "min_topup": setting.MinTopUp, + "turnstile_check": common.TurnstileCheckEnabled, + "turnstile_site_key": common.TurnstileSiteKey, + "top_up_link": common.TopUpLink, + "docs_link": operation_setting.GetGeneralSetting().DocsLink, + "quota_per_unit": common.QuotaPerUnit, + "display_in_currency": common.DisplayInCurrencyEnabled, + "enable_batch_update": common.BatchUpdateEnabled, + "enable_drawing": common.DrawingEnabled, + "enable_task": common.TaskEnabled, + "enable_data_export": common.DataExportEnabled, + "data_export_default_time": common.DataExportDefaultTime, + "default_collapse_sidebar": common.DefaultCollapseSidebar, + "enable_online_topup": setting.PayAddress != "" && setting.EpayId != "" && setting.EpayKey != "", + "mj_notify_enabled": setting.MjNotifyEnabled, + "chats": setting.Chats, + "demo_site_enabled": operation_setting.DemoSiteEnabled, + "self_use_mode_enabled": operation_setting.SelfUseModeEnabled, + "oidc_enabled": system_setting.GetOIDCSettings().Enabled, + "oidc_client_id": system_setting.GetOIDCSettings().ClientId, + "oidc_authorization_endpoint": system_setting.GetOIDCSettings().AuthorizationEndpoint, + "setup": constant.Setup, }, }) return diff --git a/controller/oidc.go b/controller/oidc.go new file mode 100644 index 00000000..440e0964 --- /dev/null +++ b/controller/oidc.go @@ -0,0 +1,240 @@ +package controller + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + "net/url" + "one-api/common" + "one-api/model" + "one-api/setting" + "one-api/setting/system_setting" + "strconv" + "strings" + "time" + + "github.com/gin-contrib/sessions" + "github.com/gin-gonic/gin" +) + +type OidcResponse struct { + AccessToken string `json:"access_token"` + IDToken string `json:"id_token"` + RefreshToken string `json:"refresh_token"` + TokenType string `json:"token_type"` + ExpiresIn int `json:"expires_in"` + Scope string `json:"scope"` +} + +type OidcUser struct { + OpenID string `json:"sub"` + Email string `json:"email"` + Name string `json:"name"` + PreferredUsername string `json:"preferred_username"` + Picture string `json:"picture"` +} + +func getOidcUserInfoByCode(code string) (*OidcUser, error) { + if code == "" { + return nil, errors.New("无效的参数") + } + + values := url.Values{} + values.Set("client_id", system_setting.GetOIDCSettings().ClientId) + values.Set("client_secret", system_setting.GetOIDCSettings().ClientSecret) + values.Set("code", code) + values.Set("grant_type", "authorization_code") + values.Set("redirect_uri", fmt.Sprintf("%s/oauth/oidc", setting.ServerAddress)) + formData := values.Encode() + req, err := http.NewRequest("POST", system_setting.GetOIDCSettings().TokenEndpoint, strings.NewReader(formData)) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", "application/x-www-form-urlencoded") + req.Header.Set("Accept", "application/json") + client := http.Client{ + Timeout: 5 * time.Second, + } + res, err := client.Do(req) + if err != nil { + common.SysLog(err.Error()) + return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!") + } + defer res.Body.Close() + var oidcResponse OidcResponse + err = json.NewDecoder(res.Body).Decode(&oidcResponse) + if err != nil { + return nil, err + } + + if oidcResponse.AccessToken == "" { + common.SysError("OIDC 获取 Token 失败,请检查设置!") + return nil, errors.New("OIDC 获取 Token 失败,请检查设置!") + } + + req, err = http.NewRequest("GET", system_setting.GetOIDCSettings().UserInfoEndpoint, nil) + if err != nil { + return nil, err + } + req.Header.Set("Authorization", "Bearer "+oidcResponse.AccessToken) + res2, err := client.Do(req) + if err != nil { + common.SysLog(err.Error()) + return nil, errors.New("无法连接至 OIDC 服务器,请稍后重试!") + } + defer res2.Body.Close() + if res2.StatusCode != http.StatusOK { + common.SysError("OIDC 获取用户信息失败!请检查设置!") + return nil, errors.New("OIDC 获取用户信息失败!请检查设置!") + } + + var oidcUser OidcUser + err = json.NewDecoder(res2.Body).Decode(&oidcUser) + if err != nil { + return nil, err + } + if oidcUser.OpenID == "" || oidcUser.Email == "" { + common.SysError("OIDC 获取用户信息为空!请检查设置!") + return nil, errors.New("OIDC 获取用户信息为空!请检查设置!") + } + return &oidcUser, nil +} + +func OidcAuth(c *gin.Context) { + session := sessions.Default(c) + state := c.Query("state") + if state == "" || session.Get("oauth_state") == nil || state != session.Get("oauth_state").(string) { + c.JSON(http.StatusForbidden, gin.H{ + "success": false, + "message": "state is empty or not same", + }) + return + } + username := session.Get("username") + if username != nil { + OidcBind(c) + return + } + if !system_setting.GetOIDCSettings().Enabled { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员未开启通过 OIDC 登录以及注册", + }) + return + } + code := c.Query("code") + oidcUser, err := getOidcUserInfoByCode(code) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + user := model.User{ + OidcId: oidcUser.OpenID, + } + if model.IsOidcIdAlreadyTaken(user.OidcId) { + err := user.FillUserByOidcId() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } else { + if common.RegisterEnabled { + user.Email = oidcUser.Email + if oidcUser.PreferredUsername != "" { + user.Username = oidcUser.PreferredUsername + } else { + user.Username = "oidc_" + strconv.Itoa(model.GetMaxUserId()+1) + } + if oidcUser.Name != "" { + user.DisplayName = oidcUser.Name + } else { + user.DisplayName = "OIDC User" + } + err := user.Insert(0) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + } else { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员关闭了新用户注册", + }) + return + } + } + + if user.Status != common.UserStatusEnabled { + c.JSON(http.StatusOK, gin.H{ + "message": "用户已被封禁", + "success": false, + }) + return + } + setupLogin(&user, c) +} + +func OidcBind(c *gin.Context) { + if !system_setting.GetOIDCSettings().Enabled { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "管理员未开启通过 OIDC 登录以及注册", + }) + return + } + code := c.Query("code") + oidcUser, err := getOidcUserInfoByCode(code) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + user := model.User{ + OidcId: oidcUser.OpenID, + } + if model.IsOidcIdAlreadyTaken(user.OidcId) { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "该 OIDC 账户已被绑定", + }) + return + } + session := sessions.Default(c) + id := session.Get("id") + // id := c.GetInt("id") // critical bug! + user.Id = id.(int) + err = user.FillUserById() + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + user.OidcId = oidcUser.OpenID + err = user.Update(false) + if err != nil { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": err.Error(), + }) + return + } + c.JSON(http.StatusOK, gin.H{ + "success": true, + "message": "bind", + }) + return +} diff --git a/controller/option.go b/controller/option.go index c82fbd7e..81ef463c 100644 --- a/controller/option.go +++ b/controller/option.go @@ -6,6 +6,7 @@ import ( "one-api/common" "one-api/model" "one-api/setting" + "one-api/setting/system_setting" "strings" "github.com/gin-gonic/gin" @@ -51,6 +52,14 @@ func UpdateOption(c *gin.Context) { }) return } + case "oidc.enabled": + if option.Value == "true" && system_setting.GetOIDCSettings().ClientId == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无法启用 OIDC 登录,请先填入 OIDC Client Id 以及 OIDC Client Secret!", + }) + return + } case "LinuxDOOAuthEnabled": if option.Value == "true" && common.LinuxDOClientId == "" { c.JSON(http.StatusOK, gin.H{ @@ -81,6 +90,15 @@ func UpdateOption(c *gin.Context) { "success": false, "message": "无法启用 Turnstile 校验,请先填入 Turnstile 校验相关配置信息!", }) + + return + } + case "TelegramOAuthEnabled": + if option.Value == "true" && common.TelegramBotToken == "" { + c.JSON(http.StatusOK, gin.H{ + "success": false, + "message": "无法启用 Telegram OAuth,请先填入 Telegram Bot Token!", + }) return } case "GroupRatio": @@ -92,6 +110,7 @@ func UpdateOption(c *gin.Context) { }) return } + } err = model.UpdateOption(option.Key, option.Value) if err != nil { diff --git a/controller/relay.go b/controller/relay.go index 460599b5..fb4c524f 100644 --- a/controller/relay.go +++ b/controller/relay.go @@ -148,6 +148,50 @@ func WssRelay(c *gin.Context) { } } +func RelayClaude(c *gin.Context) { + //relayMode := constant.Path2RelayMode(c.Request.URL.Path) + requestId := c.GetString(common.RequestIdKey) + group := c.GetString("group") + originalModel := c.GetString("original_model") + var claudeErr *dto.ClaudeErrorWithStatusCode + + for i := 0; i <= common.RetryTimes; i++ { + channel, err := getChannel(c, group, originalModel, i) + if err != nil { + common.LogError(c, err.Error()) + claudeErr = service.ClaudeErrorWrapperLocal(err, "get_channel_failed", http.StatusInternalServerError) + break + } + + claudeErr = claudeRequest(c, channel) + + if claudeErr == nil { + return // 成功处理请求,直接返回 + } + + openaiErr := service.ClaudeErrorToOpenAIError(claudeErr) + + go processChannelError(c, channel.Id, channel.Type, channel.Name, channel.GetAutoBan(), openaiErr) + + if !shouldRetry(c, openaiErr, common.RetryTimes-i) { + break + } + } + useChannel := c.GetStringSlice("use_channel") + if len(useChannel) > 1 { + retryLogStr := fmt.Sprintf("重试:%s", strings.Trim(strings.Join(strings.Fields(fmt.Sprint(useChannel)), "->"), "[]")) + common.LogInfo(c, retryLogStr) + } + + if claudeErr != nil { + claudeErr.Error.Message = common.MessageWithRequestId(claudeErr.Error.Message, requestId) + c.JSON(claudeErr.StatusCode, gin.H{ + "type": "error", + "error": claudeErr.Error, + }) + } +} + func relayRequest(c *gin.Context, relayMode int, channel *model.Channel) *dto.OpenAIErrorWithStatusCode { addUsedChannel(c, channel.Id) requestBody, _ := common.GetRequestBody(c) @@ -162,6 +206,13 @@ func wssRequest(c *gin.Context, ws *websocket.Conn, relayMode int, channel *mode return relay.WssHelper(c, ws) } +func claudeRequest(c *gin.Context, channel *model.Channel) *dto.ClaudeErrorWithStatusCode { + addUsedChannel(c, channel.Id) + requestBody, _ := common.GetRequestBody(c) + c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody)) + return relay.ClaudeHelper(c) +} + func addUsedChannel(c *gin.Context, channelId int) { useChannel := c.GetStringSlice("use_channel") useChannel = append(useChannel, fmt.Sprintf("%d", channelId)) diff --git a/controller/setup.go b/controller/setup.go new file mode 100644 index 00000000..0a13bcf9 --- /dev/null +++ b/controller/setup.go @@ -0,0 +1,173 @@ +package controller + +import ( + "github.com/gin-gonic/gin" + "one-api/common" + "one-api/constant" + "one-api/model" + "one-api/setting/operation_setting" + "time" +) + +type Setup struct { + Status bool `json:"status"` + RootInit bool `json:"root_init"` + DatabaseType string `json:"database_type"` +} + +type SetupRequest struct { + Username string `json:"username"` + Password string `json:"password"` + ConfirmPassword string `json:"confirmPassword"` + SelfUseModeEnabled bool `json:"SelfUseModeEnabled"` + DemoSiteEnabled bool `json:"DemoSiteEnabled"` +} + +func GetSetup(c *gin.Context) { + setup := Setup{ + Status: constant.Setup, + } + if constant.Setup { + c.JSON(200, gin.H{ + "success": true, + "data": setup, + }) + return + } + setup.RootInit = model.RootUserExists() + if common.UsingMySQL { + setup.DatabaseType = "mysql" + } + if common.UsingPostgreSQL { + setup.DatabaseType = "postgres" + } + if common.UsingSQLite { + setup.DatabaseType = "sqlite" + } + c.JSON(200, gin.H{ + "success": true, + "data": setup, + }) +} + +func PostSetup(c *gin.Context) { + // Check if setup is already completed + if constant.Setup { + c.JSON(400, gin.H{ + "success": false, + "message": "系统已经初始化完成", + }) + return + } + + // Check if root user already exists + rootExists := model.RootUserExists() + + var req SetupRequest + err := c.ShouldBindJSON(&req) + if err != nil { + c.JSON(400, gin.H{ + "success": false, + "message": "请求参数有误", + }) + return + } + + // If root doesn't exist, validate and create admin account + if !rootExists { + // Validate password + if req.Password != req.ConfirmPassword { + c.JSON(400, gin.H{ + "success": false, + "message": "两次输入的密码不一致", + }) + return + } + + if len(req.Password) < 8 { + c.JSON(400, gin.H{ + "success": false, + "message": "密码长度至少为8个字符", + }) + return + } + + // Create root user + hashedPassword, err := common.Password2Hash(req.Password) + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "系统错误: " + err.Error(), + }) + return + } + rootUser := model.User{ + Username: req.Username, + Password: hashedPassword, + Role: common.RoleRootUser, + Status: common.UserStatusEnabled, + DisplayName: "Root User", + AccessToken: nil, + Quota: 100000000, + } + err = model.DB.Create(&rootUser).Error + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "创建管理员账号失败: " + err.Error(), + }) + return + } + } + + // Set operation modes + operation_setting.SelfUseModeEnabled = req.SelfUseModeEnabled + operation_setting.DemoSiteEnabled = req.DemoSiteEnabled + + // Save operation modes to database for persistence + err = model.UpdateOption("SelfUseModeEnabled", boolToString(req.SelfUseModeEnabled)) + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "保存自用模式设置失败: " + err.Error(), + }) + return + } + + err = model.UpdateOption("DemoSiteEnabled", boolToString(req.DemoSiteEnabled)) + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "保存演示站点模式设置失败: " + err.Error(), + }) + return + } + + // Update setup status + constant.Setup = true + + setup := model.Setup{ + Version: common.Version, + InitializedAt: time.Now().Unix(), + } + err = model.DB.Create(&setup).Error + if err != nil { + c.JSON(500, gin.H{ + "success": false, + "message": "系统初始化失败: " + err.Error(), + }) + return + } + + c.JSON(200, gin.H{ + "success": true, + "message": "系统初始化成功", + }) +} + +func boolToString(b bool) string { + if b { + return "true" + } + return "false" +} diff --git a/controller/user.go b/controller/user.go index 51e6f955..e194f531 100644 --- a/controller/user.go +++ b/controller/user.go @@ -913,11 +913,12 @@ func TopUp(c *gin.Context) { } type UpdateUserSettingRequest struct { - QuotaWarningType string `json:"notify_type"` - QuotaWarningThreshold float64 `json:"quota_warning_threshold"` - WebhookUrl string `json:"webhook_url,omitempty"` - WebhookSecret string `json:"webhook_secret,omitempty"` - NotificationEmail string `json:"notification_email,omitempty"` + QuotaWarningType string `json:"notify_type"` + QuotaWarningThreshold float64 `json:"quota_warning_threshold"` + WebhookUrl string `json:"webhook_url,omitempty"` + WebhookSecret string `json:"webhook_secret,omitempty"` + NotificationEmail string `json:"notification_email,omitempty"` + AcceptUnsetModelRatioModel bool `json:"accept_unset_model_ratio_model"` } func UpdateUserSetting(c *gin.Context) { @@ -993,6 +994,7 @@ func UpdateUserSetting(c *gin.Context) { settings := map[string]interface{}{ constant.UserSettingNotifyType: req.QuotaWarningType, constant.UserSettingQuotaWarningThreshold: req.QuotaWarningThreshold, + "accept_unset_model_ratio_model": req.AcceptUnsetModelRatioModel, } // 如果是webhook类型,添加webhook相关设置 diff --git a/docker-compose.yml b/docker-compose.yml index 0f23cea2..483c12df 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,6 +15,7 @@ services: - SQL_DSN=root:123456@tcp(mysql:3306)/new-api # Point to the mysql service - REDIS_CONN_STRING=redis://redis - TZ=Asia/Shanghai + # - TIKTOKEN_CACHE_DIR=./tiktoken_cache # 如果需要使用tiktoken_cache,请取消注释 # - SESSION_SECRET=random_string # 多机部署时设置,必须修改这个随机字符串!!!!!!! # - NODE_TYPE=slave # Uncomment for slave node in multi-node deployment # - SYNC_FREQUENCY=60 # Uncomment if regular database syncing is needed diff --git a/docs/channel/other_setting.md b/docs/channel/other_setting.md index b3f4f969..43341660 100644 --- a/docs/channel/other_setting.md +++ b/docs/channel/other_setting.md @@ -11,7 +11,7 @@ - 类型为字符串,填写代理地址(例如 socks5 协议的代理地址) 3. thinking_to_content - - 用于标识是否将思考内容`reasoning_conetnt`转换为``标签拼接到内容中返回 + - 用于标识是否将思考内容`reasoning_content`转换为``标签拼接到内容中返回 - 类型为布尔值,设置为 true 时启用思考内容转换 -------------------------------------------------------------- @@ -30,4 +30,4 @@ -------------------------------------------------------------- -通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。 \ No newline at end of file +通过调整上述 JSON 配置中的值,可以灵活控制渠道的额外行为,比如是否进行格式化以及使用特定的网络代理。 diff --git a/dto/claude.go b/dto/claude.go new file mode 100644 index 00000000..8068feb8 --- /dev/null +++ b/dto/claude.go @@ -0,0 +1,218 @@ +package dto + +import "encoding/json" + +type ClaudeMetadata struct { + UserId string `json:"user_id"` +} + +type ClaudeMediaMessage struct { + Type string `json:"type,omitempty"` + Text *string `json:"text,omitempty"` + Model string `json:"model,omitempty"` + Source *ClaudeMessageSource `json:"source,omitempty"` + Usage *ClaudeUsage `json:"usage,omitempty"` + StopReason *string `json:"stop_reason,omitempty"` + PartialJson *string `json:"partial_json,omitempty"` + Role string `json:"role,omitempty"` + Thinking string `json:"thinking,omitempty"` + Signature string `json:"signature,omitempty"` + Delta string `json:"delta,omitempty"` + // tool_calls + Id string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Input any `json:"input,omitempty"` + Content json.RawMessage `json:"content,omitempty"` + ToolUseId string `json:"tool_use_id,omitempty"` +} + +func (c *ClaudeMediaMessage) SetText(s string) { + c.Text = &s +} + +func (c *ClaudeMediaMessage) GetText() string { + if c.Text == nil { + return "" + } + return *c.Text +} + +func (c *ClaudeMediaMessage) IsStringContent() bool { + var content string + return json.Unmarshal(c.Content, &content) == nil +} + +func (c *ClaudeMediaMessage) GetStringContent() string { + var content string + if err := json.Unmarshal(c.Content, &content); err == nil { + return content + } + return "" +} + +func (c *ClaudeMediaMessage) GetJsonRowString() string { + jsonContent, _ := json.Marshal(c) + return string(jsonContent) +} + +func (c *ClaudeMediaMessage) SetContent(content any) { + jsonContent, _ := json.Marshal(content) + c.Content = jsonContent +} + +func (c *ClaudeMediaMessage) ParseMediaContent() []ClaudeMediaMessage { + var mediaContent []ClaudeMediaMessage + if err := json.Unmarshal(c.Content, &mediaContent); err == nil { + return mediaContent + } + return make([]ClaudeMediaMessage, 0) +} + +type ClaudeMessageSource struct { + Type string `json:"type"` + MediaType string `json:"media_type,omitempty"` + Data any `json:"data,omitempty"` + Url string `json:"url,omitempty"` +} + +type ClaudeMessage struct { + Role string `json:"role"` + Content any `json:"content"` +} + +func (c *ClaudeMessage) IsStringContent() bool { + _, ok := c.Content.(string) + return ok +} + +func (c *ClaudeMessage) GetStringContent() string { + if c.IsStringContent() { + return c.Content.(string) + } + return "" +} + +func (c *ClaudeMessage) SetStringContent(content string) { + c.Content = content +} + +func (c *ClaudeMessage) ParseContent() ([]ClaudeMediaMessage, error) { + // map content to []ClaudeMediaMessage + // parse to json + jsonContent, _ := json.Marshal(c.Content) + var contentList []ClaudeMediaMessage + err := json.Unmarshal(jsonContent, &contentList) + if err != nil { + return make([]ClaudeMediaMessage, 0), err + } + return contentList, nil +} + +type Tool struct { + Name string `json:"name"` + Description string `json:"description,omitempty"` + InputSchema map[string]interface{} `json:"input_schema"` +} + +type InputSchema struct { + Type string `json:"type"` + Properties any `json:"properties,omitempty"` + Required any `json:"required,omitempty"` +} + +type ClaudeRequest struct { + Model string `json:"model"` + Prompt string `json:"prompt,omitempty"` + System any `json:"system,omitempty"` + Messages []ClaudeMessage `json:"messages,omitempty"` + MaxTokens uint `json:"max_tokens,omitempty"` + MaxTokensToSample uint `json:"max_tokens_to_sample,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + //ClaudeMetadata `json:"metadata,omitempty"` + Stream bool `json:"stream,omitempty"` + Tools any `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + Thinking *Thinking `json:"thinking,omitempty"` +} + +type Thinking struct { + Type string `json:"type"` + BudgetTokens int `json:"budget_tokens"` +} + +func (c *ClaudeRequest) IsStringSystem() bool { + _, ok := c.System.(string) + return ok +} + +func (c *ClaudeRequest) GetStringSystem() string { + if c.IsStringSystem() { + return c.System.(string) + } + return "" +} + +func (c *ClaudeRequest) SetStringSystem(system string) { + c.System = system +} + +func (c *ClaudeRequest) ParseSystem() []ClaudeMediaMessage { + // map content to []ClaudeMediaMessage + // parse to json + jsonContent, _ := json.Marshal(c.System) + var contentList []ClaudeMediaMessage + if err := json.Unmarshal(jsonContent, &contentList); err == nil { + return contentList + } + return make([]ClaudeMediaMessage, 0) +} + +type ClaudeError struct { + Type string `json:"type,omitempty"` + Message string `json:"message,omitempty"` +} + +type ClaudeErrorWithStatusCode struct { + Error ClaudeError `json:"error"` + StatusCode int `json:"status_code"` + LocalError bool +} + +type ClaudeResponse struct { + Id string `json:"id,omitempty"` + Type string `json:"type"` + Role string `json:"role,omitempty"` + Content []ClaudeMediaMessage `json:"content,omitempty"` + Completion string `json:"completion,omitempty"` + StopReason string `json:"stop_reason,omitempty"` + Model string `json:"model,omitempty"` + Error *ClaudeError `json:"error,omitempty"` + Usage *ClaudeUsage `json:"usage,omitempty"` + Index *int `json:"index,omitempty"` + ContentBlock *ClaudeMediaMessage `json:"content_block,omitempty"` + Delta *ClaudeMediaMessage `json:"delta,omitempty"` + Message *ClaudeMediaMessage `json:"message,omitempty"` +} + +// set index +func (c *ClaudeResponse) SetIndex(i int) { + c.Index = &i +} + +// get index +func (c *ClaudeResponse) GetIndex() int { + if c.Index == nil { + return 0 + } + return *c.Index +} + +type ClaudeUsage struct { + InputTokens int `json:"input_tokens"` + CacheCreationInputTokens int `json:"cache_creation_input_tokens"` + CacheReadInputTokens int `json:"cache_read_input_tokens"` + OutputTokens int `json:"output_tokens"` +} diff --git a/dto/dalle.go b/dto/dalle.go index d0bba655..562d5f1a 100644 --- a/dto/dalle.go +++ b/dto/dalle.go @@ -1,14 +1,17 @@ package dto +import "encoding/json" + type ImageRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt" binding:"required"` - N int `json:"n,omitempty"` - Size string `json:"size,omitempty"` - Quality string `json:"quality,omitempty"` - ResponseFormat string `json:"response_format,omitempty"` - Style string `json:"style,omitempty"` - User string `json:"user,omitempty"` + Model string `json:"model"` + Prompt string `json:"prompt" binding:"required"` + N int `json:"n,omitempty"` + Size string `json:"size,omitempty"` + Quality string `json:"quality,omitempty"` + ResponseFormat string `json:"response_format,omitempty"` + Style string `json:"style,omitempty"` + User string `json:"user,omitempty"` + ExtraFields json.RawMessage `json:"extra_fields,omitempty"` } type ImageResponse struct { diff --git a/dto/openai_request.go b/dto/openai_request.go index 812e14a5..28aac20f 100644 --- a/dto/openai_request.go +++ b/dto/openai_request.go @@ -18,39 +18,40 @@ type FormatJsonSchema struct { } type GeneralOpenAIRequest struct { - Model string `json:"model,omitempty"` - Messages []Message `json:"messages,omitempty"` - Prompt any `json:"prompt,omitempty"` - Prefix any `json:"prefix,omitempty"` - Suffix any `json:"suffix,omitempty"` - Stream bool `json:"stream,omitempty"` - StreamOptions *StreamOptions `json:"stream_options,omitempty"` - MaxTokens uint `json:"max_tokens,omitempty"` - MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"` - ReasoningEffort string `json:"reasoning_effort,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - Stop any `json:"stop,omitempty"` - N int `json:"n,omitempty"` - Input any `json:"input,omitempty"` - Instruction string `json:"instruction,omitempty"` - Size string `json:"size,omitempty"` - Functions any `json:"functions,omitempty"` - FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` - PresencePenalty float64 `json:"presence_penalty,omitempty"` - ResponseFormat *ResponseFormat `json:"response_format,omitempty"` - EncodingFormat any `json:"encoding_format,omitempty"` - Seed float64 `json:"seed,omitempty"` - Tools []ToolCallRequest `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - User string `json:"user,omitempty"` - LogProbs bool `json:"logprobs,omitempty"` - TopLogProbs int `json:"top_logprobs,omitempty"` - Dimensions int `json:"dimensions,omitempty"` - Modalities any `json:"modalities,omitempty"` - Audio any `json:"audio,omitempty"` - ExtraBody any `json:"extra_body,omitempty"` + Model string `json:"model,omitempty"` + Messages []Message `json:"messages,omitempty"` + Prompt any `json:"prompt,omitempty"` + Prefix any `json:"prefix,omitempty"` + Suffix any `json:"suffix,omitempty"` + Stream bool `json:"stream,omitempty"` + StreamOptions *StreamOptions `json:"stream_options,omitempty"` + MaxTokens uint `json:"max_tokens,omitempty"` + MaxCompletionTokens uint `json:"max_completion_tokens,omitempty"` + ReasoningEffort string `json:"reasoning_effort,omitempty"` + //Reasoning json.RawMessage `json:"reasoning,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Stop any `json:"stop,omitempty"` + N int `json:"n,omitempty"` + Input any `json:"input,omitempty"` + Instruction string `json:"instruction,omitempty"` + Size string `json:"size,omitempty"` + Functions any `json:"functions,omitempty"` + FrequencyPenalty float64 `json:"frequency_penalty,omitempty"` + PresencePenalty float64 `json:"presence_penalty,omitempty"` + ResponseFormat *ResponseFormat `json:"response_format,omitempty"` + EncodingFormat any `json:"encoding_format,omitempty"` + Seed float64 `json:"seed,omitempty"` + Tools []ToolCallRequest `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + User string `json:"user,omitempty"` + LogProbs bool `json:"logprobs,omitempty"` + TopLogProbs int `json:"top_logprobs,omitempty"` + Dimensions int `json:"dimensions,omitempty"` + Modalities any `json:"modalities,omitempty"` + Audio any `json:"audio,omitempty"` + ExtraBody any `json:"extra_body,omitempty"` } type ToolCallRequest struct { @@ -111,11 +112,38 @@ type MediaContent struct { Text string `json:"text,omitempty"` ImageUrl any `json:"image_url,omitempty"` InputAudio any `json:"input_audio,omitempty"` + File any `json:"file,omitempty"` +} + +func (m *MediaContent) GetImageMedia() *MessageImageUrl { + if m.ImageUrl != nil { + return m.ImageUrl.(*MessageImageUrl) + } + return nil +} + +func (m *MediaContent) GetInputAudio() *MessageInputAudio { + if m.InputAudio != nil { + return m.InputAudio.(*MessageInputAudio) + } + return nil +} + +func (m *MediaContent) GetFile() *MessageFile { + if m.File != nil { + return m.File.(*MessageFile) + } + return nil } type MessageImageUrl struct { - Url string `json:"url"` - Detail string `json:"detail"` + Url string `json:"url"` + Detail string `json:"detail"` + MimeType string +} + +func (m *MessageImageUrl) IsRemoteImage() bool { + return strings.HasPrefix(m.Url, "http") } type MessageInputAudio struct { @@ -123,10 +151,17 @@ type MessageInputAudio struct { Format string `json:"format"` } +type MessageFile struct { + FileName string `json:"filename,omitempty"` + FileData string `json:"file_data,omitempty"` + FileId string `json:"file_id,omitempty"` +} + const ( ContentTypeText = "text" ContentTypeImageURL = "image_url" ContentTypeInputAudio = "input_audio" + ContentTypeFile = "file" ) func (m *Message) GetPrefix() bool { @@ -180,6 +215,12 @@ func (m *Message) StringContent() string { return stringContent } +func (m *Message) SetNullContent() { + m.Content = nil + m.parsedStringContent = nil + m.parsedContent = nil +} + func (m *Message) SetStringContent(content string) { jsonContent, _ := json.Marshal(content) m.Content = jsonContent @@ -244,44 +285,64 @@ func (m *Message) ParseContent() []MediaContent { case ContentTypeImageURL: imageUrl := contentItem["image_url"] + temp := &MessageImageUrl{ + Detail: "high", + } switch v := imageUrl.(type) { case string: - contentList = append(contentList, MediaContent{ - Type: ContentTypeImageURL, - ImageUrl: MessageImageUrl{ - Url: v, - Detail: "high", - }, - }) + temp.Url = v case map[string]interface{}: url, ok1 := v["url"].(string) detail, ok2 := v["detail"].(string) - if !ok2 { - detail = "high" + if ok2 { + temp.Detail = detail } if ok1 { - contentList = append(contentList, MediaContent{ - Type: ContentTypeImageURL, - ImageUrl: MessageImageUrl{ - Url: url, - Detail: detail, - }, - }) + temp.Url = url } } + contentList = append(contentList, MediaContent{ + Type: ContentTypeImageURL, + ImageUrl: temp, + }) case ContentTypeInputAudio: if audioData, ok := contentItem["input_audio"].(map[string]interface{}); ok { data, ok1 := audioData["data"].(string) format, ok2 := audioData["format"].(string) if ok1 && ok2 { + temp := &MessageInputAudio{ + Data: data, + Format: format, + } contentList = append(contentList, MediaContent{ - Type: ContentTypeInputAudio, - InputAudio: MessageInputAudio{ - Data: data, - Format: format, + Type: ContentTypeInputAudio, + InputAudio: temp, + }) + } + } + case ContentTypeFile: + if fileData, ok := contentItem["file"].(map[string]interface{}); ok { + fileId, ok3 := fileData["file_id"].(string) + if ok3 { + contentList = append(contentList, MediaContent{ + Type: ContentTypeFile, + File: &MessageFile{ + FileId: fileId, }, }) + } else { + fileName, ok1 := fileData["filename"].(string) + fileDataStr, ok2 := fileData["file_data"].(string) + if ok1 && ok2 { + contentList = append(contentList, MediaContent{ + Type: ContentTypeFile, + File: &MessageFile{ + FileName: fileName, + FileData: fileDataStr, + }, + }) + } } } } diff --git a/dto/openai_response.go b/dto/openai_response.go index 9188fad7..ddd1a907 100644 --- a/dto/openai_response.go +++ b/dto/openai_response.go @@ -1,20 +1,8 @@ package dto -type TextResponseWithError struct { - Id string `json:"id"` - Object string `json:"object"` - Created int64 `json:"created"` - Choices []OpenAITextResponseChoice `json:"choices"` - Data []OpenAIEmbeddingResponseItem `json:"data"` - Model string `json:"model"` - Usage `json:"usage"` - Error OpenAIError `json:"error"` -} - type SimpleResponse struct { - Usage `json:"usage"` - Error OpenAIError `json:"error"` - Choices []OpenAITextResponseChoice `json:"choices"` + Usage `json:"usage"` + Error *OpenAIError `json:"error"` } type TextResponse struct { @@ -38,6 +26,7 @@ type OpenAITextResponse struct { Object string `json:"object"` Created int64 `json:"created"` Choices []OpenAITextResponseChoice `json:"choices"` + Error *OpenAIError `json:"error,omitempty"` Usage `json:"usage"` } @@ -125,6 +114,20 @@ type ChatCompletionsStreamResponse struct { Usage *Usage `json:"usage"` } +func (c *ChatCompletionsStreamResponse) IsToolCall() bool { + if len(c.Choices) == 0 { + return false + } + return len(c.Choices[0].Delta.ToolCalls) > 0 +} + +func (c *ChatCompletionsStreamResponse) GetFirstToolCall() *ToolCallResponse { + if c.IsToolCall() { + return &c.Choices[0].Delta.ToolCalls[0] + } + return nil +} + func (c *ChatCompletionsStreamResponse) Copy() *ChatCompletionsStreamResponse { choices := make([]ChatCompletionsStreamResponseChoice, len(c.Choices)) copy(choices, c.Choices) @@ -170,3 +173,17 @@ type Usage struct { PromptTokensDetails InputTokenDetails `json:"prompt_tokens_details"` CompletionTokenDetails OutputTokenDetails `json:"completion_tokens_details"` } + +type InputTokenDetails struct { + CachedTokens int `json:"cached_tokens"` + CachedCreationTokens int `json:"-"` + TextTokens int `json:"text_tokens"` + AudioTokens int `json:"audio_tokens"` + ImageTokens int `json:"image_tokens"` +} + +type OutputTokenDetails struct { + TextTokens int `json:"text_tokens"` + AudioTokens int `json:"audio_tokens"` + ReasoningTokens int `json:"reasoning_tokens"` +} diff --git a/dto/realtime.go b/dto/realtime.go index e28d813e..86ae352d 100644 --- a/dto/realtime.go +++ b/dto/realtime.go @@ -43,18 +43,6 @@ type RealtimeUsage struct { OutputTokenDetails OutputTokenDetails `json:"output_token_details"` } -type InputTokenDetails struct { - CachedTokens int `json:"cached_tokens"` - TextTokens int `json:"text_tokens"` - AudioTokens int `json:"audio_tokens"` - ImageTokens int `json:"image_tokens"` -} - -type OutputTokenDetails struct { - TextTokens int `json:"text_tokens"` - AudioTokens int `json:"audio_tokens"` -} - type RealtimeSession struct { Modalities []string `json:"modalities"` Instructions string `json:"instructions"` diff --git a/dto/rerank.go b/dto/rerank.go index dfa79633..21f6437c 100644 --- a/dto/rerank.go +++ b/dto/rerank.go @@ -5,18 +5,29 @@ type RerankRequest struct { Query string `json:"query"` Model string `json:"model"` TopN int `json:"top_n"` - ReturnDocuments bool `json:"return_documents,omitempty"` + ReturnDocuments *bool `json:"return_documents,omitempty"` MaxChunkPerDoc int `json:"max_chunk_per_doc,omitempty"` OverLapTokens int `json:"overlap_tokens,omitempty"` } -type RerankResponseDocument struct { +func (r *RerankRequest) GetReturnDocuments() bool { + if r.ReturnDocuments == nil { + return false + } + return *r.ReturnDocuments +} + +type RerankResponseResult struct { Document any `json:"document,omitempty"` Index int `json:"index"` RelevanceScore float64 `json:"relevance_score"` } -type RerankResponse struct { - Results []RerankResponseDocument `json:"results"` - Usage Usage `json:"usage"` +type RerankDocument struct { + Text any `json:"text"` +} + +type RerankResponse struct { + Results []RerankResponseResult `json:"results"` + Usage Usage `json:"usage"` } diff --git a/go.mod b/go.mod index d5686d03..ce768bf3 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/aws/aws-sdk-go-v2/credentials v1.17.11 github.com/aws/aws-sdk-go-v2/service/bedrockruntime v1.7.4 github.com/bytedance/gopkg v0.0.0-20220118071334-3db87571198b + github.com/bytedance/sonic v1.11.6 github.com/gin-contrib/cors v1.7.2 github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/sessions v0.0.5 @@ -28,9 +29,9 @@ require ( github.com/samber/lo v1.39.0 github.com/shirou/gopsutil v3.21.11+incompatible github.com/shopspring/decimal v1.4.0 - golang.org/x/crypto v0.27.0 + golang.org/x/crypto v0.35.0 golang.org/x/image v0.23.0 - golang.org/x/net v0.28.0 + golang.org/x/net v0.35.0 gorm.io/driver/mysql v1.4.3 gorm.io/driver/postgres v1.5.2 gorm.io/gorm v1.25.2 @@ -42,7 +43,6 @@ require ( github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.5 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.5 // indirect github.com/aws/smithy-go v1.20.2 // indirect - github.com/bytedance/sonic v1.11.6 // indirect github.com/bytedance/sonic/loader v0.1.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cloudwego/base64x v0.1.4 // indirect @@ -84,9 +84,9 @@ require ( github.com/yusufpapurcu/wmi v1.2.3 // indirect golang.org/x/arch v0.12.0 // indirect golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 // indirect - golang.org/x/sync v0.10.0 // indirect - golang.org/x/sys v0.27.0 // indirect - golang.org/x/text v0.21.0 // indirect + golang.org/x/sync v0.11.0 // indirect + golang.org/x/sys v0.30.0 // indirect + golang.org/x/text v0.22.0 // indirect google.golang.org/protobuf v1.34.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect modernc.org/libc v1.22.5 // indirect diff --git a/go.sum b/go.sum index 44de7e52..2bd81fa3 100644 --- a/go.sum +++ b/go.sum @@ -217,18 +217,18 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu golang.org/x/arch v0.12.0 h1:UsYJhbzPYGsT0HbEdmYcqtCv8UNGvnaL561NnIUvaKg= golang.org/x/arch v0.12.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.27.0 h1:GXm2NjJrPaiv/h1tb2UH8QfgC/hOf/+z0p6PT8o1w7A= -golang.org/x/crypto v0.27.0/go.mod h1:1Xngt8kV6Dvbssa53Ziq6Eqn0HqbZi5Z6R0ZpwQzt70= +golang.org/x/crypto v0.35.0 h1:b15kiHdrGCHrP6LvwaQ3c03kgNhhiMgvlhxHQhmg2Xs= +golang.org/x/crypto v0.35.0/go.mod h1:dy7dXNW32cAb/6/PRuTNsix8T+vJAqvuIy5Bli/x0YQ= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0 h1:985EYyeCOxTpcgOTJpflJUwOeEz0CQOdPt73OzpE9F8= golang.org/x/exp v0.0.0-20240404231335-c0f41cb1a7a0/go.mod h1:/lliqkxwWAhPjf5oSOIJup2XcqJaw8RGS6k3TGEc7GI= golang.org/x/image v0.23.0 h1:HseQ7c2OpPKTPVzNjG5fwJsOTCiiwS4QdsYi5XU6H68= golang.org/x/image v0.23.0/go.mod h1:wJJBTdLfCCf3tiHa1fNxpZmUI4mmoZvwMCPP0ddoNKY= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.28.0 h1:a9JDOJc5GMUJ0+UDqmLT86WiEy7iWyIhz8gz8E4e5hE= -golang.org/x/net v0.28.0/go.mod h1:yqtgsTWOOnlGLG9GFRrK3++bGOUEkNBoHZc8MEDWPNg= +golang.org/x/net v0.35.0 h1:T5GQRQb2y08kTAByq9L4/bz8cipCdA8FbRTXewonqY8= +golang.org/x/net v0.35.0/go.mod h1:EglIi67kWsHKlRzzVMUD93VMSWGFOMSZgxFjparz1Qk= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ= -golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.11.0 h1:GGz8+XQP4FvTTrjZPzNKTMFtSXH80RAzG+5ghFPgK9w= +golang.org/x/sync v0.11.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -239,14 +239,14 @@ golang.org/x/sys v0.0.0-20220110181412-a018aaa089fe/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.27.0 h1:wBqf8DvsY9Y/2P8gAfPDEYNuS30J4lPHJxXSb/nJZ+s= -golang.org/x/sys v0.27.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.30.0 h1:QjkSwP/36a20jFYWkSue1YwXzLmsV5Gfq7Eiy72C1uc= +golang.org/x/sys v0.30.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo= -golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ= +golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM= +golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/main.go b/main.go index 495057cf..95bde2eb 100644 --- a/main.go +++ b/main.go @@ -12,6 +12,7 @@ import ( "one-api/model" "one-api/router" "one-api/service" + "one-api/setting/operation_setting" "os" "strconv" @@ -33,7 +34,7 @@ var indexPage []byte func main() { err := godotenv.Load(".env") if err != nil { - common.SysLog("Support for .env file is disabled") + common.SysLog("Support for .env file is disabled: " + err.Error()) } common.LoadEnv() @@ -51,6 +52,9 @@ func main() { if err != nil { common.FatalLog("failed to initialize database: " + err.Error()) } + + model.CheckSetup() + // Initialize SQL Database err = model.InitLogDB() if err != nil { @@ -69,10 +73,13 @@ func main() { common.FatalLog("failed to initialize Redis: " + err.Error()) } + // Initialize model settings + operation_setting.InitModelSettings() // Initialize constants constant.InitEnv() // Initialize options model.InitOptionMap() + if common.RedisEnabled { // for compatibility with old versions common.MemoryCacheEnabled = true diff --git a/middleware/auth.go b/middleware/auth.go index a589f52c..fece4553 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -174,6 +174,14 @@ func TokenAuth() func(c *gin.Context) { } c.Request.Header.Set("Authorization", "Bearer "+key) } + // 检查path包含/v1/messages + if strings.Contains(c.Request.URL.Path, "/v1/messages") { + // 从x-api-key中获取key + key := c.Request.Header.Get("x-api-key") + if key != "" { + c.Request.Header.Set("Authorization", "Bearer "+key) + } + } key := c.Request.Header.Get("Authorization") parts := make([]string, 0) key = strings.TrimPrefix(key, "Bearer ") diff --git a/middleware/distributor.go b/middleware/distributor.go index 49fcf59b..fc9f5512 100644 --- a/middleware/distributor.go +++ b/middleware/distributor.go @@ -212,6 +212,7 @@ func SetupContextForSelectedChannel(c *gin.Context, channel *model.Channel, mode c.Set("channel_name", channel.Name) c.Set("channel_type", channel.Type) c.Set("channel_setting", channel.GetSetting()) + c.Set("param_override", channel.GetParamOverride()) if nil != channel.OpenAIOrganization && "" != *channel.OpenAIOrganization { c.Set("channel_organization", *channel.OpenAIOrganization) } diff --git a/middleware/model-rate-limit.go b/middleware/model-rate-limit.go index bd5f9d25..581dc451 100644 --- a/middleware/model-rate-limit.go +++ b/middleware/model-rate-limit.go @@ -5,6 +5,7 @@ import ( "fmt" "net/http" "one-api/common" + "one-api/common/limiter" "one-api/setting" "strconv" "time" @@ -78,21 +79,9 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g ctx := context.Background() rdb := common.RDB - // 1. 检查总请求数限制(当totalMaxCount为0时会自动跳过) - totalKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitCountMark, userId) - allowed, err := checkRedisRateLimit(ctx, rdb, totalKey, totalMaxCount, duration) - if err != nil { - fmt.Println("检查总请求数限制失败:", err.Error()) - abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") - return - } - if !allowed { - abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount)) - } - - // 2. 检查成功请求数限制 + // 1. 检查成功请求数限制 successKey := fmt.Sprintf("rateLimit:%s:%s", ModelRequestRateLimitSuccessCountMark, userId) - allowed, err = checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration) + allowed, err := checkRedisRateLimit(ctx, rdb, successKey, successMaxCount, duration) if err != nil { fmt.Println("检查成功请求数限制失败:", err.Error()) abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") @@ -103,8 +92,27 @@ func redisRateLimitHandler(duration int64, totalMaxCount, successMaxCount int) g return } - // 3. 记录总请求(当totalMaxCount为0时会自动跳过) - recordRedisRequest(ctx, rdb, totalKey, totalMaxCount) + //2.检查总请求数限制并记录总请求(当totalMaxCount为0时会自动跳过,使用令牌桶限流器 + totalKey := fmt.Sprintf("rateLimit:%s", userId) + // 初始化 + tb := limiter.New(ctx, rdb) + allowed, err = tb.Allow( + ctx, + totalKey, + limiter.WithCapacity(int64(totalMaxCount)*duration), + limiter.WithRate(int64(totalMaxCount)), + limiter.WithRequested(duration), + ) + + if err != nil { + fmt.Println("检查总请求数限制失败:", err.Error()) + abortWithOpenAiMessage(c, http.StatusInternalServerError, "rate_limit_check_failed") + return + } + + if !allowed { + abortWithOpenAiMessage(c, http.StatusTooManyRequests, fmt.Sprintf("您已达到总请求数限制:%d分钟内最多请求%d次,包括失败次数,请检查您的请求是否正确", setting.ModelRequestRateLimitDurationMinutes, totalMaxCount)) + } // 4. 处理请求 c.Next() diff --git a/model/channel.go b/model/channel.go index 6ff0901d..91f5384c 100644 --- a/model/channel.go +++ b/model/channel.go @@ -35,7 +35,8 @@ type Channel struct { AutoBan *int `json:"auto_ban" gorm:"default:1"` OtherInfo string `json:"other_info"` Tag *string `json:"tag" gorm:"index"` - Setting string `json:"setting" gorm:"type:text"` + Setting *string `json:"setting" gorm:"type:text"` + ParamOverride *string `json:"param_override" gorm:"type:text"` } func (channel *Channel) GetModels() []string { @@ -493,8 +494,8 @@ func SearchTags(keyword string, group string, model string, idSort bool) ([]*str func (channel *Channel) GetSetting() map[string]interface{} { setting := make(map[string]interface{}) - if channel.Setting != "" { - err := json.Unmarshal([]byte(channel.Setting), &setting) + if channel.Setting != nil && *channel.Setting != "" { + err := json.Unmarshal([]byte(*channel.Setting), &setting) if err != nil { common.SysError("failed to unmarshal setting: " + err.Error()) } @@ -508,7 +509,18 @@ func (channel *Channel) SetSetting(setting map[string]interface{}) { common.SysError("failed to marshal setting: " + err.Error()) return } - channel.Setting = string(settingBytes) + channel.Setting = common.GetPointer[string](string(settingBytes)) +} + +func (channel *Channel) GetParamOverride() map[string]interface{} { + paramOverride := make(map[string]interface{}) + if channel.ParamOverride != nil && *channel.ParamOverride != "" { + err := json.Unmarshal([]byte(*channel.ParamOverride), ¶mOverride) + if err != nil { + common.SysError("failed to unmarshal param override: " + err.Error()) + } + } + return paramOverride } func GetChannelsByIds(ids []int) ([]*Channel, error) { diff --git a/model/main.go b/model/main.go index c0bf927c..61d6bb10 100644 --- a/model/main.go +++ b/model/main.go @@ -1,16 +1,18 @@ package model import ( - "github.com/glebarez/sqlite" - "gorm.io/driver/mysql" - "gorm.io/driver/postgres" - "gorm.io/gorm" "log" "one-api/common" + "one-api/constant" "os" "strings" "sync" "time" + + "github.com/glebarez/sqlite" + "gorm.io/driver/mysql" + "gorm.io/driver/postgres" + "gorm.io/gorm" ) var groupCol string @@ -54,13 +56,40 @@ func createRootAccountIfNeed() error { return nil } +func CheckSetup() { + setup := GetSetup() + if setup == nil { + // No setup record exists, check if we have a root user + if RootUserExists() { + common.SysLog("system is not initialized, but root user exists") + // Create setup record + newSetup := Setup{ + Version: common.Version, + InitializedAt: time.Now().Unix(), + } + err := DB.Create(&newSetup).Error + if err != nil { + common.SysLog("failed to create setup record: " + err.Error()) + } + constant.Setup = true + } else { + common.SysLog("system is not initialized and no root user exists") + constant.Setup = false + } + } else { + // Setup record exists, system is initialized + common.SysLog("system is already initialized at: " + time.Unix(setup.InitializedAt, 0).String()) + constant.Setup = true + } +} + func chooseDB(envName string) (*gorm.DB, error) { defer func() { initCol() }() dsn := os.Getenv(envName) if dsn != "" { - if strings.HasPrefix(dsn, "postgres://") { + if strings.HasPrefix(dsn, "postgres://") || strings.HasPrefix(dsn, "postgresql://") { // Use PostgreSQL common.SysLog("using PostgreSQL as database") common.UsingPostgreSQL = true @@ -213,8 +242,9 @@ func migrateDB() error { if err != nil { return err } + err = DB.AutoMigrate(&Setup{}) common.SysLog("database migrated") - err = createRootAccountIfNeed() + //err = createRootAccountIfNeed() return err } diff --git a/model/setup.go b/model/setup.go new file mode 100644 index 00000000..c4d7997f --- /dev/null +++ b/model/setup.go @@ -0,0 +1,16 @@ +package model + +type Setup struct { + ID uint `json:"id" gorm:"primaryKey"` + Version string `json:"version" gorm:"type:varchar(50);not null"` + InitializedAt int64 `json:"initialized_at" gorm:"type:bigint;not null"` +} + +func GetSetup() *Setup { + var setup Setup + err := DB.First(&setup).Error + if err != nil { + return nil + } + return &setup +} diff --git a/model/user.go b/model/user.go index 524f56b6..0aea2ff5 100644 --- a/model/user.go +++ b/model/user.go @@ -9,7 +9,6 @@ import ( "strings" "github.com/bytedance/gopkg/util/gopool" - "gorm.io/gorm" ) @@ -24,6 +23,7 @@ type User struct { Status int `json:"status" gorm:"type:int;default:1"` // enabled, disabled Email string `json:"email" gorm:"index" validate:"max=50"` GitHubId string `json:"github_id" gorm:"column:github_id;index"` + OidcId string `json:"oidc_id" gorm:"column:oidc_id;index"` WeChatId string `json:"wechat_id" gorm:"column:wechat_id;index"` TelegramId string `json:"telegram_id" gorm:"column:telegram_id;index"` VerificationCode string `json:"verification_code" gorm:"-:all"` // this field is only for Email verification, don't save it to database! @@ -108,7 +108,7 @@ func CheckUserExistOrDeleted(username string, email string) (bool, error) { func GetMaxUserId() int { var user User - DB.Last(&user) + DB.Unscoped().Last(&user) return user.Id } @@ -442,6 +442,14 @@ func (user *User) FillUserByGitHubId() error { return nil } +func (user *User) FillUserByOidcId() error { + if user.OidcId == "" { + return errors.New("oidc id 为空!") + } + DB.Where(User{OidcId: user.OidcId}).First(user) + return nil +} + func (user *User) FillUserByWeChatId() error { if user.WeChatId == "" { return errors.New("WeChat id 为空!") @@ -473,6 +481,10 @@ func IsGitHubIdAlreadyTaken(githubId string) bool { return DB.Unscoped().Where("github_id = ?", githubId).Find(&User{}).RowsAffected == 1 } +func IsOidcIdAlreadyTaken(oidcId string) bool { + return DB.Where("oidc_id = ?", oidcId).Find(&User{}).RowsAffected == 1 +} + func IsTelegramIdAlreadyTaken(telegramId string) bool { return DB.Unscoped().Where("telegram_id = ?", telegramId).Find(&User{}).RowsAffected == 1 } @@ -796,3 +808,12 @@ func (user *User) FillUserByLinuxDOId() error { err := DB.Where("linux_do_id = ?", user.LinuxDOId).First(user).Error return err } + +func RootUserExists() bool { + var user User + err := DB.Where("role = ?", common.RoleRootUser).First(&user).Error + if err != nil { + return false + } + return true +} diff --git a/relay/channel/adapter.go b/relay/channel/adapter.go index c970fd48..e097dbe6 100644 --- a/relay/channel/adapter.go +++ b/relay/channel/adapter.go @@ -13,7 +13,7 @@ type Adaptor interface { Init(info *relaycommon.RelayInfo) GetRequestURL(info *relaycommon.RelayInfo) (string, error) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error - ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) + ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) @@ -22,6 +22,7 @@ type Adaptor interface { DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) GetModelList() []string GetChannelName() string + ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) } type TaskAdaptor interface { diff --git a/relay/channel/ali/adaptor.go b/relay/channel/ali/adaptor.go index 32be399b..0cbcef44 100644 --- a/relay/channel/ali/adaptor.go +++ b/relay/channel/ali/adaptor.go @@ -16,6 +16,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } @@ -44,7 +50,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -87,7 +93,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } } return diff --git a/relay/channel/ali/image.go b/relay/channel/ali/image.go index 160fabf9..44203583 100644 --- a/relay/channel/ali/image.go +++ b/relay/channel/ali/image.go @@ -26,8 +26,8 @@ func oaiImage2Ali(request dto.ImageRequest) *AliImageRequest { return &imageRequest } -func updateTask(info *relaycommon.RelayInfo, taskID string, key string) (*AliResponse, error, []byte) { - url := fmt.Sprintf("/api/v1/tasks/%s", taskID) +func updateTask(info *relaycommon.RelayInfo, taskID string) (*AliResponse, error, []byte) { + url := fmt.Sprintf("%s/api/v1/tasks/%s", info.BaseUrl, taskID) var aliResponse AliResponse @@ -36,7 +36,7 @@ func updateTask(info *relaycommon.RelayInfo, taskID string, key string) (*AliRes return &aliResponse, err, nil } - req.Header.Set("Authorization", "Bearer "+key) + req.Header.Set("Authorization", "Bearer "+info.ApiKey) client := &http.Client{} resp, err := client.Do(req) @@ -58,7 +58,7 @@ func updateTask(info *relaycommon.RelayInfo, taskID string, key string) (*AliRes return &response, nil, responseBody } -func asyncTaskWait(info *relaycommon.RelayInfo, taskID string, key string) (*AliResponse, []byte, error) { +func asyncTaskWait(info *relaycommon.RelayInfo, taskID string) (*AliResponse, []byte, error) { waitSeconds := 3 step := 0 maxStep := 20 @@ -68,7 +68,7 @@ func asyncTaskWait(info *relaycommon.RelayInfo, taskID string, key string) (*Ali for { step++ - rsp, err, body := updateTask(info, taskID, key) + rsp, err, body := updateTask(info, taskID) responseBody = body if err != nil { return &taskResponse, responseBody, err @@ -125,8 +125,6 @@ func responseAli2OpenAIImage(c *gin.Context, response *AliResponse, info *relayc } func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - apiKey := c.Request.Header.Get("Authorization") - apiKey = strings.TrimPrefix(apiKey, "Bearer ") responseFormat := c.GetString("response_format") var aliTaskResponse AliResponse @@ -148,7 +146,7 @@ func aliImageHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rela return service.OpenAIErrorWrapper(errors.New(aliTaskResponse.Message), "ali_async_task_failed", http.StatusInternalServerError), nil } - aliResponse, _, err := asyncTaskWait(info, aliTaskResponse.Output.TaskId, apiKey) + aliResponse, _, err := asyncTaskWait(info, aliTaskResponse.Output.TaskId) if err != nil { return service.OpenAIErrorWrapper(err, "ali_async_task_wait_failed", http.StatusInternalServerError), nil } diff --git a/relay/channel/api_request.go b/relay/channel/api_request.go index a60bc6f1..8b2ca889 100644 --- a/relay/channel/api_request.go +++ b/relay/channel/api_request.go @@ -7,6 +7,7 @@ import ( "github.com/gorilla/websocket" "io" "net/http" + common2 "one-api/common" "one-api/relay/common" "one-api/relay/constant" "one-api/service" @@ -31,6 +32,9 @@ func DoApiRequest(a Adaptor, c *gin.Context, info *common.RelayInfo, requestBody if err != nil { return nil, fmt.Errorf("get request url failed: %w", err) } + if common2.DebugEnabled { + println("fullRequestURL:", fullRequestURL) + } req, err := http.NewRequest(c.Request.Method, fullRequestURL, requestBody) if err != nil { return nil, fmt.Errorf("new request failed: %w", err) diff --git a/relay/channel/aws/adaptor.go b/relay/channel/aws/adaptor.go index 7f2a2841..ceed39a2 100644 --- a/relay/channel/aws/adaptor.go +++ b/relay/channel/aws/adaptor.go @@ -20,6 +20,12 @@ type Adaptor struct { RequestMode int } +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { + c.Set("request_model", request.Model) + c.Set("converted_request", request) + return request, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -43,12 +49,12 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } - var claudeReq *claude.ClaudeRequest + var claudeReq *dto.ClaudeRequest var err error claudeReq, err = claude.RequestOpenAI2ClaudeMessage(*request) if err != nil { diff --git a/relay/channel/aws/constants.go b/relay/channel/aws/constants.go index 66dc7cd9..37196fd8 100644 --- a/relay/channel/aws/constants.go +++ b/relay/channel/aws/constants.go @@ -13,4 +13,41 @@ var awsModelIDMap = map[string]string{ "claude-3-7-sonnet-20250219": "anthropic.claude-3-7-sonnet-20250219-v1:0", } +var awsModelCanCrossRegionMap = map[string]map[string]bool{ + "anthropic.claude-3-sonnet-20240229-v1:0": { + "us": true, + "eu": true, + "ap": true, + }, + "anthropic.claude-3-opus-20240229-v1:0": { + "us": true, + }, + "anthropic.claude-3-haiku-20240307-v1:0": { + "us": true, + "eu": true, + "ap": true, + }, + "anthropic.claude-3-5-sonnet-20240620-v1:0": { + "us": true, + "eu": true, + "ap": true, + }, + "anthropic.claude-3-5-sonnet-20241022-v2:0": { + "us": true, + "ap": true, + }, + "anthropic.claude-3-5-haiku-20241022-v1:0": { + "us": true, + }, + "anthropic.claude-3-7-sonnet-20250219-v1:0": { + "us": true, + }, +} + +var awsRegionCrossModelPrefixMap = map[string]string{ + "us": "us", + "eu": "eu", + "ap": "apac", +} + var ChannelName = "aws" diff --git a/relay/channel/aws/dto.go b/relay/channel/aws/dto.go index 3b615134..0188c30a 100644 --- a/relay/channel/aws/dto.go +++ b/relay/channel/aws/dto.go @@ -1,25 +1,25 @@ package aws import ( - "one-api/relay/channel/claude" + "one-api/dto" ) type AwsClaudeRequest struct { // AnthropicVersion should be "bedrock-2023-05-31" - AnthropicVersion string `json:"anthropic_version"` - System string `json:"system,omitempty"` - Messages []claude.ClaudeMessage `json:"messages"` - MaxTokens uint `json:"max_tokens,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - StopSequences []string `json:"stop_sequences,omitempty"` - Tools any `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - Thinking *claude.Thinking `json:"thinking,omitempty"` + AnthropicVersion string `json:"anthropic_version"` + System any `json:"system,omitempty"` + Messages []dto.ClaudeMessage `json:"messages"` + MaxTokens uint `json:"max_tokens,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + Tools any `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + Thinking *dto.Thinking `json:"thinking,omitempty"` } -func copyRequest(req *claude.ClaudeRequest) *AwsClaudeRequest { +func copyRequest(req *dto.ClaudeRequest) *AwsClaudeRequest { return &AwsClaudeRequest{ AnthropicVersion: "bedrock-2023-05-31", System: req.System, diff --git a/relay/channel/aws/relay-aws.go b/relay/channel/aws/relay-aws.go index 976f97ce..3c9542c6 100644 --- a/relay/channel/aws/relay-aws.go +++ b/relay/channel/aws/relay-aws.go @@ -1,21 +1,16 @@ package aws import ( - "bytes" "encoding/json" "fmt" "github.com/gin-gonic/gin" "github.com/pkg/errors" - "io" "net/http" "one-api/common" - relaymodel "one-api/dto" + "one-api/dto" "one-api/relay/channel/claude" relaycommon "one-api/relay/common" - "one-api/relay/helper" - "one-api/service" "strings" - "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/credentials" @@ -39,15 +34,37 @@ func newAwsClient(c *gin.Context, info *relaycommon.RelayInfo) (*bedrockruntime. return client, nil } -func wrapErr(err error) *relaymodel.OpenAIErrorWithStatusCode { - return &relaymodel.OpenAIErrorWithStatusCode{ +func wrapErr(err error) *dto.OpenAIErrorWithStatusCode { + return &dto.OpenAIErrorWithStatusCode{ StatusCode: http.StatusInternalServerError, - Error: relaymodel.OpenAIError{ + Error: dto.OpenAIError{ Message: fmt.Sprintf("%s", err.Error()), }, } } +func awsRegionPrefix(awsRegionId string) string { + parts := strings.Split(awsRegionId, "-") + regionPrefix := "" + if len(parts) > 0 { + regionPrefix = parts[0] + } + return regionPrefix +} + +func awsModelCanCrossRegion(awsModelId, awsRegionPrefix string) bool { + regionSet, exists := awsModelCanCrossRegionMap[awsModelId] + return exists && regionSet[awsRegionPrefix] +} + +func awsModelCrossRegion(awsModelId, awsRegionPrefix string) string { + modelPrefix, find := awsRegionCrossModelPrefixMap[awsRegionPrefix] + if !find { + return awsModelId + } + return modelPrefix + "." + awsModelId +} + func awsModelID(requestModel string) (string, error) { if awsModelID, ok := awsModelIDMap[requestModel]; ok { return awsModelID, nil @@ -56,7 +73,7 @@ func awsModelID(requestModel string) (string, error) { return requestModel, nil } -func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*relaymodel.OpenAIErrorWithStatusCode, *relaymodel.Usage) { +func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { awsCli, err := newAwsClient(c, info) if err != nil { return wrapErr(errors.Wrap(err, "newAwsClient")), nil @@ -67,6 +84,12 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (* return wrapErr(errors.Wrap(err, "awsModelID")), nil } + awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) + canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) + if canCrossRegion { + awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) + } + awsReq := &bedrockruntime.InvokeModelInput{ ModelId: aws.String(awsModelId), Accept: aws.String("application/json"), @@ -77,7 +100,7 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (* if !ok { return wrapErr(errors.New("request not found")), nil } - claudeReq := claudeReq_.(*claude.ClaudeRequest) + claudeReq := claudeReq_.(*dto.ClaudeRequest) awsClaudeReq := copyRequest(claudeReq) awsReq.Body, err = json.Marshal(awsClaudeReq) if err != nil { @@ -89,25 +112,19 @@ func awsHandler(c *gin.Context, info *relaycommon.RelayInfo, requestMode int) (* return wrapErr(errors.Wrap(err, "InvokeModel")), nil } - claudeResponse := new(claude.ClaudeResponse) - err = json.Unmarshal(awsResp.Body, claudeResponse) - if err != nil { - return wrapErr(errors.Wrap(err, "unmarshal response")), nil + claudeInfo := &claude.ClaudeResponseInfo{ + ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), + Created: common.GetTimestamp(), + Model: info.UpstreamModelName, + ResponseText: strings.Builder{}, + Usage: &dto.Usage{}, } - openaiResp := claude.ResponseClaude2OpenAI(requestMode, claudeResponse) - usage := relaymodel.Usage{ - PromptTokens: claudeResponse.Usage.InputTokens, - CompletionTokens: claudeResponse.Usage.OutputTokens, - TotalTokens: claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens, - } - openaiResp.Usage = usage - - c.JSON(http.StatusOK, openaiResp) - return nil, &usage + claude.HandleClaudeResponseData(c, info, claudeInfo, awsResp.Body, RequestModeMessage) + return nil, claudeInfo.Usage } -func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*relaymodel.OpenAIErrorWithStatusCode, *relaymodel.Usage) { +func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { awsCli, err := newAwsClient(c, info) if err != nil { return wrapErr(errors.Wrap(err, "newAwsClient")), nil @@ -118,6 +135,12 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel return wrapErr(errors.Wrap(err, "awsModelID")), nil } + awsRegionPrefix := awsRegionPrefix(awsCli.Options().Region) + canCrossRegion := awsModelCanCrossRegion(awsModelId, awsRegionPrefix) + if canCrossRegion { + awsModelId = awsModelCrossRegion(awsModelId, awsRegionPrefix) + } + awsReq := &bedrockruntime.InvokeModelWithResponseStreamInput{ ModelId: aws.String(awsModelId), Accept: aws.String("application/json"), @@ -128,7 +151,7 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel if !ok { return wrapErr(errors.New("request not found")), nil } - claudeReq := claudeReq_.(*claude.ClaudeRequest) + claudeReq := claudeReq_.(*dto.ClaudeRequest) awsClaudeReq := copyRequest(claudeReq) awsReq.Body, err = json.Marshal(awsClaudeReq) @@ -143,79 +166,31 @@ func awsStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel stream := awsResp.GetStream() defer stream.Close() - c.Writer.Header().Set("Content-Type", "text/event-stream") - var usage relaymodel.Usage - var id string - var model string - isFirst := true - createdTime := common.GetTimestamp() - c.Stream(func(w io.Writer) bool { - event, ok := <-stream.Events() - if !ok { - return false - } + claudeInfo := &claude.ClaudeResponseInfo{ + ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), + Created: common.GetTimestamp(), + Model: info.UpstreamModelName, + ResponseText: strings.Builder{}, + Usage: &dto.Usage{}, + } + for event := range stream.Events() { switch v := event.(type) { case *types.ResponseStreamMemberChunk: - if isFirst { - isFirst = false - info.FirstResponseTime = time.Now() + info.SetFirstResponseTime() + respErr := claude.HandleStreamResponseData(c, info, claudeInfo, string(v.Value.Bytes), RequestModeMessage) + if respErr != nil { + return respErr, nil } - claudeResp := new(claude.ClaudeResponse) - err := json.NewDecoder(bytes.NewReader(v.Value.Bytes)).Decode(claudeResp) - if err != nil { - common.SysError("error unmarshalling stream response: " + err.Error()) - return false - } - - response, claudeUsage := claude.StreamResponseClaude2OpenAI(requestMode, claudeResp) - if claudeUsage != nil { - usage.PromptTokens += claudeUsage.InputTokens - usage.CompletionTokens += claudeUsage.OutputTokens - } - - if response == nil { - return true - } - - if response.Id != "" { - id = response.Id - } - if response.Model != "" { - model = response.Model - } - response.Created = createdTime - response.Id = id - response.Model = model - - jsonStr, err := json.Marshal(response) - if err != nil { - common.SysError("error marshalling stream response: " + err.Error()) - return true - } - c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonStr)}) - return true case *types.UnknownUnionMember: fmt.Println("unknown tag:", v.Tag) - return false + return wrapErr(errors.New("unknown response type")), nil default: fmt.Println("union is nil or unknown type") - return false - } - }) - if info.ShouldIncludeUsage { - response := helper.GenerateFinalUsageResponse(id, createdTime, info.UpstreamModelName, usage) - err := helper.ObjectData(c, response) - if err != nil { - common.SysError("send final response failed: " + err.Error()) + return wrapErr(errors.New("nil or unknown response type")), nil } } - helper.Done(c) - if resp != nil { - err = resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapperLocal(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - } - return nil, &usage + + claude.HandleStreamFinalResponse(c, info, claudeInfo, RequestModeMessage) + return nil, claudeInfo.Usage } diff --git a/relay/channel/baidu/adaptor.go b/relay/channel/baidu/adaptor.go index 46a1f964..eecb0bac 100644 --- a/relay/channel/baidu/adaptor.go +++ b/relay/channel/baidu/adaptor.go @@ -16,6 +16,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -104,7 +110,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } diff --git a/relay/channel/baidu_v2/adaptor.go b/relay/channel/baidu_v2/adaptor.go index fd25ecc1..ec7936dc 100644 --- a/relay/channel/baidu_v2/adaptor.go +++ b/relay/channel/baidu_v2/adaptor.go @@ -15,6 +15,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -38,7 +44,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -62,7 +68,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/claude/adaptor.go b/relay/channel/claude/adaptor.go index bf03e5f5..6d65d6d4 100644 --- a/relay/channel/claude/adaptor.go +++ b/relay/channel/claude/adaptor.go @@ -22,6 +22,10 @@ type Adaptor struct { RequestMode int } +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { + return request, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -60,7 +64,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } diff --git a/relay/channel/claude/dto.go b/relay/channel/claude/dto.go index 9532ca74..89415868 100644 --- a/relay/channel/claude/dto.go +++ b/relay/channel/claude/dto.go @@ -1,94 +1,95 @@ package claude -type ClaudeMetadata struct { - UserId string `json:"user_id"` -} - -type ClaudeMediaMessage struct { - Type string `json:"type"` - Text string `json:"text,omitempty"` - Source *ClaudeMessageSource `json:"source,omitempty"` - Usage *ClaudeUsage `json:"usage,omitempty"` - StopReason *string `json:"stop_reason,omitempty"` - PartialJson string `json:"partial_json,omitempty"` - Thinking string `json:"thinking,omitempty"` - Signature string `json:"signature,omitempty"` - Delta string `json:"delta,omitempty"` - // tool_calls - Id string `json:"id,omitempty"` - Name string `json:"name,omitempty"` - Input any `json:"input,omitempty"` - Content string `json:"content,omitempty"` - ToolUseId string `json:"tool_use_id,omitempty"` -} - -type ClaudeMessageSource struct { - Type string `json:"type"` - MediaType string `json:"media_type"` - Data string `json:"data"` -} - -type ClaudeMessage struct { - Role string `json:"role"` - Content any `json:"content"` -} - -type Tool struct { - Name string `json:"name"` - Description string `json:"description,omitempty"` - InputSchema map[string]interface{} `json:"input_schema"` -} - -type InputSchema struct { - Type string `json:"type"` - Properties any `json:"properties,omitempty"` - Required any `json:"required,omitempty"` -} - -type ClaudeRequest struct { - Model string `json:"model"` - Prompt string `json:"prompt,omitempty"` - System string `json:"system,omitempty"` - Messages []ClaudeMessage `json:"messages,omitempty"` - MaxTokens uint `json:"max_tokens,omitempty"` - MaxTokensToSample uint `json:"max_tokens_to_sample,omitempty"` - StopSequences []string `json:"stop_sequences,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - //ClaudeMetadata `json:"metadata,omitempty"` - Stream bool `json:"stream,omitempty"` - Tools any `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - Thinking *Thinking `json:"thinking,omitempty"` -} - -type Thinking struct { - Type string `json:"type"` - BudgetTokens int `json:"budget_tokens"` -} - -type ClaudeError struct { - Type string `json:"type"` - Message string `json:"message"` -} - -type ClaudeResponse struct { - Id string `json:"id"` - Type string `json:"type"` - Content []ClaudeMediaMessage `json:"content"` - Completion string `json:"completion"` - StopReason string `json:"stop_reason"` - Model string `json:"model"` - Error ClaudeError `json:"error"` - Usage ClaudeUsage `json:"usage"` - Index int `json:"index"` // stream only - ContentBlock *ClaudeMediaMessage `json:"content_block"` - Delta *ClaudeMediaMessage `json:"delta"` // stream only - Message *ClaudeResponse `json:"message"` // stream only: message_start -} - -type ClaudeUsage struct { - InputTokens int `json:"input_tokens"` - OutputTokens int `json:"output_tokens"` -} +// +//type ClaudeMetadata struct { +// UserId string `json:"user_id"` +//} +// +//type ClaudeMediaMessage struct { +// Type string `json:"type"` +// Text string `json:"text,omitempty"` +// Source *ClaudeMessageSource `json:"source,omitempty"` +// Usage *ClaudeUsage `json:"usage,omitempty"` +// StopReason *string `json:"stop_reason,omitempty"` +// PartialJson string `json:"partial_json,omitempty"` +// Thinking string `json:"thinking,omitempty"` +// Signature string `json:"signature,omitempty"` +// Delta string `json:"delta,omitempty"` +// // tool_calls +// Id string `json:"id,omitempty"` +// Name string `json:"name,omitempty"` +// Input any `json:"input,omitempty"` +// Content string `json:"content,omitempty"` +// ToolUseId string `json:"tool_use_id,omitempty"` +//} +// +//type ClaudeMessageSource struct { +// Type string `json:"type"` +// MediaType string `json:"media_type"` +// Data string `json:"data"` +//} +// +//type ClaudeMessage struct { +// Role string `json:"role"` +// Content any `json:"content"` +//} +// +//type Tool struct { +// Name string `json:"name"` +// Description string `json:"description,omitempty"` +// InputSchema map[string]interface{} `json:"input_schema"` +//} +// +//type InputSchema struct { +// Type string `json:"type"` +// Properties any `json:"properties,omitempty"` +// Required any `json:"required,omitempty"` +//} +// +//type ClaudeRequest struct { +// Model string `json:"model"` +// Prompt string `json:"prompt,omitempty"` +// System string `json:"system,omitempty"` +// Messages []ClaudeMessage `json:"messages,omitempty"` +// MaxTokens uint `json:"max_tokens,omitempty"` +// MaxTokensToSample uint `json:"max_tokens_to_sample,omitempty"` +// StopSequences []string `json:"stop_sequences,omitempty"` +// Temperature *float64 `json:"temperature,omitempty"` +// TopP float64 `json:"top_p,omitempty"` +// TopK int `json:"top_k,omitempty"` +// //ClaudeMetadata `json:"metadata,omitempty"` +// Stream bool `json:"stream,omitempty"` +// Tools any `json:"tools,omitempty"` +// ToolChoice any `json:"tool_choice,omitempty"` +// Thinking *Thinking `json:"thinking,omitempty"` +//} +// +//type Thinking struct { +// Type string `json:"type"` +// BudgetTokens int `json:"budget_tokens"` +//} +// +//type ClaudeError struct { +// Type string `json:"type"` +// Message string `json:"message"` +//} +// +//type ClaudeResponse struct { +// Id string `json:"id"` +// Type string `json:"type"` +// Content []ClaudeMediaMessage `json:"content"` +// Completion string `json:"completion"` +// StopReason string `json:"stop_reason"` +// Model string `json:"model"` +// Error ClaudeError `json:"error"` +// Usage ClaudeUsage `json:"usage"` +// Index int `json:"index"` // stream only +// ContentBlock *ClaudeMediaMessage `json:"content_block"` +// Delta *ClaudeMediaMessage `json:"delta"` // stream only +// Message *ClaudeResponse `json:"message"` // stream only: message_start +//} +// +//type ClaudeUsage struct { +// InputTokens int `json:"input_tokens"` +// OutputTokens int `json:"output_tokens"` +//} diff --git a/relay/channel/claude/relay-claude.go b/relay/channel/claude/relay-claude.go index 09154bcb..95e7c4be 100644 --- a/relay/channel/claude/relay-claude.go +++ b/relay/channel/claude/relay-claude.go @@ -24,14 +24,16 @@ func stopReasonClaude2OpenAI(reason string) string { return "stop" case "max_tokens": return "max_tokens" + case "tool_use": + return "tool_calls" default: return reason } } -func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *ClaudeRequest { +func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *dto.ClaudeRequest { - claudeRequest := ClaudeRequest{ + claudeRequest := dto.ClaudeRequest{ Model: textRequest.Model, Prompt: "", StopSequences: nil, @@ -60,17 +62,19 @@ func RequestOpenAI2ClaudeComplete(textRequest dto.GeneralOpenAIRequest) *ClaudeR return &claudeRequest } -func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeRequest, error) { - claudeTools := make([]Tool, 0, len(textRequest.Tools)) +func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*dto.ClaudeRequest, error) { + claudeTools := make([]dto.Tool, 0, len(textRequest.Tools)) for _, tool := range textRequest.Tools { if params, ok := tool.Function.Parameters.(map[string]any); ok { - claudeTool := Tool{ + claudeTool := dto.Tool{ Name: tool.Function.Name, Description: tool.Function.Description, } claudeTool.InputSchema = make(map[string]interface{}) - claudeTool.InputSchema["type"] = params["type"].(string) + if params["type"] != nil { + claudeTool.InputSchema["type"] = params["type"].(string) + } claudeTool.InputSchema["properties"] = params["properties"] claudeTool.InputSchema["required"] = params["required"] for s, a := range params { @@ -83,7 +87,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR } } - claudeRequest := ClaudeRequest{ + claudeRequest := dto.ClaudeRequest{ Model: textRequest.Model, MaxTokens: textRequest.MaxTokens, StopSequences: nil, @@ -107,7 +111,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR } // BudgetTokens 为 max_tokens 的 80% - claudeRequest.Thinking = &Thinking{ + claudeRequest.Thinking = &dto.Thinking{ Type: "enabled", BudgetTokens: int(float64(claudeRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage), } @@ -165,7 +169,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR lastMessage = fmtMessage } - claudeMessages := make([]ClaudeMessage, 0) + claudeMessages := make([]dto.ClaudeMessage, 0) isFirstMessage := true for _, message := range formatMessages { if message.Role == "system" { @@ -186,63 +190,63 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR isFirstMessage = false if message.Role != "user" { // fix: first message is assistant, add user message - claudeMessage := ClaudeMessage{ + claudeMessage := dto.ClaudeMessage{ Role: "user", - Content: []ClaudeMediaMessage{ + Content: []dto.ClaudeMediaMessage{ { Type: "text", - Text: "...", + Text: common.GetPointer[string]("..."), }, }, } claudeMessages = append(claudeMessages, claudeMessage) } } - claudeMessage := ClaudeMessage{ + claudeMessage := dto.ClaudeMessage{ Role: message.Role, } if message.Role == "tool" { if len(claudeMessages) > 0 && claudeMessages[len(claudeMessages)-1].Role == "user" { lastMessage := claudeMessages[len(claudeMessages)-1] if content, ok := lastMessage.Content.(string); ok { - lastMessage.Content = []ClaudeMediaMessage{ + lastMessage.Content = []dto.ClaudeMediaMessage{ { Type: "text", - Text: content, + Text: common.GetPointer[string](content), }, } } - lastMessage.Content = append(lastMessage.Content.([]ClaudeMediaMessage), ClaudeMediaMessage{ + lastMessage.Content = append(lastMessage.Content.([]dto.ClaudeMediaMessage), dto.ClaudeMediaMessage{ Type: "tool_result", ToolUseId: message.ToolCallId, - Content: message.StringContent(), + Content: message.Content, }) claudeMessages[len(claudeMessages)-1] = lastMessage continue } else { claudeMessage.Role = "user" - claudeMessage.Content = []ClaudeMediaMessage{ + claudeMessage.Content = []dto.ClaudeMediaMessage{ { Type: "tool_result", ToolUseId: message.ToolCallId, - Content: message.StringContent(), + Content: message.Content, }, } } } else if message.IsStringContent() && message.ToolCalls == nil { claudeMessage.Content = message.StringContent() } else { - claudeMediaMessages := make([]ClaudeMediaMessage, 0) + claudeMediaMessages := make([]dto.ClaudeMediaMessage, 0) for _, mediaMessage := range message.ParseContent() { - claudeMediaMessage := ClaudeMediaMessage{ + claudeMediaMessage := dto.ClaudeMediaMessage{ Type: mediaMessage.Type, } if mediaMessage.Type == "text" { - claudeMediaMessage.Text = mediaMessage.Text + claudeMediaMessage.Text = common.GetPointer[string](mediaMessage.Text) } else { - imageUrl := mediaMessage.ImageUrl.(dto.MessageImageUrl) + imageUrl := mediaMessage.GetImageMedia() claudeMediaMessage.Type = "image" - claudeMediaMessage.Source = &ClaudeMessageSource{ + claudeMediaMessage.Source = &dto.ClaudeMessageSource{ Type: "base64", } // 判断是否是url @@ -272,7 +276,7 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR common.SysError("tool call function arguments is not a map[string]any: " + fmt.Sprintf("%v", toolCall.Function.Arguments)) continue } - claudeMediaMessages = append(claudeMediaMessages, ClaudeMediaMessage{ + claudeMediaMessages = append(claudeMediaMessages, dto.ClaudeMediaMessage{ Type: "tool_use", Id: toolCall.ID, Name: toolCall.Function.Name, @@ -290,13 +294,19 @@ func RequestOpenAI2ClaudeMessage(textRequest dto.GeneralOpenAIRequest) (*ClaudeR return &claudeRequest, nil } -func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) (*dto.ChatCompletionsStreamResponse, *ClaudeUsage) { +func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto.ChatCompletionsStreamResponse { var response dto.ChatCompletionsStreamResponse - var claudeUsage *ClaudeUsage response.Object = "chat.completion.chunk" response.Model = claudeResponse.Model response.Choices = make([]dto.ChatCompletionsStreamResponseChoice, 0) tools := make([]dto.ToolCallResponse, 0) + fcIdx := 0 + if claudeResponse.Index != nil { + fcIdx = *claudeResponse.Index - 1 + if fcIdx < 0 { + fcIdx = 0 + } + } var choice dto.ChatCompletionsStreamResponseChoice if reqMode == RequestModeCompletion { choice.Delta.SetContentString(claudeResponse.Completion) @@ -308,7 +318,7 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) (* if claudeResponse.Type == "message_start" { response.Id = claudeResponse.Message.Id response.Model = claudeResponse.Message.Model - claudeUsage = &claudeResponse.Message.Usage + //claudeUsage = &claudeResponse.Message.Usage choice.Delta.SetContentString("") choice.Delta.Role = "assistant" } else if claudeResponse.Type == "content_block_start" { @@ -316,8 +326,9 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) (* //choice.Delta.SetContentString(claudeResponse.ContentBlock.Text) if claudeResponse.ContentBlock.Type == "tool_use" { tools = append(tools, dto.ToolCallResponse{ - ID: claudeResponse.ContentBlock.Id, - Type: "function", + Index: common.GetPointer(fcIdx), + ID: claudeResponse.ContentBlock.Id, + Type: "function", Function: dto.FunctionResponse{ Name: claudeResponse.ContentBlock.Name, Arguments: "", @@ -325,17 +336,18 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) (* }) } } else { - return nil, nil + return nil } } else if claudeResponse.Type == "content_block_delta" { if claudeResponse.Delta != nil { - choice.Index = claudeResponse.Index - choice.Delta.SetContentString(claudeResponse.Delta.Text) + choice.Delta.Content = claudeResponse.Delta.Text switch claudeResponse.Delta.Type { case "input_json_delta": tools = append(tools, dto.ToolCallResponse{ + Type: "function", + Index: common.GetPointer(fcIdx), Function: dto.FunctionResponse{ - Arguments: claudeResponse.Delta.PartialJson, + Arguments: *claudeResponse.Delta.PartialJson, }, }) case "signature_delta": @@ -352,26 +364,23 @@ func StreamResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) (* if finishReason != "null" { choice.FinishReason = &finishReason } - claudeUsage = &claudeResponse.Usage + //claudeUsage = &claudeResponse.Usage } else if claudeResponse.Type == "message_stop" { - return nil, nil + return nil } else { - return nil, nil + return nil } } - if claudeUsage == nil { - claudeUsage = &ClaudeUsage{} - } if len(tools) > 0 { choice.Delta.Content = nil // compatible with other OpenAI derivative applications, like LobeOpenAICompatibleFactory ... choice.Delta.ToolCalls = tools } response.Choices = append(response.Choices, choice) - return &response, claudeUsage + return &response } -func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.OpenAITextResponse { +func ResponseClaude2OpenAI(reqMode int, claudeResponse *dto.ClaudeResponse) *dto.OpenAITextResponse { choices := make([]dto.OpenAITextResponseChoice, 0) fullTextResponse := dto.OpenAITextResponse{ Id: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), @@ -379,8 +388,10 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope Created: common.GetTimestamp(), } var responseText string + var responseThinking string if len(claudeResponse.Content) > 0 { - responseText = claudeResponse.Content[0].Text + responseText = claudeResponse.Content[0].GetText() + responseThinking = claudeResponse.Content[0].Thinking } tools := make([]dto.ToolCallResponse, 0) thinkingContent := "" @@ -415,7 +426,7 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope // 加密的不管, 只输出明文的推理过程 thinkingContent = message.Thinking case "text": - responseText = message.Text + responseText = message.GetText() } } } @@ -427,6 +438,9 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope FinishReason: stopReasonClaude2OpenAI(claudeResponse.StopReason), } choice.SetStringContent(responseText) + if len(responseThinking) > 0 { + choice.ReasoningContent = responseThinking + } if len(tools) > 0 { choice.Message.SetToolCalls(tools) } @@ -437,126 +451,228 @@ func ResponseClaude2OpenAI(reqMode int, claudeResponse *ClaudeResponse) *dto.Ope return &fullTextResponse } -func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - responseId := fmt.Sprintf("chatcmpl-%s", common.GetUUID()) - var usage *dto.Usage - usage = &dto.Usage{} - responseText := "" - createdTime := common.GetTimestamp() +type ClaudeResponseInfo struct { + ResponseId string + Created int64 + Model string + ResponseText strings.Builder + Usage *dto.Usage +} - helper.StreamScannerHandler(c, resp, info, func(data string) bool { - var claudeResponse ClaudeResponse - err := json.Unmarshal([]byte(data), &claudeResponse) - if err != nil { - common.SysError("error unmarshalling stream response: " + err.Error()) - return true +func FormatClaudeResponseInfo(requestMode int, claudeResponse *dto.ClaudeResponse, oaiResponse *dto.ChatCompletionsStreamResponse, claudeInfo *ClaudeResponseInfo) bool { + if requestMode == RequestModeCompletion { + claudeInfo.ResponseText.WriteString(claudeResponse.Completion) + } else { + if claudeResponse.Type == "message_start" { + // message_start, 获取usage + claudeInfo.ResponseId = claudeResponse.Message.Id + claudeInfo.Model = claudeResponse.Message.Model + claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens + } else if claudeResponse.Type == "content_block_delta" { + if claudeResponse.Delta.Text != nil { + claudeInfo.ResponseText.WriteString(*claudeResponse.Delta.Text) + } + } else if claudeResponse.Type == "message_delta" { + claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens + if claudeResponse.Usage.InputTokens > 0 { + claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens + } + claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeResponse.Usage.OutputTokens + } else if claudeResponse.Type == "content_block_start" { + } else { + return false } + } + if oaiResponse != nil { + oaiResponse.Id = claudeInfo.ResponseId + oaiResponse.Created = claudeInfo.Created + oaiResponse.Model = claudeInfo.Model + } + return true +} - response, claudeUsage := StreamResponseClaude2OpenAI(requestMode, &claudeResponse) - if response == nil { - return true +func HandleStreamResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data string, requestMode int) *dto.OpenAIErrorWithStatusCode { + var claudeResponse dto.ClaudeResponse + err := common.DecodeJsonStr(data, &claudeResponse) + if err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + return service.OpenAIErrorWrapper(err, "stream_response_error", http.StatusInternalServerError) + } + if claudeResponse.Error != nil && claudeResponse.Error.Type != "" { + return &dto.OpenAIErrorWithStatusCode{ + Error: dto.OpenAIError{ + Code: "stream_response_error", + Type: claudeResponse.Error.Type, + Message: claudeResponse.Error.Message, + }, + StatusCode: http.StatusInternalServerError, } + } + if info.RelayFormat == relaycommon.RelayFormatClaude { if requestMode == RequestModeCompletion { - responseText += claudeResponse.Completion - responseId = response.Id + claudeInfo.ResponseText.WriteString(claudeResponse.Completion) } else { if claudeResponse.Type == "message_start" { // message_start, 获取usage - responseId = claudeResponse.Message.Id info.UpstreamModelName = claudeResponse.Message.Model - usage.PromptTokens = claudeUsage.InputTokens + claudeInfo.Usage.PromptTokens = claudeResponse.Message.Usage.InputTokens + claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Message.Usage.CacheReadInputTokens + claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Message.Usage.CacheCreationInputTokens + claudeInfo.Usage.CompletionTokens = claudeResponse.Message.Usage.OutputTokens } else if claudeResponse.Type == "content_block_delta" { - responseText += claudeResponse.Delta.Text + claudeInfo.ResponseText.WriteString(claudeResponse.Delta.GetText()) } else if claudeResponse.Type == "message_delta" { - usage.CompletionTokens = claudeUsage.OutputTokens - usage.TotalTokens = claudeUsage.InputTokens + claudeUsage.OutputTokens - } else if claudeResponse.Type == "content_block_start" { - return true - } else { - return true + if claudeResponse.Usage.InputTokens > 0 { + // 不叠加,只取最新的 + claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens + } + claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens + claudeInfo.Usage.TotalTokens = claudeInfo.Usage.PromptTokens + claudeInfo.Usage.CompletionTokens } } - //response.Id = responseId - response.Id = responseId - response.Created = createdTime - response.Model = info.UpstreamModelName + helper.ClaudeChunkData(c, claudeResponse, data) + } else if info.RelayFormat == relaycommon.RelayFormatOpenAI { + response := StreamResponseClaude2OpenAI(requestMode, &claudeResponse) + + if !FormatClaudeResponseInfo(requestMode, &claudeResponse, response, claudeInfo) { + return nil + } err = helper.ObjectData(c, response) if err != nil { common.LogError(c, "send_stream_response_failed: "+err.Error()) } - return true - }) - - if requestMode == RequestModeCompletion { - usage, _ = service.ResponseText2Usage(responseText, info.UpstreamModelName, info.PromptTokens) - } else { - if usage.PromptTokens == 0 { - usage.PromptTokens = info.PromptTokens - } - if usage.CompletionTokens == 0 { - usage, _ = service.ResponseText2Usage(responseText, info.UpstreamModelName, usage.PromptTokens) - } } - if info.ShouldIncludeUsage { - response := helper.GenerateFinalUsageResponse(responseId, createdTime, info.UpstreamModelName, *usage) - err := helper.ObjectData(c, response) - if err != nil { - common.SysError("send final response failed: " + err.Error()) - } - } - helper.Done(c) - //resp.Body.Close() - return nil, usage + return nil } -func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil +func HandleStreamFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, requestMode int) { + if info.RelayFormat == relaycommon.RelayFormatClaude { + if requestMode == RequestModeCompletion { + claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, info.PromptTokens) + } else { + // 说明流模式建立失败,可能为官方出错 + if claudeInfo.Usage.PromptTokens == 0 { + //usage.PromptTokens = info.PromptTokens + } + if claudeInfo.Usage.CompletionTokens == 0 { + claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens) + } + } + } else if info.RelayFormat == relaycommon.RelayFormatOpenAI { + if requestMode == RequestModeCompletion { + claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, info.PromptTokens) + } else { + if claudeInfo.Usage.PromptTokens == 0 { + //上游出错 + } + if claudeInfo.Usage.CompletionTokens == 0 { + claudeInfo.Usage, _ = service.ResponseText2Usage(claudeInfo.ResponseText.String(), info.UpstreamModelName, claudeInfo.Usage.PromptTokens) + } + } + if info.ShouldIncludeUsage { + response := helper.GenerateFinalUsageResponse(claudeInfo.ResponseId, claudeInfo.Created, info.UpstreamModelName, *claudeInfo.Usage) + err := helper.ObjectData(c, response) + if err != nil { + common.SysError("send final response failed: " + err.Error()) + } + } + helper.Done(c) } - err = resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil +} + +func ClaudeStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, requestMode int) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + claudeInfo := &ClaudeResponseInfo{ + ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), + Created: common.GetTimestamp(), + Model: info.UpstreamModelName, + ResponseText: strings.Builder{}, + Usage: &dto.Usage{}, } - var claudeResponse ClaudeResponse - err = json.Unmarshal(responseBody, &claudeResponse) + var err *dto.OpenAIErrorWithStatusCode + helper.StreamScannerHandler(c, resp, info, func(data string) bool { + err = HandleStreamResponseData(c, info, claudeInfo, data, requestMode) + if err != nil { + return false + } + return true + }) if err != nil { - return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + return err, nil } - if claudeResponse.Error.Type != "" { + + HandleStreamFinalResponse(c, info, claudeInfo, requestMode) + return nil, claudeInfo.Usage +} + +func HandleClaudeResponseData(c *gin.Context, info *relaycommon.RelayInfo, claudeInfo *ClaudeResponseInfo, data []byte, requestMode int) *dto.OpenAIErrorWithStatusCode { + var claudeResponse dto.ClaudeResponse + err := common.DecodeJson(data, &claudeResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "unmarshal_claude_response_failed", http.StatusInternalServerError) + } + if claudeResponse.Error != nil && claudeResponse.Error.Type != "" { return &dto.OpenAIErrorWithStatusCode{ Error: dto.OpenAIError{ Message: claudeResponse.Error.Message, Type: claudeResponse.Error.Type, - Param: "", Code: claudeResponse.Error.Type, }, - StatusCode: resp.StatusCode, - }, nil + StatusCode: http.StatusInternalServerError, + } } - fullTextResponse := ResponseClaude2OpenAI(requestMode, &claudeResponse) - completionTokens, err := service.CountTextToken(claudeResponse.Completion, info.OriginModelName) - if err != nil { - return service.OpenAIErrorWrapper(err, "count_token_text_failed", http.StatusInternalServerError), nil - } - usage := dto.Usage{} if requestMode == RequestModeCompletion { - usage.PromptTokens = info.PromptTokens - usage.CompletionTokens = completionTokens - usage.TotalTokens = info.PromptTokens + completionTokens + completionTokens, err := service.CountTextToken(claudeResponse.Completion, info.OriginModelName) + if err != nil { + return service.OpenAIErrorWrapper(err, "count_token_text_failed", http.StatusInternalServerError) + } + claudeInfo.Usage.PromptTokens = info.PromptTokens + claudeInfo.Usage.CompletionTokens = completionTokens + claudeInfo.Usage.TotalTokens = info.PromptTokens + completionTokens } else { - usage.PromptTokens = claudeResponse.Usage.InputTokens - usage.CompletionTokens = claudeResponse.Usage.OutputTokens - usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens + claudeInfo.Usage.PromptTokens = claudeResponse.Usage.InputTokens + claudeInfo.Usage.CompletionTokens = claudeResponse.Usage.OutputTokens + claudeInfo.Usage.TotalTokens = claudeResponse.Usage.InputTokens + claudeResponse.Usage.OutputTokens + claudeInfo.Usage.PromptTokensDetails.CachedTokens = claudeResponse.Usage.CacheReadInputTokens + claudeInfo.Usage.PromptTokensDetails.CachedCreationTokens = claudeResponse.Usage.CacheCreationInputTokens } - fullTextResponse.Usage = usage - jsonResponse, err := json.Marshal(fullTextResponse) - if err != nil { - return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + var responseData []byte + switch info.RelayFormat { + case relaycommon.RelayFormatOpenAI: + openaiResponse := ResponseClaude2OpenAI(requestMode, &claudeResponse) + openaiResponse.Usage = *claudeInfo.Usage + responseData, err = json.Marshal(openaiResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError) + } + case relaycommon.RelayFormatClaude: + responseData = data } c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, err = c.Writer.Write(jsonResponse) - return nil, &usage + c.Writer.WriteHeader(http.StatusOK) + _, err = c.Writer.Write(responseData) + return nil +} + +func ClaudeHandler(c *gin.Context, resp *http.Response, requestMode int, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + claudeInfo := &ClaudeResponseInfo{ + ResponseId: fmt.Sprintf("chatcmpl-%s", common.GetUUID()), + Created: common.GetTimestamp(), + Model: info.UpstreamModelName, + ResponseText: strings.Builder{}, + Usage: &dto.Usage{}, + } + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + resp.Body.Close() + if common.DebugEnabled { + println("responseBody: ", string(responseBody)) + } + handleErr := HandleClaudeResponseData(c, info, claudeInfo, responseBody, requestMode) + if handleErr != nil { + return handleErr, nil + } + return nil, claudeInfo.Usage } diff --git a/relay/channel/cloudflare/adaptor.go b/relay/channel/cloudflare/adaptor.go index 5c2eadc2..3d5a5a8a 100644 --- a/relay/channel/cloudflare/adaptor.go +++ b/relay/channel/cloudflare/adaptor.go @@ -17,6 +17,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } @@ -37,7 +43,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } diff --git a/relay/channel/cohere/adaptor.go b/relay/channel/cohere/adaptor.go index d552a53b..53a357ad 100644 --- a/relay/channel/cohere/adaptor.go +++ b/relay/channel/cohere/adaptor.go @@ -15,6 +15,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -42,7 +48,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { return requestOpenAI2Cohere(*request), nil } @@ -59,7 +65,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return nil, errors.New("not implemented") } - func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { if info.RelayMode == constant.RelayModeRerank { err, usage = cohereRerankHandler(c, resp, info) diff --git a/relay/channel/cohere/constant.go b/relay/channel/cohere/constant.go index 734620a6..f2d2e559 100644 --- a/relay/channel/cohere/constant.go +++ b/relay/channel/cohere/constant.go @@ -1,6 +1,7 @@ package cohere var ModelList = []string{ + "command-a-03-2025", "command-r", "command-r-plus", "command-r-08-2024", "command-r-plus-08-2024", "c4ai-aya-23-35b", "c4ai-aya-23-8b", diff --git a/relay/channel/cohere/dto.go b/relay/channel/cohere/dto.go index e7452fd4..410540c0 100644 --- a/relay/channel/cohere/dto.go +++ b/relay/channel/cohere/dto.go @@ -40,8 +40,8 @@ type CohereRerankRequest struct { } type CohereRerankResponseResult struct { - Results []dto.RerankResponseDocument `json:"results"` - Meta CohereMeta `json:"meta"` + Results []dto.RerankResponseResult `json:"results"` + Meta CohereMeta `json:"meta"` } type CohereMeta struct { diff --git a/relay/channel/deepseek/adaptor.go b/relay/channel/deepseek/adaptor.go index d779ee65..f6e910e8 100644 --- a/relay/channel/deepseek/adaptor.go +++ b/relay/channel/deepseek/adaptor.go @@ -11,11 +11,18 @@ import ( "one-api/relay/channel/openai" relaycommon "one-api/relay/common" "one-api/relay/constant" + "strings" ) type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -30,9 +37,13 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + fimBaseUrl := info.BaseUrl + if !strings.HasSuffix(info.BaseUrl, "/beta") { + fimBaseUrl += "/beta" + } switch info.RelayMode { case constant.RelayModeCompletions: - return fmt.Sprintf("%s/beta/completions", info.BaseUrl), nil + return fmt.Sprintf("%s/completions", fimBaseUrl), nil default: return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil } @@ -44,7 +55,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -68,7 +79,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/dify/adaptor.go b/relay/channel/dify/adaptor.go index 2626dd7d..dddcb994 100644 --- a/relay/channel/dify/adaptor.go +++ b/relay/channel/dify/adaptor.go @@ -9,7 +9,6 @@ import ( "one-api/dto" "one-api/relay/channel" relaycommon "one-api/relay/common" - "strings" ) const ( @@ -23,6 +22,12 @@ type Adaptor struct { BotType int } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -34,15 +39,16 @@ func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInf } func (a *Adaptor) Init(info *relaycommon.RelayInfo) { - if strings.HasPrefix(info.UpstreamModelName, "agent") { - a.BotType = BotTypeAgent - } else if strings.HasPrefix(info.UpstreamModelName, "workflow") { - a.BotType = BotTypeWorkFlow - } else if strings.HasPrefix(info.UpstreamModelName, "chat") { - a.BotType = BotTypeCompletion - } else { - a.BotType = BotTypeChatFlow - } + //if strings.HasPrefix(info.UpstreamModelName, "agent") { + // a.BotType = BotTypeAgent + //} else if strings.HasPrefix(info.UpstreamModelName, "workflow") { + // a.BotType = BotTypeWorkFlow + //} else if strings.HasPrefix(info.UpstreamModelName, "chat") { + // a.BotType = BotTypeCompletion + //} else { + //} + a.BotType = BotTypeChatFlow + } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { @@ -64,11 +70,11 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } - return requestOpenAI2Dify(*request), nil + return requestOpenAI2Dify(c, info, *request), nil } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { diff --git a/relay/channel/dify/dto.go b/relay/channel/dify/dto.go index bc641a22..7c6f39b6 100644 --- a/relay/channel/dify/dto.go +++ b/relay/channel/dify/dto.go @@ -8,6 +8,14 @@ type DifyChatRequest struct { ResponseMode string `json:"response_mode"` User string `json:"user"` AutoGenerateName bool `json:"auto_generate_name"` + Files []DifyFile `json:"files"` +} + +type DifyFile struct { + Type string `json:"type"` + TransferMode string `json:"transfer_mode"` + URL string `json:"url,omitempty"` + UploadFileId string `json:"upload_file_id,omitempty"` } type DifyMetaData struct { @@ -17,6 +25,8 @@ type DifyMetaData struct { type DifyData struct { WorkflowId string `json:"workflow_id"` NodeId string `json:"node_id"` + NodeType string `json:"node_type"` + Status string `json:"status"` } type DifyChatCompletionResponse struct { diff --git a/relay/channel/dify/relay-dify.go b/relay/channel/dify/relay-dify.go index 3e62d41c..b58fbe53 100644 --- a/relay/channel/dify/relay-dify.go +++ b/relay/channel/dify/relay-dify.go @@ -1,10 +1,12 @@ package dify import ( - "bufio" + "bytes" + "encoding/base64" "encoding/json" - "github.com/gin-gonic/gin" + "fmt" "io" + "mime/multipart" "net/http" "one-api/common" "one-api/constant" @@ -12,35 +14,163 @@ import ( relaycommon "one-api/relay/common" "one-api/relay/helper" "one-api/service" + "os" "strings" + + "github.com/gin-gonic/gin" ) -func requestOpenAI2Dify(request dto.GeneralOpenAIRequest) *DifyChatRequest { - content := "" - for _, message := range request.Messages { - if message.Role == "system" { - content += "SYSTEM: \n" + message.StringContent() + "\n" - } else if message.Role == "assistant" { - content += "ASSISTANT: \n" + message.StringContent() + "\n" - } else { - content += "USER: \n" + message.StringContent() + "\n" +func uploadDifyFile(c *gin.Context, info *relaycommon.RelayInfo, user string, media dto.MediaContent) *DifyFile { + uploadUrl := fmt.Sprintf("%s/v1/files/upload", info.BaseUrl) + switch media.Type { + case dto.ContentTypeImageURL: + // Decode base64 data + imageMedia := media.GetImageMedia() + base64Data := imageMedia.Url + // Remove base64 prefix if exists (e.g., "data:image/jpeg;base64,") + if idx := strings.Index(base64Data, ","); idx != -1 { + base64Data = base64Data[idx+1:] + } + + // Decode base64 string + decodedData, err := base64.StdEncoding.DecodeString(base64Data) + if err != nil { + common.SysError("failed to decode base64: " + err.Error()) + return nil + } + + // Create temporary file + tempFile, err := os.CreateTemp("", "dify-upload-*") + if err != nil { + common.SysError("failed to create temp file: " + err.Error()) + return nil + } + defer tempFile.Close() + defer os.Remove(tempFile.Name()) + + // Write decoded data to temp file + if _, err := tempFile.Write(decodedData); err != nil { + common.SysError("failed to write to temp file: " + err.Error()) + return nil + } + + // Create multipart form + body := &bytes.Buffer{} + writer := multipart.NewWriter(body) + + // Add user field + if err := writer.WriteField("user", user); err != nil { + common.SysError("failed to add user field: " + err.Error()) + return nil + } + + // Create form file with proper mime type + mimeType := imageMedia.MimeType + if mimeType == "" { + mimeType = "image/jpeg" // default mime type + } + + // Create form file + part, err := writer.CreateFormFile("file", fmt.Sprintf("image.%s", strings.TrimPrefix(mimeType, "image/"))) + if err != nil { + common.SysError("failed to create form file: " + err.Error()) + return nil + } + + // Copy file content to form + if _, err = io.Copy(part, bytes.NewReader(decodedData)); err != nil { + common.SysError("failed to copy file content: " + err.Error()) + return nil + } + writer.Close() + + // Create HTTP request + req, err := http.NewRequest("POST", uploadUrl, body) + if err != nil { + common.SysError("failed to create request: " + err.Error()) + return nil + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey)) + + // Send request + client := service.GetImpatientHttpClient() + resp, err := client.Do(req) + if err != nil { + common.SysError("failed to send request: " + err.Error()) + return nil + } + defer resp.Body.Close() + + // Parse response + var result struct { + Id string `json:"id"` + } + if err := json.NewDecoder(resp.Body).Decode(&result); err != nil { + common.SysError("failed to decode response: " + err.Error()) + return nil + } + + return &DifyFile{ + UploadFileId: result.Id, + Type: "image", + TransferMode: "local_file", } } + return nil +} + +func requestOpenAI2Dify(c *gin.Context, info *relaycommon.RelayInfo, request dto.GeneralOpenAIRequest) *DifyChatRequest { + difyReq := DifyChatRequest{ + Inputs: make(map[string]interface{}), + AutoGenerateName: false, + } + + user := request.User + if user == "" { + user = helper.GetResponseID(c) + } + difyReq.User = user + + files := make([]DifyFile, 0) + var content strings.Builder + for _, message := range request.Messages { + if message.Role == "system" { + content.WriteString("SYSTEM: \n" + message.StringContent() + "\n") + } else if message.Role == "assistant" { + content.WriteString("ASSISTANT: \n" + message.StringContent() + "\n") + } else { + parseContent := message.ParseContent() + for _, mediaContent := range parseContent { + switch mediaContent.Type { + case dto.ContentTypeText: + content.WriteString("USER: \n" + mediaContent.Text + "\n") + case dto.ContentTypeImageURL: + media := mediaContent.GetImageMedia() + var file *DifyFile + if media.IsRemoteImage() { + file.Type = media.MimeType + file.TransferMode = "remote_url" + file.URL = media.Url + } else { + file = uploadDifyFile(c, info, difyReq.User, mediaContent) + } + if file != nil { + files = append(files, *file) + } + } + } + } + } + difyReq.Query = content.String() + difyReq.Files = files mode := "blocking" if request.Stream { mode = "streaming" } - user := request.User - if user == "" { - user = "api-user" - } - return &DifyChatRequest{ - Inputs: make(map[string]interface{}), - Query: content, - ResponseMode: mode, - User: user, - AutoGenerateName: false, - } + difyReq.ResponseMode = mode + return &difyReq } func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dto.ChatCompletionsStreamResponse { @@ -50,11 +180,29 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt Model: "dify", } var choice dto.ChatCompletionsStreamResponseChoice - if constant.DifyDebug && difyResponse.Event == "workflow_started" { - choice.Delta.SetContentString("Workflow: " + difyResponse.Data.WorkflowId + "\n") - } else if constant.DifyDebug && difyResponse.Event == "node_started" { - choice.Delta.SetContentString("Node: " + difyResponse.Data.NodeId + "\n") + if strings.HasPrefix(difyResponse.Event, "workflow_") { + if constant.DifyDebug { + text := "Workflow: " + difyResponse.Data.WorkflowId + if difyResponse.Event == "workflow_finished" { + text += " " + difyResponse.Data.Status + } + choice.Delta.SetReasoningContent(text + "\n") + } + } else if strings.HasPrefix(difyResponse.Event, "node_") { + if constant.DifyDebug { + text := "Node: " + difyResponse.Data.NodeType + if difyResponse.Event == "node_finished" { + text += " " + difyResponse.Data.Status + } + choice.Delta.SetReasoningContent(text + "\n") + } } else if difyResponse.Event == "message" || difyResponse.Event == "agent_message" { + if difyResponse.Answer == "
Thinking... \n" { + difyResponse.Answer = "" + } else if difyResponse.Answer == "
" { + difyResponse.Answer = "
" + } + choice.Delta.SetContentString(difyResponse.Answer) } response.Choices = append(response.Choices, choice) @@ -64,47 +212,40 @@ func streamResponseDify2OpenAI(difyResponse DifyChunkChatCompletionResponse) *dt func difyStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { var responseText string usage := &dto.Usage{} - scanner := bufio.NewScanner(resp.Body) - scanner.Split(bufio.ScanLines) - + var nodeToken int helper.SetEventStreamHeaders(c) - - for scanner.Scan() { - data := scanner.Text() - if len(data) < 5 || !strings.HasPrefix(data, "data:") { - continue - } - data = strings.TrimPrefix(data, "data:") + helper.StreamScannerHandler(c, resp, info, func(data string) bool { var difyResponse DifyChunkChatCompletionResponse err := json.Unmarshal([]byte(data), &difyResponse) if err != nil { common.SysError("error unmarshalling stream response: " + err.Error()) - continue + return true } var openaiResponse dto.ChatCompletionsStreamResponse if difyResponse.Event == "message_end" { usage = &difyResponse.MetaData.Usage - break + return false } else if difyResponse.Event == "error" { - break + return false } else { openaiResponse = *streamResponseDify2OpenAI(difyResponse) if len(openaiResponse.Choices) != 0 { responseText += openaiResponse.Choices[0].Delta.GetContentString() + if openaiResponse.Choices[0].Delta.ReasoningContent != nil { + nodeToken += 1 + } } } err = helper.ObjectData(c, openaiResponse) if err != nil { common.SysError(err.Error()) } - } - if err := scanner.Err(); err != nil { - common.SysError("error reading stream: " + err.Error()) - } + return true + }) helper.Done(c) err := resp.Body.Close() if err != nil { - //return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + // return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil common.SysError("close_response_body_failed: " + err.Error()) } if usage.TotalTokens == 0 { @@ -112,6 +253,7 @@ func difyStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re usage.CompletionTokens, _ = service.CountTextToken("gpt-3.5-turbo", responseText) usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens } + usage.CompletionTokens += nodeToken return nil, usage } diff --git a/relay/channel/gemini/adaptor.go b/relay/channel/gemini/adaptor.go index 37c6c9df..feaed8f4 100644 --- a/relay/channel/gemini/adaptor.go +++ b/relay/channel/gemini/adaptor.go @@ -12,7 +12,6 @@ import ( relaycommon "one-api/relay/common" "one-api/service" "one-api/setting/model_setting" - "strings" "github.com/gin-gonic/gin" @@ -21,6 +20,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -64,12 +69,28 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + + if model_setting.GetGeminiSettings().ThinkingAdapterEnabled { + // suffix -thinking and -nothinking + if strings.HasSuffix(info.OriginModelName, "-thinking") { + info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-thinking") + } else if strings.HasSuffix(info.OriginModelName, "-nothinking") { + info.UpstreamModelName = strings.TrimSuffix(info.UpstreamModelName, "-nothinking") + } + } + version := model_setting.GetGeminiVersionSetting(info.UpstreamModelName) if strings.HasPrefix(info.UpstreamModelName, "imagen") { return fmt.Sprintf("%s/%s/models/%s:predict", info.BaseUrl, version, info.UpstreamModelName), nil } + if strings.HasPrefix(info.UpstreamModelName, "text-embedding") || + strings.HasPrefix(info.UpstreamModelName, "embedding") || + strings.HasPrefix(info.UpstreamModelName, "gemini-embedding") { + return fmt.Sprintf("%s/%s/models/%s:embedContent", info.BaseUrl, version, info.UpstreamModelName), nil + } + action := "generateContent" if info.IsStream { action = "streamGenerateContent?alt=sse" @@ -83,15 +104,17 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } - ai, err := CovertGemini2OpenAI(*request) + + geminiRequest, err := CovertGemini2OpenAI(*request, info) if err != nil { return nil, err } - return ai, nil + + return geminiRequest, nil } func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { @@ -99,8 +122,37 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { - //TODO implement me - return nil, errors.New("not implemented") + if request.Input == nil { + return nil, errors.New("input is required") + } + + inputs := request.ParseInput() + if len(inputs) == 0 { + return nil, errors.New("input is empty") + } + + // only process the first input + geminiRequest := GeminiEmbeddingRequest{ + Content: GeminiChatContent{ + Parts: []GeminiPart{ + { + Text: inputs[0], + }, + }, + }, + } + + // set specific parameters for different models + // https://ai.google.dev/api/embeddings?hl=zh-cn#method:-models.embedcontent + switch info.UpstreamModelName { + case "text-embedding-004": + // except embedding-001 supports setting `OutputDimensionality` + if request.Dimensions > 0 { + geminiRequest.OutputDimensionality = request.Dimensions + } + } + + return geminiRequest, nil } func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { @@ -112,11 +164,30 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom return GeminiImageHandler(c, resp, info) } + // check if the model is an embedding model + if strings.HasPrefix(info.UpstreamModelName, "text-embedding") || + strings.HasPrefix(info.UpstreamModelName, "embedding") || + strings.HasPrefix(info.UpstreamModelName, "gemini-embedding") { + return GeminiEmbeddingHandler(c, resp, info) + } + if info.IsStream { err, usage = GeminiChatStreamHandler(c, resp, info) } else { err, usage = GeminiChatHandler(c, resp, info) } + + //if usage.(*dto.Usage).CompletionTokenDetails.ReasoningTokens > 100 { + // // 没有请求-thinking的情况下,产生思考token,则按照思考模型计费 + // if !strings.HasSuffix(info.OriginModelName, "-thinking") && + // !strings.HasSuffix(info.OriginModelName, "-nothinking") { + // thinkingModelName := info.OriginModelName + "-thinking" + // if operation_setting.SelfUseModeEnabled || helper.ContainPriceOrRatio(thinkingModelName) { + // info.OriginModelName = thinkingModelName + // } + // } + //} + return } diff --git a/relay/channel/gemini/constant.go b/relay/channel/gemini/constant.go index 1f402cbc..2c972e37 100644 --- a/relay/channel/gemini/constant.go +++ b/relay/channel/gemini/constant.go @@ -16,8 +16,14 @@ var ModelList = []string{ "gemini-2.0-pro-exp", // thinking exp "gemini-2.0-flash-thinking-exp", + "gemini-2.5-pro-exp-03-25", + "gemini-2.5-pro-preview-03-25", // imagen models "imagen-3.0-generate-002", + // embedding models + "gemini-embedding-exp-03-07", + "text-embedding-004", + "embedding-001", } var SafetySettingList = []string{ diff --git a/relay/channel/gemini/dto.go b/relay/channel/gemini/dto.go index bbcb1248..5d5c1287 100644 --- a/relay/channel/gemini/dto.go +++ b/relay/channel/gemini/dto.go @@ -8,6 +8,15 @@ type GeminiChatRequest struct { SystemInstructions *GeminiChatContent `json:"system_instruction,omitempty"` } +type GeminiThinkingConfig struct { + IncludeThoughts bool `json:"includeThoughts,omitempty"` + ThinkingBudget *int `json:"thinkingBudget,omitempty"` +} + +func (c *GeminiThinkingConfig) SetThinkingBudget(budget int) { + c.ThinkingBudget = &budget +} + type GeminiInlineData struct { MimeType string `json:"mimeType"` Data string `json:"data"` @@ -71,15 +80,17 @@ type GeminiChatTool struct { } type GeminiChatGenerationConfig struct { - Temperature *float64 `json:"temperature,omitempty"` - TopP float64 `json:"topP,omitempty"` - TopK float64 `json:"topK,omitempty"` - MaxOutputTokens uint `json:"maxOutputTokens,omitempty"` - CandidateCount int `json:"candidateCount,omitempty"` - StopSequences []string `json:"stopSequences,omitempty"` - ResponseMimeType string `json:"responseMimeType,omitempty"` - ResponseSchema any `json:"responseSchema,omitempty"` - Seed int64 `json:"seed,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"topP,omitempty"` + TopK float64 `json:"topK,omitempty"` + MaxOutputTokens uint `json:"maxOutputTokens,omitempty"` + CandidateCount int `json:"candidateCount,omitempty"` + StopSequences []string `json:"stopSequences,omitempty"` + ResponseMimeType string `json:"responseMimeType,omitempty"` + ResponseSchema any `json:"responseSchema,omitempty"` + Seed int64 `json:"seed,omitempty"` + ResponseModalities []string `json:"responseModalities,omitempty"` + ThinkingConfig *GeminiThinkingConfig `json:"thinkingConfig,omitempty"` } type GeminiChatCandidate struct { @@ -108,6 +119,7 @@ type GeminiUsageMetadata struct { PromptTokenCount int `json:"promptTokenCount"` CandidatesTokenCount int `json:"candidatesTokenCount"` TotalTokenCount int `json:"totalTokenCount"` + ThoughtsTokenCount int `json:"thoughtsTokenCount"` } // Imagen related structs @@ -136,3 +148,19 @@ type GeminiImagePrediction struct { RaiFilteredReason string `json:"raiFilteredReason,omitempty"` SafetyAttributes any `json:"safetyAttributes,omitempty"` } + +// Embedding related structs +type GeminiEmbeddingRequest struct { + Content GeminiChatContent `json:"content"` + TaskType string `json:"taskType,omitempty"` + Title string `json:"title,omitempty"` + OutputDimensionality int `json:"outputDimensionality,omitempty"` +} + +type GeminiEmbeddingResponse struct { + Embedding ContentEmbedding `json:"embedding"` +} + +type ContentEmbedding struct { + Values []float64 `json:"values"` +} diff --git a/relay/channel/gemini/relay-gemini.go b/relay/channel/gemini/relay-gemini.go index c1ce8219..c4f4af31 100644 --- a/relay/channel/gemini/relay-gemini.go +++ b/relay/channel/gemini/relay-gemini.go @@ -19,11 +19,10 @@ import ( ) // Setting safety to the lowest possible values since Gemini is already powerless enough -func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatRequest, error) { +func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest, info *relaycommon.RelayInfo) (*GeminiChatRequest, error) { geminiRequest := GeminiChatRequest{ Contents: make([]GeminiChatContent, 0, len(textRequest.Messages)), - //SafetySettings: []GeminiChatSafetySettings{}, GenerationConfig: GeminiChatGenerationConfig{ Temperature: textRequest.Temperature, TopP: textRequest.TopP, @@ -32,6 +31,30 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque }, } + if model_setting.IsGeminiModelSupportImagine(info.UpstreamModelName) { + geminiRequest.GenerationConfig.ResponseModalities = []string{ + "TEXT", + "IMAGE", + } + } + + if model_setting.GetGeminiSettings().ThinkingAdapterEnabled { + if strings.HasSuffix(info.OriginModelName, "-thinking") { + budgetTokens := model_setting.GetGeminiSettings().ThinkingAdapterBudgetTokensPercentage * float64(geminiRequest.GenerationConfig.MaxOutputTokens) + if budgetTokens == 0 || budgetTokens > 24576 { + budgetTokens = 24576 + } + geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{ + ThinkingBudget: common.GetPointer(int(budgetTokens)), + IncludeThoughts: true, + } + } else if strings.HasSuffix(info.OriginModelName, "-nothinking") { + geminiRequest.GenerationConfig.ThinkingConfig = &GeminiThinkingConfig{ + ThinkingBudget: common.GetPointer(0), + } + } + } + safetySettings := make([]GeminiChatSafetySettings, 0, len(SafetySettingList)) for _, category := range SafetySettingList { safetySettings = append(safetySettings, GeminiChatSafetySettings{ @@ -56,6 +79,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque continue } if tool.Function.Parameters != nil { + params, ok := tool.Function.Parameters.(map[string]interface{}) if ok { if props, hasProps := params["properties"].(map[string]interface{}); hasProps { @@ -65,6 +89,9 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque } } } + // Clean the parameters before appending + cleanedParams := cleanFunctionParameters(tool.Function.Parameters) + tool.Function.Parameters = cleanedParams functions = append(functions, tool.Function) } if codeExecution { @@ -86,11 +113,11 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque // json_data, _ := json.Marshal(geminiRequest.Tools) // common.SysLog("tools_json: " + string(json_data)) } else if textRequest.Functions != nil { - geminiRequest.Tools = []GeminiChatTool{ - { - FunctionDeclarations: textRequest.Functions, - }, - } + //geminiRequest.Tools = []GeminiChatTool{ + // { + // FunctionDeclarations: textRequest.Functions, + // }, + //} } if textRequest.ResponseFormat != nil && (textRequest.ResponseFormat.Type == "json_schema" || textRequest.ResponseFormat.Type == "json_object") { @@ -180,9 +207,9 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque return nil, fmt.Errorf("too many images in the message, max allowed is %d", constant.GeminiVisionMaxImageNum) } // 判断是否是url - if strings.HasPrefix(part.ImageUrl.(dto.MessageImageUrl).Url, "http") { + if strings.HasPrefix(part.GetImageMedia().Url, "http") { // 是url,获取图片的类型和base64编码的数据 - fileData, err := service.GetFileBase64FromUrl(part.ImageUrl.(dto.MessageImageUrl).Url) + fileData, err := service.GetFileBase64FromUrl(part.GetImageMedia().Url) if err != nil { return nil, fmt.Errorf("get file base64 from url failed: %s", err.Error()) } @@ -193,7 +220,7 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque }, }) } else { - format, base64String, err := service.DecodeBase64FileData(part.ImageUrl.(dto.MessageImageUrl).Url) + format, base64String, err := service.DecodeBase64FileData(part.GetImageMedia().Url) if err != nil { return nil, fmt.Errorf("decode base64 image data failed: %s", err.Error()) } @@ -204,6 +231,34 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque }, }) } + } else if part.Type == dto.ContentTypeFile { + if part.GetFile().FileId != "" { + return nil, fmt.Errorf("only base64 file is supported in gemini") + } + format, base64String, err := service.DecodeBase64FileData(part.GetFile().FileData) + if err != nil { + return nil, fmt.Errorf("decode base64 file data failed: %s", err.Error()) + } + parts = append(parts, GeminiPart{ + InlineData: &GeminiInlineData{ + MimeType: format, + Data: base64String, + }, + }) + } else if part.Type == dto.ContentTypeInputAudio { + if part.GetInputAudio().Data == "" { + return nil, fmt.Errorf("only base64 audio is supported in gemini") + } + format, base64String, err := service.DecodeBase64FileData(part.GetInputAudio().Data) + if err != nil { + return nil, fmt.Errorf("decode base64 audio data failed: %s", err.Error()) + } + parts = append(parts, GeminiPart{ + InlineData: &GeminiInlineData{ + MimeType: format, + Data: base64String, + }, + }) } } @@ -229,6 +284,102 @@ func CovertGemini2OpenAI(textRequest dto.GeneralOpenAIRequest) (*GeminiChatReque return &geminiRequest, nil } +// cleanFunctionParameters recursively removes unsupported fields from Gemini function parameters. +func cleanFunctionParameters(params interface{}) interface{} { + if params == nil { + return nil + } + + paramMap, ok := params.(map[string]interface{}) + if !ok { + // Not a map, return as is (e.g., could be an array or primitive) + return params + } + + // Create a copy to avoid modifying the original + cleanedMap := make(map[string]interface{}) + for k, v := range paramMap { + cleanedMap[k] = v + } + + // Remove unsupported root-level fields + delete(cleanedMap, "default") + delete(cleanedMap, "exclusiveMaximum") + delete(cleanedMap, "exclusiveMinimum") + delete(cleanedMap, "$schema") + delete(cleanedMap, "additionalProperties") + + // Clean properties + if props, ok := cleanedMap["properties"].(map[string]interface{}); ok && props != nil { + cleanedProps := make(map[string]interface{}) + for propName, propValue := range props { + propMap, ok := propValue.(map[string]interface{}) + if !ok { + cleanedProps[propName] = propValue // Keep non-map properties + continue + } + + // Create a copy of the property map + cleanedPropMap := make(map[string]interface{}) + for k, v := range propMap { + cleanedPropMap[k] = v + } + + // Remove unsupported fields + delete(cleanedPropMap, "default") + delete(cleanedPropMap, "exclusiveMaximum") + delete(cleanedPropMap, "exclusiveMinimum") + delete(cleanedPropMap, "$schema") + delete(cleanedPropMap, "additionalProperties") + + // Check and clean 'format' for string types + if propType, typeExists := cleanedPropMap["type"].(string); typeExists && propType == "string" { + if formatValue, formatExists := cleanedPropMap["format"].(string); formatExists { + if formatValue != "enum" && formatValue != "date-time" { + delete(cleanedPropMap, "format") + } + } + } + + // Recursively clean nested properties within this property if it's an object/array + // Check the type before recursing + if propType, typeExists := cleanedPropMap["type"].(string); typeExists && (propType == "object" || propType == "array") { + cleanedProps[propName] = cleanFunctionParameters(cleanedPropMap) + } else { + cleanedProps[propName] = cleanedPropMap // Assign the cleaned map back if not recursing + } + + } + cleanedMap["properties"] = cleanedProps + } + + // Recursively clean items in arrays if needed (e.g., type: array, items: { ... }) + if items, ok := cleanedMap["items"].(map[string]interface{}); ok && items != nil { + cleanedMap["items"] = cleanFunctionParameters(items) + } + // Also handle items if it's an array of schemas + if itemsArray, ok := cleanedMap["items"].([]interface{}); ok { + cleanedItemsArray := make([]interface{}, len(itemsArray)) + for i, item := range itemsArray { + cleanedItemsArray[i] = cleanFunctionParameters(item) + } + cleanedMap["items"] = cleanedItemsArray + } + + // Recursively clean other schema composition keywords if necessary + for _, field := range []string{"allOf", "anyOf", "oneOf"} { + if nested, ok := cleanedMap[field].([]interface{}); ok { + cleanedNested := make([]interface{}, len(nested)) + for i, item := range nested { + cleanedNested[i] = cleanFunctionParameters(item) + } + cleanedMap[field] = cleanedNested + } + } + + return cleanedMap +} + func removeAdditionalPropertiesWithDepth(schema interface{}, depth int) interface{} { if depth >= 5 { return schema @@ -427,9 +578,10 @@ func responseGeminiChat2OpenAI(response *GeminiChatResponse) *dto.OpenAITextResp return &fullTextResponse } -func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool) { +func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.ChatCompletionsStreamResponse, bool, bool) { choices := make([]dto.ChatCompletionsStreamResponseChoice, 0, len(geminiResponse.Candidates)) isStop := false + hasImage := false for _, candidate := range geminiResponse.Candidates { if candidate.FinishReason != nil && *candidate.FinishReason == "STOP" { isStop = true @@ -455,7 +607,13 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C } } for _, part := range candidate.Content.Parts { - if part.FunctionCall != nil { + if part.InlineData != nil { + if strings.HasPrefix(part.InlineData.MimeType, "image") { + imgText := "![image](data:" + part.InlineData.MimeType + ";base64," + part.InlineData.Data + ")" + texts = append(texts, imgText) + hasImage = true + } + } else if part.FunctionCall != nil { isTools = true if call := getResponseToolCall(&part); call != nil { call.SetIndex(len(choice.Delta.ToolCalls)) @@ -483,7 +641,7 @@ func streamResponseGeminiChat2OpenAI(geminiResponse *GeminiChatResponse) (*dto.C var response dto.ChatCompletionsStreamResponse response.Object = "chat.completion.chunk" response.Choices = choices - return &response, isStop + return &response, isStop, hasImage } func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { @@ -491,23 +649,27 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom id := fmt.Sprintf("chatcmpl-%s", common.GetUUID()) createAt := common.GetTimestamp() var usage = &dto.Usage{} + var imageCount int helper.StreamScannerHandler(c, resp, info, func(data string) bool { var geminiResponse GeminiChatResponse - err := json.Unmarshal([]byte(data), &geminiResponse) + err := common.DecodeJsonStr(data, &geminiResponse) if err != nil { common.LogError(c, "error unmarshalling stream response: "+err.Error()) return false } - response, isStop := streamResponseGeminiChat2OpenAI(&geminiResponse) + response, isStop, hasImage := streamResponseGeminiChat2OpenAI(&geminiResponse) + if hasImage { + imageCount++ + } response.Id = id response.Created = createAt response.Model = info.UpstreamModelName - // responseText += response.Choices[0].Delta.GetContentString() if geminiResponse.UsageMetadata.TotalTokenCount != 0 { usage.PromptTokens = geminiResponse.UsageMetadata.PromptTokenCount usage.CompletionTokens = geminiResponse.UsageMetadata.CandidatesTokenCount + usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount } err = helper.ObjectData(c, response) if err != nil { @@ -522,9 +684,15 @@ func GeminiChatStreamHandler(c *gin.Context, resp *http.Response, info *relaycom var response *dto.ChatCompletionsStreamResponse + if imageCount != 0 { + if usage.CompletionTokens == 0 { + usage.CompletionTokens = imageCount * 258 + } + } + usage.TotalTokens = usage.PromptTokens + usage.CompletionTokens usage.PromptTokensDetails.TextTokens = usage.PromptTokens - usage.CompletionTokenDetails.TextTokens = usage.CompletionTokens + //usage.CompletionTokenDetails.TextTokens = usage.CompletionTokens if info.ShouldIncludeUsage { response = helper.GenerateFinalUsageResponse(id, createAt, info.UpstreamModelName, *usage) @@ -570,6 +738,9 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re CompletionTokens: geminiResponse.UsageMetadata.CandidatesTokenCount, TotalTokens: geminiResponse.UsageMetadata.TotalTokenCount, } + + usage.CompletionTokenDetails.ReasoningTokens = geminiResponse.UsageMetadata.ThoughtsTokenCount + fullTextResponse.Usage = usage jsonResponse, err := json.Marshal(fullTextResponse) if err != nil { @@ -580,3 +751,52 @@ func GeminiChatHandler(c *gin.Context, resp *http.Response, info *relaycommon.Re _, err = c.Writer.Write(jsonResponse) return nil, &usage } + +func GeminiEmbeddingHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { + responseBody, readErr := io.ReadAll(resp.Body) + if readErr != nil { + return nil, service.OpenAIErrorWrapper(readErr, "read_response_body_failed", http.StatusInternalServerError) + } + _ = resp.Body.Close() + + var geminiResponse GeminiEmbeddingResponse + if jsonErr := json.Unmarshal(responseBody, &geminiResponse); jsonErr != nil { + return nil, service.OpenAIErrorWrapper(jsonErr, "unmarshal_response_body_failed", http.StatusInternalServerError) + } + + // convert to openai format response + openAIResponse := dto.OpenAIEmbeddingResponse{ + Object: "list", + Data: []dto.OpenAIEmbeddingResponseItem{ + { + Object: "embedding", + Embedding: geminiResponse.Embedding.Values, + Index: 0, + }, + }, + Model: info.UpstreamModelName, + } + + // calculate usage + // https://ai.google.dev/gemini-api/docs/pricing?hl=zh-cn#text-embedding-004 + // Google has not yet clarified how embedding models will be billed + // refer to openai billing method to use input tokens billing + // https://platform.openai.com/docs/guides/embeddings#what-are-embeddings + usage = &dto.Usage{ + PromptTokens: info.PromptTokens, + CompletionTokens: 0, + TotalTokens: info.PromptTokens, + } + openAIResponse.Usage = *usage.(*dto.Usage) + + jsonResponse, jsonErr := json.Marshal(openAIResponse) + if jsonErr != nil { + return nil, service.OpenAIErrorWrapper(jsonErr, "marshal_response_failed", http.StatusInternalServerError) + } + + c.Writer.Header().Set("Content-Type", "application/json") + c.Writer.WriteHeader(resp.StatusCode) + _, _ = c.Writer.Write(jsonResponse) + + return usage, nil +} diff --git a/relay/channel/jina/adaptor.go b/relay/channel/jina/adaptor.go index 77076bd4..3faac243 100644 --- a/relay/channel/jina/adaptor.go +++ b/relay/channel/jina/adaptor.go @@ -8,13 +8,21 @@ import ( "net/http" "one-api/dto" "one-api/relay/channel" + "one-api/relay/channel/openai" relaycommon "one-api/relay/common" + "one-api/relay/common_handler" "one-api/relay/constant" ) type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -43,7 +51,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { return request, nil } @@ -61,9 +69,9 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { if info.RelayMode == constant.RelayModeRerank { - err, usage = JinaRerankHandler(c, resp) + err, usage = common_handler.RerankHandler(c, info, resp) } else if info.RelayMode == constant.RelayModeEmbeddings { - err, usage = jinaEmbeddingHandler(c, resp) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/jina/relay-jina.go b/relay/channel/jina/relay-jina.go index aee7b131..d83b5854 100644 --- a/relay/channel/jina/relay-jina.go +++ b/relay/channel/jina/relay-jina.go @@ -1,60 +1 @@ package jina - -import ( - "encoding/json" - "github.com/gin-gonic/gin" - "io" - "net/http" - "one-api/dto" - "one-api/service" -) - -func JinaRerankHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil - } - err = resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - var jinaResp dto.RerankResponse - err = json.Unmarshal(responseBody, &jinaResp) - if err != nil { - return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - jsonResponse, err := json.Marshal(jinaResp) - if err != nil { - return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, err = c.Writer.Write(jsonResponse) - return nil, &jinaResp.Usage -} - -func jinaEmbeddingHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil - } - err = resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - var jinaResp dto.OpenAIEmbeddingResponse - err = json.Unmarshal(responseBody, &jinaResp) - if err != nil { - return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - - jsonResponse, err := json.Marshal(jinaResp) - if err != nil { - return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil - } - c.Writer.Header().Set("Content-Type", "application/json") - c.Writer.WriteHeader(resp.StatusCode) - _, err = c.Writer.Write(jsonResponse) - return nil, &jinaResp.Usage -} diff --git a/relay/channel/mistral/adaptor.go b/relay/channel/mistral/adaptor.go index fcea169a..82c82496 100644 --- a/relay/channel/mistral/adaptor.go +++ b/relay/channel/mistral/adaptor.go @@ -14,6 +14,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -37,7 +43,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -61,7 +67,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/mistral/text.go b/relay/channel/mistral/text.go index 8987b8f0..75272e34 100644 --- a/relay/channel/mistral/text.go +++ b/relay/channel/mistral/text.go @@ -10,7 +10,7 @@ func requestOpenAI2Mistral(request *dto.GeneralOpenAIRequest) *dto.GeneralOpenAI mediaMessages := message.ParseContent() for j, mediaMessage := range mediaMessages { if mediaMessage.Type == dto.ContentTypeImageURL { - imageUrl := mediaMessage.ImageUrl.(dto.MessageImageUrl) + imageUrl := mediaMessage.GetImageMedia() mediaMessage.ImageUrl = imageUrl.Url mediaMessages[j] = mediaMessage } diff --git a/relay/channel/mokaai/adaptor.go b/relay/channel/mokaai/adaptor.go index 9670ec94..304351fd 100644 --- a/relay/channel/mokaai/adaptor.go +++ b/relay/channel/mokaai/adaptor.go @@ -16,6 +16,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -51,7 +57,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -73,13 +79,13 @@ func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, request } func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { - + switch info.RelayMode { case constant.RelayModeEmbeddings: err, usage = mokaEmbeddingHandler(c, resp) default: // err, usage = mokaHandler(c, resp) - + } return } diff --git a/relay/channel/ollama/adaptor.go b/relay/channel/ollama/adaptor.go index 7e1c6237..39e408ab 100644 --- a/relay/channel/ollama/adaptor.go +++ b/relay/channel/ollama/adaptor.go @@ -15,6 +15,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -43,7 +49,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -69,7 +75,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.RelayMode == relayconstant.RelayModeEmbeddings { err, usage = ollamaEmbeddingHandler(c, resp, info.PromptTokens, info.UpstreamModelName, info.RelayMode) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } } return diff --git a/relay/channel/ollama/relay-ollama.go b/relay/channel/ollama/relay-ollama.go index 89e9c214..89a04646 100644 --- a/relay/channel/ollama/relay-ollama.go +++ b/relay/channel/ollama/relay-ollama.go @@ -19,7 +19,7 @@ func requestOpenAI2Ollama(request dto.GeneralOpenAIRequest) (*OllamaRequest, err mediaMessages := message.ParseContent() for j, mediaMessage := range mediaMessages { if mediaMessage.Type == dto.ContentTypeImageURL { - imageUrl := mediaMessage.ImageUrl.(dto.MessageImageUrl) + imageUrl := mediaMessage.GetImageMedia() // check if not base64 if strings.HasPrefix(imageUrl.Url, "http") { fileData, err := service.GetFileBase64FromUrl(imageUrl.Url) diff --git a/relay/channel/openai/adaptor.go b/relay/channel/openai/adaptor.go index 6dbbb17e..ef11b4fe 100644 --- a/relay/channel/openai/adaptor.go +++ b/relay/channel/openai/adaptor.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "github.com/gin-gonic/gin" "io" "mime/multipart" "net/http" @@ -14,13 +13,18 @@ import ( "one-api/dto" "one-api/relay/channel" "one-api/relay/channel/ai360" - "one-api/relay/channel/jina" "one-api/relay/channel/lingyiwanwu" "one-api/relay/channel/minimax" "one-api/relay/channel/moonshot" + "one-api/relay/channel/openrouter" + "one-api/relay/channel/xinference" relaycommon "one-api/relay/common" + "one-api/relay/common_handler" "one-api/relay/constant" + "one-api/service" "strings" + + "github.com/gin-gonic/gin" ) type Adaptor struct { @@ -28,11 +32,39 @@ type Adaptor struct { ResponseFormat string } +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { + if !strings.Contains(request.Model, "claude") { + return nil, fmt.Errorf("you are using openai channel type with path /v1/messages, only claude model supported convert, but got %s", request.Model) + } + aiRequest, err := service.ClaudeToOpenAIRequest(*request, info) + if err != nil { + return nil, err + } + if info.SupportStreamOptions { + aiRequest.StreamOptions = &dto.StreamOptions{ + IncludeUsage: true, + } + } + return a.ConvertOpenAIRequest(c, info, aiRequest) +} + func (a *Adaptor) Init(info *relaycommon.RelayInfo) { a.ChannelType = info.ChannelType + + // initialize ThinkingContentInfo when thinking_to_content is enabled + if think2Content, ok := info.ChannelSetting[constant2.ChannelSettingThinkingToContent].(bool); ok && think2Content { + info.ThinkingContentInfo = relaycommon.ThinkingContentInfo{ + IsFirstThinkingContent: true, + SendLastThinkingContent: false, + HasSentThinkingContent: false, + } + } } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + if info.RelayFormat == relaycommon.RelayFormatClaude { + return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil + } if info.RelayMode == constant.RelayModeRealtime { if strings.HasPrefix(info.BaseUrl, "https://") { baseUrl := strings.TrimPrefix(info.BaseUrl, "https://") @@ -101,28 +133,26 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, header *http.Header, info * } else { header.Set("Authorization", "Bearer "+info.ApiKey) } - //if info.ChannelType == common.ChannelTypeOpenRouter { - // req.Header.Set("HTTP-Referer", "https://github.com/songquanpeng/one-api") - // req.Header.Set("X-Title", "One API") - //} + if info.ChannelType == common.ChannelTypeOpenRouter { + header.Set("HTTP-Referer", "https://github.com/Calcium-Ion/new-api") + header.Set("X-Title", "New API") + } return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } if info.ChannelType != common.ChannelTypeOpenAI && info.ChannelType != common.ChannelTypeAzure { request.StreamOptions = nil } - if strings.HasPrefix(request.Model, "o1") || strings.HasPrefix(request.Model, "o3") { + if strings.HasPrefix(request.Model, "o") { if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 { request.MaxCompletionTokens = request.MaxTokens request.MaxTokens = 0 } - if strings.HasPrefix(request.Model, "o3") || strings.HasPrefix(request.Model, "o1") { - request.Temperature = nil - } + request.Temperature = nil if strings.HasSuffix(request.Model, "-high") { request.ReasoningEffort = "high" request.Model = strings.TrimSuffix(request.Model, "-high") @@ -135,11 +165,13 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re } info.ReasoningEffort = request.ReasoningEffort info.UpstreamModelName = request.Model - } - if request.Model == "o1" || request.Model == "o1-2024-12-17" || strings.HasPrefix(request.Model, "o3") { - //修改第一个Message的内容,将system改为developer - if len(request.Messages) > 0 && request.Messages[0].Role == "system" { - request.Messages[0].Role = "developer" + + // o系列模型developer适配(o1-mini除外) + if !strings.HasPrefix(request.Model, "o1-mini") { + //修改第一个Message的内容,将system改为developer + if len(request.Messages) > 0 && request.Messages[0].Role == "system" { + request.Messages[0].Role = "developer" + } } } @@ -230,12 +262,12 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom case constant.RelayModeImagesGenerations: err, usage = OpenaiTTSHandler(c, resp, info) case constant.RelayModeRerank: - err, usage = jina.JinaRerankHandler(c, resp) + err, usage = common_handler.RerankHandler(c, info, resp) default: if info.IsStream { err, usage = OaiStreamHandler(c, resp, info) } else { - err, usage = OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = OpenaiHandler(c, resp, info) } } return @@ -251,6 +283,10 @@ func (a *Adaptor) GetModelList() []string { return lingyiwanwu.ModelList case common.ChannelTypeMiniMax: return minimax.ModelList + case common.ChannelTypeXinference: + return xinference.ModelList + case common.ChannelTypeOpenRouter: + return openrouter.ModelList default: return ModelList } @@ -266,6 +302,10 @@ func (a *Adaptor) GetChannelName() string { return lingyiwanwu.ChannelName case common.ChannelTypeMiniMax: return minimax.ChannelName + case common.ChannelTypeXinference: + return xinference.ChannelName + case common.ChannelTypeOpenRouter: + return openrouter.ChannelName default: return ChannelName } diff --git a/relay/channel/openai/helper.go b/relay/channel/openai/helper.go new file mode 100644 index 00000000..e7ba2e7b --- /dev/null +++ b/relay/channel/openai/helper.go @@ -0,0 +1,189 @@ +package openai + +import ( + "encoding/json" + "one-api/common" + "one-api/dto" + relaycommon "one-api/relay/common" + relayconstant "one-api/relay/constant" + "one-api/relay/helper" + "one-api/service" + "strings" + + "github.com/gin-gonic/gin" +) + +// 辅助函数 +func handleStreamFormat(c *gin.Context, info *relaycommon.RelayInfo, data string, forceFormat bool, thinkToContent bool) error { + info.SendResponseCount++ + switch info.RelayFormat { + case relaycommon.RelayFormatOpenAI: + return sendStreamData(c, info, data, forceFormat, thinkToContent) + case relaycommon.RelayFormatClaude: + return handleClaudeFormat(c, data, info) + } + return nil +} + +func handleClaudeFormat(c *gin.Context, data string, info *relaycommon.RelayInfo) error { + var streamResponse dto.ChatCompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(data), &streamResponse); err != nil { + return err + } + + if streamResponse.Usage != nil { + info.ClaudeConvertInfo.Usage = streamResponse.Usage + } + claudeResponses := service.StreamResponseOpenAI2Claude(&streamResponse, info) + for _, resp := range claudeResponses { + helper.ClaudeData(c, *resp) + } + return nil +} + +func ProcessStreamResponse(streamResponse dto.ChatCompletionsStreamResponse, responseTextBuilder *strings.Builder, toolCount *int) error { + for _, choice := range streamResponse.Choices { + responseTextBuilder.WriteString(choice.Delta.GetContentString()) + responseTextBuilder.WriteString(choice.Delta.GetReasoningContent()) + if choice.Delta.ToolCalls != nil { + if len(choice.Delta.ToolCalls) > *toolCount { + *toolCount = len(choice.Delta.ToolCalls) + } + for _, tool := range choice.Delta.ToolCalls { + responseTextBuilder.WriteString(tool.Function.Name) + responseTextBuilder.WriteString(tool.Function.Arguments) + } + } + } + return nil +} + +func processTokens(relayMode int, streamItems []string, responseTextBuilder *strings.Builder, toolCount *int) error { + streamResp := "[" + strings.Join(streamItems, ",") + "]" + + switch relayMode { + case relayconstant.RelayModeChatCompletions: + return processChatCompletions(streamResp, streamItems, responseTextBuilder, toolCount) + case relayconstant.RelayModeCompletions: + return processCompletions(streamResp, streamItems, responseTextBuilder) + } + return nil +} + +func processChatCompletions(streamResp string, streamItems []string, responseTextBuilder *strings.Builder, toolCount *int) error { + var streamResponses []dto.ChatCompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(streamResp), &streamResponses); err != nil { + // 一次性解析失败,逐个解析 + common.SysError("error unmarshalling stream response: " + err.Error()) + for _, item := range streamItems { + var streamResponse dto.ChatCompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(item), &streamResponse); err != nil { + return err + } + if err := ProcessStreamResponse(streamResponse, responseTextBuilder, toolCount); err != nil { + common.SysError("error processing stream response: " + err.Error()) + } + } + return nil + } + + // 批量处理所有响应 + for _, streamResponse := range streamResponses { + for _, choice := range streamResponse.Choices { + responseTextBuilder.WriteString(choice.Delta.GetContentString()) + responseTextBuilder.WriteString(choice.Delta.GetReasoningContent()) + if choice.Delta.ToolCalls != nil { + if len(choice.Delta.ToolCalls) > *toolCount { + *toolCount = len(choice.Delta.ToolCalls) + } + for _, tool := range choice.Delta.ToolCalls { + responseTextBuilder.WriteString(tool.Function.Name) + responseTextBuilder.WriteString(tool.Function.Arguments) + } + } + } + } + return nil +} + +func processCompletions(streamResp string, streamItems []string, responseTextBuilder *strings.Builder) error { + var streamResponses []dto.CompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(streamResp), &streamResponses); err != nil { + // 一次性解析失败,逐个解析 + common.SysError("error unmarshalling stream response: " + err.Error()) + for _, item := range streamItems { + var streamResponse dto.CompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(item), &streamResponse); err != nil { + continue + } + for _, choice := range streamResponse.Choices { + responseTextBuilder.WriteString(choice.Text) + } + } + return nil + } + + // 批量处理所有响应 + for _, streamResponse := range streamResponses { + for _, choice := range streamResponse.Choices { + responseTextBuilder.WriteString(choice.Text) + } + } + return nil +} + +func handleLastResponse(lastStreamData string, responseId *string, createAt *int64, + systemFingerprint *string, model *string, usage **dto.Usage, + containStreamUsage *bool, info *relaycommon.RelayInfo, + shouldSendLastResp *bool) error { + + var lastStreamResponse dto.ChatCompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(lastStreamData), &lastStreamResponse); err != nil { + return err + } + + *responseId = lastStreamResponse.Id + *createAt = lastStreamResponse.Created + *systemFingerprint = lastStreamResponse.GetSystemFingerprint() + *model = lastStreamResponse.Model + + if service.ValidUsage(lastStreamResponse.Usage) { + *containStreamUsage = true + *usage = lastStreamResponse.Usage + if !info.ShouldIncludeUsage { + *shouldSendLastResp = false + } + } + + return nil +} + +func handleFinalResponse(c *gin.Context, info *relaycommon.RelayInfo, lastStreamData string, + responseId string, createAt int64, model string, systemFingerprint string, + usage *dto.Usage, containStreamUsage bool) { + + switch info.RelayFormat { + case relaycommon.RelayFormatOpenAI: + if info.ShouldIncludeUsage && !containStreamUsage { + response := helper.GenerateFinalUsageResponse(responseId, createAt, model, *usage) + response.SetSystemFingerprint(systemFingerprint) + helper.ObjectData(c, response) + } + helper.Done(c) + + case relaycommon.RelayFormatClaude: + info.ClaudeConvertInfo.Done = true + var streamResponse dto.ChatCompletionsStreamResponse + if err := json.Unmarshal(common.StringToByteSlice(lastStreamData), &streamResponse); err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + return + } + + info.ClaudeConvertInfo.Usage = usage + + claudeResponses := service.StreamResponseOpenAI2Claude(&streamResponse, info) + for _, resp := range claudeResponses { + helper.ClaudeData(c, *resp) + } + } +} diff --git a/relay/channel/openai/relay-openai.go b/relay/channel/openai/relay-openai.go index ffd36d3c..7e06ea12 100644 --- a/relay/channel/openai/relay-openai.go +++ b/relay/channel/openai/relay-openai.go @@ -12,7 +12,6 @@ import ( "one-api/constant" "one-api/dto" relaycommon "one-api/relay/common" - relayconstant "one-api/relay/constant" "one-api/relay/helper" "one-api/service" "os" @@ -34,7 +33,7 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo } var lastStreamResponse dto.ChatCompletionsStreamResponse - if err := json.Unmarshal(common.StringToByteSlice(data), &lastStreamResponse); err != nil { + if err := common.DecodeJsonStr(data, &lastStreamResponse); err != nil { return err } @@ -66,6 +65,7 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo response.Choices[i].Delta.Reasoning = nil } info.ThinkingContentInfo.IsFirstThinkingContent = false + info.ThinkingContentInfo.HasSentThinkingContent = true return helper.ObjectData(c, response) } } @@ -77,7 +77,8 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo // Process each choice for i, choice := range lastStreamResponse.Choices { // Handle transition from thinking to content - if hasContent && !info.ThinkingContentInfo.SendLastThinkingContent { + // only send `
` tag when previous thinking content has been sent + if hasContent && !info.ThinkingContentInfo.SendLastThinkingContent && info.ThinkingContentInfo.HasSentThinkingContent { response := lastStreamResponse.Copy() for j := range response.Choices { response.Choices[j].Delta.SetContentString("\n
\n") @@ -88,7 +89,7 @@ func sendStreamData(c *gin.Context, info *relaycommon.RelayInfo, data string, fo helper.ObjectData(c, response) } - // Convert reasoning content to regular content + // Convert reasoning content to regular content if any if len(choice.Delta.GetReasoningContent()) > 0 { lastStreamResponse.Choices[i].Delta.SetContentString(choice.Delta.GetReasoningContent()) lastStreamResponse.Choices[i].Delta.ReasoningContent = nil @@ -116,6 +117,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel model := info.UpstreamModelName var responseTextBuilder strings.Builder + var toolCount int var usage = &dto.Usage{} var streamItems []string // store stream items var forceFormat bool @@ -129,17 +131,15 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel thinkToContent = think2Content } - toolCount := 0 - var ( lastStreamData string ) helper.StreamScannerHandler(c, resp, info, func(data string) bool { if lastStreamData != "" { - err := sendStreamData(c, info, lastStreamData, forceFormat, thinkToContent) + err := handleStreamFormat(c, info, lastStreamData, forceFormat, thinkToContent) if err != nil { - common.LogError(c, "streaming error: "+err.Error()) + common.SysError("error handling stream format: " + err.Error()) } } lastStreamData = data @@ -149,7 +149,7 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel shouldSendLastResp := true var lastStreamResponse dto.ChatCompletionsStreamResponse - err := json.Unmarshal(common.StringToByteSlice(lastStreamData), &lastStreamResponse) + err := common.DecodeJsonStr(lastStreamData, &lastStreamResponse) if err == nil { responseId = lastStreamResponse.Id createAt = lastStreamResponse.Created @@ -168,87 +168,15 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel } } } + if shouldSendLastResp { sendStreamData(c, info, lastStreamData, forceFormat, thinkToContent) + //err = handleStreamFormat(c, info, lastStreamData, forceFormat, thinkToContent) } - // 计算token - streamResp := "[" + strings.Join(streamItems, ",") + "]" - switch info.RelayMode { - case relayconstant.RelayModeChatCompletions: - var streamResponses []dto.ChatCompletionsStreamResponse - err := json.Unmarshal(common.StringToByteSlice(streamResp), &streamResponses) - if err != nil { - // 一次性解析失败,逐个解析 - common.SysError("error unmarshalling stream response: " + err.Error()) - for _, item := range streamItems { - var streamResponse dto.ChatCompletionsStreamResponse - err := json.Unmarshal(common.StringToByteSlice(item), &streamResponse) - if err == nil { - //if service.ValidUsage(streamResponse.Usage) { - // usage = streamResponse.Usage - //} - for _, choice := range streamResponse.Choices { - responseTextBuilder.WriteString(choice.Delta.GetContentString()) - - // handle both reasoning_content and reasoning - responseTextBuilder.WriteString(choice.Delta.GetReasoningContent()) - - if choice.Delta.ToolCalls != nil { - if len(choice.Delta.ToolCalls) > toolCount { - toolCount = len(choice.Delta.ToolCalls) - } - for _, tool := range choice.Delta.ToolCalls { - responseTextBuilder.WriteString(tool.Function.Name) - responseTextBuilder.WriteString(tool.Function.Arguments) - } - } - } - } - } - } else { - for _, streamResponse := range streamResponses { - //if service.ValidUsage(streamResponse.Usage) { - // usage = streamResponse.Usage - // containStreamUsage = true - //} - for _, choice := range streamResponse.Choices { - responseTextBuilder.WriteString(choice.Delta.GetContentString()) - responseTextBuilder.WriteString(choice.Delta.GetReasoningContent()) // This will handle both reasoning_content and reasoning - if choice.Delta.ToolCalls != nil { - if len(choice.Delta.ToolCalls) > toolCount { - toolCount = len(choice.Delta.ToolCalls) - } - for _, tool := range choice.Delta.ToolCalls { - responseTextBuilder.WriteString(tool.Function.Name) - responseTextBuilder.WriteString(tool.Function.Arguments) - } - } - } - } - } - case relayconstant.RelayModeCompletions: - var streamResponses []dto.CompletionsStreamResponse - err := json.Unmarshal(common.StringToByteSlice(streamResp), &streamResponses) - if err != nil { - // 一次性解析失败,逐个解析 - common.SysError("error unmarshalling stream response: " + err.Error()) - for _, item := range streamItems { - var streamResponse dto.CompletionsStreamResponse - err := json.Unmarshal(common.StringToByteSlice(item), &streamResponse) - if err == nil { - for _, choice := range streamResponse.Choices { - responseTextBuilder.WriteString(choice.Text) - } - } - } - } else { - for _, streamResponse := range streamResponses { - for _, choice := range streamResponse.Choices { - responseTextBuilder.WriteString(choice.Text) - } - } - } + // 处理token计算 + if err := processTokens(info.RelayMode, streamItems, &responseTextBuilder, &toolCount); err != nil { + common.SysError("error processing tokens: " + err.Error()) } if !containStreamUsage { @@ -262,20 +190,13 @@ func OaiStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.Rel } } - if info.ShouldIncludeUsage && !containStreamUsage { - response := helper.GenerateFinalUsageResponse(responseId, createAt, model, *usage) - response.SetSystemFingerprint(systemFingerprint) - helper.ObjectData(c, response) - } + handleFinalResponse(c, info, lastStreamData, responseId, createAt, model, systemFingerprint, usage, containStreamUsage) - helper.Done(c) - - //resp.Body.Close() return nil, usage } -func OpenaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model string) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - var simpleResponse dto.SimpleResponse +func OpenaiHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + var simpleResponse dto.OpenAITextResponse responseBody, err := io.ReadAll(resp.Body) if err != nil { return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil @@ -284,16 +205,29 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model if err != nil { return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil } - err = json.Unmarshal(responseBody, &simpleResponse) + err = common.DecodeJson(responseBody, &simpleResponse) if err != nil { return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil } - if simpleResponse.Error.Type != "" { + if simpleResponse.Error != nil && simpleResponse.Error.Type != "" { return &dto.OpenAIErrorWithStatusCode{ - Error: simpleResponse.Error, + Error: *simpleResponse.Error, StatusCode: resp.StatusCode, }, nil } + + switch info.RelayFormat { + case relaycommon.RelayFormatOpenAI: + break + case relaycommon.RelayFormatClaude: + claudeResp := service.ResponseOpenAI2Claude(&simpleResponse, info) + claudeRespStr, err := json.Marshal(claudeResp) + if err != nil { + return service.OpenAIErrorWrapper(err, "marshal_response_body_failed", http.StatusInternalServerError), nil + } + responseBody = claudeRespStr + } + // Reset response body resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) // We shouldn't set the header before we parse the response body, because the parse part may fail. @@ -306,19 +240,20 @@ func OpenaiHandler(c *gin.Context, resp *http.Response, promptTokens int, model c.Writer.WriteHeader(resp.StatusCode) _, err = io.Copy(c.Writer, resp.Body) if err != nil { - return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil + //return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil + common.SysError("error copying response body: " + err.Error()) } resp.Body.Close() if simpleResponse.Usage.TotalTokens == 0 || (simpleResponse.Usage.PromptTokens == 0 && simpleResponse.Usage.CompletionTokens == 0) { completionTokens := 0 for _, choice := range simpleResponse.Choices { - ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, model) + ctkm, _ := service.CountTextToken(choice.Message.StringContent()+choice.Message.ReasoningContent+choice.Message.Reasoning, info.UpstreamModelName) completionTokens += ctkm } simpleResponse.Usage = dto.Usage{ - PromptTokens: promptTokens, + PromptTokens: info.PromptTokens, CompletionTokens: completionTokens, - TotalTokens: promptTokens + completionTokens, + TotalTokens: info.PromptTokens + completionTokens, } } return nil, &simpleResponse.Usage diff --git a/relay/channel/openrouter/adaptor.go b/relay/channel/openrouter/adaptor.go deleted file mode 100644 index 83afb6af..00000000 --- a/relay/channel/openrouter/adaptor.go +++ /dev/null @@ -1,74 +0,0 @@ -package openrouter - -import ( - "errors" - "fmt" - "github.com/gin-gonic/gin" - "io" - "net/http" - "one-api/dto" - "one-api/relay/channel" - "one-api/relay/channel/openai" - relaycommon "one-api/relay/common" -) - -type Adaptor struct { -} - -func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { - //TODO implement me - return nil, errors.New("not implemented") -} - -func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { - //TODO implement me - return nil, errors.New("not implemented") -} - -func (a *Adaptor) Init(info *relaycommon.RelayInfo) { -} - -func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil -} - -func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { - channel.SetupApiRequestHeader(info, c, req) - req.Set("Authorization", fmt.Sprintf("Bearer %s", info.ApiKey)) - req.Set("HTTP-Referer", "https://github.com/Calcium-Ion/new-api") - req.Set("X-Title", "New API") - return nil -} - -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { - return request, nil -} - -func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { - return channel.DoApiRequest(a, c, info, requestBody) -} - -func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { - return nil, errors.New("not implemented") -} - -func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { - if info.IsStream { - err, usage = openai.OaiStreamHandler(c, resp, info) - } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) - } - return -} - -func (a *Adaptor) GetModelList() []string { - return ModelList -} - -func (a *Adaptor) GetChannelName() string { - return ChannelName -} diff --git a/relay/channel/palm/adaptor.go b/relay/channel/palm/adaptor.go index f38fa95b..f0220f4f 100644 --- a/relay/channel/palm/adaptor.go +++ b/relay/channel/palm/adaptor.go @@ -15,6 +15,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -38,7 +44,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -54,7 +60,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return nil, errors.New("not implemented") } - func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/perplexity/adaptor.go b/relay/channel/perplexity/adaptor.go index 2b27bdb1..5727cac7 100644 --- a/relay/channel/perplexity/adaptor.go +++ b/relay/channel/perplexity/adaptor.go @@ -15,6 +15,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -38,7 +44,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -57,7 +63,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return nil, errors.New("not implemented") } - func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } @@ -66,7 +71,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/siliconflow/adaptor.go b/relay/channel/siliconflow/adaptor.go index 797f0244..cf38c15e 100644 --- a/relay/channel/siliconflow/adaptor.go +++ b/relay/channel/siliconflow/adaptor.go @@ -16,6 +16,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -48,7 +54,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { return request, nil } @@ -68,20 +74,16 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom switch info.RelayMode { case constant.RelayModeRerank: err, usage = siliconflowRerankHandler(c, resp) + case constant.RelayModeCompletions: + fallthrough case constant.RelayModeChatCompletions: if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) - } - case constant.RelayModeCompletions: - if info.IsStream { - err, usage = openai.OaiStreamHandler(c, resp, info) - } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } case constant.RelayModeEmbeddings: - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/siliconflow/dto.go b/relay/channel/siliconflow/dto.go index 58cf81cd..add0fd07 100644 --- a/relay/channel/siliconflow/dto.go +++ b/relay/channel/siliconflow/dto.go @@ -12,6 +12,6 @@ type SFMeta struct { } type SFRerankResponse struct { - Results []dto.RerankResponseDocument `json:"results"` - Meta SFMeta `json:"meta"` + Results []dto.RerankResponseResult `json:"results"` + Meta SFMeta `json:"meta"` } diff --git a/relay/channel/tencent/adaptor.go b/relay/channel/tencent/adaptor.go index 768ef646..f2b51ee9 100644 --- a/relay/channel/tencent/adaptor.go +++ b/relay/channel/tencent/adaptor.go @@ -23,6 +23,12 @@ type Adaptor struct { Timestamp int64 } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -52,7 +58,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -78,7 +84,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return nil, errors.New("not implemented") } - func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/vertex/adaptor.go b/relay/channel/vertex/adaptor.go index 7ccd3f30..77f29620 100644 --- a/relay/channel/vertex/adaptor.go +++ b/relay/channel/vertex/adaptor.go @@ -38,6 +38,16 @@ type Adaptor struct { AccountCredentials Credentials } +func (a *Adaptor) ConvertClaudeRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.ClaudeRequest) (any, error) { + if v, ok := claudeModelMap[info.UpstreamModelName]; ok { + c.Set("request_model", v) + } else { + c.Set("request_model", request.Model) + } + vertexClaudeReq := copyRequest(request, anthropicVersion) + return vertexClaudeReq, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -119,7 +129,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -133,7 +143,7 @@ func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, re info.UpstreamModelName = claudeReq.Model return vertexClaudeReq, nil } else if a.RequestMode == RequestModeGemini { - geminiRequest, err := gemini.CovertGemini2OpenAI(*request) + geminiRequest, err := gemini.CovertGemini2OpenAI(*request, info) if err != nil { return nil, err } @@ -175,7 +185,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom case RequestModeGemini: err, usage = gemini.GeminiChatHandler(c, resp, info) case RequestModeLlama: - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.OriginModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } } return diff --git a/relay/channel/vertex/dto.go b/relay/channel/vertex/dto.go index 4ba570de..4a571612 100644 --- a/relay/channel/vertex/dto.go +++ b/relay/channel/vertex/dto.go @@ -1,25 +1,25 @@ package vertex import ( - "one-api/relay/channel/claude" + "one-api/dto" ) type VertexAIClaudeRequest struct { - AnthropicVersion string `json:"anthropic_version"` - Messages []claude.ClaudeMessage `json:"messages"` - System any `json:"system,omitempty"` - MaxTokens uint `json:"max_tokens,omitempty"` - StopSequences []string `json:"stop_sequences,omitempty"` - Stream bool `json:"stream,omitempty"` - Temperature *float64 `json:"temperature,omitempty"` - TopP float64 `json:"top_p,omitempty"` - TopK int `json:"top_k,omitempty"` - Tools any `json:"tools,omitempty"` - ToolChoice any `json:"tool_choice,omitempty"` - Thinking *claude.Thinking `json:"thinking,omitempty"` + AnthropicVersion string `json:"anthropic_version"` + Messages []dto.ClaudeMessage `json:"messages"` + System any `json:"system,omitempty"` + MaxTokens uint `json:"max_tokens,omitempty"` + StopSequences []string `json:"stop_sequences,omitempty"` + Stream bool `json:"stream,omitempty"` + Temperature *float64 `json:"temperature,omitempty"` + TopP float64 `json:"top_p,omitempty"` + TopK int `json:"top_k,omitempty"` + Tools any `json:"tools,omitempty"` + ToolChoice any `json:"tool_choice,omitempty"` + Thinking *dto.Thinking `json:"thinking,omitempty"` } -func copyRequest(req *claude.ClaudeRequest, version string) *VertexAIClaudeRequest { +func copyRequest(req *dto.ClaudeRequest, version string) *VertexAIClaudeRequest { return &VertexAIClaudeRequest{ AnthropicVersion: version, System: req.System, diff --git a/relay/channel/volcengine/adaptor.go b/relay/channel/volcengine/adaptor.go index 3b57c67c..277285b7 100644 --- a/relay/channel/volcengine/adaptor.go +++ b/relay/channel/volcengine/adaptor.go @@ -17,6 +17,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -50,7 +56,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -75,10 +81,10 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } case constant.RelayModeEmbeddings: - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/xai/adaptor.go b/relay/channel/xai/adaptor.go new file mode 100644 index 00000000..669b8c68 --- /dev/null +++ b/relay/channel/xai/adaptor.go @@ -0,0 +1,104 @@ +package xai + +import ( + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/dto" + "one-api/relay/channel" + relaycommon "one-api/relay/common" + "strings" +) + +type Adaptor struct { +} + +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + //panic("implement me") + return nil, errors.New("not available") +} + +func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { + //not available + return nil, errors.New("not available") +} + +func (a *Adaptor) ConvertImageRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.ImageRequest) (any, error) { + request.Size = "" + return request, nil +} + +func (a *Adaptor) Init(info *relaycommon.RelayInfo) { +} + +func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { + return fmt.Sprintf("%s/v1/chat/completions", info.BaseUrl), nil +} + +func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { + channel.SetupApiRequestHeader(info, c, req) + req.Set("Authorization", "Bearer "+info.ApiKey) + return nil +} + +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { + if request == nil { + return nil, errors.New("request is nil") + } + if strings.HasPrefix(request.Model, "grok-3-mini") { + if request.MaxCompletionTokens == 0 && request.MaxTokens != 0 { + request.MaxCompletionTokens = request.MaxTokens + request.MaxTokens = 0 + } + if strings.HasSuffix(request.Model, "-high") { + request.ReasoningEffort = "high" + request.Model = strings.TrimSuffix(request.Model, "-high") + } else if strings.HasSuffix(request.Model, "-low") { + request.ReasoningEffort = "low" + request.Model = strings.TrimSuffix(request.Model, "-low") + } else if strings.HasSuffix(request.Model, "-medium") { + request.ReasoningEffort = "medium" + request.Model = strings.TrimSuffix(request.Model, "-medium") + } + info.ReasoningEffort = request.ReasoningEffort + info.UpstreamModelName = request.Model + } + return request, nil +} + +func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dto.RerankRequest) (any, error) { + return nil, nil +} + +func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { + //not available + return nil, errors.New("not available") +} + +func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { + return channel.DoApiRequest(a, c, info, requestBody) +} + +func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (usage any, err *dto.OpenAIErrorWithStatusCode) { + if info.IsStream { + err, usage = xAIStreamHandler(c, resp, info) + } else { + err, usage = xAIHandler(c, resp, info) + } + //if _, ok := usage.(*dto.Usage); ok && usage != nil { + // usage.(*dto.Usage).CompletionTokens = usage.(*dto.Usage).TotalTokens - usage.(*dto.Usage).PromptTokens + //} + + return +} + +func (a *Adaptor) GetModelList() []string { + return ModelList +} + +func (a *Adaptor) GetChannelName() string { + return ChannelName +} diff --git a/relay/channel/xai/constants.go b/relay/channel/xai/constants.go new file mode 100644 index 00000000..685fe3bb --- /dev/null +++ b/relay/channel/xai/constants.go @@ -0,0 +1,18 @@ +package xai + +var ModelList = []string{ + // grok-3 + "grok-3-beta", "grok-3-mini-beta", + // grok-3 mini + "grok-3-fast-beta", "grok-3-mini-fast-beta", + // extend grok-3-mini reasoning + "grok-3-mini-beta-high", "grok-3-mini-beta-low", "grok-3-mini-beta-medium", + "grok-3-mini-fast-beta-high", "grok-3-mini-fast-beta-low", "grok-3-mini-fast-beta-medium", + // image model + "grok-2-image", + // legacy models + "grok-2", "grok-2-vision", + "grok-beta", "grok-vision-beta", +} + +var ChannelName = "xai" diff --git a/relay/channel/xai/dto.go b/relay/channel/xai/dto.go new file mode 100644 index 00000000..7036d5f1 --- /dev/null +++ b/relay/channel/xai/dto.go @@ -0,0 +1,14 @@ +package xai + +import "one-api/dto" + +// ChatCompletionResponse represents the response from XAI chat completion API +type ChatCompletionResponse struct { + Id string `json:"id"` + Object string `json:"object"` + Created int64 `json:"created"` + Model string `json:"model"` + Choices []dto.ChatCompletionsStreamResponseChoice + Usage *dto.Usage `json:"usage"` + SystemFingerprint string `json:"system_fingerprint"` +} diff --git a/relay/channel/xai/text.go b/relay/channel/xai/text.go new file mode 100644 index 00000000..e019c2dc --- /dev/null +++ b/relay/channel/xai/text.go @@ -0,0 +1,119 @@ +package xai + +import ( + "bytes" + "encoding/json" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/common" + "one-api/dto" + "one-api/relay/channel/openai" + relaycommon "one-api/relay/common" + "one-api/relay/helper" + "one-api/service" + "strings" +) + +func streamResponseXAI2OpenAI(xAIResp *dto.ChatCompletionsStreamResponse, usage *dto.Usage) *dto.ChatCompletionsStreamResponse { + if xAIResp == nil { + return nil + } + if xAIResp.Usage != nil { + xAIResp.Usage.CompletionTokens = usage.CompletionTokens + } + openAIResp := &dto.ChatCompletionsStreamResponse{ + Id: xAIResp.Id, + Object: xAIResp.Object, + Created: xAIResp.Created, + Model: xAIResp.Model, + Choices: xAIResp.Choices, + Usage: xAIResp.Usage, + } + + return openAIResp +} + +func xAIStreamHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + usage := &dto.Usage{} + var responseTextBuilder strings.Builder + var toolCount int + var containStreamUsage bool + + helper.SetEventStreamHeaders(c) + + helper.StreamScannerHandler(c, resp, info, func(data string) bool { + var xAIResp *dto.ChatCompletionsStreamResponse + err := json.Unmarshal([]byte(data), &xAIResp) + if err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + return true + } + + // 把 xAI 的usage转换为 OpenAI 的usage + if xAIResp.Usage != nil { + containStreamUsage = true + usage.PromptTokens = xAIResp.Usage.PromptTokens + usage.TotalTokens = xAIResp.Usage.TotalTokens + usage.CompletionTokens = usage.TotalTokens - usage.PromptTokens + } + + openaiResponse := streamResponseXAI2OpenAI(xAIResp, usage) + _ = openai.ProcessStreamResponse(*openaiResponse, &responseTextBuilder, &toolCount) + err = helper.ObjectData(c, openaiResponse) + if err != nil { + common.SysError(err.Error()) + } + return true + }) + + if !containStreamUsage { + usage, _ = service.ResponseText2Usage(responseTextBuilder.String(), info.UpstreamModelName, info.PromptTokens) + usage.CompletionTokens += toolCount * 7 + } + + helper.Done(c) + err := resp.Body.Close() + if err != nil { + //return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + common.SysError("close_response_body_failed: " + err.Error()) + } + return nil, usage +} + +func xAIHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + responseBody, err := io.ReadAll(resp.Body) + var response *dto.TextResponse + err = common.DecodeJson(responseBody, &response) + if err != nil { + common.SysError("error unmarshalling stream response: " + err.Error()) + return nil, nil + } + response.Usage.CompletionTokens = response.Usage.TotalTokens - response.Usage.PromptTokens + response.Usage.CompletionTokenDetails.TextTokens = response.Usage.CompletionTokens - response.Usage.CompletionTokenDetails.ReasoningTokens + + // new body + encodeJson, err := common.EncodeJson(response) + if err != nil { + common.SysError("error marshalling stream response: " + err.Error()) + return nil, nil + } + + // set new body + resp.Body = io.NopCloser(bytes.NewBuffer(encodeJson)) + + for k, v := range resp.Header { + c.Writer.Header().Set(k, v[0]) + } + c.Writer.WriteHeader(resp.StatusCode) + _, err = io.Copy(c.Writer, resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + + return nil, &response.Usage +} diff --git a/relay/channel/xinference/constant.go b/relay/channel/xinference/constant.go new file mode 100644 index 00000000..a119084f --- /dev/null +++ b/relay/channel/xinference/constant.go @@ -0,0 +1,8 @@ +package xinference + +var ModelList = []string{ + "bge-reranker-v2-m3", + "jina-reranker-v2", +} + +var ChannelName = "xinference" diff --git a/relay/channel/xinference/dto.go b/relay/channel/xinference/dto.go new file mode 100644 index 00000000..2f12ad10 --- /dev/null +++ b/relay/channel/xinference/dto.go @@ -0,0 +1,11 @@ +package xinference + +type XinRerankResponseDocument struct { + Document string `json:"document,omitempty"` + Index int `json:"index"` + RelevanceScore float64 `json:"relevance_score"` +} + +type XinRerankResponse struct { + Results []XinRerankResponseDocument `json:"results"` +} diff --git a/relay/channel/xunfei/adaptor.go b/relay/channel/xunfei/adaptor.go index 71fd1367..9521bb47 100644 --- a/relay/channel/xunfei/adaptor.go +++ b/relay/channel/xunfei/adaptor.go @@ -16,6 +16,12 @@ type Adaptor struct { request *dto.GeneralOpenAIRequest } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -38,7 +44,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -55,7 +61,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return nil, errors.New("not implemented") } - func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { // xunfei's request is not http request, so we don't need to do anything here dummyResp := &http.Response{} diff --git a/relay/channel/zhipu/adaptor.go b/relay/channel/zhipu/adaptor.go index 87ff20d5..04369001 100644 --- a/relay/channel/zhipu/adaptor.go +++ b/relay/channel/zhipu/adaptor.go @@ -14,6 +14,12 @@ import ( type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -42,7 +48,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -61,7 +67,6 @@ func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.Rela return nil, errors.New("not implemented") } - func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } diff --git a/relay/channel/zhipu_4v/adaptor.go b/relay/channel/zhipu_4v/adaptor.go index 5983c1d9..e13a7ad2 100644 --- a/relay/channel/zhipu_4v/adaptor.go +++ b/relay/channel/zhipu_4v/adaptor.go @@ -10,11 +10,18 @@ import ( "one-api/relay/channel" "one-api/relay/channel/openai" relaycommon "one-api/relay/common" + relayconstant "one-api/relay/constant" ) type Adaptor struct { } +func (a *Adaptor) ConvertClaudeRequest(*gin.Context, *relaycommon.RelayInfo, *dto.ClaudeRequest) (any, error) { + //TODO implement me + panic("implement me") + return nil, nil +} + func (a *Adaptor) ConvertAudioRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.AudioRequest) (io.Reader, error) { //TODO implement me return nil, errors.New("not implemented") @@ -29,7 +36,13 @@ func (a *Adaptor) Init(info *relaycommon.RelayInfo) { } func (a *Adaptor) GetRequestURL(info *relaycommon.RelayInfo) (string, error) { - return fmt.Sprintf("%s/api/paas/v4/chat/completions", info.BaseUrl), nil + baseUrl := fmt.Sprintf("%s/api/paas/v4", info.BaseUrl) + switch info.RelayMode { + case relayconstant.RelayModeEmbeddings: + return fmt.Sprintf("%s/embeddings", baseUrl), nil + default: + return fmt.Sprintf("%s/chat/completions", baseUrl), nil + } } func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *relaycommon.RelayInfo) error { @@ -39,7 +52,7 @@ func (a *Adaptor) SetupRequestHeader(c *gin.Context, req *http.Header, info *rel return nil } -func (a *Adaptor) ConvertRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { +func (a *Adaptor) ConvertOpenAIRequest(c *gin.Context, info *relaycommon.RelayInfo, request *dto.GeneralOpenAIRequest) (any, error) { if request == nil { return nil, errors.New("request is nil") } @@ -54,11 +67,9 @@ func (a *Adaptor) ConvertRerankRequest(c *gin.Context, relayMode int, request dt } func (a *Adaptor) ConvertEmbeddingRequest(c *gin.Context, info *relaycommon.RelayInfo, request dto.EmbeddingRequest) (any, error) { - //TODO implement me - return nil, errors.New("not implemented") + return request, nil } - func (a *Adaptor) DoRequest(c *gin.Context, info *relaycommon.RelayInfo, requestBody io.Reader) (any, error) { return channel.DoApiRequest(a, c, info, requestBody) } @@ -67,7 +78,7 @@ func (a *Adaptor) DoResponse(c *gin.Context, resp *http.Response, info *relaycom if info.IsStream { err, usage = openai.OaiStreamHandler(c, resp, info) } else { - err, usage = openai.OpenaiHandler(c, resp, info.PromptTokens, info.UpstreamModelName) + err, usage = openai.OpenaiHandler(c, resp, info) } return } diff --git a/relay/channel/zhipu_4v/relay-zhipu_v4.go b/relay/channel/zhipu_4v/relay-zhipu_v4.go index faffec6f..271dda8f 100644 --- a/relay/channel/zhipu_4v/relay-zhipu_v4.go +++ b/relay/channel/zhipu_4v/relay-zhipu_v4.go @@ -1,17 +1,9 @@ package zhipu_4v import ( - "bufio" - "bytes" - "encoding/json" - "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" - "io" - "net/http" "one-api/common" "one-api/dto" - "one-api/relay/helper" - "one-api/service" "strings" "sync" "time" @@ -79,7 +71,7 @@ func requestOpenAI2Zhipu(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIReq mediaMessages := message.ParseContent() for j, mediaMessage := range mediaMessages { if mediaMessage.Type == dto.ContentTypeImageURL { - imageUrl := mediaMessage.ImageUrl.(dto.MessageImageUrl) + imageUrl := mediaMessage.GetImageMedia() // check if base64 if strings.HasPrefix(imageUrl.Url, "data:image/") { // 去除base64数据的URL前缀(如果有) @@ -119,163 +111,3 @@ func requestOpenAI2Zhipu(request dto.GeneralOpenAIRequest) *dto.GeneralOpenAIReq ToolChoice: request.ToolChoice, } } - -//func responseZhipu2OpenAI(response *dto.OpenAITextResponse) *dto.OpenAITextResponse { -// fullTextResponse := dto.OpenAITextResponse{ -// Id: response.Id, -// Object: "chat.completion", -// Created: common.GetTimestamp(), -// Choices: make([]dto.OpenAITextResponseChoice, 0, len(response.TextResponseChoices)), -// Usage: response.Usage, -// } -// for i, choice := range response.TextResponseChoices { -// content, _ := json.Marshal(strings.Trim(choice.Content, "\"")) -// openaiChoice := dto.OpenAITextResponseChoice{ -// Index: i, -// Message: dto.Message{ -// Role: choice.Role, -// Content: content, -// }, -// FinishReason: "", -// } -// if i == len(response.TextResponseChoices)-1 { -// openaiChoice.FinishReason = "stop" -// } -// fullTextResponse.Choices = append(fullTextResponse.Choices, openaiChoice) -// } -// return &fullTextResponse -//} - -func streamResponseZhipu2OpenAI(zhipuResponse *ZhipuV4StreamResponse) *dto.ChatCompletionsStreamResponse { - var choice dto.ChatCompletionsStreamResponseChoice - choice.Delta.Content = zhipuResponse.Choices[0].Delta.Content - choice.Delta.Role = zhipuResponse.Choices[0].Delta.Role - choice.Delta.ToolCalls = zhipuResponse.Choices[0].Delta.ToolCalls - choice.Index = zhipuResponse.Choices[0].Index - choice.FinishReason = zhipuResponse.Choices[0].FinishReason - response := dto.ChatCompletionsStreamResponse{ - Id: zhipuResponse.Id, - Object: "chat.completion.chunk", - Created: zhipuResponse.Created, - Model: "glm-4v", - Choices: []dto.ChatCompletionsStreamResponseChoice{choice}, - } - return &response -} - -func lastStreamResponseZhipuV42OpenAI(zhipuResponse *ZhipuV4StreamResponse) (*dto.ChatCompletionsStreamResponse, *dto.Usage) { - response := streamResponseZhipu2OpenAI(zhipuResponse) - return response, &zhipuResponse.Usage -} - -func zhipuStreamHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - var usage *dto.Usage - scanner := bufio.NewScanner(resp.Body) - scanner.Split(func(data []byte, atEOF bool) (advance int, token []byte, err error) { - if atEOF && len(data) == 0 { - return 0, nil, nil - } - if i := strings.Index(string(data), "\n"); i >= 0 { - return i + 1, data[0:i], nil - } - if atEOF { - return len(data), data, nil - } - return 0, nil, nil - }) - dataChan := make(chan string) - stopChan := make(chan bool) - go func() { - for scanner.Scan() { - data := scanner.Text() - if len(data) < 6 { // ignore blank line or wrong format - continue - } - if data[:6] != "data: " && data[:6] != "[DONE]" { - continue - } - dataChan <- data - } - stopChan <- true - }() - helper.SetEventStreamHeaders(c) - c.Stream(func(w io.Writer) bool { - select { - case data := <-dataChan: - if strings.HasPrefix(data, "data: [DONE]") { - data = data[:12] - } - // some implementations may add \r at the end of data - data = strings.TrimSuffix(data, "\r") - - var streamResponse ZhipuV4StreamResponse - err := json.Unmarshal([]byte(data), &streamResponse) - if err != nil { - common.SysError("error unmarshalling stream response: " + err.Error()) - } - var response *dto.ChatCompletionsStreamResponse - if strings.Contains(data, "prompt_tokens") { - response, usage = lastStreamResponseZhipuV42OpenAI(&streamResponse) - } else { - response = streamResponseZhipu2OpenAI(&streamResponse) - } - jsonResponse, err := json.Marshal(response) - if err != nil { - common.SysError("error marshalling stream response: " + err.Error()) - return true - } - c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonResponse)}) - return true - case <-stopChan: - return false - } - }) - err := resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - return nil, usage -} - -func zhipuHandler(c *gin.Context, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { - var textResponse ZhipuV4Response - responseBody, err := io.ReadAll(resp.Body) - if err != nil { - return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil - } - err = resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - err = json.Unmarshal(responseBody, &textResponse) - if err != nil { - return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil - } - if textResponse.Error.Type != "" { - return &dto.OpenAIErrorWithStatusCode{ - Error: textResponse.Error, - StatusCode: resp.StatusCode, - }, nil - } - // Reset response body - resp.Body = io.NopCloser(bytes.NewBuffer(responseBody)) - - // We shouldn't set the header before we parse the response body, because the parse part may fail. - // And then we will have to send an error response, but in this case, the header has already been set. - // So the HTTPClient will be confused by the response. - // For example, Postman will report error, and we cannot check the response at all. - for k, v := range resp.Header { - c.Writer.Header().Set(k, v[0]) - } - c.Writer.WriteHeader(resp.StatusCode) - _, err = io.Copy(c.Writer, resp.Body) - if err != nil { - return service.OpenAIErrorWrapper(err, "copy_response_body_failed", http.StatusInternalServerError), nil - } - err = resp.Body.Close() - if err != nil { - return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil - } - - return nil, &textResponse.Usage -} diff --git a/relay/claude_handler.go b/relay/claude_handler.go new file mode 100644 index 00000000..fb68a88a --- /dev/null +++ b/relay/claude_handler.go @@ -0,0 +1,163 @@ +package relay + +import ( + "bytes" + "encoding/json" + "errors" + "fmt" + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/common" + "one-api/dto" + relaycommon "one-api/relay/common" + "one-api/relay/helper" + "one-api/service" + "one-api/setting/model_setting" + "strings" +) + +func getAndValidateClaudeRequest(c *gin.Context) (textRequest *dto.ClaudeRequest, err error) { + textRequest = &dto.ClaudeRequest{} + err = c.ShouldBindJSON(textRequest) + if err != nil { + return nil, err + } + if textRequest.Messages == nil || len(textRequest.Messages) == 0 { + return nil, errors.New("field messages is required") + } + if textRequest.Model == "" { + return nil, errors.New("field model is required") + } + return textRequest, nil +} + +func ClaudeHelper(c *gin.Context) (claudeError *dto.ClaudeErrorWithStatusCode) { + + relayInfo := relaycommon.GenRelayInfoClaude(c) + + // get & validate textRequest 获取并验证文本请求 + textRequest, err := getAndValidateClaudeRequest(c) + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "invalid_claude_request", http.StatusBadRequest) + } + + if textRequest.Stream { + relayInfo.IsStream = true + } + + err = helper.ModelMappedHelper(c, relayInfo) + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "model_mapped_error", http.StatusInternalServerError) + } + + textRequest.Model = relayInfo.UpstreamModelName + + promptTokens, err := getClaudePromptTokens(textRequest, relayInfo) + // count messages token error 计算promptTokens错误 + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "count_token_messages_failed", http.StatusInternalServerError) + } + + priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(textRequest.MaxTokens)) + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError) + } + + // pre-consume quota 预消耗配额 + preConsumedQuota, userQuota, openaiErr := preConsumeQuota(c, priceData.ShouldPreConsumedQuota, relayInfo) + + if openaiErr != nil { + return service.OpenAIErrorToClaudeError(openaiErr) + } + defer func() { + if openaiErr != nil { + returnPreConsumedQuota(c, relayInfo, userQuota, preConsumedQuota) + } + }() + + adaptor := GetAdaptor(relayInfo.ApiType) + if adaptor == nil { + return service.ClaudeErrorWrapperLocal(fmt.Errorf("invalid api type: %d", relayInfo.ApiType), "invalid_api_type", http.StatusBadRequest) + } + adaptor.Init(relayInfo) + var requestBody io.Reader + + if textRequest.MaxTokens == 0 { + textRequest.MaxTokens = uint(model_setting.GetClaudeSettings().GetDefaultMaxTokens(textRequest.Model)) + } + + if model_setting.GetClaudeSettings().ThinkingAdapterEnabled && + strings.HasSuffix(textRequest.Model, "-thinking") { + if textRequest.Thinking == nil { + // 因为BudgetTokens 必须大于1024 + if textRequest.MaxTokens < 1280 { + textRequest.MaxTokens = 1280 + } + + // BudgetTokens 为 max_tokens 的 80% + textRequest.Thinking = &dto.Thinking{ + Type: "enabled", + BudgetTokens: int(float64(textRequest.MaxTokens) * model_setting.GetClaudeSettings().ThinkingAdapterBudgetTokensPercentage), + } + // TODO: 临时处理 + // https://docs.anthropic.com/en/docs/build-with-claude/extended-thinking#important-considerations-when-using-extended-thinking + textRequest.TopP = 0 + textRequest.Temperature = common.GetPointer[float64](1.0) + } + textRequest.Model = strings.TrimSuffix(textRequest.Model, "-thinking") + relayInfo.UpstreamModelName = textRequest.Model + } + + convertedRequest, err := adaptor.ConvertClaudeRequest(c, relayInfo, textRequest) + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError) + } + jsonData, err := json.Marshal(convertedRequest) + if common.DebugEnabled { + println("requestBody: ", string(jsonData)) + } + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError) + } + requestBody = bytes.NewBuffer(jsonData) + + statusCodeMappingStr := c.GetString("status_code_mapping") + var httpResp *http.Response + resp, err := adaptor.DoRequest(c, relayInfo, requestBody) + if err != nil { + return service.ClaudeErrorWrapperLocal(err, "do_request_failed", http.StatusInternalServerError) + } + + if resp != nil { + httpResp = resp.(*http.Response) + relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream") + if httpResp.StatusCode != http.StatusOK { + openaiErr = service.RelayErrorHandler(httpResp, false) + // reset status code 重置状态码 + service.ResetStatusCode(openaiErr, statusCodeMappingStr) + return service.OpenAIErrorToClaudeError(openaiErr) + } + } + + usage, openaiErr := adaptor.DoResponse(c, httpResp, relayInfo) + //log.Printf("usage: %v", usage) + if openaiErr != nil { + // reset status code 重置状态码 + service.ResetStatusCode(openaiErr, statusCodeMappingStr) + return service.OpenAIErrorToClaudeError(openaiErr) + } + service.PostClaudeConsumeQuota(c, relayInfo, usage.(*dto.Usage), preConsumedQuota, userQuota, priceData, "") + return nil +} + +func getClaudePromptTokens(textRequest *dto.ClaudeRequest, info *relaycommon.RelayInfo) (int, error) { + var promptTokens int + var err error + switch info.RelayMode { + default: + promptTokens, err = service.CountTokenClaudeRequest(*textRequest, info.UpstreamModelName) + } + info.PromptTokens = promptTokens + return promptTokens, err +} diff --git a/relay/common/relay_info.go b/relay/common/relay_info.go index c1d3f4a4..a07ec316 100644 --- a/relay/common/relay_info.go +++ b/relay/common/relay_info.go @@ -15,6 +15,32 @@ import ( type ThinkingContentInfo struct { IsFirstThinkingContent bool SendLastThinkingContent bool + HasSentThinkingContent bool +} + +const ( + LastMessageTypeNone = "none" + LastMessageTypeText = "text" + LastMessageTypeTools = "tools" + LastMessageTypeThinking = "thinking" +) + +type ClaudeConvertInfo struct { + LastMessagesType string + Index int + Usage *dto.Usage + FinishReason string + Done bool +} + +const ( + RelayFormatOpenAI = "openai" + RelayFormatClaude = "claude" +) + +type RerankerInfo struct { + Documents []any + ReturnDocuments bool } type RelayInfo struct { @@ -55,10 +81,15 @@ type RelayInfo struct { AudioUsage bool ReasoningEffort string ChannelSetting map[string]interface{} + ParamOverride map[string]interface{} UserSetting map[string]interface{} UserEmail string UserQuota int + RelayFormat string + SendResponseCount int ThinkingContentInfo + *ClaudeConvertInfo + *RerankerInfo } // 定义支持流式选项的通道类型 @@ -71,6 +102,7 @@ var streamSupportedChannels = map[int]bool{ common.ChannelTypeAzure: true, common.ChannelTypeVolcEngine: true, common.ChannelTypeOllama: true, + common.ChannelTypeXai: true, } func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { @@ -82,10 +114,31 @@ func GenRelayInfoWs(c *gin.Context, ws *websocket.Conn) *RelayInfo { return info } +func GenRelayInfoClaude(c *gin.Context) *RelayInfo { + info := GenRelayInfo(c) + info.RelayFormat = RelayFormatClaude + info.ShouldIncludeUsage = false + info.ClaudeConvertInfo = &ClaudeConvertInfo{ + LastMessagesType: LastMessageTypeNone, + } + return info +} + +func GenRelayInfoRerank(c *gin.Context, req *dto.RerankRequest) *RelayInfo { + info := GenRelayInfo(c) + info.RelayMode = relayconstant.RelayModeRerank + info.RerankerInfo = &RerankerInfo{ + Documents: req.Documents, + ReturnDocuments: req.GetReturnDocuments(), + } + return info +} + func GenRelayInfo(c *gin.Context) *RelayInfo { channelType := c.GetInt("channel_type") channelId := c.GetInt("channel_id") channelSetting := c.GetStringMap("channel_setting") + paramOverride := c.GetStringMap("param_override") tokenId := c.GetInt("token_id") tokenKey := c.GetString("token_key") @@ -123,6 +176,8 @@ func GenRelayInfo(c *gin.Context) *RelayInfo { ApiKey: strings.TrimPrefix(c.Request.Header.Get("Authorization"), "Bearer "), Organization: c.GetString("channel_organization"), ChannelSetting: channelSetting, + ParamOverride: paramOverride, + RelayFormat: RelayFormatOpenAI, ThinkingContentInfo: ThinkingContentInfo{ IsFirstThinkingContent: true, SendLastThinkingContent: false, @@ -163,6 +218,10 @@ func (info *RelayInfo) SetFirstResponseTime() { } } +func (info *RelayInfo) HasSendResponse() bool { + return info.FirstResponseTime.After(info.StartTime) +} + type TaskRelayInfo struct { *RelayInfo Action string diff --git a/relay/common_handler/rerank.go b/relay/common_handler/rerank.go new file mode 100644 index 00000000..496278b5 --- /dev/null +++ b/relay/common_handler/rerank.go @@ -0,0 +1,68 @@ +package common_handler + +import ( + "github.com/gin-gonic/gin" + "io" + "net/http" + "one-api/common" + "one-api/dto" + "one-api/relay/channel/xinference" + relaycommon "one-api/relay/common" + "one-api/service" +) + +func RerankHandler(c *gin.Context, info *relaycommon.RelayInfo, resp *http.Response) (*dto.OpenAIErrorWithStatusCode, *dto.Usage) { + responseBody, err := io.ReadAll(resp.Body) + if err != nil { + return service.OpenAIErrorWrapper(err, "read_response_body_failed", http.StatusInternalServerError), nil + } + err = resp.Body.Close() + if err != nil { + return service.OpenAIErrorWrapper(err, "close_response_body_failed", http.StatusInternalServerError), nil + } + if common.DebugEnabled { + println("reranker response body: ", string(responseBody)) + } + var jinaResp dto.RerankResponse + if info.ChannelType == common.ChannelTypeXinference { + var xinRerankResponse xinference.XinRerankResponse + err = common.DecodeJson(responseBody, &xinRerankResponse) + if err != nil { + return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + jinaRespResults := make([]dto.RerankResponseResult, len(xinRerankResponse.Results)) + for i, result := range xinRerankResponse.Results { + respResult := dto.RerankResponseResult{ + Index: result.Index, + RelevanceScore: result.RelevanceScore, + } + if info.ReturnDocuments { + var document any + if result.Document == "" { + document = info.Documents[result.Index] + } else { + document = result.Document + } + respResult.Document = document + } + jinaRespResults[i] = respResult + } + jinaResp = dto.RerankResponse{ + Results: jinaRespResults, + Usage: dto.Usage{ + PromptTokens: info.PromptTokens, + TotalTokens: info.PromptTokens, + }, + } + } else { + err = common.DecodeJson(responseBody, &jinaResp) + if err != nil { + return service.OpenAIErrorWrapper(err, "unmarshal_response_body_failed", http.StatusInternalServerError), nil + } + jinaResp.Usage.PromptTokens = jinaResp.Usage.TotalTokens + } + + c.Writer.Header().Set("Content-Type", "application/json") + c.JSON(http.StatusOK, jinaResp) + return nil, &jinaResp.Usage +} diff --git a/relay/constant/api_type.go b/relay/constant/api_type.go index 8ccfee03..fef38f23 100644 --- a/relay/constant/api_type.go +++ b/relay/constant/api_type.go @@ -31,6 +31,8 @@ const ( APITypeVolcEngine APITypeBaiduV2 APITypeOpenRouter + APITypeXinference + APITypeXai APITypeDummy // this one is only for count, do not add any channel after this ) @@ -89,6 +91,10 @@ func ChannelType2APIType(channelType int) (int, bool) { apiType = APITypeBaiduV2 case common.ChannelTypeOpenRouter: apiType = APITypeOpenRouter + case common.ChannelTypeXinference: + apiType = APITypeXinference + case common.ChannelTypeXai: + apiType = APITypeXai } if apiType == -1 { return APITypeOpenAI, false diff --git a/relay/helper/common.go b/relay/helper/common.go index 2a72d30a..ebfb6d58 100644 --- a/relay/helper/common.go +++ b/relay/helper/common.go @@ -19,6 +19,30 @@ func SetEventStreamHeaders(c *gin.Context) { c.Writer.Header().Set("X-Accel-Buffering", "no") } +func ClaudeData(c *gin.Context, resp dto.ClaudeResponse) error { + jsonData, err := json.Marshal(resp) + if err != nil { + common.SysError("error marshalling stream response: " + err.Error()) + } else { + c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)}) + c.Render(-1, common.CustomEvent{Data: "data: " + string(jsonData)}) + } + if flusher, ok := c.Writer.(http.Flusher); ok { + flusher.Flush() + } else { + return errors.New("streaming error: flusher not found") + } + return nil +} + +func ClaudeChunkData(c *gin.Context, resp dto.ClaudeResponse, data string) { + c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("event: %s\n", resp.Type)}) + c.Render(-1, common.CustomEvent{Data: fmt.Sprintf("data: %s\n", data)}) + if flusher, ok := c.Writer.(http.Flusher); ok { + flusher.Flush() + } +} + func StringData(c *gin.Context, str string) error { //str = strings.TrimPrefix(str, "data: ") //str = strings.TrimSuffix(str, "\r") @@ -31,7 +55,20 @@ func StringData(c *gin.Context, str string) error { return nil } +func PingData(c *gin.Context) error { + c.Writer.Write([]byte(": PING\n\n")) + if flusher, ok := c.Writer.(http.Flusher); ok { + flusher.Flush() + } else { + return errors.New("streaming error: flusher not found") + } + return nil +} + func ObjectData(c *gin.Context, object interface{}) error { + if object == nil { + return errors.New("object is nil") + } jsonData, err := json.Marshal(object) if err != nil { return fmt.Errorf("error marshalling object: %w", err) diff --git a/relay/helper/price.go b/relay/helper/price.go index b169df98..a68cd54d 100644 --- a/relay/helper/price.go +++ b/relay/helper/price.go @@ -4,6 +4,7 @@ import ( "fmt" "github.com/gin-gonic/gin" "one-api/common" + constant2 "one-api/constant" relaycommon "one-api/relay/common" "one-api/setting" "one-api/setting/operation_setting" @@ -16,9 +17,14 @@ type PriceData struct { CacheRatio float64 GroupRatio float64 UsePrice bool + CacheCreationRatio float64 ShouldPreConsumedQuota int } +func (p PriceData) ToSetting() string { + return fmt.Sprintf("ModelPrice: %f, ModelRatio: %f, CompletionRatio: %f, CacheRatio: %f, GroupRatio: %f, UsePrice: %t, CacheCreationRatio: %f, ShouldPreConsumedQuota: %d", p.ModelPrice, p.ModelRatio, p.CompletionRatio, p.CacheRatio, p.GroupRatio, p.UsePrice, p.CacheCreationRatio, p.ShouldPreConsumedQuota) +} + func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens int, maxTokens int) (PriceData, error) { modelPrice, usePrice := operation_setting.GetModelPrice(info.OriginModelName, false) groupRatio := setting.GetGroupRatio(info.Group) @@ -26,6 +32,7 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens var modelRatio float64 var completionRatio float64 var cacheRatio float64 + var cacheCreationRatio float64 if !usePrice { preConsumedTokens := common.PreConsumedQuota if maxTokens != 0 { @@ -34,26 +41,52 @@ func ModelPriceHelper(c *gin.Context, info *relaycommon.RelayInfo, promptTokens var success bool modelRatio, success = operation_setting.GetModelRatio(info.OriginModelName) if !success { - if info.UserId == 1 { - return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName) - } else { - return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置, 请联系管理员设置;Model %s ratio or price not set, please contact administrator to set", info.OriginModelName, info.OriginModelName) + acceptUnsetRatio := false + if accept, ok := info.UserSetting[constant2.UserAcceptUnsetRatioModel]; ok { + b, ok := accept.(bool) + if ok { + acceptUnsetRatio = b + } + } + if !acceptUnsetRatio { + return PriceData{}, fmt.Errorf("模型 %s 倍率或价格未配置,请联系管理员设置或开始自用模式;Model %s ratio or price not set, please set or start self-use mode", info.OriginModelName, info.OriginModelName) } } completionRatio = operation_setting.GetCompletionRatio(info.OriginModelName) cacheRatio, _ = operation_setting.GetCacheRatio(info.OriginModelName) + cacheCreationRatio, _ = operation_setting.GetCreateCacheRatio(info.OriginModelName) ratio := modelRatio * groupRatio preConsumedQuota = int(float64(preConsumedTokens) * ratio) } else { preConsumedQuota = int(modelPrice * common.QuotaPerUnit * groupRatio) } - return PriceData{ + + priceData := PriceData{ ModelPrice: modelPrice, ModelRatio: modelRatio, CompletionRatio: completionRatio, GroupRatio: groupRatio, UsePrice: usePrice, CacheRatio: cacheRatio, + CacheCreationRatio: cacheCreationRatio, ShouldPreConsumedQuota: preConsumedQuota, - }, nil + } + + if common.DebugEnabled { + println(fmt.Sprintf("model_price_helper result: %s", priceData.ToSetting())) + } + + return priceData, nil +} + +func ContainPriceOrRatio(modelName string) bool { + _, ok := operation_setting.GetModelPrice(modelName, false) + if ok { + return true + } + _, ok = operation_setting.GetModelRatio(modelName) + if ok { + return true + } + return false } diff --git a/relay/helper/stream_scanner.go b/relay/helper/stream_scanner.go index 7a7507f5..abb98f42 100644 --- a/relay/helper/stream_scanner.go +++ b/relay/helper/stream_scanner.go @@ -3,20 +3,29 @@ package helper import ( "bufio" "context" + "github.com/bytedance/gopkg/util/gopool" "io" "net/http" "one-api/common" "one-api/constant" relaycommon "one-api/relay/common" + "one-api/setting/operation_setting" "strings" + "sync" "time" "github.com/gin-gonic/gin" ) +const ( + InitialScannerBufferSize = 1 << 20 // 1MB (1*1024*1024) + MaxScannerBufferSize = 10 << 20 // 10MB (10*1024*1024) + DefaultPingInterval = 10 * time.Second +) + func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon.RelayInfo, dataHandler func(data string) bool) { - if resp == nil { + if resp == nil || dataHandler == nil { return } @@ -29,16 +38,32 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon } var ( - stopChan = make(chan bool, 2) - scanner = bufio.NewScanner(resp.Body) - ticker = time.NewTicker(streamingTimeout) + stopChan = make(chan bool, 2) + scanner = bufio.NewScanner(resp.Body) + ticker = time.NewTicker(streamingTimeout) + pingTicker *time.Ticker + writeMutex sync.Mutex // Mutex to protect concurrent writes ) + generalSettings := operation_setting.GetGeneralSetting() + pingEnabled := generalSettings.PingIntervalEnabled + pingInterval := time.Duration(generalSettings.PingIntervalSeconds) * time.Second + if pingInterval <= 0 { + pingInterval = DefaultPingInterval + } + + if pingEnabled { + pingTicker = time.NewTicker(pingInterval) + } + defer func() { ticker.Stop() + if pingTicker != nil { + pingTicker.Stop() + } close(stopChan) }() - + scanner.Buffer(make([]byte, InitialScannerBufferSize), MaxScannerBufferSize) scanner.Split(bufio.ScanLines) SetEventStreamHeaders(c) @@ -46,6 +71,34 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon defer cancel() ctx = context.WithValue(ctx, "stop_chan", stopChan) + + // Handle ping data sending + if pingEnabled && pingTicker != nil { + gopool.Go(func() { + for { + select { + case <-pingTicker.C: + writeMutex.Lock() // Lock before writing + err := PingData(c) + writeMutex.Unlock() // Unlock after writing + if err != nil { + common.LogError(c, "ping data error: "+err.Error()) + common.SafeSendBool(stopChan, true) + return + } + if common.DebugEnabled { + println("ping data sent") + } + case <-ctx.Done(): + if common.DebugEnabled { + println("ping data goroutine stopped") + } + return + } + } + }) + } + common.RelayCtxGo(ctx, func() { for scanner.Scan() { ticker.Reset(streamingTimeout) @@ -65,7 +118,9 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon data = strings.TrimSuffix(data, "\"") if !strings.HasPrefix(data, "[DONE]") { info.SetFirstResponseTime() + writeMutex.Lock() // Lock before writing success := dataHandler(data) + writeMutex.Unlock() // Unlock after writing if !success { break } @@ -85,7 +140,9 @@ func StreamScannerHandler(c *gin.Context, resp *http.Response, info *relaycommon case <-ticker.C: // 超时处理逻辑 common.LogError(c, "streaming timeout") + common.SafeSendBool(stopChan, true) case <-stopChan: // 正常结束 + common.LogInfo(c, "streaming finished") } } diff --git a/relay/relay-audio.go b/relay/relay-audio.go index b77ee80e..deb45c58 100644 --- a/relay/relay-audio.go +++ b/relay/relay-audio.go @@ -117,7 +117,7 @@ func AudioHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) { if resp != nil { httpResp = resp.(*http.Response) if httpResp.StatusCode != http.StatusOK { - openaiErr = service.RelayErrorHandler(httpResp) + openaiErr = service.RelayErrorHandler(httpResp, false) // reset status code 重置状态码 service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr diff --git a/relay/relay-image.go b/relay/relay-image.go index 90b423f9..f9f542a7 100644 --- a/relay/relay-image.go +++ b/relay/relay-image.go @@ -155,7 +155,7 @@ func ImageHelper(c *gin.Context) *dto.OpenAIErrorWithStatusCode { httpResp = resp.(*http.Response) relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream") if httpResp.StatusCode != http.StatusOK { - openaiErr := service.RelayErrorHandler(httpResp) + openaiErr := service.RelayErrorHandler(httpResp, false) // reset status code 重置状态码 service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr diff --git a/relay/relay-text.go b/relay/relay-text.go index b1c9d515..7b2b7fc0 100644 --- a/relay/relay-text.go +++ b/relay/relay-text.go @@ -17,6 +17,7 @@ import ( "one-api/relay/helper" "one-api/service" "one-api/setting" + "one-api/setting/model_setting" "strings" "time" @@ -108,7 +109,7 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) { c.Set("prompt_tokens", promptTokens) } - priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(textRequest.MaxTokens)) + priceData, err := helper.ModelPriceHelper(c, relayInfo, promptTokens, int(math.Max(float64(textRequest.MaxTokens), float64(textRequest.MaxCompletionTokens)))) if err != nil { return service.OpenAIErrorWrapperLocal(err, "model_price_error", http.StatusInternalServerError) } @@ -152,38 +153,57 @@ func TextHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) { adaptor.Init(relayInfo) var requestBody io.Reader - //if relayInfo.ChannelType == common.ChannelTypeOpenAI && !isModelMapped { - // body, err := common.GetRequestBody(c) - // if err != nil { - // return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError) - // } - // requestBody = bytes.NewBuffer(body) - //} else { - // - //} + if model_setting.GetGlobalSettings().PassThroughRequestEnabled { + body, err := common.GetRequestBody(c) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "get_request_body_failed", http.StatusInternalServerError) + } + requestBody = bytes.NewBuffer(body) + } else { + convertedRequest, err := adaptor.ConvertOpenAIRequest(c, relayInfo, textRequest) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError) + } + jsonData, err := json.Marshal(convertedRequest) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError) + } - convertedRequest, err := adaptor.ConvertRequest(c, relayInfo, textRequest) - if err != nil { - return service.OpenAIErrorWrapperLocal(err, "convert_request_failed", http.StatusInternalServerError) - } - jsonData, err := json.Marshal(convertedRequest) - if err != nil { - return service.OpenAIErrorWrapperLocal(err, "json_marshal_failed", http.StatusInternalServerError) - } - requestBody = bytes.NewBuffer(jsonData) + // apply param override + if len(relayInfo.ParamOverride) > 0 { + reqMap := make(map[string]interface{}) + err = json.Unmarshal(jsonData, &reqMap) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "param_override_unmarshal_failed", http.StatusInternalServerError) + } + for key, value := range relayInfo.ParamOverride { + reqMap[key] = value + } + jsonData, err = json.Marshal(reqMap) + if err != nil { + return service.OpenAIErrorWrapperLocal(err, "param_override_marshal_failed", http.StatusInternalServerError) + } + } + + if common.DebugEnabled { + println("requestBody: ", string(jsonData)) + } + requestBody = bytes.NewBuffer(jsonData) + } - statusCodeMappingStr := c.GetString("status_code_mapping") var httpResp *http.Response resp, err := adaptor.DoRequest(c, relayInfo, requestBody) if err != nil { return service.OpenAIErrorWrapper(err, "do_request_failed", http.StatusInternalServerError) } + statusCodeMappingStr := c.GetString("status_code_mapping") + if resp != nil { httpResp = resp.(*http.Response) relayInfo.IsStream = relayInfo.IsStream || strings.HasPrefix(httpResp.Header.Get("Content-Type"), "text/event-stream") if httpResp.StatusCode != http.StatusOK { - openaiErr = service.RelayErrorHandler(httpResp) + openaiErr = service.RelayErrorHandler(httpResp, false) // reset status code 重置状态码 service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr @@ -369,17 +389,18 @@ func postConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+ "tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota)) } else { - quotaDelta := quota - preConsumedQuota - if quotaDelta != 0 { - err := service.PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true) - if err != nil { - common.LogError(ctx, "error consuming token remain quota: "+err.Error()) - } - } model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota) model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota) } + quotaDelta := quota - preConsumedQuota + if quotaDelta != 0 { + err := service.PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true) + if err != nil { + common.LogError(ctx, "error consuming token remain quota: "+err.Error()) + } + } + logModel := modelName if strings.HasPrefix(logModel, "gpt-4-gizmo") { logModel = "gpt-4-gizmo-*" diff --git a/relay/relay_adaptor.go b/relay/relay_adaptor.go index 00cff316..8b4afcb3 100644 --- a/relay/relay_adaptor.go +++ b/relay/relay_adaptor.go @@ -18,7 +18,6 @@ import ( "one-api/relay/channel/mokaai" "one-api/relay/channel/ollama" "one-api/relay/channel/openai" - "one-api/relay/channel/openrouter" "one-api/relay/channel/palm" "one-api/relay/channel/perplexity" "one-api/relay/channel/siliconflow" @@ -26,6 +25,7 @@ import ( "one-api/relay/channel/tencent" "one-api/relay/channel/vertex" "one-api/relay/channel/volcengine" + "one-api/relay/channel/xai" "one-api/relay/channel/xunfei" "one-api/relay/channel/zhipu" "one-api/relay/channel/zhipu_4v" @@ -34,8 +34,6 @@ import ( func GetAdaptor(apiType int) channel.Adaptor { switch apiType { - //case constant.APITypeAIProxyLibrary: - // return &aiproxy.Adaptor{} case constant.APITypeAli: return &ali.Adaptor{} case constant.APITypeAnthropic: @@ -85,7 +83,11 @@ func GetAdaptor(apiType int) channel.Adaptor { case constant.APITypeBaiduV2: return &baidu_v2.Adaptor{} case constant.APITypeOpenRouter: - return &openrouter.Adaptor{} + return &openai.Adaptor{} + case constant.APITypeXinference: + return &openai.Adaptor{} + case constant.APITypeXai: + return &xai.Adaptor{} } return nil } diff --git a/relay/relay_embedding.go b/relay/relay_embedding.go index e5bfa863..b4909849 100644 --- a/relay/relay_embedding.go +++ b/relay/relay_embedding.go @@ -98,7 +98,7 @@ func EmbeddingHelper(c *gin.Context) (openaiErr *dto.OpenAIErrorWithStatusCode) if resp != nil { httpResp = resp.(*http.Response) if httpResp.StatusCode != http.StatusOK { - openaiErr = service.RelayErrorHandler(httpResp) + openaiErr = service.RelayErrorHandler(httpResp, false) // reset status code 重置状态码 service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr diff --git a/relay/relay_rerank.go b/relay/relay_rerank.go index a3761387..6ca98de7 100644 --- a/relay/relay_rerank.go +++ b/relay/relay_rerank.go @@ -25,7 +25,6 @@ func getRerankPromptToken(rerankRequest dto.RerankRequest) int { } func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWithStatusCode) { - relayInfo := relaycommon.GenRelayInfo(c) var rerankRequest *dto.RerankRequest err := common.UnmarshalBodyReusable(c, &rerankRequest) @@ -33,6 +32,9 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith common.LogError(c, fmt.Sprintf("getAndValidateTextRequest failed: %s", err.Error())) return service.OpenAIErrorWrapperLocal(err, "invalid_text_request", http.StatusBadRequest) } + + relayInfo := relaycommon.GenRelayInfoRerank(c, rerankRequest) + if rerankRequest.Query == "" { return service.OpenAIErrorWrapperLocal(fmt.Errorf("query is empty"), "invalid_query", http.StatusBadRequest) } @@ -90,7 +92,7 @@ func RerankHelper(c *gin.Context, relayMode int) (openaiErr *dto.OpenAIErrorWith if resp != nil { httpResp = resp.(*http.Response) if httpResp.StatusCode != http.StatusOK { - openaiErr = service.RelayErrorHandler(httpResp) + openaiErr = service.RelayErrorHandler(httpResp, false) // reset status code 重置状态码 service.ResetStatusCode(openaiErr, statusCodeMappingStr) return openaiErr diff --git a/router/api-router.go b/router/api-router.go index bc3f5d9f..1720ff57 100644 --- a/router/api-router.go +++ b/router/api-router.go @@ -13,6 +13,8 @@ func SetApiRouter(router *gin.Engine) { apiRouter.Use(gzip.Gzip(gzip.DefaultCompression)) apiRouter.Use(middleware.GlobalAPIRateLimit()) { + apiRouter.GET("/setup", controller.GetSetup) + apiRouter.POST("/setup", controller.PostSetup) apiRouter.GET("/status", controller.GetStatus) apiRouter.GET("/models", middleware.UserAuth(), controller.DashboardListModels) apiRouter.GET("/status/test", middleware.AdminAuth(), controller.TestStatus) @@ -25,6 +27,7 @@ func SetApiRouter(router *gin.Engine) { apiRouter.GET("/reset_password", middleware.CriticalRateLimit(), middleware.TurnstileCheck(), controller.SendPasswordResetEmail) apiRouter.POST("/user/reset", middleware.CriticalRateLimit(), controller.ResetPassword) apiRouter.GET("/oauth/github", middleware.CriticalRateLimit(), controller.GitHubOAuth) + apiRouter.GET("/oauth/oidc", middleware.CriticalRateLimit(), controller.OidcAuth) apiRouter.GET("/oauth/linuxdo", middleware.CriticalRateLimit(), controller.LinuxdoOAuth) apiRouter.GET("/oauth/state", middleware.CriticalRateLimit(), controller.GenerateOAuthCode) apiRouter.GET("/oauth/wechat", middleware.CriticalRateLimit(), controller.WeChatAuth) diff --git a/router/relay-router.go b/router/relay-router.go index 32e0c682..3a9122d4 100644 --- a/router/relay-router.go +++ b/router/relay-router.go @@ -35,6 +35,7 @@ func SetRelayRouter(router *gin.Engine) { //http router httpRouter := relayV1Router.Group("") httpRouter.Use(middleware.Distribute()) + httpRouter.POST("/messages", controller.RelayClaude) httpRouter.POST("/completions", controller.Relay) httpRouter.POST("/chat/completions", controller.Relay) httpRouter.POST("/edits", controller.Relay) diff --git a/service/convert.go b/service/convert.go new file mode 100644 index 00000000..cc462b40 --- /dev/null +++ b/service/convert.go @@ -0,0 +1,406 @@ +package service + +import ( + "encoding/json" + "fmt" + "one-api/common" + "one-api/dto" + relaycommon "one-api/relay/common" + "strings" +) + +func ClaudeToOpenAIRequest(claudeRequest dto.ClaudeRequest, info *relaycommon.RelayInfo) (*dto.GeneralOpenAIRequest, error) { + openAIRequest := dto.GeneralOpenAIRequest{ + Model: claudeRequest.Model, + MaxTokens: claudeRequest.MaxTokens, + Temperature: claudeRequest.Temperature, + TopP: claudeRequest.TopP, + Stream: claudeRequest.Stream, + } + + if claudeRequest.Thinking != nil { + if strings.HasSuffix(info.OriginModelName, "-thinking") && + !strings.HasSuffix(claudeRequest.Model, "-thinking") { + openAIRequest.Model = openAIRequest.Model + "-thinking" + } + } + + // Convert stop sequences + if len(claudeRequest.StopSequences) == 1 { + openAIRequest.Stop = claudeRequest.StopSequences[0] + } else if len(claudeRequest.StopSequences) > 1 { + openAIRequest.Stop = claudeRequest.StopSequences + } + + // Convert tools + tools, _ := common.Any2Type[[]dto.Tool](claudeRequest.Tools) + openAITools := make([]dto.ToolCallRequest, 0) + for _, claudeTool := range tools { + openAITool := dto.ToolCallRequest{ + Type: "function", + Function: dto.FunctionRequest{ + Name: claudeTool.Name, + Description: claudeTool.Description, + Parameters: claudeTool.InputSchema, + }, + } + openAITools = append(openAITools, openAITool) + } + openAIRequest.Tools = openAITools + + // Convert messages + openAIMessages := make([]dto.Message, 0) + + // Add system message if present + if claudeRequest.System != nil { + if claudeRequest.IsStringSystem() && claudeRequest.GetStringSystem() != "" { + openAIMessage := dto.Message{ + Role: "system", + } + openAIMessage.SetStringContent(claudeRequest.GetStringSystem()) + openAIMessages = append(openAIMessages, openAIMessage) + } else { + systems := claudeRequest.ParseSystem() + if len(systems) > 0 { + systemStr := "" + openAIMessage := dto.Message{ + Role: "system", + } + for _, system := range systems { + if system.Text != nil { + systemStr += *system.Text + } + } + openAIMessage.SetStringContent(systemStr) + openAIMessages = append(openAIMessages, openAIMessage) + } + } + } + for _, claudeMessage := range claudeRequest.Messages { + openAIMessage := dto.Message{ + Role: claudeMessage.Role, + } + + //log.Printf("claudeMessage.Content: %v", claudeMessage.Content) + if claudeMessage.IsStringContent() { + openAIMessage.SetStringContent(claudeMessage.GetStringContent()) + } else { + content, err := claudeMessage.ParseContent() + if err != nil { + return nil, err + } + contents := content + var toolCalls []dto.ToolCallRequest + mediaMessages := make([]dto.MediaContent, 0, len(contents)) + + for _, mediaMsg := range contents { + switch mediaMsg.Type { + case "text": + message := dto.MediaContent{ + Type: "text", + Text: mediaMsg.GetText(), + } + mediaMessages = append(mediaMessages, message) + case "image": + // Handle image conversion (base64 to URL or keep as is) + imageData := fmt.Sprintf("data:%s;base64,%s", mediaMsg.Source.MediaType, mediaMsg.Source.Data) + //textContent += fmt.Sprintf("[Image: %s]", imageData) + mediaMessage := dto.MediaContent{ + Type: "image_url", + ImageUrl: &dto.MessageImageUrl{Url: imageData}, + } + mediaMessages = append(mediaMessages, mediaMessage) + case "tool_use": + toolCall := dto.ToolCallRequest{ + ID: mediaMsg.Id, + Type: "function", + Function: dto.FunctionRequest{ + Name: mediaMsg.Name, + Arguments: toJSONString(mediaMsg.Input), + }, + } + toolCalls = append(toolCalls, toolCall) + case "tool_result": + // Add tool result as a separate message + oaiToolMessage := dto.Message{ + Role: "tool", + Name: &mediaMsg.Name, + ToolCallId: mediaMsg.ToolUseId, + } + //oaiToolMessage.SetStringContent(*mediaMsg.GetMediaContent().Text) + if mediaMsg.IsStringContent() { + oaiToolMessage.SetStringContent(mediaMsg.GetStringContent()) + } else { + mediaContents := mediaMsg.ParseMediaContent() + encodeJson, _ := common.EncodeJson(mediaContents) + oaiToolMessage.SetStringContent(string(encodeJson)) + } + openAIMessages = append(openAIMessages, oaiToolMessage) + } + } + + if len(toolCalls) > 0 { + openAIMessage.SetToolCalls(toolCalls) + } + + if len(mediaMessages) > 0 && len(toolCalls) == 0 { + openAIMessage.SetMediaContent(mediaMessages) + } + } + if len(openAIMessage.ParseContent()) > 0 || len(openAIMessage.ToolCalls) > 0 { + openAIMessages = append(openAIMessages, openAIMessage) + } + } + + openAIRequest.Messages = openAIMessages + + return &openAIRequest, nil +} + +func OpenAIErrorToClaudeError(openAIError *dto.OpenAIErrorWithStatusCode) *dto.ClaudeErrorWithStatusCode { + claudeError := dto.ClaudeError{ + Type: "new_api_error", + Message: openAIError.Error.Message, + } + return &dto.ClaudeErrorWithStatusCode{ + Error: claudeError, + StatusCode: openAIError.StatusCode, + } +} + +func ClaudeErrorToOpenAIError(claudeError *dto.ClaudeErrorWithStatusCode) *dto.OpenAIErrorWithStatusCode { + openAIError := dto.OpenAIError{ + Message: claudeError.Error.Message, + Type: "new_api_error", + } + return &dto.OpenAIErrorWithStatusCode{ + Error: openAIError, + StatusCode: claudeError.StatusCode, + } +} + +func generateStopBlock(index int) *dto.ClaudeResponse { + return &dto.ClaudeResponse{ + Type: "content_block_stop", + Index: common.GetPointer[int](index), + } +} + +func StreamResponseOpenAI2Claude(openAIResponse *dto.ChatCompletionsStreamResponse, info *relaycommon.RelayInfo) []*dto.ClaudeResponse { + var claudeResponses []*dto.ClaudeResponse + if info.SendResponseCount == 1 { + msg := &dto.ClaudeMediaMessage{ + Id: openAIResponse.Id, + Model: openAIResponse.Model, + Type: "message", + Role: "assistant", + Usage: &dto.ClaudeUsage{ + InputTokens: info.PromptTokens, + OutputTokens: 0, + }, + } + msg.SetContent(make([]any, 0)) + claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + Type: "message_start", + Message: msg, + }) + claudeResponses = append(claudeResponses) + //claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + // Type: "ping", + //}) + if openAIResponse.IsToolCall() { + resp := &dto.ClaudeResponse{ + Type: "content_block_start", + ContentBlock: &dto.ClaudeMediaMessage{ + Id: openAIResponse.GetFirstToolCall().ID, + Type: "tool_use", + Name: openAIResponse.GetFirstToolCall().Function.Name, + }, + } + resp.SetIndex(0) + claudeResponses = append(claudeResponses, resp) + } else { + //resp := &dto.ClaudeResponse{ + // Type: "content_block_start", + // ContentBlock: &dto.ClaudeMediaMessage{ + // Type: "text", + // Text: common.GetPointer[string](""), + // }, + //} + //resp.SetIndex(0) + //claudeResponses = append(claudeResponses, resp) + } + return claudeResponses + } + + if len(openAIResponse.Choices) == 0 { + // no choices + // TODO: handle this case + return claudeResponses + } else { + chosenChoice := openAIResponse.Choices[0] + if chosenChoice.FinishReason != nil && *chosenChoice.FinishReason != "" { + // should be done + info.FinishReason = *chosenChoice.FinishReason + return claudeResponses + } + if info.Done { + claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) + if info.ClaudeConvertInfo.Usage != nil { + claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + Type: "message_delta", + Usage: &dto.ClaudeUsage{ + InputTokens: info.ClaudeConvertInfo.Usage.PromptTokens, + OutputTokens: info.ClaudeConvertInfo.Usage.CompletionTokens, + }, + Delta: &dto.ClaudeMediaMessage{ + StopReason: common.GetPointer[string](stopReasonOpenAI2Claude(info.FinishReason)), + }, + }) + } + claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + Type: "message_stop", + }) + } else { + var claudeResponse dto.ClaudeResponse + var isEmpty bool + claudeResponse.Type = "content_block_delta" + if len(chosenChoice.Delta.ToolCalls) > 0 { + if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeTools { + claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) + info.ClaudeConvertInfo.Index++ + claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + Index: &info.ClaudeConvertInfo.Index, + Type: "content_block_start", + ContentBlock: &dto.ClaudeMediaMessage{ + Id: openAIResponse.GetFirstToolCall().ID, + Type: "tool_use", + Name: openAIResponse.GetFirstToolCall().Function.Name, + Input: map[string]interface{}{}, + }, + }) + } + info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeTools + // tools delta + claudeResponse.Delta = &dto.ClaudeMediaMessage{ + Type: "input_json_delta", + PartialJson: &chosenChoice.Delta.ToolCalls[0].Function.Arguments, + } + } else { + reasoning := chosenChoice.Delta.GetReasoningContent() + textContent := chosenChoice.Delta.GetContentString() + if reasoning != "" || textContent != "" { + if reasoning != "" { + if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeThinking { + //info.ClaudeConvertInfo.Index++ + claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + Index: &info.ClaudeConvertInfo.Index, + Type: "content_block_start", + ContentBlock: &dto.ClaudeMediaMessage{ + Type: "thinking", + Thinking: "", + }, + }) + } + info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeThinking + // text delta + claudeResponse.Delta = &dto.ClaudeMediaMessage{ + Type: "thinking_delta", + Thinking: reasoning, + } + } else { + if info.ClaudeConvertInfo.LastMessagesType != relaycommon.LastMessageTypeText { + if info.LastMessagesType == relaycommon.LastMessageTypeThinking || info.LastMessagesType == relaycommon.LastMessageTypeTools { + claudeResponses = append(claudeResponses, generateStopBlock(info.ClaudeConvertInfo.Index)) + info.ClaudeConvertInfo.Index++ + } + claudeResponses = append(claudeResponses, &dto.ClaudeResponse{ + Index: &info.ClaudeConvertInfo.Index, + Type: "content_block_start", + ContentBlock: &dto.ClaudeMediaMessage{ + Type: "text", + Text: common.GetPointer[string](""), + }, + }) + } + info.ClaudeConvertInfo.LastMessagesType = relaycommon.LastMessageTypeText + // text delta + claudeResponse.Delta = &dto.ClaudeMediaMessage{ + Type: "text_delta", + Text: common.GetPointer[string](textContent), + } + } + } else { + isEmpty = true + } + } + claudeResponse.Index = &info.ClaudeConvertInfo.Index + if !isEmpty { + claudeResponses = append(claudeResponses, &claudeResponse) + } + } + } + + return claudeResponses +} + +func ResponseOpenAI2Claude(openAIResponse *dto.OpenAITextResponse, info *relaycommon.RelayInfo) *dto.ClaudeResponse { + var stopReason string + contents := make([]dto.ClaudeMediaMessage, 0) + claudeResponse := &dto.ClaudeResponse{ + Id: openAIResponse.Id, + Type: "message", + Role: "assistant", + Model: openAIResponse.Model, + } + for _, choice := range openAIResponse.Choices { + stopReason = stopReasonOpenAI2Claude(choice.FinishReason) + claudeContent := dto.ClaudeMediaMessage{} + if choice.FinishReason == "tool_calls" { + claudeContent.Type = "tool_use" + claudeContent.Id = choice.Message.ToolCallId + claudeContent.Name = choice.Message.ParseToolCalls()[0].Function.Name + var mapParams map[string]interface{} + if err := json.Unmarshal([]byte(choice.Message.ParseToolCalls()[0].Function.Arguments), &mapParams); err == nil { + claudeContent.Input = mapParams + } else { + claudeContent.Input = choice.Message.ParseToolCalls()[0].Function.Arguments + } + } else { + claudeContent.Type = "text" + claudeContent.SetText(choice.Message.StringContent()) + } + contents = append(contents, claudeContent) + } + claudeResponse.Content = contents + claudeResponse.StopReason = stopReason + claudeResponse.Usage = &dto.ClaudeUsage{ + InputTokens: openAIResponse.PromptTokens, + OutputTokens: openAIResponse.CompletionTokens, + } + + return claudeResponse +} + +func stopReasonOpenAI2Claude(reason string) string { + switch reason { + case "stop": + return "end_turn" + case "stop_sequence": + return "stop_sequence" + case "max_tokens": + return "max_tokens" + case "tool_calls": + return "tool_use" + default: + return reason + } +} + +func toJSONString(v interface{}) string { + b, err := json.Marshal(v) + if err != nil { + return "{}" + } + return string(b) +} diff --git a/service/error.go b/service/error.go index c7601349..1bf5992b 100644 --- a/service/error.go +++ b/service/error.go @@ -50,7 +50,30 @@ func OpenAIErrorWrapperLocal(err error, code string, statusCode int) *dto.OpenAI return openaiErr } -func RelayErrorHandler(resp *http.Response) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) { +func ClaudeErrorWrapper(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode { + text := err.Error() + lowerText := strings.ToLower(text) + if strings.Contains(lowerText, "post") || strings.Contains(lowerText, "dial") || strings.Contains(lowerText, "http") { + common.SysLog(fmt.Sprintf("error: %s", text)) + text = "请求上游地址失败" + } + claudeError := dto.ClaudeError{ + Message: text, + Type: "new_api_error", + } + return &dto.ClaudeErrorWithStatusCode{ + Error: claudeError, + StatusCode: statusCode, + } +} + +func ClaudeErrorWrapperLocal(err error, code string, statusCode int) *dto.ClaudeErrorWithStatusCode { + claudeErr := ClaudeErrorWrapper(err, code, statusCode) + claudeErr.LocalError = true + return claudeErr +} + +func RelayErrorHandler(resp *http.Response, showBodyWhenFail bool) (errWithStatusCode *dto.OpenAIErrorWithStatusCode) { errWithStatusCode = &dto.OpenAIErrorWithStatusCode{ StatusCode: resp.StatusCode, Error: dto.OpenAIError{ @@ -70,6 +93,11 @@ func RelayErrorHandler(resp *http.Response) (errWithStatusCode *dto.OpenAIErrorW var errResponse dto.GeneralErrorResponse err = json.Unmarshal(responseBody, &errResponse) if err != nil { + if showBodyWhenFail { + errWithStatusCode.Error.Message = string(responseBody) + } else { + errWithStatusCode.Error.Message = fmt.Sprintf("bad response status code %d", resp.StatusCode) + } return } if errResponse.Error.Message != "" { diff --git a/service/file_decoder.go b/service/file_decoder.go index ac9f00f3..bbb188f8 100644 --- a/service/file_decoder.go +++ b/service/file_decoder.go @@ -8,9 +8,9 @@ import ( "one-api/dto" ) -var maxFileSize = constant.MaxFileDownloadMB * 1024 * 1024 - func GetFileBase64FromUrl(url string) (*dto.LocalFileData, error) { + var maxFileSize = constant.MaxFileDownloadMB * 1024 * 1024 + resp, err := DoDownloadRequest(url) if err != nil { return nil, err @@ -22,7 +22,6 @@ func GetFileBase64FromUrl(url string) (*dto.LocalFileData, error) { if err != nil { return nil, err } - // Check actual size after reading if len(fileBytes) > maxFileSize { return nil, fmt.Errorf("file size exceeds maximum allowed size: %dMB", constant.MaxFileDownloadMB) diff --git a/service/log_info_generate.go b/service/log_info_generate.go index 6406cbe1..75457b97 100644 --- a/service/log_info_generate.go +++ b/service/log_info_generate.go @@ -53,3 +53,12 @@ func GenerateAudioOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, info["audio_completion_ratio"] = audioCompletionRatio return info } + +func GenerateClaudeOtherInfo(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, modelRatio, groupRatio, completionRatio float64, + cacheTokens int, cacheRatio float64, cacheCreationTokens int, cacheCreationRatio float64, modelPrice float64) map[string]interface{} { + info := GenerateTextOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, cacheTokens, cacheRatio, modelPrice) + info["claude"] = true + info["cache_creation_tokens"] = cacheCreationTokens + info["cache_creation_ratio"] = cacheCreationRatio + return info +} diff --git a/service/quota.go b/service/quota.go index e19f1b82..0d11b4a0 100644 --- a/service/quota.go +++ b/service/quota.go @@ -194,6 +194,73 @@ func PostWssConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, mod tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other) } +func PostClaudeConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, + usage *dto.Usage, preConsumedQuota int, userQuota int, priceData helper.PriceData, extraContent string) { + + useTimeSeconds := time.Now().Unix() - relayInfo.StartTime.Unix() + promptTokens := usage.PromptTokens + completionTokens := usage.CompletionTokens + modelName := relayInfo.OriginModelName + + tokenName := ctx.GetString("token_name") + completionRatio := priceData.CompletionRatio + modelRatio := priceData.ModelRatio + groupRatio := priceData.GroupRatio + modelPrice := priceData.ModelPrice + + cacheRatio := priceData.CacheRatio + cacheTokens := usage.PromptTokensDetails.CachedTokens + + cacheCreationRatio := priceData.CacheCreationRatio + cacheCreationTokens := usage.PromptTokensDetails.CachedCreationTokens + + calculateQuota := 0.0 + if !priceData.UsePrice { + calculateQuota = float64(promptTokens) + calculateQuota += float64(cacheTokens) * cacheRatio + calculateQuota += float64(cacheCreationTokens) * cacheCreationRatio + calculateQuota += float64(completionTokens) * completionRatio + calculateQuota = calculateQuota * groupRatio * modelRatio + } else { + calculateQuota = modelPrice * common.QuotaPerUnit * groupRatio + } + + if modelRatio != 0 && calculateQuota <= 0 { + calculateQuota = 1 + } + + quota := int(calculateQuota) + + totalTokens := promptTokens + completionTokens + + var logContent string + // record all the consume log even if quota is 0 + if totalTokens == 0 { + // in this case, must be some error happened + // we cannot just return, because we may have to return the pre-consumed quota + quota = 0 + logContent += fmt.Sprintf("(可能是上游出错)") + common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+ + "tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, modelName, preConsumedQuota)) + } else { + model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota) + model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota) + } + + quotaDelta := quota - preConsumedQuota + if quotaDelta != 0 { + err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true) + if err != nil { + common.LogError(ctx, "error consuming token remain quota: "+err.Error()) + } + } + + other := GenerateClaudeOtherInfo(ctx, relayInfo, modelRatio, groupRatio, completionRatio, + cacheTokens, cacheRatio, cacheCreationTokens, cacheCreationRatio, modelPrice) + model.RecordConsumeLog(ctx, relayInfo.UserId, relayInfo.ChannelId, promptTokens, completionTokens, modelName, + tokenName, quota, logContent, relayInfo.TokenId, userQuota, int(useTimeSeconds), relayInfo.IsStream, relayInfo.Group, other) +} + func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, usage *dto.Usage, preConsumedQuota int, userQuota int, priceData helper.PriceData, extraContent string) { @@ -249,17 +316,18 @@ func PostAudioConsumeQuota(ctx *gin.Context, relayInfo *relaycommon.RelayInfo, common.LogError(ctx, fmt.Sprintf("total tokens is 0, cannot consume quota, userId %d, channelId %d, "+ "tokenId %d, model %s, pre-consumed quota %d", relayInfo.UserId, relayInfo.ChannelId, relayInfo.TokenId, relayInfo.OriginModelName, preConsumedQuota)) } else { - quotaDelta := quota - preConsumedQuota - if quotaDelta != 0 { - err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true) - if err != nil { - common.LogError(ctx, "error consuming token remain quota: "+err.Error()) - } - } model.UpdateUserUsedQuotaAndRequestCount(relayInfo.UserId, quota) model.UpdateChannelUsedQuota(relayInfo.ChannelId, quota) } + quotaDelta := quota - preConsumedQuota + if quotaDelta != 0 { + err := PostConsumeQuota(relayInfo, quotaDelta, preConsumedQuota, true) + if err != nil { + common.LogError(ctx, "error consuming token remain quota: "+err.Error()) + } + } + logModel := relayInfo.OriginModelName if extraContent != "" { logContent += ", " + extraContent diff --git a/service/token_counter.go b/service/token_counter.go index a6b8e86a..f3c3b6b0 100644 --- a/service/token_counter.go +++ b/service/token_counter.go @@ -1,6 +1,7 @@ package service import ( + "encoding/json" "errors" "fmt" "image" @@ -42,7 +43,7 @@ func InitTokenEncoders() { } else { tokenEncoderMap[model] = defaultTokenEncoder } - } else if strings.HasPrefix(model, "o1") { + } else if strings.HasPrefix(model, "o") { tokenEncoderMap[model] = o200kTokenEncoder } else { tokenEncoderMap[model] = defaultTokenEncoder @@ -85,6 +86,9 @@ func getTokenNum(tokenEncoder *tiktoken.Tiktoken, text string) int { } func getImageToken(info *relaycommon.RelayInfo, imageUrl *dto.MessageImageUrl, model string, stream bool) (int, error) { + if imageUrl == nil { + return 0, fmt.Errorf("image_url_is_nil") + } baseTokens := 85 if model == "glm-4v" { return 1047, nil @@ -92,10 +96,10 @@ func getImageToken(info *relaycommon.RelayInfo, imageUrl *dto.MessageImageUrl, m if imageUrl.Detail == "low" { return baseTokens, nil } - // TODO: 非流模式下不计算图片token数量 if !constant.GetMediaTokenNotStream && !stream { - return 256, nil + return 3 * baseTokens, nil } + // 同步One API的图片计费逻辑 if imageUrl.Detail == "auto" || imageUrl.Detail == "" { imageUrl.Detail = "high" @@ -125,18 +129,11 @@ func getImageToken(info *relaycommon.RelayInfo, imageUrl *dto.MessageImageUrl, m if err != nil { return 0, err } + imageUrl.MimeType = format if config.Width == 0 || config.Height == 0 { return 0, errors.New(fmt.Sprintf("fail to decode image config: %s", imageUrl.Url)) } - //// TODO: 适配官方auto计费 - //if config.Width < 512 && config.Height < 512 { - // if imageUrl.Detail == "auto" || imageUrl.Detail == "" { - // // 如果图片尺寸小于512,强制使用low - // imageUrl.Detail = "low" - // return 85, nil - // } - //} shortSide := config.Width otherSide := config.Height @@ -192,6 +189,110 @@ func CountTokenChatRequest(info *relaycommon.RelayInfo, request dto.GeneralOpenA return tkm, nil } +func CountTokenClaudeRequest(request dto.ClaudeRequest, model string) (int, error) { + tkm := 0 + + // Count tokens in messages + msgTokens, err := CountTokenClaudeMessages(request.Messages, model, request.Stream) + if err != nil { + return 0, err + } + tkm += msgTokens + + // Count tokens in system message + if request.System != "" { + systemTokens, err := CountTokenInput(request.System, model) + if err != nil { + return 0, err + } + tkm += systemTokens + } + + if request.Tools != nil { + // check is array + if tools, ok := request.Tools.([]any); ok { + if len(tools) > 0 { + parsedTools, err1 := common.Any2Type[[]dto.Tool](request.Tools) + if err1 != nil { + return 0, fmt.Errorf("tools: Input should be a valid list: %v", err) + } + toolTokens, err2 := CountTokenClaudeTools(parsedTools, model) + if err2 != nil { + return 0, fmt.Errorf("tools: %v", err) + } + tkm += toolTokens + } + } else { + return 0, errors.New("tools: Input should be a valid list") + } + } + + return tkm, nil +} + +func CountTokenClaudeMessages(messages []dto.ClaudeMessage, model string, stream bool) (int, error) { + tokenEncoder := getTokenEncoder(model) + tokenNum := 0 + + for _, message := range messages { + // Count tokens for role + tokenNum += getTokenNum(tokenEncoder, message.Role) + if message.IsStringContent() { + tokenNum += getTokenNum(tokenEncoder, message.GetStringContent()) + } else { + content, err := message.ParseContent() + if err != nil { + return 0, err + } + for _, mediaMessage := range content { + switch mediaMessage.Type { + case "text": + tokenNum += getTokenNum(tokenEncoder, mediaMessage.GetText()) + case "image": + //imageTokenNum, err := getClaudeImageToken(mediaMsg.Source, model, stream) + //if err != nil { + // return 0, err + //} + tokenNum += 1000 + case "tool_use": + tokenNum += getTokenNum(tokenEncoder, mediaMessage.Name) + inputJSON, _ := json.Marshal(mediaMessage.Input) + tokenNum += getTokenNum(tokenEncoder, string(inputJSON)) + case "tool_result": + contentJSON, _ := json.Marshal(mediaMessage.Content) + tokenNum += getTokenNum(tokenEncoder, string(contentJSON)) + } + } + } + } + + // Add a constant for message formatting (this may need adjustment based on Claude's exact formatting) + tokenNum += len(messages) * 2 // Assuming 2 tokens per message for formatting + + return tokenNum, nil +} + +func CountTokenClaudeTools(tools []dto.Tool, model string) (int, error) { + tokenEncoder := getTokenEncoder(model) + tokenNum := 0 + + for _, tool := range tools { + tokenNum += getTokenNum(tokenEncoder, tool.Name) + tokenNum += getTokenNum(tokenEncoder, tool.Description) + + schemaJSON, err := json.Marshal(tool.InputSchema) + if err != nil { + return 0, errors.New(fmt.Sprintf("marshal_tool_schema_fail: %s", err.Error())) + } + tokenNum += getTokenNum(tokenEncoder, string(schemaJSON)) + } + + // Add a constant for tool formatting (this may need adjustment based on Claude's exact formatting) + tokenNum += len(tools) * 3 // Assuming 3 tokens per tool for formatting + + return tokenNum, nil +} + func CountTokenRealtime(info *relaycommon.RelayInfo, request dto.RealtimeEvent, model string) (int, int, error) { audioToken := 0 textToken := 0 @@ -287,8 +388,8 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod arrayContent := message.ParseContent() for _, m := range arrayContent { if m.Type == dto.ContentTypeImageURL { - imageUrl := m.ImageUrl.(dto.MessageImageUrl) - imageTokenNum, err := getImageToken(info, &imageUrl, model, stream) + imageUrl := m.GetImageMedia() + imageTokenNum, err := getImageToken(info, imageUrl, model, stream) if err != nil { return 0, err } @@ -297,6 +398,8 @@ func CountTokenMessages(info *relaycommon.RelayInfo, messages []dto.Message, mod } else if m.Type == dto.ContentTypeInputAudio { // TODO: 音频token数量计算 tokenNum += 100 + } else if m.Type == dto.ContentTypeFile { + tokenNum += 5000 } else { tokenNum += getTokenNum(tokenEncoder, m.Text) } diff --git a/setting/group_ratio.go b/setting/group_ratio.go index e715d0a8..8b163625 100644 --- a/setting/group_ratio.go +++ b/setting/group_ratio.go @@ -4,6 +4,7 @@ import ( "encoding/json" "errors" "one-api/common" + "sync" ) var groupRatio = map[string]float64{ @@ -11,8 +12,12 @@ var groupRatio = map[string]float64{ "vip": 1, "svip": 1, } +var groupRatioMutex sync.RWMutex func GetGroupRatioCopy() map[string]float64 { + groupRatioMutex.RLock() + defer groupRatioMutex.RUnlock() + groupRatioCopy := make(map[string]float64) for k, v := range groupRatio { groupRatioCopy[k] = v @@ -21,11 +26,17 @@ func GetGroupRatioCopy() map[string]float64 { } func ContainsGroupRatio(name string) bool { + groupRatioMutex.RLock() + defer groupRatioMutex.RUnlock() + _, ok := groupRatio[name] return ok } func GroupRatio2JSONString() string { + groupRatioMutex.RLock() + defer groupRatioMutex.RUnlock() + jsonBytes, err := json.Marshal(groupRatio) if err != nil { common.SysError("error marshalling model ratio: " + err.Error()) @@ -34,11 +45,17 @@ func GroupRatio2JSONString() string { } func UpdateGroupRatioByJSONString(jsonStr string) error { + groupRatioMutex.Lock() + defer groupRatioMutex.Unlock() + groupRatio = make(map[string]float64) return json.Unmarshal([]byte(jsonStr), &groupRatio) } func GetGroupRatio(name string) float64 { + groupRatioMutex.RLock() + defer groupRatioMutex.RUnlock() + ratio, ok := groupRatio[name] if !ok { common.SysError("group ratio not found: " + name) diff --git a/setting/model_setting/gemini.go b/setting/model_setting/gemini.go index 07e993bc..f132fec8 100644 --- a/setting/model_setting/gemini.go +++ b/setting/model_setting/gemini.go @@ -6,8 +6,11 @@ import ( // GeminiSettings 定义Gemini模型的配置 type GeminiSettings struct { - SafetySettings map[string]string `json:"safety_settings"` - VersionSettings map[string]string `json:"version_settings"` + SafetySettings map[string]string `json:"safety_settings"` + VersionSettings map[string]string `json:"version_settings"` + SupportedImagineModels []string `json:"supported_imagine_models"` + ThinkingAdapterEnabled bool `json:"thinking_adapter_enabled"` + ThinkingAdapterBudgetTokensPercentage float64 `json:"thinking_adapter_budget_tokens_percentage"` } // 默认配置 @@ -20,6 +23,12 @@ var defaultGeminiSettings = GeminiSettings{ "default": "v1beta", "gemini-1.0-pro": "v1", }, + SupportedImagineModels: []string{ + "gemini-2.0-flash-exp-image-generation", + "gemini-2.0-flash-exp", + }, + ThinkingAdapterEnabled: false, + ThinkingAdapterBudgetTokensPercentage: 0.6, } // 全局实例 @@ -50,3 +59,12 @@ func GetGeminiVersionSetting(key string) string { } return geminiSettings.VersionSettings["default"] } + +func IsGeminiModelSupportImagine(model string) bool { + for _, v := range geminiSettings.SupportedImagineModels { + if v == model { + return true + } + } + return false +} diff --git a/setting/model_setting/global.go b/setting/model_setting/global.go new file mode 100644 index 00000000..de2851bb --- /dev/null +++ b/setting/model_setting/global.go @@ -0,0 +1,26 @@ +package model_setting + +import ( + "one-api/setting/config" +) + +type GlobalSettings struct { + PassThroughRequestEnabled bool `json:"pass_through_request_enabled"` +} + +// 默认配置 +var defaultOpenaiSettings = GlobalSettings{ + PassThroughRequestEnabled: false, +} + +// 全局实例 +var globalSettings = defaultOpenaiSettings + +func init() { + // 注册到全局配置管理器 + config.GlobalConfig.Register("global", &globalSettings) +} + +func GetGlobalSettings() *GlobalSettings { + return &globalSettings +} diff --git a/setting/operation_setting/cache_ratio.go b/setting/operation_setting/cache_ratio.go index 98f022ed..dd29eac2 100644 --- a/setting/operation_setting/cache_ratio.go +++ b/setting/operation_setting/cache_ratio.go @@ -7,43 +7,64 @@ import ( ) var defaultCacheRatio = map[string]float64{ - "gpt-4": 0.5, - "o1": 0.5, - "o1-2024-12-17": 0.5, - "o1-preview-2024-09-12": 0.5, - "o1-preview": 0.5, - "o1-mini-2024-09-12": 0.5, - "o1-mini": 0.5, - "gpt-4o-2024-11-20": 0.5, - "gpt-4o-2024-08-06": 0.5, - "gpt-4o": 0.5, - "gpt-4o-mini-2024-07-18": 0.5, - "gpt-4o-mini": 0.5, - "gpt-4o-realtime-preview": 0.5, - "gpt-4o-mini-realtime-preview": 0.5, - "deepseek-chat": 0.1, - "deepseek-reasoner": 0.1, - "deepseek-coder": 0.1, + "gpt-4": 0.5, + "o1": 0.5, + "o1-2024-12-17": 0.5, + "o1-preview-2024-09-12": 0.5, + "o1-preview": 0.5, + "o1-mini-2024-09-12": 0.5, + "o1-mini": 0.5, + "o3-mini": 0.5, + "o3-mini-2025-01-31": 0.5, + "gpt-4o-2024-11-20": 0.5, + "gpt-4o-2024-08-06": 0.5, + "gpt-4o": 0.5, + "gpt-4o-mini-2024-07-18": 0.5, + "gpt-4o-mini": 0.5, + "gpt-4o-realtime-preview": 0.5, + "gpt-4o-mini-realtime-preview": 0.5, + "gpt-4.5-preview": 0.5, + "gpt-4.5-preview-2025-02-27": 0.5, + "deepseek-chat": 0.25, + "deepseek-reasoner": 0.25, + "deepseek-coder": 0.25, + "claude-3-sonnet-20240229": 0.1, + "claude-3-opus-20240229": 0.1, + "claude-3-haiku-20240307": 0.1, + "claude-3-5-haiku-20241022": 0.1, + "claude-3-5-sonnet-20240620": 0.1, + "claude-3-5-sonnet-20241022": 0.1, + "claude-3-7-sonnet-20250219": 0.1, + "claude-3-7-sonnet-20250219-thinking": 0.1, } -var defaultCreateCacheRatio = map[string]float64{} +var defaultCreateCacheRatio = map[string]float64{ + "claude-3-sonnet-20240229": 1.25, + "claude-3-opus-20240229": 1.25, + "claude-3-haiku-20240307": 1.25, + "claude-3-5-haiku-20241022": 1.25, + "claude-3-5-sonnet-20240620": 1.25, + "claude-3-5-sonnet-20241022": 1.25, + "claude-3-7-sonnet-20250219": 1.25, + "claude-3-7-sonnet-20250219-thinking": 1.25, +} + +//var defaultCreateCacheRatio = map[string]float64{} var cacheRatioMap map[string]float64 var cacheRatioMapMutex sync.RWMutex // GetCacheRatioMap returns the cache ratio map func GetCacheRatioMap() map[string]float64 { - cacheRatioMapMutex.Lock() - defer cacheRatioMapMutex.Unlock() - if cacheRatioMap == nil { - cacheRatioMap = defaultCacheRatio - } + cacheRatioMapMutex.RLock() + defer cacheRatioMapMutex.RUnlock() return cacheRatioMap } // CacheRatio2JSONString converts the cache ratio map to a JSON string func CacheRatio2JSONString() string { - GetCacheRatioMap() + cacheRatioMapMutex.RLock() + defer cacheRatioMapMutex.RUnlock() jsonBytes, err := json.Marshal(cacheRatioMap) if err != nil { common.SysError("error marshalling cache ratio: " + err.Error()) @@ -61,24 +82,19 @@ func UpdateCacheRatioByJSONString(jsonStr string) error { // GetCacheRatio returns the cache ratio for a model func GetCacheRatio(name string) (float64, bool) { - GetCacheRatioMap() + cacheRatioMapMutex.RLock() + defer cacheRatioMapMutex.RUnlock() ratio, ok := cacheRatioMap[name] if !ok { - return 1, false // Default to 0.5 if not found + return 1, false // Default to 1 if not found } return ratio, true } -// DefaultCacheRatio2JSONString converts the default cache ratio map to a JSON string -func DefaultCacheRatio2JSONString() string { - jsonBytes, err := json.Marshal(defaultCacheRatio) - if err != nil { - common.SysError("error marshalling default cache ratio: " + err.Error()) +func GetCreateCacheRatio(name string) (float64, bool) { + ratio, ok := defaultCreateCacheRatio[name] + if !ok { + return 1.25, false // Default to 1.25 if not found } - return string(jsonBytes) -} - -// GetDefaultCacheRatioMap returns the default cache ratio map -func GetDefaultCacheRatioMap() map[string]float64 { - return defaultCacheRatio + return ratio, true } diff --git a/setting/operation_setting/general_setting.go b/setting/operation_setting/general_setting.go index 787f0e5f..ae0c436e 100644 --- a/setting/operation_setting/general_setting.go +++ b/setting/operation_setting/general_setting.go @@ -3,12 +3,16 @@ package operation_setting import "one-api/setting/config" type GeneralSetting struct { - DocsLink string `json:"docs_link"` + DocsLink string `json:"docs_link"` + PingIntervalEnabled bool `json:"ping_interval_enabled"` + PingIntervalSeconds int `json:"ping_interval_seconds"` } // 默认配置 var generalSetting = GeneralSetting{ - DocsLink: "https://docs.newapi.pro", + DocsLink: "https://docs.newapi.pro", + PingIntervalEnabled: false, + PingIntervalSeconds: 60, } func init() { diff --git a/setting/operation_setting/model-ratio.go b/setting/operation_setting/model-ratio.go index d9312e6c..6a80ef1a 100644 --- a/setting/operation_setting/model-ratio.go +++ b/setting/operation_setting/model-ratio.go @@ -86,94 +86,92 @@ var defaultModelRatio = map[string]float64{ "text-curie-001": 1, //"text-davinci-002": 10, //"text-davinci-003": 10, - "text-davinci-edit-001": 10, - "code-davinci-edit-001": 10, - "whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens - "tts-1": 7.5, // 1k characters -> $0.015 - "tts-1-1106": 7.5, // 1k characters -> $0.015 - "tts-1-hd": 15, // 1k characters -> $0.03 - "tts-1-hd-1106": 15, // 1k characters -> $0.03 - "davinci": 10, - "curie": 10, - "babbage": 10, - "ada": 10, - "text-embedding-3-small": 0.01, - "text-embedding-3-large": 0.065, - "text-embedding-ada-002": 0.05, - "text-search-ada-doc-001": 10, - "text-moderation-stable": 0.1, - "text-moderation-latest": 0.1, - "claude-instant-1": 0.4, // $0.8 / 1M tokens - "claude-2.0": 4, // $8 / 1M tokens - "claude-2.1": 4, // $8 / 1M tokens - "claude-3-haiku-20240307": 0.125, // $0.25 / 1M tokens - "claude-3-5-haiku-20241022": 0.5, // $1 / 1M tokens - "claude-3-sonnet-20240229": 1.5, // $3 / 1M tokens - "claude-3-5-sonnet-20240620": 1.5, - "claude-3-5-sonnet-20241022": 1.5, - "claude-3-7-sonnet-20250219": 1.5, - "claude-3-7-sonnet-20250219-thinking": 1.5, - "claude-3-opus-20240229": 7.5, // $15 / 1M tokens - "ERNIE-4.0-8K": 0.120 * RMB, - "ERNIE-3.5-8K": 0.012 * RMB, - "ERNIE-3.5-8K-0205": 0.024 * RMB, - "ERNIE-3.5-8K-1222": 0.012 * RMB, - "ERNIE-Bot-8K": 0.024 * RMB, - "ERNIE-3.5-4K-0205": 0.012 * RMB, - "ERNIE-Speed-8K": 0.004 * RMB, - "ERNIE-Speed-128K": 0.004 * RMB, - "ERNIE-Lite-8K-0922": 0.008 * RMB, - "ERNIE-Lite-8K-0308": 0.003 * RMB, - "ERNIE-Tiny-8K": 0.001 * RMB, - "BLOOMZ-7B": 0.004 * RMB, - "Embedding-V1": 0.002 * RMB, - "bge-large-zh": 0.002 * RMB, - "bge-large-en": 0.002 * RMB, - "tao-8k": 0.002 * RMB, - "PaLM-2": 1, - "gemini-pro": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens - "gemini-pro-vision": 1, // $0.00025 / 1k characters -> $0.001 / 1k tokens - "gemini-1.0-pro-vision-001": 1, - "gemini-1.0-pro-001": 1, - "gemini-1.5-pro-latest": 1.75, // $3.5 / 1M tokens - "gemini-1.5-pro-exp-0827": 1.75, // $3.5 / 1M tokens - "gemini-1.5-flash-latest": 1, - "gemini-1.5-flash-exp-0827": 1, - "gemini-1.0-pro-latest": 1, - "gemini-1.0-pro-vision-latest": 1, - "gemini-ultra": 1, - "chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens - "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens - "chatglm_std": 0.3572, // ¥0.005 / 1k tokens - "chatglm_lite": 0.1429, // ¥0.002 / 1k tokens - "glm-4": 7.143, // ¥0.1 / 1k tokens - "glm-4v": 0.05 * RMB, // ¥0.05 / 1k tokens - "glm-4-alltools": 0.1 * RMB, // ¥0.1 / 1k tokens - "glm-3-turbo": 0.3572, - "glm-4-plus": 0.05 * RMB, - "glm-4-0520": 0.1 * RMB, - "glm-4-air": 0.001 * RMB, - "glm-4-airx": 0.01 * RMB, - "glm-4-long": 0.001 * RMB, - "glm-4-flash": 0, - "glm-4v-plus": 0.01 * RMB, - "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens - "qwen-plus": 10, // ¥0.14 / 1k tokens - "text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens - "SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens - "SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens - "SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens - "SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens - "SparkDesk-v4.0": 1.2858, - "360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens - "360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens - "360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens - "360gpt-pro": 0.8572, // ¥0.012 / 1k tokens - "360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens - "embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens - "embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens - "semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens - "hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0 + "text-davinci-edit-001": 10, + "code-davinci-edit-001": 10, + "whisper-1": 15, // $0.006 / minute -> $0.006 / 150 words -> $0.006 / 200 tokens -> $0.03 / 1k tokens + "tts-1": 7.5, // 1k characters -> $0.015 + "tts-1-1106": 7.5, // 1k characters -> $0.015 + "tts-1-hd": 15, // 1k characters -> $0.03 + "tts-1-hd-1106": 15, // 1k characters -> $0.03 + "davinci": 10, + "curie": 10, + "babbage": 10, + "ada": 10, + "text-embedding-3-small": 0.01, + "text-embedding-3-large": 0.065, + "text-embedding-ada-002": 0.05, + "text-search-ada-doc-001": 10, + "text-moderation-stable": 0.1, + "text-moderation-latest": 0.1, + "claude-instant-1": 0.4, // $0.8 / 1M tokens + "claude-2.0": 4, // $8 / 1M tokens + "claude-2.1": 4, // $8 / 1M tokens + "claude-3-haiku-20240307": 0.125, // $0.25 / 1M tokens + "claude-3-5-haiku-20241022": 0.5, // $1 / 1M tokens + "claude-3-sonnet-20240229": 1.5, // $3 / 1M tokens + "claude-3-5-sonnet-20240620": 1.5, + "claude-3-5-sonnet-20241022": 1.5, + "claude-3-7-sonnet-20250219": 1.5, + "claude-3-7-sonnet-20250219-thinking": 1.5, + "claude-3-opus-20240229": 7.5, // $15 / 1M tokens + "ERNIE-4.0-8K": 0.120 * RMB, + "ERNIE-3.5-8K": 0.012 * RMB, + "ERNIE-3.5-8K-0205": 0.024 * RMB, + "ERNIE-3.5-8K-1222": 0.012 * RMB, + "ERNIE-Bot-8K": 0.024 * RMB, + "ERNIE-3.5-4K-0205": 0.012 * RMB, + "ERNIE-Speed-8K": 0.004 * RMB, + "ERNIE-Speed-128K": 0.004 * RMB, + "ERNIE-Lite-8K-0922": 0.008 * RMB, + "ERNIE-Lite-8K-0308": 0.003 * RMB, + "ERNIE-Tiny-8K": 0.001 * RMB, + "BLOOMZ-7B": 0.004 * RMB, + "Embedding-V1": 0.002 * RMB, + "bge-large-zh": 0.002 * RMB, + "bge-large-en": 0.002 * RMB, + "tao-8k": 0.002 * RMB, + "PaLM-2": 1, + "gemini-1.5-pro-latest": 1.25, // $3.5 / 1M tokens + "gemini-1.5-flash-latest": 0.075, + "gemini-2.0-flash": 0.05, + "gemini-2.5-pro-exp-03-25": 0.625, + "gemini-2.5-pro-preview-03-25": 0.625, + "gemini-2.5-flash-preview-04-17": 0.075, + "gemini-2.5-flash-preview-04-17-thinking": 0.075, + "gemini-2.5-flash-preview-04-17-nothinking": 0.075, + "text-embedding-004": 0.001, + "chatglm_turbo": 0.3572, // ¥0.005 / 1k tokens + "chatglm_pro": 0.7143, // ¥0.01 / 1k tokens + "chatglm_std": 0.3572, // ¥0.005 / 1k tokens + "chatglm_lite": 0.1429, // ¥0.002 / 1k tokens + "glm-4": 7.143, // ¥0.1 / 1k tokens + "glm-4v": 0.05 * RMB, // ¥0.05 / 1k tokens + "glm-4-alltools": 0.1 * RMB, // ¥0.1 / 1k tokens + "glm-3-turbo": 0.3572, + "glm-4-plus": 0.05 * RMB, + "glm-4-0520": 0.1 * RMB, + "glm-4-air": 0.001 * RMB, + "glm-4-airx": 0.01 * RMB, + "glm-4-long": 0.001 * RMB, + "glm-4-flash": 0, + "glm-4v-plus": 0.01 * RMB, + "qwen-turbo": 0.8572, // ¥0.012 / 1k tokens + "qwen-plus": 10, // ¥0.14 / 1k tokens + "text-embedding-v1": 0.05, // ¥0.0007 / 1k tokens + "SparkDesk-v1.1": 1.2858, // ¥0.018 / 1k tokens + "SparkDesk-v2.1": 1.2858, // ¥0.018 / 1k tokens + "SparkDesk-v3.1": 1.2858, // ¥0.018 / 1k tokens + "SparkDesk-v3.5": 1.2858, // ¥0.018 / 1k tokens + "SparkDesk-v4.0": 1.2858, + "360GPT_S2_V9": 0.8572, // ¥0.012 / 1k tokens + "360gpt-turbo": 0.0858, // ¥0.0012 / 1k tokens + "360gpt-turbo-responsibility-8k": 0.8572, // ¥0.012 / 1k tokens + "360gpt-pro": 0.8572, // ¥0.012 / 1k tokens + "360gpt2-pro": 0.8572, // ¥0.012 / 1k tokens + "embedding-bert-512-v1": 0.0715, // ¥0.001 / 1k tokens + "embedding_s1_v1": 0.0715, // ¥0.001 / 1k tokens + "semantic_similarity_s1_v1": 0.0715, // ¥0.001 / 1k tokens + "hunyuan": 7.143, // ¥0.1 / 1k tokens // https://cloud.tencent.com/document/product/1729/97731#e0e6be58-60c8-469f-bdeb-6c264ce3b4d0 // https://platform.lingyiwanwu.com/docs#-计费单元 // 已经按照 7.2 来换算美元价格 "yi-34b-chat-0205": 0.18, @@ -204,29 +202,39 @@ var defaultModelRatio = map[string]float64{ "llama-3-sonar-small-32k-online": 0.2 / 1000 * USD, "llama-3-sonar-large-32k-chat": 1 / 1000 * USD, "llama-3-sonar-large-32k-online": 1 / 1000 * USD, + // grok + "grok-3-beta": 1.5, + "grok-3-mini-beta": 0.15, + "grok-2": 1, + "grok-2-vision": 1, + "grok-beta": 2.5, + "grok-vision-beta": 2.5, + "grok-3-fast-beta": 2.5, + "grok-3-mini-fast-beta": 0.3, } var defaultModelPrice = map[string]float64{ - "suno_music": 0.1, - "suno_lyrics": 0.01, - "dall-e-3": 0.04, - "gpt-4-gizmo-*": 0.1, - "mj_imagine": 0.1, - "mj_variation": 0.1, - "mj_reroll": 0.1, - "mj_blend": 0.1, - "mj_modal": 0.1, - "mj_zoom": 0.1, - "mj_shorten": 0.1, - "mj_high_variation": 0.1, - "mj_low_variation": 0.1, - "mj_pan": 0.1, - "mj_inpaint": 0, - "mj_custom_zoom": 0, - "mj_describe": 0.05, - "mj_upscale": 0.05, - "swap_face": 0.05, - "mj_upload": 0.05, + "suno_music": 0.1, + "suno_lyrics": 0.01, + "dall-e-3": 0.04, + "imagen-3.0-generate-002": 0.03, + "gpt-4-gizmo-*": 0.1, + "mj_imagine": 0.1, + "mj_variation": 0.1, + "mj_reroll": 0.1, + "mj_blend": 0.1, + "mj_modal": 0.1, + "mj_zoom": 0.1, + "mj_shorten": 0.1, + "mj_high_variation": 0.1, + "mj_low_variation": 0.1, + "mj_pan": 0.1, + "mj_inpaint": 0, + "mj_custom_zoom": 0, + "mj_describe": 0.05, + "mj_upscale": 0.05, + "swap_face": 0.05, + "mj_upload": 0.05, } var ( @@ -249,17 +257,39 @@ var defaultCompletionRatio = map[string]float64{ "gpt-4-all": 2, } -func GetModelPriceMap() map[string]float64 { +// InitModelSettings initializes all model related settings maps +func InitModelSettings() { + // Initialize modelPriceMap modelPriceMapMutex.Lock() - defer modelPriceMapMutex.Unlock() - if modelPriceMap == nil { - modelPriceMap = defaultModelPrice - } + modelPriceMap = defaultModelPrice + modelPriceMapMutex.Unlock() + + // Initialize modelRatioMap + modelRatioMapMutex.Lock() + modelRatioMap = defaultModelRatio + modelRatioMapMutex.Unlock() + + // Initialize CompletionRatio + CompletionRatioMutex.Lock() + CompletionRatio = defaultCompletionRatio + CompletionRatioMutex.Unlock() + + // Initialize cacheRatioMap + cacheRatioMapMutex.Lock() + cacheRatioMap = defaultCacheRatio + cacheRatioMapMutex.Unlock() +} + +func GetModelPriceMap() map[string]float64 { + modelPriceMapMutex.RLock() + defer modelPriceMapMutex.RUnlock() return modelPriceMap } func ModelPrice2JSONString() string { - GetModelPriceMap() + modelPriceMapMutex.RLock() + defer modelPriceMapMutex.RUnlock() + jsonBytes, err := json.Marshal(modelPriceMap) if err != nil { common.SysError("error marshalling model price: " + err.Error()) @@ -276,7 +306,9 @@ func UpdateModelPriceByJSONString(jsonStr string) error { // GetModelPrice 返回模型的价格,如果模型不存在则返回-1,false func GetModelPrice(name string, printErr bool) (float64, bool) { - GetModelPriceMap() + modelPriceMapMutex.RLock() + defer modelPriceMapMutex.RUnlock() + if strings.HasPrefix(name, "gpt-4-gizmo") { name = "gpt-4-gizmo-*" } @@ -293,24 +325,6 @@ func GetModelPrice(name string, printErr bool) (float64, bool) { return price, true } -func GetModelRatioMap() map[string]float64 { - modelRatioMapMutex.Lock() - defer modelRatioMapMutex.Unlock() - if modelRatioMap == nil { - modelRatioMap = defaultModelRatio - } - return modelRatioMap -} - -func ModelRatio2JSONString() string { - GetModelRatioMap() - jsonBytes, err := json.Marshal(modelRatioMap) - if err != nil { - common.SysError("error marshalling model ratio: " + err.Error()) - } - return string(jsonBytes) -} - func UpdateModelRatioByJSONString(jsonStr string) error { modelRatioMapMutex.Lock() defer modelRatioMapMutex.Unlock() @@ -319,7 +333,9 @@ func UpdateModelRatioByJSONString(jsonStr string) error { } func GetModelRatio(name string) (float64, bool) { - GetModelRatioMap() + modelRatioMapMutex.RLock() + defer modelRatioMapMutex.RUnlock() + if strings.HasPrefix(name, "gpt-4-gizmo") { name = "gpt-4-gizmo-*" } @@ -343,16 +359,15 @@ func GetDefaultModelRatioMap() map[string]float64 { } func GetCompletionRatioMap() map[string]float64 { - CompletionRatioMutex.Lock() - defer CompletionRatioMutex.Unlock() - if CompletionRatio == nil { - CompletionRatio = defaultCompletionRatio - } + CompletionRatioMutex.RLock() + defer CompletionRatioMutex.RUnlock() return CompletionRatio } func CompletionRatio2JSONString() string { - GetCompletionRatioMap() + CompletionRatioMutex.RLock() + defer CompletionRatioMutex.RUnlock() + jsonBytes, err := json.Marshal(CompletionRatio) if err != nil { common.SysError("error marshalling completion ratio: " + err.Error()) @@ -368,13 +383,25 @@ func UpdateCompletionRatioByJSONString(jsonStr string) error { } func GetCompletionRatio(name string) float64 { - GetCompletionRatioMap() + CompletionRatioMutex.RLock() + defer CompletionRatioMutex.RUnlock() if strings.Contains(name, "/") { if ratio, ok := CompletionRatio[name]; ok { return ratio } } + hardCodedRatio, contain := getHardcodedCompletionModelRatio(name) + if contain { + return hardCodedRatio + } + if ratio, ok := CompletionRatio[name]; ok { + return ratio + } + return hardCodedRatio +} + +func getHardcodedCompletionModelRatio(name string) (float64, bool) { lowercaseName := strings.ToLower(name) if strings.HasPrefix(name, "gpt-4-gizmo") { name = "gpt-4-gizmo-*" @@ -385,87 +412,99 @@ func GetCompletionRatio(name string) float64 { if strings.HasPrefix(name, "gpt-4") && !strings.HasSuffix(name, "-all") && !strings.HasSuffix(name, "-gizmo-*") { if strings.HasPrefix(name, "gpt-4o") { if name == "gpt-4o-2024-05-13" { - return 3 + return 3, true } - return 4 + return 4, true } - if strings.HasPrefix(name, "gpt-4.5") { - return 2 + // gpt-4.5-preview匹配 + if strings.HasPrefix(name, "gpt-4.5-preview") { + return 2, true } - if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "preview") { - return 3 + if strings.HasPrefix(name, "gpt-4-turbo") || strings.HasSuffix(name, "gpt-4-1106") || strings.HasSuffix(name, "gpt-4-1105") { + return 3, true } - return 2 + // 没有特殊标记的 gpt-4 模型默认倍率为 2 + return 2, false } if strings.HasPrefix(name, "o1") || strings.HasPrefix(name, "o3") { - return 4 + return 4, true } if name == "chatgpt-4o-latest" { - return 3 + return 3, true } if strings.Contains(name, "claude-instant-1") { - return 3 + return 3, true } else if strings.Contains(name, "claude-2") { - return 3 + return 3, true } else if strings.Contains(name, "claude-3") { - return 5 + return 5, true } if strings.HasPrefix(name, "gpt-3.5") { if name == "gpt-3.5-turbo" || strings.HasSuffix(name, "0125") { // https://openai.com/blog/new-embedding-models-and-api-updates // Updated GPT-3.5 Turbo model and lower pricing - return 3 + return 3, true } if strings.HasSuffix(name, "1106") { - return 2 + return 2, true } - return 4.0 / 3.0 + return 4.0 / 3.0, true } if strings.HasPrefix(name, "mistral-") { - return 3 + return 3, true } if strings.HasPrefix(name, "gemini-") { - return 4 + if strings.HasPrefix(name, "gemini-1.5") { + return 4, true + } else if strings.HasPrefix(name, "gemini-2.0") { + return 4, true + } else if strings.HasPrefix(name, "gemini-2.5-pro-preview") { + return 8, true + } else if strings.HasPrefix(name, "gemini-2.5-flash-preview") { + if strings.HasSuffix(name, "-nothinking") { + return 4, false + } else { + return 3.5 / 0.6, false + } + } + return 4, false } if strings.HasPrefix(name, "command") { switch name { case "command-r": - return 3 + return 3, true case "command-r-plus": - return 5 + return 5, true case "command-r-08-2024": - return 4 + return 4, true case "command-r-plus-08-2024": - return 4 + return 4, true default: - return 4 + return 4, false } } // hint 只给官方上4倍率,由于开源模型供应商自行定价,不对其进行补全倍率进行强制对齐 if lowercaseName == "deepseek-chat" || lowercaseName == "deepseek-reasoner" { - return 4 + return 4, true } if strings.HasPrefix(name, "ERNIE-Speed-") { - return 2 + return 2, true } else if strings.HasPrefix(name, "ERNIE-Lite-") { - return 2 + return 2, true } else if strings.HasPrefix(name, "ERNIE-Character") { - return 2 + return 2, true } else if strings.HasPrefix(name, "ERNIE-Functions") { - return 2 + return 2, true } switch name { case "llama2-70b-4096": - return 0.8 / 0.64 + return 0.8 / 0.64, true case "llama3-8b-8192": - return 2 + return 2, true case "llama3-70b-8192": - return 0.79 / 0.59 + return 0.79 / 0.59, true } - if ratio, ok := CompletionRatio[name]; ok { - return ratio - } - return 1 + return 1, false } func GetAudioRatio(name string) float64 { @@ -498,3 +537,14 @@ func GetAudioCompletionRatio(name string) float64 { } return 2 } + +func ModelRatio2JSONString() string { + modelRatioMapMutex.RLock() + defer modelRatioMapMutex.RUnlock() + + jsonBytes, err := json.Marshal(modelRatioMap) + if err != nil { + common.SysError("error marshalling model ratio: " + err.Error()) + } + return string(jsonBytes) +} diff --git a/setting/system_setting/oidc.go b/setting/system_setting/oidc.go new file mode 100644 index 00000000..aed52ae0 --- /dev/null +++ b/setting/system_setting/oidc.go @@ -0,0 +1,25 @@ +package system_setting + +import "one-api/setting/config" + +type OIDCSettings struct { + Enabled bool `json:"enabled"` + ClientId string `json:"client_id"` + ClientSecret string `json:"client_secret"` + WellKnown string `json:"well_known"` + AuthorizationEndpoint string `json:"authorization_endpoint"` + TokenEndpoint string `json:"token_endpoint"` + UserInfoEndpoint string `json:"user_info_endpoint"` +} + +// 默认配置 +var defaultOIDCSettings = OIDCSettings{} + +func init() { + // 注册到全局配置管理器 + config.GlobalConfig.Register("oidc", &defaultOIDCSettings) +} + +func GetOIDCSettings() *OIDCSettings { + return &defaultOIDCSettings +} diff --git a/web/.prettierrc.mjs b/web/.prettierrc.mjs index ecae84d3..5140bc3e 100644 --- a/web/.prettierrc.mjs +++ b/web/.prettierrc.mjs @@ -1 +1 @@ -module.exports = require("@so1ve/prettier-config"); +module.exports = require('@so1ve/prettier-config'); diff --git a/web/package.json b/web/package.json index edb0ccf5..e6ce588d 100644 --- a/web/package.json +++ b/web/package.json @@ -23,7 +23,7 @@ "react-turnstile": "^1.0.5", "semantic-ui-offline": "^2.5.0", "semantic-ui-react": "^2.1.3", - "sse": "github:mpetazzoni/sse.js", + "sse": "https://github.com/mpetazzoni/sse.js", "i18next": "^23.16.8", "react-i18next": "^13.0.0", "i18next-browser-languagedetector": "^7.2.0" diff --git a/web/pnpm-lock.yaml b/web/pnpm-lock.yaml index e9e40d21..c503b5bb 100644 --- a/web/pnpm-lock.yaml +++ b/web/pnpm-lock.yaml @@ -1,766 +1,3670 @@ -lockfileVersion: '6.0' +lockfileVersion: '9.0' settings: autoInstallPeers: true excludeLinksFromLockfile: false -dependencies: - '@douyinfe/semi-icons': - specifier: ^2.63.1 - version: 2.66.1(react@18.2.0) - '@douyinfe/semi-ui': - specifier: ^2.69.1 - version: 2.72.0(react-dom@18.2.0)(react@18.2.0) - '@visactor/react-vchart': - specifier: ~1.8.8 - version: 1.8.11(react-dom@18.2.0)(react@18.2.0) - '@visactor/vchart': - specifier: ~1.8.8 - version: 1.8.11 - '@visactor/vchart-semi-theme': - specifier: ~1.8.8 - version: 1.8.8(@visactor/vchart@1.8.11) - axios: - specifier: ^0.27.2 - version: 0.27.2 - dayjs: - specifier: ^1.11.11 - version: 1.11.11 - history: - specifier: ^5.3.0 - version: 5.3.0 - i18next: - specifier: ^23.16.8 - version: 23.16.8 - i18next-browser-languagedetector: - specifier: ^7.2.0 - version: 7.2.2 - marked: - specifier: ^4.1.1 - version: 4.3.0 - react: - specifier: ^18.2.0 - version: 18.2.0 - react-dom: - specifier: ^18.2.0 - version: 18.2.0(react@18.2.0) - react-dropzone: - specifier: ^14.2.3 - version: 14.2.3(react@18.2.0) - react-fireworks: - specifier: ^1.0.4 - version: 1.0.4 - react-i18next: - specifier: ^13.0.0 - version: 13.5.0(i18next@23.16.8)(react-dom@18.2.0)(react@18.2.0) - react-router-dom: - specifier: ^6.3.0 - version: 6.22.2(react-dom@18.2.0)(react@18.2.0) - react-telegram-login: - specifier: ^1.1.2 - version: 1.1.2(react@18.2.0) - react-toastify: - specifier: ^9.0.8 - version: 9.1.3(react-dom@18.2.0)(react@18.2.0) - react-turnstile: - specifier: ^1.0.5 - version: 1.1.3(react-dom@18.2.0)(react@18.2.0) - semantic-ui-offline: - specifier: ^2.5.0 - version: 2.5.0 - semantic-ui-react: - specifier: ^2.1.3 - version: 2.1.5(react-dom@18.2.0)(react@18.2.0) - sse: - specifier: github:mpetazzoni/sse.js - version: github.com/mpetazzoni/sse.js/ee7d4cd0b5798c944ca0b2723c7250a5c5e6c65a - -devDependencies: - '@so1ve/prettier-config': - specifier: ^3.1.0 - version: 3.1.0(prettier@3.2.5) - '@vitejs/plugin-react': - specifier: ^4.2.1 - version: 4.2.1(vite@5.2.5) - prettier: - specifier: ^3.0.0 - version: 3.2.5 - typescript: - specifier: 4.4.2 - version: 4.4.2 - vite: - specifier: ^5.2.0 - version: 5.2.5 +importers: + .: + dependencies: + '@douyinfe/semi-icons': + specifier: ^2.63.1 + version: 2.77.1(react@18.3.1) + '@douyinfe/semi-ui': + specifier: ^2.69.1 + version: 2.77.1(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@visactor/react-vchart': + specifier: ~1.8.8 + version: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@visactor/vchart': + specifier: ~1.8.8 + version: 1.8.11 + '@visactor/vchart-semi-theme': + specifier: ~1.8.8 + version: 1.8.8(@visactor/vchart@1.8.11) + axios: + specifier: ^0.27.2 + version: 0.27.2 + dayjs: + specifier: ^1.11.11 + version: 1.11.13 + history: + specifier: ^5.3.0 + version: 5.3.0 + i18next: + specifier: ^23.16.8 + version: 23.16.8 + i18next-browser-languagedetector: + specifier: ^7.2.0 + version: 7.2.2 + marked: + specifier: ^4.1.1 + version: 4.3.0 + react: + specifier: ^18.2.0 + version: 18.3.1 + react-dom: + specifier: ^18.2.0 + version: 18.3.1(react@18.3.1) + react-dropzone: + specifier: ^14.2.3 + version: 14.3.8(react@18.3.1) + react-fireworks: + specifier: ^1.0.4 + version: 1.0.4 + react-i18next: + specifier: ^13.0.0 + version: 13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-router-dom: + specifier: ^6.3.0 + version: 6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-telegram-login: + specifier: ^1.1.2 + version: 1.1.2(react@18.3.1) + react-toastify: + specifier: ^9.0.8 + version: 9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-turnstile: + specifier: ^1.0.5 + version: 1.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + semantic-ui-offline: + specifier: ^2.5.0 + version: 2.5.0 + semantic-ui-react: + specifier: ^2.1.3 + version: 2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + sse: + specifier: github:mpetazzoni/sse.js + version: sse.js@https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6 + devDependencies: + '@so1ve/prettier-config': + specifier: ^3.1.0 + version: 3.1.0(prettier@3.5.3) + '@vitejs/plugin-react': + specifier: ^4.2.1 + version: 4.3.4(vite@5.4.16) + prettier: + specifier: ^3.0.0 + version: 3.5.3 + typescript: + specifier: 4.4.2 + version: 4.4.2 + vite: + specifier: ^5.2.0 + version: 5.4.16 packages: + '@ampproject/remapping@2.3.0': + resolution: + { + integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==, + } + engines: { node: '>=6.0.0' } - /@ampproject/remapping@2.3.0: - resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} - engines: {node: '>=6.0.0'} + '@astrojs/compiler@2.11.0': + resolution: + { + integrity: sha512-zZOO7i+JhojO8qmlyR/URui6LyfHJY6m+L9nwyX5GiKD78YoRaZ5tzz6X0fkl+5bD3uwlDHayf6Oe8Fu36RKNg==, + } + + '@babel/code-frame@7.26.2': + resolution: + { + integrity: sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==, + } + engines: { node: '>=6.9.0' } + + '@babel/compat-data@7.26.8': + resolution: + { + integrity: sha512-oH5UPLMWR3L2wEFLnFJ1TZXqHufiTKAiLfqw5zkhS4dKXLJ10yVztfil/twG8EDTA4F/tvVNw9nOl4ZMslB8rQ==, + } + engines: { node: '>=6.9.0' } + + '@babel/core@7.26.10': + resolution: + { + integrity: sha512-vMqyb7XCDMPvJFFOaT9kxtiRh42GwlZEg1/uIgtZshS5a/8OaduUfCi7kynKgc3Tw/6Uo2D+db9qBttghhmxwQ==, + } + engines: { node: '>=6.9.0' } + + '@babel/generator@7.27.0': + resolution: + { + integrity: sha512-VybsKvpiN1gU1sdMZIp7FcqphVVKEwcuj02x73uvcHE0PTihx1nlBcowYWhDwjpoAXRv43+gDzyggGnn1XZhVw==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-compilation-targets@7.27.0': + resolution: + { + integrity: sha512-LVk7fbXml0H2xH34dFzKQ7TDZ2G4/rVTOrq9V+icbbadjbVxxeFeDsNHv2SrZeWoA+6ZiTyWYWtScEIW07EAcA==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-module-imports@7.25.9': + resolution: + { + integrity: sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-module-transforms@7.26.0': + resolution: + { + integrity: sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-plugin-utils@7.26.5': + resolution: + { + integrity: sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-string-parser@7.25.9': + resolution: + { + integrity: sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-validator-identifier@7.25.9': + resolution: + { + integrity: sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==, + } + engines: { node: '>=6.9.0' } + + '@babel/helper-validator-option@7.25.9': + resolution: + { + integrity: sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==, + } + engines: { node: '>=6.9.0' } + + '@babel/helpers@7.27.0': + resolution: + { + integrity: sha512-U5eyP/CTFPuNE3qk+WZMxFkp/4zUzdceQlfzf7DdGdhp+Fezd7HD+i8Y24ZuTMKX3wQBld449jijbGq6OdGNQg==, + } + engines: { node: '>=6.9.0' } + + '@babel/parser@7.27.0': + resolution: + { + integrity: sha512-iaepho73/2Pz7w2eMS0Q5f83+0RKI7i4xmiYeBmDzfRVbQtTOG7Ts0S4HzJVsTMGI9keU8rNfuZr8DKfSt7Yyg==, + } + engines: { node: '>=6.0.0' } + hasBin: true + + '@babel/plugin-transform-react-jsx-self@7.25.9': + resolution: + { + integrity: sha512-y8quW6p0WHkEhmErnfe58r7x0A70uKphQm8Sp8cV7tjNQwK56sNVK0M73LK3WuYmsuyrftut4xAkjjgU0twaMg==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-react-jsx-source@7.25.9': + resolution: + { + integrity: sha512-+iqjT8xmXhhYv4/uiYd8FNQsraMFZIfxVSqxxVSZP0WbbSAWvBXAul0m/zu+7Vv4O/3WtApy9pmaTMiumEZgfg==, + } + engines: { node: '>=6.9.0' } + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/runtime@7.27.0': + resolution: + { + integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==, + } + engines: { node: '>=6.9.0' } + + '@babel/template@7.27.0': + resolution: + { + integrity: sha512-2ncevenBqXI6qRMukPlXwHKHchC7RyMuu4xv5JBXRfOGVcTy1mXCD12qrp7Jsoxll1EV3+9sE4GugBVRjT2jFA==, + } + engines: { node: '>=6.9.0' } + + '@babel/traverse@7.27.0': + resolution: + { + integrity: sha512-19lYZFzYVQkkHkl4Cy4WrAVcqBkgvV2YM2TU3xG6DIwO7O3ecbDPfW3yM3bjAGcqcQHi+CCtjMR3dIEHxsd6bA==, + } + engines: { node: '>=6.9.0' } + + '@babel/types@7.27.0': + resolution: + { + integrity: sha512-H45s8fVLYjbhFH62dIJ3WtmJ6RSPt/3DRO0ZcT2SUiYiQyz3BLVb9ADEnLl91m74aQPS3AzzeajZHYOalWe3bg==, + } + engines: { node: '>=6.9.0' } + + '@dnd-kit/accessibility@3.1.1': + resolution: + { + integrity: sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==, + } + peerDependencies: + react: '>=16.8.0' + + '@dnd-kit/core@6.3.1': + resolution: + { + integrity: sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==, + } + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@dnd-kit/sortable@7.0.2': + resolution: + { + integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==, + } + peerDependencies: + '@dnd-kit/core': ^6.0.7 + react: '>=16.8.0' + + '@dnd-kit/utilities@3.2.2': + resolution: + { + integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==, + } + peerDependencies: + react: '>=16.8.0' + + '@douyinfe/semi-animation-react@2.77.1': + resolution: + { + integrity: sha512-imELR02pufgGFkZURfTd9oBUtZPYhHvXv9WsYoRvEoBM9U7yzxrR6Fb/Lc3TH+WHVJ2oZHH2S0APS5t1MceEOw==, + } + + '@douyinfe/semi-animation-styled@2.77.1': + resolution: + { + integrity: sha512-FBRroqVJroel1CXmBgV58ulZHG2xUVInJF7k0FAag54noKKaToEobSxRjiTJ6JHne3ZDU1M6sBqpbzYJElFnPQ==, + } + + '@douyinfe/semi-animation@2.77.1': + resolution: + { + integrity: sha512-Q1D7whvQe0D+mPov8hXeH/e1uR/iBhpGGcW1LCTL2pSVMEZEYGJLf2KeXTTiCIgRVWm0PRH3Sux7auJ64zg7vw==, + } + + '@douyinfe/semi-foundation@2.77.1': + resolution: + { + integrity: sha512-DAXRy8ryLNzbKAiTAv+RrivGCoMU0asv2cO7PNV5aBq0ICB8XXn97FHyZo6Wb5NpqpyMhOaOr8Ro1bfpd0FeaA==, + } + + '@douyinfe/semi-icons@2.77.1': + resolution: + { + integrity: sha512-IbGqYzbjzCoSd+//HlO/Gn1c3XmbulQwGys+JgDfQhYIbPeGyhQfLk56Q7ku3vJGC8BGy7dUmR9MbeTf1UQGtw==, + } + peerDependencies: + react: '>=16.0.0' + + '@douyinfe/semi-illustrations@2.77.1': + resolution: + { + integrity: sha512-FlESLOPaY0SadiSIFcP4gqJUk+CYkd4rHK6YP9bfjmU26v7h1S02H7pGLLV1lS0WnY4j0ad4zqRV9tbXFvba9g==, + } + peerDependencies: + react: '>=16.0.0' + + '@douyinfe/semi-json-viewer-core@2.77.1': + resolution: + { + integrity: sha512-LOW+7ga2OzFIL9pGKftwHfl1kKLTV3x6Cs857iyvq9GIF/GHbAboiHcKUy2OZIHfy66zvP+Focs+yhfZG7IcZw==, + } + + '@douyinfe/semi-theme-default@2.77.1': + resolution: + { + integrity: sha512-Rug75C7jjSqmCP2L2tBI0K4dnXuo4GardzwSzdSjxDkiaIXwOwR5KE0K1FRbKWkQ7xmxbyRu4S6Pff+CDEJ/lA==, + } + + '@douyinfe/semi-ui@2.77.1': + resolution: + { + integrity: sha512-eIy7kr9OleCwlNRby3VICSGScHM23Zt2u7TJpID68qN3WrfQowGaB4wQ/0k5bvpLzv463HQnVWFk5aak+v46yw==, + } + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@esbuild/aix-ppc64@0.21.5': + resolution: + { + integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==, + } + engines: { node: '>=12' } + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.21.5': + resolution: + { + integrity: sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==, + } + engines: { node: '>=12' } + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.21.5': + resolution: + { + integrity: sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==, + } + engines: { node: '>=12' } + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.21.5': + resolution: + { + integrity: sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.21.5': + resolution: + { + integrity: sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==, + } + engines: { node: '>=12' } + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.21.5': + resolution: + { + integrity: sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.21.5': + resolution: + { + integrity: sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==, + } + engines: { node: '>=12' } + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.21.5': + resolution: + { + integrity: sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.21.5': + resolution: + { + integrity: sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==, + } + engines: { node: '>=12' } + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.21.5': + resolution: + { + integrity: sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==, + } + engines: { node: '>=12' } + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.21.5': + resolution: + { + integrity: sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==, + } + engines: { node: '>=12' } + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.21.5': + resolution: + { + integrity: sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==, + } + engines: { node: '>=12' } + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.21.5': + resolution: + { + integrity: sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==, + } + engines: { node: '>=12' } + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.21.5': + resolution: + { + integrity: sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==, + } + engines: { node: '>=12' } + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.21.5': + resolution: + { + integrity: sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==, + } + engines: { node: '>=12' } + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.21.5': + resolution: + { + integrity: sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==, + } + engines: { node: '>=12' } + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.21.5': + resolution: + { + integrity: sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-x64@0.21.5': + resolution: + { + integrity: sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-x64@0.21.5': + resolution: + { + integrity: sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [openbsd] + + '@esbuild/sunos-x64@0.21.5': + resolution: + { + integrity: sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.21.5': + resolution: + { + integrity: sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==, + } + engines: { node: '>=12' } + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.21.5': + resolution: + { + integrity: sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==, + } + engines: { node: '>=12' } + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.21.5': + resolution: + { + integrity: sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==, + } + engines: { node: '>=12' } + cpu: [x64] + os: [win32] + + '@fluentui/react-component-event-listener@0.63.1': + resolution: + { + integrity: sha512-gSMdOh6tI3IJKZFqxfQwbTpskpME0CvxdxGM2tdglmf6ZPVDi0L4+KKIm+2dN8nzb8Ya1A8ZT+Ddq0KmZtwVQg==, + } + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + '@fluentui/react-component-ref@0.63.1': + resolution: + { + integrity: sha512-8MkXX4+R3i80msdbD4rFpEB4WWq2UDvGwG386g3ckIWbekdvN9z2kWAd9OXhRGqB7QeOsoAGWocp6gAMCivRlw==, + } + peerDependencies: + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + '@jridgewell/gen-mapping@0.3.8': + resolution: + { + integrity: sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==, + } + engines: { node: '>=6.0.0' } + + '@jridgewell/resolve-uri@3.1.2': + resolution: + { + integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==, + } + engines: { node: '>=6.0.0' } + + '@jridgewell/set-array@1.2.1': + resolution: + { + integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==, + } + engines: { node: '>=6.0.0' } + + '@jridgewell/sourcemap-codec@1.5.0': + resolution: + { + integrity: sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ==, + } + + '@jridgewell/trace-mapping@0.3.25': + resolution: + { + integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==, + } + + '@mdx-js/mdx@3.1.0': + resolution: + { + integrity: sha512-/QxEhPAvGwbQmy1Px8F899L5Uc2KZ6JtXwlCgJmjSTBedwOZkByYcBG4GceIGPXRDsmfxhHazuS+hlOShRLeDw==, + } + + '@popperjs/core@2.11.8': + resolution: + { + integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==, + } + + '@remix-run/router@1.23.0': + resolution: + { + integrity: sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA==, + } + engines: { node: '>=14.0.0' } + + '@resvg/resvg-js-android-arm-eabi@2.4.1': + resolution: + { + integrity: sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw==, + } + engines: { node: '>= 10' } + cpu: [arm] + os: [android] + + '@resvg/resvg-js-android-arm64@2.4.1': + resolution: + { + integrity: sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg==, + } + engines: { node: '>= 10' } + cpu: [arm64] + os: [android] + + '@resvg/resvg-js-darwin-arm64@2.4.1': + resolution: + { + integrity: sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==, + } + engines: { node: '>= 10' } + cpu: [arm64] + os: [darwin] + + '@resvg/resvg-js-darwin-x64@2.4.1': + resolution: + { + integrity: sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw==, + } + engines: { node: '>= 10' } + cpu: [x64] + os: [darwin] + + '@resvg/resvg-js-linux-arm-gnueabihf@2.4.1': + resolution: + { + integrity: sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw==, + } + engines: { node: '>= 10' } + cpu: [arm] + os: [linux] + + '@resvg/resvg-js-linux-arm64-gnu@2.4.1': + resolution: + { + integrity: sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg==, + } + engines: { node: '>= 10' } + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-arm64-musl@2.4.1': + resolution: + { + integrity: sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==, + } + engines: { node: '>= 10' } + cpu: [arm64] + os: [linux] + + '@resvg/resvg-js-linux-x64-gnu@2.4.1': + resolution: + { + integrity: sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==, + } + engines: { node: '>= 10' } + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-linux-x64-musl@2.4.1': + resolution: + { + integrity: sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==, + } + engines: { node: '>= 10' } + cpu: [x64] + os: [linux] + + '@resvg/resvg-js-win32-arm64-msvc@2.4.1': + resolution: + { + integrity: sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==, + } + engines: { node: '>= 10' } + cpu: [arm64] + os: [win32] + + '@resvg/resvg-js-win32-ia32-msvc@2.4.1': + resolution: + { + integrity: sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg==, + } + engines: { node: '>= 10' } + cpu: [ia32] + os: [win32] + + '@resvg/resvg-js-win32-x64-msvc@2.4.1': + resolution: + { + integrity: sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw==, + } + engines: { node: '>= 10' } + cpu: [x64] + os: [win32] + + '@resvg/resvg-js@2.4.1': + resolution: + { + integrity: sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==, + } + engines: { node: '>= 10' } + + '@rollup/rollup-android-arm-eabi@4.39.0': + resolution: + { + integrity: sha512-lGVys55Qb00Wvh8DMAocp5kIcaNzEFTmGhfFd88LfaogYTRKrdxgtlO5H6S49v2Nd8R2C6wLOal0qv6/kCkOwA==, + } + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.39.0': + resolution: + { + integrity: sha512-It9+M1zE31KWfqh/0cJLrrsCPiF72PoJjIChLX+rEcujVRCb4NLQ5QzFkzIZW8Kn8FTbvGQBY5TkKBau3S8cCQ==, + } + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.39.0': + resolution: + { + integrity: sha512-lXQnhpFDOKDXiGxsU9/l8UEGGM65comrQuZ+lDcGUx+9YQ9dKpF3rSEGepyeR5AHZ0b5RgiligsBhWZfSSQh8Q==, + } + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.39.0': + resolution: + { + integrity: sha512-mKXpNZLvtEbgu6WCkNij7CGycdw9cJi2k9v0noMb++Vab12GZjFgUXD69ilAbBh034Zwn95c2PNSz9xM7KYEAQ==, + } + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.39.0': + resolution: + { + integrity: sha512-jivRRlh2Lod/KvDZx2zUR+I4iBfHcu2V/BA2vasUtdtTN2Uk3jfcZczLa81ESHZHPHy4ih3T/W5rPFZ/hX7RtQ==, + } + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.39.0': + resolution: + { + integrity: sha512-8RXIWvYIRK9nO+bhVz8DwLBepcptw633gv/QT4015CpJ0Ht8punmoHU/DuEd3iw9Hr8UwUV+t+VNNuZIWYeY7Q==, + } + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.39.0': + resolution: + { + integrity: sha512-mz5POx5Zu58f2xAG5RaRRhp3IZDK7zXGk5sdEDj4o96HeaXhlUwmLFzNlc4hCQi5sGdR12VDgEUqVSHer0lI9g==, + } + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm-musleabihf@4.39.0': + resolution: + { + integrity: sha512-+YDwhM6gUAyakl0CD+bMFpdmwIoRDzZYaTWV3SDRBGkMU/VpIBYXXEvkEcTagw/7VVkL2vA29zU4UVy1mP0/Yw==, + } + cpu: [arm] + os: [linux] + + '@rollup/rollup-linux-arm64-gnu@4.39.0': + resolution: + { + integrity: sha512-EKf7iF7aK36eEChvlgxGnk7pdJfzfQbNvGV/+l98iiMwU23MwvmV0Ty3pJ0p5WQfm3JRHOytSIqD9LB7Bq7xdQ==, + } + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-arm64-musl@4.39.0': + resolution: + { + integrity: sha512-vYanR6MtqC7Z2SNr8gzVnzUul09Wi1kZqJaek3KcIlI/wq5Xtq4ZPIZ0Mr/st/sv/NnaPwy/D4yXg5x0B3aUUA==, + } + cpu: [arm64] + os: [linux] + + '@rollup/rollup-linux-loongarch64-gnu@4.39.0': + resolution: + { + integrity: sha512-NMRUT40+h0FBa5fb+cpxtZoGAggRem16ocVKIv5gDB5uLDgBIwrIsXlGqYbLwW8YyO3WVTk1FkFDjMETYlDqiw==, + } + cpu: [loong64] + os: [linux] + + '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': + resolution: + { + integrity: sha512-0pCNnmxgduJ3YRt+D+kJ6Ai/r+TaePu9ZLENl+ZDV/CdVczXl95CbIiwwswu4L+K7uOIGf6tMo2vm8uadRaICQ==, + } + cpu: [ppc64] + os: [linux] + + '@rollup/rollup-linux-riscv64-gnu@4.39.0': + resolution: + { + integrity: sha512-t7j5Zhr7S4bBtksT73bO6c3Qa2AV/HqiGlj9+KB3gNF5upcVkx+HLgxTm8DK4OkzsOYqbdqbLKwvGMhylJCPhQ==, + } + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-riscv64-musl@4.39.0': + resolution: + { + integrity: sha512-m6cwI86IvQ7M93MQ2RF5SP8tUjD39Y7rjb1qjHgYh28uAPVU8+k/xYWvxRO3/tBN2pZkSMa5RjnPuUIbrwVxeA==, + } + cpu: [riscv64] + os: [linux] + + '@rollup/rollup-linux-s390x-gnu@4.39.0': + resolution: + { + integrity: sha512-iRDJd2ebMunnk2rsSBYlsptCyuINvxUfGwOUldjv5M4tpa93K8tFMeYGpNk2+Nxl+OBJnBzy2/JCscGeO507kA==, + } + cpu: [s390x] + os: [linux] + + '@rollup/rollup-linux-x64-gnu@4.39.0': + resolution: + { + integrity: sha512-t9jqYw27R6Lx0XKfEFe5vUeEJ5pF3SGIM6gTfONSMb7DuG6z6wfj2yjcoZxHg129veTqU7+wOhY6GX8wmf90dA==, + } + cpu: [x64] + os: [linux] + + '@rollup/rollup-linux-x64-musl@4.39.0': + resolution: + { + integrity: sha512-ThFdkrFDP55AIsIZDKSBWEt/JcWlCzydbZHinZ0F/r1h83qbGeenCt/G/wG2O0reuENDD2tawfAj2s8VK7Bugg==, + } + cpu: [x64] + os: [linux] + + '@rollup/rollup-win32-arm64-msvc@4.39.0': + resolution: + { + integrity: sha512-jDrLm6yUtbOg2TYB3sBF3acUnAwsIksEYjLeHL+TJv9jg+TmTwdyjnDex27jqEMakNKf3RwwPahDIt7QXCSqRQ==, + } + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.39.0': + resolution: + { + integrity: sha512-6w9uMuza+LbLCVoNKL5FSLE7yvYkq9laSd09bwS0tMjkwXrmib/4KmoJcrKhLWHvw19mwU+33ndC69T7weNNjQ==, + } + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.39.0': + resolution: + { + integrity: sha512-yAkUOkIKZlK5dl7u6dg897doBgLXmUHhIINM2c+sND3DZwnrdQkkSiDh7N75Ll4mM4dxSkYfXqU9fW3lLkMFug==, + } + cpu: [x64] + os: [win32] + + '@semantic-ui-react/event-stack@3.1.3': + resolution: + { + integrity: sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ==, + } + peerDependencies: + react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + + '@so1ve/prettier-config@3.1.0': + resolution: + { + integrity: sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg==, + } + peerDependencies: + prettier: ^3.0.0 + + '@so1ve/prettier-plugin-toml@3.1.0': + resolution: + { + integrity: sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg==, + } + peerDependencies: + prettier: ^3.0.0 + + '@turf/boolean-clockwise@6.5.0': + resolution: + { + integrity: sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==, + } + + '@turf/clone@6.5.0': + resolution: + { + integrity: sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw==, + } + + '@turf/flatten@6.5.0': + resolution: + { + integrity: sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==, + } + + '@turf/helpers@6.5.0': + resolution: + { + integrity: sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==, + } + + '@turf/invariant@6.5.0': + resolution: + { + integrity: sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==, + } + + '@turf/meta@3.14.0': + resolution: + { + integrity: sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg==, + } + + '@turf/meta@6.5.0': + resolution: + { + integrity: sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==, + } + + '@turf/rewind@6.5.0': + resolution: + { + integrity: sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ==, + } + + '@types/babel__core@7.20.5': + resolution: + { + integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==, + } + + '@types/babel__generator@7.6.8': + resolution: + { + integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==, + } + + '@types/babel__template@7.4.4': + resolution: + { + integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==, + } + + '@types/babel__traverse@7.20.7': + resolution: + { + integrity: sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==, + } + + '@types/debug@4.1.12': + resolution: + { + integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==, + } + + '@types/estree-jsx@1.0.5': + resolution: + { + integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==, + } + + '@types/estree@1.0.7': + resolution: + { + integrity: sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==, + } + + '@types/hast@3.0.4': + resolution: + { + integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==, + } + + '@types/mdast@4.0.4': + resolution: + { + integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==, + } + + '@types/mdx@2.0.13': + resolution: + { + integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==, + } + + '@types/ms@2.1.0': + resolution: + { + integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==, + } + + '@types/parse-author@2.0.3': + resolution: + { + integrity: sha512-pgRW2K/GVQoogylrGJXDl7PBLW9A6T4OOc9Hy9MLT5f7vgufK2GQ8FcfAbjFHR5HjcN9ByzuCczAORk49REqoA==, + } + + '@types/parse-json@4.0.2': + resolution: + { + integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==, + } + + '@types/unist@2.0.11': + resolution: + { + integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==, + } + + '@types/unist@3.0.3': + resolution: + { + integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==, + } + + '@ungap/structured-clone@1.3.0': + resolution: + { + integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==, + } + + '@visactor/react-vchart@1.8.11': + resolution: + { + integrity: sha512-wHnCex9gOpnttTtSu04ozKJhTveUk8Ln2KX/7PZyCJxqlXq+eWvW4zvM6Ja8T8kGXfXtFYVVNh9zBMQ7y2T/Sw==, + } + peerDependencies: + react: '>=16.0.0' + react-dom: '>=16.0.0' + + '@visactor/vchart-semi-theme@1.8.8': + resolution: + { + integrity: sha512-lm57CX3r6Bm7iGBYYyWhDY+1BvkyhNVLEckKx2PnlPKpJHikKSIK2ACyI5SmHuSOOdYzhY2QK6ZfYa2NShJ83w==, + } + peerDependencies: + '@visactor/vchart': ~1.8.8 + + '@visactor/vchart-theme-utils@1.8.8': + resolution: + { + integrity: sha512-RdCey3/t0+82EYyFZvx210rgJJWti9rsgcL3ROZS7o9CtRW1CMj9u9LKLDNIcPLNcLNACFC0aoT03jpdD1BCpA==, + } + peerDependencies: + '@visactor/vchart': ~1.8.8 + + '@visactor/vchart@1.8.11': + resolution: + { + integrity: sha512-RdQ822J02GgAQNXvO1LiT0T3O6FjdgPdcm9hVBFyrpBBmuI8MH02IE7Y1kGe9NiFTH4tDwP0ixRgBmqNSGSLZQ==, + } + + '@visactor/vdataset@0.17.5': + resolution: + { + integrity: sha512-zVBdLWHWrhldGc8JDjSYF9lvpFT4ZEFQDB0b6yvfSiHzHKHiSco+rWmUFvA7r4ObT6j2QWF1vZAV9To8Ml4vHw==, + } + + '@visactor/vgrammar-coordinate@0.10.11': + resolution: + { + integrity: sha512-XSUvEkaf/NQHFafmTwqoIMZicp9fF3o6NB2FDpuWrK4DI1lTuip/0RkqrC+kBAjc5erjt0em0TiITyqXpp4G6w==, + } + + '@visactor/vgrammar-core@0.10.11': + resolution: + { + integrity: sha512-VL9vcLPDg1LrHl7EOx0Ga9ATsoaChKIaCGzxjrPEjWiIS5VPU9Rs0jBKP+ch8BjamAoSuqL5mKd0L/RaUBqlaA==, + } + + '@visactor/vgrammar-hierarchy@0.10.11': + resolution: + { + integrity: sha512-0r3k51pPlJHu63BduG3htsV/ul62aVcKJxFftRfvKkwGjm1KeHoOZEEAwIf78U2puio0BkLqVn2Ek2L4FYZaIg==, + } + + '@visactor/vgrammar-projection@0.10.11': + resolution: + { + integrity: sha512-yEiKsxdfs5+g60wv5xZ1kyS/EDrAsUzAxCMpFFASVUYbQObHvW+elm+UPq2TBX6KZqAM0gsd1inzaLvfsCrLSg==, + } + + '@visactor/vgrammar-sankey@0.10.11': + resolution: + { + integrity: sha512-BbJTPuyydsL/L5XtQv59Q82GgJeePY7Wleac798usx3GnDK0GAOrPsI3bubSsOESJ4pNk3V4HPGEQDG1vCPb4w==, + } + + '@visactor/vgrammar-util@0.10.11': + resolution: + { + integrity: sha512-cJZLmKZvN95Y+yGhX+28+UpZu3bhYYlXDlHJNvXHyonI76ZYgtceyon2b3lI6XIsUsBGcD4Uo777s949X5os3g==, + } + + '@visactor/vgrammar-wordcloud-shape@0.10.11': + resolution: + { + integrity: sha512-NsQOYJp+9WHnIApMvkcUOaajxIg5U/r6rD8LKnoXW/HqAN2TFYXcRR3Daqmk9rrpM5VztQimKOsA1yZWyzozrA==, + } + + '@visactor/vgrammar-wordcloud@0.10.11': + resolution: + { + integrity: sha512-JWDqjGhr9JlYkKVBeEkiOqLQk7C1x1BtnsZ+E8oN541gzUqHwfS9qZyhwI3OyoSLewJlsSSPu1vXLKSQzLzKPA==, + } + + '@visactor/vrender-components@0.17.17': + resolution: + { + integrity: sha512-7gYFQrozvBkyGF7s/JHXdWDZnATzymxzug63CZd4EB7A0OXKatVDImXRePqwzlPD3QamF7QMVWn0CuIx3gQ2gA==, + } + + '@visactor/vrender-core@0.17.17': + resolution: + { + integrity: sha512-pAZGaimunDAWOBdFhzPh0auH5ryxAHr+MVoz+QdASG+6RZXy8D02l8v2QYu4+e4uorxe/s2ZkdNDm81SlNkoHQ==, + } + + '@visactor/vrender-kits@0.17.17': + resolution: + { + integrity: sha512-noRP1hAHvPCv36nf2P6sZ930Tk+dJ8jpPWIUm1cFYmUNdcumgIS8Cug0RyeZ+saSqVt5FDTwIwifhOqupw5Zaw==, + } + + '@visactor/vscale@0.17.5': + resolution: + { + integrity: sha512-2dkS1IlAJ/IdTp8JElbctOOv6lkHKBKPDm8KvwBo0NuGWQeYAebSeyN3QCdwKbj76gMlCub4zc+xWrS5YiA2zA==, + } + + '@visactor/vutils-extension@1.8.11': + resolution: + { + integrity: sha512-Hknzpy3+xh4sdL0iSn5N93BHiMJF4FdwSwhHYEibRpriZmWKG6wBxsJ0Bll4d7oS4f+svxt8Sg2vRYKzQEcIxQ==, + } + + '@visactor/vutils@0.17.5': + resolution: + { + integrity: sha512-HFN6Pk1Wc1RK842g02MeKOlvdri5L7/nqxMVTqxIvi0XMhHXpmoqN4+/9H+h8LmJpVohyrI/MT85TRBV/rManw==, + } + + '@vitejs/plugin-react@4.3.4': + resolution: + { + integrity: sha512-SCCPBJtYLdE8PX/7ZQAs1QAZ8Jqwih+0VBLum1EGqmCCQal+MIUqLCzj3ZUy8ufbC0cAM4LRlSTm7IQJwWT4ug==, + } + engines: { node: ^14.18.0 || >=16.0.0 } + peerDependencies: + vite: ^4.2.0 || ^5.0.0 || ^6.0.0 + + abs-svg-path@0.1.1: + resolution: + { + integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==, + } + + acorn-jsx@5.3.2: + resolution: + { + integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==, + } + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.14.1: + resolution: + { + integrity: sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==, + } + engines: { node: '>=0.4.0' } + hasBin: true + + array-source@0.0.4: + resolution: + { + integrity: sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==, + } + + astring@1.9.0: + resolution: + { + integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==, + } + hasBin: true + + async-validator@3.5.2: + resolution: + { + integrity: sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==, + } + + asynckit@0.4.0: + resolution: + { + integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==, + } + + attr-accept@2.2.5: + resolution: + { + integrity: sha512-0bDNnY/u6pPwHDMoF0FieU354oBi0a8rD9FcsLwzcGWbc8KS8KPIi7y+s13OlVY+gMWc/9xEMUgNE6Qm8ZllYQ==, + } + engines: { node: '>=4' } + + author-regex@1.0.0: + resolution: + { + integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==, + } + engines: { node: '>=0.8' } + + axios@0.27.2: + resolution: + { + integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==, + } + + bail@2.0.2: + resolution: + { + integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==, + } + + balanced-match@1.0.2: + resolution: + { + integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==, + } + + bezier-easing@2.1.0: + resolution: + { + integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==, + } + + brace-expansion@1.1.11: + resolution: + { + integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==, + } + + browserslist@4.24.4: + resolution: + { + integrity: sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==, + } + engines: { node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7 } + hasBin: true + + buffer-from@1.1.2: + resolution: + { + integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==, + } + + call-bind-apply-helpers@1.0.2: + resolution: + { + integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==, + } + engines: { node: '>= 0.4' } + + callsites@3.1.0: + resolution: + { + integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==, + } + engines: { node: '>=6' } + + caniuse-lite@1.0.30001709: + resolution: + { + integrity: sha512-NgL3vUTnDrPCZ3zTahp4fsugQ4dc7EKTSzwQDPEel6DMoMnfH2jhry9n2Zm8onbSR+f/QtKHFOA+iAQu4kbtWA==, + } + + ccount@2.0.1: + resolution: + { + integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==, + } + + character-entities-html4@2.1.0: + resolution: + { + integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==, + } + + character-entities-legacy@3.0.0: + resolution: + { + integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==, + } + + character-entities@2.0.2: + resolution: + { + integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==, + } + + character-reference-invalid@2.0.1: + resolution: + { + integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==, + } + + classnames@2.5.1: + resolution: + { + integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==, + } + + clsx@1.2.1: + resolution: + { + integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==, + } + engines: { node: '>=6' } + + collapse-white-space@2.1.0: + resolution: + { + integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==, + } + + color-convert@2.0.1: + resolution: + { + integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==, + } + engines: { node: '>=7.0.0' } + + color-name@1.1.4: + resolution: + { + integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==, + } + + combined-stream@1.0.8: + resolution: + { + integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==, + } + engines: { node: '>= 0.8' } + + comma-separated-tokens@2.0.3: + resolution: + { + integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==, + } + + commander@2.20.3: + resolution: + { + integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==, + } + + commander@4.1.1: + resolution: + { + integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==, + } + engines: { node: '>= 6' } + + compute-scroll-into-view@1.0.20: + resolution: + { + integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==, + } + + concat-map@0.0.1: + resolution: + { + integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==, + } + + concat-stream@1.4.11: + resolution: + { + integrity: sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==, + } + engines: { '0': node >= 0.8 } + + concat-stream@2.0.0: + resolution: + { + integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==, + } + engines: { '0': node >= 6.0 } + + convert-source-map@2.0.0: + resolution: + { + integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==, + } + + copy-text-to-clipboard@2.2.0: + resolution: + { + integrity: sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ==, + } + engines: { node: '>=6' } + + core-util-is@1.0.3: + resolution: + { + integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==, + } + + cosmiconfig@7.1.0: + resolution: + { + integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==, + } + engines: { node: '>=10' } + + d3-array@1.2.4: + resolution: + { + integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==, + } + + d3-dsv@2.0.0: + resolution: + { + integrity: sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==, + } + hasBin: true + + d3-geo@1.12.1: + resolution: + { + integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==, + } + + d3-hexbin@0.2.2: + resolution: + { + integrity: sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==, + } + + d3-hierarchy@3.1.2: + resolution: + { + integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==, + } + engines: { node: '>=12' } + + date-fns-tz@1.3.8: + resolution: + { + integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==, + } + peerDependencies: + date-fns: '>=2.0.0' + + date-fns@2.30.0: + resolution: + { + integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==, + } + engines: { node: '>=0.11' } + + dayjs@1.11.13: + resolution: + { + integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==, + } + + debug@4.4.0: + resolution: + { + integrity: sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==, + } + engines: { node: '>=6.0' } + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.1.0: + resolution: + { + integrity: sha512-Wy+JTSbFThEOXQIR2L6mxJvEs+veIzpmqD7ynWxMXGpnk3smkHQOp6forLdHsKpAMW9iJpaBBIxz285t1n1C3w==, + } + + delayed-stream@1.0.0: + resolution: + { + integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==, + } + engines: { node: '>=0.4.0' } + + dequal@2.0.3: + resolution: + { + integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==, + } + engines: { node: '>=6' } + + devlop@1.1.0: + resolution: + { + integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==, + } + + dunder-proto@1.0.1: + resolution: + { + integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==, + } + engines: { node: '>= 0.4' } + + electron-to-chromium@1.5.130: + resolution: + { + integrity: sha512-Ou2u7L9j2XLZbhqzyX0jWDj6gA8D3jIfVzt4rikLf3cGBa0VdReuFimBKS9tQJA4+XpeCxj1NoWlfBXzbMa9IA==, + } + + error-ex@1.3.2: + resolution: + { + integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==, + } + + es-define-property@1.0.1: + resolution: + { + integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==, + } + engines: { node: '>= 0.4' } + + es-errors@1.3.0: + resolution: + { + integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==, + } + engines: { node: '>= 0.4' } + + es-object-atoms@1.1.1: + resolution: + { + integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==, + } + engines: { node: '>= 0.4' } + + es-set-tostringtag@2.1.0: + resolution: + { + integrity: sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==, + } + engines: { node: '>= 0.4' } + + esast-util-from-estree@2.0.0: + resolution: + { + integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==, + } + + esast-util-from-js@2.0.1: + resolution: + { + integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==, + } + + esbuild@0.21.5: + resolution: + { + integrity: sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==, + } + engines: { node: '>=12' } + hasBin: true + + escalade@3.2.0: + resolution: + { + integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==, + } + engines: { node: '>=6' } + + escape-string-regexp@5.0.0: + resolution: + { + integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==, + } + engines: { node: '>=12' } + + estree-util-attach-comments@3.0.0: + resolution: + { + integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==, + } + + estree-util-build-jsx@3.0.1: + resolution: + { + integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==, + } + + estree-util-is-identifier-name@3.0.0: + resolution: + { + integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==, + } + + estree-util-scope@1.0.0: + resolution: + { + integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==, + } + + estree-util-to-js@2.0.0: + resolution: + { + integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==, + } + + estree-util-visit@2.0.0: + resolution: + { + integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==, + } + + estree-walker@3.0.3: + resolution: + { + integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==, + } + + eventemitter3@4.0.7: + resolution: + { + integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==, + } + + exenv@1.2.2: + resolution: + { + integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==, + } + + extend@3.0.2: + resolution: + { + integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==, + } + + fast-copy@3.0.2: + resolution: + { + integrity: sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ==, + } + + file-selector@2.1.2: + resolution: + { + integrity: sha512-QgXo+mXTe8ljeqUFaX3QVHc5osSItJ/Km+xpocx0aSqWGMSCf6qYs/VnzZgS864Pjn5iceMRFigeAV7AfTlaig==, + } + engines: { node: '>= 12' } + + file-source@0.6.1: + resolution: + { + integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==, + } + + follow-redirects@1.15.9: + resolution: + { + integrity: sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==, + } + engines: { node: '>=4.0' } + peerDependencies: + debug: '*' + peerDependenciesMeta: + debug: + optional: true + + form-data@4.0.2: + resolution: + { + integrity: sha512-hGfm/slu0ZabnNt4oaRZ6uREyfCj6P4fT/n6A1rGV+Z0VdGXjfOhVUpkn6qVQONHGIFwmveGXyDs75+nr6FM8w==, + } + engines: { node: '>= 6' } + + fs-extra@10.1.0: + resolution: + { + integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==, + } + engines: { node: '>=12' } + + fs-extra@4.0.3: + resolution: + { + integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==, + } + + fs.realpath@1.0.0: + resolution: + { + integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, + } + + fsevents@2.3.3: + resolution: + { + integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==, + } + engines: { node: ^8.16.0 || ^10.6.0 || >=11.0.0 } + os: [darwin] + + function-bind@1.1.2: + resolution: + { + integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==, + } + + gensync@1.0.0-beta.2: + resolution: + { + integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==, + } + engines: { node: '>=6.9.0' } + + geobuf@3.0.2: + resolution: + { + integrity: sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==, + } + hasBin: true + + geojson-dissolve@3.1.0: + resolution: + { + integrity: sha512-JXHfn+A3tU392HA703gJbjmuHaQOAE/C1KzbELCczFRFux+GdY6zt1nKb1VMBHp4LWeE7gUY2ql+g06vJqhiwQ==, + } + + geojson-flatten@0.2.4: + resolution: + { + integrity: sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA==, + } + hasBin: true + + geojson-linestring-dissolve@0.0.1: + resolution: + { + integrity: sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw==, + } + + get-intrinsic@1.3.0: + resolution: + { + integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==, + } + engines: { node: '>= 0.4' } + + get-proto@1.0.1: + resolution: + { + integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==, + } + engines: { node: '>= 0.4' } + + get-stdin@6.0.0: + resolution: + { + integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==, + } + engines: { node: '>=4' } + + glob@7.2.3: + resolution: + { + integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, + } + deprecated: Glob versions prior to v9 are no longer supported + + globals@11.12.0: + resolution: + { + integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==, + } + engines: { node: '>=4' } + + gopd@1.2.0: + resolution: + { + integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==, + } + engines: { node: '>= 0.4' } + + graceful-fs@4.2.11: + resolution: + { + integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==, + } + + has-symbols@1.1.0: + resolution: + { + integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==, + } + engines: { node: '>= 0.4' } + + has-tostringtag@1.0.2: + resolution: + { + integrity: sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==, + } + engines: { node: '>= 0.4' } + + hasown@2.0.2: + resolution: + { + integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==, + } + engines: { node: '>= 0.4' } + + hast-util-to-estree@3.1.3: + resolution: + { + integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==, + } + + hast-util-to-jsx-runtime@2.3.6: + resolution: + { + integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==, + } + + hast-util-whitespace@3.0.0: + resolution: + { + integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==, + } + + history@5.3.0: + resolution: + { + integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==, + } + + html-parse-stringify@3.0.1: + resolution: + { + integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==, + } + + i18next-browser-languagedetector@7.2.2: + resolution: + { + integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==, + } + + i18next@23.16.8: + resolution: + { + integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==, + } + + iconv-lite@0.4.24: + resolution: + { + integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==, + } + engines: { node: '>=0.10.0' } + + ieee754@1.2.1: + resolution: + { + integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==, + } + + import-fresh@3.3.1: + resolution: + { + integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==, + } + engines: { node: '>=6' } + + inflight@1.0.6: + resolution: + { + integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, + } + deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + + inherits@2.0.4: + resolution: + { + integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==, + } + + inline-style-parser@0.2.4: + resolution: + { + integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==, + } + + is-alphabetical@2.0.1: + resolution: + { + integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==, + } + + is-alphanumerical@2.0.1: + resolution: + { + integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==, + } + + is-arrayish@0.2.1: + resolution: + { + integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==, + } + + is-decimal@2.0.1: + resolution: + { + integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==, + } + + is-hexadecimal@2.0.1: + resolution: + { + integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==, + } + + is-plain-obj@4.1.0: + resolution: + { + integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==, + } + engines: { node: '>=12' } + + isarray@0.0.1: + resolution: + { + integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==, + } + + jquery@3.7.1: + resolution: + { + integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==, + } + + js-tokens@4.0.0: + resolution: + { + integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==, + } + + jsesc@3.1.0: + resolution: + { + integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==, + } + engines: { node: '>=6' } + hasBin: true + + json-parse-even-better-errors@2.3.1: + resolution: + { + integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==, + } + + json5@2.2.3: + resolution: + { + integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==, + } + engines: { node: '>=6' } + hasBin: true + + jsonc-parser@3.3.1: + resolution: + { + integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==, + } + + jsonfile@4.0.0: + resolution: + { + integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==, + } + + jsonfile@6.1.0: + resolution: + { + integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==, + } + + keyboard-key@1.1.0: + resolution: + { + integrity: sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==, + } + + lines-and-columns@1.2.4: + resolution: + { + integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==, + } + + lodash-es@4.17.21: + resolution: + { + integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==, + } + + lodash@4.17.21: + resolution: + { + integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==, + } + + longest-streak@3.1.0: + resolution: + { + integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==, + } + + loose-envify@1.4.0: + resolution: + { + integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==, + } + hasBin: true + + lottie-web@5.12.2: + resolution: + { + integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==, + } + + lru-cache@5.1.1: + resolution: + { + integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==, + } + + markdown-extensions@2.0.0: + resolution: + { + integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==, + } + engines: { node: '>=16' } + + markdown-table@3.0.4: + resolution: + { + integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==, + } + + marked@4.3.0: + resolution: + { + integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==, + } + engines: { node: '>= 12' } + hasBin: true + + math-intrinsics@1.1.0: + resolution: + { + integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==, + } + engines: { node: '>= 0.4' } + + mdast-util-find-and-replace@3.0.2: + resolution: + { + integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==, + } + + mdast-util-from-markdown@2.0.2: + resolution: + { + integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==, + } + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: + { + integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==, + } + + mdast-util-gfm-footnote@2.1.0: + resolution: + { + integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==, + } + + mdast-util-gfm-strikethrough@2.0.0: + resolution: + { + integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==, + } + + mdast-util-gfm-table@2.0.0: + resolution: + { + integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==, + } + + mdast-util-gfm-task-list-item@2.0.0: + resolution: + { + integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==, + } + + mdast-util-gfm@3.1.0: + resolution: + { + integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==, + } + + mdast-util-mdx-expression@2.0.1: + resolution: + { + integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==, + } + + mdast-util-mdx-jsx@3.2.0: + resolution: + { + integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==, + } + + mdast-util-mdx@3.0.0: + resolution: + { + integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==, + } + + mdast-util-mdxjs-esm@2.0.1: + resolution: + { + integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==, + } + + mdast-util-phrasing@4.1.0: + resolution: + { + integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==, + } + + mdast-util-to-hast@13.2.0: + resolution: + { + integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==, + } + + mdast-util-to-markdown@2.1.2: + resolution: + { + integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==, + } + + mdast-util-to-string@4.0.0: + resolution: + { + integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==, + } + + memoize-one@5.2.1: + resolution: + { + integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==, + } + + micromark-core-commonmark@2.0.3: + resolution: + { + integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==, + } + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: + { + integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==, + } + + micromark-extension-gfm-footnote@2.1.0: + resolution: + { + integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==, + } + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: + { + integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==, + } + + micromark-extension-gfm-table@2.1.1: + resolution: + { + integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==, + } + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: + { + integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==, + } + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: + { + integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==, + } + + micromark-extension-gfm@3.0.0: + resolution: + { + integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==, + } + + micromark-extension-mdx-expression@3.0.1: + resolution: + { + integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==, + } + + micromark-extension-mdx-jsx@3.0.2: + resolution: + { + integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==, + } + + micromark-extension-mdx-md@2.0.0: + resolution: + { + integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==, + } + + micromark-extension-mdxjs-esm@3.0.0: + resolution: + { + integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==, + } + + micromark-extension-mdxjs@3.0.0: + resolution: + { + integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==, + } + + micromark-factory-destination@2.0.1: + resolution: + { + integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==, + } + + micromark-factory-label@2.0.1: + resolution: + { + integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==, + } + + micromark-factory-mdx-expression@2.0.3: + resolution: + { + integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==, + } + + micromark-factory-space@2.0.1: + resolution: + { + integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==, + } + + micromark-factory-title@2.0.1: + resolution: + { + integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==, + } + + micromark-factory-whitespace@2.0.1: + resolution: + { + integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==, + } + + micromark-util-character@2.1.1: + resolution: + { + integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==, + } + + micromark-util-chunked@2.0.1: + resolution: + { + integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==, + } + + micromark-util-classify-character@2.0.1: + resolution: + { + integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==, + } + + micromark-util-combine-extensions@2.0.1: + resolution: + { + integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==, + } + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: + { + integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==, + } + + micromark-util-decode-string@2.0.1: + resolution: + { + integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==, + } + + micromark-util-encode@2.0.1: + resolution: + { + integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==, + } + + micromark-util-events-to-acorn@2.0.3: + resolution: + { + integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==, + } + + micromark-util-html-tag-name@2.0.1: + resolution: + { + integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==, + } + + micromark-util-normalize-identifier@2.0.1: + resolution: + { + integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==, + } + + micromark-util-resolve-all@2.0.1: + resolution: + { + integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==, + } + + micromark-util-sanitize-uri@2.0.1: + resolution: + { + integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==, + } + + micromark-util-subtokenize@2.1.0: + resolution: + { + integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==, + } + + micromark-util-symbol@2.0.1: + resolution: + { + integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==, + } + + micromark-util-types@2.0.2: + resolution: + { + integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==, + } + + micromark@4.0.2: + resolution: + { + integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==, + } + + mime-db@1.52.0: + resolution: + { + integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==, + } + engines: { node: '>= 0.6' } + + mime-types@2.1.35: + resolution: + { + integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==, + } + engines: { node: '>= 0.6' } + + minimatch@3.1.2: + resolution: + { + integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==, + } + + minimist@1.2.0: + resolution: + { + integrity: sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==, + } + + minimist@1.2.6: + resolution: + { + integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==, + } + + ms@2.1.3: + resolution: + { + integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==, + } + + nanoid@3.3.11: + resolution: + { + integrity: sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==, + } + engines: { node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1 } + hasBin: true + + node-releases@2.0.19: + resolution: + { + integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==, + } + + object-assign@4.1.1: + resolution: + { + integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==, + } + engines: { node: '>=0.10.0' } + + once@1.4.0: + resolution: + { + integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, + } + + parent-module@1.0.1: + resolution: + { + integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==, + } + engines: { node: '>=6' } + + parse-author@2.0.0: + resolution: + { + integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==, + } + engines: { node: '>=0.10.0' } + + parse-entities@4.0.2: + resolution: + { + integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==, + } + + parse-json@5.2.0: + resolution: + { + integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==, + } + engines: { node: '>=8' } + + parse-svg-path@0.1.2: + resolution: + { + integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==, + } + + path-browserify@1.0.1: + resolution: + { + integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==, + } + + path-data-parser@0.1.0: + resolution: + { + integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==, + } + + path-is-absolute@1.0.1: + resolution: + { + integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, + } + engines: { node: '>=0.10.0' } + + path-source@0.1.3: + resolution: + { + integrity: sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==, + } + + path-type@4.0.0: + resolution: + { + integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==, + } + engines: { node: '>=8' } + + pbf@3.3.0: + resolution: + { + integrity: sha512-XDF38WCH3z5OV/OVa8GKUNtLAyneuzbCisx7QUCF8Q6Nutx0WnJrQe5O+kOtBlLfRNUws98Y58Lblp+NJG5T4Q==, + } + hasBin: true + + picocolors@1.1.1: + resolution: + { + integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==, + } + + point-at-length@1.1.0: + resolution: + { + integrity: sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw==, + } + + points-on-curve@0.2.0: + resolution: + { + integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==, + } + + points-on-path@0.2.1: + resolution: + { + integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==, + } + + postcss@8.5.3: + resolution: + { + integrity: sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==, + } + engines: { node: ^10 || ^12 || >=14 } + + prettier-package-json@2.8.0: + resolution: + { + integrity: sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ==, + } + hasBin: true + + prettier-plugin-astro@0.14.1: + resolution: + { + integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==, + } + engines: { node: ^14.15.0 || >=16.0.0 } + + prettier-plugin-curly-and-jsdoc@3.1.0: + resolution: + { + integrity: sha512-4QMOHnLlkP2jTRWS0MFH6j+cuOiXLvXOqCLKbtwwVd8PPyq8NenW5AAwfwqiTNHBQG/DmzViPphRrwgN0XkUVQ==, + } + peerDependencies: + prettier: ^3.0.0 + + prettier-plugin-pkgsort@0.2.1: + resolution: + { + integrity: sha512-/k5MIw84EhgoH7dmq4+6ozHjJ0VYbxbw17g4C+WPGHODkLivGwJoA6U1YPR/KObyRDMQJHXAfXKu++9smg7Jyw==, + } + peerDependencies: + prettier: ^3.0.0 + + prettier@3.5.3: + resolution: + { + integrity: sha512-QQtaxnoDJeAkDvDKWCLiwIXkTgRhwYDEQCghU9Z6q03iyek/rxRh/2lC3HB7P8sWT2xC/y5JDctPLBIGzHKbhw==, + } + engines: { node: '>=14' } + hasBin: true + + prismjs@1.30.0: + resolution: + { + integrity: sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==, + } + engines: { node: '>=6' } + + prop-types@15.8.1: + resolution: + { + integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==, + } + + property-information@7.0.0: + resolution: + { + integrity: sha512-7D/qOz/+Y4X/rzSB6jKxKUsQnphO046ei8qxG59mtM3RG3DHgTK81HrxrmoDVINJb8NKT5ZsRbwHvQ6B68Iyhg==, + } + + protocol-buffers-schema@3.6.0: + resolution: + { + integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==, + } + + react-dom@18.3.1: + resolution: + { + integrity: sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==, + } + peerDependencies: + react: ^18.3.1 + + react-draggable@4.4.6: + resolution: + { + integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==, + } + peerDependencies: + react: '>= 16.3.0' + react-dom: '>= 16.3.0' + + react-dropzone@14.3.8: + resolution: + { + integrity: sha512-sBgODnq+lcA4P296DY4wacOZz3JFpD99fp+hb//iBO2HHnyeZU3FwWyXJ6salNpqQdsZrgMrotuko/BdJMV8Ug==, + } + engines: { node: '>= 10.13' } + peerDependencies: + react: '>= 16.8 || 18.0.0' + + react-fast-compare@3.2.2: + resolution: + { + integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==, + } + + react-fireworks@1.0.4: + resolution: + { + integrity: sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw==, + } + + react-i18next@13.5.0: + resolution: + { + integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==, + } + peerDependencies: + i18next: '>= 23.2.3' + react: '>= 16.8.0' + react-dom: '*' + react-native: '*' + peerDependenciesMeta: + react-dom: + optional: true + react-native: + optional: true + + react-is@16.13.1: + resolution: + { + integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==, + } + + react-is@18.3.1: + resolution: + { + integrity: sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==, + } + + react-popper@2.3.0: + resolution: + { + integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==, + } + peerDependencies: + '@popperjs/core': ^2.0.0 + react: ^16.8.0 || ^17 || ^18 + react-dom: ^16.8.0 || ^17 || ^18 + + react-refresh@0.14.2: + resolution: + { + integrity: sha512-jCvmsr+1IUSMUyzOkRcvnVbX3ZYC6g9TDrDbFuFmRDq7PD4yaGbLKNQL6k2jnArV8hjYxh7hVhAZB6s9HDGpZA==, + } + engines: { node: '>=0.10.0' } + + react-resizable@3.0.5: + resolution: + { + integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==, + } + peerDependencies: + react: '>= 16.3' + + react-router-dom@6.30.0: + resolution: + { + integrity: sha512-x30B78HV5tFk8ex0ITwzC9TTZMua4jGyA9IUlH1JLQYQTFyxr/ZxwOJq7evg1JX1qGVUcvhsmQSKdPncQrjTgA==, + } + engines: { node: '>=14.0.0' } + peerDependencies: + react: '>=16.8' + react-dom: '>=16.8' + + react-router@6.30.0: + resolution: + { + integrity: sha512-D3X8FyH9nBcTSHGdEKurK7r8OYE1kKFn3d/CF+CoxbSHkxU7o37+Uh7eAHRXr6k2tSExXYO++07PeXJtA/dEhQ==, + } + engines: { node: '>=14.0.0' } + peerDependencies: + react: '>=16.8' + + react-telegram-login@1.1.2: + resolution: + { + integrity: sha512-pDP+bvfaklWgnK5O6yvZnIwgky0nnYUU6Zhk0EjdMSkPsLQoOzZRsXIoZnbxyBXhi7346bsxMH+EwwJPTxClDw==, + } + peerDependencies: + react: ^16.13.1 + + react-toastify@9.1.3: + resolution: + { + integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==, + } + peerDependencies: + react: '>=16' + react-dom: '>=16' + + react-turnstile@1.1.4: + resolution: + { + integrity: sha512-oluyRWADdsufCt5eMqacW4gfw8/csr6Tk+fmuaMx0PWMKP1SX1iCviLvD2D5w92eAzIYDHi/krUWGHhlfzxTpQ==, + } + peerDependencies: + react: '>= 16.13.1' + react-dom: '>= 16.13.1' + + react-window@1.8.11: + resolution: + { + integrity: sha512-+SRbUVT2scadgFSWx+R1P754xHPEqvcfSfVX10QYg6POOz+WNgkN48pS+BtZNIMGiL1HYrSEiCkwsMS15QogEQ==, + } + engines: { node: '>8.0.0' } + peerDependencies: + react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + react@18.3.1: + resolution: + { + integrity: sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==, + } + engines: { node: '>=0.10.0' } + + readable-stream@1.1.14: + resolution: + { + integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==, + } + + readable-stream@3.6.2: + resolution: + { + integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==, + } + engines: { node: '>= 6' } + + recma-build-jsx@1.0.0: + resolution: + { + integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==, + } + + recma-jsx@1.0.0: + resolution: + { + integrity: sha512-5vwkv65qWwYxg+Atz95acp8DMu1JDSqdGkA2Of1j6rCreyFUE/gp15fC8MnGEuG1W68UKjM6x6+YTWIh7hZM/Q==, + } + + recma-parse@1.0.0: + resolution: + { + integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==, + } + + recma-stringify@1.0.0: + resolution: + { + integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==, + } + + regenerator-runtime@0.14.1: + resolution: + { + integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==, + } + + rehype-recma@1.0.0: + resolution: + { + integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==, + } + + remark-gfm@4.0.1: + resolution: + { + integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==, + } + + remark-mdx@3.1.0: + resolution: + { + integrity: sha512-Ngl/H3YXyBV9RcRNdlYsZujAmhsxwzxpDzpDEhFBVAGthS4GDgnctpDjgFl/ULx5UEDzqtW1cyBSNKqYYrqLBA==, + } + + remark-parse@11.0.0: + resolution: + { + integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==, + } + + remark-rehype@11.1.2: + resolution: + { + integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==, + } + + remark-stringify@11.0.0: + resolution: + { + integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==, + } + + resolve-from@4.0.0: + resolution: + { + integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==, + } + engines: { node: '>=4' } + + resolve-protobuf-schema@2.1.0: + resolution: + { + integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==, + } + + rollup@4.39.0: + resolution: + { + integrity: sha512-thI8kNc02yNvnmJp8dr3fNWJ9tCONDhp6TV35X6HkKGGs9E6q7YWCHbe5vKiTa7TAiNcFEmXKj3X/pG2b3ci0g==, + } + engines: { node: '>=18.0.0', npm: '>=8.0.0' } + hasBin: true + + roughjs@4.5.2: + resolution: + { + integrity: sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg==, + } + + rw@1.3.3: + resolution: + { + integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==, + } + + s.color@0.0.15: + resolution: + { + integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==, + } + + safe-buffer@5.2.1: + resolution: + { + integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==, + } + + safer-buffer@2.1.2: + resolution: + { + integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==, + } + + sass-formatter@0.7.9: + resolution: + { + integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==, + } + + scheduler@0.23.2: + resolution: + { + integrity: sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==, + } + + scroll-into-view-if-needed@2.2.31: + resolution: + { + integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==, + } + + semantic-ui-offline@2.5.0: + resolution: + { + integrity: sha512-Fldx3SfaVtWx5EeCb/5EiJwYkzrGbtsAwVs02xLkeV5z5l8GJmplWEVOeJVjbEpmyiwPWp7cA48JwT5RjbWBVA==, + } + + semantic-ui-react@2.1.5: + resolution: + { + integrity: sha512-nIqmmUNpFHfovEb+RI2w3E2/maZQutd8UIWyRjf1SLse+XF51hI559xbz/sLN3O6RpLjr/echLOOXwKCirPy3Q==, + } + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + + semver@6.3.1: + resolution: + { + integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==, + } + hasBin: true + + shallowequal@1.1.0: + resolution: + { + integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==, + } + + shapefile@0.6.6: + resolution: + { + integrity: sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==, + } + hasBin: true + + simple-statistics@7.8.8: + resolution: + { + integrity: sha512-CUtP0+uZbcbsFpqEyvNDYjJCl+612fNgjT8GaVuvMG7tBuJg8gXGpsP5M7X658zy0IcepWOZ6nPBu1Qb9ezA1w==, + } + + simplify-geojson@1.0.5: + resolution: + { + integrity: sha512-02l1W4UipP5ivNVq6kX15mAzCRIV1oI3tz0FUEyOsNiv1ltuFDjbNhO+nbv/xhbDEtKqWLYuzpWhUsJrjR/ypA==, + } + hasBin: true + + simplify-geometry@0.0.2: + resolution: + { + integrity: sha512-ZEyrplkqgCqDlL7V8GbbYgTLlcnNF+MWWUdy8s8ZeJru50bnI71rDew/I+HG36QS2mPOYAq1ZjwNXxHJ8XOVBw==, + } + + slice-source@0.4.1: + resolution: + { + integrity: sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==, + } + + sort-object-keys@1.1.3: + resolution: + { + integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==, + } + + sort-order@1.1.2: + resolution: + { + integrity: sha512-Q8tOrwB1TSv9fNUXym9st3TZJODtmcOIi2JWCkVNQPrRg17KPwlpwweTEb7pMwUIFMTAgx2/JsQQXEPFzYQj3A==, + } + + source-map-js@1.2.1: + resolution: + { + integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==, + } + engines: { node: '>=0.10.0' } + + source-map@0.7.4: + resolution: + { + integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==, + } + engines: { node: '>= 8' } + + space-separated-tokens@2.0.2: + resolution: + { + integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==, + } + + sse.js@https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6: + resolution: + { + tarball: https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6, + } + version: 2.6.0 + + stream-source@0.3.5: + resolution: + { + integrity: sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==, + } + + string_decoder@0.10.31: + resolution: + { + integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==, + } + + string_decoder@1.3.0: + resolution: + { + integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==, + } + + stringify-entities@4.0.4: + resolution: + { + integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==, + } + + style-to-js@1.1.16: + resolution: + { + integrity: sha512-/Q6ld50hKYPH3d/r6nr117TZkHR0w0kGGIVfpG9N6D8NymRPM9RqCUv4pRpJ62E5DqOYx2AFpbZMyCPnjQCnOw==, + } + + style-to-object@1.0.8: + resolution: + { + integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==, + } + + suf-log@2.5.3: + resolution: + { + integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==, + } + + text-encoding@0.6.4: + resolution: + { + integrity: sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==, + } + deprecated: no longer maintained + + topojson-client@3.1.0: + resolution: + { + integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==, + } + hasBin: true + + topojson-server@3.0.1: + resolution: + { + integrity: sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==, + } + hasBin: true + + trim-lines@3.0.1: + resolution: + { + integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==, + } + + trough@2.2.0: + resolution: + { + integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==, + } + + tslib@2.8.1: + resolution: + { + integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==, + } + + typedarray@0.0.6: + resolution: + { + integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==, + } + + typedarray@0.0.7: + resolution: + { + integrity: sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ==, + } + + typescript@4.4.2: + resolution: + { + integrity: sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==, + } + engines: { node: '>=4.2.0' } + hasBin: true + + unified@11.0.5: + resolution: + { + integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==, + } + + unist-util-is@6.0.0: + resolution: + { + integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==, + } + + unist-util-position-from-estree@2.0.0: + resolution: + { + integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==, + } + + unist-util-position@5.0.0: + resolution: + { + integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==, + } + + unist-util-stringify-position@4.0.0: + resolution: + { + integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==, + } + + unist-util-visit-parents@6.0.1: + resolution: + { + integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==, + } + + unist-util-visit@5.0.0: + resolution: + { + integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==, + } + + universalify@0.1.2: + resolution: + { + integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==, + } + engines: { node: '>= 4.0.0' } + + universalify@2.0.1: + resolution: + { + integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==, + } + engines: { node: '>= 10.0.0' } + + update-browserslist-db@1.1.3: + resolution: + { + integrity: sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==, + } + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + util-deprecate@1.0.2: + resolution: + { + integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==, + } + + utility-types@3.11.0: + resolution: + { + integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==, + } + engines: { node: '>= 4' } + + vfile-message@4.0.2: + resolution: + { + integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==, + } + + vfile@6.0.3: + resolution: + { + integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==, + } + + vite@5.4.16: + resolution: + { + integrity: sha512-Y5gnfp4NemVfgOTDQAunSD4346fal44L9mszGGY/e+qxsRT5y1sMlS/8tiQ8AFAp+MFgYNSINdfEchJiPm41vQ==, + } + engines: { node: ^18.0.0 || >=20.0.0 } + hasBin: true + peerDependencies: + '@types/node': ^18.0.0 || >=20.0.0 + less: '*' + lightningcss: ^1.21.0 + sass: '*' + sass-embedded: '*' + stylus: '*' + sugarss: '*' + terser: ^5.4.0 + peerDependenciesMeta: + '@types/node': + optional: true + less: + optional: true + lightningcss: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + + void-elements@3.1.0: + resolution: + { + integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==, + } + engines: { node: '>=0.10.0' } + + warning@4.0.3: + resolution: + { + integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==, + } + + wrappy@1.0.2: + resolution: + { + integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, + } + + yallist@3.1.1: + resolution: + { + integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==, + } + + yaml@1.10.2: + resolution: + { + integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==, + } + engines: { node: '>= 6' } + + zwitch@2.0.4: + resolution: + { + integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==, + } + +snapshots: + '@ampproject/remapping@2.3.0': dependencies: - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.24 - dev: true + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 - /@astrojs/compiler@2.10.3: - resolution: {integrity: sha512-bL/O7YBxsFt55YHU021oL+xz+B/9HvGNId3F9xURN16aeqDK9juHGktdkCSXz+U4nqFACq6ZFvWomOzhV+zfPw==} - dev: true + '@astrojs/compiler@2.11.0': {} - /@babel/code-frame@7.23.5: - resolution: {integrity: sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==} - engines: {node: '>=6.9.0'} + '@babel/code-frame@7.26.2': dependencies: - '@babel/highlight': 7.23.4 - chalk: 2.4.2 - dev: true + '@babel/helper-validator-identifier': 7.25.9 + js-tokens: 4.0.0 + picocolors: 1.1.1 - /@babel/compat-data@7.23.5: - resolution: {integrity: sha512-uU27kfDRlhfKl+w1U6vp16IuvSLtjAxdArVXPa9BvLkrr7CYIsxH5adpHObeAGY/41+syctUWOZ140a2Rvkgjw==} - engines: {node: '>=6.9.0'} - dev: true + '@babel/compat-data@7.26.8': {} - /@babel/core@7.24.0: - resolution: {integrity: sha512-fQfkg0Gjkza3nf0c7/w6Xf34BW4YvzNfACRLmmb7XRLa6XHdR+K9AlJlxneFfWYf6uhOzuzZVTjF/8KfndZANw==} - engines: {node: '>=6.9.0'} + '@babel/core@7.26.10': dependencies: '@ampproject/remapping': 2.3.0 - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-compilation-targets': 7.23.6 - '@babel/helper-module-transforms': 7.23.3(@babel/core@7.24.0) - '@babel/helpers': 7.24.0 - '@babel/parser': 7.24.0 - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/helper-compilation-targets': 7.27.0 + '@babel/helper-module-transforms': 7.26.0(@babel/core@7.26.10) + '@babel/helpers': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 convert-source-map: 2.0.0 - debug: 4.3.4 + debug: 4.4.0 gensync: 1.0.0-beta.2 json5: 2.2.3 semver: 6.3.1 transitivePeerDependencies: - supports-color - dev: true - /@babel/generator@7.23.6: - resolution: {integrity: sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==} - engines: {node: '>=6.9.0'} + '@babel/generator@7.27.0': dependencies: - '@babel/types': 7.24.0 - '@jridgewell/gen-mapping': 0.3.5 - '@jridgewell/trace-mapping': 0.3.24 - jsesc: 2.5.2 - dev: true + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 + '@jridgewell/gen-mapping': 0.3.8 + '@jridgewell/trace-mapping': 0.3.25 + jsesc: 3.1.0 - /@babel/helper-compilation-targets@7.23.6: - resolution: {integrity: sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==} - engines: {node: '>=6.9.0'} + '@babel/helper-compilation-targets@7.27.0': dependencies: - '@babel/compat-data': 7.23.5 - '@babel/helper-validator-option': 7.23.5 - browserslist: 4.23.0 + '@babel/compat-data': 7.26.8 + '@babel/helper-validator-option': 7.25.9 + browserslist: 4.24.4 lru-cache: 5.1.1 semver: 6.3.1 - dev: true - /@babel/helper-environment-visitor@7.22.20: - resolution: {integrity: sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-function-name@7.23.0: - resolution: {integrity: sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==} - engines: {node: '>=6.9.0'} + '@babel/helper-module-imports@7.25.9': dependencies: - '@babel/template': 7.24.0 - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-hoist-variables@7.22.5: - resolution: {integrity: sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-module-imports@7.22.15: - resolution: {integrity: sha512-0pYVBnDKZO2fnSPCrgM/6WMc7eS20Fbok+0r88fp+YtWVLZrp4CkafFGIp+W0VKw4a22sgebPT99y+FDNMdP4w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-module-transforms@7.23.3(@babel/core@7.24.0): - resolution: {integrity: sha512-7bBs4ED9OmswdfDzpz4MpWgSrV7FXlc3zIagvLFjS5H+Mk7Snr21vQ6QwrsoCGMfNC4e4LQPdoULEt4ykz0SRQ==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0 - dependencies: - '@babel/core': 7.24.0 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-module-imports': 7.22.15 - '@babel/helper-simple-access': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/helper-validator-identifier': 7.22.20 - dev: true - - /@babel/helper-plugin-utils@7.24.0: - resolution: {integrity: sha512-9cUznXMG0+FxRuJfvL82QlTqIzhVW9sL0KjMPHhAOOvpQGL8QtdxnBKILjBqxlHyliz0yCa1G903ZXI/FuHy2w==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-simple-access@7.22.5: - resolution: {integrity: sha512-n0H99E/K+Bika3++WNL17POvo4rKWZ7lZEp1Q+fStVbUi8nxPQEBOlTmCOxW/0JsS56SKKQ+ojAe2pHKJHN35w==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-split-export-declaration@7.22.6: - resolution: {integrity: sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/types': 7.24.0 - dev: true - - /@babel/helper-string-parser@7.23.4: - resolution: {integrity: sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-identifier@7.22.20: - resolution: {integrity: sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helper-validator-option@7.23.5: - resolution: {integrity: sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==} - engines: {node: '>=6.9.0'} - dev: true - - /@babel/helpers@7.24.0: - resolution: {integrity: sha512-ulDZdc0Aj5uLc5nETsa7EPx2L7rM0YJM8r7ck7U73AXi7qOV44IHHRAYZHY6iU1rr3C5N4NtTmMRUJP6kwCWeA==} - engines: {node: '>=6.9.0'} - dependencies: - '@babel/template': 7.24.0 - '@babel/traverse': 7.24.0 - '@babel/types': 7.24.0 + '@babel/traverse': 7.27.0 + '@babel/types': 7.27.0 transitivePeerDependencies: - supports-color - dev: true - /@babel/highlight@7.23.4: - resolution: {integrity: sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==} - engines: {node: '>=6.9.0'} + '@babel/helper-module-transforms@7.26.0(@babel/core@7.26.10)': dependencies: - '@babel/helper-validator-identifier': 7.22.20 - chalk: 2.4.2 - js-tokens: 4.0.0 - dev: true + '@babel/core': 7.26.10 + '@babel/helper-module-imports': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 + '@babel/traverse': 7.27.0 + transitivePeerDependencies: + - supports-color - /@babel/parser@7.24.0: - resolution: {integrity: sha512-QuP/FxEAzMSjXygs8v4N9dvdXzEHN4W1oF3PxuWAtPo08UdM17u89RDMgjLn/mlc56iM0HlLmVkO/wgR+rDgHg==} - engines: {node: '>=6.0.0'} - hasBin: true + '@babel/helper-plugin-utils@7.26.5': {} + + '@babel/helper-string-parser@7.25.9': {} + + '@babel/helper-validator-identifier@7.25.9': {} + + '@babel/helper-validator-option@7.25.9': {} + + '@babel/helpers@7.27.0': dependencies: - '@babel/types': 7.24.0 - dev: true + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 - /@babel/plugin-transform-react-jsx-self@7.24.1(@babel/core@7.24.0): - resolution: {integrity: sha512-kDJgnPujTmAZ/9q2CN4m2/lRsUUPDvsG3+tSHWUJIzMGTt5U/b/fwWd3RO3n+5mjLrsBrVa5eKFRVSQbi3dF1w==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/parser@7.27.0': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 - dev: true + '@babel/types': 7.27.0 - /@babel/plugin-transform-react-jsx-source@7.24.1(@babel/core@7.24.0): - resolution: {integrity: sha512-1v202n7aUq4uXAieRTKcwPzNyphlCuqHHDcdSNc+vdhoTEZcFMh+L5yZuCmGaIO7bs1nJUNfHB89TZyoL48xNA==} - engines: {node: '>=6.9.0'} - peerDependencies: - '@babel/core': ^7.0.0-0 + '@babel/plugin-transform-react-jsx-self@7.25.9(@babel/core@7.26.10)': dependencies: - '@babel/core': 7.24.0 - '@babel/helper-plugin-utils': 7.24.0 - dev: true + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 - /@babel/runtime@7.24.0: - resolution: {integrity: sha512-Chk32uHMg6TnQdvw2e9IlqPpFX/6NLuK0Ys2PqLb7/gL5uFn9mXvK715FGLlOLQrcO4qIkNHkvPGktzzXexsFw==} - engines: {node: '>=6.9.0'} + '@babel/plugin-transform-react-jsx-source@7.25.9(@babel/core@7.26.10)': + dependencies: + '@babel/core': 7.26.10 + '@babel/helper-plugin-utils': 7.26.5 + + '@babel/runtime@7.27.0': dependencies: regenerator-runtime: 0.14.1 - dev: false - /@babel/template@7.24.0: - resolution: {integrity: sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==} - engines: {node: '>=6.9.0'} + '@babel/template@7.27.0': dependencies: - '@babel/code-frame': 7.23.5 - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 - dev: true + '@babel/code-frame': 7.26.2 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 - /@babel/traverse@7.24.0: - resolution: {integrity: sha512-HfuJlI8qq3dEDmNU5ChzzpZRWq+oxCZQyMzIMEqLho+AQnhMnKQUzH6ydo3RBl/YjPCuk68Y6s0Gx0AeyULiWw==} - engines: {node: '>=6.9.0'} + '@babel/traverse@7.27.0': dependencies: - '@babel/code-frame': 7.23.5 - '@babel/generator': 7.23.6 - '@babel/helper-environment-visitor': 7.22.20 - '@babel/helper-function-name': 7.23.0 - '@babel/helper-hoist-variables': 7.22.5 - '@babel/helper-split-export-declaration': 7.22.6 - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 - debug: 4.3.4 + '@babel/code-frame': 7.26.2 + '@babel/generator': 7.27.0 + '@babel/parser': 7.27.0 + '@babel/template': 7.27.0 + '@babel/types': 7.27.0 + debug: 4.4.0 globals: 11.12.0 transitivePeerDependencies: - supports-color - dev: true - /@babel/types@7.24.0: - resolution: {integrity: sha512-+j7a5c253RfKh8iABBhywc8NSfP5LURe7Uh4qpsh6jc+aLJguvmIUBdjSdEMQv2bENrCR5MfRdjGo7vzS/ob7w==} - engines: {node: '>=6.9.0'} + '@babel/types@7.27.0': dependencies: - '@babel/helper-string-parser': 7.23.4 - '@babel/helper-validator-identifier': 7.22.20 - to-fast-properties: 2.0.0 - dev: true + '@babel/helper-string-parser': 7.25.9 + '@babel/helper-validator-identifier': 7.25.9 - /@dnd-kit/accessibility@3.1.0(react@18.2.0): - resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==} - peerDependencies: - react: '>=16.8.0' + '@dnd-kit/accessibility@3.1.1(react@18.3.1)': dependencies: - react: 18.2.0 - tslib: 2.6.2 - dev: false + react: 18.3.1 + tslib: 2.8.1 - /@dnd-kit/core@6.1.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==} - peerDependencies: - react: '>=16.8.0' - react-dom: '>=16.8.0' + '@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@dnd-kit/accessibility': 3.1.0(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - tslib: 2.6.2 - dev: false + '@dnd-kit/accessibility': 3.1.1(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + tslib: 2.8.1 - /@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0): - resolution: {integrity: sha512-wDkBHHf9iCi1veM834Gbk1429bd4lHX4RpAwT0y2cHLf246GAvU2sVw/oxWNpPKQNQRQaeGXhAVgrOl1IT+iyA==} - peerDependencies: - '@dnd-kit/core': ^6.0.7 - react: '>=16.8.0' + '@dnd-kit/sortable@7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)': dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - react: 18.2.0 - tslib: 2.6.2 - dev: false + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + react: 18.3.1 + tslib: 2.8.1 - /@dnd-kit/utilities@3.2.2(react@18.2.0): - resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==} - peerDependencies: - react: '>=16.8.0' + '@dnd-kit/utilities@3.2.2(react@18.3.1)': dependencies: - react: 18.2.0 - tslib: 2.6.2 - dev: false + react: 18.3.1 + tslib: 2.8.1 - /@douyinfe/semi-animation-react@2.72.0: - resolution: {integrity: sha512-uBY0fHEmNCbwyEdu/7quWfqQDMPa2aZz+TF/3U06R96caajUihjYy9drDkor1U3tTWLEKEeM6X0BR/QcqcP4mg==} + '@douyinfe/semi-animation-react@2.77.1': dependencies: - '@douyinfe/semi-animation': 2.72.0 - '@douyinfe/semi-animation-styled': 2.72.0 + '@douyinfe/semi-animation': 2.77.1 + '@douyinfe/semi-animation-styled': 2.77.1 classnames: 2.5.1 - dev: false - /@douyinfe/semi-animation-styled@2.72.0: - resolution: {integrity: sha512-zJba1jtUdlTXo1/GAAv4ImGfllT04CRH0wOT876a408lVXHauJjrGJby0ESbkMKaFTAdNYh76fJ+sp+qLOweXQ==} - dev: false + '@douyinfe/semi-animation-styled@2.77.1': {} - /@douyinfe/semi-animation@2.72.0: - resolution: {integrity: sha512-6K0YUysNA20akUWwzUdeED4XQXU2iD0gJdmnW44ZwbKGv5thKo38bQ0YYPcmjsk60EtlmQy8xSfnTNoRCq/FBg==} + '@douyinfe/semi-animation@2.77.1': dependencies: bezier-easing: 2.1.0 - dev: false - /@douyinfe/semi-foundation@2.72.0: - resolution: {integrity: sha512-2Z+cdPYGRwCY+eCsptq5UCPQZ9ltIWcY7lpe5rudDIBI8Z2GprXvpmMG2HbdpqCz047AMvI+g/x9xzhHi5FR+A==} + '@douyinfe/semi-foundation@2.77.1(acorn@8.14.1)': dependencies: - '@douyinfe/semi-animation': 2.72.0 - '@douyinfe/semi-json-viewer-core': 2.72.0 - '@mdx-js/mdx': 3.0.1 + '@douyinfe/semi-animation': 2.77.1 + '@douyinfe/semi-json-viewer-core': 2.77.1 + '@mdx-js/mdx': 3.1.0(acorn@8.14.1) async-validator: 3.5.2 classnames: 2.5.1 date-fns: 2.30.0 date-fns-tz: 1.3.8(date-fns@2.30.0) - fast-copy: 3.0.1 + fast-copy: 3.0.2 lodash: 4.17.21 lottie-web: 5.12.2 memoize-one: 5.2.1 - prismjs: 1.29.0 - remark-gfm: 4.0.0 + prismjs: 1.30.0 + remark-gfm: 4.0.1 scroll-into-view-if-needed: 2.2.31 transitivePeerDependencies: + - acorn - supports-color - dev: false - /@douyinfe/semi-icons@2.66.1(react@18.2.0): - resolution: {integrity: sha512-zQtBKg8hXA3f7Gp6UFWsudFjpdCYE9rxTzM84HpTyd/rvKd3v9p6MD4rBPNbuFV2dS90eXajgsvroPz3S+ss/A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@douyinfe/semi-icons@2.77.1(react@18.3.1)': dependencies: classnames: 2.5.1 - react: 18.2.0 - dev: false + react: 18.3.1 - /@douyinfe/semi-icons@2.72.0(react@18.2.0): - resolution: {integrity: sha512-MsQ7NO6MljJ0LZNGD7pJqEnf9GnDYNPOd9zwn/7T232PH5tZzSdwVjvUVueWSDy7Bx3UHBp5aG6NWOfPIFL4Hw==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + '@douyinfe/semi-illustrations@2.77.1(react@18.3.1)': dependencies: - classnames: 2.5.1 - react: 18.2.0 - dev: false + react: 18.3.1 - /@douyinfe/semi-illustrations@2.72.0(react@18.2.0): - resolution: {integrity: sha512-8Pr/ms2IRE6FXei8UWQhDJ6oDPWSC/7naDV80ESZUe316N60J1gQFEm64XtZIWzL0QCQSNz9wdl9GRpJL05s/A==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - dependencies: - react: 18.2.0 - dev: false - - /@douyinfe/semi-json-viewer-core@2.72.0: - resolution: {integrity: sha512-A7mJXKu1pz/iFbXLcWTPmnvl5J+Yj9UZYR/NNx9UczqvCmYaUz9px+Aa9ynKsdsWhSD1B4xWbp8x6A4FlBJCAA==} + '@douyinfe/semi-json-viewer-core@2.77.1': dependencies: jsonc-parser: 3.3.1 - dev: false - /@douyinfe/semi-theme-default@2.72.0: - resolution: {integrity: sha512-l8SOMqiaDFkZKgRmRKe7bTADprVQc5VsJIAbu14qSoo8pdQvJMPnRgeCsTRkTzLTw8YU1IpGeD6514dZP6qlvA==} - dev: false + '@douyinfe/semi-theme-default@2.77.1': {} - /@douyinfe/semi-ui@2.72.0(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-njJGvNKggioy9ouZRcIRzrpNAJq9FrKe0100QcK/3ygITY1eE5TcUOCuZAKvMtzyo2ieaypN1EgutaeIofEXLA==} - peerDependencies: - react: '>=16.0.0' - react-dom: '>=16.0.0' + '@douyinfe/semi-ui@2.77.1(acorn@8.14.1)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@dnd-kit/core': 6.1.0(react-dom@18.2.0)(react@18.2.0) - '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.1.0)(react@18.2.0) - '@dnd-kit/utilities': 3.2.2(react@18.2.0) - '@douyinfe/semi-animation': 2.72.0 - '@douyinfe/semi-animation-react': 2.72.0 - '@douyinfe/semi-foundation': 2.72.0 - '@douyinfe/semi-icons': 2.72.0(react@18.2.0) - '@douyinfe/semi-illustrations': 2.72.0(react@18.2.0) - '@douyinfe/semi-theme-default': 2.72.0 + '@dnd-kit/core': 6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@dnd-kit/sortable': 7.0.2(@dnd-kit/core@6.3.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1) + '@dnd-kit/utilities': 3.2.2(react@18.3.1) + '@douyinfe/semi-animation': 2.77.1 + '@douyinfe/semi-animation-react': 2.77.1 + '@douyinfe/semi-foundation': 2.77.1(acorn@8.14.1) + '@douyinfe/semi-icons': 2.77.1(react@18.3.1) + '@douyinfe/semi-illustrations': 2.77.1(react@18.3.1) + '@douyinfe/semi-theme-default': 2.77.1 async-validator: 3.5.2 classnames: 2.5.1 copy-text-to-clipboard: 2.2.0 date-fns: 2.30.0 date-fns-tz: 1.3.8(date-fns@2.30.0) - fast-copy: 3.0.1 + fast-copy: 3.0.2 jsonc-parser: 3.3.1 lodash: 4.17.21 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-resizable: 3.0.5(react-dom@18.2.0)(react@18.2.0) - react-window: 1.8.10(react-dom@18.2.0)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-resizable: 3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + react-window: 1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1) scroll-into-view-if-needed: 2.2.31 utility-types: 3.11.0 transitivePeerDependencies: + - acorn - supports-color - dev: false - /@esbuild/aix-ppc64@0.20.2: - resolution: {integrity: sha512-D+EBOJHXdNZcLJRBkhENNG8Wji2kgc9AZ9KiPr1JuZjsNtyHzrsfLRrY0tk2H2aoFu6RANO1y1iPPUCDYWkb5g==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [aix] - requiresBuild: true - dev: true + '@esbuild/aix-ppc64@0.21.5': optional: true - /@esbuild/android-arm64@0.20.2: - resolution: {integrity: sha512-mRzjLacRtl/tWU0SvD8lUEwb61yP9cqQo6noDZP/O8VkwafSYwZ4yWy24kan8jE/IMERpYncRt2dw438LP3Xmg==} - engines: {node: '>=12'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-arm64@0.21.5': optional: true - /@esbuild/android-arm@0.20.2: - resolution: {integrity: sha512-t98Ra6pw2VaDhqNWO2Oph2LXbz/EJcnLmKLGBJwEwXX/JAN83Fym1rU8l0JUWK6HkIbWONCSSatf4sf2NBRx/w==} - engines: {node: '>=12'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-arm@0.21.5': optional: true - /@esbuild/android-x64@0.20.2: - resolution: {integrity: sha512-btzExgV+/lMGDDa194CcUQm53ncxzeBrWJcncOBxuC6ndBkKxnHdFJn86mCIgTELsooUmwUm9FkhSp5HYu00Rg==} - engines: {node: '>=12'} - cpu: [x64] - os: [android] - requiresBuild: true - dev: true + '@esbuild/android-x64@0.21.5': optional: true - /@esbuild/darwin-arm64@0.20.2: - resolution: {integrity: sha512-4J6IRT+10J3aJH3l1yzEg9y3wkTDgDk7TSDFX+wKFiWjqWp/iCfLIYzGyasx9l0SAFPT1HwSCR+0w/h1ES/MjA==} - engines: {node: '>=12'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + '@esbuild/darwin-arm64@0.21.5': optional: true - /@esbuild/darwin-x64@0.20.2: - resolution: {integrity: sha512-tBcXp9KNphnNH0dfhv8KYkZhjc+H3XBkF5DKtswJblV7KlT9EI2+jeA8DgBjp908WEuYll6pF+UStUCfEpdysA==} - engines: {node: '>=12'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + '@esbuild/darwin-x64@0.21.5': optional: true - /@esbuild/freebsd-arm64@0.20.2: - resolution: {integrity: sha512-d3qI41G4SuLiCGCFGUrKsSeTXyWG6yem1KcGZVS+3FYlYhtNoNgYrWcvkOoaqMhwXSMrZRl69ArHsGJ9mYdbbw==} - engines: {node: '>=12'} - cpu: [arm64] - os: [freebsd] - requiresBuild: true - dev: true + '@esbuild/freebsd-arm64@0.21.5': optional: true - /@esbuild/freebsd-x64@0.20.2: - resolution: {integrity: sha512-d+DipyvHRuqEeM5zDivKV1KuXn9WeRX6vqSqIDgwIfPQtwMP4jaDsQsDncjTDDsExT4lR/91OLjRo8bmC1e+Cw==} - engines: {node: '>=12'} - cpu: [x64] - os: [freebsd] - requiresBuild: true - dev: true + '@esbuild/freebsd-x64@0.21.5': optional: true - /@esbuild/linux-arm64@0.20.2: - resolution: {integrity: sha512-9pb6rBjGvTFNira2FLIWqDk/uaf42sSyLE8j1rnUpuzsODBq7FvpwHYZxQ/It/8b+QOS1RYfqgGFNLRI+qlq2A==} - engines: {node: '>=12'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-arm64@0.21.5': optional: true - /@esbuild/linux-arm@0.20.2: - resolution: {integrity: sha512-VhLPeR8HTMPccbuWWcEUD1Az68TqaTYyj6nfE4QByZIQEQVWBB8vup8PpR7y1QHL3CpcF6xd5WVBU/+SBEvGTg==} - engines: {node: '>=12'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-arm@0.21.5': optional: true - /@esbuild/linux-ia32@0.20.2: - resolution: {integrity: sha512-o10utieEkNPFDZFQm9CoP7Tvb33UutoJqg3qKf1PWVeeJhJw0Q347PxMvBgVVFgouYLGIhFYG0UGdBumROyiig==} - engines: {node: '>=12'} - cpu: [ia32] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-ia32@0.21.5': optional: true - /@esbuild/linux-loong64@0.20.2: - resolution: {integrity: sha512-PR7sp6R/UC4CFVomVINKJ80pMFlfDfMQMYynX7t1tNTeivQ6XdX5r2XovMmha/VjR1YN/HgHWsVcTRIMkymrgQ==} - engines: {node: '>=12'} - cpu: [loong64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-loong64@0.21.5': optional: true - /@esbuild/linux-mips64el@0.20.2: - resolution: {integrity: sha512-4BlTqeutE/KnOiTG5Y6Sb/Hw6hsBOZapOVF6njAESHInhlQAghVVZL1ZpIctBOoTFbQyGW+LsVYZ8lSSB3wkjA==} - engines: {node: '>=12'} - cpu: [mips64el] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-mips64el@0.21.5': optional: true - /@esbuild/linux-ppc64@0.20.2: - resolution: {integrity: sha512-rD3KsaDprDcfajSKdn25ooz5J5/fWBylaaXkuotBDGnMnDP1Uv5DLAN/45qfnf3JDYyJv/ytGHQaziHUdyzaAg==} - engines: {node: '>=12'} - cpu: [ppc64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-ppc64@0.21.5': optional: true - /@esbuild/linux-riscv64@0.20.2: - resolution: {integrity: sha512-snwmBKacKmwTMmhLlz/3aH1Q9T8v45bKYGE3j26TsaOVtjIag4wLfWSiZykXzXuE1kbCE+zJRmwp+ZbIHinnVg==} - engines: {node: '>=12'} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-riscv64@0.21.5': optional: true - /@esbuild/linux-s390x@0.20.2: - resolution: {integrity: sha512-wcWISOobRWNm3cezm5HOZcYz1sKoHLd8VL1dl309DiixxVFoFe/o8HnwuIwn6sXre88Nwj+VwZUvJf4AFxkyrQ==} - engines: {node: '>=12'} - cpu: [s390x] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-s390x@0.21.5': optional: true - /@esbuild/linux-x64@0.20.2: - resolution: {integrity: sha512-1MdwI6OOTsfQfek8sLwgyjOXAu+wKhLEoaOLTjbijk6E2WONYpH9ZU2mNtR+lZ2B4uwr+usqGuVfFT9tMtGvGw==} - engines: {node: '>=12'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@esbuild/linux-x64@0.21.5': optional: true - /@esbuild/netbsd-x64@0.20.2: - resolution: {integrity: sha512-K8/DhBxcVQkzYc43yJXDSyjlFeHQJBiowJ0uVL6Tor3jGQfSGHNNJcWxNbOI8v5k82prYqzPuwkzHt3J1T1iZQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [netbsd] - requiresBuild: true - dev: true + '@esbuild/netbsd-x64@0.21.5': optional: true - /@esbuild/openbsd-x64@0.20.2: - resolution: {integrity: sha512-eMpKlV0SThJmmJgiVyN9jTPJ2VBPquf6Kt/nAoo6DgHAoN57K15ZghiHaMvqjCye/uU4X5u3YSMgVBI1h3vKrQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [openbsd] - requiresBuild: true - dev: true + '@esbuild/openbsd-x64@0.21.5': optional: true - /@esbuild/sunos-x64@0.20.2: - resolution: {integrity: sha512-2UyFtRC6cXLyejf/YEld4Hajo7UHILetzE1vsRcGL3earZEW77JxrFjH4Ez2qaTiEfMgAXxfAZCm1fvM/G/o8w==} - engines: {node: '>=12'} - cpu: [x64] - os: [sunos] - requiresBuild: true - dev: true + '@esbuild/sunos-x64@0.21.5': optional: true - /@esbuild/win32-arm64@0.20.2: - resolution: {integrity: sha512-GRibxoawM9ZCnDxnP3usoUDO9vUkpAxIIZ6GQI+IlVmr5kP3zUq+l17xELTHMWTWzjxa2guPNyrpq1GWmPvcGQ==} - engines: {node: '>=12'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/win32-arm64@0.21.5': optional: true - /@esbuild/win32-ia32@0.20.2: - resolution: {integrity: sha512-HfLOfn9YWmkSKRQqovpnITazdtquEW8/SoHW7pWpuEeguaZI4QnCRW6b+oZTztdBnZOS2hqJ6im/D5cPzBTTlQ==} - engines: {node: '>=12'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/win32-ia32@0.21.5': optional: true - /@esbuild/win32-x64@0.20.2: - resolution: {integrity: sha512-N49X4lJX27+l9jbLKSqZ6bKNjzQvHaT8IIFUy+YIqmXQdjYCToGWwOItDrfby14c78aDd5NHQl29xingXfCdLQ==} - engines: {node: '>=12'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + '@esbuild/win32-x64@0.21.5': optional: true - /@fluentui/react-component-event-listener@0.63.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-gSMdOh6tI3IJKZFqxfQwbTpskpME0CvxdxGM2tdglmf6ZPVDi0L4+KKIm+2dN8nzb8Ya1A8ZT+Ddq0KmZtwVQg==} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 - react-dom: ^16.8.0 || ^17 || ^18 + '@fluentui/react-component-event-listener@0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + '@babel/runtime': 7.27.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /@fluentui/react-component-ref@0.63.1(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-8MkXX4+R3i80msdbD4rFpEB4WWq2UDvGwG386g3ckIWbekdvN9z2kWAd9OXhRGqB7QeOsoAGWocp6gAMCivRlw==} - peerDependencies: - react: ^16.8.0 || ^17 || ^18 - react-dom: ^16.8.0 || ^17 || ^18 + '@fluentui/react-component-ref@0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: - '@babel/runtime': 7.24.0 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + '@babel/runtime': 7.27.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) react-is: 16.13.1 - dev: false - /@jridgewell/gen-mapping@0.3.5: - resolution: {integrity: sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==} - engines: {node: '>=6.0.0'} + '@jridgewell/gen-mapping@0.3.8': dependencies: '@jridgewell/set-array': 1.2.1 - '@jridgewell/sourcemap-codec': 1.4.15 - '@jridgewell/trace-mapping': 0.3.24 - dev: true + '@jridgewell/sourcemap-codec': 1.5.0 + '@jridgewell/trace-mapping': 0.3.25 - /@jridgewell/resolve-uri@3.1.2: - resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/resolve-uri@3.1.2': {} - /@jridgewell/set-array@1.2.1: - resolution: {integrity: sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==} - engines: {node: '>=6.0.0'} - dev: true + '@jridgewell/set-array@1.2.1': {} - /@jridgewell/sourcemap-codec@1.4.15: - resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} - dev: true + '@jridgewell/sourcemap-codec@1.5.0': {} - /@jridgewell/trace-mapping@0.3.24: - resolution: {integrity: sha512-+VaWXDa6+l6MhflBvVXjIEAzb59nQ2JUK3bwRp2zRpPtU+8TFRy9Gg/5oIcNlkEL5PGlBFGfemUVvIgLnTzq7Q==} + '@jridgewell/trace-mapping@0.3.25': dependencies: '@jridgewell/resolve-uri': 3.1.2 - '@jridgewell/sourcemap-codec': 1.4.15 - dev: true + '@jridgewell/sourcemap-codec': 1.5.0 - /@mdx-js/mdx@3.0.1: - resolution: {integrity: sha512-eIQ4QTrOWyL3LWEe/bu6Taqzq2HQvHcyTMaOrI95P2/LmJE7AsfPfgJGuFLPVqBUE1BC1rik3VIhU+s9u72arA==} + '@mdx-js/mdx@3.1.0(acorn@8.14.1)': dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdx': 2.0.13 collapse-white-space: 2.1.0 devlop: 1.1.0 - estree-util-build-jsx: 3.0.1 estree-util-is-identifier-name: 3.0.0 - estree-util-to-js: 2.0.0 + estree-util-scope: 1.0.0 estree-walker: 3.0.3 - hast-util-to-estree: 3.1.0 - hast-util-to-jsx-runtime: 2.3.0 + hast-util-to-jsx-runtime: 2.3.6 markdown-extensions: 2.0.0 - periscopic: 3.1.0 - remark-mdx: 3.0.1 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.0(acorn@8.14.1) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.0 remark-parse: 11.0.0 - remark-rehype: 11.1.1 + remark-rehype: 11.1.2 source-map: 0.7.4 unified: 11.0.5 unist-util-position-from-estree: 2.0.0 @@ -768,129 +3672,50 @@ packages: unist-util-visit: 5.0.0 vfile: 6.0.3 transitivePeerDependencies: + - acorn - supports-color - dev: false - /@popperjs/core@2.11.8: - resolution: {integrity: sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A==} - dev: false + '@popperjs/core@2.11.8': {} - /@remix-run/router@1.15.2: - resolution: {integrity: sha512-+Rnav+CaoTE5QJc4Jcwh5toUpnVLKYbpU6Ys0zqbakqbaLQHeglLVHPfxOiQqdNmUy5C2lXz5dwC6tQNX2JW2Q==} - engines: {node: '>=14.0.0'} - dev: false + '@remix-run/router@1.23.0': {} - /@resvg/resvg-js-android-arm-eabi@2.4.1: - resolution: {integrity: sha512-AA6f7hS0FAPpvQMhBCf6f1oD1LdlqNXKCxAAPpKh6tR11kqV0YIB9zOlIYgITM14mq2YooLFl6XIbbvmY+jwUw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [android] - requiresBuild: true - dev: false + '@resvg/resvg-js-android-arm-eabi@2.4.1': optional: true - /@resvg/resvg-js-android-arm64@2.4.1: - resolution: {integrity: sha512-/QleoRdPfsEuH9jUjilYcDtKK/BkmWcK+1LXM8L2nsnf/CI8EnFyv7ZzCj4xAIvZGAy9dTYr/5NZBcTwxG2HQg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: false + '@resvg/resvg-js-android-arm64@2.4.1': optional: true - /@resvg/resvg-js-darwin-arm64@2.4.1: - resolution: {integrity: sha512-U1oMNhea+kAXgiEXgzo7EbFGCD1Edq5aSlQoe6LMly6UjHzgx2W3N5kEXCwU/CgN5FiQhZr7PlSJSlcr7mdhfg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: false + '@resvg/resvg-js-darwin-arm64@2.4.1': optional: true - /@resvg/resvg-js-darwin-x64@2.4.1: - resolution: {integrity: sha512-avyVh6DpebBfHHtTQTZYSr6NG1Ur6TEilk1+H0n7V+g4F7x7WPOo8zL00ZhQCeRQ5H4f8WXNWIEKL8fwqcOkYw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: false + '@resvg/resvg-js-darwin-x64@2.4.1': optional: true - /@resvg/resvg-js-linux-arm-gnueabihf@2.4.1: - resolution: {integrity: sha512-isY/mdKoBWH4VB5v621co+8l101jxxYjuTkwOLsbW+5RK9EbLciPlCB02M99ThAHzI2MYxIUjXNmNgOW8btXvw==} - engines: {node: '>= 10'} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: false + '@resvg/resvg-js-linux-arm-gnueabihf@2.4.1': optional: true - /@resvg/resvg-js-linux-arm64-gnu@2.4.1: - resolution: {integrity: sha512-uY5voSCrFI8TH95vIYBm5blpkOtltLxLRODyhKJhGfskOI7XkRw5/t1u0sWAGYD8rRSNX+CA+np86otKjubrNg==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false + '@resvg/resvg-js-linux-arm64-gnu@2.4.1': optional: true - /@resvg/resvg-js-linux-arm64-musl@2.4.1: - resolution: {integrity: sha512-6mT0+JBCsermKMdi/O2mMk3m7SqOjwi9TKAwSngRZ/nQoL3Z0Z5zV+572ztgbWr0GODB422uD8e9R9zzz38dRQ==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: false + '@resvg/resvg-js-linux-arm64-musl@2.4.1': optional: true - /@resvg/resvg-js-linux-x64-gnu@2.4.1: - resolution: {integrity: sha512-60KnrscLj6VGhkYOJEmmzPlqqfcw1keDh6U+vMcNDjPhV3B5vRSkpP/D/a8sfokyeh4VEacPSYkWGezvzS2/mg==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false + '@resvg/resvg-js-linux-x64-gnu@2.4.1': optional: true - /@resvg/resvg-js-linux-x64-musl@2.4.1: - resolution: {integrity: sha512-0AMyZSICC1D7ge115cOZQW8Pcad6PjWuZkBFF3FJuSxC6Dgok0MQnLTs2MfMdKBlAcwO9dXsf3bv9tJZj8pATA==} - engines: {node: '>= 10'} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: false + '@resvg/resvg-js-linux-x64-musl@2.4.1': optional: true - /@resvg/resvg-js-win32-arm64-msvc@2.4.1: - resolution: {integrity: sha512-76XDFOFSa3d0QotmcNyChh2xHwk+JTFiEQBVxMlHpHMeq7hNrQJ1IpE1zcHSQvrckvkdfLboKRrlGB86B10Qjw==} - engines: {node: '>= 10'} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: false + '@resvg/resvg-js-win32-arm64-msvc@2.4.1': optional: true - /@resvg/resvg-js-win32-ia32-msvc@2.4.1: - resolution: {integrity: sha512-odyVFGrEWZIzzJ89KdaFtiYWaIJh9hJRW/frcEcG3agJ464VXkN/2oEVF5ulD+5mpGlug9qJg7htzHcKxDN8sg==} - engines: {node: '>= 10'} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: false + '@resvg/resvg-js-win32-ia32-msvc@2.4.1': optional: true - /@resvg/resvg-js-win32-x64-msvc@2.4.1: - resolution: {integrity: sha512-vY4kTLH2S3bP+puU5x7hlAxHv+ulFgcK6Zn3efKSr0M0KnZ9A3qeAjZteIpkowEFfUeMPNg2dvvoFRJA9zqxSw==} - engines: {node: '>= 10'} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: false + '@resvg/resvg-js-win32-x64-msvc@2.4.1': optional: true - /@resvg/resvg-js@2.4.1: - resolution: {integrity: sha512-wTOf1zerZX8qYcMmLZw3czR4paI4hXqPjShNwJRh5DeHxvgffUS5KM7XwxtbIheUW6LVYT5fhT2AJiP6mU7U4A==} - engines: {node: '>= 10'} + '@resvg/resvg-js@2.4.1': optionalDependencies: '@resvg/resvg-js-android-arm-eabi': 2.4.1 '@resvg/resvg-js-android-arm64': 2.4.1 @@ -904,319 +3729,194 @@ packages: '@resvg/resvg-js-win32-arm64-msvc': 2.4.1 '@resvg/resvg-js-win32-ia32-msvc': 2.4.1 '@resvg/resvg-js-win32-x64-msvc': 2.4.1 - dev: false - /@rollup/rollup-android-arm-eabi@4.13.0: - resolution: {integrity: sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==} - cpu: [arm] - os: [android] - requiresBuild: true - dev: true + '@rollup/rollup-android-arm-eabi@4.39.0': optional: true - /@rollup/rollup-android-arm64@4.13.0: - resolution: {integrity: sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==} - cpu: [arm64] - os: [android] - requiresBuild: true - dev: true + '@rollup/rollup-android-arm64@4.39.0': optional: true - /@rollup/rollup-darwin-arm64@4.13.0: - resolution: {integrity: sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==} - cpu: [arm64] - os: [darwin] - requiresBuild: true - dev: true + '@rollup/rollup-darwin-arm64@4.39.0': optional: true - /@rollup/rollup-darwin-x64@4.13.0: - resolution: {integrity: sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==} - cpu: [x64] - os: [darwin] - requiresBuild: true - dev: true + '@rollup/rollup-darwin-x64@4.39.0': optional: true - /@rollup/rollup-linux-arm-gnueabihf@4.13.0: - resolution: {integrity: sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==} - cpu: [arm] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-freebsd-arm64@4.39.0': optional: true - /@rollup/rollup-linux-arm64-gnu@4.13.0: - resolution: {integrity: sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-freebsd-x64@4.39.0': optional: true - /@rollup/rollup-linux-arm64-musl@4.13.0: - resolution: {integrity: sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==} - cpu: [arm64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm-gnueabihf@4.39.0': optional: true - /@rollup/rollup-linux-riscv64-gnu@4.13.0: - resolution: {integrity: sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==} - cpu: [riscv64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm-musleabihf@4.39.0': optional: true - /@rollup/rollup-linux-x64-gnu@4.13.0: - resolution: {integrity: sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm64-gnu@4.39.0': optional: true - /@rollup/rollup-linux-x64-musl@4.13.0: - resolution: {integrity: sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==} - cpu: [x64] - os: [linux] - requiresBuild: true - dev: true + '@rollup/rollup-linux-arm64-musl@4.39.0': optional: true - /@rollup/rollup-win32-arm64-msvc@4.13.0: - resolution: {integrity: sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==} - cpu: [arm64] - os: [win32] - requiresBuild: true - dev: true + '@rollup/rollup-linux-loongarch64-gnu@4.39.0': optional: true - /@rollup/rollup-win32-ia32-msvc@4.13.0: - resolution: {integrity: sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==} - cpu: [ia32] - os: [win32] - requiresBuild: true - dev: true + '@rollup/rollup-linux-powerpc64le-gnu@4.39.0': optional: true - /@rollup/rollup-win32-x64-msvc@4.13.0: - resolution: {integrity: sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==} - cpu: [x64] - os: [win32] - requiresBuild: true - dev: true + '@rollup/rollup-linux-riscv64-gnu@4.39.0': optional: true - /@semantic-ui-react/event-stack@3.1.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-FdTmJyWvJaYinHrKRsMLDrz4tTMGdFfds299Qory53hBugiDvGC0tEJf+cHsi5igDwWb/CLOgOiChInHwq8URQ==} - peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 + '@rollup/rollup-linux-riscv64-musl@4.39.0': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.39.0': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.39.0': + optional: true + + '@rollup/rollup-linux-x64-musl@4.39.0': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.39.0': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.39.0': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.39.0': + optional: true + + '@semantic-ui-react/event-stack@3.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: exenv: 1.2.2 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /@so1ve/prettier-config@3.1.0(prettier@3.2.5): - resolution: {integrity: sha512-9GJ1yXKBC4DzqCTTaZoBf8zw7WWkVuXcccZt1Aqk4lj6ab/GiNUnjPGajUVYLjaqAEOKqM7jUSUfTjk2JTjCAg==} - peerDependencies: - prettier: ^3.0.0 + '@so1ve/prettier-config@3.1.0(prettier@3.5.3)': dependencies: - '@so1ve/prettier-plugin-toml': 3.1.0(prettier@3.2.5) - prettier: 3.2.5 + '@so1ve/prettier-plugin-toml': 3.1.0(prettier@3.5.3) + prettier: 3.5.3 prettier-plugin-astro: 0.14.1 - prettier-plugin-curly-and-jsdoc: 3.1.0(prettier@3.2.5) - prettier-plugin-pkgsort: 0.2.1(prettier@3.2.5) - dev: true + prettier-plugin-curly-and-jsdoc: 3.1.0(prettier@3.5.3) + prettier-plugin-pkgsort: 0.2.1(prettier@3.5.3) - /@so1ve/prettier-plugin-toml@3.1.0(prettier@3.2.5): - resolution: {integrity: sha512-8WZAGjAVNIJlkfWL6wHKxlUuEBY45fdd5qY5bR/Z6r/txgzKXk/r9qi1DTwc17gi/WcNuRrcRugecRT+mWbIYg==} - peerDependencies: - prettier: ^3.0.0 + '@so1ve/prettier-plugin-toml@3.1.0(prettier@3.5.3)': dependencies: - prettier: 3.2.5 - dev: true + prettier: 3.5.3 - /@turf/boolean-clockwise@6.5.0: - resolution: {integrity: sha512-45+C7LC5RMbRWrxh3Z0Eihsc8db1VGBO5d9BLTOAwU4jR6SgsunTfRWR16X7JUwIDYlCVEmnjcXJNi/kIU3VIw==} + '@turf/boolean-clockwise@6.5.0': dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 - dev: false - /@turf/clone@6.5.0: - resolution: {integrity: sha512-mzVtTFj/QycXOn6ig+annKrM6ZlimreKYz6f/GSERytOpgzodbQyOgkfwru100O1KQhhjSudKK4DsQ0oyi9cTw==} + '@turf/clone@6.5.0': dependencies: '@turf/helpers': 6.5.0 - dev: false - /@turf/flatten@6.5.0: - resolution: {integrity: sha512-IBZVwoNLVNT6U/bcUUllubgElzpMsNoCw8tLqBw6dfYg9ObGmpEjf9BIYLr7a2Yn5ZR4l7YIj2T7kD5uJjZADQ==} + '@turf/flatten@6.5.0': dependencies: '@turf/helpers': 6.5.0 '@turf/meta': 6.5.0 - dev: false - /@turf/helpers@6.5.0: - resolution: {integrity: sha512-VbI1dV5bLFzohYYdgqwikdMVpe7pJ9X3E+dlr425wa2/sMJqYDhTO++ec38/pcPvPE6oD9WEEeU3Xu3gza+VPw==} - dev: false + '@turf/helpers@6.5.0': {} - /@turf/invariant@6.5.0: - resolution: {integrity: sha512-Wv8PRNCtPD31UVbdJE/KVAWKe7l6US+lJItRR/HOEW3eh+U/JwRCSUl/KZ7bmjM/C+zLNoreM2TU6OoLACs4eg==} + '@turf/invariant@6.5.0': dependencies: '@turf/helpers': 6.5.0 - dev: false - /@turf/meta@3.14.0: - resolution: {integrity: sha512-OtXqLQuR9hlQ/HkAF/OdzRea7E0eZK1ay8y8CBXkoO2R6v34CsDrWYLMSo0ZzMsaQDpKo76NPP2GGo+PyG1cSg==} - dev: false + '@turf/meta@3.14.0': {} - /@turf/meta@6.5.0: - resolution: {integrity: sha512-RrArvtsV0vdsCBegoBtOalgdSOfkBrTJ07VkpiCnq/491W67hnMWmDu7e6Ztw0C3WldRYTXkg3SumfdzZxLBHA==} + '@turf/meta@6.5.0': dependencies: '@turf/helpers': 6.5.0 - dev: false - /@turf/rewind@6.5.0: - resolution: {integrity: sha512-IoUAMcHWotBWYwSYuYypw/LlqZmO+wcBpn8ysrBNbazkFNkLf3btSDZMkKJO/bvOzl55imr/Xj4fi3DdsLsbzQ==} + '@turf/rewind@6.5.0': dependencies: '@turf/boolean-clockwise': 6.5.0 '@turf/clone': 6.5.0 '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 '@turf/meta': 6.5.0 - dev: false - /@types/acorn@4.0.6: - resolution: {integrity: sha512-veQTnWP+1D/xbxVrPC3zHnCZRjSrKfhbMUlEA43iMZLu7EsnTtkJklIuwrCPbOi8YkvDQAiW05VQQFvvz9oieQ==} + '@types/babel__core@7.20.5': dependencies: - '@types/estree': 1.0.5 - dev: false - - /@types/babel__core@7.20.5: - resolution: {integrity: sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==} - dependencies: - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 '@types/babel__generator': 7.6.8 '@types/babel__template': 7.4.4 - '@types/babel__traverse': 7.20.5 - dev: true + '@types/babel__traverse': 7.20.7 - /@types/babel__generator@7.6.8: - resolution: {integrity: sha512-ASsj+tpEDsEiFr1arWrlN6V3mdfjRMZt6LtK/Vp/kreFLnr5QH5+DhvD5nINYZXzwJvXeGq+05iUXcAzVrqWtw==} + '@types/babel__generator@7.6.8': dependencies: - '@babel/types': 7.24.0 - dev: true + '@babel/types': 7.27.0 - /@types/babel__template@7.4.4: - resolution: {integrity: sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==} + '@types/babel__template@7.4.4': dependencies: - '@babel/parser': 7.24.0 - '@babel/types': 7.24.0 - dev: true + '@babel/parser': 7.27.0 + '@babel/types': 7.27.0 - /@types/babel__traverse@7.20.5: - resolution: {integrity: sha512-WXCyOcRtH37HAUkpXhUduaxdm82b4GSlyTqajXviN4EfiuPgNYR109xMCKvpl6zPIpua0DGlMEDCq+g8EdoheQ==} + '@types/babel__traverse@7.20.7': dependencies: - '@babel/types': 7.24.0 - dev: true + '@babel/types': 7.27.0 - /@types/debug@4.1.12: - resolution: {integrity: sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==} + '@types/debug@4.1.12': dependencies: - '@types/ms': 0.7.34 - dev: false + '@types/ms': 2.1.0 - /@types/estree-jsx@1.0.5: - resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + '@types/estree-jsx@1.0.5': dependencies: - '@types/estree': 1.0.5 - dev: false + '@types/estree': 1.0.7 - /@types/estree@1.0.5: - resolution: {integrity: sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==} + '@types/estree@1.0.7': {} - /@types/hast@3.0.4: - resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + '@types/hast@3.0.4': dependencies: '@types/unist': 3.0.3 - dev: false - /@types/mdast@4.0.4: - resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + '@types/mdast@4.0.4': dependencies: '@types/unist': 3.0.3 - dev: false - /@types/mdx@2.0.13: - resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} - dev: false + '@types/mdx@2.0.13': {} - /@types/ms@0.7.34: - resolution: {integrity: sha512-nG96G3Wp6acyAgJqGasjODb+acrI7KltPiRxzHPXnP3NgI28bpQDRv53olbqGXbfcgF5aiiHmO3xpwEpS5Ld9g==} - dev: false + '@types/ms@2.1.0': {} - /@types/parse-author@2.0.3: - resolution: {integrity: sha512-pgRW2K/GVQoogylrGJXDl7PBLW9A6T4OOc9Hy9MLT5f7vgufK2GQ8FcfAbjFHR5HjcN9ByzuCczAORk49REqoA==} - dev: true + '@types/parse-author@2.0.3': {} - /@types/parse-json@4.0.2: - resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==} - dev: true + '@types/parse-json@4.0.2': {} - /@types/unist@2.0.11: - resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} - dev: false + '@types/unist@2.0.11': {} - /@types/unist@3.0.3: - resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} - dev: false + '@types/unist@3.0.3': {} - /@ungap/structured-clone@1.2.0: - resolution: {integrity: sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==} - dev: false + '@ungap/structured-clone@1.3.0': {} - /@visactor/react-vchart@1.8.11(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-wHnCex9gOpnttTtSu04ozKJhTveUk8Ln2KX/7PZyCJxqlXq+eWvW4zvM6Ja8T8kGXfXtFYVVNh9zBMQ7y2T/Sw==} - peerDependencies: - react: '>=16.0.0' - react-dom: '>=16.0.0' + '@visactor/react-vchart@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': dependencies: '@visactor/vchart': 1.8.11 '@visactor/vgrammar-core': 0.10.11 '@visactor/vrender-core': 0.17.17 '@visactor/vrender-kits': 0.17.17 '@visactor/vutils': 0.17.5 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-is: 18.2.0 - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 - /@visactor/vchart-semi-theme@1.8.8(@visactor/vchart@1.8.11): - resolution: {integrity: sha512-lm57CX3r6Bm7iGBYYyWhDY+1BvkyhNVLEckKx2PnlPKpJHikKSIK2ACyI5SmHuSOOdYzhY2QK6ZfYa2NShJ83w==} - peerDependencies: - '@visactor/vchart': ~1.8.8 + '@visactor/vchart-semi-theme@1.8.8(@visactor/vchart@1.8.11)': dependencies: '@visactor/vchart': 1.8.11 '@visactor/vchart-theme-utils': 1.8.8(@visactor/vchart@1.8.11) - dev: false - /@visactor/vchart-theme-utils@1.8.8(@visactor/vchart@1.8.11): - resolution: {integrity: sha512-RdCey3/t0+82EYyFZvx210rgJJWti9rsgcL3ROZS7o9CtRW1CMj9u9LKLDNIcPLNcLNACFC0aoT03jpdD1BCpA==} - peerDependencies: - '@visactor/vchart': ~1.8.8 + '@visactor/vchart-theme-utils@1.8.8(@visactor/vchart@1.8.11)': dependencies: '@visactor/vchart': 1.8.11 - dev: false - /@visactor/vchart@1.8.11: - resolution: {integrity: sha512-RdQ822J02GgAQNXvO1LiT0T3O6FjdgPdcm9hVBFyrpBBmuI8MH02IE7Y1kGe9NiFTH4tDwP0ixRgBmqNSGSLZQ==} + '@visactor/vchart@1.8.11': dependencies: '@visactor/vdataset': 0.17.5 '@visactor/vgrammar-core': 0.10.11 @@ -1232,10 +3932,8 @@ packages: '@visactor/vscale': 0.17.5 '@visactor/vutils': 0.17.5 '@visactor/vutils-extension': 1.8.11 - dev: false - /@visactor/vdataset@0.17.5: - resolution: {integrity: sha512-zVBdLWHWrhldGc8JDjSYF9lvpFT4ZEFQDB0b6yvfSiHzHKHiSco+rWmUFvA7r4ObT6j2QWF1vZAV9To8Ml4vHw==} + '@visactor/vdataset@0.17.5': dependencies: '@turf/flatten': 6.5.0 '@turf/helpers': 6.5.0 @@ -1249,22 +3947,18 @@ packages: geobuf: 3.0.2 geojson-dissolve: 3.1.0 path-browserify: 1.0.1 - pbf: 3.2.1 + pbf: 3.3.0 point-at-length: 1.1.0 - simple-statistics: 7.8.3 + simple-statistics: 7.8.8 simplify-geojson: 1.0.5 topojson-client: 3.1.0 - dev: false - /@visactor/vgrammar-coordinate@0.10.11: - resolution: {integrity: sha512-XSUvEkaf/NQHFafmTwqoIMZicp9fF3o6NB2FDpuWrK4DI1lTuip/0RkqrC+kBAjc5erjt0em0TiITyqXpp4G6w==} + '@visactor/vgrammar-coordinate@0.10.11': dependencies: '@visactor/vgrammar-util': 0.10.11 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vgrammar-core@0.10.11: - resolution: {integrity: sha512-VL9vcLPDg1LrHl7EOx0Ga9ATsoaChKIaCGzxjrPEjWiIS5VPU9Rs0jBKP+ch8BjamAoSuqL5mKd0L/RaUBqlaA==} + '@visactor/vgrammar-core@0.10.11': dependencies: '@visactor/vdataset': 0.17.5 '@visactor/vgrammar-coordinate': 0.10.11 @@ -1274,45 +3968,35 @@ packages: '@visactor/vrender-kits': 0.17.17 '@visactor/vscale': 0.17.5 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vgrammar-hierarchy@0.10.11: - resolution: {integrity: sha512-0r3k51pPlJHu63BduG3htsV/ul62aVcKJxFftRfvKkwGjm1KeHoOZEEAwIf78U2puio0BkLqVn2Ek2L4FYZaIg==} + '@visactor/vgrammar-hierarchy@0.10.11': dependencies: '@visactor/vgrammar-core': 0.10.11 '@visactor/vgrammar-util': 0.10.11 '@visactor/vrender-core': 0.17.17 '@visactor/vrender-kits': 0.17.17 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vgrammar-projection@0.10.11: - resolution: {integrity: sha512-yEiKsxdfs5+g60wv5xZ1kyS/EDrAsUzAxCMpFFASVUYbQObHvW+elm+UPq2TBX6KZqAM0gsd1inzaLvfsCrLSg==} + '@visactor/vgrammar-projection@0.10.11': dependencies: '@visactor/vgrammar-core': 0.10.11 '@visactor/vgrammar-util': 0.10.11 '@visactor/vutils': 0.17.5 d3-geo: 1.12.1 - dev: false - /@visactor/vgrammar-sankey@0.10.11: - resolution: {integrity: sha512-BbJTPuyydsL/L5XtQv59Q82GgJeePY7Wleac798usx3GnDK0GAOrPsI3bubSsOESJ4pNk3V4HPGEQDG1vCPb4w==} + '@visactor/vgrammar-sankey@0.10.11': dependencies: '@visactor/vgrammar-core': 0.10.11 '@visactor/vgrammar-util': 0.10.11 '@visactor/vrender-core': 0.17.17 '@visactor/vrender-kits': 0.17.17 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vgrammar-util@0.10.11: - resolution: {integrity: sha512-cJZLmKZvN95Y+yGhX+28+UpZu3bhYYlXDlHJNvXHyonI76ZYgtceyon2b3lI6XIsUsBGcD4Uo777s949X5os3g==} + '@visactor/vgrammar-util@0.10.11': dependencies: '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vgrammar-wordcloud-shape@0.10.11: - resolution: {integrity: sha512-NsQOYJp+9WHnIApMvkcUOaajxIg5U/r6rD8LKnoXW/HqAN2TFYXcRR3Daqmk9rrpM5VztQimKOsA1yZWyzozrA==} + '@visactor/vgrammar-wordcloud-shape@0.10.11': dependencies: '@visactor/vgrammar-core': 0.10.11 '@visactor/vgrammar-util': 0.10.11 @@ -1320,617 +4004,415 @@ packages: '@visactor/vrender-kits': 0.17.17 '@visactor/vscale': 0.17.5 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vgrammar-wordcloud@0.10.11: - resolution: {integrity: sha512-JWDqjGhr9JlYkKVBeEkiOqLQk7C1x1BtnsZ+E8oN541gzUqHwfS9qZyhwI3OyoSLewJlsSSPu1vXLKSQzLzKPA==} + '@visactor/vgrammar-wordcloud@0.10.11': dependencies: '@visactor/vgrammar-core': 0.10.11 '@visactor/vgrammar-util': 0.10.11 '@visactor/vrender-core': 0.17.17 '@visactor/vrender-kits': 0.17.17 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vrender-components@0.17.17: - resolution: {integrity: sha512-7gYFQrozvBkyGF7s/JHXdWDZnATzymxzug63CZd4EB7A0OXKatVDImXRePqwzlPD3QamF7QMVWn0CuIx3gQ2gA==} + '@visactor/vrender-components@0.17.17': dependencies: '@visactor/vrender-core': 0.17.17 '@visactor/vrender-kits': 0.17.17 '@visactor/vscale': 0.17.5 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vrender-core@0.17.17: - resolution: {integrity: sha512-pAZGaimunDAWOBdFhzPh0auH5ryxAHr+MVoz+QdASG+6RZXy8D02l8v2QYu4+e4uorxe/s2ZkdNDm81SlNkoHQ==} + '@visactor/vrender-core@0.17.17': dependencies: '@visactor/vutils': 0.17.5 color-convert: 2.0.1 - dev: false - /@visactor/vrender-kits@0.17.17: - resolution: {integrity: sha512-noRP1hAHvPCv36nf2P6sZ930Tk+dJ8jpPWIUm1cFYmUNdcumgIS8Cug0RyeZ+saSqVt5FDTwIwifhOqupw5Zaw==} + '@visactor/vrender-kits@0.17.17': dependencies: '@resvg/resvg-js': 2.4.1 '@visactor/vrender-core': 0.17.17 '@visactor/vutils': 0.17.5 roughjs: 4.5.2 - dev: false - /@visactor/vscale@0.17.5: - resolution: {integrity: sha512-2dkS1IlAJ/IdTp8JElbctOOv6lkHKBKPDm8KvwBo0NuGWQeYAebSeyN3QCdwKbj76gMlCub4zc+xWrS5YiA2zA==} + '@visactor/vscale@0.17.5': dependencies: '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vutils-extension@1.8.11: - resolution: {integrity: sha512-Hknzpy3+xh4sdL0iSn5N93BHiMJF4FdwSwhHYEibRpriZmWKG6wBxsJ0Bll4d7oS4f+svxt8Sg2vRYKzQEcIxQ==} + '@visactor/vutils-extension@1.8.11': dependencies: '@visactor/vrender-core': 0.17.17 '@visactor/vrender-kits': 0.17.17 '@visactor/vscale': 0.17.5 '@visactor/vutils': 0.17.5 - dev: false - /@visactor/vutils@0.17.5: - resolution: {integrity: sha512-HFN6Pk1Wc1RK842g02MeKOlvdri5L7/nqxMVTqxIvi0XMhHXpmoqN4+/9H+h8LmJpVohyrI/MT85TRBV/rManw==} + '@visactor/vutils@0.17.5': dependencies: '@turf/helpers': 6.5.0 '@turf/invariant': 6.5.0 eventemitter3: 4.0.7 - dev: false - /@vitejs/plugin-react@4.2.1(vite@5.2.5): - resolution: {integrity: sha512-oojO9IDc4nCUUi8qIR11KoQm0XFFLIwsRBwHRR4d/88IWghn1y6ckz/bJ8GHDCsYEJee8mDzqtJxh15/cisJNQ==} - engines: {node: ^14.18.0 || >=16.0.0} - peerDependencies: - vite: ^4.2.0 || ^5.0.0 + '@vitejs/plugin-react@4.3.4(vite@5.4.16)': dependencies: - '@babel/core': 7.24.0 - '@babel/plugin-transform-react-jsx-self': 7.24.1(@babel/core@7.24.0) - '@babel/plugin-transform-react-jsx-source': 7.24.1(@babel/core@7.24.0) + '@babel/core': 7.26.10 + '@babel/plugin-transform-react-jsx-self': 7.25.9(@babel/core@7.26.10) + '@babel/plugin-transform-react-jsx-source': 7.25.9(@babel/core@7.26.10) '@types/babel__core': 7.20.5 - react-refresh: 0.14.0 - vite: 5.2.5 + react-refresh: 0.14.2 + vite: 5.4.16 transitivePeerDependencies: - supports-color - dev: true - /abs-svg-path@0.1.1: - resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} - dev: false + abs-svg-path@0.1.1: {} - /acorn-jsx@5.3.2(acorn@8.12.1): - resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} - peerDependencies: - acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + acorn-jsx@5.3.2(acorn@8.14.1): dependencies: - acorn: 8.12.1 - dev: false + acorn: 8.14.1 - /acorn@8.12.1: - resolution: {integrity: sha512-tcpGyI9zbizT9JbV6oYE477V6mTlXvvi0T0G3SNIYE2apm/G5huBa1+K89VGeovbg+jycCrfhl3ADxErOuO6Jg==} - engines: {node: '>=0.4.0'} - hasBin: true - dev: false + acorn@8.14.1: {} - /ansi-styles@3.2.1: - resolution: {integrity: sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==} - engines: {node: '>=4'} + array-source@0.0.4: {} + + astring@1.9.0: {} + + async-validator@3.5.2: {} + + asynckit@0.4.0: {} + + attr-accept@2.2.5: {} + + author-regex@1.0.0: {} + + axios@0.27.2: dependencies: - color-convert: 1.9.3 - dev: true - - /array-source@0.0.4: - resolution: {integrity: sha512-frNdc+zBn80vipY+GdcJkLEbMWj3xmzArYApmUGxoiV8uAu/ygcs9icPdsGdA26h0MkHUMW6EN2piIvVx+M5Mw==} - dev: false - - /astring@1.9.0: - resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} - hasBin: true - dev: false - - /async-validator@3.5.2: - resolution: {integrity: sha512-8eLCg00W9pIRZSB781UUX/H6Oskmm8xloZfr09lz5bikRpBVDlJ3hRVuxxP1SxcwsEYfJ4IU8Q19Y8/893r3rQ==} - dev: false - - /asynckit@0.4.0: - resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==} - dev: false - - /attr-accept@2.2.2: - resolution: {integrity: sha512-7prDjvt9HmqiZ0cl5CRjtS84sEyhsHP2coDkaZKRKVfCDo9s7iw7ChVmar78Gu9pC4SoR/28wFu/G5JJhTnqEg==} - engines: {node: '>=4'} - dev: false - - /author-regex@1.0.0: - resolution: {integrity: sha512-KbWgR8wOYRAPekEmMXrYYdc7BRyhn2Ftk7KWfMUnQ43hFdojWEFRxhhRUm3/OFEdPa1r0KAvTTg9YQK57xTe0g==} - engines: {node: '>=0.8'} - dev: true - - /axios@0.27.2: - resolution: {integrity: sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==} - dependencies: - follow-redirects: 1.15.5 - form-data: 4.0.0 + follow-redirects: 1.15.9 + form-data: 4.0.2 transitivePeerDependencies: - debug - dev: false - /bail@2.0.2: - resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} - dev: false + bail@2.0.2: {} - /balanced-match@1.0.2: - resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} - dev: true + balanced-match@1.0.2: {} - /bezier-easing@2.1.0: - resolution: {integrity: sha512-gbIqZ/eslnUFC1tjEvtz0sgx+xTK20wDnYMIA27VA04R7w6xxXQPZDbibjA9DTWZRA2CXtwHykkVzlCaAJAZig==} - dev: false + bezier-easing@2.1.0: {} - /brace-expansion@1.1.11: - resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} + brace-expansion@1.1.11: dependencies: balanced-match: 1.0.2 concat-map: 0.0.1 - dev: true - /browserslist@4.23.0: - resolution: {integrity: sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==} - engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} - hasBin: true + browserslist@4.24.4: dependencies: - caniuse-lite: 1.0.30001591 - electron-to-chromium: 1.4.690 - node-releases: 2.0.14 - update-browserslist-db: 1.0.13(browserslist@4.23.0) - dev: true + caniuse-lite: 1.0.30001709 + electron-to-chromium: 1.5.130 + node-releases: 2.0.19 + update-browserslist-db: 1.1.3(browserslist@4.24.4) - /buffer-from@1.1.2: - resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} - dev: false + buffer-from@1.1.2: {} - /callsites@3.1.0: - resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} - engines: {node: '>=6'} - dev: true - - /caniuse-lite@1.0.30001591: - resolution: {integrity: sha512-PCzRMei/vXjJyL5mJtzNiUCKP59dm8Apqc3PH8gJkMnMXZGox93RbE76jHsmLwmIo6/3nsYIpJtx0O7u5PqFuQ==} - dev: true - - /ccount@2.0.1: - resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} - dev: false - - /chalk@2.4.2: - resolution: {integrity: sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==} - engines: {node: '>=4'} + call-bind-apply-helpers@1.0.2: dependencies: - ansi-styles: 3.2.1 - escape-string-regexp: 1.0.5 - supports-color: 5.5.0 - dev: true + es-errors: 1.3.0 + function-bind: 1.1.2 - /character-entities-html4@2.1.0: - resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} - dev: false + callsites@3.1.0: {} - /character-entities-legacy@3.0.0: - resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} - dev: false + caniuse-lite@1.0.30001709: {} - /character-entities@2.0.2: - resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} - dev: false + ccount@2.0.1: {} - /character-reference-invalid@2.0.1: - resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} - dev: false + character-entities-html4@2.1.0: {} - /classnames@2.5.1: - resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} - dev: false + character-entities-legacy@3.0.0: {} - /clsx@1.2.1: - resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==} - engines: {node: '>=6'} - dev: false + character-entities@2.0.2: {} - /collapse-white-space@2.1.0: - resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} - dev: false + character-reference-invalid@2.0.1: {} - /color-convert@1.9.3: - resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==} - dependencies: - color-name: 1.1.3 - dev: true + classnames@2.5.1: {} - /color-convert@2.0.1: - resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} - engines: {node: '>=7.0.0'} + clsx@1.2.1: {} + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: dependencies: color-name: 1.1.4 - dev: false - /color-name@1.1.3: - resolution: {integrity: sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==} - dev: true + color-name@1.1.4: {} - /color-name@1.1.4: - resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - dev: false - - /combined-stream@1.0.8: - resolution: {integrity: sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==} - engines: {node: '>= 0.8'} + combined-stream@1.0.8: dependencies: delayed-stream: 1.0.0 - dev: false - /comma-separated-tokens@2.0.3: - resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} - dev: false + comma-separated-tokens@2.0.3: {} - /commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - dev: false + commander@2.20.3: {} - /commander@4.1.1: - resolution: {integrity: sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==} - engines: {node: '>= 6'} - dev: true + commander@4.1.1: {} - /compute-scroll-into-view@1.0.20: - resolution: {integrity: sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==} - dev: false + compute-scroll-into-view@1.0.20: {} - /concat-map@0.0.1: - resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} - dev: true + concat-map@0.0.1: {} - /concat-stream@1.4.11: - resolution: {integrity: sha512-X3JMh8+4je3U1cQpG87+f9lXHDrqcb2MVLg9L7o8b1UZ0DzhRrUpdn65ttzu10PpJPPI3MQNkis+oha6TSA9Mw==} - engines: {'0': node >= 0.8} + concat-stream@1.4.11: dependencies: inherits: 2.0.4 readable-stream: 1.1.14 typedarray: 0.0.7 - dev: false - /concat-stream@2.0.0: - resolution: {integrity: sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==} - engines: {'0': node >= 6.0} + concat-stream@2.0.0: dependencies: buffer-from: 1.1.2 inherits: 2.0.4 readable-stream: 3.6.2 typedarray: 0.0.6 - dev: false - /convert-source-map@2.0.0: - resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} - dev: true + convert-source-map@2.0.0: {} - /copy-text-to-clipboard@2.2.0: - resolution: {integrity: sha512-WRvoIdnTs1rgPMkgA2pUOa/M4Enh2uzCwdKsOMYNAJiz/4ZvEJgmbF4OmninPmlFdAWisfeh0tH+Cpf7ni3RqQ==} - engines: {node: '>=6'} - dev: false + copy-text-to-clipboard@2.2.0: {} - /core-util-is@1.0.3: - resolution: {integrity: sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==} - dev: false + core-util-is@1.0.3: {} - /cosmiconfig@7.1.0: - resolution: {integrity: sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==} - engines: {node: '>=10'} + cosmiconfig@7.1.0: dependencies: '@types/parse-json': 4.0.2 - import-fresh: 3.3.0 + import-fresh: 3.3.1 parse-json: 5.2.0 path-type: 4.0.0 yaml: 1.10.2 - dev: true - /d3-array@1.2.4: - resolution: {integrity: sha512-KHW6M86R+FUPYGb3R5XiYjXPq7VzwxZ22buHhAEVG5ztoEcZZMLov530mmccaqA1GghZArjQV46fuc8kUqhhHw==} - dev: false + d3-array@1.2.4: {} - /d3-dsv@2.0.0: - resolution: {integrity: sha512-E+Pn8UJYx9mViuIUkoc93gJGGYut6mSDKy2+XaPwccwkRGlR+LO97L2VCCRjQivTwLHkSnAJG7yo00BWY6QM+w==} - hasBin: true + d3-dsv@2.0.0: dependencies: commander: 2.20.3 iconv-lite: 0.4.24 rw: 1.3.3 - dev: false - /d3-geo@1.12.1: - resolution: {integrity: sha512-XG4d1c/UJSEX9NfU02KwBL6BYPj8YKHxgBEw5om2ZnTRSbIcego6dhHwcxuSR3clxh0EpE38os1DVPOmnYtTPg==} + d3-geo@1.12.1: dependencies: d3-array: 1.2.4 - dev: false - /d3-hexbin@0.2.2: - resolution: {integrity: sha512-KS3fUT2ReD4RlGCjvCEm1RgMtp2NFZumdMu4DBzQK8AZv3fXRM6Xm8I4fSU07UXvH4xxg03NwWKWdvxfS/yc4w==} - dev: false + d3-hexbin@0.2.2: {} - /d3-hierarchy@3.1.2: - resolution: {integrity: sha512-FX/9frcub54beBdugHjDCdikxThEqjnR93Qt7PvQTOHxyiNCAlvMrHhclk3cD5VeAaq9fxmfRp+CnWw9rEMBuA==} - engines: {node: '>=12'} - dev: false + d3-hierarchy@3.1.2: {} - /date-fns-tz@1.3.8(date-fns@2.30.0): - resolution: {integrity: sha512-qwNXUFtMHTTU6CFSFjoJ80W8Fzzp24LntbjFFBgL/faqds4e5mo9mftoRLgr3Vi1trISsg4awSpYVsOQCRnapQ==} - peerDependencies: - date-fns: '>=2.0.0' + date-fns-tz@1.3.8(date-fns@2.30.0): dependencies: date-fns: 2.30.0 - dev: false - /date-fns@2.30.0: - resolution: {integrity: sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==} - engines: {node: '>=0.11'} + date-fns@2.30.0: dependencies: - '@babel/runtime': 7.24.0 - dev: false + '@babel/runtime': 7.27.0 - /dayjs@1.11.11: - resolution: {integrity: sha512-okzr3f11N6WuqYtZSvm+F776mB41wRZMhKP+hc34YdW+KmtYYK9iqvHSwo2k9FEH3fhGXvOPV6yz2IcSrfRUDg==} - dev: false + dayjs@1.11.13: {} - /debug@4.3.4: - resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} - engines: {node: '>=6.0'} - peerDependencies: - supports-color: '*' - peerDependenciesMeta: - supports-color: - optional: true + debug@4.4.0: dependencies: - ms: 2.1.2 + ms: 2.1.3 - /decode-named-character-reference@1.0.2: - resolution: {integrity: sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg==} + decode-named-character-reference@1.1.0: dependencies: character-entities: 2.0.2 - dev: false - /delayed-stream@1.0.0: - resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} - engines: {node: '>=0.4.0'} - dev: false + delayed-stream@1.0.0: {} - /dequal@2.0.3: - resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} - engines: {node: '>=6'} - dev: false + dequal@2.0.3: {} - /devlop@1.1.0: - resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + devlop@1.1.0: dependencies: dequal: 2.0.3 - dev: false - /electron-to-chromium@1.4.690: - resolution: {integrity: sha512-+2OAGjUx68xElQhydpcbqH50hE8Vs2K6TkAeLhICYfndb67CVH0UsZaijmRUE3rHlIxU1u0jxwhgVe6fK3YANA==} - dev: true + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 - /error-ex@1.3.2: - resolution: {integrity: sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==} + electron-to-chromium@1.5.130: {} + + error-ex@1.3.2: dependencies: is-arrayish: 0.2.1 - dev: true - /esbuild@0.20.2: - resolution: {integrity: sha512-WdOOppmUNU+IbZ0PaDiTst80zjnrOkyJNHoKupIcVyU8Lvla3Ugx94VzkQ32Ijqd7UhHJy75gNWDMUekcrSJ6g==} - engines: {node: '>=12'} - hasBin: true - requiresBuild: true - optionalDependencies: - '@esbuild/aix-ppc64': 0.20.2 - '@esbuild/android-arm': 0.20.2 - '@esbuild/android-arm64': 0.20.2 - '@esbuild/android-x64': 0.20.2 - '@esbuild/darwin-arm64': 0.20.2 - '@esbuild/darwin-x64': 0.20.2 - '@esbuild/freebsd-arm64': 0.20.2 - '@esbuild/freebsd-x64': 0.20.2 - '@esbuild/linux-arm': 0.20.2 - '@esbuild/linux-arm64': 0.20.2 - '@esbuild/linux-ia32': 0.20.2 - '@esbuild/linux-loong64': 0.20.2 - '@esbuild/linux-mips64el': 0.20.2 - '@esbuild/linux-ppc64': 0.20.2 - '@esbuild/linux-riscv64': 0.20.2 - '@esbuild/linux-s390x': 0.20.2 - '@esbuild/linux-x64': 0.20.2 - '@esbuild/netbsd-x64': 0.20.2 - '@esbuild/openbsd-x64': 0.20.2 - '@esbuild/sunos-x64': 0.20.2 - '@esbuild/win32-arm64': 0.20.2 - '@esbuild/win32-ia32': 0.20.2 - '@esbuild/win32-x64': 0.20.2 - dev: true + es-define-property@1.0.1: {} - /escalade@3.1.2: - resolution: {integrity: sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==} - engines: {node: '>=6'} - dev: true + es-errors@1.3.0: {} - /escape-string-regexp@1.0.5: - resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} - engines: {node: '>=0.8.0'} - dev: true - - /escape-string-regexp@5.0.0: - resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} - engines: {node: '>=12'} - dev: false - - /estree-util-attach-comments@3.0.0: - resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + es-object-atoms@1.1.1: dependencies: - '@types/estree': 1.0.5 - dev: false + es-errors: 1.3.0 - /estree-util-build-jsx@3.0.1: - resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + es-set-tostringtag@2.1.0: + dependencies: + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + has-tostringtag: 1.0.2 + hasown: 2.0.2 + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.14.1 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.2 + + esbuild@0.21.5: + optionalDependencies: + '@esbuild/aix-ppc64': 0.21.5 + '@esbuild/android-arm': 0.21.5 + '@esbuild/android-arm64': 0.21.5 + '@esbuild/android-x64': 0.21.5 + '@esbuild/darwin-arm64': 0.21.5 + '@esbuild/darwin-x64': 0.21.5 + '@esbuild/freebsd-arm64': 0.21.5 + '@esbuild/freebsd-x64': 0.21.5 + '@esbuild/linux-arm': 0.21.5 + '@esbuild/linux-arm64': 0.21.5 + '@esbuild/linux-ia32': 0.21.5 + '@esbuild/linux-loong64': 0.21.5 + '@esbuild/linux-mips64el': 0.21.5 + '@esbuild/linux-ppc64': 0.21.5 + '@esbuild/linux-riscv64': 0.21.5 + '@esbuild/linux-s390x': 0.21.5 + '@esbuild/linux-x64': 0.21.5 + '@esbuild/netbsd-x64': 0.21.5 + '@esbuild/openbsd-x64': 0.21.5 + '@esbuild/sunos-x64': 0.21.5 + '@esbuild/win32-arm64': 0.21.5 + '@esbuild/win32-ia32': 0.21.5 + '@esbuild/win32-x64': 0.21.5 + + escalade@3.2.0: {} + + escape-string-regexp@5.0.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.7 + + estree-util-build-jsx@3.0.1: dependencies: '@types/estree-jsx': 1.0.5 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 estree-walker: 3.0.3 - dev: false - /estree-util-is-identifier-name@3.0.0: - resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} - dev: false + estree-util-is-identifier-name@3.0.0: {} - /estree-util-to-js@2.0.0: - resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.7 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 astring: 1.9.0 source-map: 0.7.4 - dev: false - /estree-util-visit@2.0.0: - resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + estree-util-visit@2.0.0: dependencies: '@types/estree-jsx': 1.0.5 '@types/unist': 3.0.3 - dev: false - /estree-walker@3.0.3: - resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + estree-walker@3.0.3: dependencies: - '@types/estree': 1.0.5 - dev: false + '@types/estree': 1.0.7 - /eventemitter3@4.0.7: - resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} - dev: false + eventemitter3@4.0.7: {} - /exenv@1.2.2: - resolution: {integrity: sha512-Z+ktTxTwv9ILfgKCk32OX3n/doe+OcLTRtqK9pcL+JsP3J1/VW8Uvl4ZjLlKqeW4rzK4oesDOGMEMRIZqtP4Iw==} - dev: false + exenv@1.2.2: {} - /extend@3.0.2: - resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} - dev: false + extend@3.0.2: {} - /fast-copy@3.0.1: - resolution: {integrity: sha512-Knr7NOtK3HWRYGtHoJrjkaWepqT8thIVGAwt0p0aUs1zqkAzXZV4vo9fFNwyb5fcqK1GKYFYxldQdIDVKhUAfA==} - dev: false + fast-copy@3.0.2: {} - /file-selector@0.6.0: - resolution: {integrity: sha512-QlZ5yJC0VxHxQQsQhXvBaC7VRJ2uaxTf+Tfpu4Z/OcVQJVpZO+DGU0rkoVW5ce2SccxugvpBJoMvUs59iILYdw==} - engines: {node: '>= 12'} + file-selector@2.1.2: dependencies: - tslib: 2.6.2 - dev: false + tslib: 2.8.1 - /file-source@0.6.1: - resolution: {integrity: sha512-1R1KneL7eTXmXfKxC10V/9NeGOdbsAXJ+lQ//fvvcHUgtaZcZDWNJNblxAoVOyV1cj45pOtUrR3vZTBwqcW8XA==} + file-source@0.6.1: dependencies: stream-source: 0.3.5 - dev: false - /follow-redirects@1.15.5: - resolution: {integrity: sha512-vSFWUON1B+yAw1VN4xMfxgn5fTUiaOzAJCKBwIIgT/+7CuGy9+r+5gITvP62j3RmaD5Ph65UaERdOSRGUzZtgw==} - engines: {node: '>=4.0'} - peerDependencies: - debug: '*' - peerDependenciesMeta: - debug: - optional: true - dev: false + follow-redirects@1.15.9: {} - /form-data@4.0.0: - resolution: {integrity: sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==} - engines: {node: '>= 6'} + form-data@4.0.2: dependencies: asynckit: 0.4.0 combined-stream: 1.0.8 + es-set-tostringtag: 2.1.0 mime-types: 2.1.35 - dev: false - /fs-extra@10.1.0: - resolution: {integrity: sha512-oRXApq54ETRj4eMiFzGnHWGy+zo5raudjuxN0b8H7s/RU2oW0Wvsx9O0ACRN/kRq9E8Vu/ReskGB5o3ji+FzHQ==} - engines: {node: '>=12'} + fs-extra@10.1.0: dependencies: graceful-fs: 4.2.11 jsonfile: 6.1.0 universalify: 2.0.1 - dev: true - /fs-extra@4.0.3: - resolution: {integrity: sha512-q6rbdDd1o2mAnQreO7YADIxf/Whx4AHBiRf6d+/cVT8h44ss+lHgxf1FemcqDnQt9X3ct4McHr+JMGlYSsK7Cg==} + fs-extra@4.0.3: dependencies: graceful-fs: 4.2.11 jsonfile: 4.0.0 universalify: 0.1.2 - dev: false - /fs.realpath@1.0.0: - resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} - dev: true + fs.realpath@1.0.0: {} - /fsevents@2.3.3: - resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} - engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} - os: [darwin] - requiresBuild: true - dev: true + fsevents@2.3.3: optional: true - /gensync@1.0.0-beta.2: - resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} - engines: {node: '>=6.9.0'} - dev: true + function-bind@1.1.2: {} - /geobuf@3.0.2: - resolution: {integrity: sha512-ASgKwEAQQRnyNFHNvpd5uAwstbVYmiTW0Caw3fBb509tNTqXyAAPMyFs5NNihsLZhLxU1j/kjFhkhLWA9djuVg==} - hasBin: true + gensync@1.0.0-beta.2: {} + + geobuf@3.0.2: dependencies: concat-stream: 2.0.0 - pbf: 3.2.1 + pbf: 3.3.0 shapefile: 0.6.6 - dev: false - /geojson-dissolve@3.1.0: - resolution: {integrity: sha512-JXHfn+A3tU392HA703gJbjmuHaQOAE/C1KzbELCczFRFux+GdY6zt1nKb1VMBHp4LWeE7gUY2ql+g06vJqhiwQ==} + geojson-dissolve@3.1.0: dependencies: '@turf/meta': 3.14.0 geojson-flatten: 0.2.4 geojson-linestring-dissolve: 0.0.1 topojson-client: 3.1.0 topojson-server: 3.0.1 - dev: false - /geojson-flatten@0.2.4: - resolution: {integrity: sha512-LiX6Jmot8adiIdZ/fthbcKKPOfWjTQchX/ggHnwMZ2e4b0I243N1ANUos0LvnzepTEsj0+D4fIJ5bKhBrWnAHA==} - hasBin: true + geojson-flatten@0.2.4: dependencies: get-stdin: 6.0.0 minimist: 1.2.0 - dev: false - /geojson-linestring-dissolve@0.0.1: - resolution: {integrity: sha512-Y8I2/Ea28R/Xeki7msBcpMvJL2TaPfaPKP8xqueJfQ9/jEhps+iOJxOR2XCBGgVb12Z6XnDb1CMbaPfLepsLaw==} - dev: false + geojson-linestring-dissolve@0.0.1: {} - /get-stdin@6.0.0: - resolution: {integrity: sha512-jp4tHawyV7+fkkSKyvjuLZswblUtz+SQKzSWnBbii16BuZksJlU1wuBYXY75r+duh/llF1ur6oNwi+2ZzjKZ7g==} - engines: {node: '>=4'} - dev: false + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.2 + math-intrinsics: 1.1.0 - /glob@7.2.3: - resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + get-stdin@6.0.0: {} + + glob@7.2.3: dependencies: fs.realpath: 1.0.0 inflight: 1.0.6 @@ -1938,25 +4420,26 @@ packages: minimatch: 3.1.2 once: 1.4.0 path-is-absolute: 1.0.1 - dev: true - /globals@11.12.0: - resolution: {integrity: sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==} - engines: {node: '>=4'} - dev: true + globals@11.12.0: {} - /graceful-fs@4.2.11: - resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + gopd@1.2.0: {} - /has-flag@3.0.0: - resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} - engines: {node: '>=4'} - dev: true + graceful-fs@4.2.11: {} - /hast-util-to-estree@3.1.0: - resolution: {integrity: sha512-lfX5g6hqVh9kjS/B9E2gSkvHH4SZNiQFiqWS0x9fENzEl+8W12RqdRxX6d/Cwxi30tPQs3bIO+aolQJNp1bIyw==} + has-symbols@1.1.0: {} + + has-tostringtag@1.0.2: dependencies: - '@types/estree': 1.0.5 + has-symbols: 1.1.0 + + hasown@2.0.2: + dependencies: + function-bind: 1.1.2 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.7 '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 comma-separated-tokens: 2.0.3 @@ -1965,21 +4448,19 @@ packages: estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdx-jsx: 3.2.0 mdast-util-mdxjs-esm: 2.0.1 - property-information: 6.5.0 + property-information: 7.0.0 space-separated-tokens: 2.0.2 - style-to-object: 0.4.4 + style-to-js: 1.1.16 unist-util-position: 5.0.0 zwitch: 2.0.4 transitivePeerDependencies: - supports-color - dev: false - /hast-util-to-jsx-runtime@2.3.0: - resolution: {integrity: sha512-H/y0+IWPdsLLS738P8tDnrQ8Z+dj12zQQ6WC11TIM21C8WFVoIxcqWXf2H3hiTVZjF1AWqoimGwrTWecWrnmRQ==} + hast-util-to-jsx-runtime@2.3.6: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 '@types/hast': 3.0.4 '@types/unist': 3.0.3 comma-separated-tokens: 2.0.3 @@ -1987,328 +4468,216 @@ packages: estree-util-is-identifier-name: 3.0.0 hast-util-whitespace: 3.0.0 mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdx-jsx: 3.2.0 mdast-util-mdxjs-esm: 2.0.1 - property-information: 6.5.0 + property-information: 7.0.0 space-separated-tokens: 2.0.2 - style-to-object: 1.0.8 + style-to-js: 1.1.16 unist-util-position: 5.0.0 vfile-message: 4.0.2 transitivePeerDependencies: - supports-color - dev: false - /hast-util-whitespace@3.0.0: - resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + hast-util-whitespace@3.0.0: dependencies: '@types/hast': 3.0.4 - dev: false - /history@5.3.0: - resolution: {integrity: sha512-ZqaKwjjrAYUYfLG+htGaIIZ4nioX2L70ZUMIFysS3xvBsSG4x/n1V6TXV3N8ZYNuFGlDirFg32T7B6WOUPDYcQ==} + history@5.3.0: dependencies: - '@babel/runtime': 7.24.0 - dev: false + '@babel/runtime': 7.27.0 - /html-parse-stringify@3.0.1: - resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==} + html-parse-stringify@3.0.1: dependencies: void-elements: 3.1.0 - dev: false - /i18next-browser-languagedetector@7.2.2: - resolution: {integrity: sha512-6b7r75uIJDWCcCflmbof+sJ94k9UQO4X0YR62oUfqGI/GjCLVzlCwu8TFdRZIqVLzWbzNcmkmhfqKEr4TLz4HQ==} + i18next-browser-languagedetector@7.2.2: dependencies: - '@babel/runtime': 7.24.0 - dev: false + '@babel/runtime': 7.27.0 - /i18next@23.16.8: - resolution: {integrity: sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==} + i18next@23.16.8: dependencies: - '@babel/runtime': 7.24.0 - dev: false + '@babel/runtime': 7.27.0 - /iconv-lite@0.4.24: - resolution: {integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==} - engines: {node: '>=0.10.0'} + iconv-lite@0.4.24: dependencies: safer-buffer: 2.1.2 - dev: false - /ieee754@1.2.1: - resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} - dev: false + ieee754@1.2.1: {} - /import-fresh@3.3.0: - resolution: {integrity: sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==} - engines: {node: '>=6'} + import-fresh@3.3.1: dependencies: parent-module: 1.0.1 resolve-from: 4.0.0 - dev: true - /inflight@1.0.6: - resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} - deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. + inflight@1.0.6: dependencies: once: 1.4.0 wrappy: 1.0.2 - dev: true - /inherits@2.0.4: - resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + inherits@2.0.4: {} - /inline-style-parser@0.1.1: - resolution: {integrity: sha512-7NXolsK4CAS5+xvdj5OMMbI962hU/wvwoxk+LWR9Ek9bVtyuuYScDN6eS0rUm6TxApFpw7CX1o4uJzcd4AyD3Q==} - dev: false + inline-style-parser@0.2.4: {} - /inline-style-parser@0.2.4: - resolution: {integrity: sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==} - dev: false + is-alphabetical@2.0.1: {} - /is-alphabetical@2.0.1: - resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} - dev: false - - /is-alphanumerical@2.0.1: - resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + is-alphanumerical@2.0.1: dependencies: is-alphabetical: 2.0.1 is-decimal: 2.0.1 - dev: false - /is-arrayish@0.2.1: - resolution: {integrity: sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==} - dev: true + is-arrayish@0.2.1: {} - /is-decimal@2.0.1: - resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} - dev: false + is-decimal@2.0.1: {} - /is-hexadecimal@2.0.1: - resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} - dev: false + is-hexadecimal@2.0.1: {} - /is-plain-obj@4.1.0: - resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} - engines: {node: '>=12'} - dev: false + is-plain-obj@4.1.0: {} - /is-reference@3.0.2: - resolution: {integrity: sha512-v3rht/LgVcsdZa3O2Nqs+NMowLOxeOm7Ay9+/ARQ2F+qEoANRcqrjAZKGN0v8ymUetZGgkp26LTnGT7H0Qo9Pg==} - dependencies: - '@types/estree': 1.0.5 - dev: false + isarray@0.0.1: {} - /isarray@0.0.1: - resolution: {integrity: sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==} - dev: false + jquery@3.7.1: {} - /jquery@3.7.1: - resolution: {integrity: sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==} - dev: false + js-tokens@4.0.0: {} - /js-tokens@4.0.0: - resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + jsesc@3.1.0: {} - /jsesc@2.5.2: - resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==} - engines: {node: '>=4'} - hasBin: true - dev: true + json-parse-even-better-errors@2.3.1: {} - /json-parse-even-better-errors@2.3.1: - resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} - dev: true + json5@2.2.3: {} - /json5@2.2.3: - resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} - engines: {node: '>=6'} - hasBin: true - dev: true + jsonc-parser@3.3.1: {} - /jsonc-parser@3.3.1: - resolution: {integrity: sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==} - dev: false - - /jsonfile@4.0.0: - resolution: {integrity: sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==} + jsonfile@4.0.0: optionalDependencies: graceful-fs: 4.2.11 - dev: false - /jsonfile@6.1.0: - resolution: {integrity: sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==} + jsonfile@6.1.0: dependencies: universalify: 2.0.1 optionalDependencies: graceful-fs: 4.2.11 - dev: true - /keyboard-key@1.1.0: - resolution: {integrity: sha512-qkBzPTi3rlAKvX7k0/ub44sqOfXeLc/jcnGGmj5c7BJpU8eDrEVPyhCvNYAaoubbsLm9uGWwQJO1ytQK1a9/dQ==} - dev: false + keyboard-key@1.1.0: {} - /lines-and-columns@1.2.4: - resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} - dev: true + lines-and-columns@1.2.4: {} - /lodash-es@4.17.21: - resolution: {integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==} - dev: false + lodash-es@4.17.21: {} - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: false + lodash@4.17.21: {} - /longest-streak@3.1.0: - resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} - dev: false + longest-streak@3.1.0: {} - /loose-envify@1.4.0: - resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} - hasBin: true + loose-envify@1.4.0: dependencies: js-tokens: 4.0.0 - dev: false - /lottie-web@5.12.2: - resolution: {integrity: sha512-uvhvYPC8kGPjXT3MyKMrL3JitEAmDMp30lVkuq/590Mw9ok6pWcFCwXJveo0t5uqYw1UREQHofD+jVpdjBv8wg==} - dev: false + lottie-web@5.12.2: {} - /lru-cache@5.1.1: - resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + lru-cache@5.1.1: dependencies: yallist: 3.1.1 - dev: true - /markdown-extensions@2.0.0: - resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} - engines: {node: '>=16'} - dev: false + markdown-extensions@2.0.0: {} - /markdown-table@3.0.3: - resolution: {integrity: sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw==} - dev: false + markdown-table@3.0.4: {} - /marked@4.3.0: - resolution: {integrity: sha512-PRsaiG84bK+AMvxziE/lCFss8juXjNaWzVbN5tXAm4XjeaS9NAHhop+PjQxz2A9h8Q4M/xGmzP8vqNwy6JeK0A==} - engines: {node: '>= 12'} - hasBin: true - dev: false + marked@4.3.0: {} - /mdast-util-find-and-replace@3.0.1: - resolution: {integrity: sha512-SG21kZHGC3XRTSUhtofZkBzZTJNM5ecCi0SK2IMKmSXR8vO3peL+kb1O0z7Zl83jKtutG4k5Wv/W7V3/YHvzPA==} + math-intrinsics@1.1.0: {} + + mdast-util-find-and-replace@3.0.2: dependencies: '@types/mdast': 4.0.4 escape-string-regexp: 5.0.0 unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - dev: false - /mdast-util-from-markdown@2.0.1: - resolution: {integrity: sha512-aJEUyzZ6TzlsX2s5B4Of7lN7EQtAxvtradMMglCQDyaTFgse6CmtmdJ15ElnVRlCg1vpNyVtbem0PWzlNieZsA==} + mdast-util-from-markdown@2.0.2: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 devlop: 1.1.0 mdast-util-to-string: 4.0.0 - micromark: 4.0.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-decode-string: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 unist-util-stringify-position: 4.0.0 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-gfm-autolink-literal@2.0.1: - resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + mdast-util-gfm-autolink-literal@2.0.1: dependencies: '@types/mdast': 4.0.4 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-find-and-replace: 3.0.1 - micromark-util-character: 2.1.0 - dev: false + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 - /mdast-util-gfm-footnote@2.0.0: - resolution: {integrity: sha512-5jOT2boTSVkMnQ7LTrd6n/18kqwjmuYqo7JUPe+tRCY6O7dAuTFMtTPauYYrMPpox9hlN0uOx/FL8XvEfG9/mQ==} + mdast-util-gfm-footnote@2.1.0: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 - micromark-util-normalize-identifier: 2.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-gfm-strikethrough@2.0.0: - resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + mdast-util-gfm-strikethrough@2.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-gfm-table@2.0.0: - resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + mdast-util-gfm-table@2.0.0: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - markdown-table: 3.0.3 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-gfm-task-list-item@2.0.0: - resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + mdast-util-gfm-task-list-item@2.0.0: dependencies: '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-gfm@3.0.0: - resolution: {integrity: sha512-dgQEX5Amaq+DuUqf26jJqSK9qgixgd6rYDHAv4aTBuA92cTknZlKpPfa86Z/s8Dj8xsAQpFfBmPUHWJBWqS4Bw==} + mdast-util-gfm@3.1.0: dependencies: - mdast-util-from-markdown: 2.0.1 + mdast-util-from-markdown: 2.0.2 mdast-util-gfm-autolink-literal: 2.0.1 - mdast-util-gfm-footnote: 2.0.0 + mdast-util-gfm-footnote: 2.1.0 mdast-util-gfm-strikethrough: 2.0.0 mdast-util-gfm-table: 2.0.0 mdast-util-gfm-task-list-item: 2.0.0 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-mdx-expression@2.0.1: - resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + mdast-util-mdx-expression@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-mdx-jsx@3.1.3: - resolution: {integrity: sha512-bfOjvNt+1AcbPLTFMFWY149nJz0OjmewJs3LQQ5pIyVGxP4CdOqNVJL6kTaM5c68p8q82Xv3nCyFfUnuEcH3UQ==} + mdast-util-mdx-jsx@3.2.0: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 @@ -2316,588 +4685,428 @@ packages: '@types/unist': 3.0.3 ccount: 2.0.1 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 - parse-entities: 4.0.1 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 stringify-entities: 4.0.4 unist-util-stringify-position: 4.0.0 vfile-message: 4.0.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-mdx@3.0.0: - resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + mdast-util-mdx@3.0.0: dependencies: - mdast-util-from-markdown: 2.0.1 + mdast-util-from-markdown: 2.0.2 mdast-util-mdx-expression: 2.0.1 - mdast-util-mdx-jsx: 3.1.3 + mdast-util-mdx-jsx: 3.2.0 mdast-util-mdxjs-esm: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-mdxjs-esm@2.0.1: - resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + mdast-util-mdxjs-esm@2.0.1: dependencies: '@types/estree-jsx': 1.0.5 '@types/hast': 3.0.4 '@types/mdast': 4.0.4 devlop: 1.1.0 - mdast-util-from-markdown: 2.0.1 - mdast-util-to-markdown: 2.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 transitivePeerDependencies: - supports-color - dev: false - /mdast-util-phrasing@4.1.0: - resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + mdast-util-phrasing@4.1.0: dependencies: '@types/mdast': 4.0.4 unist-util-is: 6.0.0 - dev: false - /mdast-util-to-hast@13.2.0: - resolution: {integrity: sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==} + mdast-util-to-hast@13.2.0: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 - '@ungap/structured-clone': 1.2.0 + '@ungap/structured-clone': 1.3.0 devlop: 1.1.0 - micromark-util-sanitize-uri: 2.0.0 + micromark-util-sanitize-uri: 2.0.1 trim-lines: 3.0.1 unist-util-position: 5.0.0 unist-util-visit: 5.0.0 vfile: 6.0.3 - dev: false - /mdast-util-to-markdown@2.1.0: - resolution: {integrity: sha512-SR2VnIEdVNCJbP6y7kVTJgPLifdr8WEU440fQec7qHoHOUz/oJ2jmNRqdDQ3rbiStOXb2mCDGTuwsK5OPUgYlQ==} + mdast-util-to-markdown@2.1.2: dependencies: '@types/mdast': 4.0.4 '@types/unist': 3.0.3 longest-streak: 3.1.0 mdast-util-phrasing: 4.1.0 mdast-util-to-string: 4.0.0 - micromark-util-decode-string: 2.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 unist-util-visit: 5.0.0 zwitch: 2.0.4 - dev: false - /mdast-util-to-string@4.0.0: - resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + mdast-util-to-string@4.0.0: dependencies: '@types/mdast': 4.0.4 - dev: false - /memoize-one@5.2.1: - resolution: {integrity: sha512-zYiwtZUcYyXKo/np96AGZAckk+FWWsUdJ3cHGGmld7+AhvcWmQyGCYUh1hc4Q/pkOhb65dQR/pqCyK0cOaHz4Q==} - dev: false + memoize-one@5.2.1: {} - /micromark-core-commonmark@2.0.1: - resolution: {integrity: sha512-CUQyKr1e///ZODyD1U3xit6zXwy1a8q2a1S1HKtIlmgvurrEpaw/Y9y6KSIbF8P59cn/NjzHyO+Q2fAyYLQrAA==} + micromark-core-commonmark@2.0.3: dependencies: - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 devlop: 1.1.0 - micromark-factory-destination: 2.0.0 - micromark-factory-label: 2.0.0 - micromark-factory-space: 2.0.0 - micromark-factory-title: 2.0.0 - micromark-factory-whitespace: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-classify-character: 2.0.0 - micromark-util-html-tag-name: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-subtokenize: 2.0.1 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-gfm-autolink-literal@2.1.0: - resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + micromark-extension-gfm-autolink-literal@2.1.0: dependencies: - micromark-util-character: 2.1.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-gfm-footnote@2.1.0: - resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + micromark-extension-gfm-footnote@2.1.0: dependencies: devlop: 1.1.0 - micromark-core-commonmark: 2.0.1 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-gfm-strikethrough@2.1.0: - resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + micromark-extension-gfm-strikethrough@2.1.0: dependencies: devlop: 1.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-classify-character: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-gfm-table@2.1.0: - resolution: {integrity: sha512-Ub2ncQv+fwD70/l4ou27b4YzfNaCJOvyX4HxXU15m7mpYY+rjuWzsLIPZHJL253Z643RpbcP1oeIJlQ/SKW67g==} + micromark-extension-gfm-table@2.1.1: dependencies: devlop: 1.1.0 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-gfm-tagfilter@2.0.0: - resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + micromark-extension-gfm-tagfilter@2.0.0: dependencies: - micromark-util-types: 2.0.0 - dev: false + micromark-util-types: 2.0.2 - /micromark-extension-gfm-task-list-item@2.1.0: - resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + micromark-extension-gfm-task-list-item@2.1.0: dependencies: devlop: 1.1.0 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-gfm@3.0.0: - resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + micromark-extension-gfm@3.0.0: dependencies: micromark-extension-gfm-autolink-literal: 2.1.0 micromark-extension-gfm-footnote: 2.1.0 micromark-extension-gfm-strikethrough: 2.1.0 - micromark-extension-gfm-table: 2.1.0 + micromark-extension-gfm-table: 2.1.1 micromark-extension-gfm-tagfilter: 2.0.0 micromark-extension-gfm-task-list-item: 2.1.0 - micromark-util-combine-extensions: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-mdx-expression@3.0.0: - resolution: {integrity: sha512-sI0nwhUDz97xyzqJAbHQhp5TfaxEvZZZ2JDqUo+7NvyIYG6BZ5CPPqj2ogUoPJlmXHBnyZUzISg9+oUmU6tUjQ==} + micromark-extension-mdx-expression@3.0.1: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 devlop: 1.1.0 - micromark-factory-mdx-expression: 2.0.2 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-events-to-acorn: 2.0.2 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-extension-mdx-jsx@3.0.1: - resolution: {integrity: sha512-vNuFb9czP8QCtAQcEJn0UJQJZA8Dk6DXKBqx+bg/w0WGuSxDxNr7hErW89tHUY31dUW4NqEOWwmEUNhjTFmHkg==} + micromark-extension-mdx-jsx@3.0.2: dependencies: - '@types/acorn': 4.0.6 - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 devlop: 1.1.0 estree-util-is-identifier-name: 3.0.0 - micromark-factory-mdx-expression: 2.0.2 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-events-to-acorn: 2.0.2 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 vfile-message: 4.0.2 - dev: false - /micromark-extension-mdx-md@2.0.0: - resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + micromark-extension-mdx-md@2.0.0: dependencies: - micromark-util-types: 2.0.0 - dev: false + micromark-util-types: 2.0.2 - /micromark-extension-mdxjs-esm@3.0.0: - resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + micromark-extension-mdxjs-esm@3.0.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 devlop: 1.1.0 - micromark-core-commonmark: 2.0.1 - micromark-util-character: 2.1.0 - micromark-util-events-to-acorn: 2.0.2 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 unist-util-position-from-estree: 2.0.0 vfile-message: 4.0.2 - dev: false - /micromark-extension-mdxjs@3.0.0: - resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + micromark-extension-mdxjs@3.0.0: dependencies: - acorn: 8.12.1 - acorn-jsx: 5.3.2(acorn@8.12.1) - micromark-extension-mdx-expression: 3.0.0 - micromark-extension-mdx-jsx: 3.0.1 + acorn: 8.14.1 + acorn-jsx: 5.3.2(acorn@8.14.1) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 micromark-extension-mdx-md: 2.0.0 micromark-extension-mdxjs-esm: 3.0.0 - micromark-util-combine-extensions: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-factory-destination@2.0.0: - resolution: {integrity: sha512-j9DGrQLm/Uhl2tCzcbLhy5kXsgkHUrjJHg4fFAeoMRwJmJerT9aw4FEhIbZStWN8A3qMwOp1uzHr4UL8AInxtA==} + micromark-factory-destination@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-factory-label@2.0.0: - resolution: {integrity: sha512-RR3i96ohZGde//4WSe/dJsxOX6vxIg9TimLAS3i4EhBAFx8Sm5SmqVfR8E87DPSR31nEAjZfbt91OMZWcNgdZw==} + micromark-factory-label@2.0.1: dependencies: devlop: 1.1.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-factory-mdx-expression@2.0.2: - resolution: {integrity: sha512-5E5I2pFzJyg2CtemqAbcyCktpHXuJbABnsb32wX2U8IQKhhVFBqkcZR5LRm1WVoFqa4kTueZK4abep7wdo9nrw==} + micromark-factory-mdx-expression@2.0.3: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 devlop: 1.1.0 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-events-to-acorn: 2.0.2 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 unist-util-position-from-estree: 2.0.0 vfile-message: 4.0.2 - dev: false - /micromark-factory-space@2.0.0: - resolution: {integrity: sha512-TKr+LIDX2pkBJXFLzpyPyljzYK3MtmllMUMODTQJIUfDGncESaqB90db9IAUcz4AZAJFdd8U9zOp9ty1458rxg==} + micromark-factory-space@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 - /micromark-factory-title@2.0.0: - resolution: {integrity: sha512-jY8CSxmpWLOxS+t8W+FG3Xigc0RDQA9bKMY/EwILvsesiRniiVMejYTE4wumNc2f4UbAa4WsHqe3J1QS1sli+A==} + micromark-factory-title@2.0.1: dependencies: - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-factory-whitespace@2.0.0: - resolution: {integrity: sha512-28kbwaBjc5yAI1XadbdPYHX/eDnqaUFVikLwrO7FDnKG7lpgxnvk/XGRhX/PN0mOZ+dBSZ+LgunHS+6tYQAzhA==} + micromark-factory-whitespace@2.0.1: dependencies: - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-character@2.1.0: - resolution: {integrity: sha512-KvOVV+X1yLBfs9dCBSopq/+G1PcgT3lAK07mC4BzXi5E7ahzMAF8oIupDDJ6mievI6F+lAATkbQQlQixJfT3aQ==} + micromark-util-character@2.1.1: dependencies: - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-chunked@2.0.0: - resolution: {integrity: sha512-anK8SWmNphkXdaKgz5hJvGa7l00qmcaUQoMYsBwDlSKFKjc6gjGXPDw3FNL3Nbwq5L8gE+RCbGqTw49FK5Qyvg==} + micromark-util-chunked@2.0.1: dependencies: - micromark-util-symbol: 2.0.0 - dev: false + micromark-util-symbol: 2.0.1 - /micromark-util-classify-character@2.0.0: - resolution: {integrity: sha512-S0ze2R9GH+fu41FA7pbSqNWObo/kzwf8rN/+IGlW/4tC6oACOs8B++bh+i9bVyNnwCcuksbFwsBme5OCKXCwIw==} + micromark-util-classify-character@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-combine-extensions@2.0.0: - resolution: {integrity: sha512-vZZio48k7ON0fVS3CUgFatWHoKbbLTK/rT7pzpJ4Bjp5JjkZeasRfrS9wsBdDJK2cJLHMckXZdzPSSr1B8a4oQ==} + micromark-util-combine-extensions@2.0.1: dependencies: - micromark-util-chunked: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-decode-numeric-character-reference@2.0.1: - resolution: {integrity: sha512-bmkNc7z8Wn6kgjZmVHOX3SowGmVdhYS7yBpMnuMnPzDq/6xwVA604DuOXMZTO1lvq01g+Adfa0pE2UKGlxL1XQ==} + micromark-util-decode-numeric-character-reference@2.0.2: dependencies: - micromark-util-symbol: 2.0.0 - dev: false + micromark-util-symbol: 2.0.1 - /micromark-util-decode-string@2.0.0: - resolution: {integrity: sha512-r4Sc6leeUTn3P6gk20aFMj2ntPwn6qpDZqWvYmAG6NgvFTIlj4WtrAudLi65qYoaGdXYViXYw2pkmn7QnIFasA==} + micromark-util-decode-string@2.0.1: dependencies: - decode-named-character-reference: 1.0.2 - micromark-util-character: 2.1.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-symbol: 2.0.0 - dev: false + decode-named-character-reference: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 - /micromark-util-encode@2.0.0: - resolution: {integrity: sha512-pS+ROfCXAGLWCOc8egcBvT0kf27GoWMqtdarNfDcjb6YLuV5cM3ioG45Ys2qOVqeqSbjaKg72vU+Wby3eddPsA==} - dev: false + micromark-util-encode@2.0.1: {} - /micromark-util-events-to-acorn@2.0.2: - resolution: {integrity: sha512-Fk+xmBrOv9QZnEDguL9OI9/NQQp6Hz4FuQ4YmCb/5V7+9eAh1s6AYSvL20kHkD67YIg7EpE54TiSlcsf3vyZgA==} + micromark-util-events-to-acorn@2.0.3: dependencies: - '@types/acorn': 4.0.6 - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 '@types/unist': 3.0.3 devlop: 1.1.0 estree-util-visit: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 vfile-message: 4.0.2 - dev: false - /micromark-util-html-tag-name@2.0.0: - resolution: {integrity: sha512-xNn4Pqkj2puRhKdKTm8t1YHC/BAjx6CEwRFXntTaRf/x16aqka6ouVoutm+QdkISTlT7e2zU7U4ZdlDLJd2Mcw==} - dev: false + micromark-util-html-tag-name@2.0.1: {} - /micromark-util-normalize-identifier@2.0.0: - resolution: {integrity: sha512-2xhYT0sfo85FMrUPtHcPo2rrp1lwbDEEzpx7jiH2xXJLqBuy4H0GgXk5ToU8IEwoROtXuL8ND0ttVa4rNqYK3w==} + micromark-util-normalize-identifier@2.0.1: dependencies: - micromark-util-symbol: 2.0.0 - dev: false + micromark-util-symbol: 2.0.1 - /micromark-util-resolve-all@2.0.0: - resolution: {integrity: sha512-6KU6qO7DZ7GJkaCgwBNtplXCvGkJToU86ybBAUdavvgsCiG8lSSvYxr9MhwmQ+udpzywHsl4RpGJsYWG1pDOcA==} + micromark-util-resolve-all@2.0.1: dependencies: - micromark-util-types: 2.0.0 - dev: false + micromark-util-types: 2.0.2 - /micromark-util-sanitize-uri@2.0.0: - resolution: {integrity: sha512-WhYv5UEcZrbAtlsnPuChHUAsu/iBPOVaEVsntLBIdpibO0ddy8OzavZz3iL2xVvBZOpolujSliP65Kq0/7KIYw==} + micromark-util-sanitize-uri@2.0.1: dependencies: - micromark-util-character: 2.1.0 - micromark-util-encode: 2.0.0 - micromark-util-symbol: 2.0.0 - dev: false + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 - /micromark-util-subtokenize@2.0.1: - resolution: {integrity: sha512-jZNtiFl/1aY73yS3UGQkutD0UbhTt68qnRpw2Pifmz5wV9h8gOVsN70v+Lq/f1rKaU/W8pxRe8y8Q9FX1AOe1Q==} + micromark-util-subtokenize@2.1.0: dependencies: devlop: 1.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 - dev: false + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 - /micromark-util-symbol@2.0.0: - resolution: {integrity: sha512-8JZt9ElZ5kyTnO94muPxIGS8oyElRJaiJO8EzV6ZSyGQ1Is8xwl4Q45qU5UOg+bGH4AikWziz0iN4sFLWs8PGw==} - dev: false + micromark-util-symbol@2.0.1: {} - /micromark-util-types@2.0.0: - resolution: {integrity: sha512-oNh6S2WMHWRZrmutsRmDDfkzKtxF+bc2VxLC9dvtrDIRFln627VsFP6fLMgTryGDljgLPjkrzQSDcPrjPyDJ5w==} - dev: false + micromark-util-types@2.0.2: {} - /micromark@4.0.0: - resolution: {integrity: sha512-o/sd0nMof8kYff+TqcDx3VSrgBTcZpSvYcAHIfHhv5VAuNmisCxjhx6YmxS8PFEpb9z5WKWKPdzf0jM23ro3RQ==} + micromark@4.0.2: dependencies: '@types/debug': 4.1.12 - debug: 4.3.4 - decode-named-character-reference: 1.0.2 + debug: 4.4.0 + decode-named-character-reference: 1.1.0 devlop: 1.1.0 - micromark-core-commonmark: 2.0.1 - micromark-factory-space: 2.0.0 - micromark-util-character: 2.1.0 - micromark-util-chunked: 2.0.0 - micromark-util-combine-extensions: 2.0.0 - micromark-util-decode-numeric-character-reference: 2.0.1 - micromark-util-encode: 2.0.0 - micromark-util-normalize-identifier: 2.0.0 - micromark-util-resolve-all: 2.0.0 - micromark-util-sanitize-uri: 2.0.0 - micromark-util-subtokenize: 2.0.1 - micromark-util-symbol: 2.0.0 - micromark-util-types: 2.0.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 transitivePeerDependencies: - supports-color - dev: false - /mime-db@1.52.0: - resolution: {integrity: sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==} - engines: {node: '>= 0.6'} - dev: false + mime-db@1.52.0: {} - /mime-types@2.1.35: - resolution: {integrity: sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==} - engines: {node: '>= 0.6'} + mime-types@2.1.35: dependencies: mime-db: 1.52.0 - dev: false - /minimatch@3.1.2: - resolution: {integrity: sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==} + minimatch@3.1.2: dependencies: brace-expansion: 1.1.11 - dev: true - /minimist@1.2.0: - resolution: {integrity: sha512-7Wl+Jz+IGWuSdgsQEJ4JunV0si/iMhg42MnQQG6h1R6TNeVenp4U9x5CC5v/gYqz/fENLQITAWXidNtVL0NNbw==} - dev: false + minimist@1.2.0: {} - /minimist@1.2.6: - resolution: {integrity: sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==} - dev: false + minimist@1.2.6: {} - /ms@2.1.2: - resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} + ms@2.1.3: {} - /nanoid@3.3.7: - resolution: {integrity: sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==} - engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} - hasBin: true - dev: true + nanoid@3.3.11: {} - /node-releases@2.0.14: - resolution: {integrity: sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==} - dev: true + node-releases@2.0.19: {} - /object-assign@4.1.1: - resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} - engines: {node: '>=0.10.0'} - dev: false + object-assign@4.1.1: {} - /once@1.4.0: - resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} + once@1.4.0: dependencies: wrappy: 1.0.2 - dev: true - /parent-module@1.0.1: - resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} - engines: {node: '>=6'} + parent-module@1.0.1: dependencies: callsites: 3.1.0 - dev: true - /parse-author@2.0.0: - resolution: {integrity: sha512-yx5DfvkN8JsHL2xk2Os9oTia467qnvRgey4ahSm2X8epehBLx/gWLcy5KI+Y36ful5DzGbCS6RazqZGgy1gHNw==} - engines: {node: '>=0.10.0'} + parse-author@2.0.0: dependencies: author-regex: 1.0.0 - dev: true - /parse-entities@4.0.1: - resolution: {integrity: sha512-SWzvYcSJh4d/SGLIOQfZ/CoNv6BTlI6YEQ7Nj82oDVnRpwe/Z/F1EMx42x3JAOwGBlCjeCH0BRJQbQ/opHL17w==} + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 - character-entities: 2.0.2 character-entities-legacy: 3.0.0 character-reference-invalid: 2.0.1 - decode-named-character-reference: 1.0.2 + decode-named-character-reference: 1.1.0 is-alphanumerical: 2.0.1 is-decimal: 2.0.1 is-hexadecimal: 2.0.1 - dev: false - /parse-json@5.2.0: - resolution: {integrity: sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==} - engines: {node: '>=8'} + parse-json@5.2.0: dependencies: - '@babel/code-frame': 7.23.5 + '@babel/code-frame': 7.26.2 error-ex: 1.3.2 json-parse-even-better-errors: 2.3.1 lines-and-columns: 1.2.4 - dev: true - /parse-svg-path@0.1.2: - resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} - dev: false + parse-svg-path@0.1.2: {} - /path-browserify@1.0.1: - resolution: {integrity: sha512-b7uo2UCUOYZcnF/3ID0lulOJi/bafxa1xPe7ZPsammBSpjSWQkjNxlt635YGS2MiR9GjvuXCtz2emr3jbsz98g==} - dev: false + path-browserify@1.0.1: {} - /path-data-parser@0.1.0: - resolution: {integrity: sha512-NOnmBpt5Y2RWbuv0LMzsayp3lVylAHLPUTut412ZA3l+C4uw4ZVkQbjShYCQ8TCpUMdPapr4YjUqLYD6v68j+w==} - dev: false + path-data-parser@0.1.0: {} - /path-is-absolute@1.0.1: - resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} - engines: {node: '>=0.10.0'} - dev: true + path-is-absolute@1.0.1: {} - /path-source@0.1.3: - resolution: {integrity: sha512-dWRHm5mIw5kw0cs3QZLNmpUWty48f5+5v9nWD2dw3Y0Hf+s01Ag8iJEWV0Sm0kocE8kK27DrIowha03e1YR+Qw==} + path-source@0.1.3: dependencies: array-source: 0.0.4 file-source: 0.6.1 - dev: false - /path-type@4.0.0: - resolution: {integrity: sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==} - engines: {node: '>=8'} - dev: true + path-type@4.0.0: {} - /pbf@3.2.1: - resolution: {integrity: sha512-ClrV7pNOn7rtmoQVF4TS1vyU0WhYRnP92fzbfF75jAIwpnzdJXf8iTd4CMEqO4yUenH6NDqLiwjqlh6QgZzgLQ==} - hasBin: true + pbf@3.3.0: dependencies: ieee754: 1.2.1 resolve-protobuf-schema: 2.1.0 - dev: false - /periscopic@3.1.0: - resolution: {integrity: sha512-vKiQ8RRtkl9P+r/+oefh25C3fhybptkHKCZSPlcXiJux2tJF55GnEj3BVn4A5gKfq9NWWXXrxkHBwVPUfH0opw==} - dependencies: - '@types/estree': 1.0.5 - estree-walker: 3.0.3 - is-reference: 3.0.2 - dev: false + picocolors@1.1.1: {} - /picocolors@1.0.0: - resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} - dev: true - - /point-at-length@1.1.0: - resolution: {integrity: sha512-nNHDk9rNEh/91o2Y8kHLzBLNpLf80RYd2gCun9ss+V0ytRSf6XhryBTx071fesktjbachRmGuUbId+JQmzhRXw==} + point-at-length@1.1.0: dependencies: abs-svg-path: 0.1.1 isarray: 0.0.1 parse-svg-path: 0.1.2 - dev: false - /points-on-curve@0.2.0: - resolution: {integrity: sha512-0mYKnYYe9ZcqMCWhUjItv/oHjvgEsfKvnUTg8sAtnHr3GVy7rGkXCb6d5cSyqrWqL4k81b9CPg3urd+T7aop3A==} - dev: false + points-on-curve@0.2.0: {} - /points-on-path@0.2.1: - resolution: {integrity: sha512-25ClnWWuw7JbWZcgqY/gJ4FQWadKxGWk+3kR/7kD0tCaDtPPMj7oHu2ToLaVhfpnHrZzYby2w6tUA0eOIuUg8g==} + points-on-path@0.2.1: dependencies: path-data-parser: 0.1.0 points-on-curve: 0.2.0 - dev: false - /postcss@8.4.38: - resolution: {integrity: sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==} - engines: {node: ^10 || ^12 || >=14} + postcss@8.5.3: dependencies: - nanoid: 3.3.7 - picocolors: 1.0.0 - source-map-js: 1.2.0 - dev: true + nanoid: 3.3.11 + picocolors: 1.1.1 + source-map-js: 1.2.1 - /prettier-package-json@2.8.0: - resolution: {integrity: sha512-WxtodH/wWavfw3MR7yK/GrS4pASEQ+iSTkdtSxPJWvqzG55ir5nvbLt9rw5AOiEcqqPCRM92WCtR1rk3TG3JSQ==} - hasBin: true + prettier-package-json@2.8.0: dependencies: '@types/parse-author': 2.0.3 commander: 4.1.1 @@ -2908,425 +5117,309 @@ packages: parse-author: 2.0.0 sort-object-keys: 1.1.3 sort-order: 1.1.2 - dev: true - /prettier-plugin-astro@0.14.1: - resolution: {integrity: sha512-RiBETaaP9veVstE4vUwSIcdATj6dKmXljouXc/DDNwBSPTp8FRkLGDSGFClKsAFeeg+13SB0Z1JZvbD76bigJw==} - engines: {node: ^14.15.0 || >=16.0.0} + prettier-plugin-astro@0.14.1: dependencies: - '@astrojs/compiler': 2.10.3 - prettier: 3.2.5 + '@astrojs/compiler': 2.11.0 + prettier: 3.5.3 sass-formatter: 0.7.9 - dev: true - /prettier-plugin-curly-and-jsdoc@3.1.0(prettier@3.2.5): - resolution: {integrity: sha512-4QMOHnLlkP2jTRWS0MFH6j+cuOiXLvXOqCLKbtwwVd8PPyq8NenW5AAwfwqiTNHBQG/DmzViPphRrwgN0XkUVQ==} - peerDependencies: - prettier: ^3.0.0 + prettier-plugin-curly-and-jsdoc@3.1.0(prettier@3.5.3): dependencies: - prettier: 3.2.5 - dev: true + prettier: 3.5.3 - /prettier-plugin-pkgsort@0.2.1(prettier@3.2.5): - resolution: {integrity: sha512-/k5MIw84EhgoH7dmq4+6ozHjJ0VYbxbw17g4C+WPGHODkLivGwJoA6U1YPR/KObyRDMQJHXAfXKu++9smg7Jyw==} - peerDependencies: - prettier: ^3.0.0 + prettier-plugin-pkgsort@0.2.1(prettier@3.5.3): dependencies: - prettier: 3.2.5 + prettier: 3.5.3 prettier-package-json: 2.8.0 - dev: true - /prettier@3.2.5: - resolution: {integrity: sha512-3/GWa9aOC0YeD7LUfvOG2NiDyhOWRvt1k+rcKhOuYnMY24iiCphgneUfJDyFXd6rZCAnuLBv6UeAULtrhT/F4A==} - engines: {node: '>=14'} - hasBin: true - dev: true + prettier@3.5.3: {} - /prismjs@1.29.0: - resolution: {integrity: sha512-Kx/1w86q/epKcmte75LNrEoT+lX8pBpavuAbvJWRXar7Hz8jrtF+e3vY751p0R8H9HdArwaCTNDDzHg/ScJK1Q==} - engines: {node: '>=6'} - dev: false + prismjs@1.30.0: {} - /prop-types@15.8.1: - resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + prop-types@15.8.1: dependencies: loose-envify: 1.4.0 object-assign: 4.1.1 react-is: 16.13.1 - dev: false - /property-information@6.5.0: - resolution: {integrity: sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==} - dev: false + property-information@7.0.0: {} - /protocol-buffers-schema@3.6.0: - resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} - dev: false + protocol-buffers-schema@3.6.0: {} - /react-dom@18.2.0(react@18.2.0): - resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} - peerDependencies: - react: ^18.2.0 + react-dom@18.3.1(react@18.3.1): dependencies: loose-envify: 1.4.0 - react: 18.2.0 - scheduler: 0.23.0 - dev: false + react: 18.3.1 + scheduler: 0.23.2 - /react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==} - peerDependencies: - react: '>= 16.3.0' - react-dom: '>= 16.3.0' + react-draggable@4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react-dropzone@14.2.3(react@18.2.0): - resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==} - engines: {node: '>= 10.13'} - peerDependencies: - react: '>= 16.8 || 18.0.0' + react-dropzone@14.3.8(react@18.3.1): dependencies: - attr-accept: 2.2.2 - file-selector: 0.6.0 + attr-accept: 2.2.5 + file-selector: 2.1.2 prop-types: 15.8.1 - react: 18.2.0 - dev: false + react: 18.3.1 - /react-fast-compare@3.2.2: - resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} - dev: false + react-fast-compare@3.2.2: {} - /react-fireworks@1.0.4: - resolution: {integrity: sha512-jj1a+HTicB4pR6g2lqhVyAox0GTE0TOrZK2XaJFRYOwltgQWeYErZxnvU9+zH/blY+Hpmu9IKyb39OD3KcCMJw==} - dev: false + react-fireworks@1.0.4: {} - /react-i18next@13.5.0(i18next@23.16.8)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-CFJ5NDGJ2MUyBohEHxljOq/39NQ972rh1ajnadG9BjTk+UXbHLq4z5DKEbEQBDoIhUmmbuS/fIMJKo6VOax1HA==} - peerDependencies: - i18next: '>= 23.2.3' - react: '>= 16.8.0' - react-dom: '*' - react-native: '*' - peerDependenciesMeta: - react-dom: - optional: true - react-native: - optional: true + react-i18next@13.5.0(i18next@23.16.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.27.0 html-parse-stringify: 3.0.1 i18next: 23.16.8 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + react: 18.3.1 + optionalDependencies: + react-dom: 18.3.1(react@18.3.1) - /react-is@16.13.1: - resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} - dev: false + react-is@16.13.1: {} - /react-is@18.2.0: - resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} - dev: false + react-is@18.3.1: {} - /react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==} - peerDependencies: - '@popperjs/core': ^2.0.0 - react: ^16.8.0 || ^17 || ^18 - react-dom: ^16.8.0 || ^17 || ^18 + react-popper@2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: '@popperjs/core': 2.11.8 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) react-fast-compare: 3.2.2 warning: 4.0.3 - dev: false - /react-refresh@0.14.0: - resolution: {integrity: sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==} - engines: {node: '>=0.10.0'} - dev: true + react-refresh@0.14.2: {} - /react-resizable@3.0.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-vKpeHhI5OZvYn82kXOs1bC8aOXktGU5AmKAgaZS4F5JPburCtbmDPqE7Pzp+1kN4+Wb81LlF33VpGwWwtXem+w==} - peerDependencies: - react: '>= 16.3' + react-resizable@3.0.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: prop-types: 15.8.1 - react: 18.2.0 - react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0) + react: 18.3.1 + react-draggable: 4.4.6(react-dom@18.3.1(react@18.3.1))(react@18.3.1) transitivePeerDependencies: - react-dom - dev: false - /react-router-dom@6.22.2(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-WgqxD2qySEIBPZ3w0sHH+PUAiamDeszls9tzqMPBDA1YYVucTBXLU7+gtRfcSnhe92A3glPnvSxK2dhNoAVOIQ==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' - react-dom: '>=16.8' + react-router-dom@6.30.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@remix-run/router': 1.15.2 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-router: 6.22.2(react@18.2.0) - dev: false + '@remix-run/router': 1.23.0 + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-router: 6.30.0(react@18.3.1) - /react-router@6.22.2(react@18.2.0): - resolution: {integrity: sha512-YD3Dzprzpcq+tBMHBS822tCjnWD3iIZbTeSXMY9LPSG541EfoBGyZ3bS25KEnaZjLcmQpw2AVLkFyfgXY8uvcw==} - engines: {node: '>=14.0.0'} - peerDependencies: - react: '>=16.8' + react-router@6.30.0(react@18.3.1): dependencies: - '@remix-run/router': 1.15.2 - react: 18.2.0 - dev: false + '@remix-run/router': 1.23.0 + react: 18.3.1 - /react-telegram-login@1.1.2(react@18.2.0): - resolution: {integrity: sha512-pDP+bvfaklWgnK5O6yvZnIwgky0nnYUU6Zhk0EjdMSkPsLQoOzZRsXIoZnbxyBXhi7346bsxMH+EwwJPTxClDw==} - peerDependencies: - react: ^16.13.1 + react-telegram-login@1.1.2(react@18.3.1): dependencies: - react: 18.2.0 - dev: false + react: 18.3.1 - /react-toastify@9.1.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-fPfb8ghtn/XMxw3LkxQBk3IyagNpF/LIKjOBflbexr2AWxAH1MJgvnESwEwBn9liLFXgTKWgBSdZpw9m4OTHTg==} - peerDependencies: - react: '>=16' - react-dom: '>=16' + react-toastify@9.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: clsx: 1.2.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react-turnstile@1.1.3(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-nWgsnN2IgDSj91BK2iF/9GMVRJK0KPuDDxgnhs4o/7zfIRfyZG/ALWs+JJ8unW84MtFXpcEiPsookkd/FIb4aw==} - peerDependencies: - react: '>= 16.13.1' - react-dom: '>= 16.13.1' + react-turnstile@1.1.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react-window@1.8.10(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-Y0Cx+dnU6NLa5/EvoHukUD0BklJ8qITCtVEPY1C/nL8wwoZ0b5aEw8Ff1dOVHw7fCzMt55XfJDd8S8W8LCaUCg==} - engines: {node: '>8.0.0'} - peerDependencies: - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 - react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react-window@1.8.11(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.24.0 + '@babel/runtime': 7.27.0 memoize-one: 5.2.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - dev: false + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) - /react@18.2.0: - resolution: {integrity: sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==} - engines: {node: '>=0.10.0'} + react@18.3.1: dependencies: loose-envify: 1.4.0 - dev: false - /readable-stream@1.1.14: - resolution: {integrity: sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==} + readable-stream@1.1.14: dependencies: core-util-is: 1.0.3 inherits: 2.0.4 isarray: 0.0.1 string_decoder: 0.10.31 - dev: false - /readable-stream@3.6.2: - resolution: {integrity: sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==} - engines: {node: '>= 6'} + readable-stream@3.6.2: dependencies: inherits: 2.0.4 string_decoder: 1.3.0 util-deprecate: 1.0.2 - dev: false - /regenerator-runtime@0.14.1: - resolution: {integrity: sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==} - dev: false + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.7 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 - /remark-gfm@4.0.0: - resolution: {integrity: sha512-U92vJgBPkbw4Zfu/IiW2oTZLSL3Zpv+uI7My2eq8JxKgqraFdU8YUGicEJCEgSbeaG+QDFqIcwwfMTOEelPxuA==} + recma-jsx@1.0.0(acorn@8.14.1): + dependencies: + acorn-jsx: 5.3.2(acorn@8.14.1) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - acorn + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.7 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.7 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regenerator-runtime@0.14.1: {} + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.7 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: dependencies: '@types/mdast': 4.0.4 - mdast-util-gfm: 3.0.0 + mdast-util-gfm: 3.1.0 micromark-extension-gfm: 3.0.0 remark-parse: 11.0.0 remark-stringify: 11.0.0 unified: 11.0.5 transitivePeerDependencies: - supports-color - dev: false - /remark-mdx@3.0.1: - resolution: {integrity: sha512-3Pz3yPQ5Rht2pM5R+0J2MrGoBSrzf+tJG94N+t/ilfdh8YLyyKYtidAYwTveB20BoHAcwIopOUqhcmh2F7hGYA==} + remark-mdx@3.1.0: dependencies: mdast-util-mdx: 3.0.0 micromark-extension-mdxjs: 3.0.0 transitivePeerDependencies: - supports-color - dev: false - /remark-parse@11.0.0: - resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + remark-parse@11.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-from-markdown: 2.0.1 - micromark-util-types: 2.0.0 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 unified: 11.0.5 transitivePeerDependencies: - supports-color - dev: false - /remark-rehype@11.1.1: - resolution: {integrity: sha512-g/osARvjkBXb6Wo0XvAeXQohVta8i84ACbenPpoSsxTOQH/Ae0/RGP4WZgnMH5pMLpsj4FG7OHmcIcXxpza8eQ==} + remark-rehype@11.1.2: dependencies: '@types/hast': 3.0.4 '@types/mdast': 4.0.4 mdast-util-to-hast: 13.2.0 unified: 11.0.5 vfile: 6.0.3 - dev: false - /remark-stringify@11.0.0: - resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + remark-stringify@11.0.0: dependencies: '@types/mdast': 4.0.4 - mdast-util-to-markdown: 2.1.0 + mdast-util-to-markdown: 2.1.2 unified: 11.0.5 - dev: false - /resolve-from@4.0.0: - resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} - engines: {node: '>=4'} - dev: true + resolve-from@4.0.0: {} - /resolve-protobuf-schema@2.1.0: - resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + resolve-protobuf-schema@2.1.0: dependencies: protocol-buffers-schema: 3.6.0 - dev: false - /rollup@4.13.0: - resolution: {integrity: sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==} - engines: {node: '>=18.0.0', npm: '>=8.0.0'} - hasBin: true + rollup@4.39.0: dependencies: - '@types/estree': 1.0.5 + '@types/estree': 1.0.7 optionalDependencies: - '@rollup/rollup-android-arm-eabi': 4.13.0 - '@rollup/rollup-android-arm64': 4.13.0 - '@rollup/rollup-darwin-arm64': 4.13.0 - '@rollup/rollup-darwin-x64': 4.13.0 - '@rollup/rollup-linux-arm-gnueabihf': 4.13.0 - '@rollup/rollup-linux-arm64-gnu': 4.13.0 - '@rollup/rollup-linux-arm64-musl': 4.13.0 - '@rollup/rollup-linux-riscv64-gnu': 4.13.0 - '@rollup/rollup-linux-x64-gnu': 4.13.0 - '@rollup/rollup-linux-x64-musl': 4.13.0 - '@rollup/rollup-win32-arm64-msvc': 4.13.0 - '@rollup/rollup-win32-ia32-msvc': 4.13.0 - '@rollup/rollup-win32-x64-msvc': 4.13.0 + '@rollup/rollup-android-arm-eabi': 4.39.0 + '@rollup/rollup-android-arm64': 4.39.0 + '@rollup/rollup-darwin-arm64': 4.39.0 + '@rollup/rollup-darwin-x64': 4.39.0 + '@rollup/rollup-freebsd-arm64': 4.39.0 + '@rollup/rollup-freebsd-x64': 4.39.0 + '@rollup/rollup-linux-arm-gnueabihf': 4.39.0 + '@rollup/rollup-linux-arm-musleabihf': 4.39.0 + '@rollup/rollup-linux-arm64-gnu': 4.39.0 + '@rollup/rollup-linux-arm64-musl': 4.39.0 + '@rollup/rollup-linux-loongarch64-gnu': 4.39.0 + '@rollup/rollup-linux-powerpc64le-gnu': 4.39.0 + '@rollup/rollup-linux-riscv64-gnu': 4.39.0 + '@rollup/rollup-linux-riscv64-musl': 4.39.0 + '@rollup/rollup-linux-s390x-gnu': 4.39.0 + '@rollup/rollup-linux-x64-gnu': 4.39.0 + '@rollup/rollup-linux-x64-musl': 4.39.0 + '@rollup/rollup-win32-arm64-msvc': 4.39.0 + '@rollup/rollup-win32-ia32-msvc': 4.39.0 + '@rollup/rollup-win32-x64-msvc': 4.39.0 fsevents: 2.3.3 - dev: true - /roughjs@4.5.2: - resolution: {integrity: sha512-2xSlLDKdsWyFxrveYWk9YQ/Y9UfK38EAMRNkYkMqYBJvPX8abCa9PN0x3w02H8Oa6/0bcZICJU+U95VumPqseg==} + roughjs@4.5.2: dependencies: path-data-parser: 0.1.0 points-on-curve: 0.2.0 points-on-path: 0.2.1 - dev: false - /rw@1.3.3: - resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} - dev: false + rw@1.3.3: {} - /s.color@0.0.15: - resolution: {integrity: sha512-AUNrbEUHeKY8XsYr/DYpl+qk5+aM+DChopnWOPEzn8YKzOhv4l2zH6LzZms3tOZP3wwdOyc0RmTciyi46HLIuA==} - dev: true + s.color@0.0.15: {} - /safe-buffer@5.2.1: - resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} - dev: false + safe-buffer@5.2.1: {} - /safer-buffer@2.1.2: - resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} - dev: false + safer-buffer@2.1.2: {} - /sass-formatter@0.7.9: - resolution: {integrity: sha512-CWZ8XiSim+fJVG0cFLStwDvft1VI7uvXdCNJYXhDvowiv+DsbD1nXLiQ4zrE5UBvj5DWZJ93cwN0NX5PMsr1Pw==} + sass-formatter@0.7.9: dependencies: suf-log: 2.5.3 - dev: true - /scheduler@0.23.0: - resolution: {integrity: sha512-CtuThmgHNg7zIZWAXi3AsyIzA3n4xx7aNyjwC2VJldO2LMVDhFK+63xGqq6CsJH4rTAt6/M+N4GhZiDYPx9eUw==} + scheduler@0.23.2: dependencies: loose-envify: 1.4.0 - dev: false - /scroll-into-view-if-needed@2.2.31: - resolution: {integrity: sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==} + scroll-into-view-if-needed@2.2.31: dependencies: compute-scroll-into-view: 1.0.20 - dev: false - /semantic-ui-offline@2.5.0: - resolution: {integrity: sha512-Fldx3SfaVtWx5EeCb/5EiJwYkzrGbtsAwVs02xLkeV5z5l8GJmplWEVOeJVjbEpmyiwPWp7cA48JwT5RjbWBVA==} + semantic-ui-offline@2.5.0: dependencies: fs-extra: 4.0.3 jquery: 3.7.1 - dev: false - /semantic-ui-react@2.1.5(react-dom@18.2.0)(react@18.2.0): - resolution: {integrity: sha512-nIqmmUNpFHfovEb+RI2w3E2/maZQutd8UIWyRjf1SLse+XF51hI559xbz/sLN3O6RpLjr/echLOOXwKCirPy3Q==} - peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 - react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 + semantic-ui-react@2.1.5(react-dom@18.3.1(react@18.3.1))(react@18.3.1): dependencies: - '@babel/runtime': 7.24.0 - '@fluentui/react-component-event-listener': 0.63.1(react-dom@18.2.0)(react@18.2.0) - '@fluentui/react-component-ref': 0.63.1(react-dom@18.2.0)(react@18.2.0) + '@babel/runtime': 7.27.0 + '@fluentui/react-component-event-listener': 0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + '@fluentui/react-component-ref': 0.63.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1) '@popperjs/core': 2.11.8 - '@semantic-ui-react/event-stack': 3.1.3(react-dom@18.2.0)(react@18.2.0) + '@semantic-ui-react/event-stack': 3.1.3(react-dom@18.3.1(react@18.3.1))(react@18.3.1) clsx: 1.2.1 keyboard-key: 1.1.0 lodash: 4.17.21 lodash-es: 4.17.21 prop-types: 15.8.1 - react: 18.2.0 - react-dom: 18.2.0(react@18.2.0) - react-is: 18.2.0 - react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.2.0)(react@18.2.0) + react: 18.3.1 + react-dom: 18.3.1(react@18.3.1) + react-is: 18.3.1 + react-popper: 2.3.0(@popperjs/core@2.11.8)(react-dom@18.3.1(react@18.3.1))(react@18.3.1) shallowequal: 1.1.0 - dev: false - /semver@6.3.1: - resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} - hasBin: true - dev: true + semver@6.3.1: {} - /shallowequal@1.1.0: - resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} - dev: false + shallowequal@1.1.0: {} - /shapefile@0.6.6: - resolution: {integrity: sha512-rLGSWeK2ufzCVx05wYd+xrWnOOdSV7xNUW5/XFgx3Bc02hBkpMlrd2F1dDII7/jhWzv0MSyBFh5uJIy9hLdfuw==} - hasBin: true + shapefile@0.6.6: dependencies: array-source: 0.0.4 commander: 2.20.3 @@ -3334,149 +5427,80 @@ packages: slice-source: 0.4.1 stream-source: 0.3.5 text-encoding: 0.6.4 - dev: false - /simple-statistics@7.8.3: - resolution: {integrity: sha512-JFvMY00t6SBGtwMuJ+nqgsx9ylkMiJ5JlK9bkj8AdvniIe5615wWQYkKHXe84XtSuc40G/tlrPu0A5/NlJvv8A==} - dev: false + simple-statistics@7.8.8: {} - /simplify-geojson@1.0.5: - resolution: {integrity: sha512-02l1W4UipP5ivNVq6kX15mAzCRIV1oI3tz0FUEyOsNiv1ltuFDjbNhO+nbv/xhbDEtKqWLYuzpWhUsJrjR/ypA==} - hasBin: true + simplify-geojson@1.0.5: dependencies: concat-stream: 1.4.11 minimist: 1.2.6 simplify-geometry: 0.0.2 - dev: false - /simplify-geometry@0.0.2: - resolution: {integrity: sha512-ZEyrplkqgCqDlL7V8GbbYgTLlcnNF+MWWUdy8s8ZeJru50bnI71rDew/I+HG36QS2mPOYAq1ZjwNXxHJ8XOVBw==} - dev: false + simplify-geometry@0.0.2: {} - /slice-source@0.4.1: - resolution: {integrity: sha512-YiuPbxpCj4hD9Qs06hGAz/OZhQ0eDuALN0lRWJez0eD/RevzKqGdUx1IOMUnXgpr+sXZLq3g8ERwbAH0bCb8vg==} - dev: false + slice-source@0.4.1: {} - /sort-object-keys@1.1.3: - resolution: {integrity: sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg==} - dev: true + sort-object-keys@1.1.3: {} - /sort-order@1.1.2: - resolution: {integrity: sha512-Q8tOrwB1TSv9fNUXym9st3TZJODtmcOIi2JWCkVNQPrRg17KPwlpwweTEb7pMwUIFMTAgx2/JsQQXEPFzYQj3A==} - dev: true + sort-order@1.1.2: {} - /source-map-js@1.2.0: - resolution: {integrity: sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==} - engines: {node: '>=0.10.0'} - dev: true + source-map-js@1.2.1: {} - /source-map@0.7.4: - resolution: {integrity: sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==} - engines: {node: '>= 8'} - dev: false + source-map@0.7.4: {} - /space-separated-tokens@2.0.2: - resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} - dev: false + space-separated-tokens@2.0.2: {} - /stream-source@0.3.5: - resolution: {integrity: sha512-ZuEDP9sgjiAwUVoDModftG0JtYiLUV8K4ljYD1VyUMRWtbVf92474o4kuuul43iZ8t/hRuiDAx1dIJSvirrK/g==} - dev: false + sse.js@https://codeload.github.com/mpetazzoni/sse.js/tar.gz/39b9b82aae95fd58d9d08b487845fe230f4b14e6: + {} - /string_decoder@0.10.31: - resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} - dev: false + stream-source@0.3.5: {} - /string_decoder@1.3.0: - resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + string_decoder@0.10.31: {} + + string_decoder@1.3.0: dependencies: safe-buffer: 5.2.1 - dev: false - /stringify-entities@4.0.4: - resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + stringify-entities@4.0.4: dependencies: character-entities-html4: 2.1.0 character-entities-legacy: 3.0.0 - dev: false - /style-to-object@0.4.4: - resolution: {integrity: sha512-HYNoHZa2GorYNyqiCaBgsxvcJIn7OHq6inEga+E6Ke3m5JkoqpQbnFssk4jwe+K7AhGa2fcha4wSOf1Kn01dMg==} + style-to-js@1.1.16: dependencies: - inline-style-parser: 0.1.1 - dev: false + style-to-object: 1.0.8 - /style-to-object@1.0.8: - resolution: {integrity: sha512-xT47I/Eo0rwJmaXC4oilDGDWLohVhR6o/xAQcPQN8q6QBuZVL8qMYL85kLmST5cPjAorwvqIA4qXTRQoYHaL6g==} + style-to-object@1.0.8: dependencies: inline-style-parser: 0.2.4 - dev: false - /suf-log@2.5.3: - resolution: {integrity: sha512-KvC8OPjzdNOe+xQ4XWJV2whQA0aM1kGVczMQ8+dStAO6KfEB140JEVQ9dE76ONZ0/Ylf67ni4tILPJB41U0eow==} + suf-log@2.5.3: dependencies: s.color: 0.0.15 - dev: true - /supports-color@5.5.0: - resolution: {integrity: sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==} - engines: {node: '>=4'} - dependencies: - has-flag: 3.0.0 - dev: true + text-encoding@0.6.4: {} - /text-encoding@0.6.4: - resolution: {integrity: sha512-hJnc6Qg3dWoOMkqP53F0dzRIgtmsAge09kxUIqGrEUS4qr5rWLckGYaQAVr+opBrIMRErGgy6f5aPnyPpyGRfg==} - deprecated: no longer maintained - dev: false - - /to-fast-properties@2.0.0: - resolution: {integrity: sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==} - engines: {node: '>=4'} - dev: true - - /topojson-client@3.1.0: - resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} - hasBin: true + topojson-client@3.1.0: dependencies: commander: 2.20.3 - dev: false - /topojson-server@3.0.1: - resolution: {integrity: sha512-/VS9j/ffKr2XAOjlZ9CgyyeLmgJ9dMwq6Y0YEON8O7p/tGGk+dCWnrE03zEdu7i4L7YsFZLEPZPzCvcB7lEEXw==} - hasBin: true + topojson-server@3.0.1: dependencies: commander: 2.20.3 - dev: false - /trim-lines@3.0.1: - resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} - dev: false + trim-lines@3.0.1: {} - /trough@2.2.0: - resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} - dev: false + trough@2.2.0: {} - /tslib@2.6.2: - resolution: {integrity: sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==} - dev: false + tslib@2.8.1: {} - /typedarray@0.0.6: - resolution: {integrity: sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==} - dev: false + typedarray@0.0.6: {} - /typedarray@0.0.7: - resolution: {integrity: sha512-ueeb9YybpjhivjbHP2LdFDAjbS948fGEPj+ACAMs4xCMmh72OCOMQWBQKlaN4ZNQ04yfLSDLSx1tGRIoWimObQ==} - dev: false + typedarray@0.0.7: {} - /typescript@4.4.2: - resolution: {integrity: sha512-gzP+t5W4hdy4c+68bfcv0t400HVJMMd2+H9B7gae1nQlBzCqvrXX+6GL/b3GAgyTH966pzrZ70/fRjwAtZksSQ==} - engines: {node: '>=4.2.0'} - hasBin: true - dev: true + typescript@4.4.2: {} - /unified@11.0.5: - resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + unified@11.0.5: dependencies: '@types/unist': 3.0.3 bail: 2.0.2 @@ -3485,156 +5509,76 @@ packages: is-plain-obj: 4.1.0 trough: 2.2.0 vfile: 6.0.3 - dev: false - /unist-util-is@6.0.0: - resolution: {integrity: sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==} + unist-util-is@6.0.0: dependencies: '@types/unist': 3.0.3 - dev: false - /unist-util-position-from-estree@2.0.0: - resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + unist-util-position-from-estree@2.0.0: dependencies: '@types/unist': 3.0.3 - dev: false - /unist-util-position@5.0.0: - resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + unist-util-position@5.0.0: dependencies: '@types/unist': 3.0.3 - dev: false - /unist-util-stringify-position@4.0.0: - resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + unist-util-stringify-position@4.0.0: dependencies: '@types/unist': 3.0.3 - dev: false - /unist-util-visit-parents@6.0.1: - resolution: {integrity: sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==} + unist-util-visit-parents@6.0.1: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.0 - dev: false - /unist-util-visit@5.0.0: - resolution: {integrity: sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==} + unist-util-visit@5.0.0: dependencies: '@types/unist': 3.0.3 unist-util-is: 6.0.0 unist-util-visit-parents: 6.0.1 - dev: false - /universalify@0.1.2: - resolution: {integrity: sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==} - engines: {node: '>= 4.0.0'} - dev: false + universalify@0.1.2: {} - /universalify@2.0.1: - resolution: {integrity: sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==} - engines: {node: '>= 10.0.0'} - dev: true + universalify@2.0.1: {} - /update-browserslist-db@1.0.13(browserslist@4.23.0): - resolution: {integrity: sha512-xebP81SNcPuNpPP3uzeW1NYXxI3rxyJzF3pD6sH4jE7o/IX+WtSpwnVU+qIsDPyk0d3hmFQ7mjqc6AtV604hbg==} - hasBin: true - peerDependencies: - browserslist: '>= 4.21.0' + update-browserslist-db@1.1.3(browserslist@4.24.4): dependencies: - browserslist: 4.23.0 - escalade: 3.1.2 - picocolors: 1.0.0 - dev: true + browserslist: 4.24.4 + escalade: 3.2.0 + picocolors: 1.1.1 - /util-deprecate@1.0.2: - resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} - dev: false + util-deprecate@1.0.2: {} - /utility-types@3.11.0: - resolution: {integrity: sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==} - engines: {node: '>= 4'} - dev: false + utility-types@3.11.0: {} - /vfile-message@4.0.2: - resolution: {integrity: sha512-jRDZ1IMLttGj41KcZvlrYAaI3CfqpLpfpf+Mfig13viT6NKvRzWZ+lXz0Y5D60w6uJIBAOGq9mSHf0gktF0duw==} + vfile-message@4.0.2: dependencies: '@types/unist': 3.0.3 unist-util-stringify-position: 4.0.0 - dev: false - /vfile@6.0.3: - resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + vfile@6.0.3: dependencies: '@types/unist': 3.0.3 vfile-message: 4.0.2 - dev: false - /vite@5.2.5: - resolution: {integrity: sha512-a+rTAqkMmJ2hQpC6dfAyyc5M0YLH3BGZKLpA6pU9AhzlcK1YZS8P/ov9OcdHxaf+j0sM0DIh/txH7ydTHUpISg==} - engines: {node: ^18.0.0 || >=20.0.0} - hasBin: true - peerDependencies: - '@types/node': ^18.0.0 || >=20.0.0 - less: '*' - lightningcss: ^1.21.0 - sass: '*' - stylus: '*' - sugarss: '*' - terser: ^5.4.0 - peerDependenciesMeta: - '@types/node': - optional: true - less: - optional: true - lightningcss: - optional: true - sass: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true + vite@5.4.16: dependencies: - esbuild: 0.20.2 - postcss: 8.4.38 - rollup: 4.13.0 + esbuild: 0.21.5 + postcss: 8.5.3 + rollup: 4.39.0 optionalDependencies: fsevents: 2.3.3 - dev: true - /void-elements@3.1.0: - resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==} - engines: {node: '>=0.10.0'} - dev: false + void-elements@3.1.0: {} - /warning@4.0.3: - resolution: {integrity: sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==} + warning@4.0.3: dependencies: loose-envify: 1.4.0 - dev: false - /wrappy@1.0.2: - resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} - dev: true + wrappy@1.0.2: {} - /yallist@3.1.1: - resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} - dev: true + yallist@3.1.1: {} - /yaml@1.10.2: - resolution: {integrity: sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==} - engines: {node: '>= 6'} - dev: true + yaml@1.10.2: {} - /zwitch@2.0.4: - resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} - dev: false - - github.com/mpetazzoni/sse.js/ee7d4cd0b5798c944ca0b2723c7250a5c5e6c65a: - resolution: {tarball: https://codeload.github.com/mpetazzoni/sse.js/tar.gz/ee7d4cd0b5798c944ca0b2723c7250a5c5e6c65a} - name: sse.js - version: 2.5.0 - dev: false + zwitch@2.0.4: {} diff --git a/web/src/App.js b/web/src/App.js index 9e9407c0..ed53f6a0 100644 --- a/web/src/App.js +++ b/web/src/App.js @@ -1,5 +1,5 @@ import React, { lazy, Suspense, useContext, useEffect } from 'react'; -import { Route, Routes } from 'react-router-dom'; +import { Route, Routes, useLocation } from 'react-router-dom'; import Loading from './components/Loading'; import User from './pages/User'; import { PrivateRoute } from './components/PrivateRoute'; @@ -8,10 +8,8 @@ import LoginForm from './components/LoginForm'; import NotFound from './pages/NotFound'; import Setting from './pages/Setting'; import EditUser from './pages/User/EditUser'; -import { getLogo, getSystemName } from './helpers'; import PasswordResetForm from './components/PasswordResetForm'; import PasswordResetConfirm from './components/PasswordResetConfirm'; -import { UserContext } from './context/User'; import Channel from './pages/Channel'; import Token from './pages/Token'; import EditChannel from './pages/Channel/EditChannel'; @@ -23,31 +21,39 @@ import Chat2Link from './pages/Chat2Link'; import { Layout } from '@douyinfe/semi-ui'; import Midjourney from './pages/Midjourney'; import Pricing from './pages/Pricing/index.js'; -import Task from "./pages/Task/index.js"; +import Task from './pages/Task/index.js'; import Playground from './pages/Playground/Playground.js'; -import OAuth2Callback from "./components/OAuth2Callback.js"; -import { useTranslation } from 'react-i18next'; -import { StatusContext } from './context/Status'; -import { setStatusData } from './helpers/data.js'; -import { API, showError } from './helpers'; +import OAuth2Callback from './components/OAuth2Callback.js'; import PersonalSetting from './components/PersonalSetting.js'; +import Setup from './pages/Setup/index.js'; +import SetupCheck from './components/SetupCheck'; const Home = lazy(() => import('./pages/Home')); const Detail = lazy(() => import('./pages/Detail')); const About = lazy(() => import('./pages/About')); function App() { + const location = useLocation(); + return ( - <> + }> + } key={location.pathname}> } /> + } key={location.pathname}> + + + } + /> }> + } key={location.pathname}> } @@ -67,7 +73,7 @@ function App() { }> + } key={location.pathname}> } @@ -107,7 +113,7 @@ function App() { }> + } key={location.pathname}> } @@ -115,7 +121,7 @@ function App() { }> + } key={location.pathname}> } @@ -123,7 +129,7 @@ function App() { }> + } key={location.pathname}> } @@ -131,7 +137,7 @@ function App() { }> + } key={location.pathname}> } @@ -139,7 +145,7 @@ function App() { }> + } key={location.pathname}> } @@ -147,7 +153,7 @@ function App() { }> + } key={location.pathname}> } @@ -155,16 +161,24 @@ function App() { }> + } key={location.pathname}> } /> + }> + + + } + /> }> - + } key={location.pathname}> + } /> @@ -172,7 +186,7 @@ function App() { path='/setting' element={ - }> + } key={location.pathname}> @@ -182,7 +196,7 @@ function App() { path='/personal' element={ - }> + } key={location.pathname}> @@ -192,7 +206,7 @@ function App() { path='/topup' element={ - }> + } key={location.pathname}> @@ -210,7 +224,7 @@ function App() { path='/detail' element={ - }> + } key={location.pathname}> @@ -220,7 +234,7 @@ function App() { path='/midjourney' element={ - }> + } key={location.pathname}> @@ -230,7 +244,7 @@ function App() { path='/task' element={ - }> + } key={location.pathname}> @@ -239,7 +253,7 @@ function App() { }> + } key={location.pathname}> } @@ -247,7 +261,7 @@ function App() { }> + } key={location.pathname}> } @@ -255,25 +269,25 @@ function App() { }> + } key={location.pathname}> } /> {/* 方便使用chat2link直接跳转聊天... */} - - }> - - - - } - /> - } /> - - + + } key={location.pathname}> + + + + } + /> + } /> + + ); } diff --git a/web/src/components/ChannelsTable.js b/web/src/components/ChannelsTable.js index 2cd6c15d..15b4059a 100644 --- a/web/src/components/ChannelsTable.js +++ b/web/src/components/ChannelsTable.js @@ -7,7 +7,7 @@ import { showInfo, showSuccess, showWarning, - timestamp2string + timestamp2string, } from '../helpers'; import { CHANNEL_OPTIONS, ITEMS_PER_PAGE } from '../constants'; @@ -15,13 +15,18 @@ import { getQuotaPerUnit, renderGroup, renderNumberWithPoint, - renderQuota, renderQuotaWithPrompt, stringToColor + renderQuota, + renderQuotaWithPrompt, + stringToColor, } from '../helpers/render'; import { - Button, Divider, + Button, + Divider, Dropdown, - Form, Input, - InputNumber, Modal, + Form, + Input, + InputNumber, + Modal, Popconfirm, Space, SplitButtonGroup, @@ -31,10 +36,18 @@ import { Tooltip, Typography, Checkbox, - Layout + Layout, } from '@douyinfe/semi-ui'; import EditChannel from '../pages/Channel/EditChannel'; -import { IconList, IconTreeTriangleDown, IconClose, IconFilter, IconPlus, IconRefresh, IconSetting } from '@douyinfe/semi-icons'; +import { + IconList, + IconTreeTriangleDown, + IconClose, + IconFilter, + IconPlus, + IconRefresh, + IconSetting, +} from '@douyinfe/semi-icons'; import { loadChannelModels } from './utils.js'; import EditTagModal from '../pages/Channel/EditTagModal.js'; import TextNumberInput from './custom/TextNumberInput.js'; @@ -58,7 +71,7 @@ const ChannelsTable = () => { type2label[0] = { value: 0, label: t('未知类型'), color: 'grey' }; } return ( - + {type2label[type]?.label} ); @@ -82,25 +95,25 @@ const ChannelsTable = () => { switch (status) { case 1: return ( - + {t('已启用')} ); case 2: return ( - + {t('已禁用')} ); case 3: return ( - + {t('自动禁用')} ); default: return ( - + {t('未知状态')} ); @@ -112,31 +125,31 @@ const ChannelsTable = () => { time = time.toFixed(2) + t(' 秒'); if (responseTime === 0) { return ( - + {t('未测试')} ); } else if (responseTime <= 1000) { return ( - + {time} ); } else if (responseTime <= 3000) { return ( - + {time} ); } else if (responseTime <= 5000) { return ( - + {time} ); } else { return ( - + {time} ); @@ -154,7 +167,7 @@ const ChannelsTable = () => { BALANCE: 'balance', PRIORITY: 'priority', WEIGHT: 'weight', - OPERATE: 'operate' + OPERATE: 'operate', }; // State for column visibility @@ -184,7 +197,10 @@ const ChannelsTable = () => { useEffect(() => { if (Object.keys(visibleColumns).length > 0) { // Save to localStorage - localStorage.setItem('channels-table-columns', JSON.stringify(visibleColumns)); + localStorage.setItem( + 'channels-table-columns', + JSON.stringify(visibleColumns), + ); } }, [visibleColumns]); @@ -200,7 +216,7 @@ const ChannelsTable = () => { [COLUMN_KEYS.BALANCE]: true, [COLUMN_KEYS.PRIORITY]: true, [COLUMN_KEYS.WEIGHT]: true, - [COLUMN_KEYS.OPERATE]: true + [COLUMN_KEYS.OPERATE]: true, }; }; @@ -218,13 +234,13 @@ const ChannelsTable = () => { // Handle "Select All" checkbox const handleSelectAll = (checked) => { - const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]); + const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); const updatedColumns = {}; - - allKeys.forEach(key => { + + allKeys.forEach((key) => { updatedColumns[key] = checked; }); - + setVisibleColumns(updatedColumns); }; @@ -233,12 +249,12 @@ const ChannelsTable = () => { { key: COLUMN_KEYS.ID, title: t('ID'), - dataIndex: 'id' + dataIndex: 'id', }, { key: COLUMN_KEYS.NAME, title: t('名称'), - dataIndex: 'name' + dataIndex: 'name', }, { key: COLUMN_KEYS.GROUP, @@ -248,7 +264,8 @@ const ChannelsTable = () => { return (
- {text?.split(',') + {text + ?.split(',') .sort((a, b) => { if (a === 'default') return -1; if (b === 'default') return 1; @@ -260,7 +277,7 @@ const ChannelsTable = () => {
); - } + }, }, { key: COLUMN_KEYS.TYPE, @@ -272,7 +289,7 @@ const ChannelsTable = () => { } else { return <>{renderTagType()}; } - } + }, }, { key: COLUMN_KEYS.STATUS, @@ -288,7 +305,11 @@ const ChannelsTable = () => { let time = otherInfo['status_time']; return (
- + {renderStatus(text)}
@@ -296,7 +317,7 @@ const ChannelsTable = () => { } else { return renderStatus(text); } - } + }, }, { key: COLUMN_KEYS.RESPONSE_TIME, @@ -304,7 +325,7 @@ const ChannelsTable = () => { dataIndex: 'response_time', render: (text, record, index) => { return
{renderResponseTime(text)}
; - } + }, }, { key: COLUMN_KEYS.BALANCE, @@ -316,15 +337,17 @@ const ChannelsTable = () => {
- + {renderQuota(record.used_quota)} - + { updateChannelBalance(record); }} @@ -336,13 +359,15 @@ const ChannelsTable = () => {
); } else { - return - - {renderQuota(record.used_quota)} - - ; + return ( + + + {renderQuota(record.used_quota)} + + + ); } - } + }, }, { key: COLUMN_KEYS.PRIORITY, @@ -354,7 +379,7 @@ const ChannelsTable = () => {
{ manageChannel(record.id, 'priority', record, e.target.value); }} @@ -366,33 +391,38 @@ const ChannelsTable = () => {
); } else { - return <> - { - Modal.warning({ - title: t('修改子渠道优先级'), - content: t('确定要修改所有子渠道优先级为 ') + e.target.value + t(' 吗?'), - onOk: () => { - if (e.target.value === '') { - return; - } - submitTagEdit('priority', { - tag: record.key, - priority: e.target.value - }) - }, - }) - }} - innerButtons - defaultValue={record.priority} - min={-999} - /> - ; + return ( + <> + { + Modal.warning({ + title: t('修改子渠道优先级'), + content: + t('确定要修改所有子渠道优先级为 ') + + e.target.value + + t(' 吗?'), + onOk: () => { + if (e.target.value === '') { + return; + } + submitTagEdit('priority', { + tag: record.key, + priority: e.target.value, + }); + }, + }); + }} + innerButtons + defaultValue={record.priority} + min={-999} + /> + + ); } - } + }, }, { key: COLUMN_KEYS.WEIGHT, @@ -404,7 +434,7 @@ const ChannelsTable = () => {
{ manageChannel(record.id, 'weight', record, e.target.value); }} @@ -419,22 +449,25 @@ const ChannelsTable = () => { return ( { Modal.warning({ title: t('修改子渠道权重'), - content: t('确定要修改所有子渠道权重为 ') + e.target.value + t(' 吗?'), + content: + t('确定要修改所有子渠道权重为 ') + + e.target.value + + t(' 吗?'), onOk: () => { if (e.target.value === '') { return; } submitTagEdit('weight', { tag: record.key, - weight: e.target.value - }) + weight: e.target.value, + }); }, - }) + }); }} innerButtons defaultValue={record.weight} @@ -442,7 +475,7 @@ const ChannelsTable = () => { /> ); } - } + }, }, { key: COLUMN_KEYS.OPERATE, @@ -450,13 +483,6 @@ const ChannelsTable = () => { dataIndex: 'operate', render: (text, record, index) => { if (record.children === undefined) { - // 构建模型测试菜单 - const modelMenuItems = record.models.split(',').map(model => ({ - node: 'item', - name: model, - onClick: () => testChannel(record, model) - })); - return (
{ aria-label={t('测试单个渠道操作项目组')} > {record.status === 1 ? ( ) : ( )} @@ -549,8 +575,8 @@ const ChannelsTable = () => { return ( <> - - + + } - style={{ width: 500 }} + style={{ width: isMobile() ? '90%' : 500 }} bodyStyle={{ padding: '24px' }} >
v === true)} - indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)} - onChange={e => handleSelectAll(e.target.checked)} + checked={Object.values(visibleColumns).every((v) => v === true)} + indeterminate={ + Object.values(visibleColumns).some((v) => v === true) && + !Object.values(visibleColumns).every((v) => v === true) + } + onChange={(e) => handleSelectAll(e.target.checked)} > {t('全选')}
-
- {allColumns.map(column => { +
+ {allColumns.map((column) => { // Skip columns without title if (!column.title) { return null; } - + return ( -
+
handleColumnVisibilityChange(column.key, e.target.checked)} + onChange={(e) => + handleColumnVisibilityChange(column.key, e.target.checked) + } > {column.title} @@ -659,14 +703,14 @@ const ChannelsTable = () => { const [updatingBalance, setUpdatingBalance] = useState(false); const [pageSize, setPageSize] = useState(ITEMS_PER_PAGE); const [showPrompt, setShowPrompt] = useState( - shouldShowPrompt('channel-test') + shouldShowPrompt('channel-test'), ); const [channelCount, setChannelCount] = useState(pageSize); const [groupOptions, setGroupOptions] = useState([]); const [showEdit, setShowEdit] = useState(false); const [enableBatchDelete, setEnableBatchDelete] = useState(false); const [editingChannel, setEditingChannel] = useState({ - id: undefined + id: undefined, }); const [showEditTag, setShowEditTag] = useState(false); const [editingTag, setEditingTag] = useState(''); @@ -679,7 +723,6 @@ const ChannelsTable = () => { const [currentTestChannel, setCurrentTestChannel] = useState(null); const [modelSearchKeyword, setModelSearchKeyword] = useState(''); - const removeRecord = (record) => { let newDataSource = [...channels]; if (record.id != null) { @@ -692,7 +735,7 @@ const ChannelsTable = () => { } } } else { - return data.id === record.id + return data.id === record.id; } }); @@ -711,7 +754,7 @@ const ChannelsTable = () => { if (!enableTagMode) { channelDates.push(channels[i]); } else { - let tag = channels[i].tag ? channels[i].tag : ""; + let tag = channels[i].tag ? channels[i].tag : ''; // find from channelTags let tagIndex = channelTags[tag]; let tagChannelDates = undefined; @@ -770,7 +813,6 @@ const ChannelsTable = () => { tagChannelDates.response_time += channels[i].response_time; tagChannelDates.response_time = tagChannelDates.response_time / 2; } - } // data.key = '' + data.id setChannels(channelDates); @@ -784,7 +826,7 @@ const ChannelsTable = () => { const loadChannels = async (startIdx, pageSize, idSort, enableTagMode) => { setLoading(true); const res = await API.get( - `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}` + `/api/channel/?p=${startIdx}&page_size=${pageSize}&id_sort=${idSort}&tag_mode=${enableTagMode}`, ); if (res === undefined) { return; @@ -805,7 +847,7 @@ const ChannelsTable = () => { }; const copySelectedChannel = async (record) => { - const channelToCopy = record + const channelToCopy = record; channelToCopy.name += t('_复制'); channelToCopy.created_time = null; channelToCopy.balance = 0; @@ -902,12 +944,12 @@ const ChannelsTable = () => { switch (action) { case 'enable': res = await API.post('/api/channel/tag/enabled', { - tag: tag + tag: tag, }); break; case 'disable': res = await API.post('/api/channel/tag/disabled', { - tag: tag + tag: tag, }); break; } @@ -930,7 +972,12 @@ const ChannelsTable = () => { } }; - const searchChannels = async (searchKeyword, searchGroup, searchModel, enableTagMode) => { + const searchChannels = async ( + searchKeyword, + searchGroup, + searchModel, + enableTagMode, + ) => { if (searchKeyword === '' && searchGroup === '' && searchModel === '') { await loadChannels(0, pageSize, idSort, enableTagMode); setActivePage(1); @@ -938,7 +985,7 @@ const ChannelsTable = () => { } setSearching(true); const res = await API.get( - `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}` + `/api/channel/search?keyword=${searchKeyword}&group=${searchGroup}&model=${searchModel}&id_sort=${idSort}&tag_mode=${enableTagMode}`, ); const { success, message, data } = res.data; if (success) { @@ -956,10 +1003,10 @@ const ChannelsTable = () => { let updated = false; // Find and update the correct channel - newChannels.forEach(channel => { + newChannels.forEach((channel) => { if (channel.children !== undefined) { // If this is a tag group, search in its children - channel.children.forEach(child => { + channel.children.forEach((child) => { if (child.id === channelId) { updateFn(child); updated = true; @@ -987,8 +1034,12 @@ const ChannelsTable = () => { channel.response_time = time * 1000; channel.test_time = Date.now() / 1000; }); - - showInfo(t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。').replace('${name}', record.name).replace('${time.toFixed(2)}', time.toFixed(2))); + + showInfo( + t('通道 ${name} 测试成功,耗时 ${time.toFixed(2)} 秒。') + .replace('${name}', record.name) + .replace('${time.toFixed(2)}', time.toFixed(2)), + ); } else { showError(message); } @@ -1002,7 +1053,9 @@ const ChannelsTable = () => { channel.balance = balance; channel.balance_updated_time = Date.now() / 1000; }); - showInfo(t('通道 ${name} 余额更新成功!').replace('${name}', record.name)); + showInfo( + t('通道 ${name} 余额更新成功!').replace('${name}', record.name), + ); } else { showError(message); } @@ -1022,7 +1075,9 @@ const ChannelsTable = () => { const res = await API.delete(`/api/channel/disabled`); const { success, message, data } = res.data; if (success) { - showSuccess(t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data)); + showSuccess( + t('已删除所有禁用渠道,共计 ${data} 个').replace('${data}', data), + ); await refresh(); } else { showError(message); @@ -1075,15 +1130,14 @@ const ChannelsTable = () => { let pageData = channels.slice( (activePage - 1) * pageSize, - activePage * pageSize + activePage * pageSize, ); const handlePageChange = (page) => { setActivePage(page); if (page === Math.ceil(channels.length / pageSize) + 1) { // In this case we have to load more data and then append them. - loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => { - }); + loadChannels(page - 1, pageSize, idSort, enableTagMode).then((r) => {}); } }; @@ -1109,8 +1163,8 @@ const ChannelsTable = () => { setGroupOptions( res.data.data.map((group) => ({ label: group, - value: group - })) + value: group, + })), ); } catch (error) { showError(error.message); @@ -1127,12 +1181,16 @@ const ChannelsTable = () => { data.priority = parseInt(data.priority); break; case 'weight': - if (data.weight === undefined || data.weight < 0 || data.weight === '') { + if ( + data.weight === undefined || + data.weight < 0 || + data.weight === '' + ) { showInfo('权重必须是非负整数!'); return; } data.weight = parseInt(data.weight); - break + break; } try { @@ -1144,7 +1202,7 @@ const ChannelsTable = () => { } catch (error) { showError(error); } - } + }; const closeEdit = () => { setShowEdit(false); @@ -1154,8 +1212,8 @@ const ChannelsTable = () => { if (record.status !== 1) { return { style: { - background: 'var(--semi-color-disabled-border)' - } + background: 'var(--semi-color-disabled-border)', + }, }; } else { return {}; @@ -1171,13 +1229,15 @@ const ChannelsTable = () => { showError(t('标签不能为空!')); return; } - let ids = selectedChannels.map(channel => channel.id); + let ids = selectedChannels.map((channel) => channel.id); const res = await API.post('/api/channel/batch/tag', { ids: ids, - tag: batchSetTagValue === '' ? null : batchSetTagValue + tag: batchSetTagValue === '' ? null : batchSetTagValue, }); if (res.data.success) { - showSuccess(t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data)); + showSuccess( + t('已为 ${count} 个渠道设置标签!').replace('${count}', res.data.data), + ); await refresh(); setShowBatchSetTag(false); } else { @@ -1202,14 +1262,19 @@ const ChannelsTable = () => { />
{ - searchChannels(searchKeyword, searchGroup, searchModel, enableTagMode); + searchChannels( + searchKeyword, + searchGroup, + searchModel, + enableTagMode, + ); }} - labelPosition="left" + labelPosition='left' >
{ }} /> { }} /> { setSearchGroup(v); @@ -1240,9 +1308,9 @@ const ChannelsTable = () => { /> - - - - - - - - - + + {t('使用ID排序')} + + { + localStorage.setItem('id-sort', v + ''); + setIdSort(v); + loadChannels(0, pageSize, v, enableTagMode) + .then() + .catch((reason) => { + showError(reason); + }); + }} + > +
- + + + + + + + + + + + + + + + + + + + + + + } + > + + +
-
- - {t('开启批量操作')} +
+
+ + {t('开启批量操作')} + { onChange={(v) => { setEnableBatchDelete(v); }} - > + /> +
+ +
- - - +
-
- - {t('标签聚合模式')} + +
+
+ + {t('标签聚合模式')} + { loadChannels(0, pageSize, idSort, v); }} /> +
+ +
+ - +
- { onPageSizeChange: (size) => { handlePageSizeChange(size).then(); }, - onPageChange: handlePageChange + onPageChange: handlePageChange, }} + expandAllRows={false} onRow={handleRow} rowSelection={ enableBatchDelete ? { - onChange: (selectedRowKeys, selectedRows) => { - // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); - setSelectedChannels(selectedRows); + onChange: (selectedRowKeys, selectedRows) => { + // console.log(`selectedRowKeys: ${selectedRowKeys}`, 'selectedRows: ', selectedRows); + setSelectedChannels(selectedRows); + }, } - } : null } /> @@ -1442,6 +1613,7 @@ const ChannelsTable = () => { onCancel={() => setShowBatchSetTag(false)} maskClosable={false} centered={true} + style={{ width: isMobile() ? '90%' : 500 }} >
{t('请输入要设置的标签名称')} @@ -1450,9 +1622,18 @@ const ChannelsTable = () => { placeholder={t('请输入标签名称')} value={batchSetTagValue} onChange={(v) => setBatchSetTagValue(v)} + size='large' /> +
+ + {t('已选择 ${count} 个渠道').replace( + '${count}', + selectedChannels.length, + )} + +
- + {/* 模型测试弹窗 */} { footer={null} maskClosable={true} centered={true} - width={600} >
{currentTestChannel && ( @@ -1472,34 +1652,46 @@ const ChannelsTable = () => { {t('渠道')}: {currentTestChannel.name} - + {/* 搜索框 */} setModelSearchKeyword(value)} + onChange={(v) => setModelSearchKeyword(v)} style={{ marginBottom: '16px' }} + prefix={} showClear /> - -
- {currentTestChannel.models.split(',') - .filter(model => model.toLowerCase().includes(modelSearchKeyword.toLowerCase())) - .map((model, index) => { +
+ {currentTestChannel.models + .split(',') + .filter((model) => + model + .toLowerCase() + .includes(modelSearchKeyword.toLowerCase()), + ) + .map((model, index) => { return (
- + {/* 显示搜索结果数量 */} {modelSearchKeyword && ( - - {t('找到')} {currentTestChannel.models.split(',').filter(model => - model.toLowerCase().includes(modelSearchKeyword.toLowerCase()) - ).length} {t('个模型')} + + {t('找到')}{' '} + { + currentTestChannel.models + .split(',') + .filter((model) => + model + .toLowerCase() + .includes(modelSearchKeyword.toLowerCase()), + ).length + }{' '} + {t('个模型')} )}
diff --git a/web/src/components/Footer.js b/web/src/components/Footer.js index c7b39205..7092b873 100644 --- a/web/src/components/Footer.js +++ b/web/src/components/Footer.js @@ -1,12 +1,14 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useContext } from 'react'; import { useTranslation } from 'react-i18next'; import { getFooterHTML, getSystemName } from '../helpers'; import { Layout, Tooltip } from '@douyinfe/semi-ui'; +import { StyleContext } from '../context/Style/index.js'; const FooterBar = () => { const { t } = useTranslation(); const systemName = getSystemName(); const [footer, setFooter] = useState(getFooterHTML()); + const [styleState] = useContext(StyleContext); let remainCheckTimes = 5; const loadFooter = () => { @@ -26,11 +28,7 @@ const FooterBar = () => { New API {import.meta.env.VITE_REACT_APP_VERSION}{' '} {t('由')}{' '} - + Calcium-Ion {' '} {t('开发,基于')}{' '} @@ -57,7 +55,12 @@ const FooterBar = () => { }, []); return ( -
+
{footer ? (
{ const { t, i18n } = useTranslation(); const [userState, userDispatch] = useContext(UserContext); @@ -39,8 +120,7 @@ const HeaderBar = () => { const logo = getLogo(); const currentDate = new Date(); // enable fireworks on new year(1.1 and 2.9-2.24) - const isNewYear = - (currentDate.getMonth() === 0 && currentDate.getDate() === 1); + const isNewYear = currentDate.getMonth() === 0 && currentDate.getDate() === 1; // Check if self-use mode is enabled const isSelfUseMode = statusState?.status?.self_use_mode_enabled || false; @@ -52,28 +132,37 @@ const HeaderBar = () => { text: t('首页'), itemKey: 'home', to: '/', + icon: , }, { text: t('控制台'), itemKey: 'detail', to: '/', + icon: , }, { text: t('定价'), itemKey: 'pricing', to: '/pricing', + icon: , }, // Only include the docs button if docsLink exists - ...(docsLink ? [{ - text: t('文档'), - itemKey: 'docs', - isExternal: true, - externalLink: docsLink, - }] : []), + ...(docsLink + ? [ + { + text: t('文档'), + itemKey: 'docs', + isExternal: true, + externalLink: docsLink, + icon: , + }, + ] + : []), { text: t('关于'), itemKey: 'about', to: '/about', + icon: , }, ]; @@ -143,6 +232,9 @@ const HeaderBar = () => {

- {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')} + {t( + '微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)', + )}

diff --git a/web/src/components/LogsTable.js b/web/src/components/LogsTable.js index 2c4f1343..7834d51a 100644 --- a/web/src/components/LogsTable.js +++ b/web/src/components/LogsTable.js @@ -12,25 +12,33 @@ import { import { Avatar, - Button, Descriptions, + Button, + Descriptions, Form, Layout, - Modal, Popover, + Modal, + Popover, Select, Space, Spin, Table, Tag, Tooltip, - Checkbox + Checkbox, } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; import { - renderAudioModelPrice, renderGroup, - renderModelPrice, renderModelPriceSimple, + renderAudioModelPrice, + renderClaudeLogContent, + renderClaudeModelPrice, + renderClaudeModelPriceSimple, + renderGroup, + renderLogContent, + renderModelPrice, + renderModelPriceSimple, renderNumber, renderQuota, - stringToColor + stringToColor, } from '../helpers/render'; import Paragraph from '@douyinfe/semi-ui/lib/es/typography/paragraph'; import { getLogOther } from '../helpers/other.js'; @@ -72,23 +80,51 @@ const LogsTable = () => { function renderType(type) { switch (type) { case 1: - return {t('充值')}; + return ( + + {t('充值')} + + ); case 2: - return {t('消费')}; + return ( + + {t('消费')} + + ); case 3: - return {t('管理')}; + return ( + + {t('管理')} + + ); case 4: - return {t('系统')}; + return ( + + {t('系统')} + + ); default: - return {t('未知')}; + return ( + + {t('未知')} + + ); } } function renderIsStream(bool) { if (bool) { - return {t('流')}; + return ( + + {t('流')} + + ); } else { - return {t('非流')}; + return ( + + {t('非流')} + + ); } } @@ -146,56 +182,70 @@ const LogsTable = () => { } function renderModelName(record) { - let other = getLogOther(record.other); - let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== ''; + let modelMapped = + other?.is_model_mapped && + other?.upstream_model_name && + other?.upstream_model_name !== ''; if (!modelMapped) { - return { - copyText(event, record.model_name).then(r => {}); - }} - > - {' '}{record.model_name}{' '} - ; + return ( + { + copyText(event, record.model_name).then((r) => {}); + }} + > + {' '} + {record.model_name}{' '} + + ); } else { return ( <> - - - { - copyText(event, record.model_name).then(r => {}); - }} - > - {t('请求并计费模型')}{' '}{record.model_name}{' '} - - { - copyText(event, other.upstream_model_name).then(r => {}); - }} - > - {t('实际模型')}{' '}{other.upstream_model_name}{' '} - - -
- }> + + + { + copyText(event, record.model_name).then((r) => {}); + }} + > + {t('请求并计费模型')} {record.model_name}{' '} + + { + copyText(event, other.upstream_model_name).then( + (r) => {}, + ); + }} + > + {t('实际模型')} {other.upstream_model_name}{' '} + + +
+ } + > { - copyText(event, record.model_name).then(r => {}); + copyText(event, record.model_name).then((r) => {}); }} - suffixIcon={} + suffixIcon={ + + } > - {' '}{record.model_name}{' '} + {' '} + {record.model_name}{' '} {/**/} @@ -213,7 +263,6 @@ const LogsTable = () => { ); } - } // Define column keys for selection @@ -230,7 +279,7 @@ const LogsTable = () => { COMPLETION: 'completion', COST: 'cost', RETRY: 'retry', - DETAILS: 'details' + DETAILS: 'details', }; // State for column visibility @@ -271,7 +320,7 @@ const LogsTable = () => { [COLUMN_KEYS.COMPLETION]: true, [COLUMN_KEYS.COST]: true, [COLUMN_KEYS.RETRY]: isAdminUser, - [COLUMN_KEYS.DETAILS]: true + [COLUMN_KEYS.DETAILS]: true, }; }; @@ -290,18 +339,23 @@ const LogsTable = () => { // Handle "Select All" checkbox const handleSelectAll = (checked) => { - const allKeys = Object.keys(COLUMN_KEYS).map(key => COLUMN_KEYS[key]); + const allKeys = Object.keys(COLUMN_KEYS).map((key) => COLUMN_KEYS[key]); const updatedColumns = {}; - - allKeys.forEach(key => { + + allKeys.forEach((key) => { // For admin-only columns, only enable them if user is admin - if ((key === COLUMN_KEYS.CHANNEL || key === COLUMN_KEYS.USERNAME || key === COLUMN_KEYS.RETRY) && !isAdminUser) { + if ( + (key === COLUMN_KEYS.CHANNEL || + key === COLUMN_KEYS.USERNAME || + key === COLUMN_KEYS.RETRY) && + !isAdminUser + ) { updatedColumns[key] = false; } else { updatedColumns[key] = checked; } }); - + setVisibleColumns(updatedColumns); }; @@ -355,7 +409,7 @@ const LogsTable = () => { style={{ marginRight: 4 }} onClick={(event) => { event.stopPropagation(); - showUserInfo(record.user_id) + showUserInfo(record.user_id); }} > {typeof text === 'string' && text.slice(0, 1)} @@ -397,32 +451,27 @@ const LogsTable = () => { dataIndex: 'group', render: (text, record, index) => { if (record.type === 0 || record.type === 2) { - if (record.group) { - return ( - <> - {renderGroup(record.group)} - - ); - } else { - let other = null; - try { - other = JSON.parse(record.other); - } catch (e) { - console.error(`Failed to parse record.other: "${record.other}".`, e); - } - if (other === null) { - return <>; - } - if (other.group !== undefined) { - return ( - <> - {renderGroup(other.group)} - - ); - } else { - return <>; - } - } + if (record.group) { + return <>{renderGroup(record.group)}; + } else { + let other = null; + try { + other = JSON.parse(record.other); + } catch (e) { + console.error( + `Failed to parse record.other: "${record.other}".`, + e, + ); + } + if (other === null) { + return <>; + } + if (other.group !== undefined) { + return <>{renderGroup(other.group)}; + } else { + return <>; + } + } } else { return <>; } @@ -564,22 +613,32 @@ const LogsTable = () => { ); } - let content = renderModelPriceSimple( - other.model_ratio, - other.model_price, - other.group_ratio, - other.cache_tokens || 0, - other.cache_ratio || 1.0, - ); + let content = other?.claude + ? renderClaudeModelPriceSimple( + other.model_ratio, + other.model_price, + other.group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + other.cache_creation_tokens || 0, + other.cache_creation_ratio || 1.0, + ) + : renderModelPriceSimple( + other.model_ratio, + other.model_price, + other.group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + ); return ( - - {content} - + + {content} + ); }, }, @@ -589,13 +648,16 @@ const LogsTable = () => { useEffect(() => { if (Object.keys(visibleColumns).length > 0) { // Save to localStorage - localStorage.setItem('logs-table-columns', JSON.stringify(visibleColumns)); + localStorage.setItem( + 'logs-table-columns', + JSON.stringify(visibleColumns), + ); } }, [visibleColumns]); // Filter columns based on visibility settings const getVisibleColumns = () => { - return allColumns.filter(column => visibleColumns[column.key]); + return allColumns.filter((column) => visibleColumns[column.key]); }; // Column selector modal @@ -608,42 +670,59 @@ const LogsTable = () => { footer={ <> - - + + } >
v === true)} - indeterminate={Object.values(visibleColumns).some(v => v === true) && !Object.values(visibleColumns).every(v => v === true)} - onChange={e => handleSelectAll(e.target.checked)} + checked={Object.values(visibleColumns).every((v) => v === true)} + indeterminate={ + Object.values(visibleColumns).some((v) => v === true) && + !Object.values(visibleColumns).every((v) => v === true) + } + onChange={(e) => handleSelectAll(e.target.checked)} > {t('全选')}
-
- {allColumns.map(column => { +
+ {allColumns.map((column) => { // Skip admin-only columns for non-admin users - if (!isAdminUser && (column.key === COLUMN_KEYS.CHANNEL || - column.key === COLUMN_KEYS.USERNAME || - column.key === COLUMN_KEYS.RETRY)) { + if ( + !isAdminUser && + (column.key === COLUMN_KEYS.CHANNEL || + column.key === COLUMN_KEYS.USERNAME || + column.key === COLUMN_KEYS.RETRY) + ) { return null; } - + return ( -
+
handleColumnVisibilityChange(column.key, e.target.checked)} + onChange={(e) => + handleColumnVisibilityChange(column.key, e.target.checked) + } > {column.title} @@ -693,7 +772,7 @@ const LogsTable = () => { }); const handleInputChange = (value, name) => { - setInputs(inputs => ({ ...inputs, [name]: value })); + setInputs((inputs) => ({ ...inputs, [name]: value })); }; const getLogSelfStat = async () => { @@ -749,10 +828,18 @@ const LogsTable = () => { title: t('用户信息'), content: (
-

{t('用户名')}: {data.username}

-

{t('余额')}: {renderQuota(data.quota)}

-

{t('已用额度')}:{renderQuota(data.used_quota)}

-

{t('请求次数')}:{renderNumber(data.request_count)}

+

+ {t('用户名')}: {data.username} +

+

+ {t('余额')}: {renderQuota(data.quota)} +

+

+ {t('已用额度')}:{renderQuota(data.used_quota)} +

+

+ {t('请求次数')}:{renderNumber(data.request_count)} +

), centered: true, @@ -787,11 +874,11 @@ const LogsTable = () => { // key: '渠道重试', // value: content, // }) - } + } if (isAdminUser && (logs[i].type === 0 || logs[i].type === 2)) { expandDataLocal.push({ key: t('渠道信息'), - value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}` + value: `${logs[i].channel} - ${logs[i].channel_name || '[未知]'}`, }); } if (other?.ws || other?.audio) { @@ -818,12 +905,39 @@ const LogsTable = () => { value: other.cache_tokens, }); } - expandDataLocal.push({ - key: t('日志详情'), - value: logs[i].content, - }); + if (other?.cache_creation_tokens > 0) { + expandDataLocal.push({ + key: t('缓存创建 Tokens'), + value: other.cache_creation_tokens, + }); + } if (logs[i].type === 2) { - let modelMapped = other?.is_model_mapped && other?.upstream_model_name && other?.upstream_model_name !== ''; + expandDataLocal.push({ + key: t('日志详情'), + value: other?.claude + ? renderClaudeLogContent( + other?.model_ratio, + other.completion_ratio, + other.model_price, + other.group_ratio, + other.user_group_ratio, + other.cache_ratio || 1.0, + other.cache_creation_ratio || 1.0, + ) + : renderLogContent( + other?.model_ratio, + other.completion_ratio, + other.model_price, + other.group_ratio, + other.user_group_ratio, + ), + }); + } + if (logs[i].type === 2) { + let modelMapped = + other?.is_model_mapped && + other?.upstream_model_name && + other?.upstream_model_name !== ''; if (modelMapped) { expandDataLocal.push({ key: t('请求并计费模型'), @@ -850,6 +964,19 @@ const LogsTable = () => { other?.cache_tokens || 0, other?.cache_ratio || 1.0, ); + } else if (other?.claude) { + content = renderClaudeModelPrice( + logs[i].prompt_tokens, + logs[i].completion_tokens, + other.model_ratio, + other.model_price, + other.completion_ratio, + other.group_ratio, + other.cache_tokens || 0, + other.cache_ratio || 1.0, + other.cache_creation_tokens || 0, + other.cache_creation_ratio || 1.0, + ); } else { content = renderModelPrice( logs[i].prompt_tokens, @@ -958,16 +1085,44 @@ const LogsTable = () => { <> {renderColumnSelector()} -
+
- - {t('总消耗额度')}: {renderQuota(stat.quota)} + + {t('消耗额度')}: {renderQuota(stat.quota)} - + RPM: {stat.rpm} - + TPM: {stat.tpm} @@ -977,46 +1132,46 @@ const LogsTable = () => { <>
- { - styleState.isMobile ? ( -
- { - console.log(value); - handleInputChange(value, 'start_timestamp') - }} - /> - handleInputChange(value, 'end_timestamp')} - /> -
- ) : ( + {styleState.isMobile ? ( +
{ - if (Array.isArray(value) && value.length === 2) { - handleInputChange(value[0], 'start_timestamp'); - handleInputChange(value[1], 'end_timestamp'); - } + console.log(value); + handleInputChange(value, 'start_timestamp'); }} /> - ) - } + + handleInputChange(value, 'end_timestamp') + } + /> +
+ ) : ( + { + if (Array.isArray(value) && value.length === 2) { + handleInputChange(value[0], 'start_timestamp'); + handleInputChange(value[1], 'end_timestamp'); + } + }} + /> + )}
{ -
+
{ @@ -392,7 +413,7 @@ const ModelPricing = () => { t('第 {{start}} - {{end}} 条,共 {{total}} 条', { start: page.currentStart, end: page.currentEnd, - total: models.length + total: models.length, }), pageSize: models.length, showSizeChanger: false, diff --git a/web/src/components/ModelSetting.js b/web/src/components/ModelSetting.js index 904b4015..2a566d6b 100644 --- a/web/src/components/ModelSetting.js +++ b/web/src/components/ModelSetting.js @@ -1,21 +1,27 @@ import React, { useEffect, useState } from 'react'; import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; - import { API, showError, showSuccess } from '../helpers'; import { useTranslation } from 'react-i18next'; import SettingGeminiModel from '../pages/Setting/Model/SettingGeminiModel.js'; import SettingClaudeModel from '../pages/Setting/Model/SettingClaudeModel.js'; +import SettingGlobalModel from '../pages/Setting/Model/SettingGlobalModel.js'; const ModelSetting = () => { const { t } = useTranslation(); let [inputs, setInputs] = useState({ 'gemini.safety_settings': '', 'gemini.version_settings': '', + 'gemini.supported_imagine_models': '', 'claude.model_headers_settings': '', 'claude.thinking_adapter_enabled': true, 'claude.default_max_tokens': '', 'claude.thinking_adapter_budget_tokens_percentage': 0.8, + 'global.pass_through_request_enabled': false, + 'general_setting.ping_interval_enabled': false, + 'general_setting.ping_interval_seconds': 60, + 'gemini.thinking_adapter_enabled': false, + 'gemini.thinking_adapter_budget_tokens_percentage': 0.6, }); let [loading, setLoading] = useState(false); @@ -29,14 +35,13 @@ const ModelSetting = () => { if ( item.key === 'gemini.safety_settings' || item.key === 'gemini.version_settings' || - item.key === 'claude.model_headers_settings'|| - item.key === 'claude.default_max_tokens' + item.key === 'claude.model_headers_settings' || + item.key === 'claude.default_max_tokens' || + item.key === 'gemini.supported_imagine_models' ) { item.value = JSON.stringify(JSON.parse(item.value), null, 2); } - if ( - item.key.endsWith('Enabled') - ) { + if (item.key.endsWith('Enabled') || item.key.endsWith('enabled')) { newInputs[item.key] = item.value === 'true' ? true : false; } else { newInputs[item.key] = item.value; @@ -67,6 +72,10 @@ const ModelSetting = () => { return ( <> + {/* OpenAI */} + + + {/* Gemini */} diff --git a/web/src/components/OAuth2Callback.js b/web/src/components/OAuth2Callback.js index cb8a4fcb..616ec313 100644 --- a/web/src/components/OAuth2Callback.js +++ b/web/src/components/OAuth2Callback.js @@ -6,56 +6,58 @@ import { UserContext } from '../context/User'; import { setUserData } from '../helpers/data.js'; const OAuth2Callback = (props) => { - const [searchParams, setSearchParams] = useSearchParams(); + const [searchParams, setSearchParams] = useSearchParams(); - const [userState, userDispatch] = useContext(UserContext); - const [prompt, setPrompt] = useState('处理中...'); - const [processing, setProcessing] = useState(true); + const [userState, userDispatch] = useContext(UserContext); + const [prompt, setPrompt] = useState('处理中...'); + const [processing, setProcessing] = useState(true); - let navigate = useNavigate(); + let navigate = useNavigate(); - const sendCode = async (code, state, count) => { - const res = await API.get(`/api/oauth/${props.type}?code=${code}&state=${state}`); - const { success, message, data } = res.data; - if (success) { - if (message === 'bind') { - showSuccess('绑定成功!'); - navigate('/setting'); - } else { - userDispatch({ type: 'login', payload: data }); - localStorage.setItem('user', JSON.stringify(data)); - setUserData(data); - updateAPI() - showSuccess('登录成功!'); - navigate('/token'); - } - } else { - showError(message); - if (count === 0) { - setPrompt(`操作失败,重定向至登录界面中...`); - navigate('/setting'); // in case this is failed to bind GitHub - return; - } - count++; - setPrompt(`出现错误,第 ${count} 次重试中...`); - await new Promise((resolve) => setTimeout(resolve, count * 2000)); - await sendCode(code, state, count); - } - }; - - useEffect(() => { - let code = searchParams.get('code'); - let state = searchParams.get('state'); - sendCode(code, state, 0).then(); - }, []); - - return ( - - - {prompt} - - + const sendCode = async (code, state, count) => { + const res = await API.get( + `/api/oauth/${props.type}?code=${code}&state=${state}`, ); + const { success, message, data } = res.data; + if (success) { + if (message === 'bind') { + showSuccess('绑定成功!'); + navigate('/setting'); + } else { + userDispatch({ type: 'login', payload: data }); + localStorage.setItem('user', JSON.stringify(data)); + setUserData(data); + updateAPI(); + showSuccess('登录成功!'); + navigate('/token'); + } + } else { + showError(message); + if (count === 0) { + setPrompt(`操作失败,重定向至登录界面中...`); + navigate('/setting'); // in case this is failed to bind GitHub + return; + } + count++; + setPrompt(`出现错误,第 ${count} 次重试中...`); + await new Promise((resolve) => setTimeout(resolve, count * 2000)); + await sendCode(code, state, count); + } + }; + + useEffect(() => { + let code = searchParams.get('code'); + let state = searchParams.get('state'); + sendCode(code, state, 0).then(); + }, []); + + return ( + + + {prompt} + + + ); }; export default OAuth2Callback; diff --git a/web/src/components/OIDCIcon.js b/web/src/components/OIDCIcon.js new file mode 100644 index 00000000..eec3e655 --- /dev/null +++ b/web/src/components/OIDCIcon.js @@ -0,0 +1,38 @@ +import React from 'react'; +import { Icon } from '@douyinfe/semi-ui'; + +const OIDCIcon = (props) => { + function CustomIcon() { + return ( + + + + + ); + } + + return } />; +}; + +export default OIDCIcon; diff --git a/web/src/components/OperationSetting.js b/web/src/components/OperationSetting.js index 2e650a5e..28fb3a8f 100644 --- a/web/src/components/OperationSetting.js +++ b/web/src/components/OperationSetting.js @@ -7,12 +7,10 @@ import SettingsLog from '../pages/Setting/Operation/SettingsLog.js'; import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js'; import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js'; import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js'; -import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js'; import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js'; import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js'; import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js'; - import { API, showError, showSuccess } from '../helpers'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; import { useTranslation } from 'react-i18next'; @@ -59,7 +57,7 @@ const OperationSetting = () => { DataExportInterval: 5, DefaultCollapseSidebar: false, // 默认折叠侧边栏 RetryTimes: 0, - Chats: "[]", + Chats: '[]', DemoSiteEnabled: false, SelfUseModeEnabled: false, AutomaticDisableKeywords: '', @@ -155,14 +153,14 @@ const OperationSetting = () => { {/* 合并模型倍率设置和可视化倍率设置 */} - - + + - + - + diff --git a/web/src/components/OtherSetting.js b/web/src/components/OtherSetting.js index e3295fb1..570a86f8 100644 --- a/web/src/components/OtherSetting.js +++ b/web/src/components/OtherSetting.js @@ -1,5 +1,14 @@ import React, { useContext, useEffect, useRef, useState } from 'react'; -import { Banner, Button, Col, Form, Row, Modal, Space } from '@douyinfe/semi-ui'; +import { + Banner, + Button, + Col, + Form, + Row, + Modal, + Space, + Card, +} from '@douyinfe/semi-ui'; import { API, showError, showSuccess, timestamp2string } from '../helpers'; import { marked } from 'marked'; import { useTranslation } from 'react-i18next'; @@ -46,7 +55,7 @@ const OtherSetting = () => { HomePageContent: false, About: false, Footer: false, - CheckUpdate: false + CheckUpdate: false, }); const handleInputChange = async (value, e) => { const name = e.target.id; @@ -151,27 +160,30 @@ const OtherSetting = () => { const checkUpdate = async () => { try { - setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: true })); + setLoadingInput((loadingInput) => ({ + ...loadingInput, + CheckUpdate: true, + })); // Use a CORS proxy to avoid direct cross-origin requests to GitHub API // Option 1: Use a public CORS proxy service // const proxyUrl = 'https://cors-anywhere.herokuapp.com/'; // const res = await API.get( // `${proxyUrl}https://api.github.com/repos/Calcium-Ion/new-api/releases/latest`, // ); - + // Option 2: Use the JSON proxy approach which often works better with GitHub API const res = await fetch( 'https://api.github.com/repos/Calcium-Ion/new-api/releases/latest', { headers: { - 'Accept': 'application/json', + Accept: 'application/json', 'Content-Type': 'application/json', // Adding User-Agent which is often required by GitHub API - 'User-Agent': 'new-api-update-checker' - } - } - ).then(response => response.json()); - + 'User-Agent': 'new-api-update-checker', + }, + }, + ).then((response) => response.json()); + // Option 3: Use a local proxy endpoint // Create a cached version of the response to avoid frequent GitHub API calls // const res = await API.get('/api/status/github-latest-release'); @@ -190,7 +202,10 @@ const OtherSetting = () => { console.error('Failed to check for updates:', error); showError('检查更新失败,请稍后再试'); } finally { - setLoadingInput((loadingInput) => ({ ...loadingInput, CheckUpdate: false })); + setLoadingInput((loadingInput) => ({ + ...loadingInput, + CheckUpdate: false, + })); } }; const getOptions = async () => { @@ -217,7 +232,10 @@ const OtherSetting = () => { // Function to open GitHub release page const openGitHubRelease = () => { - window.open(`https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`, '_blank'); + window.open( + `https://github.com/Calcium-Ion/new-api/releases/tag/${updateData.tag_name}`, + '_blank', + ); }; const getStartTimeString = () => { @@ -227,120 +245,149 @@ const OtherSetting = () => { return ( -
+ {/* 版本信息 */} - - - - - + + + + + + + + {t('当前版本')}: + {statusState?.status?.version || t('未知')} + + + + + + + - {t('当前版本')}:{statusState?.status?.version || t('未知')} + {t('启动时间')}:{getStartTimeString()} - - - - - - - {t('启动时间')}:{getStartTimeString()} - - - + + + + {/* 通用设置 */} (formAPISettingGeneral.current = formAPI)} - style={{ marginBottom: 15 }} > - - - - + + + + + + {/* 个性化设置 */} (formAPIPersonalization.current = formAPI)} - style={{ marginBottom: 15 }} > - - - - - - - - - - {/* */} - - - - + + + + + + + + + + + {/* */} + + + + + { visible={showUpdateModal} onCancel={() => setShowUpdateModal(false)} footer={[ - + , ]} >
diff --git a/web/src/components/PageLayout.js b/web/src/components/PageLayout.js index da1c09f9..d52bc0d4 100644 --- a/web/src/components/PageLayout.js +++ b/web/src/components/PageLayout.js @@ -13,7 +13,6 @@ import { UserContext } from '../context/User/index.js'; import { StatusContext } from '../context/Status/index.js'; const { Sider, Content, Header, Footer } = Layout; - const PageLayout = () => { const [userState, userDispatch] = useContext(UserContext); const [statusState, statusDispatch] = useContext(StatusContext); @@ -62,31 +61,104 @@ const PageLayout = () => { if (savedLang) { i18n.changeLanguage(savedLang); } + + // 默认显示侧边栏 + styleDispatch({ type: 'SET_SIDER', payload: true }); }, [i18n]); + // 获取侧边栏折叠状态 + const isSidebarCollapsed = + localStorage.getItem('default_collapse_sidebar') === 'true'; + return ( - -
+ +
- - - {styleState.showSider ? : null} - - + + {styleState.showSider && ( + + + + )} + - + - ) -} + ); +}; -export default PageLayout; \ No newline at end of file +export default PageLayout; diff --git a/web/src/components/PersonalSetting.js b/web/src/components/PersonalSetting.js index 5ca95397..d1e03db2 100644 --- a/web/src/components/PersonalSetting.js +++ b/web/src/components/PersonalSetting.js @@ -1,795 +1,884 @@ -import React, {useContext, useEffect, useState} from 'react'; -import {useNavigate} from 'react-router-dom'; +import React, { useContext, useEffect, useState } from 'react'; +import { useNavigate } from 'react-router-dom'; import { - API, - copy, - isRoot, - showError, - showInfo, - showSuccess, + API, + copy, + isRoot, + showError, + showInfo, + showSuccess, } from '../helpers'; import Turnstile from 'react-turnstile'; -import {UserContext} from '../context/User'; -import {onGitHubOAuthClicked, onLinuxDOOAuthClicked} from './utils'; +import { UserContext } from '../context/User'; import { - Avatar, - Banner, - Button, - Card, - Descriptions, - Image, - Input, - InputNumber, - Layout, - Modal, - Space, - Tag, - Typography, - Collapsible, - Select, - Radio, - RadioGroup, - AutoComplete, + onGitHubOAuthClicked, + onOIDCClicked, + onLinuxDOOAuthClicked, +} from './utils'; +import { + Avatar, + Banner, + Button, + Card, + Descriptions, + Image, + Input, + InputNumber, + Layout, + Modal, + Space, + Tag, + Typography, + Collapsible, + Select, + Radio, + RadioGroup, + AutoComplete, + Checkbox, + Tabs, + TabPane, } from '@douyinfe/semi-ui'; import { - getQuotaPerUnit, - renderQuota, - renderQuotaWithPrompt, - stringToColor, + getQuotaPerUnit, + renderQuota, + renderQuotaWithPrompt, + stringToColor, } from '../helpers/render'; import TelegramLoginButton from 'react-telegram-login'; import { useTranslation } from 'react-i18next'; const PersonalSetting = () => { - const [userState, userDispatch] = useContext(UserContext); - let navigate = useNavigate(); - const { t } = useTranslation(); + const [userState, userDispatch] = useContext(UserContext); + let navigate = useNavigate(); + const { t } = useTranslation(); - const [inputs, setInputs] = useState({ - wechat_verification_code: '', - email_verification_code: '', - email: '', - self_account_deletion_confirmation: '', - set_new_password: '', - set_new_password_confirmation: '', + const [inputs, setInputs] = useState({ + wechat_verification_code: '', + email_verification_code: '', + email: '', + self_account_deletion_confirmation: '', + set_new_password: '', + set_new_password_confirmation: '', + }); + const [status, setStatus] = useState({}); + const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); + const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); + const [showEmailBindModal, setShowEmailBindModal] = useState(false); + const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); + const [turnstileEnabled, setTurnstileEnabled] = useState(false); + const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); + const [turnstileToken, setTurnstileToken] = useState(''); + const [loading, setLoading] = useState(false); + const [disableButton, setDisableButton] = useState(false); + const [countdown, setCountdown] = useState(30); + const [affLink, setAffLink] = useState(''); + const [systemToken, setSystemToken] = useState(''); + const [models, setModels] = useState([]); + const [openTransfer, setOpenTransfer] = useState(false); + const [transferAmount, setTransferAmount] = useState(0); + const [isModelsExpanded, setIsModelsExpanded] = useState(() => { + // Initialize from localStorage if available + const savedState = localStorage.getItem('modelsExpanded'); + return savedState ? JSON.parse(savedState) : false; + }); + const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量 + const [notificationSettings, setNotificationSettings] = useState({ + warningType: 'email', + warningThreshold: 100000, + webhookUrl: '', + webhookSecret: '', + notificationEmail: '', + acceptUnsetModelRatioModel: false, + }); + const [showWebhookDocs, setShowWebhookDocs] = useState(false); + + useEffect(() => { + let status = localStorage.getItem('status'); + if (status) { + status = JSON.parse(status); + setStatus(status); + if (status.turnstile_check) { + setTurnstileEnabled(true); + setTurnstileSiteKey(status.turnstile_site_key); + } + } + getUserData().then((res) => { + console.log(userState); }); - const [status, setStatus] = useState({}); - const [showChangePasswordModal, setShowChangePasswordModal] = useState(false); - const [showWeChatBindModal, setShowWeChatBindModal] = useState(false); - const [showEmailBindModal, setShowEmailBindModal] = useState(false); - const [showAccountDeleteModal, setShowAccountDeleteModal] = useState(false); - const [turnstileEnabled, setTurnstileEnabled] = useState(false); - const [turnstileSiteKey, setTurnstileSiteKey] = useState(''); - const [turnstileToken, setTurnstileToken] = useState(''); - const [loading, setLoading] = useState(false); - const [disableButton, setDisableButton] = useState(false); - const [countdown, setCountdown] = useState(30); - const [affLink, setAffLink] = useState(''); - const [systemToken, setSystemToken] = useState(''); - const [models, setModels] = useState([]); - const [openTransfer, setOpenTransfer] = useState(false); - const [transferAmount, setTransferAmount] = useState(0); - const [isModelsExpanded, setIsModelsExpanded] = useState(() => { - // Initialize from localStorage if available - const savedState = localStorage.getItem('modelsExpanded'); - return savedState ? JSON.parse(savedState) : false; + loadModels().then(); + getAffLink().then(); + setTransferAmount(getQuotaPerUnit()); + }, []); + + useEffect(() => { + let countdownInterval = null; + if (disableButton && countdown > 0) { + countdownInterval = setInterval(() => { + setCountdown(countdown - 1); + }, 1000); + } else if (countdown === 0) { + setDisableButton(false); + setCountdown(30); + } + return () => clearInterval(countdownInterval); // Clean up on unmount + }, [disableButton, countdown]); + + useEffect(() => { + if (userState?.user?.setting) { + const settings = JSON.parse(userState.user.setting); + setNotificationSettings({ + warningType: settings.notify_type || 'email', + warningThreshold: settings.quota_warning_threshold || 500000, + webhookUrl: settings.webhook_url || '', + webhookSecret: settings.webhook_secret || '', + notificationEmail: settings.notification_email || '', + acceptUnsetModelRatioModel: + settings.accept_unset_model_ratio_model || false, + }); + } + }, [userState?.user?.setting]); + + // Save models expanded state to localStorage whenever it changes + useEffect(() => { + localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded)); + }, [isModelsExpanded]); + + const handleInputChange = (name, value) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; + + const generateAccessToken = async () => { + const res = await API.get('/api/user/token'); + const { success, message, data } = res.data; + if (success) { + setSystemToken(data); + await copy(data); + showSuccess(t('令牌已重置并已复制到剪贴板')); + } else { + showError(message); + } + }; + + const getAffLink = async () => { + const res = await API.get('/api/user/aff'); + const { success, message, data } = res.data; + if (success) { + let link = `${window.location.origin}/register?aff=${data}`; + setAffLink(link); + } else { + showError(message); + } + }; + + const getUserData = async () => { + let res = await API.get(`/api/user/self`); + const { success, message, data } = res.data; + if (success) { + userDispatch({ type: 'login', payload: data }); + } else { + showError(message); + } + }; + + const loadModels = async () => { + let res = await API.get(`/api/user/models`); + const { success, message, data } = res.data; + if (success) { + if (data != null) { + setModels(data); + } + } else { + showError(message); + } + }; + + const handleAffLinkClick = async (e) => { + e.target.select(); + await copy(e.target.value); + showSuccess(t('邀请链接已复制到剪切板')); + }; + + const handleSystemTokenClick = async (e) => { + e.target.select(); + await copy(e.target.value); + showSuccess(t('系统令牌已复制到剪切板')); + }; + + const deleteAccount = async () => { + if (inputs.self_account_deletion_confirmation !== userState.user.username) { + showError(t('请输入你的账户名以确认删除!')); + return; + } + + const res = await API.delete('/api/user/self'); + const { success, message } = res.data; + + if (success) { + showSuccess(t('账户已删除!')); + await API.get('/api/user/logout'); + userDispatch({ type: 'logout' }); + localStorage.removeItem('user'); + navigate('/login'); + } else { + showError(message); + } + }; + + const bindWeChat = async () => { + if (inputs.wechat_verification_code === '') return; + const res = await API.get( + `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, + ); + const { success, message } = res.data; + if (success) { + showSuccess(t('微信账户绑定成功!')); + setShowWeChatBindModal(false); + } else { + showError(message); + } + }; + + const changePassword = async () => { + if (inputs.set_new_password !== inputs.set_new_password_confirmation) { + showError(t('两次输入的密码不一致!')); + return; + } + const res = await API.put(`/api/user/self`, { + password: inputs.set_new_password, }); - const MODELS_DISPLAY_COUNT = 10; // 默认显示的模型数量 - const [notificationSettings, setNotificationSettings] = useState({ - warningType: 'email', - warningThreshold: 100000, - webhookUrl: '', - webhookSecret: '', - notificationEmail: '' + const { success, message } = res.data; + if (success) { + showSuccess(t('密码修改成功!')); + setShowWeChatBindModal(false); + } else { + showError(message); + } + setShowChangePasswordModal(false); + }; + + const transfer = async () => { + if (transferAmount < getQuotaPerUnit()) { + showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit())); + return; + } + const res = await API.post(`/api/user/aff_transfer`, { + quota: transferAmount, }); - const [showWebhookDocs, setShowWebhookDocs] = useState(false); + const { success, message } = res.data; + if (success) { + showSuccess(message); + setOpenTransfer(false); + getUserData().then(); + } else { + showError(message); + } + }; - useEffect(() => { - let status = localStorage.getItem('status'); - if (status) { - status = JSON.parse(status); - setStatus(status); - if (status.turnstile_check) { - setTurnstileEnabled(true); - setTurnstileSiteKey(status.turnstile_site_key); - } - } - getUserData().then((res) => { - console.log(userState); - }); - loadModels().then(); - getAffLink().then(); - setTransferAmount(getQuotaPerUnit()); - }, []); + const sendVerificationCode = async () => { + if (inputs.email === '') { + showError(t('请输入邮箱!')); + return; + } + setDisableButton(true); + if (turnstileEnabled && turnstileToken === '') { + showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); + return; + } + setLoading(true); + const res = await API.get( + `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, + ); + const { success, message } = res.data; + if (success) { + showSuccess(t('验证码发送成功,请检查邮箱!')); + } else { + showError(message); + } + setLoading(false); + }; - useEffect(() => { - let countdownInterval = null; - if (disableButton && countdown > 0) { - countdownInterval = setInterval(() => { - setCountdown(countdown - 1); - }, 1000); - } else if (countdown === 0) { - setDisableButton(false); - setCountdown(30); - } - return () => clearInterval(countdownInterval); // Clean up on unmount - }, [disableButton, countdown]); + const bindEmail = async () => { + if (inputs.email_verification_code === '') { + showError(t('请输入邮箱验证码!')); + return; + } + setLoading(true); + const res = await API.get( + `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, + ); + const { success, message } = res.data; + if (success) { + showSuccess(t('邮箱账户绑定成功!')); + setShowEmailBindModal(false); + userState.user.email = inputs.email; + } else { + showError(message); + } + setLoading(false); + }; - useEffect(() => { - if (userState?.user?.setting) { - const settings = JSON.parse(userState.user.setting); - setNotificationSettings({ - warningType: settings.notify_type || 'email', - warningThreshold: settings.quota_warning_threshold || 500000, - webhookUrl: settings.webhook_url || '', - webhookSecret: settings.webhook_secret || '', - notificationEmail: settings.notification_email || '' - }); - } - }, [userState?.user?.setting]); + const getUsername = () => { + if (userState.user) { + return userState.user.username; + } else { + return 'null'; + } + }; - // Save models expanded state to localStorage whenever it changes - useEffect(() => { - localStorage.setItem('modelsExpanded', JSON.stringify(isModelsExpanded)); - }, [isModelsExpanded]); + const handleCancel = () => { + setOpenTransfer(false); + }; - const handleInputChange = (name, value) => { - setInputs((inputs) => ({...inputs, [name]: value})); - }; + const copyText = async (text) => { + if (await copy(text)) { + showSuccess(t('已复制:') + text); + } else { + // setSearchKeyword(text); + Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: text }); + } + }; - const generateAccessToken = async () => { - const res = await API.get('/api/user/token'); - const {success, message, data} = res.data; - if (success) { - setSystemToken(data); - await copy(data); - showSuccess(t('令牌已重置并已复制到剪贴板')); - } else { - showError(message); - } - }; + const handleNotificationSettingChange = (type, value) => { + setNotificationSettings((prev) => ({ + ...prev, + [type]: value.target ? value.target.value : value, // 处理 Radio 事件对象 + })); + }; - const getAffLink = async () => { - const res = await API.get('/api/user/aff'); - const {success, message, data} = res.data; - if (success) { - let link = `${window.location.origin}/register?aff=${data}`; - setAffLink(link); - } else { - showError(message); - } - }; + const saveNotificationSettings = async () => { + try { + const res = await API.put('/api/user/setting', { + notify_type: notificationSettings.warningType, + quota_warning_threshold: parseFloat( + notificationSettings.warningThreshold, + ), + webhook_url: notificationSettings.webhookUrl, + webhook_secret: notificationSettings.webhookSecret, + notification_email: notificationSettings.notificationEmail, + accept_unset_model_ratio_model: + notificationSettings.acceptUnsetModelRatioModel, + }); - const getUserData = async () => { - let res = await API.get(`/api/user/self`); - const {success, message, data} = res.data; - if (success) { - userDispatch({type: 'login', payload: data}); - } else { - showError(message); - } - }; + if (res.data.success) { + showSuccess(t('通知设置已更新')); + await getUserData(); + } else { + showError(res.data.message); + } + } catch (error) { + showError(t('更新通知设置失败')); + } + }; - const loadModels = async () => { - let res = await API.get(`/api/user/models`); - const {success, message, data} = res.data; - if (success) { - if (data != null) { - setModels(data); - } - } else { - showError(message); - } - }; - - const handleAffLinkClick = async (e) => { - e.target.select(); - await copy(e.target.value); - showSuccess(t('邀请链接已复制到剪切板')); - }; - - const handleSystemTokenClick = async (e) => { - e.target.select(); - await copy(e.target.value); - showSuccess(t('系统令牌已复制到剪切板')); - }; - - const deleteAccount = async () => { - if (inputs.self_account_deletion_confirmation !== userState.user.username) { - showError(t('请输入你的账户名以确认删除!')); - return; - } - - const res = await API.delete('/api/user/self'); - const {success, message} = res.data; - - if (success) { - showSuccess(t('账户已删除!')); - await API.get('/api/user/logout'); - userDispatch({type: 'logout'}); - localStorage.removeItem('user'); - navigate('/login'); - } else { - showError(message); - } - }; - - const bindWeChat = async () => { - if (inputs.wechat_verification_code === '') return; - const res = await API.get( - `/api/oauth/wechat/bind?code=${inputs.wechat_verification_code}`, - ); - const {success, message} = res.data; - if (success) { - showSuccess(t('微信账户绑定成功!')); - setShowWeChatBindModal(false); - } else { - showError(message); - } - }; - - const changePassword = async () => { - if (inputs.set_new_password !== inputs.set_new_password_confirmation) { - showError(t('两次输入的密码不一致!')); - return; - } - const res = await API.put(`/api/user/self`, { - password: inputs.set_new_password, - }); - const {success, message} = res.data; - if (success) { - showSuccess(t('密码修改成功!')); - setShowWeChatBindModal(false); - } else { - showError(message); - } - setShowChangePasswordModal(false); - }; - - const transfer = async () => { - if (transferAmount < getQuotaPerUnit()) { - showError(t('划转金额最低为') + ' ' + renderQuota(getQuotaPerUnit())); - return; - } - const res = await API.post(`/api/user/aff_transfer`, { - quota: transferAmount, - }); - const {success, message} = res.data; - if (success) { - showSuccess(message); - setOpenTransfer(false); - getUserData().then(); - } else { - showError(message); - } - }; - - const sendVerificationCode = async () => { - if (inputs.email === '') { - showError(t('请输入邮箱!')); - return; - } - setDisableButton(true); - if (turnstileEnabled && turnstileToken === '') { - showInfo('请稍后几秒重试,Turnstile 正在检查用户环境!'); - return; - } - setLoading(true); - const res = await API.get( - `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, - ); - const {success, message} = res.data; - if (success) { - showSuccess(t('验证码发送成功,请检查邮箱!')); - } else { - showError(message); - } - setLoading(false); - }; - - const bindEmail = async () => { - if (inputs.email_verification_code === '') { - showError(t('请输入邮箱验证码!')); - return; - } - setLoading(true); - const res = await API.get( - `/api/oauth/email/bind?email=${inputs.email}&code=${inputs.email_verification_code}`, - ); - const {success, message} = res.data; - if (success) { - showSuccess(t('邮箱账户绑定成功!')); - setShowEmailBindModal(false); - userState.user.email = inputs.email; - } else { - showError(message); - } - setLoading(false); - }; - - const getUsername = () => { - if (userState.user) { - return userState.user.username; - } else { - return 'null'; - } - }; - - const handleCancel = () => { - setOpenTransfer(false); - }; - - const copyText = async (text) => { - if (await copy(text)) { - showSuccess(t('已复制:') + text); - } else { - // setSearchKeyword(text); - Modal.error({title: t('无法复制到剪贴板,请手动复制'), content: text}); - } - }; - - const handleNotificationSettingChange = (type, value) => { - setNotificationSettings(prev => ({ - ...prev, - [type]: value.target ? value.target.value : value // 处理 Radio 事件对象 - })); - }; - - const saveNotificationSettings = async () => { - try { - const res = await API.put('/api/user/setting', { - notify_type: notificationSettings.warningType, - quota_warning_threshold: parseFloat(notificationSettings.warningThreshold), - webhook_url: notificationSettings.webhookUrl, - webhook_secret: notificationSettings.webhookSecret, - notification_email: notificationSettings.notificationEmail - }); - - if (res.data.success) { - showSuccess(t('通知设置已更新')); - await getUserData(); - } else { - showError(res.data.message); - } - } catch (error) { - showError(t('更新通知设置失败')); - } - }; - - return ( - -
- - - + + + +
+ + {t('可用额度')} + {renderQuotaWithPrompt(userState?.user?.aff_quota)} + + +
+
+ + {t('划转额度')} + {renderQuotaWithPrompt(transferAmount)}{' '} + {t('最低') + renderQuota(getQuotaPerUnit())} + +
+ setTransferAmount(value)} + disabled={false} + > +
+
+
+
+ -
- {t('可用额度')}{renderQuotaWithPrompt(userState?.user?.aff_quota)} - -
-
- - {t('划转额度')}{renderQuotaWithPrompt(transferAmount)} {t('最低') + renderQuota(getQuotaPerUnit())} - -
- setTransferAmount(value)} - disabled={false} - > -
-
- -
- - {typeof getUsername() === 'string' && - getUsername().slice(0, 1)} - - } - title={{getUsername()}} - description={ - isRoot() ? ( - {t('管理员')} - ) : ( - {t('普通用户')} - ) - } - > - } - headerExtraContent={ - <> - - {'ID: ' + userState?.user?.id} - {userState?.user?.group} - - - } - footer={ - <> -
- {t('可用模型')} -
-
- {models.length <= MODELS_DISPLAY_COUNT ? ( - - {models.map((model) => ( - { - copyText(model); - }} - > - {model} - - ))} - - ) : ( - <> - - - {models.map((model) => ( - { - copyText(model); - }} - > - {model} - - ))} - setIsModelsExpanded(false)} - > - {t('收起')} - - - - {!isModelsExpanded && ( - - {models.slice(0, MODELS_DISPLAY_COUNT).map((model) => ( - { - copyText(model); - }} - > - {model} - - ))} - setIsModelsExpanded(true)} - > - {t('更多')} {models.length - MODELS_DISPLAY_COUNT} {t('个模型')} - - - )} - - )} -
- + {typeof getUsername() === 'string' && + getUsername().slice(0, 1)} + + } + title={{getUsername()}} + description={ + isRoot() ? ( + {t('管理员')} + ) : ( + {t('普通用户')} + ) + } + > + } + headerExtraContent={ + <> + + {'ID: ' + userState?.user?.id} + {userState?.user?.group} + + + } + footer={ + <> +
+ + {t('可用模型')} + +
+
+ {models.length <= MODELS_DISPLAY_COUNT ? ( + + {models.map((model) => ( + { + copyText(model); + }} + > + {model} + + ))} + + ) : ( + <> + + + {models.map((model) => ( + { + copyText(model); + }} + > + {model} + + ))} + setIsModelsExpanded(false)} + > + {t('收起')} + + + + {!isModelsExpanded && ( + + {models + .slice(0, MODELS_DISPLAY_COUNT) + .map((model) => ( + { + copyText(model); + }} + > + {model} + + ))} + setIsModelsExpanded(true)} + > + {t('更多')} {models.length - MODELS_DISPLAY_COUNT}{' '} + {t('个模型')} + + + )} + + )} +
+ + } + > + + + {renderQuota(userState?.user?.quota)} + + + {renderQuota(userState?.user?.used_quota)} + + + {userState.user?.request_count} + + +
+ + {t('邀请链接')} + +
+ } + > + {t('邀请信息')} +
+ + + + {renderQuota(userState?.user?.aff_quota)} + + + + + {renderQuota(userState?.user?.aff_history_quota)} + + + {userState?.user?.aff_count} + + +
+
+ + {t('个人信息')} +
+ {t('邮箱')} +
+
+ +
+
+ +
+
+
+
+ {t('微信')} +
+
+ +
+
+ +
+
+
+
+ {t('GitHub')} +
+
+ +
+
+ +
+
+
+
+ {t('OIDC')} +
+
+ +
+
+ +
+
+
+
+ {t('Telegram')} +
+
+ +
+
+ {status.telegram_oauth ? ( + userState.user.telegram_id !== '' ? ( + + ) : ( + + ) + ) : ( + + )} +
+
+
+
+ {t('LinuxDO')} +
+
+ +
+
+ +
+
+
+
+ + + + + + {systemToken && ( + + )} + setShowWeChatBindModal(false)} + visible={showWeChatBindModal} + size={'small'} + > + +
+

+ 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) +

+
+ + handleInputChange('wechat_verification_code', v) + } + /> + +
+
+
+ + + +
+ {t('通知方式')} +
+ + handleNotificationSettingChange('warningType', value) + } + > + {t('邮件通知')} + {t('Webhook通知')} + +
+
+ {notificationSettings.warningType === 'webhook' && ( + <> +
+ + {t('Webhook地址')} + +
+ + handleNotificationSettingChange('webhookUrl', val) } - > - - - {renderQuota(userState?.user?.quota)} - - - {renderQuota(userState?.user?.used_quota)} - - - {userState.user?.request_count} - - - - - {t('邀请链接')} - -
- } - > - {t('邀请信息')} -
- - - - {renderQuota(userState?.user?.aff_quota)} - - - - - {renderQuota(userState?.user?.aff_history_quota)} - - - {userState?.user?.aff_count} - - + placeholder={t( + '请输入Webhook地址,例如: https://example.com/webhook', + )} + /> + + {t( + '只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求', + )} + + +
+ setShowWebhookDocs(!showWebhookDocs) + } + > + {t('Webhook请求结构')}{' '} + {showWebhookDocs ? '▼' : '▶'}
- - - {t('个人信息')} -
- {t('邮箱')} -
-
- -
-
- -
-
-
-
- {t('微信')} -
-
- -
-
- -
-
-
-
- {t('GitHub')} -
-
- -
-
- -
-
-
-
- {t('Telegram')} -
-
- -
-
- {status.telegram_oauth ? ( - userState.user.telegram_id !== '' ? ( - - ) : ( - - ) - ) : ( - - )} -
-
-
-
- {t('LinuxDO')} -
-
- -
-
- -
-
-
-
- - - - - - - {systemToken && ( - - )} - setShowWeChatBindModal(false)} - visible={showWeChatBindModal} - size={'small'} - > - -
-

- 微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效) -

-
- - handleInputChange('wechat_verification_code', v) - } - /> - -
-
-
- - {t('通知设置')} -
- {t('通知方式')} -
- handleNotificationSettingChange('warningType', value)} - > - {t('邮件通知')} - {t('Webhook通知')} - -
-
- {notificationSettings.warningType === 'webhook' && ( - <> -
- {t('Webhook地址')} -
- handleNotificationSettingChange('webhookUrl', val)} - placeholder={t('请输入Webhook地址,例如: https://example.com/webhook')} - /> - - {t('只支持https,系统将以 POST 方式发送通知,请确保地址可以接收 POST 请求')} - - -
setShowWebhookDocs(!showWebhookDocs)}> - {t('Webhook请求结构')} {showWebhookDocs ? '▼' : '▶'} -
- -
-{`{
+                            
+                              
+                                {`{
     "type": "quota_exceed",      // 通知类型
     "title": "标题",             // 通知标题
     "content": "通知内容",       // 通知内容,支持 {{value}} 变量占位符
@@ -805,202 +894,263 @@ const PersonalSetting = () => {
     "values": ["$0.99"],
     "timestamp": 1739950503
 }`}
-                                                    
-
- -
-
-
- {t('接口凭证(可选)')} -
- handleNotificationSettingChange('webhookSecret', val)} - placeholder={t('请输入密钥')} - /> - - {t('密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性')} - - - {t('Authorization: Bearer your-secret-key')} - -
-
- + + +
+
+
+
+ + {t('接口凭证(可选)')} + +
+ + handleNotificationSettingChange( + 'webhookSecret', + val, + ) + } + placeholder={t('请输入密钥')} + /> + + {t( + '密钥将以 Bearer 方式添加到请求头中,用于验证webhook请求的合法性', )} - {notificationSettings.warningType === 'email' && ( -
- {t('通知邮箱')} -
- handleNotificationSettingChange('notificationEmail', val)} - placeholder={t('留空则使用账号绑定的邮箱')} - /> - - {t('设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱')} - -
-
- )} -
- {t('额度预警阈值')} {renderQuotaWithPrompt(notificationSettings.warningThreshold)} -
- handleNotificationSettingChange('warningThreshold', val)} - style={{width: 200}} - placeholder={t('请输入预警额度')} - data={[ - { value: 100000, label: '0.2$' }, - { value: 500000, label: '1$' }, - { value: 1000000, label: '5$' }, - { value: 5000000, label: '10$' } - ]} - /> -
- - {t('当剩余额度低于此数值时,系统将通过选择的方式发送通知')} - -
-
- -
- - setShowEmailBindModal(false)} - onOk={bindEmail} - visible={showEmailBindModal} - size={'small'} - centered={true} - maskClosable={false} +
+ + {t('Authorization: Bearer your-secret-key')} + +
+
+ + )} + {notificationSettings.warningType === 'email' && ( +
+ {t('通知邮箱')} +
+ + handleNotificationSettingChange( + 'notificationEmail', + val, + ) + } + placeholder={t('留空则使用账号绑定的邮箱')} + /> + - {t('绑定邮箱地址')} -
- handleInputChange('email', value)} - name='email' - type='email' - /> - -
-
- - handleInputChange('email_verification_code', value) - } - /> -
- {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} - - setShowAccountDeleteModal(false)} - visible={showAccountDeleteModal} - size={'small'} - centered={true} - onOk={deleteAccount} - > -
- -
-
- - handleInputChange( - 'self_account_deletion_confirmation', - value, - ) - } - /> - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} -
-
- setShowChangePasswordModal(false)} - visible={showChangePasswordModal} - size={'small'} - centered={true} - onOk={changePassword} - > -
- - handleInputChange('set_new_password', value) - } - /> - - handleInputChange('set_new_password_confirmation', value) - } - /> - {turnstileEnabled ? ( - { - setTurnstileToken(token); - }} - /> - ) : ( - <> - )} -
-
+ {t( + '设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱', + )} +
+
- - -
- ); + )} +
+ + {t('额度预警阈值')}{' '} + {renderQuotaWithPrompt( + notificationSettings.warningThreshold, + )} + +
+ + handleNotificationSettingChange( + 'warningThreshold', + val, + ) + } + style={{ width: 200 }} + placeholder={t('请输入预警额度')} + data={[ + { value: 100000, label: '0.2$' }, + { value: 500000, label: '1$' }, + { value: 1000000, label: '5$' }, + { value: 5000000, label: '10$' }, + ]} + /> +
+ + {t( + '当剩余额度低于此数值时,系统将通过选择的方式发送通知', + )} + +
+ + +
+ {t('接受未设置价格模型')} +
+ handleNotificationSettingChange('acceptUnsetModelRatioModel', e.target.checked)} + > + {t('接受未设置价格模型')} + + + {t('当模型没有设置价格时仍接受调用,仅当您信任该网站时使用,可能会产生高额费用')} + +
+
+
+ + +
+ +
+ + setShowEmailBindModal(false)} + onOk={bindEmail} + visible={showEmailBindModal} + size={'small'} + centered={true} + maskClosable={false} + > + + {t('绑定邮箱地址')} + +
+ handleInputChange('email', value)} + name='email' + type='email' + /> + +
+
+ + handleInputChange('email_verification_code', value) + } + /> +
+ {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} +
+ setShowAccountDeleteModal(false)} + visible={showAccountDeleteModal} + size={'small'} + centered={true} + onOk={deleteAccount} + > +
+ +
+
+ + handleInputChange( + 'self_account_deletion_confirmation', + value, + ) + } + /> + {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} +
+
+ setShowChangePasswordModal(false)} + visible={showChangePasswordModal} + size={'small'} + centered={true} + onOk={changePassword} + > +
+ + handleInputChange('set_new_password', value) + } + /> + + handleInputChange('set_new_password_confirmation', value) + } + /> + {turnstileEnabled ? ( + { + setTurnstileToken(token); + }} + /> + ) : ( + <> + )} +
+
+
+ +
+ + ); }; export default PersonalSetting; diff --git a/web/src/components/RateLimitSetting.js b/web/src/components/RateLimitSetting.js index b6c92917..e06038d6 100644 --- a/web/src/components/RateLimitSetting.js +++ b/web/src/components/RateLimitSetting.js @@ -1,17 +1,5 @@ import React, { useEffect, useState } from 'react'; import { Card, Spin, Tabs } from '@douyinfe/semi-ui'; -import SettingsGeneral from '../pages/Setting/Operation/SettingsGeneral.js'; -import SettingsDrawing from '../pages/Setting/Operation/SettingsDrawing.js'; -import SettingsSensitiveWords from '../pages/Setting/Operation/SettingsSensitiveWords.js'; -import SettingsLog from '../pages/Setting/Operation/SettingsLog.js'; -import SettingsDataDashboard from '../pages/Setting/Operation/SettingsDataDashboard.js'; -import SettingsMonitoring from '../pages/Setting/Operation/SettingsMonitoring.js'; -import SettingsCreditLimit from '../pages/Setting/Operation/SettingsCreditLimit.js'; -import SettingsMagnification from '../pages/Setting/Operation/SettingsMagnification.js'; -import ModelSettingsVisualEditor from '../pages/Setting/Operation/ModelSettingsVisualEditor.js'; -import GroupRatioSettings from '../pages/Setting/Operation/GroupRatioSettings.js'; -import ModelRatioSettings from '../pages/Setting/Operation/ModelRatioSettings.js'; - import { API, showError, showSuccess } from '../helpers'; import SettingsChats from '../pages/Setting/Operation/SettingsChats.js'; @@ -35,9 +23,7 @@ const RateLimitSetting = () => { if (success) { let newInputs = {}; data.forEach((item) => { - if ( - item.key.endsWith('Enabled') - ) { + if (item.key.endsWith('Enabled')) { newInputs[item.key] = item.value === 'true' ? true : false; } else { newInputs[item.key] = item.value; diff --git a/web/src/components/RedemptionsTable.js b/web/src/components/RedemptionsTable.js index 7822e6a7..f4efca06 100644 --- a/web/src/components/RedemptionsTable.js +++ b/web/src/components/RedemptionsTable.js @@ -10,7 +10,8 @@ import { import { ITEMS_PER_PAGE } from '../constants'; import { renderQuota } from '../helpers/render'; import { - Button, Divider, + Button, + Divider, Form, Modal, Popconfirm, @@ -193,15 +194,17 @@ const RedemptionsTable = () => { }; const loadRedemptions = async (startIdx, pageSize) => { - const res = await API.get(`/api/redemption/?p=${startIdx}&page_size=${pageSize}`); + const res = await API.get( + `/api/redemption/?p=${startIdx}&page_size=${pageSize}`, + ); const { success, message, data } = res.data; if (success) { - const newPageData = data.items; - setActivePage(data.page); - setTokenCount(data.total); - setRedemptionFormat(newPageData); + const newPageData = data.items; + setActivePage(data.page); + setTokenCount(data.total); + setRedemptionFormat(newPageData); } else { - showError(message); + showError(message); } setLoading(false); }; @@ -282,19 +285,21 @@ const RedemptionsTable = () => { const searchRedemptions = async (keyword, page, pageSize) => { if (searchKeyword === '') { - await loadRedemptions(page, pageSize); - return; + await loadRedemptions(page, pageSize); + return; } setSearching(true); - const res = await API.get(`/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`); + const res = await API.get( + `/api/redemption/search?keyword=${keyword}&p=${page}&page_size=${pageSize}`, + ); const { success, message, data } = res.data; if (success) { - const newPageData = data.items; - setActivePage(data.page); - setTokenCount(data.total); - setRedemptionFormat(newPageData); + const newPageData = data.items; + setActivePage(data.page); + setTokenCount(data.total); + setRedemptionFormat(newPageData); } else { - showError(message); + showError(message); } setSearching(false); }; @@ -355,9 +360,11 @@ const RedemptionsTable = () => { visiable={showEdit} handleClose={closeEdit} > -
{ - searchRedemptions(searchKeyword, activePage, pageSize).then(); - }}> + { + searchRedemptions(searchKeyword, activePage, pageSize).then(); + }} + > { onChange={handleKeywordChange} /> - +
@@ -417,7 +425,7 @@ const RedemptionsTable = () => { t('第 {{start}} - {{end}} 条,共 {{total}} 条', { start: page.currentStart, end: page.currentEnd, - total: tokenCount + total: tokenCount, }), onPageSizeChange: (size) => { setPageSize(size); diff --git a/web/src/components/RegisterForm.js b/web/src/components/RegisterForm.js index 4f9c1698..50fe4def 100644 --- a/web/src/components/RegisterForm.js +++ b/web/src/components/RegisterForm.js @@ -1,12 +1,32 @@ import React, { useContext, useEffect, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; -import { API, getLogo, showError, showInfo, showSuccess, updateAPI } from '../helpers'; +import { + API, + getLogo, + showError, + showInfo, + showSuccess, + updateAPI, +} from '../helpers'; import Turnstile from 'react-turnstile'; -import { Button, Card, Divider, Form, Icon, Layout, Modal } from '@douyinfe/semi-ui'; +import { + Button, + Card, + Divider, + Form, + Icon, + Layout, + Modal, +} from '@douyinfe/semi-ui'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; import Text from '@douyinfe/semi-ui/lib/es/typography/text'; import { IconGithubLogo } from '@douyinfe/semi-icons'; -import { onGitHubOAuthClicked, onLinuxDOOAuthClicked } from './utils.js'; +import { + onGitHubOAuthClicked, + onLinuxDOOAuthClicked, + onOIDCClicked, +} from './utils.js'; +import OIDCIcon from './OIDCIcon.js'; import LinuxDoIcon from './LinuxDoIcon.js'; import WeChatIcon from './WeChatIcon.js'; import TelegramLoginButton from 'react-telegram-login/src'; @@ -21,7 +41,7 @@ const RegisterForm = () => { password: '', password2: '', email: '', - verification_code: '' + verification_code: '', }); const { username, password, password2 } = inputs; const [showEmailVerification, setShowEmailVerification] = useState(false); @@ -53,7 +73,6 @@ const RegisterForm = () => { } }); - const onWeChatLoginClicked = () => { setShowWeChatLoginModal(true); }; @@ -105,7 +124,7 @@ const RegisterForm = () => { inputs.aff_code = affCode; const res = await API.post( `/api/user/register?turnstile=${turnstileToken}`, - inputs + inputs, ); const { success, message } = res.data; if (success) { @@ -126,7 +145,7 @@ const RegisterForm = () => { } setLoading(true); const res = await API.get( - `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}` + `/api/verification?email=${inputs.email}&turnstile=${turnstileToken}`, ); const { success, message } = res.data; if (success) { @@ -168,7 +187,6 @@ const RegisterForm = () => { } }; - return (
@@ -178,7 +196,7 @@ const RegisterForm = () => { style={{ justifyContent: 'center', display: 'flex', - marginTop: 120 + marginTop: 120, }} >
@@ -186,28 +204,28 @@ const RegisterForm = () => { {t('新用户注册')} -
+ handleChange('username', value)} /> handleChange('password', value)} /> handleChange('password2', value)} /> {showEmailVerification ? ( @@ -217,10 +235,13 @@ const RegisterForm = () => { label={t('邮箱')} placeholder={t('输入邮箱地址')} onChange={(value) => handleChange('email', value)} - name="email" - type="email" + name='email' + type='email' suffix={ - } @@ -229,8 +250,10 @@ const RegisterForm = () => { field={'verification_code'} label={t('验证码')} placeholder={t('输入验证码')} - onChange={(value) => handleChange('verification_code', value)} - name="verification_code" + onChange={(value) => + handleChange('verification_code', value) + } + name='verification_code' /> ) : ( @@ -251,17 +274,16 @@ const RegisterForm = () => { style={{ display: 'flex', justifyContent: 'space-between', - marginTop: 20 + marginTop: 20, }} > {t('已有账户?')} - - {t('点击登录')} - + {t('点击登录')}
{status.github_oauth || + status.oidc_enabled || status.wechat_login || status.telegram_oauth || status.linuxdo_oauth ? ( @@ -287,6 +309,20 @@ const RegisterForm = () => { ) : ( <> )} + {status.oidc_enabled ? ( +

- {t('微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)')} + {t( + '微信扫码关注公众号,输入「验证码」获取验证码(三分钟内有效)', + )}

diff --git a/web/src/components/SafetySetting.js b/web/src/components/SafetySetting.js deleted file mode 100644 index 7d08838e..00000000 --- a/web/src/components/SafetySetting.js +++ /dev/null @@ -1,790 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { - Button, - Divider, - Form, - Grid, - Header, - Message, - Modal, -} from 'semantic-ui-react'; -import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; - -import { useTheme } from '../context/Theme'; - -const SafetySetting = () => { - let [inputs, setInputs] = useState({ - PasswordLoginEnabled: '', - PasswordRegisterEnabled: '', - EmailVerificationEnabled: '', - GitHubOAuthEnabled: '', - GitHubClientId: '', - GitHubClientSecret: '', - Notice: '', - SMTPServer: '', - SMTPPort: '', - SMTPAccount: '', - SMTPFrom: '', - SMTPToken: '', - ServerAddress: '', - WorkerUrl: '', - WorkerValidKey: '', - EpayId: '', - EpayKey: '', - Price: 7.3, - MinTopUp: 1, - TopupGroupRatio: '', - PayAddress: '', - CustomCallbackAddress: '', - Footer: '', - WeChatAuthEnabled: '', - WeChatServerAddress: '', - WeChatServerToken: '', - WeChatAccountQRCodeImageURL: '', - TurnstileCheckEnabled: '', - TurnstileSiteKey: '', - TurnstileSecretKey: '', - RegisterEnabled: '', - EmailDomainRestrictionEnabled: '', - EmailAliasRestrictionEnabled: '', - SMTPSSLEnabled: '', - EmailDomainWhitelist: [], - // telegram login - TelegramOAuthEnabled: '', - TelegramBotToken: '', - TelegramBotName: '', - }); - const [originInputs, setOriginInputs] = useState({}); - let [loading, setLoading] = useState(false); - const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]); - const [restrictedDomainInput, setRestrictedDomainInput] = useState(''); - const [showPasswordWarningModal, setShowPasswordWarningModal] = - useState(false); - - const theme = useTheme(); - const isDark = theme === 'dark'; - - const getOptions = async () => { - const res = await API.get('/api/option/'); - const { success, message, data } = res.data; - if (success) { - let newInputs = {}; - data.forEach((item) => { - if (item.key === 'TopupGroupRatio') { - item.value = JSON.stringify(JSON.parse(item.value), null, 2); - } - newInputs[item.key] = item.value; - }); - setInputs({ - ...newInputs, - EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','), - }); - setOriginInputs(newInputs); - - setEmailDomainWhitelist( - newInputs.EmailDomainWhitelist.split(',').map((item) => { - return { key: item, text: item, value: item }; - }), - ); - } else { - showError(message); - } - }; - - useEffect(() => { - getOptions().then(); - }, []); - useEffect(() => {}, [inputs.EmailDomainWhitelist]); - - const updateOption = async (key, value) => { - setLoading(true); - switch (key) { - case 'PasswordLoginEnabled': - case 'PasswordRegisterEnabled': - case 'EmailVerificationEnabled': - case 'GitHubOAuthEnabled': - case 'WeChatAuthEnabled': - case 'TelegramOAuthEnabled': - case 'TurnstileCheckEnabled': - case 'EmailDomainRestrictionEnabled': - case 'EmailAliasRestrictionEnabled': - case 'SMTPSSLEnabled': - case 'RegisterEnabled': - value = inputs[key] === 'true' ? 'false' : 'true'; - break; - default: - break; - } - const res = await API.put('/api/option/', { - key, - value, - }); - const { success, message } = res.data; - if (success) { - if (key === 'EmailDomainWhitelist') { - value = value.split(','); - } - if (key === 'Price') { - value = parseFloat(value); - } - setInputs((inputs) => ({ - ...inputs, - [key]: value, - })); - } else { - showError(message); - } - setLoading(false); - }; - - const handleInputChange = async (e, { name, value }) => { - if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') { - // block disabling password login - setShowPasswordWarningModal(true); - return; - } - if ( - name === 'Notice' || - (name.startsWith('SMTP') && name !== 'SMTPSSLEnabled') || - name === 'ServerAddress' || - name === 'WorkerUrl' || - name === 'WorkerValidKey' || - name === 'EpayId' || - name === 'EpayKey' || - name === 'Price' || - name === 'PayAddress' || - name === 'GitHubClientId' || - name === 'GitHubClientSecret' || - name === 'WeChatServerAddress' || - name === 'WeChatServerToken' || - name === 'WeChatAccountQRCodeImageURL' || - name === 'TurnstileSiteKey' || - name === 'TurnstileSecretKey' || - name === 'EmailDomainWhitelist' || - name === 'TopupGroupRatio' || - name === 'TelegramBotToken' || - name === 'TelegramBotName' - ) { - setInputs((inputs) => ({ ...inputs, [name]: value })); - } else { - await updateOption(name, value); - } - }; - - const submitServerAddress = async () => { - let ServerAddress = removeTrailingSlash(inputs.ServerAddress); - await updateOption('ServerAddress', ServerAddress); - }; - - const submitWorker = async () => { - let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl); - await updateOption('WorkerUrl', WorkerUrl); - if (inputs.WorkerValidKey !== '') { - await updateOption('WorkerValidKey', inputs.WorkerValidKey); - } - } - - const submitPayAddress = async () => { - if (inputs.ServerAddress === '') { - showError('请先填写服务器地址'); - return; - } - if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { - if (!verifyJSON(inputs.TopupGroupRatio)) { - showError('充值分组倍率不是合法的 JSON 字符串'); - return; - } - await updateOption('TopupGroupRatio', inputs.TopupGroupRatio); - } - let PayAddress = removeTrailingSlash(inputs.PayAddress); - await updateOption('PayAddress', PayAddress); - if (inputs.EpayId !== '') { - await updateOption('EpayId', inputs.EpayId); - } - if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') { - await updateOption('EpayKey', inputs.EpayKey); - } - await updateOption('Price', '' + inputs.Price); - }; - - const submitSMTP = async () => { - if (originInputs['SMTPServer'] !== inputs.SMTPServer) { - await updateOption('SMTPServer', inputs.SMTPServer); - } - if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) { - await updateOption('SMTPAccount', inputs.SMTPAccount); - } - if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) { - await updateOption('SMTPFrom', inputs.SMTPFrom); - } - if ( - originInputs['SMTPPort'] !== inputs.SMTPPort && - inputs.SMTPPort !== '' - ) { - await updateOption('SMTPPort', inputs.SMTPPort); - } - if ( - originInputs['SMTPToken'] !== inputs.SMTPToken && - inputs.SMTPToken !== '' - ) { - await updateOption('SMTPToken', inputs.SMTPToken); - } - }; - - const submitEmailDomainWhitelist = async () => { - if ( - originInputs['EmailDomainWhitelist'] !== - inputs.EmailDomainWhitelist.join(',') && - inputs.SMTPToken !== '' - ) { - await updateOption( - 'EmailDomainWhitelist', - inputs.EmailDomainWhitelist.join(','), - ); - } - }; - - const submitWeChat = async () => { - if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { - await updateOption( - 'WeChatServerAddress', - removeTrailingSlash(inputs.WeChatServerAddress), - ); - } - if ( - originInputs['WeChatAccountQRCodeImageURL'] !== - inputs.WeChatAccountQRCodeImageURL - ) { - await updateOption( - 'WeChatAccountQRCodeImageURL', - inputs.WeChatAccountQRCodeImageURL, - ); - } - if ( - originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && - inputs.WeChatServerToken !== '' - ) { - await updateOption('WeChatServerToken', inputs.WeChatServerToken); - } - }; - - const submitGitHubOAuth = async () => { - if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { - await updateOption('GitHubClientId', inputs.GitHubClientId); - } - if ( - originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && - inputs.GitHubClientSecret !== '' - ) { - await updateOption('GitHubClientSecret', inputs.GitHubClientSecret); - } - }; - - const submitTelegramSettings = async () => { - // await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled); - await updateOption('TelegramBotToken', inputs.TelegramBotToken); - await updateOption('TelegramBotName', inputs.TelegramBotName); - }; - - const submitTurnstile = async () => { - if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { - await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); - } - if ( - originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && - inputs.TurnstileSecretKey !== '' - ) { - await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey); - } - }; - - const submitNewRestrictedDomain = () => { - const localDomainList = inputs.EmailDomainWhitelist; - if ( - restrictedDomainInput !== '' && - !localDomainList.includes(restrictedDomainInput) - ) { - setRestrictedDomainInput(''); - setInputs({ - ...inputs, - EmailDomainWhitelist: [...localDomainList, restrictedDomainInput], - }); - setEmailDomainWhitelist([ - ...EmailDomainWhitelist, - { - key: restrictedDomainInput, - text: restrictedDomainInput, - value: restrictedDomainInput, - }, - ]); - } - }; - - return ( - - - -
- 通用设置 -
- - - - - 更新服务器地址 - -
- 代理设置(支持 new-api-worker) -
- - - - - - 更新Worker设置 - - -
- 支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!) -
- - - - - - - - - - - - - - 更新支付设置 - -
- 配置登录注册 -
- - - {showPasswordWarningModal && ( - setShowPasswordWarningModal(false)} - size={'tiny'} - style={{ maxWidth: '450px' }} - > - 警告 - -

- 取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消? -

-
- - - - -
- )} - - - - - -
- - - - - -
- 配置邮箱域名白名单 - - 用以防止恶意用户利用临时邮箱批量注册 - -
- - - - - - - - - { - submitNewRestrictedDomain(); - }} - > - 填入 - - } - onKeyDown={(e) => { - if (e.key === 'Enter') { - submitNewRestrictedDomain(); - } - }} - autoComplete='new-password' - placeholder='输入新的允许的邮箱域名' - value={restrictedDomainInput} - onChange={(e, { value }) => { - setRestrictedDomainInput(value); - }} - /> - - - 保存邮箱域名白名单设置 - - -
- 配置 SMTP - 用以支持系统的邮件发送 -
- - - - - - - - - - - - - 保存 SMTP 设置 - -
- 配置 GitHub OAuth App - - 用以支持通过 GitHub 进行登录注册, - - 点击此处 - - 管理你的 GitHub OAuth App - -
- - Homepage URL 填 {inputs.ServerAddress} - ,Authorization callback URL 填{' '} - {`${inputs.ServerAddress}/oauth/github`} - - - - - - - 保存 GitHub OAuth 设置 - - -
- 配置 WeChat Server - - 用以支持通过微信进行登录注册, - - 点击此处 - - 了解 WeChat Server - -
- - - - - - - 保存 WeChat Server 设置 - - -
- 配置 Telegram 登录 -
- - - - - - 保存 Telegram 登录设置 - - -
- 配置 Turnstile - - 用以支持用户校验, - - 点击此处 - - 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type - -
- - - - - - 保存 Turnstile 设置 - - -
-
- ); -}; - -export default SystemSetting; diff --git a/web/src/components/SetupCheck.js b/web/src/components/SetupCheck.js new file mode 100644 index 00000000..99364b00 --- /dev/null +++ b/web/src/components/SetupCheck.js @@ -0,0 +1,18 @@ +import React, { useContext, useEffect } from 'react'; +import { Navigate, useLocation } from 'react-router-dom'; +import { StatusContext } from '../context/Status'; + +const SetupCheck = ({ children }) => { + const [statusState] = useContext(StatusContext); + const location = useLocation(); + + useEffect(() => { + if (statusState?.status?.setup === false && location.pathname !== '/setup') { + window.location.href = '/setup'; + } + }, [statusState?.status?.setup, location.pathname]); + + return children; +}; + +export default SetupCheck; \ No newline at end of file diff --git a/web/src/components/SiderBar.js b/web/src/components/SiderBar.js index f2a27c8f..25b350d1 100644 --- a/web/src/components/SiderBar.js +++ b/web/src/components/SiderBar.js @@ -1,5 +1,5 @@ import React, { useContext, useEffect, useMemo, useState } from 'react'; -import { Link, useNavigate } from 'react-router-dom'; +import { Link, useNavigate, useLocation } from 'react-router-dom'; import { UserContext } from '../context/User'; import { StatusContext } from '../context/Status'; import { useTranslation } from 'react-i18next'; @@ -15,10 +15,13 @@ import { import '../index.css'; import { - IconCalendarClock, IconChecklistStroked, - IconComment, IconCommentStroked, + IconCalendarClock, + IconChecklistStroked, + IconComment, + IconCommentStroked, IconCreditCard, - IconGift, IconHelpCircle, + IconGift, + IconHelpCircle, IconHistogram, IconHome, IconImage, @@ -26,15 +29,69 @@ import { IconLayers, IconPriceTag, IconSetting, - IconUser + IconUser, } from '@douyinfe/semi-icons'; -import { Avatar, Dropdown, Layout, Nav, Switch, Divider } from '@douyinfe/semi-ui'; +import { + Avatar, + Dropdown, + Layout, + Nav, + Switch, + Divider, +} from '@douyinfe/semi-ui'; import { setStatusData } from '../helpers/data.js'; import { stringToColor } from '../helpers/render.js'; import { useSetTheme, useTheme } from '../context/Theme/index.js'; import { StyleContext } from '../context/Style/index.js'; +import Text from '@douyinfe/semi-ui/lib/es/typography/text'; -// HeaderBar Buttons +// 自定义侧边栏按钮样式 +const navItemStyle = { + borderRadius: '6px', + margin: '4px 8px', +}; + +// 自定义侧边栏按钮悬停样式 +const navItemHoverStyle = { + backgroundColor: 'var(--semi-color-primary-light-default)', + color: 'var(--semi-color-primary)', +}; + +// 自定义侧边栏按钮选中样式 +const navItemSelectedStyle = { + backgroundColor: 'var(--semi-color-primary-light-default)', + color: 'var(--semi-color-primary)', + fontWeight: '600', +}; + +// 自定义图标样式 +const iconStyle = (itemKey, selectedKeys) => { + return { + fontSize: '18px', + color: selectedKeys.includes(itemKey) + ? 'var(--semi-color-primary)' + : 'var(--semi-color-text-2)', + }; +}; + +// Define routerMap as a constant outside the component +const routerMap = { + home: '/', + channel: '/channel', + token: '/token', + redemption: '/redemption', + topup: '/topup', + user: '/user', + log: '/log', + midjourney: '/midjourney', + setting: '/setting', + about: '/about', + detail: '/detail', + pricing: '/pricing', + task: '/task', + playground: '/playground', + personal: '/personal', +}; const SiderBar = () => { const { t } = useTranslation(); @@ -46,27 +103,47 @@ const SiderBar = () => { const [selectedKeys, setSelectedKeys] = useState(['home']); const [isCollapsed, setIsCollapsed] = useState(defaultIsCollapsed); const [chatItems, setChatItems] = useState([]); + const [openedKeys, setOpenedKeys] = useState([]); const theme = useTheme(); const setTheme = useSetTheme(); + const location = useLocation(); + const [routerMapState, setRouterMapState] = useState(routerMap); - const routerMap = { - home: '/', - channel: '/channel', - token: '/token', - redemption: '/redemption', - topup: '/topup', - user: '/user', - log: '/log', - midjourney: '/midjourney', - setting: '/setting', - about: '/about', - chat: '/chat', - detail: '/detail', - pricing: '/pricing', - task: '/task', - playground: '/playground', - personal: '/personal', - }; + // 预先计算所有可能的图标样式 + const allItemKeys = useMemo(() => { + const keys = [ + 'home', + 'channel', + 'token', + 'redemption', + 'topup', + 'user', + 'log', + 'midjourney', + 'setting', + 'about', + 'chat', + 'detail', + 'pricing', + 'task', + 'playground', + 'personal', + ]; + // 添加聊天项的keys + for (let i = 0; i < chatItems.length; i++) { + keys.push('chat' + i); + } + return keys; + }, [chatItems]); + + // 使用useMemo一次性计算所有图标样式 + const iconStyles = useMemo(() => { + const styles = {}; + allItemKeys.forEach((key) => { + styles[key] = iconStyle(key, selectedKeys); + }); + return styles; + }, [allItemKeys, selectedKeys]); const workspaceItems = useMemo( () => [ @@ -108,10 +185,8 @@ const SiderBar = () => { to: '/task', icon: , className: - localStorage.getItem('enable_task') === 'true' - ? '' - : 'tableHiddle', - } + localStorage.getItem('enable_task') === 'true' ? '' : 'tableHiddle', + }, ], [ localStorage.getItem('enable_data_export'), @@ -189,16 +264,24 @@ const SiderBar = () => { [chatItems, t], ); - useEffect(() => { - let localKey = window.location.pathname.split('/')[1]; - if (localKey === '') { - localKey = 'home'; - } - setSelectedKeys([localKey]); + // Function to update router map with chat routes + const updateRouterMapWithChats = (chats) => { + const newRouterMap = { ...routerMap }; + if (Array.isArray(chats) && chats.length > 0) { + for (let i = 0; i < chats.length; i++) { + newRouterMap['chat' + i] = '/chat/' + i; + } + } + + setRouterMapState(newRouterMap); + return newRouterMap; + }; + + // Update the useEffect for chat items + useEffect(() => { let chats = localStorage.getItem('chats'); if (chats) { - // console.log(chats); try { chats = JSON.parse(chats); if (Array.isArray(chats)) { @@ -210,19 +293,46 @@ const SiderBar = () => { chat.itemKey = 'chat' + i; chat.to = '/chat/' + i; } - // setRouterMap({ ...routerMap, chat: '/chat/' + i }) chatItems.push(chat); } setChatItems(chatItems); + + // Update router map with chat routes + updateRouterMapWithChats(chats); } } catch (e) { console.error(e); - showError('聊天数据解析失败') + showError('聊天数据解析失败'); + } + } + }, []); + + // Update the useEffect for route selection + useEffect(() => { + const currentPath = location.pathname; + let matchingKey = Object.keys(routerMapState).find( + (key) => routerMapState[key] === currentPath, + ); + + // Handle chat routes + if (!matchingKey && currentPath.startsWith('/chat/')) { + const chatIndex = currentPath.split('/').pop(); + if (!isNaN(chatIndex)) { + matchingKey = 'chat' + chatIndex; + } else { + matchingKey = 'chat'; } } - setIsCollapsed(localStorage.getItem('default_collapse_sidebar') === 'true'); - }, []); + // If we found a matching key, update the selected keys + if (matchingKey) { + setSelectedKeys([matchingKey]); + } + }, [location.pathname, routerMapState]); + + useEffect(() => { + setIsCollapsed(styleState.siderCollapsed); + }, [styleState.siderCollapsed]); // Custom divider style const dividerStyle = { @@ -235,44 +345,62 @@ const SiderBar = () => { padding: '8px 16px', color: 'var(--semi-color-text-2)', fontSize: '12px', - fontWeight: 'normal', + fontWeight: 'bold', + textTransform: 'uppercase', + letterSpacing: '0.5px', }; return ( <> diff --git a/web/src/components/SystemSetting.js b/web/src/components/SystemSetting.js index 3149f91e..96cd0386 100644 --- a/web/src/components/SystemSetting.js +++ b/web/src/components/SystemSetting.js @@ -1,16 +1,24 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useRef } from 'react'; import { Button, - Divider, Form, - Grid, - Header, - Message, + Row, + Col, + Typography, Modal, -} from 'semantic-ui-react'; -import { API, removeTrailingSlash, showError, verifyJSON } from '../helpers'; - -import { useTheme } from '../context/Theme'; + Banner, + TagInput, + Spin, + Card, +} from '@douyinfe/semi-ui'; +const { Text } = Typography; +import { + removeTrailingSlash, + showError, + showSuccess, + verifyJSON, +} from '../helpers/utils'; +import { API } from '../helpers/api'; const SystemSetting = () => { let [inputs, setInputs] = useState({ @@ -20,6 +28,13 @@ const SystemSetting = () => { GitHubOAuthEnabled: '', GitHubClientId: '', GitHubClientSecret: '', + 'oidc.enabled': '', + 'oidc.client_id': '', + 'oidc.client_secret': '', + 'oidc.well_known': '', + 'oidc.authorization_endpoint': '', + 'oidc.token_endpoint': '', + 'oidc.user_info_endpoint': '', Notice: '', SMTPServer: '', SMTPPort: '', @@ -57,137 +72,144 @@ const SystemSetting = () => { LinuxDOClientId: '', LinuxDOClientSecret: '', }); - const [originInputs, setOriginInputs] = useState({}); - let [loading, setLoading] = useState(false); - const [EmailDomainWhitelist, setEmailDomainWhitelist] = useState([]); - const [restrictedDomainInput, setRestrictedDomainInput] = useState(''); - const [showPasswordWarningModal, setShowPasswordWarningModal] = - useState(false); - const theme = useTheme(); - const isDark = theme === 'dark'; + const [originInputs, setOriginInputs] = useState({}); + const [loading, setLoading] = useState(false); + const [isLoaded, setIsLoaded] = useState(false); + const formApiRef = useRef(null); + const [emailDomainWhitelist, setEmailDomainWhitelist] = useState([]); + const [showPasswordLoginConfirmModal, setShowPasswordLoginConfirmModal] = + useState(false); + const [linuxDOOAuthEnabled, setLinuxDOOAuthEnabled] = useState(false); + const [emailToAdd, setEmailToAdd] = useState(''); const getOptions = async () => { + setLoading(true); const res = await API.get('/api/option/'); const { success, message, data } = res.data; if (success) { let newInputs = {}; data.forEach((item) => { - if (item.key === 'TopupGroupRatio') { - item.value = JSON.stringify(JSON.parse(item.value), null, 2); + switch (item.key) { + case 'TopupGroupRatio': + item.value = JSON.stringify(JSON.parse(item.value), null, 2); + break; + case 'EmailDomainWhitelist': + setEmailDomainWhitelist(item.value ? item.value.split(',') : []); + break; + case 'PasswordLoginEnabled': + case 'PasswordRegisterEnabled': + case 'EmailVerificationEnabled': + case 'GitHubOAuthEnabled': + case 'WeChatAuthEnabled': + case 'TelegramOAuthEnabled': + case 'RegisterEnabled': + case 'TurnstileCheckEnabled': + case 'EmailDomainRestrictionEnabled': + case 'EmailAliasRestrictionEnabled': + case 'SMTPSSLEnabled': + case 'LinuxDOOAuthEnabled': + case 'oidc.enabled': + item.value = item.value === 'true'; + break; + case 'Price': + case 'MinTopUp': + item.value = parseFloat(item.value); + break; + default: + break; } newInputs[item.key] = item.value; }); - setInputs({ - ...newInputs, - EmailDomainWhitelist: newInputs.EmailDomainWhitelist.split(','), - }); + setInputs(newInputs); setOriginInputs(newInputs); - - setEmailDomainWhitelist( - newInputs.EmailDomainWhitelist.split(',').map((item) => { - return { key: item, text: item, value: item }; - }), - ); - } else { - showError(message); - } - }; - - useEffect(() => { - getOptions().then(); - }, []); - useEffect(() => {}, [inputs.EmailDomainWhitelist]); - - const updateOption = async (key, value) => { - setLoading(true); - switch (key) { - case 'PasswordLoginEnabled': - case 'PasswordRegisterEnabled': - case 'EmailVerificationEnabled': - case 'GitHubOAuthEnabled': - case 'LinuxDOOAuthEnabled': - case 'WeChatAuthEnabled': - case 'TelegramOAuthEnabled': - case 'TurnstileCheckEnabled': - case 'EmailDomainRestrictionEnabled': - case 'EmailAliasRestrictionEnabled': - case 'SMTPSSLEnabled': - case 'RegisterEnabled': - value = inputs[key] === 'true' ? 'false' : 'true'; - break; - default: - break; - } - const res = await API.put('/api/option/', { - key, - value, - }); - const { success, message } = res.data; - if (success) { - if (key === 'EmailDomainWhitelist') { - value = value.split(','); + if (formApiRef.current) { + formApiRef.current.setValues(newInputs); } - if (key === 'Price') { - value = parseFloat(value); - } - setInputs((inputs) => ({ - ...inputs, - [key]: value, - })); + setIsLoaded(true); } else { showError(message); } setLoading(false); }; - const handleInputChange = async (e, { name, value }) => { - if (name === 'PasswordLoginEnabled' && inputs[name] === 'true') { - // block disabling password login - setShowPasswordWarningModal(true); - return; - } - if ( - name === 'Notice' || - (name.startsWith('SMTP') && name !== 'SMTPSSLEnabled') || - name === 'ServerAddress' || - name === 'WorkerUrl' || - name === 'WorkerValidKey' || - name === 'EpayId' || - name === 'EpayKey' || - name === 'Price' || - name === 'PayAddress' || - name === 'GitHubClientId' || - name === 'GitHubClientSecret' || - name === 'WeChatServerAddress' || - name === 'WeChatServerToken' || - name === 'WeChatAccountQRCodeImageURL' || - name === 'TurnstileSiteKey' || - name === 'TurnstileSecretKey' || - name === 'EmailDomainWhitelist' || - name === 'TopupGroupRatio' || - name === 'TelegramBotToken' || - name === 'TelegramBotName' || - name === 'LinuxDOClientId' || - name === 'LinuxDOClientSecret' - ) { - setInputs((inputs) => ({ ...inputs, [name]: value })); - } else { - await updateOption(name, value); + useEffect(() => { + getOptions(); + }, []); + + const updateOptions = async (options) => { + setLoading(true); + try { + // 分离 checkbox 类型的选项和其他选项 + const checkboxOptions = options.filter((opt) => + opt.key.toLowerCase().endsWith('enabled'), + ); + const otherOptions = options.filter( + (opt) => !opt.key.toLowerCase().endsWith('enabled'), + ); + + // 处理 checkbox 类型的选项 + for (const opt of checkboxOptions) { + const res = await API.put('/api/option/', { + key: opt.key, + value: opt.value.toString(), + }); + if (!res.data.success) { + showError(res.data.message); + return; + } + } + + // 处理其他选项 + if (otherOptions.length > 0) { + const requestQueue = otherOptions.map((opt) => + API.put('/api/option/', { + key: opt.key, + value: + typeof opt.value === 'boolean' ? opt.value.toString() : opt.value, + }), + ); + + const results = await Promise.all(requestQueue); + + // 检查所有请求是否成功 + const errorResults = results.filter((res) => !res.data.success); + errorResults.forEach((res) => { + showError(res.data.message); + }); + } + + showSuccess('更新成功'); + // 更新本地状态 + const newInputs = { ...inputs }; + options.forEach((opt) => { + newInputs[opt.key] = opt.value; + }); + setInputs(newInputs); + } catch (error) { + showError('更新失败'); } + setLoading(false); + }; + + const handleFormChange = (values) => { + setInputs(values); }; const submitServerAddress = async () => { let ServerAddress = removeTrailingSlash(inputs.ServerAddress); - await updateOption('ServerAddress', ServerAddress); + await updateOptions([{ key: 'ServerAddress', value: ServerAddress }]); }; const submitWorker = async () => { let WorkerUrl = removeTrailingSlash(inputs.WorkerUrl); - await updateOption('WorkerUrl', WorkerUrl); - if (inputs.WorkerValidKey !== '') { - await updateOption('WorkerValidKey', inputs.WorkerValidKey); + const options = [ + { key: 'WorkerUrl', value: WorkerUrl }, + ] + if (inputs.WorkerValidKey !== '' || WorkerUrl === '') { + options.push({ key: 'WorkerValidKey', value: inputs.WorkerValidKey }); } + await updateOptions(options); }; const submitPayAddress = async () => { @@ -200,673 +222,907 @@ const SystemSetting = () => { showError('充值分组倍率不是合法的 JSON 字符串'); return; } - await updateOption('TopupGroupRatio', inputs.TopupGroupRatio); } - let PayAddress = removeTrailingSlash(inputs.PayAddress); - await updateOption('PayAddress', PayAddress); + + const options = [ + { key: 'PayAddress', value: removeTrailingSlash(inputs.PayAddress) }, + ]; + if (inputs.EpayId !== '') { - await updateOption('EpayId', inputs.EpayId); + options.push({ key: 'EpayId', value: inputs.EpayId }); } if (inputs.EpayKey !== undefined && inputs.EpayKey !== '') { - await updateOption('EpayKey', inputs.EpayKey); + options.push({ key: 'EpayKey', value: inputs.EpayKey }); } - await updateOption('Price', '' + inputs.Price); + if (inputs.Price !== '') { + options.push({ key: 'Price', value: inputs.Price.toString() }); + } + if (inputs.MinTopUp !== '') { + options.push({ key: 'MinTopUp', value: inputs.MinTopUp.toString() }); + } + if (inputs.CustomCallbackAddress !== '') { + options.push({ + key: 'CustomCallbackAddress', + value: inputs.CustomCallbackAddress, + }); + } + if (originInputs['TopupGroupRatio'] !== inputs.TopupGroupRatio) { + options.push({ key: 'TopupGroupRatio', value: inputs.TopupGroupRatio }); + } + + await updateOptions(options); }; const submitSMTP = async () => { + const options = []; + if (originInputs['SMTPServer'] !== inputs.SMTPServer) { - await updateOption('SMTPServer', inputs.SMTPServer); + options.push({ key: 'SMTPServer', value: inputs.SMTPServer }); } if (originInputs['SMTPAccount'] !== inputs.SMTPAccount) { - await updateOption('SMTPAccount', inputs.SMTPAccount); + options.push({ key: 'SMTPAccount', value: inputs.SMTPAccount }); } if (originInputs['SMTPFrom'] !== inputs.SMTPFrom) { - await updateOption('SMTPFrom', inputs.SMTPFrom); + options.push({ key: 'SMTPFrom', value: inputs.SMTPFrom }); } if ( originInputs['SMTPPort'] !== inputs.SMTPPort && inputs.SMTPPort !== '' ) { - await updateOption('SMTPPort', inputs.SMTPPort); + options.push({ key: 'SMTPPort', value: inputs.SMTPPort }); } if ( originInputs['SMTPToken'] !== inputs.SMTPToken && inputs.SMTPToken !== '' ) { - await updateOption('SMTPToken', inputs.SMTPToken); + options.push({ key: 'SMTPToken', value: inputs.SMTPToken }); + } + + if (options.length > 0) { + await updateOptions(options); } }; const submitEmailDomainWhitelist = async () => { - if ( - originInputs['EmailDomainWhitelist'] !== - inputs.EmailDomainWhitelist.join(',') && - inputs.SMTPToken !== '' - ) { - await updateOption( - 'EmailDomainWhitelist', - inputs.EmailDomainWhitelist.join(','), - ); + if (Array.isArray(emailDomainWhitelist)) { + await updateOptions([ + { + key: 'EmailDomainWhitelist', + value: emailDomainWhitelist.join(','), + }, + ]); + } else { + showError('邮箱域名白名单格式不正确'); + } + }; + + const handleAddEmail = () => { + if (emailToAdd && emailToAdd.trim() !== '') { + const domain = emailToAdd.trim(); + + // 验证域名格式 + const domainRegex = /^([a-zA-Z0-9]([a-zA-Z0-9\-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/; + if (!domainRegex.test(domain)) { + showError('邮箱域名格式不正确,请输入有效的域名,如 gmail.com'); + return; + } + + // 检查是否已存在 + if (emailDomainWhitelist.includes(domain)) { + showError('该域名已存在于白名单中'); + return; + } + + setEmailDomainWhitelist([...emailDomainWhitelist, domain]); + setEmailToAdd(''); + showSuccess('已添加到白名单'); } }; const submitWeChat = async () => { + const options = []; + if (originInputs['WeChatServerAddress'] !== inputs.WeChatServerAddress) { - await updateOption( - 'WeChatServerAddress', - removeTrailingSlash(inputs.WeChatServerAddress), - ); + options.push({ + key: 'WeChatServerAddress', + value: removeTrailingSlash(inputs.WeChatServerAddress), + }); } if ( originInputs['WeChatAccountQRCodeImageURL'] !== inputs.WeChatAccountQRCodeImageURL ) { - await updateOption( - 'WeChatAccountQRCodeImageURL', - inputs.WeChatAccountQRCodeImageURL, - ); + options.push({ + key: 'WeChatAccountQRCodeImageURL', + value: inputs.WeChatAccountQRCodeImageURL, + }); } if ( originInputs['WeChatServerToken'] !== inputs.WeChatServerToken && inputs.WeChatServerToken !== '' ) { - await updateOption('WeChatServerToken', inputs.WeChatServerToken); + options.push({ + key: 'WeChatServerToken', + value: inputs.WeChatServerToken, + }); + } + + if (options.length > 0) { + await updateOptions(options); } }; const submitGitHubOAuth = async () => { + const options = []; + if (originInputs['GitHubClientId'] !== inputs.GitHubClientId) { - await updateOption('GitHubClientId', inputs.GitHubClientId); + options.push({ key: 'GitHubClientId', value: inputs.GitHubClientId }); } if ( originInputs['GitHubClientSecret'] !== inputs.GitHubClientSecret && inputs.GitHubClientSecret !== '' ) { - await updateOption('GitHubClientSecret', inputs.GitHubClientSecret); + options.push({ + key: 'GitHubClientSecret', + value: inputs.GitHubClientSecret, + }); + } + + if (options.length > 0) { + await updateOptions(options); + } + }; + + const submitOIDCSettings = async () => { + if (inputs['oidc.well_known'] !== '') { + if ( + !inputs['oidc.well_known'].startsWith('http://') && + !inputs['oidc.well_known'].startsWith('https://') + ) { + showError('Well-Known URL 必须以 http:// 或 https:// 开头'); + return; + } + try { + const res = await API.get(inputs['oidc.well_known']); + inputs['oidc.authorization_endpoint'] = + res.data['authorization_endpoint']; + inputs['oidc.token_endpoint'] = res.data['token_endpoint']; + inputs['oidc.user_info_endpoint'] = res.data['userinfo_endpoint']; + showSuccess('获取 OIDC 配置成功!'); + } catch (err) { + console.error(err); + showError( + '获取 OIDC 配置失败,请检查网络状况和 Well-Known URL 是否正确', + ); + return; + } + } + + const options = []; + + if (originInputs['oidc.well_known'] !== inputs['oidc.well_known']) { + options.push({ + key: 'oidc.well_known', + value: inputs['oidc.well_known'], + }); + } + if (originInputs['oidc.client_id'] !== inputs['oidc.client_id']) { + options.push({ key: 'oidc.client_id', value: inputs['oidc.client_id'] }); + } + if ( + originInputs['oidc.client_secret'] !== inputs['oidc.client_secret'] && + inputs['oidc.client_secret'] !== '' + ) { + options.push({ + key: 'oidc.client_secret', + value: inputs['oidc.client_secret'], + }); + } + if ( + originInputs['oidc.authorization_endpoint'] !== + inputs['oidc.authorization_endpoint'] + ) { + options.push({ + key: 'oidc.authorization_endpoint', + value: inputs['oidc.authorization_endpoint'], + }); + } + if (originInputs['oidc.token_endpoint'] !== inputs['oidc.token_endpoint']) { + options.push({ + key: 'oidc.token_endpoint', + value: inputs['oidc.token_endpoint'], + }); + } + if ( + originInputs['oidc.user_info_endpoint'] !== + inputs['oidc.user_info_endpoint'] + ) { + options.push({ + key: 'oidc.user_info_endpoint', + value: inputs['oidc.user_info_endpoint'], + }); + } + + if (options.length > 0) { + await updateOptions(options); } }; const submitTelegramSettings = async () => { - // await updateOption('TelegramOAuthEnabled', inputs.TelegramOAuthEnabled); - await updateOption('TelegramBotToken', inputs.TelegramBotToken); - await updateOption('TelegramBotName', inputs.TelegramBotName); + const options = [ + { key: 'TelegramBotToken', value: inputs.TelegramBotToken }, + { key: 'TelegramBotName', value: inputs.TelegramBotName }, + ]; + await updateOptions(options); }; const submitTurnstile = async () => { + const options = []; + if (originInputs['TurnstileSiteKey'] !== inputs.TurnstileSiteKey) { - await updateOption('TurnstileSiteKey', inputs.TurnstileSiteKey); + options.push({ key: 'TurnstileSiteKey', value: inputs.TurnstileSiteKey }); } if ( originInputs['TurnstileSecretKey'] !== inputs.TurnstileSecretKey && inputs.TurnstileSecretKey !== '' ) { - await updateOption('TurnstileSecretKey', inputs.TurnstileSecretKey); - } - }; - - const submitNewRestrictedDomain = () => { - const localDomainList = inputs.EmailDomainWhitelist; - if ( - restrictedDomainInput !== '' && - !localDomainList.includes(restrictedDomainInput) - ) { - setRestrictedDomainInput(''); - setInputs({ - ...inputs, - EmailDomainWhitelist: [...localDomainList, restrictedDomainInput], + options.push({ + key: 'TurnstileSecretKey', + value: inputs.TurnstileSecretKey, }); - setEmailDomainWhitelist([ - ...EmailDomainWhitelist, - { - key: restrictedDomainInput, - text: restrictedDomainInput, - value: restrictedDomainInput, - }, - ]); + } + + if (options.length > 0) { + await updateOptions(options); } }; const submitLinuxDOOAuth = async () => { + const options = []; + if (originInputs['LinuxDOClientId'] !== inputs.LinuxDOClientId) { - await updateOption('LinuxDOClientId', inputs.LinuxDOClientId); + options.push({ key: 'LinuxDOClientId', value: inputs.LinuxDOClientId }); } if ( originInputs['LinuxDOClientSecret'] !== inputs.LinuxDOClientSecret && inputs.LinuxDOClientSecret !== '' ) { - await updateOption('LinuxDOClientSecret', inputs.LinuxDOClientSecret); + options.push({ + key: 'LinuxDOClientSecret', + value: inputs.LinuxDOClientSecret, + }); + } + + if (options.length > 0) { + await updateOptions(options); } }; + const handleCheckboxChange = async (optionKey, event) => { + const value = event.target.checked; + + if (optionKey === 'PasswordLoginEnabled' && !value) { + setShowPasswordLoginConfirmModal(true); + } else { + await updateOptions([{ key: optionKey, value }]); + } + if (optionKey === 'LinuxDOOAuthEnabled') { + setLinuxDOOAuthEnabled(value); + } + }; + + const handlePasswordLoginConfirm = async () => { + await updateOptions([{ key: 'PasswordLoginEnabled', value: false }]); + setShowPasswordLoginConfirmModal(false); + }; + return ( - - -
-
- 通用设置 -
- - - - - 更新服务器地址 - -
- 代理设置(支持{' '} - + {isLoaded ? ( + (formApiRef.current = api)} + > + {({ formState, values, formApi }) => ( +
- - 注意:代理功能仅对图片请求和 Webhook 请求生效,不会影响其他 API 请求。如需配置 API 请求代理,请参考 - - {' '}API 代理设置文档 - - 。 - - - - - - 更新Worker设置 - -
- 支付设置(当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!) -
- - - - - - - - - - - - - - 更新支付设置 - -
- 配置登录注册 -
- - - {showPasswordWarningModal && ( - setShowPasswordWarningModal(false)} - size={'tiny'} - style={{ maxWidth: '450px' }} - > - 警告 - -

- 取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消? -

-
- - - + + + + + + (支持{' '} + + new-api-worker + + ) + + - 确定 +
+ + + + + + + + + + + + + + (当前仅支持易支付接口,默认使用上方服务器地址作为回调地址!) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + handleCheckboxChange('PasswordLoginEnabled', e) + } + > + 允许通过密码进行登录 + + + handleCheckboxChange('PasswordRegisterEnabled', e) + } + > + 允许通过密码进行注册 + + + handleCheckboxChange('EmailVerificationEnabled', e) + } + > + 通过密码注册时需要进行邮箱验证 + + + handleCheckboxChange('RegisterEnabled', e) + } + > + 允许新用户注册 + + + handleCheckboxChange('TurnstileCheckEnabled', e) + } + > + 启用 Turnstile 用户校验 + + + + + handleCheckboxChange('GitHubOAuthEnabled', e) + } + > + 允许通过 GitHub 账户登录 & 注册 + + + handleCheckboxChange('LinuxDOOAuthEnabled', e) + } + > + 允许通过 Linux DO 账户登录 & 注册 + + + handleCheckboxChange('WeChatAuthEnabled', e) + } + > + 允许通过微信登录 & 注册 + + + handleCheckboxChange('TelegramOAuthEnabled', e) + } + > + 允许通过 Telegram 进行登录 + + + handleCheckboxChange('oidc.enabled', e) + } + > + 允许通过 OIDC 进行登录 + + + + + + + + + 用以防止恶意用户利用临时邮箱批量注册 + + + + handleCheckboxChange( + 'EmailDomainRestrictionEnabled', + e, + ) + } + > + 启用邮箱域名白名单 + + + + + handleCheckboxChange( + 'EmailAliasRestrictionEnabled', + e, + ) + } + > + 启用邮箱别名限制 + + + + + setEmailToAdd(value)} + style={{ marginTop: 16 }} + suffix={ + + } + onEnterPress={handleAddEmail} + /> + - + + + + + 用以支持系统的邮件发送 + + + + + + + + + + + + + + + + + + + + + handleCheckboxChange('SMTPSSLEnabled', e) + } + > + 启用SMTP SSL + + + + + + + + + + 用以支持通过 OIDC 登录,例如 Okta、Auth0 等兼容 OIDC 协议的 + IdP + + + + 若你的 OIDC Provider 支持 Discovery Endpoint,你可以仅填写 + OIDC Well-Known URL,系统会自动获取 OIDC 配置 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 用以支持通过 GitHub 进行登录注册 + + + + + + + + + + + + + + + + 用以支持通过 Linux DO 进行登录注册 + + 点击此处 + + 管理你的 LinuxDO OAuth App + + + + + + + + + + + + + + + + + 用以支持通过微信进行登录注册 + + + + + + + + + + + + + + + + + + 用以支持通过 Telegram 进行登录注册 + + + + + + + + + + + + + + + 用以支持用户校验 + + + + + + + + + + + + + { + setShowPasswordLoginConfirmModal(false); + formApiRef.current.setValue('PasswordLoginEnabled', true); + }} + okText='确认' + cancelText='取消' + > +

您确定要取消密码登录功能吗?这可能会影响用户的登录方式。

- )} - - - - - - - - - - - - -
- 配置邮箱域名白名单 - - 用以防止恶意用户利用临时邮箱批量注册 - -
- - - - - - - - - { - submitNewRestrictedDomain(); - }} - > - 填入 - - } - onKeyDown={(e) => { - if (e.key === 'Enter') { - submitNewRestrictedDomain(); - } - }} - autoComplete='new-password' - placeholder='输入新的允许的邮箱域名' - value={restrictedDomainInput} - onChange={(e, { value }) => { - setRestrictedDomainInput(value); - }} - /> - - - 保存邮箱域名白名单设置 - - -
- 配置 SMTP - 用以支持系统的邮件发送 -
- - - - - - - - - - - - - 保存 SMTP 设置 - -
- 配置 GitHub OAuth App - - 用以支持通过 GitHub 进行登录注册, - - 点击此处 - - 管理你的 GitHub OAuth App - -
- - Homepage URL 填 {inputs.ServerAddress} - ,Authorization callback URL 填{' '} - {`${inputs.ServerAddress}/oauth/github`} - - - - - - - 保存 GitHub OAuth 设置 - - -
- 配置 WeChat Server - - 用以支持通过微信进行登录注册, - - 点击此处 - - 了解 WeChat Server - -
- - - - - - - 保存 WeChat Server 设置 - - -
- 配置 Telegram 登录 -
- - - - - - 保存 Telegram 登录设置 - - -
- 配置 Turnstile - - 用以支持用户校验, - - 点击此处 - - 管理你的 Turnstile Sites,推荐选择 Invisible Widget Type - -
- - - - - - 保存 Turnstile 设置 - - -
- 配置 LinuxDO OAuth App - - 用以支持通过 LinuxDO 进行登录注册, - - 点击此处 - - 管理你的 LinuxDO OAuth App - -
- - Homepage URL 填 {inputs.ServerAddress} - ,Authorization callback URL 填{' '} - {`${inputs.ServerAddress}/oauth/linuxdo`} - - - - - - - 保存 LinuxDO OAuth 设置 - + + )} - - + ) : ( +
+ +
+ )} + ); }; diff --git a/web/src/components/TaskLogsTable.js b/web/src/components/TaskLogsTable.js index 52bf39bb..4d243133 100644 --- a/web/src/components/TaskLogsTable.js +++ b/web/src/components/TaskLogsTable.js @@ -1,400 +1,512 @@ import React, { useEffect, useState } from 'react'; import { Label } from 'semantic-ui-react'; -import { API, copy, isAdmin, showError, showSuccess, timestamp2string } from '../helpers'; +import { + API, + copy, + isAdmin, + showError, + showSuccess, + timestamp2string, +} from '../helpers'; import { - Table, - Tag, - Form, - Button, - Layout, - Modal, - Typography, Progress, Card + Table, + Tag, + Form, + Button, + Layout, + Modal, + Typography, + Progress, + Card, } from '@douyinfe/semi-ui'; import { ITEMS_PER_PAGE } from '../constants'; -const colors = ['amber', 'blue', 'cyan', 'green', 'grey', 'indigo', - 'light-blue', 'lime', 'orange', 'pink', - 'purple', 'red', 'teal', 'violet', 'yellow' -] - +const colors = [ + 'amber', + 'blue', + 'cyan', + 'green', + 'grey', + 'indigo', + 'light-blue', + 'lime', + 'orange', + 'pink', + 'purple', + 'red', + 'teal', + 'violet', + 'yellow', +]; const renderTimestamp = (timestampInSeconds) => { - const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒 + const date = new Date(timestampInSeconds * 1000); // 从秒转换为毫秒 - const year = date.getFullYear(); // 获取年份 - const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数 - const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数 - const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数 - const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数 - const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数 + const year = date.getFullYear(); // 获取年份 + const month = ('0' + (date.getMonth() + 1)).slice(-2); // 获取月份,从0开始需要+1,并保证两位数 + const day = ('0' + date.getDate()).slice(-2); // 获取日期,并保证两位数 + const hours = ('0' + date.getHours()).slice(-2); // 获取小时,并保证两位数 + const minutes = ('0' + date.getMinutes()).slice(-2); // 获取分钟,并保证两位数 + const seconds = ('0' + date.getSeconds()).slice(-2); // 获取秒钟,并保证两位数 - return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出 + return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`; // 格式化输出 }; function renderDuration(submit_time, finishTime) { - // 确保startTime和finishTime都是有效的时间戳 - if (!submit_time || !finishTime) return 'N/A'; + // 确保startTime和finishTime都是有效的时间戳 + if (!submit_time || !finishTime) return 'N/A'; - // 将时间戳转换为Date对象 - const start = new Date(submit_time); - const finish = new Date(finishTime); + // 将时间戳转换为Date对象 + const start = new Date(submit_time); + const finish = new Date(finishTime); - // 计算时间差(毫秒) - const durationMs = finish - start; + // 计算时间差(毫秒) + const durationMs = finish - start; - // 将时间差转换为秒,并保留一位小数 - const durationSec = (durationMs / 1000).toFixed(1); + // 将时间差转换为秒,并保留一位小数 + const durationSec = (durationMs / 1000).toFixed(1); - // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色 - const color = durationSec > 60 ? 'red' : 'green'; + // 设置颜色:大于60秒则为红色,小于等于60秒则为绿色 + const color = durationSec > 60 ? 'red' : 'green'; - // 返回带有样式的颜色标签 - return ( - - {durationSec} 秒 - - ); + // 返回带有样式的颜色标签 + return ( + + {durationSec} 秒 + + ); } const LogsTable = () => { - const [isModalOpen, setIsModalOpen] = useState(false); - const [modalContent, setModalContent] = useState(''); - const isAdminUser = isAdmin(); - const columns = [ - { - title: "提交时间", - dataIndex: 'submit_time', - render: (text, record, index) => { - return ( -
- {text ? renderTimestamp(text) : "-"} -
- ); - }, - }, - { - title: "结束时间", - dataIndex: 'finish_time', - render: (text, record, index) => { - return ( -
- {text ? renderTimestamp(text) : "-"} -
- ); - }, - }, - { - title: '进度', - dataIndex: 'progress', - width: 50, - render: (text, record, index) => { - return ( -
- { - // 转换例如100%为数字100,如果text未定义,返回0 - isNaN(text.replace('%', '')) ? text : - } -
- ); - }, - }, - { - title: '花费时间', - dataIndex: 'finish_time', // 以finish_time作为dataIndex - key: 'finish_time', - render: (finish, record) => { - // 假设record.start_time是存在的,并且finish是完成时间的时间戳 - return <> - { - finish ? renderDuration(record.submit_time, finish) : "-" - } - - }, - }, - { - title: "渠道", - dataIndex: 'channel_id', - className: isAdminUser ? 'tableShow' : 'tableHiddle', - render: (text, record, index) => { - return ( -
- { - copyText(text); // 假设copyText是用于文本复制的函数 - }} - > - {' '} - {text}{' '} - -
- ); - }, - }, - { - title: "平台", - dataIndex: 'platform', - render: (text, record, index) => { - return ( -
- {renderPlatform(text)} -
- ); - }, - }, - { - title: '类型', - dataIndex: 'action', - render: (text, record, index) => { - return ( -
- {renderType(text)} -
- ); - }, - }, - { - title: '任务ID(点击查看详情)', - dataIndex: 'task_id', - render: (text, record, index) => { - return ( { - setModalContent(JSON.stringify(record, null, 2)); - setIsModalOpen(true); - }} - > -
- {text} -
-
); - }, - }, - { - title: '任务状态', - dataIndex: 'status', - render: (text, record, index) => { - return ( -
- {renderStatus(text)} -
- ); - }, - }, - - { - title: '失败原因', - dataIndex: 'fail_reason', - render: (text, record, index) => { - // 如果text未定义,返回替代文本,例如空字符串''或其他 - if (!text) { - return '无'; - } - - return ( - { - setModalContent(text); - setIsModalOpen(true); - }} - > - {text} - - ); + const [isModalOpen, setIsModalOpen] = useState(false); + const [modalContent, setModalContent] = useState(''); + const isAdminUser = isAdmin(); + const columns = [ + { + title: '提交时间', + dataIndex: 'submit_time', + render: (text, record, index) => { + return
{text ? renderTimestamp(text) : '-'}
; + }, + }, + { + title: '结束时间', + dataIndex: 'finish_time', + render: (text, record, index) => { + return
{text ? renderTimestamp(text) : '-'}
; + }, + }, + { + title: '进度', + dataIndex: 'progress', + width: 50, + render: (text, record, index) => { + return ( +
+ { + // 转换例如100%为数字100,如果text未定义,返回0 + isNaN(text.replace('%', '')) ? ( + text + ) : ( + + ) } +
+ ); + }, + }, + { + title: '花费时间', + dataIndex: 'finish_time', // 以finish_time作为dataIndex + key: 'finish_time', + render: (finish, record) => { + // 假设record.start_time是存在的,并且finish是完成时间的时间戳 + return <>{finish ? renderDuration(record.submit_time, finish) : '-'}; + }, + }, + { + title: '渠道', + dataIndex: 'channel_id', + className: isAdminUser ? 'tableShow' : 'tableHiddle', + render: (text, record, index) => { + return ( +
+ { + copyText(text); // 假设copyText是用于文本复制的函数 + }} + > + {' '} + {text}{' '} + +
+ ); + }, + }, + { + title: '平台', + dataIndex: 'platform', + render: (text, record, index) => { + return
{renderPlatform(text)}
; + }, + }, + { + title: '类型', + dataIndex: 'action', + render: (text, record, index) => { + return
{renderType(text)}
; + }, + }, + { + title: '任务ID(点击查看详情)', + dataIndex: 'task_id', + render: (text, record, index) => { + return ( + { + setModalContent(JSON.stringify(record, null, 2)); + setIsModalOpen(true); + }} + > +
{text}
+
+ ); + }, + }, + { + title: '任务状态', + dataIndex: 'status', + render: (text, record, index) => { + return
{renderStatus(text)}
; + }, + }, + + { + title: '失败原因', + dataIndex: 'fail_reason', + render: (text, record, index) => { + // 如果text未定义,返回替代文本,例如空字符串''或其他 + if (!text) { + return '无'; } - ]; - const [logs, setLogs] = useState([]); - const [loading, setLoading] = useState(true); - const [activePage, setActivePage] = useState(1); - const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); - const [logType] = useState(0); + return ( + { + setModalContent(text); + setIsModalOpen(true); + }} + > + {text} + + ); + }, + }, + ]; - let now = new Date(); - // 初始化start_timestamp为前一天 - let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate()); - const [inputs, setInputs] = useState({ - channel_id: '', - task_id: '', - start_timestamp: timestamp2string(zeroNow.getTime() /1000), - end_timestamp: '', - }); - const { channel_id, task_id, start_timestamp, end_timestamp } = inputs; + const [logs, setLogs] = useState([]); + const [loading, setLoading] = useState(true); + const [activePage, setActivePage] = useState(1); + const [logCount, setLogCount] = useState(ITEMS_PER_PAGE); + const [logType] = useState(0); - const handleInputChange = (value, name) => { - setInputs((inputs) => ({ ...inputs, [name]: value })); - }; + let now = new Date(); + // 初始化start_timestamp为前一天 + let zeroNow = new Date(now.getFullYear(), now.getMonth(), now.getDate()); + const [inputs, setInputs] = useState({ + channel_id: '', + task_id: '', + start_timestamp: timestamp2string(zeroNow.getTime() / 1000), + end_timestamp: '', + }); + const { channel_id, task_id, start_timestamp, end_timestamp } = inputs; + const handleInputChange = (value, name) => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }; - const setLogsFormat = (logs) => { - for (let i = 0; i < logs.length; i++) { - logs[i].timestamp2string = timestamp2string(logs[i].created_at); - logs[i].key = '' + logs[i].id; - } - // data.key = '' + data.id - setLogs(logs); - setLogCount(logs.length + ITEMS_PER_PAGE); - // console.log(logCount); + const setLogsFormat = (logs) => { + for (let i = 0; i < logs.length; i++) { + logs[i].timestamp2string = timestamp2string(logs[i].created_at); + logs[i].key = '' + logs[i].id; } + // data.key = '' + data.id + setLogs(logs); + setLogCount(logs.length + ITEMS_PER_PAGE); + // console.log(logCount); + }; - const loadLogs = async (startIdx) => { - setLoading(true); + const loadLogs = async (startIdx) => { + setLoading(true); - let url = ''; - let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000); - let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000 ); - if (isAdminUser) { - url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; - } else { - url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; - } - const res = await API.get(url); - let { success, message, data } = res.data; - if (success) { - if (startIdx === 0) { - setLogsFormat(data); - } else { - let newLogs = [...logs]; - newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); - setLogsFormat(newLogs); - } - } else { - showError(message); - } - setLoading(false); - }; - - const pageData = logs.slice((activePage - 1) * ITEMS_PER_PAGE, activePage * ITEMS_PER_PAGE); - - const handlePageChange = page => { - setActivePage(page); - if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { - // In this case we have to load more data and then append them. - loadLogs(page - 1).then(r => { - }); - } - }; - - const refresh = async () => { - // setLoading(true); - setActivePage(1); - await loadLogs(0); - }; - - const copyText = async (text) => { - if (await copy(text)) { - showSuccess('已复制:' + text); - } else { - // setSearchKeyword(text); - Modal.error({ title: "无法复制到剪贴板,请手动复制", content: text }); - } + let url = ''; + let localStartTimestamp = parseInt(Date.parse(start_timestamp) / 1000); + let localEndTimestamp = parseInt(Date.parse(end_timestamp) / 1000); + if (isAdminUser) { + url = `/api/task/?p=${startIdx}&channel_id=${channel_id}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; + } else { + url = `/api/task/self?p=${startIdx}&task_id=${task_id}&start_timestamp=${localStartTimestamp}&end_timestamp=${localEndTimestamp}`; } - - useEffect(() => { - refresh().then(); - }, [logType]); - - const renderType = (type) => { - switch (type) { - case 'MUSIC': - return ; - case 'LYRICS': - return ; - - default: - return ; - } + const res = await API.get(url); + let { success, message, data } = res.data; + if (success) { + if (startIdx === 0) { + setLogsFormat(data); + } else { + let newLogs = [...logs]; + newLogs.splice(startIdx * ITEMS_PER_PAGE, data.length, ...data); + setLogsFormat(newLogs); + } + } else { + showError(message); } + setLoading(false); + }; - const renderPlatform = (type) => { - switch (type) { - case "suno": - return ; - default: - return ; - } + const pageData = logs.slice( + (activePage - 1) * ITEMS_PER_PAGE, + activePage * ITEMS_PER_PAGE, + ); + + const handlePageChange = (page) => { + setActivePage(page); + if (page === Math.ceil(logs.length / ITEMS_PER_PAGE) + 1) { + // In this case we have to load more data and then append them. + loadLogs(page - 1).then((r) => {}); } + }; - const renderStatus = (type) => { - switch (type) { - case 'SUCCESS': - return ; - case 'NOT_START': - return ; - case 'SUBMITTED': - return ; - case 'IN_PROGRESS': - return ; - case 'FAILURE': - return ; - case 'QUEUED': - return ; - case 'UNKNOWN': - return ; - case '': - return ; - default: - return ; - } + const refresh = async () => { + // setLoading(true); + setActivePage(1); + await loadLogs(0); + }; + + const copyText = async (text) => { + if (await copy(text)) { + showSuccess('已复制:' + text); + } else { + // setSearchKeyword(text); + Modal.error({ title: '无法复制到剪贴板,请手动复制', content: text }); } + }; - return ( - <> + useEffect(() => { + refresh().then(); + }, [logType]); - -
- <> - {isAdminUser && handleInputChange(value, 'channel_id')} /> - } - handleInputChange(value, 'task_id')} /> + const renderType = (type) => { + switch (type) { + case 'MUSIC': + return ( + + ); + case 'LYRICS': + return ( + + ); - handleInputChange(value, 'start_timestamp')} /> - handleInputChange(value, 'end_timestamp')} /> - - - - -
- - setIsModalOpen(false)} - onCancel={() => setIsModalOpen(false)} - closable={null} - bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式 - width={800} // 设置模态框宽度 - > -

{modalContent}

-
- - - ); + default: + return ( + + ); + } + }; + + const renderPlatform = (type) => { + switch (type) { + case 'suno': + return ( + + ); + default: + return ( + + ); + } + }; + + const renderStatus = (type) => { + switch (type) { + case 'SUCCESS': + return ( + + ); + case 'NOT_START': + return ( + + ); + case 'SUBMITTED': + return ( + + ); + case 'IN_PROGRESS': + return ( + + ); + case 'FAILURE': + return ( + + ); + case 'QUEUED': + return ( + + ); + case 'UNKNOWN': + return ( + + ); + case '': + return ( + + ); + default: + return ( + + ); + } + }; + + return ( + <> + +
+ <> + {isAdminUser && ( + handleInputChange(value, 'channel_id')} + /> + )} + handleInputChange(value, 'task_id')} + /> + + handleInputChange(value, 'start_timestamp')} + /> + handleInputChange(value, 'end_timestamp')} + /> + + + + +
+ + setIsModalOpen(false)} + onCancel={() => setIsModalOpen(false)} + closable={null} + bodyStyle={{ height: '400px', overflow: 'auto' }} // 设置模态框内容区域样式 + width={800} // 设置模态框宽度 + > +

{modalContent}

+
+ + + ); }; export default LogsTable; diff --git a/web/src/components/TokensTable.js b/web/src/components/TokensTable.js index ba2a8be3..599b3459 100644 --- a/web/src/components/TokensTable.js +++ b/web/src/components/TokensTable.js @@ -8,14 +8,16 @@ import { } from '../helpers'; import { ITEMS_PER_PAGE } from '../constants'; -import {renderGroup, renderQuota} from '../helpers/render'; +import { renderGroup, renderQuota } from '../helpers/render'; import { - Button, Divider, + Button, + Divider, Dropdown, Form, Modal, Popconfirm, - Popover, Space, + Popover, + Space, SplitButtonGroup, Table, Tag, @@ -30,7 +32,6 @@ function renderTimestamp(timestamp) { } const TokensTable = () => { - const { t } = useTranslation(); const renderStatus = (status, model_limits_enabled = false) => { @@ -86,12 +87,14 @@ const TokensTable = () => { dataIndex: 'status', key: 'status', render: (text, record, index) => { - return
- - {renderStatus(text, record.model_limits_enabled)} - {renderGroup(record.group)} - -
; + return ( +
+ + {renderStatus(text, record.model_limits_enabled)} + {renderGroup(record.group)} + +
+ ); }, }, { @@ -143,7 +146,7 @@ const TokensTable = () => { dataIndex: 'operate', render: (text, record, index) => { let chats = localStorage.getItem('chats'); - let chatsArray = [] + let chatsArray = []; let shouldUseCustom = true; if (shouldUseCustom) { @@ -153,7 +156,7 @@ const TokensTable = () => { // check chats is array if (Array.isArray(chats)) { for (let i = 0; i < chats.length; i++) { - let chat = {} + let chat = {}; chat.node = 'item'; // c is a map // chat.key = chats[i].name; @@ -164,13 +167,12 @@ const TokensTable = () => { chat.name = key; chat.onClick = () => { onOpenLink(key, chats[i][key], record); - } + }; } } chatsArray.push(chat); } } - } catch (e) { console.log(e); showError(t('聊天链接配置错误,请联系管理员')); @@ -208,7 +210,11 @@ const TokensTable = () => { if (chatsArray.length === 0) { showError(t('请联系管理员配置聊天链接')); } else { - onOpenLink('default', chats[0][Object.keys(chats[0])[0]], record); + onOpenLink( + 'default', + chats[0][Object.keys(chats[0])[0]], + record, + ); } }} > @@ -539,36 +545,36 @@ const TokensTable = () => { {t('查询')} - +
@@ -588,7 +594,7 @@ const TokensTable = () => { t('第 {{start}} - {{end}} 条,共 {{total}} 条', { start: page.currentStart, end: page.currentEnd, - total: tokens.length + total: tokens.length, }), onPageSizeChange: (size) => { setPageSize(size); diff --git a/web/src/components/UsersTable.js b/web/src/components/UsersTable.js index a1e43b45..b77f7396 100644 --- a/web/src/components/UsersTable.js +++ b/web/src/components/UsersTable.js @@ -167,7 +167,11 @@ const UsersTable = () => { manageUser(record.id, 'demote', record); }} > - @@ -261,7 +265,7 @@ const UsersTable = () => { users[i].key = users[i].id; } setUsers(users); - } + }; const loadUsers = async (startIdx, pageSize) => { const res = await API.get(`/api/user/?p=${startIdx}&page_size=${pageSize}`); @@ -277,7 +281,6 @@ const UsersTable = () => { setLoading(false); }; - useEffect(() => { loadUsers(0, pageSize) .then() @@ -327,22 +330,29 @@ const UsersTable = () => { } }; - const searchUsers = async (startIdx, pageSize, searchKeyword, searchGroup) => { + const searchUsers = async ( + startIdx, + pageSize, + searchKeyword, + searchGroup, + ) => { if (searchKeyword === '' && searchGroup === '') { - // if keyword is blank, load files instead. - await loadUsers(startIdx, pageSize); - return; + // if keyword is blank, load files instead. + await loadUsers(startIdx, pageSize); + return; } setSearching(true); - const res = await API.get(`/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`); + const res = await API.get( + `/api/user/search?keyword=${searchKeyword}&group=${searchGroup}&p=${startIdx}&page_size=${pageSize}`, + ); const { success, message, data } = res.data; if (success) { - const newPageData = data.items; - setActivePage(data.page); - setUserCount(data.total); - setUserFormat(newPageData); + const newPageData = data.items; + setActivePage(data.page); + setUserCount(data.total); + setUserFormat(newPageData); } else { - showError(message); + showError(message); } setSearching(false); }; @@ -354,9 +364,9 @@ const UsersTable = () => { const handlePageChange = (page) => { setActivePage(page); if (searchKeyword === '' && searchGroup === '') { - loadUsers(page, pageSize).then(); + loadUsers(page, pageSize).then(); } else { - searchUsers(page, pageSize, searchKeyword, searchGroup).then(); + searchUsers(page, pageSize, searchKeyword, searchGroup).then(); } }; @@ -372,7 +382,7 @@ const UsersTable = () => { }; const refresh = async () => { - setActivePage(1) + setActivePage(1); if (searchKeyword === '') { await loadUsers(activePage, pageSize); } else { @@ -431,7 +441,9 @@ const UsersTable = () => { >
- + { onChange={(value) => handleKeywordChange(value)} /> - + { t('第 {{start}} - {{end}} 条,共 {{total}} 条', { start: page.currentStart, end: page.currentEnd, - total: users.length + total: users.length, }), currentPage: activePage, pageSize: pageSize, diff --git a/web/src/components/custom/TextInput.js b/web/src/components/custom/TextInput.js index b4ace8f1..8c137fe1 100644 --- a/web/src/components/custom/TextInput.js +++ b/web/src/components/custom/TextInput.js @@ -1,7 +1,14 @@ import { Input, Typography } from '@douyinfe/semi-ui'; import React from 'react'; -const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) => { +const TextInput = ({ + label, + name, + value, + onChange, + placeholder, + type = 'text', +}) => { return ( <>
@@ -12,10 +19,10 @@ const TextInput = ({ label, name, value, onChange, placeholder, type = 'text' }) placeholder={placeholder} onChange={(value) => onChange(value)} value={value} - autoComplete="new-password" + autoComplete='new-password' /> ); -} +}; -export default TextInput; \ No newline at end of file +export default TextInput; diff --git a/web/src/components/custom/TextNumberInput.js b/web/src/components/custom/TextNumberInput.js index e25c7725..36e0cac0 100644 --- a/web/src/components/custom/TextNumberInput.js +++ b/web/src/components/custom/TextNumberInput.js @@ -12,10 +12,10 @@ const TextNumberInput = ({ label, name, value, onChange, placeholder }) => { placeholder={placeholder} onChange={(value) => onChange(value)} value={value} - autoComplete="new-password" + autoComplete='new-password' /> ); -} +}; -export default TextNumberInput; \ No newline at end of file +export default TextNumberInput; diff --git a/web/src/components/fetchTokenKeys.js b/web/src/components/fetchTokenKeys.js index 46a70f15..e9cec001 100644 --- a/web/src/components/fetchTokenKeys.js +++ b/web/src/components/fetchTokenKeys.js @@ -13,7 +13,7 @@ async function fetchTokenKeys() { throw new Error('Failed to fetch token keys'); } } catch (error) { - console.error("Error fetching token keys:", error); + console.error('Error fetching token keys:', error); return []; } } @@ -27,7 +27,7 @@ function getServerAddress() { status = JSON.parse(status); serverAddress = status.server_address || ''; } catch (error) { - console.error("Failed to parse status from localStorage:", error); + console.error('Failed to parse status from localStorage:', error); } } @@ -65,4 +65,4 @@ export function useTokenKeys(id) { }, []); return { keys, serverAddress, isLoading }; -} \ No newline at end of file +} diff --git a/web/src/components/utils.js b/web/src/components/utils.js index a22f76ed..93a5fb85 100644 --- a/web/src/components/utils.js +++ b/web/src/components/utils.js @@ -16,6 +16,20 @@ export async function getOAuthState() { } } +export async function onOIDCClicked(auth_url, client_id, openInNewTab = false) { + const state = await getOAuthState(); + if (!state) return; + const redirect_uri = `${window.location.origin}/oauth/oidc`; + const response_type = 'code'; + const scope = 'openid profile email'; + const url = `${auth_url}?client_id=${client_id}&redirect_uri=${redirect_uri}&response_type=${response_type}&scope=${scope}&state=${state}`; + if (openInNewTab) { + window.open(url); + } else { + window.location.href = url; + } +} + export async function onGitHubOAuthClicked(github_client_id) { const state = await getOAuthState(); if (!state) return; diff --git a/web/src/constants/channel.constants.js b/web/src/constants/channel.constants.js index 5738d656..fa59bcce 100644 --- a/web/src/constants/channel.constants.js +++ b/web/src/constants/channel.constants.js @@ -3,88 +3,89 @@ export const CHANNEL_OPTIONS = [ { value: 2, color: 'light-blue', - label: 'Midjourney Proxy' + label: 'Midjourney Proxy', }, { value: 5, color: 'blue', - label: 'Midjourney Proxy Plus' + label: 'Midjourney Proxy Plus', }, { value: 36, color: 'purple', - label: 'Suno API' + label: 'Suno API', }, { value: 4, color: 'grey', label: 'Ollama' }, { value: 14, color: 'indigo', - label: 'Anthropic Claude' + label: 'Anthropic Claude', }, { value: 33, color: 'indigo', - label: 'AWS Claude' + label: 'AWS Claude', }, { value: 41, color: 'blue', label: 'Vertex AI' }, { value: 3, color: 'teal', - label: 'Azure OpenAI' + label: 'Azure OpenAI', }, { value: 34, color: 'purple', - label: 'Cohere' + label: 'Cohere', }, { value: 39, color: 'grey', label: 'Cloudflare' }, { value: 43, color: 'blue', label: 'DeepSeek' }, { value: 15, color: 'blue', - label: '百度文心千帆' + label: '百度文心千帆', }, { value: 46, color: 'blue', - label: '百度文心千帆V2' + label: '百度文心千帆V2', }, { value: 17, color: 'orange', - label: '阿里通义千问' + label: '阿里通义千问', }, { value: 18, color: 'blue', - label: '讯飞星火认知' + label: '讯飞星火认知', }, { value: 16, color: 'violet', - label: '智谱 ChatGLM' + label: '智谱 ChatGLM', }, { value: 26, color: 'purple', - label: '智谱 GLM-4V' + label: '智谱 GLM-4V', }, { value: 24, color: 'orange', - label: 'Google Gemini' + label: 'Google Gemini', }, { value: 11, color: 'orange', - label: 'Google PaLM2' + label: 'Google PaLM2', }, { - value: 45, + value: 47, color: 'blue', - label: '字节火山方舟、豆包、DeepSeek通用' + label: 'Xinference', }, { value: 25, color: 'green', label: 'Moonshot' }, + { value: 20, color: 'green', label: 'OpenRouter' }, { value: 19, color: 'blue', label: '360 智脑' }, { value: 23, color: 'teal', label: '腾讯混元' }, { value: 31, color: 'green', label: '零一万物' }, @@ -97,16 +98,26 @@ export const CHANNEL_OPTIONS = [ { value: 22, color: 'blue', - label: '知识库:FastGPT' + label: '知识库:FastGPT', }, { value: 21, color: 'purple', - label: '知识库:AI Proxy' + label: '知识库:AI Proxy', }, { value: 44, color: 'purple', - label: '嵌入模型:MokaAI M3E' + label: '嵌入模型:MokaAI M3E', + }, + { + value: 45, + color: 'blue', + label: '字节火山方舟、豆包、DeepSeek通用', + }, + { + value: 48, + color: 'blue', + label: 'xAI' } ]; diff --git a/web/src/context/Style/index.js b/web/src/context/Style/index.js index c3f28e62..db9b0dd1 100644 --- a/web/src/context/Style/index.js +++ b/web/src/context/Style/index.js @@ -9,8 +9,9 @@ export const StyleContext = React.createContext({ export const StyleProvider = ({ children }) => { const [state, setState] = useState({ - isMobile: false, + isMobile: isMobile(), showSider: false, + siderCollapsed: false, shouldInnerPadding: false, }); @@ -18,28 +19,37 @@ export const StyleProvider = ({ children }) => { if ('type' in action) { switch (action.type) { case 'TOGGLE_SIDER': - setState(prev => ({ ...prev, showSider: !prev.showSider })); + setState((prev) => ({ ...prev, showSider: !prev.showSider })); break; case 'SET_SIDER': - setState(prev => ({ ...prev, showSider: action.payload })); + setState((prev) => ({ ...prev, showSider: action.payload })); break; case 'SET_MOBILE': - setState(prev => ({ ...prev, isMobile: action.payload })); + setState((prev) => ({ ...prev, isMobile: action.payload })); + break; + case 'SET_SIDER_COLLAPSED': + setState((prev) => ({ ...prev, siderCollapsed: action.payload })); break; case 'SET_INNER_PADDING': - setState(prev => ({ ...prev, shouldInnerPadding: action.payload })); + setState((prev) => ({ ...prev, shouldInnerPadding: action.payload })); break; default: - setState(prev => ({ ...prev, ...action })); + setState((prev) => ({ ...prev, ...action })); } } else { - setState(prev => ({ ...prev, ...action })); + setState((prev) => ({ ...prev, ...action })); } }; useEffect(() => { const updateIsMobile = () => { - dispatch({ type: 'SET_MOBILE', payload: isMobile() }); + const mobileDetected = isMobile(); + dispatch({ type: 'SET_MOBILE', payload: mobileDetected }); + + // If on mobile, we might want to auto-hide the sidebar + if (mobileDetected && state.showSider) { + dispatch({ type: 'SET_SIDER', payload: false }); + } }; updateIsMobile(); @@ -47,28 +57,44 @@ export const StyleProvider = ({ children }) => { const updateShowSider = () => { // check pathname const pathname = window.location.pathname; - if (pathname === '' || pathname === '/' || pathname.includes('/home') || pathname.includes('/chat')) { + if ( + pathname === '' || + pathname === '/' || + pathname.includes('/home') || + pathname.includes('/chat') + ) { + dispatch({ type: 'SET_SIDER', payload: false }); + dispatch({ type: 'SET_INNER_PADDING', payload: false }); + } else if (pathname === '/setup') { dispatch({ type: 'SET_SIDER', payload: false }); dispatch({ type: 'SET_INNER_PADDING', payload: false }); } else { - dispatch({ type: 'SET_SIDER', payload: true }); + // Only show sidebar on non-mobile devices by default + dispatch({ type: 'SET_SIDER', payload: !isMobile() }); dispatch({ type: 'SET_INNER_PADDING', payload: true }); } - - if (isMobile()) { - dispatch({ type: 'SET_SIDER', payload: false }); - } }; - updateShowSider() + updateShowSider(); + const updateSiderCollapsed = () => { + const isCollapsed = + localStorage.getItem('default_collapse_sidebar') === 'true'; + dispatch({ type: 'SET_SIDER_COLLAPSED', payload: isCollapsed }); + }; - // Optionally, add event listeners to handle window resize - window.addEventListener('resize', updateIsMobile); + updateSiderCollapsed(); + + // Add event listeners to handle window resize + const handleResize = () => { + updateIsMobile(); + }; + + window.addEventListener('resize', handleResize); // Cleanup event listener on component unmount return () => { - window.removeEventListener('resize', updateIsMobile); + window.removeEventListener('resize', handleResize); }; }, []); diff --git a/web/src/helpers/api.js b/web/src/helpers/api.js index 635d63b8..84d2df1f 100644 --- a/web/src/helpers/api.js +++ b/web/src/helpers/api.js @@ -7,8 +7,8 @@ export let API = axios.create({ : '', headers: { 'New-API-User': getUserIdFromLocalStorage(), - 'Cache-Control': 'no-store' - } + 'Cache-Control': 'no-store', + }, }); export function updateAPI() { @@ -18,8 +18,8 @@ export function updateAPI() { : '', headers: { 'New-API-User': getUserIdFromLocalStorage(), - 'Cache-Control': 'no-store' - } + 'Cache-Control': 'no-store', + }, }); } diff --git a/web/src/helpers/other.js b/web/src/helpers/other.js index 3e172180..c5d8c269 100644 --- a/web/src/helpers/other.js +++ b/web/src/helpers/other.js @@ -1,7 +1,7 @@ -export function getLogOther(otherStr) { - if (otherStr === undefined || otherStr === '') { - otherStr = '{}' - } - let other = JSON.parse(otherStr) - return other -} \ No newline at end of file +export function getLogOther(otherStr) { + if (otherStr === undefined || otherStr === '') { + otherStr = '{}'; + } + let other = JSON.parse(otherStr); + return other; +} diff --git a/web/src/helpers/render.js b/web/src/helpers/render.js index 3ac81420..4d1a3113 100644 --- a/web/src/helpers/render.js +++ b/web/src/helpers/render.js @@ -44,7 +44,10 @@ export function renderGroup(group) { if (await copy(group)) { showSuccess(i18next.t('已复制:') + group); } else { - Modal.error({ title: t('无法复制到剪贴板,请手动复制'), content: group }); + Modal.error({ + title: t('无法复制到剪贴板,请手动复制'), + content: group, + }); } }} > @@ -64,28 +67,37 @@ export function renderRatio(ratio) { } else if (ratio > 1) { color = 'blue'; } - return {ratio}x {i18next.t('倍率')}; + return ( + + {ratio}x {i18next.t('倍率')} + + ); } -const measureTextWidth = (text, style = { - fontSize: '14px', - fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif' -}, containerWidth) => { +const measureTextWidth = ( + text, + style = { + fontSize: '14px', + fontFamily: + '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif', + }, + containerWidth, +) => { const span = document.createElement('span'); - + span.style.visibility = 'hidden'; span.style.position = 'absolute'; span.style.whiteSpace = 'nowrap'; span.style.fontSize = style.fontSize; span.style.fontFamily = style.fontFamily; - + span.textContent = text; - + document.body.appendChild(span); const width = span.offsetWidth; - + document.body.removeChild(span); - + return width; }; @@ -94,7 +106,7 @@ export function truncateText(text, maxWidth = 200) { return text; } if (!text) return text; - + try { // Handle percentage-based maxWidth let actualMaxWidth = maxWidth; @@ -103,19 +115,19 @@ export function truncateText(text, maxWidth = 200) { // Use window width as fallback container width actualMaxWidth = window.innerWidth * percentage; } - + const width = measureTextWidth(text); if (width <= actualMaxWidth) return text; - + let left = 0; let right = text.length; let result = text; - + while (left <= right) { const mid = Math.floor((left + right) / 2); const truncated = text.slice(0, mid) + '...'; const currentWidth = measureTextWidth(truncated); - + if (currentWidth <= actualMaxWidth) { result = truncated; left = mid + 1; @@ -123,10 +135,13 @@ export function truncateText(text, maxWidth = 200) { right = mid - 1; } } - + return result; } catch (error) { - console.warn('Text measurement failed, falling back to character count', error); + console.warn( + 'Text measurement failed, falling back to character count', + error, + ); if (text.length > 20) { return text.slice(0, 17) + '...'; } @@ -149,11 +164,11 @@ export const renderGroupOption = (item) => { emptyContent, ...rest } = item; - + const baseStyle = { - display: 'flex', - justifyContent: 'space-between', - alignItems: 'center', + display: 'flex', + justifyContent: 'space-between', + alignItems: 'center', padding: '8px 16px', cursor: disabled ? 'not-allowed' : 'pointer', backgroundColor: focused ? 'var(--semi-color-fill-0)' : 'transparent', @@ -162,8 +177,8 @@ export const renderGroupOption = (item) => { backgroundColor: 'var(--semi-color-primary-light-default)', }), '&:hover': { - backgroundColor: !disabled && 'var(--semi-color-fill-1)' - } + backgroundColor: !disabled && 'var(--semi-color-fill-1)', + }, }; const handleClick = () => { @@ -177,9 +192,9 @@ export const renderGroupOption = (item) => { onMouseEnter(e); } }; - + return ( -
{ {value} - + {label}
@@ -222,8 +237,7 @@ export function renderQuotaNumberWithDigit(num, digits = 2) { } export function renderNumberWithPoint(num) { - if (num === undefined) - return ''; + if (num === undefined) return ''; num = num.toFixed(2); if (num >= 100000) { // Convert number to string to manipulate it @@ -302,11 +316,14 @@ export function renderModelPrice( cacheRatio = 1.0, ) { if (modelPrice !== -1) { - return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', { - price: modelPrice, - ratio: groupRatio, - total: modelPrice * groupRatio - }); + return i18next.t( + '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', + { + price: modelPrice, + ratio: groupRatio, + total: modelPrice * groupRatio, + }, + ); } else { if (completionRatio === undefined) { completionRatio = 0; @@ -314,55 +331,72 @@ export function renderModelPrice( let inputRatioPrice = modelRatio * 2.0; let completionRatioPrice = modelRatio * 2.0 * completionRatio; let cacheRatioPrice = modelRatio * 2.0 * cacheRatio; - + // Calculate effective input tokens (non-cached + cached with ratio applied) - const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio); - + const effectiveInputTokens = + inputTokens - cacheTokens + cacheTokens * cacheRatio; + let price = (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + (completionTokens / 1000000) * completionRatioPrice * groupRatio; - + return ( <>
-

{i18next.t('提示价格:${{price}} = ${{total}} / 1M tokens', { - price: inputRatioPrice, - total: inputRatioPrice - })}

-

{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', { - price: inputRatioPrice, - total: completionRatioPrice, - completionRatio: completionRatio - })}

- {cacheTokens > 0 && ( -

{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { +

+ {i18next.t('提示价格:${{price}} / 1M tokens', { price: inputRatioPrice, - total: inputRatioPrice * cacheRatio, - cacheRatio: cacheRatio - })}

+ })} +

+

+ {i18next.t( + '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', + { + price: inputRatioPrice, + total: completionRatioPrice, + completionRatio: completionRatio, + }, + )} +

+ {cacheTokens > 0 && ( +

+ {i18next.t( + '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + { + price: inputRatioPrice, + total: inputRatioPrice * cacheRatio, + cacheRatio: cacheRatio, + }, + )} +

)}

- {cacheTokens > 0 ? - i18next.t('提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { - nonCacheInput: inputTokens - cacheTokens, - cacheInput: cacheTokens, - cachePrice: inputRatioPrice * cacheRatio, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - ratio: groupRatio, - total: price.toFixed(6) - }) : - i18next.t('提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', { - input: inputTokens, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - ratio: groupRatio, - total: price.toFixed(6) - }) - } + {cacheTokens > 0 + ? i18next.t( + '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', + { + nonCacheInput: inputTokens - cacheTokens, + cacheInput: cacheTokens, + cachePrice: inputRatioPrice * cacheRatio, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + total: price.toFixed(6), + }, + ) + : i18next.t( + '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + total: price.toFixed(6), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -381,19 +415,22 @@ export function renderModelPriceSimple( if (modelPrice !== -1) { return i18next.t('价格:${{price}} * 分组:{{ratio}}', { price: modelPrice, - ratio: groupRatio + ratio: groupRatio, }); } else { if (cacheTokens !== 0) { - return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', { - ratio: modelRatio, - groupRatio: groupRatio, - cacheRatio: cacheRatio - }); + return i18next.t( + '模型: {{ratio}} * 分组: {{groupRatio}} * 缓存: {{cacheRatio}}', + { + ratio: modelRatio, + groupRatio: groupRatio, + cacheRatio: cacheRatio, + }, + ); } else { return i18next.t('模型: {{ratio}} * 分组: {{groupRatio}}', { ratio: modelRatio, - groupRatio: groupRatio + groupRatio: groupRatio, }); } } @@ -415,11 +452,14 @@ export function renderAudioModelPrice( ) { // 1 ratio = $0.002 / 1K tokens if (modelPrice !== -1) { - return i18next.t('模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', { - price: modelPrice, - ratio: groupRatio, - total: modelPrice * groupRatio - }); + return i18next.t( + '模型价格:${{price}} * 分组倍率:{{ratio}} = ${{total}}', + { + price: modelPrice, + ratio: groupRatio, + total: modelPrice * groupRatio, + }, + ); } else { if (completionRatio === undefined) { completionRatio = 0; @@ -431,82 +471,120 @@ export function renderAudioModelPrice( let inputRatioPrice = modelRatio * 2.0; let completionRatioPrice = modelRatio * 2.0 * completionRatio; let cacheRatioPrice = modelRatio * 2.0 * cacheRatio; - + // Calculate effective input tokens (non-cached + cached with ratio applied) - const effectiveInputTokens = (inputTokens - cacheTokens) + (cacheTokens * cacheRatio); - + const effectiveInputTokens = + inputTokens - cacheTokens + cacheTokens * cacheRatio; + let textPrice = (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + - (completionTokens / 1000000) * completionRatioPrice * groupRatio + (completionTokens / 1000000) * completionRatioPrice * groupRatio; let audioPrice = (audioInputTokens / 1000000) * inputRatioPrice * audioRatio * groupRatio + - (audioCompletionTokens / 1000000) * inputRatioPrice * audioRatio * audioCompletionRatio * groupRatio; + (audioCompletionTokens / 1000000) * + inputRatioPrice * + audioRatio * + audioCompletionRatio * + groupRatio; let price = textPrice + audioPrice; return ( <>
-

{i18next.t('提示价格:${{price}} = ${{total}} / 1M tokens', { - price: inputRatioPrice, - total: inputRatioPrice - })}

-

{i18next.t('补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', { - price: inputRatioPrice, - total: completionRatioPrice, - completionRatio: completionRatio - })}

- {cacheTokens > 0 && ( -

{i18next.t('缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', { +

+ {i18next.t('提示价格:${{price}} / 1M tokens', { price: inputRatioPrice, - total: inputRatioPrice * cacheRatio, - cacheRatio: cacheRatio - })}

+ })} +

+

+ {i18next.t( + '补全价格:${{price}} * {{completionRatio}} = ${{total}} / 1M tokens (补全倍率: {{completionRatio}})', + { + price: inputRatioPrice, + total: completionRatioPrice, + completionRatio: completionRatio, + }, + )} +

+ {cacheTokens > 0 && ( +

+ {i18next.t( + '缓存价格:${{price}} * {{cacheRatio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + { + price: inputRatioPrice, + total: inputRatioPrice * cacheRatio, + cacheRatio: cacheRatio, + }, + )} +

)} -

{i18next.t('音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * audioRatio, - audioRatio: audioRatio - })}

-

{i18next.t('音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', { - price: inputRatioPrice, - total: inputRatioPrice * audioRatio * audioCompletionRatio, - audioRatio: audioRatio, - audioCompRatio: audioCompletionRatio - })}

- {cacheTokens > 0 ? - i18next.t('文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', { - nonCacheInput: inputTokens - cacheTokens, - cacheInput: cacheTokens, - cachePrice: inputRatioPrice * cacheRatio, + {i18next.t( + '音频提示价格:${{price}} * {{audioRatio}} = ${{total}} / 1M tokens (音频倍率: {{audioRatio}})', + { price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6) - }) : - i18next.t('文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', { - input: inputTokens, - price: inputRatioPrice, - completion: completionTokens, - compPrice: completionRatioPrice, - total: textPrice.toFixed(6) - }) - } + total: inputRatioPrice * audioRatio, + audioRatio: audioRatio, + }, + )}

- {i18next.t('音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', { - input: audioInputTokens, - completion: audioCompletionTokens, - audioInputPrice: audioRatio * inputRatioPrice, - audioCompPrice: audioRatio * audioCompletionRatio * inputRatioPrice, - total: audioPrice.toFixed(6) - })} + {i18next.t( + '音频补全价格:${{price}} * {{audioRatio}} * {{audioCompRatio}} = ${{total}} / 1M tokens (音频补全倍率: {{audioCompRatio}})', + { + price: inputRatioPrice, + total: inputRatioPrice * audioRatio * audioCompletionRatio, + audioRatio: audioRatio, + audioCompRatio: audioCompletionRatio, + }, + )}

- {i18next.t('总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', { - total: price.toFixed(6), - textPrice: textPrice.toFixed(6), - audioPrice: audioPrice.toFixed(6) - })} + {cacheTokens > 0 + ? i18next.t( + '文字提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + { + nonCacheInput: inputTokens - cacheTokens, + cacheInput: cacheTokens, + cachePrice: inputRatioPrice * cacheRatio, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + total: textPrice.toFixed(6), + }, + ) + : i18next.t( + '文字提示 {{input}} tokens / 1M tokens * ${{price}} + 文字补全 {{completion}} tokens / 1M tokens * ${{compPrice}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + total: textPrice.toFixed(6), + }, + )} +

+

+ {i18next.t( + '音频提示 {{input}} tokens / 1M tokens * ${{audioInputPrice}} + 音频补全 {{completion}} tokens / 1M tokens * ${{audioCompPrice}} = ${{total}}', + { + input: audioInputTokens, + completion: audioCompletionTokens, + audioInputPrice: audioRatio * inputRatioPrice, + audioCompPrice: + audioRatio * audioCompletionRatio * inputRatioPrice, + total: audioPrice.toFixed(6), + }, + )} +

+

+ {i18next.t( + '总价:文字价格 {{textPrice}} + 音频价格 {{audioPrice}} = ${{total}}', + { + total: price.toFixed(6), + textPrice: textPrice.toFixed(6), + audioPrice: audioPrice.toFixed(6), + }, + )}

{i18next.t('仅供参考,以实际扣费为准')}

@@ -519,7 +597,9 @@ export function renderQuotaWithPrompt(quota, digits) { let displayInCurrency = localStorage.getItem('display_in_currency'); displayInCurrency = displayInCurrency === 'true'; if (displayInCurrency) { - return ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + ''; + return ( + ' | ' + i18next.t('等价金额') + ': ' + renderQuota(quota, digits) + '' + ); } return ''; } @@ -539,7 +619,7 @@ const colors = [ 'red', 'teal', 'violet', - 'yellow' + 'yellow', ]; // 基础10色色板 (N ≤ 10) @@ -553,7 +633,7 @@ const baseColors = [ '#304D77', '#B48DEB', '#009488', - '#FF7DDA' + '#FF7DDA', ]; // 扩展20色色板 (10 < N ≤ 20) @@ -577,7 +657,7 @@ const extendedColors = [ '#009488', '#59BAA8', '#FF7DDA', - '#FFCFEE' + '#FFCFEE', ]; export const modelColorMap = { @@ -633,14 +713,14 @@ export function modelToColor(modelName) { // 2. 生成一个稳定的数字作为索引 let hash = 0; for (let i = 0; i < modelName.length; i++) { - hash = ((hash << 5) - hash) + modelName.charCodeAt(i); + hash = (hash << 5) - hash + modelName.charCodeAt(i); hash = hash & hash; // Convert to 32-bit integer } hash = Math.abs(hash); // 3. 根据模型名称长度选择不同的色板 const colorPalette = modelName.length > 10 ? extendedColors : baseColors; - + // 4. 使用hash值选择颜色 const index = hash % colorPalette.length; return colorPalette[index]; @@ -654,3 +734,229 @@ export function stringToColor(str) { let i = sum % colors.length; return colors[i]; } + +export function renderClaudeModelPrice( + inputTokens, + completionTokens, + modelRatio, + modelPrice = -1, + completionRatio, + groupRatio, + cacheTokens = 0, + cacheRatio = 1.0, + cacheCreationTokens = 0, + cacheCreationRatio = 1.0, +) { + const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); + + if (modelPrice !== -1) { + return i18next.t( + '模型价格:${{price}} * {{ratioType}}:{{ratio}} = ${{total}}', + { + price: modelPrice, + ratioType: ratioLabel, + ratio: groupRatio, + total: modelPrice * groupRatio, + }, + ); + } else { + if (completionRatio === undefined) { + completionRatio = 0; + } + + const completionRatioValue = completionRatio || 0; + const inputRatioPrice = modelRatio * 2.0; + const completionRatioPrice = modelRatio * 2.0 * completionRatioValue; + let cacheRatioPrice = (modelRatio * 2.0 * cacheRatio).toFixed(2); + let cacheCreationRatioPrice = modelRatio * 2.0 * cacheCreationRatio; + + // Calculate effective input tokens (non-cached + cached with ratio applied + cache creation with ratio applied) + const nonCachedTokens = inputTokens; + const effectiveInputTokens = + nonCachedTokens + + cacheTokens * cacheRatio + + cacheCreationTokens * cacheCreationRatio; + + let price = + (effectiveInputTokens / 1000000) * inputRatioPrice * groupRatio + + (completionTokens / 1000000) * completionRatioPrice * groupRatio; + + return ( + <> +
+

+ {i18next.t('提示价格:${{price}} / 1M tokens', { + price: inputRatioPrice, + })} +

+

+ {i18next.t( + '补全价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens', + { + price: inputRatioPrice, + ratio: completionRatio, + total: completionRatioPrice, + }, + )} +

+ {cacheTokens > 0 && ( +

+ {i18next.t( + '缓存价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})', + { + price: inputRatioPrice, + ratio: cacheRatio, + total: cacheRatioPrice, + cacheRatio: cacheRatio, + }, + )} +

+ )} + {cacheCreationTokens > 0 && ( +

+ {i18next.t( + '缓存创建价格:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存创建倍率: {{cacheCreationRatio}})', + { + price: inputRatioPrice, + ratio: cacheCreationRatio, + total: cacheCreationRatioPrice, + cacheCreationRatio: cacheCreationRatio, + }, + )} +

+ )} +

+

+ {cacheTokens > 0 || cacheCreationTokens > 0 + ? i18next.t( + '提示 {{nonCacheInput}} tokens / 1M tokens * ${{price}} + 缓存 {{cacheInput}} tokens / 1M tokens * ${{cachePrice}} + 缓存创建 {{cacheCreationInput}} tokens / 1M tokens * ${{cacheCreationPrice}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', + { + nonCacheInput: nonCachedTokens, + cacheInput: cacheTokens, + cacheRatio: cacheRatio, + cacheCreationInput: cacheCreationTokens, + cacheCreationRatio: cacheCreationRatio, + cachePrice: cacheRatioPrice, + cacheCreationPrice: cacheCreationRatioPrice, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + total: price.toFixed(6), + }, + ) + : i18next.t( + '提示 {{input}} tokens / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}', + { + input: inputTokens, + price: inputRatioPrice, + completion: completionTokens, + compPrice: completionRatioPrice, + ratio: groupRatio, + total: price.toFixed(6), + }, + )} +

+

{i18next.t('仅供参考,以实际扣费为准')}

+
+ + ); + } +} + +export function renderClaudeLogContent( + modelRatio, + completionRatio, + modelPrice = -1, + groupRatio, + cacheRatio = 1.0, + cacheCreationRatio = 1.0, +) { + const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); + + if (modelPrice !== -1) { + return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { + price: modelPrice, + ratioType: ratioLabel, + ratio: groupRatio, + }); + } else { + return i18next.t( + '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},缓存倍率 {{cacheRatio}},缓存创建倍率 {{cacheCreationRatio}},{{ratioType}} {{ratio}}', + { + modelRatio: modelRatio, + completionRatio: completionRatio, + cacheRatio: cacheRatio, + cacheCreationRatio: cacheCreationRatio, + ratioType: ratioLabel, + ratio: groupRatio, + }, + ); + } +} + +export function renderClaudeModelPriceSimple( + modelRatio, + modelPrice = -1, + groupRatio, + cacheTokens = 0, + cacheRatio = 1.0, + cacheCreationTokens = 0, + cacheCreationRatio = 1.0, +) { + const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组'); + + if (modelPrice !== -1) { + return i18next.t('价格:${{price}} * {{ratioType}}:{{ratio}}', { + price: modelPrice, + ratioType: ratioLabel, + ratio: groupRatio, + }); + } else { + if (cacheTokens !== 0 || cacheCreationTokens !== 0) { + return i18next.t( + '模型: {{ratio}} * {{ratioType}}: {{groupRatio}} * 缓存: {{cacheRatio}}', + { + ratio: modelRatio, + ratioType: ratioLabel, + groupRatio: groupRatio, + cacheRatio: cacheRatio, + cacheCreationRatio: cacheCreationRatio, + }, + ); + } else { + return i18next.t('模型: {{ratio}} * {{ratioType}}: {{groupRatio}}', { + ratio: modelRatio, + ratioType: ratioLabel, + groupRatio: groupRatio, + }); + } + } +} + +export function renderLogContent( + modelRatio, + completionRatio, + modelPrice = -1, + groupRatio, +) { + const ratioLabel = false ? i18next.t('专属倍率') : i18next.t('分组倍率'); + + if (modelPrice !== -1) { + return i18next.t('模型价格 ${{price}},{{ratioType}} {{ratio}}', { + price: modelPrice, + ratioType: ratioLabel, + ratio: groupRatio, + }); + } else { + return i18next.t( + '模型倍率 {{modelRatio}},补全倍率 {{completionRatio}},{{ratioType}} {{ratio}}', + { + modelRatio: modelRatio, + completionRatio: completionRatio, + ratioType: ratioLabel, + ratio: groupRatio, + }, + ); + } +} diff --git a/web/src/helpers/utils.js b/web/src/helpers/utils.js index a40b2079..38c25045 100644 --- a/web/src/helpers/utils.js +++ b/web/src/helpers/utils.js @@ -51,11 +51,11 @@ export async function copy(text) { } catch (e) { try { // 构建input 执行 复制命令 - var _input = window.document.createElement("input"); + var _input = window.document.createElement('input'); _input.value = text; window.document.body.appendChild(_input); _input.select(); - window.document.execCommand("Copy"); + window.document.execCommand('Copy'); window.document.body.removeChild(_input); } catch (e) { okay = false; @@ -143,6 +143,7 @@ export function openPage(url) { } export function removeTrailingSlash(url) { + if (!url) return ''; if (url.endsWith('/')) { return url.slice(0, -1); } else { @@ -191,7 +192,7 @@ export function timestamp2string1(timestamp, dataExportDefaultTime = 'hour') { let day = date.getDate().toString(); let hour = date.getHours().toString(); if (day === '24') { - console.log("timestamp", timestamp); + console.log('timestamp', timestamp); } if (month.length === 1) { month = '0' + month; @@ -247,7 +248,6 @@ export function verifyJSONPromise(value) { } } - export function shouldShowPrompt(id) { let prompt = localStorage.getItem(`prompt-${id}`); return !prompt; diff --git a/web/src/i18n/i18n.js b/web/src/i18n/i18n.js index f0d6687d..c1bf5860 100644 --- a/web/src/i18n/i18n.js +++ b/web/src/i18n/i18n.js @@ -11,16 +11,16 @@ i18n .init({ resources: { en: { - translation: enTranslation + translation: enTranslation, }, zh: { - translation: zhTranslation - } + translation: zhTranslation, + }, }, fallbackLng: 'zh', interpolation: { - escapeValue: false - } + escapeValue: false, + }, }); -export default i18n; \ No newline at end of file +export default i18n; diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 9951534e..8aaaab77 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1,5 +1,6 @@ { "主页": "Home", + "文档": "Docs", "控制台": "Console", "$%.6f 额度": "$%.6f quota", "%d 点额度": "%d point quota", @@ -192,6 +193,8 @@ "通用设置": "General Settings", "充值链接": "Recharge Link", "例如发卡网站的购买链接": "E.g., purchase link from card issuing website", + "文档地址": "Document Link", + "例如 https://docs.newapi.pro": "E.g., https://docs.newapi.pro", "聊天页面链接": "Chat Page Link", "例如 ChatGPT Next Web 的部署地址": "E.g., ChatGPT Next Web deployment address", "单位美元额度": "Quota per USD", @@ -489,7 +492,7 @@ "请输入默认 API 版本,例如:2023-03-15-preview,该配置可以被实际的请求查询参数所覆盖": "Please enter the default API version, for example: 2023-03-15-preview, this configuration can be overridden by the actual request query parameters", "默认": "default", "图片演示": "Image demo", - "参数替换为你的部署名称(模型名称中的点会被剔除)": "Replace the parameter with your deployment name (dots in the model name will be removed)", + "注意,系统请求的时模型名称中的点会被剔除,例如:gpt-4.5-preview会请求为gpt-45-preview,所以部署的模型名称需要去掉点": "Note that the dot in the model name requested by the system will be removed, for example: gpt-4.5-preview will be requested as gpt-45-preview, so the deployed model name needs to remove the dot", "模型映射必须是合法的 JSON 格式!": "Model mapping must be in valid JSON format!", "取消无限额度": "Cancel unlimited quota", "取消": "Cancel", @@ -511,7 +514,7 @@ ",图片演示。": "related image demo.", "令牌创建成功,请在列表页面点击复制获取令牌!": "Token created successfully, please click copy on the list page to get the token!", "代理": "Proxy", - "此项可选,用于通过代理站来进行 API 调用,请输入代理站地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", + "此项可选,用于通过自定义API地址来进行 API 调用,请输入API地址,格式为:https://domain.com": "This is optional, used to make API calls through the proxy site, please enter the proxy site address, the format is: https://domain.com", "取消密码登录将导致所有未绑定其他登录方式的用户(包括管理员)无法通过密码登录,确认取消?": "Canceling password login will cause all users (including administrators) who have not bound other login methods to be unable to log in via password, confirm cancel?", "按照如下格式输入:": "Enter in the following format:", "模型版本": "Model version", @@ -1062,7 +1065,7 @@ "价格:${{price}} * 分组:{{ratio}}": "Price: ${{price}} * Group: {{ratio}}", "模型: {{ratio}} * 分组: {{groupRatio}}": "Model: {{ratio}} * Group: {{groupRatio}}", "统计额度": "Statistical quota", - "统计Tokens": "Statistical Tokens", + "统计Tokens": "Statistical Tokens", "统计次数": "Statistical count", "平均RPM": "Average RPM", "平均TPM": "Average TPM", @@ -1108,7 +1111,7 @@ "如果你对接的是上游One API或者New API等转发项目,请使用OpenAI类型,不要使用此类型,除非你知道你在做什么。": "If you are connecting to upstream One API or New API forwarding projects, please use OpenAI type. Do not use this type unless you know what you are doing.", "完整的 Base URL,支持变量{model}": "Complete Base URL, supports variable {model}", "请输入完整的URL,例如:https://api.openai.com/v1/chat/completions": "Please enter complete URL, e.g.: https://api.openai.com/v1/chat/completions", - "此项可选,用于通过代理站来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through proxy sites, do not end with /v1 and /", + "此项可选,用于通过自定义API地址来进行 API 调用,末尾不要带/v1和/": "Optional for API calls through custom API address, do not add /v1 and / at the end", "私有部署地址": "Private Deployment Address", "请输入私有部署地址,格式为:https://fastgpt.run/api/openapi": "Please enter private deployment address, format: https://fastgpt.run/api/openapi", "注意非Chat API,请务必填写正确的API地址,否则可能导致无法使用": "Note: For non-Chat API, please make sure to enter the correct API address, otherwise it may not work", @@ -1269,9 +1272,10 @@ "通知邮箱": "Notification email", "设置用于接收额度预警的邮箱地址,不填则使用账号绑定的邮箱": "Set the email address for receiving quota warning notifications, if not set, the email address bound to the account will be used", "留空则使用账号绑定的邮箱": "If left blank, the email address bound to the account will be used", - "代理站地址": "Base URL", + "API地址": "Base URL", "对于官方渠道,new-api已经内置地址,除非是第三方代理站点或者Azure的特殊接入地址,否则不需要填写": "For official channels, the new-api has a built-in address. Unless it is a third-party proxy site or a special Azure access address, there is no need to fill it in", "渠道额外设置": "Channel extra settings", + "参数覆盖": "Parameters override", "模型请求速率限制": "Model request rate limit", "启用用户模型请求速率限制(可能会影响高并发性能)": "Enable user model request rate limit (may affect high concurrency performance)", "限制周期": "Limit period", @@ -1342,5 +1346,26 @@ "提示缓存倍率": "Prompt cache ratio", "缓存:${{price}} * {{ratio}} = ${{total}} / 1M tokens (缓存倍率: {{cacheRatio}})": "Cache: ${{price}} * {{ratio}} = ${{total}} / 1M tokens (cache ratio: {{cacheRatio}})", "提示 {{nonCacheInput}} tokens + 缓存 {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + 补全 {{completion}} tokens / 1M tokens * ${{compPrice}} * 分组 {{ratio}} = ${{total}}": "Prompt {{nonCacheInput}} tokens + cache {{cacheInput}} tokens * {{cacheRatio}} / 1M tokens * ${{price}} + completion {{completion}} tokens / 1M tokens * ${{compPrice}} * group {{ratio}} = ${{total}}", - "缓存 Tokens": "Cache Tokens" + "缓存 Tokens": "Cache Tokens", + "系统初始化": "System initialization", + "管理员账号已经初始化过,请继续设置系统参数": "The admin account has already been initialized, please continue to set the system parameters", + "管理员账号": "Admin account", + "请输入管理员用户名": "Please enter the admin username", + "请输入管理员密码": "Please enter the admin password", + "请确认管理员密码": "Please confirm the admin password", + "请选择使用模式": "Please select the usage mode", + "数据库警告": "Database warning", + "您正在使用 SQLite 数据库。如果您在容器环境中运行,请确保已正确设置数据库文件的持久化映射,否则容器重启后所有数据将丢失!": "You are using the SQLite database. If you are running in a container environment, please ensure that the database file persistence mapping is correctly set, otherwise all data will be lost after container restart!", + "建议在生产环境中使用 MySQL 或 PostgreSQL 数据库,或确保 SQLite 数据库文件已映射到宿主机的持久化存储。": "It is recommended to use MySQL or PostgreSQL databases in production environments, or ensure that the SQLite database file is mapped to the persistent storage of the host machine.", + "使用模式": "Usage mode", + "对外运营模式": "Default mode", + "密码长度至少为8个字符": "Password must be at least 8 characters long", + "表单引用错误,请刷新页面重试": "Form reference error, please refresh the page and try again", + "默认模式,适用于为多个用户提供服务的场景。": "Default mode, suitable for scenarios where multiple users are provided.", + "此模式下,系统将计算每次调用的用量,您需要对每个模型都设置价格,如果没有设置价格,用户将无法使用该模型。": "In this mode, the system will calculate the usage of each call, you need to set the price for each model, if the price is not set, the user will not be able to use the model.", + "适用于个人使用的场景。": "Suitable for personal use.", + "不需要设置模型价格,系统将弱化用量计算,您可专注于使用模型。": "No need to set the model price, the system will weaken the usage calculation, you can focus on using the model.", + "适用于展示系统功能的场景。": "Suitable for scenarios where the system functions are displayed.", + "可在初始化后修改": "Can be modified after initialization", + "初始化系统": "Initialize system" } diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index b2cf894c..5c7904fc 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -10,4 +10,4 @@ "展开侧边栏": "展开侧边栏", "关闭侧边栏": "关闭侧边栏", "注销成功!": "注销成功!" -} \ No newline at end of file +} diff --git a/web/src/index.css b/web/src/index.css index e5f9b530..c2e8ecd0 100644 --- a/web/src/index.css +++ b/web/src/index.css @@ -1,9 +1,8 @@ body { margin: 0; - padding-top: 55px; - overflow-y: scroll; - font-family: Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', - sans-serif; + padding-top: 0; + font-family: + Lato, 'Helvetica Neue', Arial, Helvetica, 'Microsoft YaHei', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; scrollbar-width: none; @@ -13,11 +12,26 @@ body { } #root { - height: 100vh; + height: 100%; + display: flex; flex-direction: column; + overflow: hidden; } -#root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li > span{ +#root + > section + > header + > section + > div + > div + > div + > div.semi-navigation-header-list-outer + > div.semi-navigation-list-wrapper + > ul + > div + > a + > li + > span { font-weight: 600 !important; } @@ -29,18 +43,59 @@ body { /*.semi-navigation-sub-wrap .semi-navigation-sub-title, .semi-navigation-item {*/ /* padding: 0 0;*/ /*}*/ + .topnav { + padding: 0 8px; + } + + .topnav .semi-navigation-item { + margin: 0 1px; + padding: 0 4px; + } + .topnav .semi-navigation-list-wrapper { max-width: calc(55vw - 20px); overflow-x: auto; scrollbar-width: none; } - #root > section > header > section > div > div > div > div.semi-navigation-footer > div > a > li { + #root + > section + > header + > section + > div + > div + > div + > div.semi-navigation-footer + > div + > a + > li { padding: 0 0; } - #root > section > header > section > div > div > div > div.semi-navigation-header-list-outer > div.semi-navigation-list-wrapper > ul > div > a > li { + #root + > section + > header + > section + > div + > div + > div + > div.semi-navigation-header-list-outer + > div.semi-navigation-list-wrapper + > ul + > div + > a + > li { padding: 0 5px; } - #root > section > header > section > div > div > div > div.semi-navigation-footer > div:nth-child(1) > a > li { + #root + > section + > header + > section + > div + > div + > div + > div.semi-navigation-footer + > div:nth-child(1) + > a + > li { padding: 0 5px; } .semi-navigation-footer { @@ -72,6 +127,31 @@ body { .semi-navigation-horizontal .semi-navigation-header { margin-right: 0; } + + /* 确保移动端内容可滚动 */ + .semi-layout-content { + -webkit-overflow-scrolling: touch !important; + overscroll-behavior-y: auto !important; + } + + /* 修复移动端下拉刷新 */ + body { + overflow: visible !important; + overscroll-behavior-y: auto !important; + position: static !important; + height: 100% !important; + } + + /* 确保内容区域在移动端可以正常滚动 */ + #root { + overflow: visible !important; + height: 100% !important; + } + + /* 隐藏在移动设备上 */ + .hide-on-mobile { + display: none !important; + } } .semi-table-tbody > .semi-table-row > .semi-table-row-cell { @@ -112,23 +192,55 @@ body::-webkit-scrollbar { } code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; + font-family: + source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace; } .semi-navigation-item { margin-bottom: 0; } -.semi-navigation-vertical { - /*flex: 0 0 auto;*/ - /*display: flex;*/ - /*flex-direction: column;*/ - /*width: 100%;*/ - height: 100%; +/* 自定义侧边栏按钮悬停效果 */ +.semi-navigation-item:hover { + transform: translateX(2px); + box-shadow: 0 2px 8px rgba(var(--semi-color-primary-rgb), 0.2); +} + +/* 自定义侧边栏按钮选中效果 */ +.semi-navigation-item-selected { + position: relative; overflow: hidden; } +.semi-navigation-item-selected::before { + content: ''; + position: absolute; + left: 0; + top: 0; + height: 100%; + width: 4px; + background-color: var(--semi-color-primary); + animation: slideIn 0.3s ease; +} + +@keyframes slideIn { + from { + transform: translateY(-100%); + } + to { + transform: translateY(0); + } +} + +/*.semi-navigation-vertical {*/ +/* !*flex: 0 0 auto;*!*/ +/* !*display: flex;*!*/ +/* !*flex-direction: column;*!*/ +/* !*width: 100%;*!*/ +/* height: 100%;*/ +/* overflow: hidden;*/ +/*}*/ + .main-content { padding: 4px; height: 100%; @@ -142,8 +254,67 @@ code { font-size: 1.1em; } -@media only screen and (max-width: 600px) { - .hide-on-mobile { - display: none !important; - } +/* 顶部栏样式 */ +.topnav { + padding: 0 16px; } + +.topnav .semi-navigation-item { + border-radius: 4px; + margin: 0 2px; + transition: all 0.3s ease; +} + +.topnav .semi-navigation-item:hover { + background-color: var(--semi-color-primary-light-default); + transform: translateY(-2px); + box-shadow: 0 2px 8px rgba(var(--semi-color-primary-rgb), 0.2); +} + +.topnav .semi-navigation-item-selected { + background-color: var(--semi-color-primary-light-default); + color: var(--semi-color-primary); + font-weight: 600; +} + +/* 顶部栏文本样式 */ +.header-bar-text { + color: var(--semi-color-text-0); + font-weight: 500; + transition: all 0.3s ease; +} + +.header-bar-text:hover { + color: var(--semi-color-primary); +} + +/* 自定义滚动条样式 */ +.semi-layout-content::-webkit-scrollbar, +.semi-sider::-webkit-scrollbar { + width: 6px; + height: 6px; +} + +.semi-layout-content::-webkit-scrollbar-thumb, +.semi-sider::-webkit-scrollbar-thumb { + background: var(--semi-color-tertiary-light-default); + border-radius: 3px; +} + +.semi-layout-content::-webkit-scrollbar-thumb:hover, +.semi-sider::-webkit-scrollbar-thumb:hover { + background: var(--semi-color-tertiary); +} + +.semi-layout-content::-webkit-scrollbar-track, +.semi-sider::-webkit-scrollbar-track { + background: transparent; +} + +/* Custom sidebar shadow */ +/*.custom-sidebar-nav {*/ +/* box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/ +/* -webkit-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/ +/* -moz-box-shadow: 0 1px 6px rgba(0, 0, 0, 0.08) !important;*/ +/* min-height: 100%;*/ +/*}*/ diff --git a/web/src/index.js b/web/src/index.js index bc16e36b..aa709ff8 100644 --- a/web/src/index.js +++ b/web/src/index.js @@ -28,7 +28,7 @@ root.render( - + diff --git a/web/src/pages/Channel/EditChannel.js b/web/src/pages/Channel/EditChannel.js index bfc611fe..037f5e18 100644 --- a/web/src/pages/Channel/EditChannel.js +++ b/web/src/pages/Channel/EditChannel.js @@ -7,7 +7,8 @@ import { showError, showInfo, showSuccess, - verifyJSON + showWarning, + verifyJSON, } from '../../helpers'; import { CHANNEL_OPTIONS } from '../../constants'; import Title from '@douyinfe/semi-ui/lib/es/typography/title'; @@ -22,27 +23,24 @@ import { Select, TextArea, Checkbox, - Banner + Banner, + Modal, } from '@douyinfe/semi-ui'; -import { Divider } from 'semantic-ui-react'; import { getChannelModels, loadChannelModels } from '../../components/utils.js'; -import axios from 'axios'; const MODEL_MAPPING_EXAMPLE = { - 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125' + 'gpt-3.5-turbo': 'gpt-3.5-turbo-0125', }; const STATUS_CODE_MAPPING_EXAMPLE = { - 400: '500' + 400: '500', }; const REGION_EXAMPLE = { - 'default': 'us-central1', - 'claude-3-5-sonnet-20240620': 'europe-west1' + default: 'us-central1', + 'claude-3-5-sonnet-20240620': 'europe-west1', }; -const fetchButtonTips = '1. 新建渠道时,请求通过当前浏览器发出;2. 编辑已有渠道,请求通过后端服务器发出'; - function type2secretPrompt(type) { // inputs.type === 15 ? '按照如下格式输入:APIKey|SecretKey' : (inputs.type === 18 ? '按照如下格式输入:APPID|APISecret|APIKey' : '请输入渠道对应的鉴权密钥') switch (type) { @@ -86,7 +84,7 @@ const EditChannel = (props) => { groups: ['default'], priority: 0, weight: 0, - tag: '' + tag: '', }; const [batch, setBatch] = useState(false); const [autoBan, setAutoBan] = useState(true); @@ -99,6 +97,17 @@ const EditChannel = (props) => { const [fullModels, setFullModels] = useState([]); const [customModel, setCustomModel] = useState(''); const handleInputChange = (name, value) => { + if (name === 'base_url' && value.endsWith('/v1')) { + Modal.confirm({ + title: '警告', + content: + '不需要在末尾加/v1,New API会自动处理,添加后可能导致请求失败,是否继续?', + onOk: () => { + setInputs((inputs) => ({ ...inputs, [name]: value })); + }, + }); + return; + } setInputs((inputs) => ({ ...inputs, [name]: value })); if (name === 'type') { let localModels = []; @@ -111,7 +120,7 @@ const EditChannel = (props) => { 'mj_blend', 'mj_upscale', 'mj_describe', - 'mj_uploads' + 'mj_uploads', ]; break; case 5: @@ -131,14 +140,11 @@ const EditChannel = (props) => { 'mj_high_variation', 'mj_low_variation', 'mj_pan', - 'mj_uploads' + 'mj_uploads', ]; break; case 36: - localModels = [ - 'suno_music', - 'suno_lyrics' - ]; + localModels = ['suno_music', 'suno_lyrics']; break; default: localModels = getChannelModels(value); @@ -174,7 +180,7 @@ const EditChannel = (props) => { data.model_mapping = JSON.stringify( JSON.parse(data.model_mapping), null, - 2 + 2, ); } setInputs(data); @@ -191,7 +197,6 @@ const EditChannel = (props) => { setLoading(false); }; - const fetchUpstreamModelList = async (name) => { // if (inputs['type'] !== 1) { // showError(t('仅支持 OpenAI 接口格式')); @@ -219,9 +224,9 @@ const EditChannel = (props) => { const res = await API.post('/api/channel/fetch_models', { base_url: inputs['base_url'], type: inputs['type'], - key: inputs['key'] + key: inputs['key'], }); - + if (res.data && res.data.success) { models.push(...res.data.data); } else { @@ -248,7 +253,7 @@ const EditChannel = (props) => { let res = await API.get(`/api/channel/models`); let localModelOptions = res.data.data.map((model) => ({ label: model.id, - value: model.id + value: model.id, })); setOriginModelOptions(localModelOptions); setFullModels(res.data.data.map((model) => model.id)); @@ -257,7 +262,7 @@ const EditChannel = (props) => { .filter((model) => { return model.id.startsWith('gpt-') || model.id.startsWith('text-'); }) - .map((model) => model.id) + .map((model) => model.id), ); } catch (error) { showError(error.message); @@ -273,8 +278,8 @@ const EditChannel = (props) => { setGroupOptions( res.data.data.map((group) => ({ label: group, - value: group - })) + value: group, + })), ); } catch (error) { showError(error.message); @@ -287,7 +292,7 @@ const EditChannel = (props) => { if (!localModelOptions.find((option) => option.label === model)) { localModelOptions.push({ label: model, - value: model + value: model, }); } }); @@ -298,7 +303,7 @@ const EditChannel = (props) => { fetchModels().then(); fetchGroups().then(); if (isEdit) { - loadChannel().then(() => {}); + loadChannel().then(() => { }); } else { setInputs(originInputs); let localModels = getChannelModels(inputs.type); @@ -324,7 +329,7 @@ const EditChannel = (props) => { if (localInputs.base_url && localInputs.base_url.endsWith('/')) { localInputs.base_url = localInputs.base_url.slice( 0, - localInputs.base_url.length - 1 + localInputs.base_url.length - 1, ); } if (localInputs.type === 18 && localInputs.other === '') { @@ -342,7 +347,7 @@ const EditChannel = (props) => { if (isEdit) { res = await API.put(`/api/channel/`, { ...localInputs, - id: parseInt(channelId) + id: parseInt(channelId), }); } else { res = await API.post(`/api/channel/`, localInputs); @@ -376,7 +381,7 @@ const EditChannel = (props) => { localModelOptions.push({ key: model, text: model, - value: model + value: model, }); } else if (model) { showError(t('某些模型已存在!')); @@ -391,14 +396,15 @@ const EditChannel = (props) => { handleInputChange('models', localModels); }; - return ( <> {isEdit ? t('更新渠道信息') : t('创建新的渠道')} + + {isEdit ? t('更新渠道信息') : t('创建新的渠道')} + } headerStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} bodyStyle={{ borderBottom: '1px solid var(--semi-color-border)' }} @@ -406,11 +412,11 @@ const EditChannel = (props) => { footer={
-