Upload folder using huggingface_hub
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .dockerignore +7 -0
- .env.example +67 -0
- .gitattributes +2 -0
- .github/FUNDING.yml +1 -0
- .github/ISSUE_TEMPLATE/bug_report.md +31 -0
- .github/ISSUE_TEMPLATE/config.yml +5 -0
- .github/ISSUE_TEMPLATE/feature_request.md +26 -0
- .github/workflows/changelog.yml +59 -0
- .github/workflows/docker.yml +92 -0
- .github/workflows/release.yml +202 -0
- .gitignore +18 -0
- CHANGELOG.md +0 -0
- CLAUDE.md +17 -0
- CODE_OF_CONDUCT.md +128 -0
- Dockerfile +35 -0
- LICENSE +674 -0
- README.md +10 -5
- VELOERA_PROJ +11 -0
- VERSION +0 -0
- bin/migration_v0.2-v0.3.sql +6 -0
- bin/migration_v0.3-v0.4.sql +17 -0
- bin/time_test.sh +40 -0
- common/constants.go +330 -0
- common/crypto.go +54 -0
- common/custom-event.go +98 -0
- common/database.go +24 -0
- common/email-outlook-auth.go +56 -0
- common/email.go +105 -0
- common/embed-file-system.go +48 -0
- common/env.go +54 -0
- common/gin.go +61 -0
- common/go-channel.go +69 -0
- common/gopool.go +40 -0
- common/init.go +114 -0
- common/json.go +34 -0
- common/logger.go +137 -0
- common/pprof.go +60 -0
- common/rate-limit.go +86 -0
- common/redis.go +336 -0
- common/str.go +100 -0
- common/topup-ratio.go +49 -0
- common/utils.go +403 -0
- common/validate.go +25 -0
- common/verification.go +93 -0
- constant/azure.go +21 -0
- constant/cache_key.go +39 -0
- constant/channel_setting.go +25 -0
- constant/context_key.go +26 -0
- constant/env.go +68 -0
- constant/finish_reason.go +25 -0
.dockerignore
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.github
|
| 2 |
+
.git
|
| 3 |
+
*.md
|
| 4 |
+
.vscode
|
| 5 |
+
.gitignore
|
| 6 |
+
Makefile
|
| 7 |
+
docs
|
.env.example
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 端口号
|
| 2 |
+
# PORT=3000
|
| 3 |
+
# 前端基础URL
|
| 4 |
+
# FRONTEND_BASE_URL=https://your-frontend-url.com
|
| 5 |
+
|
| 6 |
+
|
| 7 |
+
# 调试相关配置
|
| 8 |
+
# 启用pprof
|
| 9 |
+
# ENABLE_PPROF=true
|
| 10 |
+
|
| 11 |
+
# 数据库相关配置
|
| 12 |
+
# 数据库连接字符串
|
| 13 |
+
# SQL_DSN=user:password@tcp(127.0.0.1:3306)/dbname?parseTime=true
|
| 14 |
+
# 日志数据库连接字符串
|
| 15 |
+
# LOG_SQL_DSN=user:password@tcp(127.0.0.1:3306)/logdb?parseTime=true
|
| 16 |
+
# SQLite数据库路径
|
| 17 |
+
# SQLITE_PATH=/path/to/sqlite.db
|
| 18 |
+
# 数据库最大空闲连接数
|
| 19 |
+
# SQL_MAX_IDLE_CONNS=100
|
| 20 |
+
# 数据库最大打开连接数
|
| 21 |
+
# SQL_MAX_OPEN_CONNS=1000
|
| 22 |
+
# 数据库连接最大生命周期(秒)
|
| 23 |
+
# SQL_MAX_LIFETIME=60
|
| 24 |
+
|
| 25 |
+
|
| 26 |
+
# 缓存相关配置
|
| 27 |
+
# Redis连接字符串
|
| 28 |
+
# REDIS_CONN_STRING=redis://user:password@localhost:6379/0
|
| 29 |
+
# 同步频率(单位:秒)
|
| 30 |
+
# SYNC_FREQUENCY=60
|
| 31 |
+
# 内存缓存启用
|
| 32 |
+
# MEMORY_CACHE_ENABLED=true
|
| 33 |
+
# 渠道更新频率(单位:秒)
|
| 34 |
+
# CHANNEL_UPDATE_FREQUENCY=30
|
| 35 |
+
# 批量更新启用
|
| 36 |
+
# BATCH_UPDATE_ENABLED=true
|
| 37 |
+
# 批量更新间隔(单位:秒)
|
| 38 |
+
# BATCH_UPDATE_INTERVAL=5
|
| 39 |
+
|
| 40 |
+
# 任务和功能配置
|
| 41 |
+
# 更新任务启用
|
| 42 |
+
# UPDATE_TASK=true
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
# 会话密钥
|
| 46 |
+
# SESSION_SECRET=random_string
|
| 47 |
+
|
| 48 |
+
# 其他配置
|
| 49 |
+
# 渠道测试频率(单位:秒)
|
| 50 |
+
# CHANNEL_TEST_FREQUENCY=10
|
| 51 |
+
# 生成默认token
|
| 52 |
+
# GENERATE_DEFAULT_TOKEN=false
|
| 53 |
+
# Cohere 安全设置
|
| 54 |
+
# COHERE_SAFETY_SETTING=NONE
|
| 55 |
+
# 是否统计图片token
|
| 56 |
+
# GET_MEDIA_TOKEN=true
|
| 57 |
+
# 是否在非流(stream=false)情况下统计图片token
|
| 58 |
+
# GET_MEDIA_TOKEN_NOT_STREAM=true
|
| 59 |
+
# 设置 Dify 渠道是否输出工作流和节点信息到客户端
|
| 60 |
+
# DIFY_DEBUG=true
|
| 61 |
+
# 设置流式一次回复的超时时间
|
| 62 |
+
# STREAMING_TIMEOUT=90
|
| 63 |
+
|
| 64 |
+
|
| 65 |
+
# 节点类型
|
| 66 |
+
# 如果是主节点则为master
|
| 67 |
+
# NODE_TYPE=master
|
.gitattributes
CHANGED
|
@@ -33,3 +33,5 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
| 33 |
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
| 36 |
+
web/bun.lockb filter=lfs diff=lfs merge=lfs -text
|
| 37 |
+
web/public/ratio.png filter=lfs diff=lfs merge=lfs -text
|
.github/FUNDING.yml
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
ko_fi: isplexus
|
.github/ISSUE_TEMPLATE/bug_report.md
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: 报告问题
|
| 3 |
+
about: 使用简练详细的语言描述你遇到的问题
|
| 4 |
+
title: ''
|
| 5 |
+
labels: bug
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
**例行检查**
|
| 11 |
+
|
| 12 |
+
[//]: # (方框内删除已有的空格,填 x 号)
|
| 13 |
+
|
| 14 |
+
**必选**
|
| 15 |
+
+ [ ] 我已确认目前没有类似 issue
|
| 16 |
+
+ [ ] 我已确认我已升级到最新版本
|
| 17 |
+
+ [ ] 我已完整查看过项目 README,尤其是常见问题部分
|
| 18 |
+
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
|
| 19 |
+
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
|
| 20 |
+
|
| 21 |
+
**可选**
|
| 22 |
+
+ [ ] 我有能力解决这个问题并且提交 pull request
|
| 23 |
+
|
| 24 |
+
**问题描述**
|
| 25 |
+
|
| 26 |
+
**复现步骤**
|
| 27 |
+
|
| 28 |
+
**预期结果**
|
| 29 |
+
|
| 30 |
+
**相关截图**
|
| 31 |
+
如果没有的话,请删除此节。
|
.github/ISSUE_TEMPLATE/config.yml
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
blank_issues_enabled: false
|
| 2 |
+
contact_links:
|
| 3 |
+
- name: Discussions
|
| 4 |
+
url: https://github.com/Veloera/Veloera/discussions
|
| 5 |
+
about: 如果这不是一个问题/功能请求, 请在 Discussions 提出
|
.github/ISSUE_TEMPLATE/feature_request.md
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
name: 功能请求
|
| 3 |
+
about: 使用简练详细的语言描述希望加入的新功能
|
| 4 |
+
title: ''
|
| 5 |
+
labels: enhancement
|
| 6 |
+
assignees: ''
|
| 7 |
+
|
| 8 |
+
---
|
| 9 |
+
|
| 10 |
+
**例行检查**
|
| 11 |
+
|
| 12 |
+
[//]: # (方框内删除已有的空格,填 x 号)
|
| 13 |
+
|
| 14 |
+
**必选**
|
| 15 |
+
+ [ ] 我已确认目前没有类似 issue
|
| 16 |
+
+ [ ] 我已确认我已升级到最新版本
|
| 17 |
+
+ [ ] 我已完整查看过项目 README,已确定现有版本无法满足需求
|
| 18 |
+
+ [ ] 我理解并愿意跟进此 issue,协助测试和提供反馈
|
| 19 |
+
+ [ ] 我理解并认可上述内容,并理解项目维护者精力有限,**不遵循规则的 issue 可能会被无视或直接关闭**
|
| 20 |
+
|
| 21 |
+
**可选**
|
| 22 |
+
+ [ ] 我有能力解决这个问题并且提交 pull request
|
| 23 |
+
|
| 24 |
+
**功能描述**
|
| 25 |
+
|
| 26 |
+
**应用场景**
|
.github/workflows/changelog.yml
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Generate Release Notes
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
tags:
|
| 6 |
+
- '*' # 每次有新的tag推送到远程仓库时触发
|
| 7 |
+
workflow_dispatch: # 允许手动触发
|
| 8 |
+
|
| 9 |
+
env:
|
| 10 |
+
# AI 配置,确保在 GitHub Secrets 中设置
|
| 11 |
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
| 12 |
+
OPENAI_API_BASE_URL: ${{ secrets.OPENAI_API_BASE_URL }}
|
| 13 |
+
OPENAI_MODEL: gpt-4.1-mini # 可以根据需要修改模型
|
| 14 |
+
|
| 15 |
+
jobs:
|
| 16 |
+
generate_and_push:
|
| 17 |
+
runs-on: ubuntu-latest
|
| 18 |
+
|
| 19 |
+
permissions:
|
| 20 |
+
contents: write # 允许写入文件到仓库
|
| 21 |
+
|
| 22 |
+
steps:
|
| 23 |
+
- name: Checkout repository
|
| 24 |
+
uses: actions/checkout@v4
|
| 25 |
+
with:
|
| 26 |
+
# Fetch all tags and history for accurate git log
|
| 27 |
+
fetch-depth: 0
|
| 28 |
+
|
| 29 |
+
- name: Set up Python
|
| 30 |
+
uses: actions/setup-python@v5
|
| 31 |
+
with:
|
| 32 |
+
python-version: '3.x'
|
| 33 |
+
|
| 34 |
+
- name: Install Python dependencies
|
| 35 |
+
run: pip install openai
|
| 36 |
+
|
| 37 |
+
- name: Run release notes generator script
|
| 38 |
+
id: generate_notes # 添加一个id以便引用脚本的输出
|
| 39 |
+
run: python scripts/release_notes_generator.py
|
| 40 |
+
env:
|
| 41 |
+
GITHUB_REPOSITORY: ${{ github.repository }} # 传递仓库信息给Python脚本
|
| 42 |
+
|
| 43 |
+
- name: Commit and push release notes
|
| 44 |
+
# 仅当生成了文件时才执行
|
| 45 |
+
if: success() && steps.generate_notes.outputs.release_notes_file != ''
|
| 46 |
+
run: |
|
| 47 |
+
git config user.name "github-actions[bot]"
|
| 48 |
+
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
|
| 49 |
+
|
| 50 |
+
# 获取脚本输出的tag名称和文件名
|
| 51 |
+
TAG_NAME="${{ steps.generate_notes.outputs.tag_name }}"
|
| 52 |
+
RELEASE_FILE="${{ steps.generate_notes.outputs.release_notes_file }}"
|
| 53 |
+
|
| 54 |
+
echo "Committing release notes for tag: $TAG_NAME"
|
| 55 |
+
echo "File path: $RELEASE_FILE"
|
| 56 |
+
|
| 57 |
+
git add "$RELEASE_FILE"
|
| 58 |
+
git commit -m "docs(release): Add release notes for $TAG_NAME" || echo "No changes to commit"
|
| 59 |
+
git push origin HEAD:main
|
.github/workflows/docker.yml
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Publish Docker images
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
tags:
|
| 6 |
+
- '*'
|
| 7 |
+
workflow_dispatch:
|
| 8 |
+
inputs:
|
| 9 |
+
reason:
|
| 10 |
+
description: 'Reason for manual trigger'
|
| 11 |
+
required: false
|
| 12 |
+
|
| 13 |
+
jobs:
|
| 14 |
+
push_to_registries:
|
| 15 |
+
name: Build and push Docker images
|
| 16 |
+
runs-on: ubuntu-latest
|
| 17 |
+
permissions:
|
| 18 |
+
packages: write
|
| 19 |
+
contents: read
|
| 20 |
+
steps:
|
| 21 |
+
- name: Checkout code
|
| 22 |
+
uses: actions/checkout@v4
|
| 23 |
+
with:
|
| 24 |
+
fetch-depth: 0
|
| 25 |
+
|
| 26 |
+
- name: Save version info
|
| 27 |
+
run: |
|
| 28 |
+
git describe --tags > VERSION
|
| 29 |
+
|
| 30 |
+
- name: Set up QEMU
|
| 31 |
+
uses: docker/setup-qemu-action@v3
|
| 32 |
+
|
| 33 |
+
- name: Set up Docker Buildx
|
| 34 |
+
uses: docker/setup-buildx-action@v3
|
| 35 |
+
with:
|
| 36 |
+
# Use buildx driver with multiple concurrent builders
|
| 37 |
+
driver-opts: |
|
| 38 |
+
network=host
|
| 39 |
+
# Enable BuildKit cache
|
| 40 |
+
buildkitd-flags: --debug
|
| 41 |
+
|
| 42 |
+
- name: Cache Docker layers
|
| 43 |
+
uses: actions/cache@v3
|
| 44 |
+
with:
|
| 45 |
+
path: /tmp/.buildx-cache
|
| 46 |
+
key: ${{ runner.os }}-buildx-${{ github.sha }}
|
| 47 |
+
restore-keys: |
|
| 48 |
+
${{ runner.os }}-buildx-
|
| 49 |
+
|
| 50 |
+
- name: Login to Container registry
|
| 51 |
+
uses: docker/login-action@v3
|
| 52 |
+
with:
|
| 53 |
+
registry: ghcr.io
|
| 54 |
+
username: ${{ github.actor }}
|
| 55 |
+
password: ${{ secrets.GITHUB_TOKEN }}
|
| 56 |
+
|
| 57 |
+
- name: Extract metadata for Docker
|
| 58 |
+
id: meta
|
| 59 |
+
uses: docker/metadata-action@v5
|
| 60 |
+
with:
|
| 61 |
+
images: |
|
| 62 |
+
ghcr.io/${{ github.repository }}
|
| 63 |
+
tags: |
|
| 64 |
+
type=ref,event=tag
|
| 65 |
+
type=raw,value=latest,enable=${{ !endsWith(github.ref, '-alpha') && !endsWith(github.ref, '-beta') }}
|
| 66 |
+
type=raw,value=alpha,enable=${{ endsWith(github.ref, '-alpha') }}
|
| 67 |
+
type=raw,value=beta,enable=${{ endsWith(github.ref, '-beta') }}
|
| 68 |
+
|
| 69 |
+
- name: Build and push Docker images
|
| 70 |
+
uses: docker/build-push-action@v5
|
| 71 |
+
with:
|
| 72 |
+
context: .
|
| 73 |
+
platforms: linux/amd64,linux/arm64
|
| 74 |
+
push: true
|
| 75 |
+
tags: ${{ steps.meta.outputs.tags }}
|
| 76 |
+
labels: ${{ steps.meta.outputs.labels }}
|
| 77 |
+
# Use cache for faster builds
|
| 78 |
+
cache-from: type=local,src=/tmp/.buildx-cache
|
| 79 |
+
cache-to: type=local,dest=/tmp/.buildx-cache-new,mode=max
|
| 80 |
+
# Optimize build performance
|
| 81 |
+
build-args: |
|
| 82 |
+
BUILDKIT_INLINE_CACHE=1
|
| 83 |
+
# Parallel processing
|
| 84 |
+
builder: ${{ steps.buildx.outputs.name }}
|
| 85 |
+
# Set output to suppress verbose logs
|
| 86 |
+
outputs: type=image,name=target,annotation-index.org.opencontainers.image.description=Docker image for ${{ github.repository }}
|
| 87 |
+
|
| 88 |
+
# Temp fix for https://github.com/docker/build-push-action/issues/252
|
| 89 |
+
- name: Move cache
|
| 90 |
+
run: |
|
| 91 |
+
rm -rf /tmp/.buildx-cache
|
| 92 |
+
mv /tmp/.buildx-cache-new /tmp/.buildx-cache
|
.github/workflows/release.yml
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Create Release
|
| 2 |
+
|
| 3 |
+
permissions:
|
| 4 |
+
contents: write
|
| 5 |
+
|
| 6 |
+
on:
|
| 7 |
+
push:
|
| 8 |
+
tags:
|
| 9 |
+
- '*'
|
| 10 |
+
workflow_dispatch:
|
| 11 |
+
inputs:
|
| 12 |
+
tag_name:
|
| 13 |
+
description: 'Tag name for release (e.g. v1.0.0)'
|
| 14 |
+
required: true
|
| 15 |
+
type: string
|
| 16 |
+
|
| 17 |
+
jobs:
|
| 18 |
+
build-linux:
|
| 19 |
+
name: Build Linux Version
|
| 20 |
+
runs-on: ubuntu-latest
|
| 21 |
+
steps:
|
| 22 |
+
- name: Checkout code
|
| 23 |
+
uses: actions/checkout@v4
|
| 24 |
+
with:
|
| 25 |
+
fetch-depth: 1
|
| 26 |
+
|
| 27 |
+
- name: Install pnpm
|
| 28 |
+
uses: pnpm/action-setup@v4
|
| 29 |
+
with:
|
| 30 |
+
version: 10
|
| 31 |
+
run_install: false
|
| 32 |
+
|
| 33 |
+
- name: Setup Node.js
|
| 34 |
+
uses: actions/setup-node@v3
|
| 35 |
+
with:
|
| 36 |
+
node-version: 20
|
| 37 |
+
cache: 'pnpm'
|
| 38 |
+
cache-dependency-path: 'web/pnpm-lock.yaml'
|
| 39 |
+
|
| 40 |
+
- name: Build Frontend
|
| 41 |
+
env:
|
| 42 |
+
CI: ""
|
| 43 |
+
run: |
|
| 44 |
+
cd web
|
| 45 |
+
pnpm install
|
| 46 |
+
VITE_REACT_APP_VERSION=$(git describe --tags) pnpm run build
|
| 47 |
+
cd ..
|
| 48 |
+
|
| 49 |
+
- name: Setup Go
|
| 50 |
+
uses: actions/setup-go@v5
|
| 51 |
+
with:
|
| 52 |
+
go-version-file: 'go.mod'
|
| 53 |
+
cache-dependency-path: '**/go.sum'
|
| 54 |
+
|
| 55 |
+
- name: Build Backend (amd64)
|
| 56 |
+
run: |
|
| 57 |
+
go build -ldflags "-s -w -X 'veloera/common.Version=$(git describe --tags)' -extldflags '-static'" -o veloera-linux
|
| 58 |
+
|
| 59 |
+
- name: Build Backend (arm64)
|
| 60 |
+
run: |
|
| 61 |
+
sudo apt-get update
|
| 62 |
+
DEBIAN_FRONTEND=noninteractive sudo apt-get install -y gcc-aarch64-linux-gnu
|
| 63 |
+
CC=aarch64-linux-gnu-gcc CGO_ENABLED=1 GOOS=linux GOARCH=arm64 go build -ldflags "-s -w -X 'veloera/common.Version=$(git describe --tags)' -extldflags '-static'" -o veloera-linux-arm64
|
| 64 |
+
|
| 65 |
+
- name: Upload Linux builds
|
| 66 |
+
uses: actions/upload-artifact@v4
|
| 67 |
+
with:
|
| 68 |
+
name: linux-builds
|
| 69 |
+
path: |
|
| 70 |
+
veloera-linux
|
| 71 |
+
veloera-linux-arm64
|
| 72 |
+
retention-days: 1
|
| 73 |
+
|
| 74 |
+
build-windows:
|
| 75 |
+
name: Build Windows Version
|
| 76 |
+
runs-on: windows-latest
|
| 77 |
+
defaults:
|
| 78 |
+
run:
|
| 79 |
+
shell: bash
|
| 80 |
+
steps:
|
| 81 |
+
- name: Checkout code
|
| 82 |
+
uses: actions/checkout@v4
|
| 83 |
+
with:
|
| 84 |
+
fetch-depth: 1
|
| 85 |
+
|
| 86 |
+
- name: Install pnpm
|
| 87 |
+
uses: pnpm/action-setup@v4
|
| 88 |
+
with:
|
| 89 |
+
version: 10
|
| 90 |
+
run_install: false
|
| 91 |
+
|
| 92 |
+
- name: Setup Node.js
|
| 93 |
+
uses: actions/setup-node@v3
|
| 94 |
+
with:
|
| 95 |
+
node-version: 20
|
| 96 |
+
cache: 'pnpm'
|
| 97 |
+
cache-dependency-path: 'web/pnpm-lock.yaml'
|
| 98 |
+
|
| 99 |
+
- name: Build Frontend
|
| 100 |
+
env:
|
| 101 |
+
CI: ""
|
| 102 |
+
run: |
|
| 103 |
+
cd web
|
| 104 |
+
pnpm install
|
| 105 |
+
VITE_REACT_APP_VERSION=$(git describe --tags) pnpm run build
|
| 106 |
+
cd ..
|
| 107 |
+
|
| 108 |
+
- name: Setup Go
|
| 109 |
+
uses: actions/setup-go@v5
|
| 110 |
+
with:
|
| 111 |
+
go-version-file: 'go.mod'
|
| 112 |
+
cache-dependency-path: '**/go.sum'
|
| 113 |
+
|
| 114 |
+
- name: Build Backend
|
| 115 |
+
run: |
|
| 116 |
+
go build -ldflags "-s -w -X 'veloera/common.Version=$(git describe --tags)'" -o veloera-windows.exe
|
| 117 |
+
|
| 118 |
+
- name: Upload Windows builds
|
| 119 |
+
uses: actions/upload-artifact@v4
|
| 120 |
+
with:
|
| 121 |
+
name: windows-builds
|
| 122 |
+
path: veloera-windows.exe
|
| 123 |
+
retention-days: 1
|
| 124 |
+
|
| 125 |
+
build-macos:
|
| 126 |
+
name: Build macOS Version
|
| 127 |
+
runs-on: macos-latest
|
| 128 |
+
steps:
|
| 129 |
+
- name: Checkout code
|
| 130 |
+
uses: actions/checkout@v4
|
| 131 |
+
with:
|
| 132 |
+
fetch-depth: 1
|
| 133 |
+
|
| 134 |
+
- name: Install pnpm
|
| 135 |
+
uses: pnpm/action-setup@v4
|
| 136 |
+
with:
|
| 137 |
+
version: 10
|
| 138 |
+
run_install: false
|
| 139 |
+
|
| 140 |
+
- name: Setup Node.js
|
| 141 |
+
uses: actions/setup-node@v3
|
| 142 |
+
with:
|
| 143 |
+
node-version: 20
|
| 144 |
+
cache: 'pnpm'
|
| 145 |
+
cache-dependency-path: 'web/pnpm-lock.yaml'
|
| 146 |
+
|
| 147 |
+
- name: Build Frontend
|
| 148 |
+
env:
|
| 149 |
+
CI: ""
|
| 150 |
+
run: |
|
| 151 |
+
cd web
|
| 152 |
+
pnpm install
|
| 153 |
+
VITE_REACT_APP_VERSION=$(git describe --tags) pnpm run build
|
| 154 |
+
cd ..
|
| 155 |
+
|
| 156 |
+
- name: Setup Go
|
| 157 |
+
uses: actions/setup-go@v5
|
| 158 |
+
with:
|
| 159 |
+
go-version-file: 'go.mod'
|
| 160 |
+
cache-dependency-path: '**/go.sum'
|
| 161 |
+
|
| 162 |
+
- name: Build Backend
|
| 163 |
+
run: |
|
| 164 |
+
go build -ldflags "-X 'veloera/common.Version=$(git describe --tags)'" -o veloera-macos
|
| 165 |
+
|
| 166 |
+
- name: Upload macOS builds
|
| 167 |
+
uses: actions/upload-artifact@v4
|
| 168 |
+
with:
|
| 169 |
+
name: macos-builds
|
| 170 |
+
path: veloera-macos
|
| 171 |
+
retention-days: 1
|
| 172 |
+
|
| 173 |
+
create-release:
|
| 174 |
+
name: Create and Publish Release
|
| 175 |
+
needs: [build-linux, build-windows, build-macos]
|
| 176 |
+
runs-on: ubuntu-latest
|
| 177 |
+
if: startsWith(github.ref, 'refs/tags/') || github.event_name == 'workflow_dispatch'
|
| 178 |
+
steps:
|
| 179 |
+
- name: Checkout code
|
| 180 |
+
uses: actions/checkout@v4
|
| 181 |
+
with:
|
| 182 |
+
fetch-depth: 1
|
| 183 |
+
|
| 184 |
+
- name: Download all build artifacts
|
| 185 |
+
uses: actions/download-artifact@v4
|
| 186 |
+
|
| 187 |
+
- name: Display downloaded files
|
| 188 |
+
run: ls -R
|
| 189 |
+
|
| 190 |
+
- name: Create Release
|
| 191 |
+
id: create_release
|
| 192 |
+
uses: softprops/action-gh-release@v1
|
| 193 |
+
with:
|
| 194 |
+
files: |
|
| 195 |
+
linux-builds/veloera-linux
|
| 196 |
+
linux-builds/veloera-linux-arm64
|
| 197 |
+
windows-builds/veloera-windows.exe
|
| 198 |
+
macos-builds/veloera-macos
|
| 199 |
+
draft: false
|
| 200 |
+
generate_release_notes: true
|
| 201 |
+
env:
|
| 202 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
.gitignore
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.idea
|
| 2 |
+
.vscode
|
| 3 |
+
upload
|
| 4 |
+
*.exe
|
| 5 |
+
*.db
|
| 6 |
+
build
|
| 7 |
+
*.db-journal
|
| 8 |
+
logs
|
| 9 |
+
web/dist
|
| 10 |
+
.env
|
| 11 |
+
veloera
|
| 12 |
+
one-api
|
| 13 |
+
.DS_Store
|
| 14 |
+
tiktoken_cache
|
| 15 |
+
.aider*
|
| 16 |
+
.history/
|
| 17 |
+
.kiro/
|
| 18 |
+
.venv
|
CHANGELOG.md
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
CLAUDE.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# CLAUDE.md
|
| 2 |
+
|
| 3 |
+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
| 4 |
+
|
| 5 |
+
## Build & Test Commands
|
| 6 |
+
- Build frontend: `cd web && pnpm build`
|
| 7 |
+
- Build backend: `go build -o main`
|
| 8 |
+
- Run dev server: `cd web && pnpm build && cd .. && go run main.go`
|
| 9 |
+
|
| 10 |
+
## Code Style Guidelines
|
| 11 |
+
- **Go Style**: Follow standard Go conventions with camelCase for unexported and PascalCase for exported
|
| 12 |
+
- **Error Handling**: Use package errors and common logging utilities in common/logger.go
|
| 13 |
+
- **Frontend**: React with Semi UI components; use single quotes for strings/JSX
|
| 14 |
+
- **Types**: Define structured DTOs in dto/ package; use strong typing
|
| 15 |
+
- **Imports**: Group standard library, external packages, and internal packages
|
| 16 |
+
- **Formatting**: Use gofmt for Go code, prettier for frontend (configured in package.json)
|
| 17 |
+
- **File Organization**: Group related functionality in packages following project structure
|
CODE_OF_CONDUCT.md
ADDED
|
@@ -0,0 +1,128 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Contributor Covenant Code of Conduct
|
| 2 |
+
|
| 3 |
+
## Our Pledge
|
| 4 |
+
|
| 5 |
+
We as members, contributors, and leaders pledge to make participation in our
|
| 6 |
+
community a harassment-free experience for everyone, regardless of age, body
|
| 7 |
+
size, visible or invisible disability, ethnicity, sex characteristics, gender
|
| 8 |
+
identity and expression, level of experience, education, socio-economic status,
|
| 9 |
+
nationality, personal appearance, race, religion, or sexual identity
|
| 10 |
+
and orientation.
|
| 11 |
+
|
| 12 |
+
We pledge to act and interact in ways that contribute to an open, welcoming,
|
| 13 |
+
diverse, inclusive, and healthy community.
|
| 14 |
+
|
| 15 |
+
## Our Standards
|
| 16 |
+
|
| 17 |
+
Examples of behavior that contributes to a positive environment for our
|
| 18 |
+
community include:
|
| 19 |
+
|
| 20 |
+
* Demonstrating empathy and kindness toward other people
|
| 21 |
+
* Being respectful of differing opinions, viewpoints, and experiences
|
| 22 |
+
* Giving and gracefully accepting constructive feedback
|
| 23 |
+
* Accepting responsibility and apologizing to those affected by our mistakes,
|
| 24 |
+
and learning from the experience
|
| 25 |
+
* Focusing on what is best not just for us as individuals, but for the
|
| 26 |
+
overall community
|
| 27 |
+
|
| 28 |
+
Examples of unacceptable behavior include:
|
| 29 |
+
|
| 30 |
+
* The use of sexualized language or imagery, and sexual attention or
|
| 31 |
+
advances of any kind
|
| 32 |
+
* Trolling, insulting or derogatory comments, and personal or political attacks
|
| 33 |
+
* Public or private harassment
|
| 34 |
+
* Publishing others' private information, such as a physical or email
|
| 35 |
+
address, without their explicit permission
|
| 36 |
+
* Other conduct which could reasonably be considered inappropriate in a
|
| 37 |
+
professional setting
|
| 38 |
+
|
| 39 |
+
## Enforcement Responsibilities
|
| 40 |
+
|
| 41 |
+
Community leaders are responsible for clarifying and enforcing our standards of
|
| 42 |
+
acceptable behavior and will take appropriate and fair corrective action in
|
| 43 |
+
response to any behavior that they deem inappropriate, threatening, offensive,
|
| 44 |
+
or harmful.
|
| 45 |
+
|
| 46 |
+
Community leaders have the right and responsibility to remove, edit, or reject
|
| 47 |
+
comments, commits, code, wiki edits, issues, and other contributions that are
|
| 48 |
+
not aligned to this Code of Conduct, and will communicate reasons for moderation
|
| 49 |
+
decisions when appropriate.
|
| 50 |
+
|
| 51 |
+
## Scope
|
| 52 |
+
|
| 53 |
+
This Code of Conduct applies within all community spaces, and also applies when
|
| 54 |
+
an individual is officially representing the community in public spaces.
|
| 55 |
+
Examples of representing our community include using an official e-mail address,
|
| 56 |
+
posting via an official social media account, or acting as an appointed
|
| 57 |
+
representative at an online or offline event.
|
| 58 |
+
|
| 59 |
+
## Enforcement
|
| 60 |
+
|
| 61 |
+
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
| 62 |
+
reported to the community leaders responsible for enforcement at
|
| 63 |
+
press@mail.tethysplex.com.
|
| 64 |
+
All complaints will be reviewed and investigated promptly and fairly.
|
| 65 |
+
|
| 66 |
+
All community leaders are obligated to respect the privacy and security of the
|
| 67 |
+
reporter of any incident.
|
| 68 |
+
|
| 69 |
+
## Enforcement Guidelines
|
| 70 |
+
|
| 71 |
+
Community leaders will follow these Community Impact Guidelines in determining
|
| 72 |
+
the consequences for any action they deem in violation of this Code of Conduct:
|
| 73 |
+
|
| 74 |
+
### 1. Correction
|
| 75 |
+
|
| 76 |
+
**Community Impact**: Use of inappropriate language or other behavior deemed
|
| 77 |
+
unprofessional or unwelcome in the community.
|
| 78 |
+
|
| 79 |
+
**Consequence**: A private, written warning from community leaders, providing
|
| 80 |
+
clarity around the nature of the violation and an explanation of why the
|
| 81 |
+
behavior was inappropriate. A public apology may be requested.
|
| 82 |
+
|
| 83 |
+
### 2. Warning
|
| 84 |
+
|
| 85 |
+
**Community Impact**: A violation through a single incident or series
|
| 86 |
+
of actions.
|
| 87 |
+
|
| 88 |
+
**Consequence**: A warning with consequences for continued behavior. No
|
| 89 |
+
interaction with the people involved, including unsolicited interaction with
|
| 90 |
+
those enforcing the Code of Conduct, for a specified period of time. This
|
| 91 |
+
includes avoiding interactions in community spaces as well as external channels
|
| 92 |
+
like social media. Violating these terms may lead to a temporary or
|
| 93 |
+
permanent ban.
|
| 94 |
+
|
| 95 |
+
### 3. Temporary Ban
|
| 96 |
+
|
| 97 |
+
**Community Impact**: A serious violation of community standards, including
|
| 98 |
+
sustained inappropriate behavior.
|
| 99 |
+
|
| 100 |
+
**Consequence**: A temporary ban from any sort of interaction or public
|
| 101 |
+
communication with the community for a specified period of time. No public or
|
| 102 |
+
private interaction with the people involved, including unsolicited interaction
|
| 103 |
+
with those enforcing the Code of Conduct, is allowed during this period.
|
| 104 |
+
Violating these terms may lead to a permanent ban.
|
| 105 |
+
|
| 106 |
+
### 4. Permanent Ban
|
| 107 |
+
|
| 108 |
+
**Community Impact**: Demonstrating a pattern of violation of community
|
| 109 |
+
standards, including sustained inappropriate behavior, harassment of an
|
| 110 |
+
individual, or aggression toward or disparagement of classes of individuals.
|
| 111 |
+
|
| 112 |
+
**Consequence**: A permanent ban from any sort of public interaction within
|
| 113 |
+
the community.
|
| 114 |
+
|
| 115 |
+
## Attribution
|
| 116 |
+
|
| 117 |
+
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
|
| 118 |
+
version 2.0, available at
|
| 119 |
+
https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
|
| 120 |
+
|
| 121 |
+
Community Impact Guidelines were inspired by [Mozilla's code of conduct
|
| 122 |
+
enforcement ladder](https://github.com/mozilla/diversity).
|
| 123 |
+
|
| 124 |
+
[homepage]: https://www.contributor-covenant.org
|
| 125 |
+
|
| 126 |
+
For answers to common questions about this code of conduct, see the FAQ at
|
| 127 |
+
https://www.contributor-covenant.org/faq. Translations are available at
|
| 128 |
+
https://www.contributor-covenant.org/translations.
|
Dockerfile
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
FROM oven/bun:latest AS builder
|
| 2 |
+
|
| 3 |
+
WORKDIR /build
|
| 4 |
+
COPY web/package.json .
|
| 5 |
+
RUN bun install
|
| 6 |
+
COPY ./web .
|
| 7 |
+
COPY ./VERSION .
|
| 8 |
+
RUN DISABLE_ESLINT_PLUGIN='true' VITE_REACT_APP_VERSION=$(cat VERSION) bun run build
|
| 9 |
+
|
| 10 |
+
FROM golang:alpine AS builder2
|
| 11 |
+
|
| 12 |
+
ENV GO111MODULE=on \
|
| 13 |
+
CGO_ENABLED=0 \
|
| 14 |
+
GOOS=linux
|
| 15 |
+
|
| 16 |
+
WORKDIR /build
|
| 17 |
+
|
| 18 |
+
ADD go.mod go.sum ./
|
| 19 |
+
RUN go mod download
|
| 20 |
+
|
| 21 |
+
COPY . .
|
| 22 |
+
COPY --from=builder /build/dist ./web/dist
|
| 23 |
+
RUN go build -ldflags "-s -w -X 'veloera/common.Version=$(cat VERSION)'" -o veloera
|
| 24 |
+
|
| 25 |
+
FROM alpine
|
| 26 |
+
|
| 27 |
+
RUN apk update \
|
| 28 |
+
&& apk upgrade \
|
| 29 |
+
&& apk add --no-cache ca-certificates tzdata ffmpeg \
|
| 30 |
+
&& update-ca-certificates
|
| 31 |
+
|
| 32 |
+
COPY --from=builder2 /build/veloera /
|
| 33 |
+
EXPOSE 3000
|
| 34 |
+
WORKDIR /data
|
| 35 |
+
ENTRYPOINT ["/veloera"]
|
LICENSE
ADDED
|
@@ -0,0 +1,674 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
GNU GENERAL PUBLIC LICENSE
|
| 2 |
+
Version 3, 29 June 2007
|
| 3 |
+
|
| 4 |
+
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
| 5 |
+
Everyone is permitted to copy and distribute verbatim copies
|
| 6 |
+
of this license document, but changing it is not allowed.
|
| 7 |
+
|
| 8 |
+
Preamble
|
| 9 |
+
|
| 10 |
+
The GNU General Public License is a free, copyleft license for
|
| 11 |
+
software and other kinds of works.
|
| 12 |
+
|
| 13 |
+
The licenses for most software and other practical works are designed
|
| 14 |
+
to take away your freedom to share and change the works. By contrast,
|
| 15 |
+
the GNU General Public License is intended to guarantee your freedom to
|
| 16 |
+
share and change all versions of a program--to make sure it remains free
|
| 17 |
+
software for all its users. We, the Free Software Foundation, use the
|
| 18 |
+
GNU General Public License for most of our software; it applies also to
|
| 19 |
+
any other work released this way by its authors. You can apply it to
|
| 20 |
+
your programs, too.
|
| 21 |
+
|
| 22 |
+
When we speak of free software, we are referring to freedom, not
|
| 23 |
+
price. Our General Public Licenses are designed to make sure that you
|
| 24 |
+
have the freedom to distribute copies of free software (and charge for
|
| 25 |
+
them if you wish), that you receive source code or can get it if you
|
| 26 |
+
want it, that you can change the software or use pieces of it in new
|
| 27 |
+
free programs, and that you know you can do these things.
|
| 28 |
+
|
| 29 |
+
To protect your rights, we need to prevent others from denying you
|
| 30 |
+
these rights or asking you to surrender the rights. Therefore, you have
|
| 31 |
+
certain responsibilities if you distribute copies of the software, or if
|
| 32 |
+
you modify it: responsibilities to respect the freedom of others.
|
| 33 |
+
|
| 34 |
+
For example, if you distribute copies of such a program, whether
|
| 35 |
+
gratis or for a fee, you must pass on to the recipients the same
|
| 36 |
+
freedoms that you received. You must make sure that they, too, receive
|
| 37 |
+
or can get the source code. And you must show them these terms so they
|
| 38 |
+
know their rights.
|
| 39 |
+
|
| 40 |
+
Developers that use the GNU GPL protect your rights with two steps:
|
| 41 |
+
(1) assert copyright on the software, and (2) offer you this License
|
| 42 |
+
giving you legal permission to copy, distribute and/or modify it.
|
| 43 |
+
|
| 44 |
+
For the developers' and authors' protection, the GPL clearly explains
|
| 45 |
+
that there is no warranty for this free software. For both users' and
|
| 46 |
+
authors' sake, the GPL requires that modified versions be marked as
|
| 47 |
+
changed, so that their problems will not be attributed erroneously to
|
| 48 |
+
authors of previous versions.
|
| 49 |
+
|
| 50 |
+
Some devices are designed to deny users access to install or run
|
| 51 |
+
modified versions of the software inside them, although the manufacturer
|
| 52 |
+
can do so. This is fundamentally incompatible with the aim of
|
| 53 |
+
protecting users' freedom to change the software. The systematic
|
| 54 |
+
pattern of such abuse occurs in the area of products for individuals to
|
| 55 |
+
use, which is precisely where it is most unacceptable. Therefore, we
|
| 56 |
+
have designed this version of the GPL to prohibit the practice for those
|
| 57 |
+
products. If such problems arise substantially in other domains, we
|
| 58 |
+
stand ready to extend this provision to those domains in future versions
|
| 59 |
+
of the GPL, as needed to protect the freedom of users.
|
| 60 |
+
|
| 61 |
+
Finally, every program is threatened constantly by software patents.
|
| 62 |
+
States should not allow patents to restrict development and use of
|
| 63 |
+
software on general-purpose computers, but in those that do, we wish to
|
| 64 |
+
avoid the special danger that patents applied to a free program could
|
| 65 |
+
make it effectively proprietary. To prevent this, the GPL assures that
|
| 66 |
+
patents cannot be used to render the program non-free.
|
| 67 |
+
|
| 68 |
+
The precise terms and conditions for copying, distribution and
|
| 69 |
+
modification follow.
|
| 70 |
+
|
| 71 |
+
TERMS AND CONDITIONS
|
| 72 |
+
|
| 73 |
+
0. Definitions.
|
| 74 |
+
|
| 75 |
+
"This License" refers to version 3 of the GNU General Public License.
|
| 76 |
+
|
| 77 |
+
"Copyright" also means copyright-like laws that apply to other kinds of
|
| 78 |
+
works, such as semiconductor masks.
|
| 79 |
+
|
| 80 |
+
"The Program" refers to any copyrightable work licensed under this
|
| 81 |
+
License. Each licensee is addressed as "you". "Licensees" and
|
| 82 |
+
"recipients" may be individuals or organizations.
|
| 83 |
+
|
| 84 |
+
To "modify" a work means to copy from or adapt all or part of the work
|
| 85 |
+
in a fashion requiring copyright permission, other than the making of an
|
| 86 |
+
exact copy. The resulting work is called a "modified version" of the
|
| 87 |
+
earlier work or a work "based on" the earlier work.
|
| 88 |
+
|
| 89 |
+
A "covered work" means either the unmodified Program or a work based
|
| 90 |
+
on the Program.
|
| 91 |
+
|
| 92 |
+
To "propagate" a work means to do anything with it that, without
|
| 93 |
+
permission, would make you directly or secondarily liable for
|
| 94 |
+
infringement under applicable copyright law, except executing it on a
|
| 95 |
+
computer or modifying a private copy. Propagation includes copying,
|
| 96 |
+
distribution (with or without modification), making available to the
|
| 97 |
+
public, and in some countries other activities as well.
|
| 98 |
+
|
| 99 |
+
To "convey" a work means any kind of propagation that enables other
|
| 100 |
+
parties to make or receive copies. Mere interaction with a user through
|
| 101 |
+
a computer network, with no transfer of a copy, is not conveying.
|
| 102 |
+
|
| 103 |
+
An interactive user interface displays "Appropriate Legal Notices"
|
| 104 |
+
to the extent that it includes a convenient and prominently visible
|
| 105 |
+
feature that (1) displays an appropriate copyright notice, and (2)
|
| 106 |
+
tells the user that there is no warranty for the work (except to the
|
| 107 |
+
extent that warranties are provided), that licensees may convey the
|
| 108 |
+
work under this License, and how to view a copy of this License. If
|
| 109 |
+
the interface presents a list of user commands or options, such as a
|
| 110 |
+
menu, a prominent item in the list meets this criterion.
|
| 111 |
+
|
| 112 |
+
1. Source Code.
|
| 113 |
+
|
| 114 |
+
The "source code" for a work means the preferred form of the work
|
| 115 |
+
for making modifications to it. "Object code" means any non-source
|
| 116 |
+
form of a work.
|
| 117 |
+
|
| 118 |
+
A "Standard Interface" means an interface that either is an official
|
| 119 |
+
standard defined by a recognized standards body, or, in the case of
|
| 120 |
+
interfaces specified for a particular programming language, one that
|
| 121 |
+
is widely used among developers working in that language.
|
| 122 |
+
|
| 123 |
+
The "System Libraries" of an executable work include anything, other
|
| 124 |
+
than the work as a whole, that (a) is included in the normal form of
|
| 125 |
+
packaging a Major Component, but which is not part of that Major
|
| 126 |
+
Component, and (b) serves only to enable use of the work with that
|
| 127 |
+
Major Component, or to implement a Standard Interface for which an
|
| 128 |
+
implementation is available to the public in source code form. A
|
| 129 |
+
"Major Component", in this context, means a major essential component
|
| 130 |
+
(kernel, window system, and so on) of the specific operating system
|
| 131 |
+
(if any) on which the executable work runs, or a compiler used to
|
| 132 |
+
produce the work, or an object code interpreter used to run it.
|
| 133 |
+
|
| 134 |
+
The "Corresponding Source" for a work in object code form means all
|
| 135 |
+
the source code needed to generate, install, and (for an executable
|
| 136 |
+
work) run the object code and to modify the work, including scripts to
|
| 137 |
+
control those activities. However, it does not include the work's
|
| 138 |
+
System Libraries, or general-purpose tools or generally available free
|
| 139 |
+
programs which are used unmodified in performing those activities but
|
| 140 |
+
which are not part of the work. For example, Corresponding Source
|
| 141 |
+
includes interface definition files associated with source files for
|
| 142 |
+
the work, and the source code for shared libraries and dynamically
|
| 143 |
+
linked subprograms that the work is specifically designed to require,
|
| 144 |
+
such as by intimate data communication or control flow between those
|
| 145 |
+
subprograms and other parts of the work.
|
| 146 |
+
|
| 147 |
+
The Corresponding Source need not include anything that users
|
| 148 |
+
can regenerate automatically from other parts of the Corresponding
|
| 149 |
+
Source.
|
| 150 |
+
|
| 151 |
+
The Corresponding Source for a work in source code form is that
|
| 152 |
+
same work.
|
| 153 |
+
|
| 154 |
+
2. Basic Permissions.
|
| 155 |
+
|
| 156 |
+
All rights granted under this License are granted for the term of
|
| 157 |
+
copyright on the Program, and are irrevocable provided the stated
|
| 158 |
+
conditions are met. This License explicitly affirms your unlimited
|
| 159 |
+
permission to run the unmodified Program. The output from running a
|
| 160 |
+
covered work is covered by this License only if the output, given its
|
| 161 |
+
content, constitutes a covered work. This License acknowledges your
|
| 162 |
+
rights of fair use or other equivalent, as provided by copyright law.
|
| 163 |
+
|
| 164 |
+
You may make, run and propagate covered works that you do not
|
| 165 |
+
convey, without conditions so long as your license otherwise remains
|
| 166 |
+
in force. You may convey covered works to others for the sole purpose
|
| 167 |
+
of having them make modifications exclusively for you, or provide you
|
| 168 |
+
with facilities for running those works, provided that you comply with
|
| 169 |
+
the terms of this License in conveying all material for which you do
|
| 170 |
+
not control copyright. Those thus making or running the covered works
|
| 171 |
+
for you must do so exclusively on your behalf, under your direction
|
| 172 |
+
and control, on terms that prohibit them from making any copies of
|
| 173 |
+
your copyrighted material outside their relationship with you.
|
| 174 |
+
|
| 175 |
+
Conveying under any other circumstances is permitted solely under
|
| 176 |
+
the conditions stated below. Sublicensing is not allowed; section 10
|
| 177 |
+
makes it unnecessary.
|
| 178 |
+
|
| 179 |
+
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
| 180 |
+
|
| 181 |
+
No covered work shall be deemed part of an effective technological
|
| 182 |
+
measure under any applicable law fulfilling obligations under article
|
| 183 |
+
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
| 184 |
+
similar laws prohibiting or restricting circumvention of such
|
| 185 |
+
measures.
|
| 186 |
+
|
| 187 |
+
When you convey a covered work, you waive any legal power to forbid
|
| 188 |
+
circumvention of technological measures to the extent such circumvention
|
| 189 |
+
is effected by exercising rights under this License with respect to
|
| 190 |
+
the covered work, and you disclaim any intention to limit operation or
|
| 191 |
+
modification of the work as a means of enforcing, against the work's
|
| 192 |
+
users, your or third parties' legal rights to forbid circumvention of
|
| 193 |
+
technological measures.
|
| 194 |
+
|
| 195 |
+
4. Conveying Verbatim Copies.
|
| 196 |
+
|
| 197 |
+
You may convey verbatim copies of the Program's source code as you
|
| 198 |
+
receive it, in any medium, provided that you conspicuously and
|
| 199 |
+
appropriately publish on each copy an appropriate copyright notice;
|
| 200 |
+
keep intact all notices stating that this License and any
|
| 201 |
+
non-permissive terms added in accord with section 7 apply to the code;
|
| 202 |
+
keep intact all notices of the absence of any warranty; and give all
|
| 203 |
+
recipients a copy of this License along with the Program.
|
| 204 |
+
|
| 205 |
+
You may charge any price or no price for each copy that you convey,
|
| 206 |
+
and you may offer support or warranty protection for a fee.
|
| 207 |
+
|
| 208 |
+
5. Conveying Modified Source Versions.
|
| 209 |
+
|
| 210 |
+
You may convey a work based on the Program, or the modifications to
|
| 211 |
+
produce it from the Program, in the form of source code under the
|
| 212 |
+
terms of section 4, provided that you also meet all of these conditions:
|
| 213 |
+
|
| 214 |
+
a) The work must carry prominent notices stating that you modified
|
| 215 |
+
it, and giving a relevant date.
|
| 216 |
+
|
| 217 |
+
b) The work must carry prominent notices stating that it is
|
| 218 |
+
released under this License and any conditions added under section
|
| 219 |
+
7. This requirement modifies the requirement in section 4 to
|
| 220 |
+
"keep intact all notices".
|
| 221 |
+
|
| 222 |
+
c) You must license the entire work, as a whole, under this
|
| 223 |
+
License to anyone who comes into possession of a copy. This
|
| 224 |
+
License will therefore apply, along with any applicable section 7
|
| 225 |
+
additional terms, to the whole of the work, and all its parts,
|
| 226 |
+
regardless of how they are packaged. This License gives no
|
| 227 |
+
permission to license the work in any other way, but it does not
|
| 228 |
+
invalidate such permission if you have separately received it.
|
| 229 |
+
|
| 230 |
+
d) If the work has interactive user interfaces, each must display
|
| 231 |
+
Appropriate Legal Notices; however, if the Program has interactive
|
| 232 |
+
interfaces that do not display Appropriate Legal Notices, your
|
| 233 |
+
work need not make them do so.
|
| 234 |
+
|
| 235 |
+
A compilation of a covered work with other separate and independent
|
| 236 |
+
works, which are not by their nature extensions of the covered work,
|
| 237 |
+
and which are not combined with it such as to form a larger program,
|
| 238 |
+
in or on a volume of a storage or distribution medium, is called an
|
| 239 |
+
"aggregate" if the compilation and its resulting copyright are not
|
| 240 |
+
used to limit the access or legal rights of the compilation's users
|
| 241 |
+
beyond what the individual works permit. Inclusion of a covered work
|
| 242 |
+
in an aggregate does not cause this License to apply to the other
|
| 243 |
+
parts of the aggregate.
|
| 244 |
+
|
| 245 |
+
6. Conveying Non-Source Forms.
|
| 246 |
+
|
| 247 |
+
You may convey a covered work in object code form under the terms
|
| 248 |
+
of sections 4 and 5, provided that you also convey the
|
| 249 |
+
machine-readable Corresponding Source under the terms of this License,
|
| 250 |
+
in one of these ways:
|
| 251 |
+
|
| 252 |
+
a) Convey the object code in, or embodied in, a physical product
|
| 253 |
+
(including a physical distribution medium), accompanied by the
|
| 254 |
+
Corresponding Source fixed on a durable physical medium
|
| 255 |
+
customarily used for software interchange.
|
| 256 |
+
|
| 257 |
+
b) Convey the object code in, or embodied in, a physical product
|
| 258 |
+
(including a physical distribution medium), accompanied by a
|
| 259 |
+
written offer, valid for at least three years and valid for as
|
| 260 |
+
long as you offer spare parts or customer support for that product
|
| 261 |
+
model, to give anyone who possesses the object code either (1) a
|
| 262 |
+
copy of the Corresponding Source for all the software in the
|
| 263 |
+
product that is covered by this License, on a durable physical
|
| 264 |
+
medium customarily used for software interchange, for a price no
|
| 265 |
+
more than your reasonable cost of physically performing this
|
| 266 |
+
conveying of source, or (2) access to copy the
|
| 267 |
+
Corresponding Source from a network server at no charge.
|
| 268 |
+
|
| 269 |
+
c) Convey individual copies of the object code with a copy of the
|
| 270 |
+
written offer to provide the Corresponding Source. This
|
| 271 |
+
alternative is allowed only occasionally and noncommercially, and
|
| 272 |
+
only if you received the object code with such an offer, in accord
|
| 273 |
+
with subsection 6b.
|
| 274 |
+
|
| 275 |
+
d) Convey the object code by offering access from a designated
|
| 276 |
+
place (gratis or for a charge), and offer equivalent access to the
|
| 277 |
+
Corresponding Source in the same way through the same place at no
|
| 278 |
+
further charge. You need not require recipients to copy the
|
| 279 |
+
Corresponding Source along with the object code. If the place to
|
| 280 |
+
copy the object code is a network server, the Corresponding Source
|
| 281 |
+
may be on a different server (operated by you or a third party)
|
| 282 |
+
that supports equivalent copying facilities, provided you maintain
|
| 283 |
+
clear directions next to the object code saying where to find the
|
| 284 |
+
Corresponding Source. Regardless of what server hosts the
|
| 285 |
+
Corresponding Source, you remain obligated to ensure that it is
|
| 286 |
+
available for as long as needed to satisfy these requirements.
|
| 287 |
+
|
| 288 |
+
e) Convey the object code using peer-to-peer transmission, provided
|
| 289 |
+
you inform other peers where the object code and Corresponding
|
| 290 |
+
Source of the work are being offered to the general public at no
|
| 291 |
+
charge under subsection 6d.
|
| 292 |
+
|
| 293 |
+
A separable portion of the object code, whose source code is excluded
|
| 294 |
+
from the Corresponding Source as a System Library, need not be
|
| 295 |
+
included in conveying the object code work.
|
| 296 |
+
|
| 297 |
+
A "User Product" is either (1) a "consumer product", which means any
|
| 298 |
+
tangible personal property which is normally used for personal, family,
|
| 299 |
+
or household purposes, or (2) anything designed or sold for incorporation
|
| 300 |
+
into a dwelling. In determining whether a product is a consumer product,
|
| 301 |
+
doubtful cases shall be resolved in favor of coverage. For a particular
|
| 302 |
+
product received by a particular user, "normally used" refers to a
|
| 303 |
+
typical or common use of that class of product, regardless of the status
|
| 304 |
+
of the particular user or of the way in which the particular user
|
| 305 |
+
actually uses, or expects or is expected to use, the product. A product
|
| 306 |
+
is a consumer product regardless of whether the product has substantial
|
| 307 |
+
commercial, industrial or non-consumer uses, unless such uses represent
|
| 308 |
+
the only significant mode of use of the product.
|
| 309 |
+
|
| 310 |
+
"Installation Information" for a User Product means any methods,
|
| 311 |
+
procedures, authorization keys, or other information required to install
|
| 312 |
+
and execute modified versions of a covered work in that User Product from
|
| 313 |
+
a modified version of its Corresponding Source. The information must
|
| 314 |
+
suffice to ensure that the continued functioning of the modified object
|
| 315 |
+
code is in no case prevented or interfered with solely because
|
| 316 |
+
modification has been made.
|
| 317 |
+
|
| 318 |
+
If you convey an object code work under this section in, or with, or
|
| 319 |
+
specifically for use in, a User Product, and the conveying occurs as
|
| 320 |
+
part of a transaction in which the right of possession and use of the
|
| 321 |
+
User Product is transferred to the recipient in perpetuity or for a
|
| 322 |
+
fixed term (regardless of how the transaction is characterized), the
|
| 323 |
+
Corresponding Source conveyed under this section must be accompanied
|
| 324 |
+
by the Installation Information. But this requirement does not apply
|
| 325 |
+
if neither you nor any third party retains the ability to install
|
| 326 |
+
modified object code on the User Product (for example, the work has
|
| 327 |
+
been installed in ROM).
|
| 328 |
+
|
| 329 |
+
The requirement to provide Installation Information does not include a
|
| 330 |
+
requirement to continue to provide support service, warranty, or updates
|
| 331 |
+
for a work that has been modified or installed by the recipient, or for
|
| 332 |
+
the User Product in which it has been modified or installed. Access to a
|
| 333 |
+
network may be denied when the modification itself materially and
|
| 334 |
+
adversely affects the operation of the network or violates the rules and
|
| 335 |
+
protocols for communication across the network.
|
| 336 |
+
|
| 337 |
+
Corresponding Source conveyed, and Installation Information provided,
|
| 338 |
+
in accord with this section must be in a format that is publicly
|
| 339 |
+
documented (and with an implementation available to the public in
|
| 340 |
+
source code form), and must require no special password or key for
|
| 341 |
+
unpacking, reading or copying.
|
| 342 |
+
|
| 343 |
+
7. Additional Terms.
|
| 344 |
+
|
| 345 |
+
"Additional permissions" are terms that supplement the terms of this
|
| 346 |
+
License by making exceptions from one or more of its conditions.
|
| 347 |
+
Additional permissions that are applicable to the entire Program shall
|
| 348 |
+
be treated as though they were included in this License, to the extent
|
| 349 |
+
that they are valid under applicable law. If additional permissions
|
| 350 |
+
apply only to part of the Program, that part may be used separately
|
| 351 |
+
under those permissions, but the entire Program remains governed by
|
| 352 |
+
this License without regard to the additional permissions.
|
| 353 |
+
|
| 354 |
+
When you convey a copy of a covered work, you may at your option
|
| 355 |
+
remove any additional permissions from that copy, or from any part of
|
| 356 |
+
it. (Additional permissions may be written to require their own
|
| 357 |
+
removal in certain cases when you modify the work.) You may place
|
| 358 |
+
additional permissions on material, added by you to a covered work,
|
| 359 |
+
for which you have or can give appropriate copyright permission.
|
| 360 |
+
|
| 361 |
+
Notwithstanding any other provision of this License, for material you
|
| 362 |
+
add to a covered work, you may (if authorized by the copyright holders of
|
| 363 |
+
that material) supplement the terms of this License with terms:
|
| 364 |
+
|
| 365 |
+
a) Disclaiming warranty or limiting liability differently from the
|
| 366 |
+
terms of sections 15 and 16 of this License; or
|
| 367 |
+
|
| 368 |
+
b) Requiring preservation of specified reasonable legal notices or
|
| 369 |
+
author attributions in that material or in the Appropriate Legal
|
| 370 |
+
Notices displayed by works containing it; or
|
| 371 |
+
|
| 372 |
+
c) Prohibiting misrepresentation of the origin of that material, or
|
| 373 |
+
requiring that modified versions of such material be marked in
|
| 374 |
+
reasonable ways as different from the original version; or
|
| 375 |
+
|
| 376 |
+
d) Limiting the use for publicity purposes of names of licensors or
|
| 377 |
+
authors of the material; or
|
| 378 |
+
|
| 379 |
+
e) Declining to grant rights under trademark law for use of some
|
| 380 |
+
trade names, trademarks, or service marks; or
|
| 381 |
+
|
| 382 |
+
f) Requiring indemnification of licensors and authors of that
|
| 383 |
+
material by anyone who conveys the material (or modified versions of
|
| 384 |
+
it) with contractual assumptions of liability to the recipient, for
|
| 385 |
+
any liability that these contractual assumptions directly impose on
|
| 386 |
+
those licensors and authors.
|
| 387 |
+
|
| 388 |
+
All other non-permissive additional terms are considered "further
|
| 389 |
+
restrictions" within the meaning of section 10. If the Program as you
|
| 390 |
+
received it, or any part of it, contains a notice stating that it is
|
| 391 |
+
governed by this License along with a term that is a further
|
| 392 |
+
restriction, you may remove that term. If a license document contains
|
| 393 |
+
a further restriction but permits relicensing or conveying under this
|
| 394 |
+
License, you may add to a covered work material governed by the terms
|
| 395 |
+
of that license document, provided that the further restriction does
|
| 396 |
+
not survive such relicensing or conveying.
|
| 397 |
+
|
| 398 |
+
If you add terms to a covered work in accord with this section, you
|
| 399 |
+
must place, in the relevant source files, a statement of the
|
| 400 |
+
additional terms that apply to those files, or a notice indicating
|
| 401 |
+
where to find the applicable terms.
|
| 402 |
+
|
| 403 |
+
Additional terms, permissive or non-permissive, may be stated in the
|
| 404 |
+
form of a separately written license, or stated as exceptions;
|
| 405 |
+
the above requirements apply either way.
|
| 406 |
+
|
| 407 |
+
8. Termination.
|
| 408 |
+
|
| 409 |
+
You may not propagate or modify a covered work except as expressly
|
| 410 |
+
provided under this License. Any attempt otherwise to propagate or
|
| 411 |
+
modify it is void, and will automatically terminate your rights under
|
| 412 |
+
this License (including any patent licenses granted under the third
|
| 413 |
+
paragraph of section 11).
|
| 414 |
+
|
| 415 |
+
However, if you cease all violation of this License, then your
|
| 416 |
+
license from a particular copyright holder is reinstated (a)
|
| 417 |
+
provisionally, unless and until the copyright holder explicitly and
|
| 418 |
+
finally terminates your license, and (b) permanently, if the copyright
|
| 419 |
+
holder fails to notify you of the violation by some reasonable means
|
| 420 |
+
prior to 60 days after the cessation.
|
| 421 |
+
|
| 422 |
+
Moreover, your license from a particular copyright holder is
|
| 423 |
+
reinstated permanently if the copyright holder notifies you of the
|
| 424 |
+
violation by some reasonable means, this is the first time you have
|
| 425 |
+
received notice of violation of this License (for any work) from that
|
| 426 |
+
copyright holder, and you cure the violation prior to 30 days after
|
| 427 |
+
your receipt of the notice.
|
| 428 |
+
|
| 429 |
+
Termination of your rights under this section does not terminate the
|
| 430 |
+
licenses of parties who have received copies or rights from you under
|
| 431 |
+
this License. If your rights have been terminated and not permanently
|
| 432 |
+
reinstated, you do not qualify to receive new licenses for the same
|
| 433 |
+
material under section 10.
|
| 434 |
+
|
| 435 |
+
9. Acceptance Not Required for Having Copies.
|
| 436 |
+
|
| 437 |
+
You are not required to accept this License in order to receive or
|
| 438 |
+
run a copy of the Program. Ancillary propagation of a covered work
|
| 439 |
+
occurring solely as a consequence of using peer-to-peer transmission
|
| 440 |
+
to receive a copy likewise does not require acceptance. However,
|
| 441 |
+
nothing other than this License grants you permission to propagate or
|
| 442 |
+
modify any covered work. These actions infringe copyright if you do
|
| 443 |
+
not accept this License. Therefore, by modifying or propagating a
|
| 444 |
+
covered work, you indicate your acceptance of this License to do so.
|
| 445 |
+
|
| 446 |
+
10. Automatic Licensing of Downstream Recipients.
|
| 447 |
+
|
| 448 |
+
Each time you convey a covered work, the recipient automatically
|
| 449 |
+
receives a license from the original licensors, to run, modify and
|
| 450 |
+
propagate that work, subject to this License. You are not responsible
|
| 451 |
+
for enforcing compliance by third parties with this License.
|
| 452 |
+
|
| 453 |
+
An "entity transaction" is a transaction transferring control of an
|
| 454 |
+
organization, or substantially all assets of one, or subdividing an
|
| 455 |
+
organization, or merging organizations. If propagation of a covered
|
| 456 |
+
work results from an entity transaction, each party to that
|
| 457 |
+
transaction who receives a copy of the work also receives whatever
|
| 458 |
+
licenses to the work the party's predecessor in interest had or could
|
| 459 |
+
give under the previous paragraph, plus a right to possession of the
|
| 460 |
+
Corresponding Source of the work from the predecessor in interest, if
|
| 461 |
+
the predecessor has it or can get it with reasonable efforts.
|
| 462 |
+
|
| 463 |
+
You may not impose any further restrictions on the exercise of the
|
| 464 |
+
rights granted or affirmed under this License. For example, you may
|
| 465 |
+
not impose a license fee, royalty, or other charge for exercise of
|
| 466 |
+
rights granted under this License, and you may not initiate litigation
|
| 467 |
+
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
| 468 |
+
any patent claim is infringed by making, using, selling, offering for
|
| 469 |
+
sale, or importing the Program or any portion of it.
|
| 470 |
+
|
| 471 |
+
11. Patents.
|
| 472 |
+
|
| 473 |
+
A "contributor" is a copyright holder who authorizes use under this
|
| 474 |
+
License of the Program or a work on which the Program is based. The
|
| 475 |
+
work thus licensed is called the contributor's "contributor version".
|
| 476 |
+
|
| 477 |
+
A contributor's "essential patent claims" are all patent claims
|
| 478 |
+
owned or controlled by the contributor, whether already acquired or
|
| 479 |
+
hereafter acquired, that would be infringed by some manner, permitted
|
| 480 |
+
by this License, of making, using, or selling its contributor version,
|
| 481 |
+
but do not include claims that would be infringed only as a
|
| 482 |
+
consequence of further modification of the contributor version. For
|
| 483 |
+
purposes of this definition, "control" includes the right to grant
|
| 484 |
+
patent sublicenses in a manner consistent with the requirements of
|
| 485 |
+
this License.
|
| 486 |
+
|
| 487 |
+
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
| 488 |
+
patent license under the contributor's essential patent claims, to
|
| 489 |
+
make, use, sell, offer for sale, import and otherwise run, modify and
|
| 490 |
+
propagate the contents of its contributor version.
|
| 491 |
+
|
| 492 |
+
In the following three paragraphs, a "patent license" is any express
|
| 493 |
+
agreement or commitment, however denominated, not to enforce a patent
|
| 494 |
+
(such as an express permission to practice a patent or covenant not to
|
| 495 |
+
sue for patent infringement). To "grant" such a patent license to a
|
| 496 |
+
party means to make such an agreement or commitment not to enforce a
|
| 497 |
+
patent against the party.
|
| 498 |
+
|
| 499 |
+
If you convey a covered work, knowingly relying on a patent license,
|
| 500 |
+
and the Corresponding Source of the work is not available for anyone
|
| 501 |
+
to copy, free of charge and under the terms of this License, through a
|
| 502 |
+
publicly available network server or other readily accessible means,
|
| 503 |
+
then you must either (1) cause the Corresponding Source to be so
|
| 504 |
+
available, or (2) arrange to deprive yourself of the benefit of the
|
| 505 |
+
patent license for this particular work, or (3) arrange, in a manner
|
| 506 |
+
consistent with the requirements of this License, to extend the patent
|
| 507 |
+
license to downstream recipients. "Knowingly relying" means you have
|
| 508 |
+
actual knowledge that, but for the patent license, your conveying the
|
| 509 |
+
covered work in a country, or your recipient's use of the covered work
|
| 510 |
+
in a country, would infringe one or more identifiable patents in that
|
| 511 |
+
country that you have reason to believe are valid.
|
| 512 |
+
|
| 513 |
+
If, pursuant to or in connection with a single transaction or
|
| 514 |
+
arrangement, you convey, or propagate by procuring conveyance of, a
|
| 515 |
+
covered work, and grant a patent license to some of the parties
|
| 516 |
+
receiving the covered work authorizing them to use, propagate, modify
|
| 517 |
+
or convey a specific copy of the covered work, then the patent license
|
| 518 |
+
you grant is automatically extended to all recipients of the covered
|
| 519 |
+
work and works based on it.
|
| 520 |
+
|
| 521 |
+
A patent license is "discriminatory" if it does not include within
|
| 522 |
+
the scope of its coverage, prohibits the exercise of, or is
|
| 523 |
+
conditioned on the non-exercise of one or more of the rights that are
|
| 524 |
+
specifically granted under this License. You may not convey a covered
|
| 525 |
+
work if you are a party to an arrangement with a third party that is
|
| 526 |
+
in the business of distributing software, under which you make payment
|
| 527 |
+
to the third party based on the extent of your activity of conveying
|
| 528 |
+
the work, and under which the third party grants, to any of the
|
| 529 |
+
parties who would receive the covered work from you, a discriminatory
|
| 530 |
+
patent license (a) in connection with copies of the covered work
|
| 531 |
+
conveyed by you (or copies made from those copies), or (b) primarily
|
| 532 |
+
for and in connection with specific products or compilations that
|
| 533 |
+
contain the covered work, unless you entered into that arrangement,
|
| 534 |
+
or that patent license was granted, prior to 28 March 2007.
|
| 535 |
+
|
| 536 |
+
Nothing in this License shall be construed as excluding or limiting
|
| 537 |
+
any implied license or other defenses to infringement that may
|
| 538 |
+
otherwise be available to you under applicable patent law.
|
| 539 |
+
|
| 540 |
+
12. No Surrender of Others' Freedom.
|
| 541 |
+
|
| 542 |
+
If conditions are imposed on you (whether by court order, agreement or
|
| 543 |
+
otherwise) that contradict the conditions of this License, they do not
|
| 544 |
+
excuse you from the conditions of this License. If you cannot convey a
|
| 545 |
+
covered work so as to satisfy simultaneously your obligations under this
|
| 546 |
+
License and any other pertinent obligations, then as a consequence you may
|
| 547 |
+
not convey it at all. For example, if you agree to terms that obligate you
|
| 548 |
+
to collect a royalty for further conveying from those to whom you convey
|
| 549 |
+
the Program, the only way you could satisfy both those terms and this
|
| 550 |
+
License would be to refrain entirely from conveying the Program.
|
| 551 |
+
|
| 552 |
+
13. Use with the GNU Affero General Public License.
|
| 553 |
+
|
| 554 |
+
Notwithstanding any other provision of this License, you have
|
| 555 |
+
permission to link or combine any covered work with a work licensed
|
| 556 |
+
under version 3 of the GNU Affero General Public License into a single
|
| 557 |
+
combined work, and to convey the resulting work. The terms of this
|
| 558 |
+
License will continue to apply to the part which is the covered work,
|
| 559 |
+
but the special requirements of the GNU Affero General Public License,
|
| 560 |
+
section 13, concerning interaction through a network will apply to the
|
| 561 |
+
combination as such.
|
| 562 |
+
|
| 563 |
+
14. Revised Versions of this License.
|
| 564 |
+
|
| 565 |
+
The Free Software Foundation may publish revised and/or new versions of
|
| 566 |
+
the GNU General Public License from time to time. Such new versions will
|
| 567 |
+
be similar in spirit to the present version, but may differ in detail to
|
| 568 |
+
address new problems or concerns.
|
| 569 |
+
|
| 570 |
+
Each version is given a distinguishing version number. If the
|
| 571 |
+
Program specifies that a certain numbered version of the GNU General
|
| 572 |
+
Public License "or any later version" applies to it, you have the
|
| 573 |
+
option of following the terms and conditions either of that numbered
|
| 574 |
+
version or of any later version published by the Free Software
|
| 575 |
+
Foundation. If the Program does not specify a version number of the
|
| 576 |
+
GNU General Public License, you may choose any version ever published
|
| 577 |
+
by the Free Software Foundation.
|
| 578 |
+
|
| 579 |
+
If the Program specifies that a proxy can decide which future
|
| 580 |
+
versions of the GNU General Public License can be used, that proxy's
|
| 581 |
+
public statement of acceptance of a version permanently authorizes you
|
| 582 |
+
to choose that version for the Program.
|
| 583 |
+
|
| 584 |
+
Later license versions may give you additional or different
|
| 585 |
+
permissions. However, no additional obligations are imposed on any
|
| 586 |
+
author or copyright holder as a result of your choosing to follow a
|
| 587 |
+
later version.
|
| 588 |
+
|
| 589 |
+
15. Disclaimer of Warranty.
|
| 590 |
+
|
| 591 |
+
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
| 592 |
+
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
| 593 |
+
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
| 594 |
+
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
| 595 |
+
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
| 596 |
+
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
| 597 |
+
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
| 598 |
+
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
| 599 |
+
|
| 600 |
+
16. Limitation of Liability.
|
| 601 |
+
|
| 602 |
+
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
| 603 |
+
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
| 604 |
+
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
| 605 |
+
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
| 606 |
+
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
| 607 |
+
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
| 608 |
+
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
| 609 |
+
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
| 610 |
+
SUCH DAMAGES.
|
| 611 |
+
|
| 612 |
+
17. Interpretation of Sections 15 and 16.
|
| 613 |
+
|
| 614 |
+
If the disclaimer of warranty and limitation of liability provided
|
| 615 |
+
above cannot be given local legal effect according to their terms,
|
| 616 |
+
reviewing courts shall apply local law that most closely approximates
|
| 617 |
+
an absolute waiver of all civil liability in connection with the
|
| 618 |
+
Program, unless a warranty or assumption of liability accompanies a
|
| 619 |
+
copy of the Program in return for a fee.
|
| 620 |
+
|
| 621 |
+
END OF TERMS AND CONDITIONS
|
| 622 |
+
|
| 623 |
+
How to Apply These Terms to Your New Programs
|
| 624 |
+
|
| 625 |
+
If you develop a new program, and you want it to be of the greatest
|
| 626 |
+
possible use to the public, the best way to achieve this is to make it
|
| 627 |
+
free software which everyone can redistribute and change under these terms.
|
| 628 |
+
|
| 629 |
+
To do so, attach the following notices to the program. It is safest
|
| 630 |
+
to attach them to the start of each source file to most effectively
|
| 631 |
+
state the exclusion of warranty; and each file should have at least
|
| 632 |
+
the "copyright" line and a pointer to where the full notice is found.
|
| 633 |
+
|
| 634 |
+
<one line to give the program's name and a brief idea of what it does.>
|
| 635 |
+
Copyright (C) <year> <name of author>
|
| 636 |
+
|
| 637 |
+
This program is free software: you can redistribute it and/or modify
|
| 638 |
+
it under the terms of the GNU General Public License as published by
|
| 639 |
+
the Free Software Foundation, either version 3 of the License, or
|
| 640 |
+
(at your option) any later version.
|
| 641 |
+
|
| 642 |
+
This program is distributed in the hope that it will be useful,
|
| 643 |
+
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 644 |
+
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 645 |
+
GNU General Public License for more details.
|
| 646 |
+
|
| 647 |
+
You should have received a copy of the GNU General Public License
|
| 648 |
+
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 649 |
+
|
| 650 |
+
Also add information on how to contact you by electronic and paper mail.
|
| 651 |
+
|
| 652 |
+
If the program does terminal interaction, make it output a short
|
| 653 |
+
notice like this when it starts in an interactive mode:
|
| 654 |
+
|
| 655 |
+
<program> Copyright (C) <year> <name of author>
|
| 656 |
+
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
| 657 |
+
This is free software, and you are welcome to redistribute it
|
| 658 |
+
under certain conditions; type `show c' for details.
|
| 659 |
+
|
| 660 |
+
The hypothetical commands `show w' and `show c' should show the appropriate
|
| 661 |
+
parts of the General Public License. Of course, your program's commands
|
| 662 |
+
might be different; for a GUI interface, you would use an "about box".
|
| 663 |
+
|
| 664 |
+
You should also get your employer (if you work as a programmer) or school,
|
| 665 |
+
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
| 666 |
+
For more information on this, and how to apply and follow the GNU GPL, see
|
| 667 |
+
<https://www.gnu.org/licenses/>.
|
| 668 |
+
|
| 669 |
+
The GNU General Public License does not permit incorporating your program
|
| 670 |
+
into proprietary programs. If your program is a subroutine library, you
|
| 671 |
+
may consider it more useful to permit linking proprietary applications with
|
| 672 |
+
the library. If this is what you want to do, use the GNU Lesser General
|
| 673 |
+
Public License instead of this License. But first, please read
|
| 674 |
+
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
README.md
CHANGED
|
@@ -1,10 +1,15 @@
|
|
| 1 |
---
|
| 2 |
-
title:
|
| 3 |
-
emoji:
|
| 4 |
colorFrom: blue
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
-
|
| 8 |
---
|
| 9 |
|
| 10 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
+
title: "velo"
|
| 3 |
+
emoji: "🚀"
|
| 4 |
colorFrom: blue
|
| 5 |
+
colorTo: green
|
| 6 |
sdk: docker
|
| 7 |
+
app_port: 3000
|
| 8 |
---
|
| 9 |
|
| 10 |
+
### 🚀 一键部署
|
| 11 |
+
[](https://github.com/kfcx/HFSpaceDeploy)
|
| 12 |
+
|
| 13 |
+
本项目由[HFSpaceDeploy](https://github.com/kfcx/HFSpaceDeploy)一键部署
|
| 14 |
+
|
| 15 |
+
|
VELOERA_PROJ
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
如果您在阅读这个, 则证明该项目基于 Veloera 二次开发, 如果您在 /veloera 上阅读到了此文件, 那么该网站同样基于 Veloera.
|
| 2 |
+
|
| 3 |
+
Veloera 是一个智能的开源 LLM API 网关, 为商用和大并发准备的. 该项目基于 GPL v3 开源, 附加条款适用.
|
| 4 |
+
访问源代码: https://github.com/Veloera/Veloera
|
| 5 |
+
|
| 6 |
+
---
|
| 7 |
+
|
| 8 |
+
If you are reading this, it means the project is a secondary development based on Veloera. If you are reading this file under /veloera, then that site is also based on Veloera.
|
| 9 |
+
|
| 10 |
+
Veloera is an intelligent open-source LLM API gateway, built for commercial use and high concurrency. This project is open-sourced under the GPL v3 license, with additional terms applicable.
|
| 11 |
+
Source code available at: https://github.com/Veloera/Veloera
|
VERSION
ADDED
|
File without changes
|
bin/migration_v0.2-v0.3.sql
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
UPDATE users
|
| 2 |
+
SET quota = quota + (
|
| 3 |
+
SELECT SUM(remain_quota)
|
| 4 |
+
FROM tokens
|
| 5 |
+
WHERE tokens.user_id = users.id
|
| 6 |
+
)
|
bin/migration_v0.3-v0.4.sql
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
INSERT INTO abilities (`group`, model, channel_id, enabled)
|
| 2 |
+
SELECT c.`group`, m.model, c.id, 1
|
| 3 |
+
FROM channels c
|
| 4 |
+
CROSS JOIN (
|
| 5 |
+
SELECT 'gpt-3.5-turbo' AS model UNION ALL
|
| 6 |
+
SELECT 'gpt-3.5-turbo-0301' AS model UNION ALL
|
| 7 |
+
SELECT 'gpt-4' AS model UNION ALL
|
| 8 |
+
SELECT 'gpt-4-0314' AS model
|
| 9 |
+
) AS m
|
| 10 |
+
WHERE c.status = 1
|
| 11 |
+
AND NOT EXISTS (
|
| 12 |
+
SELECT 1
|
| 13 |
+
FROM abilities a
|
| 14 |
+
WHERE a.`group` = c.`group`
|
| 15 |
+
AND a.model = m.model
|
| 16 |
+
AND a.channel_id = c.id
|
| 17 |
+
);
|
bin/time_test.sh
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/bin/bash
|
| 2 |
+
|
| 3 |
+
if [ $# -lt 3 ]; then
|
| 4 |
+
echo "Usage: time_test.sh <domain> <key> <count> [<model>]"
|
| 5 |
+
exit 1
|
| 6 |
+
fi
|
| 7 |
+
|
| 8 |
+
domain=$1
|
| 9 |
+
key=$2
|
| 10 |
+
count=$3
|
| 11 |
+
model=${4:-"gpt-3.5-turbo"} # 设置默认模型为 gpt-3.5-turbo
|
| 12 |
+
|
| 13 |
+
total_time=0
|
| 14 |
+
times=()
|
| 15 |
+
|
| 16 |
+
for ((i=1; i<=count; i++)); do
|
| 17 |
+
result=$(curl -o /dev/null -s -w "%{http_code} %{time_total}\\n" \
|
| 18 |
+
https://"$domain"/v1/chat/completions \
|
| 19 |
+
-H "Content-Type: application/json" \
|
| 20 |
+
-H "Authorization: Bearer $key" \
|
| 21 |
+
-d '{"messages": [{"content": "echo hi", "role": "user"}], "model": "'"$model"'", "stream": false, "max_tokens": 1}')
|
| 22 |
+
http_code=$(echo "$result" | awk '{print $1}')
|
| 23 |
+
time=$(echo "$result" | awk '{print $2}')
|
| 24 |
+
echo "HTTP status code: $http_code, Time taken: $time"
|
| 25 |
+
total_time=$(bc <<< "$total_time + $time")
|
| 26 |
+
times+=("$time")
|
| 27 |
+
done
|
| 28 |
+
|
| 29 |
+
average_time=$(echo "scale=4; $total_time / $count" | bc)
|
| 30 |
+
|
| 31 |
+
sum_of_squares=0
|
| 32 |
+
for time in "${times[@]}"; do
|
| 33 |
+
difference=$(echo "scale=4; $time - $average_time" | bc)
|
| 34 |
+
square=$(echo "scale=4; $difference * $difference" | bc)
|
| 35 |
+
sum_of_squares=$(echo "scale=4; $sum_of_squares + $square" | bc)
|
| 36 |
+
done
|
| 37 |
+
|
| 38 |
+
standard_deviation=$(echo "scale=4; sqrt($sum_of_squares / $count)" | bc)
|
| 39 |
+
|
| 40 |
+
echo "Average time: $average_time±$standard_deviation"
|
common/constants.go
ADDED
|
@@ -0,0 +1,330 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
//"os"
|
| 21 |
+
//"strconv"
|
| 22 |
+
"sync"
|
| 23 |
+
"time"
|
| 24 |
+
|
| 25 |
+
"github.com/google/uuid"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
var StartTime = time.Now().Unix() // unit: second
|
| 29 |
+
var Version = "v0.0.0" // this hard coding will be replaced automatically when building, no need to manually change
|
| 30 |
+
var SystemName = "Veloera"
|
| 31 |
+
var Footer = ""
|
| 32 |
+
var Logo = ""
|
| 33 |
+
var TopUpLink = ""
|
| 34 |
+
|
| 35 |
+
// var ChatLink = ""
|
| 36 |
+
// var ChatLink2 = ""
|
| 37 |
+
var QuotaPerUnit = 500 * 1000.0 // $0.002 / 1K tokens
|
| 38 |
+
var DisplayInCurrencyEnabled = true
|
| 39 |
+
var DisplayTokenStatEnabled = true
|
| 40 |
+
var DrawingEnabled = true
|
| 41 |
+
var TaskEnabled = true
|
| 42 |
+
var DataExportEnabled = true
|
| 43 |
+
var DataExportInterval = 5 // unit: minute
|
| 44 |
+
var DataExportDefaultTime = "hour" // unit: minute
|
| 45 |
+
var DefaultCollapseSidebar = false // default value of collapse sidebar
|
| 46 |
+
|
| 47 |
+
// Any options with "Secret", "Token" in its key won't be return by GetOptions
|
| 48 |
+
|
| 49 |
+
var SessionSecret = uuid.New().String()
|
| 50 |
+
var CryptoSecret = uuid.New().String()
|
| 51 |
+
|
| 52 |
+
var OptionMap map[string]string
|
| 53 |
+
var OptionMapRWMutex sync.RWMutex
|
| 54 |
+
|
| 55 |
+
var ItemsPerPage = 10
|
| 56 |
+
var MaxRecentItems = 100
|
| 57 |
+
|
| 58 |
+
var PasswordLoginEnabled = true
|
| 59 |
+
var PasswordRegisterEnabled = true
|
| 60 |
+
var EmailVerificationEnabled = false
|
| 61 |
+
var GitHubOAuthEnabled = false
|
| 62 |
+
var LinuxDOOAuthEnabled = false
|
| 63 |
+
var IDCFlareOAuthEnabled = false
|
| 64 |
+
var WeChatAuthEnabled = false
|
| 65 |
+
var TelegramOAuthEnabled = false
|
| 66 |
+
var TurnstileCheckEnabled = false
|
| 67 |
+
var RegisterEnabled = true
|
| 68 |
+
|
| 69 |
+
var EmailDomainRestrictionEnabled = false // 是否启用邮箱域名限制
|
| 70 |
+
var EmailAliasRestrictionEnabled = false // 是否启用邮箱别名限制
|
| 71 |
+
var EmailDomainWhitelist = []string{
|
| 72 |
+
"gmail.com",
|
| 73 |
+
"163.com",
|
| 74 |
+
"126.com",
|
| 75 |
+
"qq.com",
|
| 76 |
+
"outlook.com",
|
| 77 |
+
"hotmail.com",
|
| 78 |
+
"icloud.com",
|
| 79 |
+
"yahoo.com",
|
| 80 |
+
"foxmail.com",
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
var DebugEnabled bool
|
| 84 |
+
var MemoryCacheEnabled bool
|
| 85 |
+
|
| 86 |
+
var LogConsumeEnabled = true
|
| 87 |
+
var LogChatContentEnabled = false
|
| 88 |
+
var LogErrorEnabled = false
|
| 89 |
+
|
| 90 |
+
var SMTPServer = ""
|
| 91 |
+
var SMTPPort = 587
|
| 92 |
+
var SMTPSSLEnabled = false
|
| 93 |
+
var SMTPAccount = ""
|
| 94 |
+
var SMTPFrom = ""
|
| 95 |
+
var SMTPToken = ""
|
| 96 |
+
|
| 97 |
+
var GitHubClientId = ""
|
| 98 |
+
var GitHubClientSecret = ""
|
| 99 |
+
var LinuxDOClientId = ""
|
| 100 |
+
var LinuxDOClientSecret = ""
|
| 101 |
+
var LinuxDOMinimumTrustLevel = 0
|
| 102 |
+
var IDCFlareClientId = ""
|
| 103 |
+
var IDCFlareClientSecret = ""
|
| 104 |
+
var IDCFlareMinimumTrustLevel = 0
|
| 105 |
+
|
| 106 |
+
var WeChatServerAddress = ""
|
| 107 |
+
var WeChatServerToken = ""
|
| 108 |
+
var WeChatAccountQRCodeImageURL = ""
|
| 109 |
+
|
| 110 |
+
var TurnstileSiteKey = ""
|
| 111 |
+
var TurnstileSecretKey = ""
|
| 112 |
+
|
| 113 |
+
var TelegramBotToken = ""
|
| 114 |
+
var TelegramBotName = ""
|
| 115 |
+
|
| 116 |
+
var QuotaForNewUser = 0
|
| 117 |
+
var QuotaForInviter = 0
|
| 118 |
+
var QuotaForInvitee = 0
|
| 119 |
+
var ChannelDisableThreshold = 5.0
|
| 120 |
+
var AutomaticDisableChannelEnabled = false
|
| 121 |
+
var AutomaticEnableChannelEnabled = false
|
| 122 |
+
var AffEnabled = true
|
| 123 |
+
var QuotaRemindThreshold = 1000
|
| 124 |
+
var PreConsumedQuota = 500
|
| 125 |
+
|
| 126 |
+
var RetryTimes = 0
|
| 127 |
+
|
| 128 |
+
var CheckInEnabled = false
|
| 129 |
+
var CheckInQuota = 1000
|
| 130 |
+
var CheckInMaxQuota = 0
|
| 131 |
+
|
| 132 |
+
// 返佣设置
|
| 133 |
+
var RebateEnabled = false
|
| 134 |
+
var RebatePercentage = 0.0
|
| 135 |
+
|
| 136 |
+
// 反向代理设置
|
| 137 |
+
var ReverseProxyEnabled = false
|
| 138 |
+
var ReverseProxyProvider = "nginx"
|
| 139 |
+
|
| 140 |
+
//var RootUserEmail = ""
|
| 141 |
+
|
| 142 |
+
var IsMasterNode bool
|
| 143 |
+
|
| 144 |
+
var requestInterval int
|
| 145 |
+
var RequestInterval time.Duration
|
| 146 |
+
|
| 147 |
+
var SyncFrequency int // unit is second
|
| 148 |
+
|
| 149 |
+
var BatchUpdateEnabled = false
|
| 150 |
+
var BatchUpdateInterval int
|
| 151 |
+
|
| 152 |
+
var RelayTimeout int // unit is second
|
| 153 |
+
|
| 154 |
+
var GeminiSafetySetting string
|
| 155 |
+
|
| 156 |
+
// https://docs.cohere.com/docs/safety-modes Type; NONE/CONTEXTUAL/STRICT
|
| 157 |
+
var CohereSafetySetting string
|
| 158 |
+
|
| 159 |
+
const (
|
| 160 |
+
RequestIdKey = "X-Veloera-Request-Id"
|
| 161 |
+
)
|
| 162 |
+
|
| 163 |
+
const (
|
| 164 |
+
RoleGuestUser = 0
|
| 165 |
+
RoleCommonUser = 1
|
| 166 |
+
RoleAdminUser = 10
|
| 167 |
+
RoleRootUser = 100
|
| 168 |
+
)
|
| 169 |
+
|
| 170 |
+
func IsValidateRole(role int) bool {
|
| 171 |
+
return role == RoleGuestUser || role == RoleCommonUser || role == RoleAdminUser || role == RoleRootUser
|
| 172 |
+
}
|
| 173 |
+
|
| 174 |
+
var (
|
| 175 |
+
FileUploadPermission = RoleGuestUser
|
| 176 |
+
FileDownloadPermission = RoleGuestUser
|
| 177 |
+
ImageUploadPermission = RoleGuestUser
|
| 178 |
+
ImageDownloadPermission = RoleGuestUser
|
| 179 |
+
)
|
| 180 |
+
|
| 181 |
+
// All duration's unit is seconds
|
| 182 |
+
// Shouldn't larger then RateLimitKeyExpirationDuration
|
| 183 |
+
var (
|
| 184 |
+
GlobalApiRateLimitEnable bool
|
| 185 |
+
GlobalApiRateLimitNum int
|
| 186 |
+
GlobalApiRateLimitDuration int64
|
| 187 |
+
|
| 188 |
+
GlobalWebRateLimitEnable bool
|
| 189 |
+
GlobalWebRateLimitNum int
|
| 190 |
+
GlobalWebRateLimitDuration int64
|
| 191 |
+
|
| 192 |
+
UploadRateLimitNum = 10
|
| 193 |
+
UploadRateLimitDuration int64 = 60
|
| 194 |
+
|
| 195 |
+
DownloadRateLimitNum = 10
|
| 196 |
+
DownloadRateLimitDuration int64 = 60
|
| 197 |
+
|
| 198 |
+
CriticalRateLimitNum = 20
|
| 199 |
+
CriticalRateLimitDuration int64 = 20 * 60
|
| 200 |
+
)
|
| 201 |
+
|
| 202 |
+
var RateLimitKeyExpirationDuration = 20 * time.Minute
|
| 203 |
+
|
| 204 |
+
const (
|
| 205 |
+
UserStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 206 |
+
UserStatusDisabled = 2 // also don't use 0
|
| 207 |
+
)
|
| 208 |
+
|
| 209 |
+
const (
|
| 210 |
+
TokenStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 211 |
+
TokenStatusDisabled = 2 // also don't use 0
|
| 212 |
+
TokenStatusExpired = 3
|
| 213 |
+
TokenStatusExhausted = 4
|
| 214 |
+
)
|
| 215 |
+
|
| 216 |
+
const (
|
| 217 |
+
RedemptionCodeStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 218 |
+
RedemptionCodeStatusDisabled = 2 // also don't use 0
|
| 219 |
+
RedemptionCodeStatusUsed = 3 // also don't use 0
|
| 220 |
+
)
|
| 221 |
+
|
| 222 |
+
const (
|
| 223 |
+
ChannelStatusUnknown = 0
|
| 224 |
+
ChannelStatusEnabled = 1 // don't use 0, 0 is the default value!
|
| 225 |
+
ChannelStatusManuallyDisabled = 2 // also don't use 0
|
| 226 |
+
ChannelStatusAutoDisabled = 3
|
| 227 |
+
)
|
| 228 |
+
|
| 229 |
+
const (
|
| 230 |
+
ChannelTypeUnknown = 0
|
| 231 |
+
ChannelTypeOpenAI = 1
|
| 232 |
+
ChannelTypeMidjourney = 2
|
| 233 |
+
ChannelTypeAzure = 3
|
| 234 |
+
ChannelTypeOllama = 4
|
| 235 |
+
ChannelTypeMidjourneyPlus = 5
|
| 236 |
+
ChannelTypeOpenAIMax = 6
|
| 237 |
+
ChannelTypeOhMyGPT = 7
|
| 238 |
+
ChannelTypeCustom = 8
|
| 239 |
+
ChannelTypeAILS = 9
|
| 240 |
+
ChannelTypeAIProxy = 10
|
| 241 |
+
ChannelTypePaLM = 11
|
| 242 |
+
ChannelTypeAPI2GPT = 12
|
| 243 |
+
ChannelTypeAIGC2D = 13
|
| 244 |
+
ChannelTypeAnthropic = 14
|
| 245 |
+
ChannelTypeBaidu = 15
|
| 246 |
+
ChannelTypeZhipu = 16
|
| 247 |
+
ChannelTypeAli = 17
|
| 248 |
+
ChannelTypeXunfei = 18
|
| 249 |
+
ChannelType360 = 19
|
| 250 |
+
ChannelTypeOpenRouter = 20
|
| 251 |
+
ChannelTypeAIProxyLibrary = 21
|
| 252 |
+
ChannelTypeFastGPT = 22
|
| 253 |
+
ChannelTypeTencent = 23
|
| 254 |
+
ChannelTypeGemini = 24
|
| 255 |
+
ChannelTypeMoonshot = 25
|
| 256 |
+
ChannelTypePerplexity = 27
|
| 257 |
+
ChannelTypeLingYiWanWu = 31
|
| 258 |
+
ChannelTypeAws = 33
|
| 259 |
+
ChannelTypeCohere = 34
|
| 260 |
+
ChannelTypeMiniMax = 35
|
| 261 |
+
ChannelTypeSunoAPI = 36
|
| 262 |
+
ChannelTypeDify = 37
|
| 263 |
+
ChannelTypeJina = 38
|
| 264 |
+
ChannelCloudflare = 39
|
| 265 |
+
ChannelTypeSiliconFlow = 40
|
| 266 |
+
ChannelTypeVertexAi = 41
|
| 267 |
+
ChannelTypeMistral = 42
|
| 268 |
+
ChannelTypeDeepSeek = 43
|
| 269 |
+
ChannelTypeMokaAI = 44
|
| 270 |
+
ChannelTypeVolcEngine = 45
|
| 271 |
+
ChannelTypeBaiduV2 = 46
|
| 272 |
+
ChannelTypeXinference = 47
|
| 273 |
+
ChannelTypeXai = 48
|
| 274 |
+
ChannelTypeGitHub = 49
|
| 275 |
+
ChannelTypeDummy // this one is only for count, do not add any channel after this
|
| 276 |
+
|
| 277 |
+
)
|
| 278 |
+
|
| 279 |
+
var ChannelBaseURLs = []string{
|
| 280 |
+
"", // 0
|
| 281 |
+
"https://api.openai.com", // 1
|
| 282 |
+
"https://oa.api2d.net", // 2
|
| 283 |
+
"", // 3
|
| 284 |
+
"http://localhost:11434", // 4
|
| 285 |
+
"https://api.openai-sb.com", // 5
|
| 286 |
+
"https://api.openaimax.com", // 6
|
| 287 |
+
"https://api.ohmygpt.com", // 7
|
| 288 |
+
"", // 8
|
| 289 |
+
"https://api.caipacity.com", // 9
|
| 290 |
+
"https://api.aiproxy.io", // 10
|
| 291 |
+
"", // 11
|
| 292 |
+
"https://api.api2gpt.com", // 12
|
| 293 |
+
"https://api.aigc2d.com", // 13
|
| 294 |
+
"https://api.anthropic.com", // 14
|
| 295 |
+
"https://aip.baidubce.com", // 15
|
| 296 |
+
"https://open.bigmodel.cn", // 16
|
| 297 |
+
"https://dashscope.aliyuncs.com", // 17
|
| 298 |
+
"", // 18
|
| 299 |
+
"https://api.360.cn", // 19
|
| 300 |
+
"https://openrouter.ai/api", // 20
|
| 301 |
+
"https://api.aiproxy.io", // 21
|
| 302 |
+
"https://fastgpt.run/api/openapi", // 22
|
| 303 |
+
"https://hunyuan.tencentcloudapi.com", //23
|
| 304 |
+
"https://generativelanguage.googleapis.com", //24
|
| 305 |
+
"https://api.moonshot.cn", //25
|
| 306 |
+
"https://open.bigmodel.cn", //26
|
| 307 |
+
"https://api.perplexity.ai", //27
|
| 308 |
+
"", //28
|
| 309 |
+
"", //29
|
| 310 |
+
"", //30
|
| 311 |
+
"https://api.lingyiwanwu.com", //31
|
| 312 |
+
"", //32
|
| 313 |
+
"", //33
|
| 314 |
+
"https://api.cohere.ai", //34
|
| 315 |
+
"https://api.minimax.chat", //35
|
| 316 |
+
"", //36
|
| 317 |
+
"https://api.dify.ai", //37
|
| 318 |
+
"https://api.jina.ai", //38
|
| 319 |
+
"https://api.cloudflare.com", //39
|
| 320 |
+
"https://api.siliconflow.cn", //40
|
| 321 |
+
"", //41
|
| 322 |
+
"https://api.mistral.ai", //42
|
| 323 |
+
"https://api.deepseek.com", //43
|
| 324 |
+
"https://api.moka.ai", //44
|
| 325 |
+
"https://ark.cn-beijing.volces.com", //45
|
| 326 |
+
"https://qianfan.baidubce.com", //46
|
| 327 |
+
"", //47
|
| 328 |
+
"https://api.x.ai", //48
|
| 329 |
+
"https://models.github.ai/inference", //49
|
| 330 |
+
}
|
common/crypto.go
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"crypto/hmac"
|
| 21 |
+
"crypto/md5"
|
| 22 |
+
"crypto/sha256"
|
| 23 |
+
"encoding/hex"
|
| 24 |
+
"golang.org/x/crypto/bcrypt"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
func GenerateHMACWithKey(key []byte, data string) string {
|
| 28 |
+
h := hmac.New(sha256.New, key)
|
| 29 |
+
h.Write([]byte(data))
|
| 30 |
+
return hex.EncodeToString(h.Sum(nil))
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func GenerateHMAC(data string) string {
|
| 34 |
+
h := hmac.New(sha256.New, []byte(CryptoSecret))
|
| 35 |
+
h.Write([]byte(data))
|
| 36 |
+
return hex.EncodeToString(h.Sum(nil))
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func Password2Hash(password string) (string, error) {
|
| 40 |
+
passwordBytes := []byte(password)
|
| 41 |
+
hashedPassword, err := bcrypt.GenerateFromPassword(passwordBytes, bcrypt.DefaultCost)
|
| 42 |
+
return string(hashedPassword), err
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
func ValidatePasswordAndHash(password string, hash string) bool {
|
| 46 |
+
err := bcrypt.CompareHashAndPassword([]byte(hash), []byte(password))
|
| 47 |
+
return err == nil
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
// GetMD5Hash generates an MD5 hash for the given string
|
| 51 |
+
func GetMD5Hash(text string) string {
|
| 52 |
+
hash := md5.Sum([]byte(text))
|
| 53 |
+
return hex.EncodeToString(hash[:])
|
| 54 |
+
}
|
common/custom-event.go
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
// Copyright 2014 Manu Martinez-Almeida. All rights reserved.
|
| 18 |
+
// Use of this source code is governed by a MIT style
|
| 19 |
+
// license that can be found in the LICENSE file.
|
| 20 |
+
|
| 21 |
+
package common
|
| 22 |
+
|
| 23 |
+
import (
|
| 24 |
+
"fmt"
|
| 25 |
+
"io"
|
| 26 |
+
"net/http"
|
| 27 |
+
"strings"
|
| 28 |
+
)
|
| 29 |
+
|
| 30 |
+
type stringWriter interface {
|
| 31 |
+
io.Writer
|
| 32 |
+
writeString(string) (int, error)
|
| 33 |
+
}
|
| 34 |
+
|
| 35 |
+
type stringWrapper struct {
|
| 36 |
+
io.Writer
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
func (w stringWrapper) writeString(str string) (int, error) {
|
| 40 |
+
return w.Writer.Write([]byte(str))
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func checkWriter(writer io.Writer) stringWriter {
|
| 44 |
+
if w, ok := writer.(stringWriter); ok {
|
| 45 |
+
return w
|
| 46 |
+
} else {
|
| 47 |
+
return stringWrapper{writer}
|
| 48 |
+
}
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
// Server-Sent Events
|
| 52 |
+
// W3C Working Draft 29 October 2009
|
| 53 |
+
// http://www.w3.org/TR/2009/WD-eventsource-20091029/
|
| 54 |
+
|
| 55 |
+
var contentType = []string{"text/event-stream"}
|
| 56 |
+
var noCache = []string{"no-cache"}
|
| 57 |
+
|
| 58 |
+
var fieldReplacer = strings.NewReplacer(
|
| 59 |
+
"\n", "\\n",
|
| 60 |
+
"\r", "\\r")
|
| 61 |
+
|
| 62 |
+
var dataReplacer = strings.NewReplacer(
|
| 63 |
+
"\n", "\n",
|
| 64 |
+
"\r", "\\r")
|
| 65 |
+
|
| 66 |
+
type CustomEvent struct {
|
| 67 |
+
Event string
|
| 68 |
+
Id string
|
| 69 |
+
Retry uint
|
| 70 |
+
Data interface{}
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
func encode(writer io.Writer, event CustomEvent) error {
|
| 74 |
+
w := checkWriter(writer)
|
| 75 |
+
return writeData(w, event.Data)
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
func writeData(w stringWriter, data interface{}) error {
|
| 79 |
+
dataReplacer.WriteString(w, fmt.Sprint(data))
|
| 80 |
+
if strings.HasPrefix(data.(string), "data") {
|
| 81 |
+
w.writeString("\n\n")
|
| 82 |
+
}
|
| 83 |
+
return nil
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
func (r CustomEvent) Render(w http.ResponseWriter) error {
|
| 87 |
+
r.WriteContentType(w)
|
| 88 |
+
return encode(w, r)
|
| 89 |
+
}
|
| 90 |
+
|
| 91 |
+
func (r CustomEvent) WriteContentType(w http.ResponseWriter) {
|
| 92 |
+
header := w.Header()
|
| 93 |
+
header["Content-Type"] = contentType
|
| 94 |
+
|
| 95 |
+
if _, exist := header["Cache-Control"]; !exist {
|
| 96 |
+
header["Cache-Control"] = noCache
|
| 97 |
+
}
|
| 98 |
+
}
|
common/database.go
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
var UsingSQLite = false
|
| 20 |
+
var UsingPostgreSQL = false
|
| 21 |
+
var UsingMySQL = false
|
| 22 |
+
var UsingClickHouse = false
|
| 23 |
+
|
| 24 |
+
var SQLitePath = "veloera.db?_busy_timeout=5000"
|
common/email-outlook-auth.go
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"errors"
|
| 21 |
+
"net/smtp"
|
| 22 |
+
"strings"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
type outlookAuth struct {
|
| 26 |
+
username, password string
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func LoginAuth(username, password string) smtp.Auth {
|
| 30 |
+
return &outlookAuth{username, password}
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func (a *outlookAuth) Start(_ *smtp.ServerInfo) (string, []byte, error) {
|
| 34 |
+
return "LOGIN", []byte{}, nil
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func (a *outlookAuth) Next(fromServer []byte, more bool) ([]byte, error) {
|
| 38 |
+
if more {
|
| 39 |
+
switch string(fromServer) {
|
| 40 |
+
case "Username:":
|
| 41 |
+
return []byte(a.username), nil
|
| 42 |
+
case "Password:":
|
| 43 |
+
return []byte(a.password), nil
|
| 44 |
+
default:
|
| 45 |
+
return nil, errors.New("unknown fromServer")
|
| 46 |
+
}
|
| 47 |
+
}
|
| 48 |
+
return nil, nil
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
func isOutlookServer(server string) bool {
|
| 52 |
+
// 兼容多地区的outlook邮箱和ofb邮箱
|
| 53 |
+
// 其实应该加一个Option来区分是否用LOGIN的方式登录
|
| 54 |
+
// 先临时兼容一下
|
| 55 |
+
return strings.Contains(server, "outlook") || strings.Contains(server, "onmicrosoft")
|
| 56 |
+
}
|
common/email.go
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"crypto/tls"
|
| 21 |
+
"encoding/base64"
|
| 22 |
+
"fmt"
|
| 23 |
+
"net/smtp"
|
| 24 |
+
"strings"
|
| 25 |
+
"time"
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
func generateMessageID() (string, error) {
|
| 29 |
+
split := strings.Split(SMTPFrom, "@")
|
| 30 |
+
if len(split) < 2 {
|
| 31 |
+
return "", fmt.Errorf("invalid SMTP account")
|
| 32 |
+
}
|
| 33 |
+
domain := strings.Split(SMTPFrom, "@")[1]
|
| 34 |
+
return fmt.Sprintf("<%d.%s@%s>", time.Now().UnixNano(), GetRandomString(12), domain), nil
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func SendEmail(subject string, receiver string, content string) error {
|
| 38 |
+
if SMTPFrom == "" { // for compatibility
|
| 39 |
+
SMTPFrom = SMTPAccount
|
| 40 |
+
}
|
| 41 |
+
id, err2 := generateMessageID()
|
| 42 |
+
if err2 != nil {
|
| 43 |
+
return err2
|
| 44 |
+
}
|
| 45 |
+
if SMTPServer == "" && SMTPAccount == "" {
|
| 46 |
+
return fmt.Errorf("SMTP 服务器未配置")
|
| 47 |
+
}
|
| 48 |
+
encodedSubject := fmt.Sprintf("=?UTF-8?B?%s?=", base64.StdEncoding.EncodeToString([]byte(subject)))
|
| 49 |
+
mail := []byte(fmt.Sprintf("To: %s\r\n"+
|
| 50 |
+
"From: %s<%s>\r\n"+
|
| 51 |
+
"Subject: %s\r\n"+
|
| 52 |
+
"Date: %s\r\n"+
|
| 53 |
+
"Message-ID: %s\r\n"+ // 添加 Message-ID 头
|
| 54 |
+
"Content-Type: text/html; charset=UTF-8\r\n\r\n%s\r\n",
|
| 55 |
+
receiver, SystemName, SMTPFrom, encodedSubject, time.Now().Format(time.RFC1123Z), id, content))
|
| 56 |
+
auth := smtp.PlainAuth("", SMTPAccount, SMTPToken, SMTPServer)
|
| 57 |
+
addr := fmt.Sprintf("%s:%d", SMTPServer, SMTPPort)
|
| 58 |
+
to := strings.Split(receiver, ";")
|
| 59 |
+
var err error
|
| 60 |
+
if SMTPPort == 465 || SMTPSSLEnabled {
|
| 61 |
+
tlsConfig := &tls.Config{
|
| 62 |
+
InsecureSkipVerify: true,
|
| 63 |
+
ServerName: SMTPServer,
|
| 64 |
+
}
|
| 65 |
+
conn, err := tls.Dial("tcp", fmt.Sprintf("%s:%d", SMTPServer, SMTPPort), tlsConfig)
|
| 66 |
+
if err != nil {
|
| 67 |
+
return err
|
| 68 |
+
}
|
| 69 |
+
client, err := smtp.NewClient(conn, SMTPServer)
|
| 70 |
+
if err != nil {
|
| 71 |
+
return err
|
| 72 |
+
}
|
| 73 |
+
defer client.Close()
|
| 74 |
+
if err = client.Auth(auth); err != nil {
|
| 75 |
+
return err
|
| 76 |
+
}
|
| 77 |
+
if err = client.Mail(SMTPFrom); err != nil {
|
| 78 |
+
return err
|
| 79 |
+
}
|
| 80 |
+
receiverEmails := strings.Split(receiver, ";")
|
| 81 |
+
for _, receiver := range receiverEmails {
|
| 82 |
+
if err = client.Rcpt(receiver); err != nil {
|
| 83 |
+
return err
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
w, err := client.Data()
|
| 87 |
+
if err != nil {
|
| 88 |
+
return err
|
| 89 |
+
}
|
| 90 |
+
_, err = w.Write(mail)
|
| 91 |
+
if err != nil {
|
| 92 |
+
return err
|
| 93 |
+
}
|
| 94 |
+
err = w.Close()
|
| 95 |
+
if err != nil {
|
| 96 |
+
return err
|
| 97 |
+
}
|
| 98 |
+
} else if isOutlookServer(SMTPAccount) || SMTPServer == "smtp.azurecomm.net" {
|
| 99 |
+
auth = LoginAuth(SMTPAccount, SMTPToken)
|
| 100 |
+
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
| 101 |
+
} else {
|
| 102 |
+
err = smtp.SendMail(addr, auth, SMTPFrom, to, mail)
|
| 103 |
+
}
|
| 104 |
+
return err
|
| 105 |
+
}
|
common/embed-file-system.go
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"embed"
|
| 21 |
+
"github.com/gin-contrib/static"
|
| 22 |
+
"io/fs"
|
| 23 |
+
"net/http"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
// Credit: https://github.com/gin-contrib/static/issues/19
|
| 27 |
+
|
| 28 |
+
type embedFileSystem struct {
|
| 29 |
+
http.FileSystem
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func (e embedFileSystem) Exists(prefix string, path string) bool {
|
| 33 |
+
_, err := e.Open(path)
|
| 34 |
+
if err != nil {
|
| 35 |
+
return false
|
| 36 |
+
}
|
| 37 |
+
return true
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
func EmbedFolder(fsEmbed embed.FS, targetPath string) static.ServeFileSystem {
|
| 41 |
+
efs, err := fs.Sub(fsEmbed, targetPath)
|
| 42 |
+
if err != nil {
|
| 43 |
+
panic(err)
|
| 44 |
+
}
|
| 45 |
+
return embedFileSystem{
|
| 46 |
+
FileSystem: http.FS(efs),
|
| 47 |
+
}
|
| 48 |
+
}
|
common/env.go
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"fmt"
|
| 21 |
+
"os"
|
| 22 |
+
"strconv"
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
func GetEnvOrDefault(env string, defaultValue int) int {
|
| 26 |
+
if env == "" || os.Getenv(env) == "" {
|
| 27 |
+
return defaultValue
|
| 28 |
+
}
|
| 29 |
+
num, err := strconv.Atoi(os.Getenv(env))
|
| 30 |
+
if err != nil {
|
| 31 |
+
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %d", env, err.Error(), defaultValue))
|
| 32 |
+
return defaultValue
|
| 33 |
+
}
|
| 34 |
+
return num
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func GetEnvOrDefaultString(env string, defaultValue string) string {
|
| 38 |
+
if env == "" || os.Getenv(env) == "" {
|
| 39 |
+
return defaultValue
|
| 40 |
+
}
|
| 41 |
+
return os.Getenv(env)
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func GetEnvOrDefaultBool(env string, defaultValue bool) bool {
|
| 45 |
+
if env == "" || os.Getenv(env) == "" {
|
| 46 |
+
return defaultValue
|
| 47 |
+
}
|
| 48 |
+
b, err := strconv.ParseBool(os.Getenv(env))
|
| 49 |
+
if err != nil {
|
| 50 |
+
SysError(fmt.Sprintf("failed to parse %s: %s, using default value: %t", env, err.Error(), defaultValue))
|
| 51 |
+
return defaultValue
|
| 52 |
+
}
|
| 53 |
+
return b
|
| 54 |
+
}
|
common/gin.go
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"bytes"
|
| 21 |
+
"encoding/json"
|
| 22 |
+
"github.com/gin-gonic/gin"
|
| 23 |
+
"io"
|
| 24 |
+
"strings"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
const KeyRequestBody = "key_request_body"
|
| 28 |
+
|
| 29 |
+
func GetRequestBody(c *gin.Context) ([]byte, error) {
|
| 30 |
+
requestBody, _ := c.Get(KeyRequestBody)
|
| 31 |
+
if requestBody != nil {
|
| 32 |
+
return requestBody.([]byte), nil
|
| 33 |
+
}
|
| 34 |
+
requestBody, err := io.ReadAll(c.Request.Body)
|
| 35 |
+
if err != nil {
|
| 36 |
+
return nil, err
|
| 37 |
+
}
|
| 38 |
+
_ = c.Request.Body.Close()
|
| 39 |
+
c.Set(KeyRequestBody, requestBody)
|
| 40 |
+
return requestBody.([]byte), nil
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func UnmarshalBodyReusable(c *gin.Context, v any) error {
|
| 44 |
+
requestBody, err := GetRequestBody(c)
|
| 45 |
+
if err != nil {
|
| 46 |
+
return err
|
| 47 |
+
}
|
| 48 |
+
contentType := c.Request.Header.Get("Content-Type")
|
| 49 |
+
if strings.HasPrefix(contentType, "application/json") {
|
| 50 |
+
err = json.Unmarshal(requestBody, &v)
|
| 51 |
+
} else {
|
| 52 |
+
// skip for now
|
| 53 |
+
// TODO: someday non json request have variant model, we will need to implementation this
|
| 54 |
+
}
|
| 55 |
+
if err != nil {
|
| 56 |
+
return err
|
| 57 |
+
}
|
| 58 |
+
// Reset request body
|
| 59 |
+
c.Request.Body = io.NopCloser(bytes.NewBuffer(requestBody))
|
| 60 |
+
return nil
|
| 61 |
+
}
|
common/go-channel.go
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"time"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
func SafeSendBool(ch chan bool, value bool) (closed bool) {
|
| 24 |
+
defer func() {
|
| 25 |
+
// Recover from panic if one occurred. A panic would mean the channel was closed.
|
| 26 |
+
if recover() != nil {
|
| 27 |
+
closed = true
|
| 28 |
+
}
|
| 29 |
+
}()
|
| 30 |
+
|
| 31 |
+
// This will panic if the channel is closed.
|
| 32 |
+
ch <- value
|
| 33 |
+
|
| 34 |
+
// If the code reaches here, then the channel was not closed.
|
| 35 |
+
return false
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func SafeSendString(ch chan string, value string) (closed bool) {
|
| 39 |
+
defer func() {
|
| 40 |
+
// Recover from panic if one occurred. A panic would mean the channel was closed.
|
| 41 |
+
if recover() != nil {
|
| 42 |
+
closed = true
|
| 43 |
+
}
|
| 44 |
+
}()
|
| 45 |
+
|
| 46 |
+
// This will panic if the channel is closed.
|
| 47 |
+
ch <- value
|
| 48 |
+
|
| 49 |
+
// If the code reaches here, then the channel was not closed.
|
| 50 |
+
return false
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
// SafeSendStringTimeout send, return true, else return false
|
| 54 |
+
func SafeSendStringTimeout(ch chan string, value string, timeout int) (closed bool) {
|
| 55 |
+
defer func() {
|
| 56 |
+
// Recover from panic if one occurred. A panic would mean the channel was closed.
|
| 57 |
+
if recover() != nil {
|
| 58 |
+
closed = false
|
| 59 |
+
}
|
| 60 |
+
}()
|
| 61 |
+
|
| 62 |
+
// This will panic if the channel is closed.
|
| 63 |
+
select {
|
| 64 |
+
case ch <- value:
|
| 65 |
+
return true
|
| 66 |
+
case <-time.After(time.Duration(timeout) * time.Second):
|
| 67 |
+
return false
|
| 68 |
+
}
|
| 69 |
+
}
|
common/gopool.go
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"context"
|
| 21 |
+
"fmt"
|
| 22 |
+
"github.com/bytedance/gopkg/util/gopool"
|
| 23 |
+
"math"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
var relayGoPool gopool.Pool
|
| 27 |
+
|
| 28 |
+
func init() {
|
| 29 |
+
relayGoPool = gopool.NewPool("gopool.RelayPool", math.MaxInt32, gopool.NewConfig())
|
| 30 |
+
relayGoPool.SetPanicHandler(func(ctx context.Context, i interface{}) {
|
| 31 |
+
if stopChan, ok := ctx.Value("stop_chan").(chan bool); ok {
|
| 32 |
+
SafeSendBool(stopChan, true)
|
| 33 |
+
}
|
| 34 |
+
SysError(fmt.Sprintf("panic in gopool.RelayPool: %v", i))
|
| 35 |
+
})
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
func RelayCtxGo(ctx context.Context, f func()) {
|
| 39 |
+
relayGoPool.CtxGo(ctx, f)
|
| 40 |
+
}
|
common/init.go
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"flag"
|
| 21 |
+
"fmt"
|
| 22 |
+
"log"
|
| 23 |
+
"os"
|
| 24 |
+
"path/filepath"
|
| 25 |
+
"strconv"
|
| 26 |
+
"time"
|
| 27 |
+
)
|
| 28 |
+
|
| 29 |
+
var (
|
| 30 |
+
Port = flag.Int("port", 3000, "the listening port")
|
| 31 |
+
PrintVersion = flag.Bool("version", false, "print version and exit")
|
| 32 |
+
PrintHelp = flag.Bool("help", false, "print help and exit")
|
| 33 |
+
LogDir = flag.String("log-dir", "./logs", "specify the log directory")
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
func printHelp() {
|
| 37 |
+
fmt.Println("New API " + Version + " - All in one API service for OpenAI API.")
|
| 38 |
+
fmt.Println("Copyright (C) 2023 JustSong. All rights reserved.")
|
| 39 |
+
fmt.Println("GitHub: https://github.com/songquanpeng/veloera")
|
| 40 |
+
fmt.Println("Usage: veloera [--port <port>] [--log-dir <log directory>] [--version] [--help]")
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
func LoadEnv() {
|
| 44 |
+
flag.Parse()
|
| 45 |
+
|
| 46 |
+
if *PrintVersion {
|
| 47 |
+
fmt.Println(Version)
|
| 48 |
+
os.Exit(0)
|
| 49 |
+
}
|
| 50 |
+
|
| 51 |
+
if *PrintHelp {
|
| 52 |
+
printHelp()
|
| 53 |
+
os.Exit(0)
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
if os.Getenv("SESSION_SECRET") != "" {
|
| 57 |
+
ss := os.Getenv("SESSION_SECRET")
|
| 58 |
+
if ss == "random_string" {
|
| 59 |
+
log.Println("WARNING: SESSION_SECRET is set to the default value 'random_string', please change it to a random string.")
|
| 60 |
+
log.Println("警告:SESSION_SECRET被设置为默认值'random_string',请修改为随机字符串。")
|
| 61 |
+
log.Fatal("Please set SESSION_SECRET to a random string.")
|
| 62 |
+
} else {
|
| 63 |
+
SessionSecret = ss
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
if os.Getenv("CRYPTO_SECRET") != "" {
|
| 67 |
+
CryptoSecret = os.Getenv("CRYPTO_SECRET")
|
| 68 |
+
} else {
|
| 69 |
+
CryptoSecret = SessionSecret
|
| 70 |
+
}
|
| 71 |
+
if os.Getenv("SQLITE_PATH") != "" {
|
| 72 |
+
SQLitePath = os.Getenv("SQLITE_PATH")
|
| 73 |
+
}
|
| 74 |
+
if *LogDir != "" {
|
| 75 |
+
var err error
|
| 76 |
+
*LogDir, err = filepath.Abs(*LogDir)
|
| 77 |
+
if err != nil {
|
| 78 |
+
log.Fatal(err)
|
| 79 |
+
}
|
| 80 |
+
if _, err := os.Stat(*LogDir); os.IsNotExist(err) {
|
| 81 |
+
err = os.Mkdir(*LogDir, 0777)
|
| 82 |
+
if err != nil {
|
| 83 |
+
log.Fatal(err)
|
| 84 |
+
}
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// Initialize variables from constants.go that were using environment variables
|
| 89 |
+
DebugEnabled = os.Getenv("DEBUG") == "true"
|
| 90 |
+
MemoryCacheEnabled = os.Getenv("MEMORY_CACHE_ENABLED") == "true"
|
| 91 |
+
IsMasterNode = os.Getenv("NODE_TYPE") != "slave"
|
| 92 |
+
|
| 93 |
+
// Parse requestInterval and set RequestInterval
|
| 94 |
+
requestInterval, _ = strconv.Atoi(os.Getenv("POLLING_INTERVAL"))
|
| 95 |
+
RequestInterval = time.Duration(requestInterval) * time.Second
|
| 96 |
+
|
| 97 |
+
// Initialize variables with GetEnvOrDefault
|
| 98 |
+
SyncFrequency = GetEnvOrDefault("SYNC_FREQUENCY", 60)
|
| 99 |
+
BatchUpdateInterval = GetEnvOrDefault("BATCH_UPDATE_INTERVAL", 5)
|
| 100 |
+
RelayTimeout = GetEnvOrDefault("RELAY_TIMEOUT", 0)
|
| 101 |
+
|
| 102 |
+
// Initialize string variables with GetEnvOrDefaultString
|
| 103 |
+
GeminiSafetySetting = GetEnvOrDefaultString("GEMINI_SAFETY_SETTING", "BLOCK_NONE")
|
| 104 |
+
CohereSafetySetting = GetEnvOrDefaultString("COHERE_SAFETY_SETTING", "NONE")
|
| 105 |
+
|
| 106 |
+
// Initialize rate limit variables
|
| 107 |
+
GlobalApiRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_API_RATE_LIMIT_ENABLE", true)
|
| 108 |
+
GlobalApiRateLimitNum = GetEnvOrDefault("GLOBAL_API_RATE_LIMIT", 180)
|
| 109 |
+
GlobalApiRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_API_RATE_LIMIT_DURATION", 180))
|
| 110 |
+
|
| 111 |
+
GlobalWebRateLimitEnable = GetEnvOrDefaultBool("GLOBAL_WEB_RATE_LIMIT_ENABLE", true)
|
| 112 |
+
GlobalWebRateLimitNum = GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT", 60)
|
| 113 |
+
GlobalWebRateLimitDuration = int64(GetEnvOrDefault("GLOBAL_WEB_RATE_LIMIT_DURATION", 180))
|
| 114 |
+
}
|
common/json.go
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"bytes"
|
| 21 |
+
"encoding/json"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
func DecodeJson(data []byte, v any) error {
|
| 25 |
+
return json.NewDecoder(bytes.NewReader(data)).Decode(v)
|
| 26 |
+
}
|
| 27 |
+
|
| 28 |
+
func DecodeJsonStr(data string, v any) error {
|
| 29 |
+
return DecodeJson(StringToByteSlice(data), v)
|
| 30 |
+
}
|
| 31 |
+
|
| 32 |
+
func EncodeJson(v any) ([]byte, error) {
|
| 33 |
+
return json.Marshal(v)
|
| 34 |
+
}
|
common/logger.go
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"context"
|
| 21 |
+
"encoding/json"
|
| 22 |
+
"fmt"
|
| 23 |
+
"io"
|
| 24 |
+
"log"
|
| 25 |
+
"os"
|
| 26 |
+
"path/filepath"
|
| 27 |
+
"sync"
|
| 28 |
+
"time"
|
| 29 |
+
|
| 30 |
+
"github.com/bytedance/gopkg/util/gopool"
|
| 31 |
+
"github.com/gin-gonic/gin"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
const (
|
| 35 |
+
loggerINFO = "INFO"
|
| 36 |
+
loggerWarn = "WARN"
|
| 37 |
+
loggerError = "ERR"
|
| 38 |
+
)
|
| 39 |
+
|
| 40 |
+
const maxLogCount = 1000000
|
| 41 |
+
|
| 42 |
+
var logCount int
|
| 43 |
+
var setupLogLock sync.Mutex
|
| 44 |
+
var setupLogWorking bool
|
| 45 |
+
|
| 46 |
+
func SetupLogger() {
|
| 47 |
+
if *LogDir != "" {
|
| 48 |
+
ok := setupLogLock.TryLock()
|
| 49 |
+
if !ok {
|
| 50 |
+
log.Println("setup log is already working")
|
| 51 |
+
return
|
| 52 |
+
}
|
| 53 |
+
defer func() {
|
| 54 |
+
setupLogLock.Unlock()
|
| 55 |
+
setupLogWorking = false
|
| 56 |
+
}()
|
| 57 |
+
logPath := filepath.Join(*LogDir, fmt.Sprintf("veloera-%s.log", time.Now().Format("20060102150405")))
|
| 58 |
+
fd, err := os.OpenFile(logPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
| 59 |
+
if err != nil {
|
| 60 |
+
log.Fatal("failed to open log file")
|
| 61 |
+
}
|
| 62 |
+
gin.DefaultWriter = io.MultiWriter(os.Stdout, fd)
|
| 63 |
+
gin.DefaultErrorWriter = io.MultiWriter(os.Stderr, fd)
|
| 64 |
+
}
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
func SysLog(s string) {
|
| 68 |
+
t := time.Now()
|
| 69 |
+
_, _ = fmt.Fprintf(gin.DefaultWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func SysError(s string) {
|
| 73 |
+
t := time.Now()
|
| 74 |
+
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[SYS] %v | %s \n", t.Format("2006/01/02 - 15:04:05"), s)
|
| 75 |
+
}
|
| 76 |
+
|
| 77 |
+
func LogInfo(ctx context.Context, msg string) {
|
| 78 |
+
logHelper(ctx, loggerINFO, msg)
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
func LogWarn(ctx context.Context, msg string) {
|
| 82 |
+
logHelper(ctx, loggerWarn, msg)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
func LogError(ctx context.Context, msg string) {
|
| 86 |
+
logHelper(ctx, loggerError, msg)
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
func logHelper(ctx context.Context, level string, msg string) {
|
| 90 |
+
writer := gin.DefaultErrorWriter
|
| 91 |
+
if level == loggerINFO {
|
| 92 |
+
writer = gin.DefaultWriter
|
| 93 |
+
}
|
| 94 |
+
id := ctx.Value(RequestIdKey)
|
| 95 |
+
now := time.Now()
|
| 96 |
+
_, _ = fmt.Fprintf(writer, "[%s] %v | %s | %s \n", level, now.Format("2006/01/02 - 15:04:05"), id, msg)
|
| 97 |
+
logCount++ // we don't need accurate count, so no lock here
|
| 98 |
+
if logCount > maxLogCount && !setupLogWorking {
|
| 99 |
+
logCount = 0
|
| 100 |
+
setupLogWorking = true
|
| 101 |
+
gopool.Go(func() {
|
| 102 |
+
SetupLogger()
|
| 103 |
+
})
|
| 104 |
+
}
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
func FatalLog(v ...any) {
|
| 108 |
+
t := time.Now()
|
| 109 |
+
_, _ = fmt.Fprintf(gin.DefaultErrorWriter, "[FATAL] %v | %v \n", t.Format("2006/01/02 - 15:04:05"), v)
|
| 110 |
+
os.Exit(1)
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
func LogQuota(quota int) string {
|
| 114 |
+
if DisplayInCurrencyEnabled {
|
| 115 |
+
return fmt.Sprintf("$%.6f 额度", float64(quota)/QuotaPerUnit)
|
| 116 |
+
} else {
|
| 117 |
+
return fmt.Sprintf("%d 点额度", quota)
|
| 118 |
+
}
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
func FormatQuota(quota int) string {
|
| 122 |
+
if DisplayInCurrencyEnabled {
|
| 123 |
+
return fmt.Sprintf("$%.6f", float64(quota)/QuotaPerUnit)
|
| 124 |
+
} else {
|
| 125 |
+
return fmt.Sprintf("%d", quota)
|
| 126 |
+
}
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// LogJson 仅供测试使用 only for test
|
| 130 |
+
func LogJson(ctx context.Context, msg string, obj any) {
|
| 131 |
+
jsonStr, err := json.Marshal(obj)
|
| 132 |
+
if err != nil {
|
| 133 |
+
LogError(ctx, fmt.Sprintf("json marshal failed: %s", err.Error()))
|
| 134 |
+
return
|
| 135 |
+
}
|
| 136 |
+
LogInfo(ctx, fmt.Sprintf("%s | %s", msg, string(jsonStr)))
|
| 137 |
+
}
|
common/pprof.go
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"fmt"
|
| 21 |
+
"github.com/shirou/gopsutil/cpu"
|
| 22 |
+
"os"
|
| 23 |
+
"runtime/pprof"
|
| 24 |
+
"time"
|
| 25 |
+
)
|
| 26 |
+
|
| 27 |
+
// Monitor 定时监控cpu使用率,超过阈值输出pprof文件
|
| 28 |
+
func Monitor() {
|
| 29 |
+
for {
|
| 30 |
+
percent, err := cpu.Percent(time.Second, false)
|
| 31 |
+
if err != nil {
|
| 32 |
+
panic(err)
|
| 33 |
+
}
|
| 34 |
+
if percent[0] > 80 {
|
| 35 |
+
fmt.Println("cpu usage too high")
|
| 36 |
+
// write pprof file
|
| 37 |
+
if _, err := os.Stat("./pprof"); os.IsNotExist(err) {
|
| 38 |
+
err := os.Mkdir("./pprof", os.ModePerm)
|
| 39 |
+
if err != nil {
|
| 40 |
+
SysLog("创建pprof文件夹失败 " + err.Error())
|
| 41 |
+
continue
|
| 42 |
+
}
|
| 43 |
+
}
|
| 44 |
+
f, err := os.Create("./pprof/" + fmt.Sprintf("cpu-%s.pprof", time.Now().Format("20060102150405")))
|
| 45 |
+
if err != nil {
|
| 46 |
+
SysLog("创建pprof文件失败 " + err.Error())
|
| 47 |
+
continue
|
| 48 |
+
}
|
| 49 |
+
err = pprof.StartCPUProfile(f)
|
| 50 |
+
if err != nil {
|
| 51 |
+
SysLog("启动pprof失败 " + err.Error())
|
| 52 |
+
continue
|
| 53 |
+
}
|
| 54 |
+
time.Sleep(10 * time.Second) // profile for 30 seconds
|
| 55 |
+
pprof.StopCPUProfile()
|
| 56 |
+
f.Close()
|
| 57 |
+
}
|
| 58 |
+
time.Sleep(30 * time.Second)
|
| 59 |
+
}
|
| 60 |
+
}
|
common/rate-limit.go
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"sync"
|
| 21 |
+
"time"
|
| 22 |
+
)
|
| 23 |
+
|
| 24 |
+
type InMemoryRateLimiter struct {
|
| 25 |
+
store map[string]*[]int64
|
| 26 |
+
mutex sync.Mutex
|
| 27 |
+
expirationDuration time.Duration
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
func (l *InMemoryRateLimiter) Init(expirationDuration time.Duration) {
|
| 31 |
+
if l.store == nil {
|
| 32 |
+
l.mutex.Lock()
|
| 33 |
+
if l.store == nil {
|
| 34 |
+
l.store = make(map[string]*[]int64)
|
| 35 |
+
l.expirationDuration = expirationDuration
|
| 36 |
+
if expirationDuration > 0 {
|
| 37 |
+
go l.clearExpiredItems()
|
| 38 |
+
}
|
| 39 |
+
}
|
| 40 |
+
l.mutex.Unlock()
|
| 41 |
+
}
|
| 42 |
+
}
|
| 43 |
+
|
| 44 |
+
func (l *InMemoryRateLimiter) clearExpiredItems() {
|
| 45 |
+
for {
|
| 46 |
+
time.Sleep(l.expirationDuration)
|
| 47 |
+
l.mutex.Lock()
|
| 48 |
+
now := time.Now().Unix()
|
| 49 |
+
for key := range l.store {
|
| 50 |
+
queue := l.store[key]
|
| 51 |
+
size := len(*queue)
|
| 52 |
+
if size == 0 || now-(*queue)[size-1] > int64(l.expirationDuration.Seconds()) {
|
| 53 |
+
delete(l.store, key)
|
| 54 |
+
}
|
| 55 |
+
}
|
| 56 |
+
l.mutex.Unlock()
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
// Request parameter duration's unit is seconds
|
| 61 |
+
func (l *InMemoryRateLimiter) Request(key string, maxRequestNum int, duration int64) bool {
|
| 62 |
+
l.mutex.Lock()
|
| 63 |
+
defer l.mutex.Unlock()
|
| 64 |
+
// [old <-- new]
|
| 65 |
+
queue, ok := l.store[key]
|
| 66 |
+
now := time.Now().Unix()
|
| 67 |
+
if ok {
|
| 68 |
+
if len(*queue) < maxRequestNum {
|
| 69 |
+
*queue = append(*queue, now)
|
| 70 |
+
return true
|
| 71 |
+
} else {
|
| 72 |
+
if now-(*queue)[0] >= duration {
|
| 73 |
+
*queue = (*queue)[1:]
|
| 74 |
+
*queue = append(*queue, now)
|
| 75 |
+
return true
|
| 76 |
+
} else {
|
| 77 |
+
return false
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
} else {
|
| 81 |
+
s := make([]int64, 0, maxRequestNum)
|
| 82 |
+
l.store[key] = &s
|
| 83 |
+
*(l.store[key]) = append(*(l.store[key]), now)
|
| 84 |
+
}
|
| 85 |
+
return true
|
| 86 |
+
}
|
common/redis.go
ADDED
|
@@ -0,0 +1,336 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"context"
|
| 21 |
+
"errors"
|
| 22 |
+
"fmt"
|
| 23 |
+
"os"
|
| 24 |
+
"reflect"
|
| 25 |
+
"strconv"
|
| 26 |
+
"time"
|
| 27 |
+
|
| 28 |
+
"github.com/go-redis/redis/v8"
|
| 29 |
+
"gorm.io/gorm"
|
| 30 |
+
)
|
| 31 |
+
|
| 32 |
+
var RDB *redis.Client
|
| 33 |
+
var RedisEnabled = true
|
| 34 |
+
|
| 35 |
+
// InitRedisClient This function is called after init()
|
| 36 |
+
func InitRedisClient() (err error) {
|
| 37 |
+
if os.Getenv("REDIS_CONN_STRING") == "" {
|
| 38 |
+
RedisEnabled = false
|
| 39 |
+
SysLog("REDIS_CONN_STRING not set, Redis is not enabled")
|
| 40 |
+
return nil
|
| 41 |
+
}
|
| 42 |
+
if os.Getenv("SYNC_FREQUENCY") == "" {
|
| 43 |
+
SysLog("SYNC_FREQUENCY not set, use default value 60")
|
| 44 |
+
SyncFrequency = 60
|
| 45 |
+
}
|
| 46 |
+
SysLog("Redis is enabled")
|
| 47 |
+
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
| 48 |
+
if err != nil {
|
| 49 |
+
FatalLog("failed to parse Redis connection string: " + err.Error())
|
| 50 |
+
}
|
| 51 |
+
opt.PoolSize = GetEnvOrDefault("REDIS_POOL_SIZE", 10)
|
| 52 |
+
RDB = redis.NewClient(opt)
|
| 53 |
+
|
| 54 |
+
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
| 55 |
+
defer cancel()
|
| 56 |
+
|
| 57 |
+
_, err = RDB.Ping(ctx).Result()
|
| 58 |
+
if err != nil {
|
| 59 |
+
FatalLog("Redis ping test failed: " + err.Error())
|
| 60 |
+
}
|
| 61 |
+
if DebugEnabled {
|
| 62 |
+
SysLog(fmt.Sprintf("Redis connected to %s", opt.Addr))
|
| 63 |
+
SysLog(fmt.Sprintf("Redis database: %d", opt.DB))
|
| 64 |
+
}
|
| 65 |
+
return err
|
| 66 |
+
}
|
| 67 |
+
|
| 68 |
+
func ParseRedisOption() *redis.Options {
|
| 69 |
+
opt, err := redis.ParseURL(os.Getenv("REDIS_CONN_STRING"))
|
| 70 |
+
if err != nil {
|
| 71 |
+
FatalLog("failed to parse Redis connection string: " + err.Error())
|
| 72 |
+
}
|
| 73 |
+
return opt
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
func RedisSet(key string, value string, expiration time.Duration) error {
|
| 77 |
+
if DebugEnabled {
|
| 78 |
+
SysLog(fmt.Sprintf("Redis SET: key=%s, value=%s, expiration=%v", key, value, expiration))
|
| 79 |
+
}
|
| 80 |
+
ctx := context.Background()
|
| 81 |
+
return RDB.Set(ctx, key, value, expiration).Err()
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
func RedisGet(key string) (string, error) {
|
| 85 |
+
if DebugEnabled {
|
| 86 |
+
SysLog(fmt.Sprintf("Redis GET: key=%s", key))
|
| 87 |
+
}
|
| 88 |
+
ctx := context.Background()
|
| 89 |
+
val, err := RDB.Get(ctx, key).Result()
|
| 90 |
+
return val, err
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
//func RedisExpire(key string, expiration time.Duration) error {
|
| 94 |
+
// ctx := context.Background()
|
| 95 |
+
// return RDB.Expire(ctx, key, expiration).Err()
|
| 96 |
+
//}
|
| 97 |
+
//
|
| 98 |
+
//func RedisGetEx(key string, expiration time.Duration) (string, error) {
|
| 99 |
+
// ctx := context.Background()
|
| 100 |
+
// return RDB.GetSet(ctx, key, expiration).Result()
|
| 101 |
+
//}
|
| 102 |
+
|
| 103 |
+
func RedisDel(key string) error {
|
| 104 |
+
if DebugEnabled {
|
| 105 |
+
SysLog(fmt.Sprintf("Redis DEL: key=%s", key))
|
| 106 |
+
}
|
| 107 |
+
ctx := context.Background()
|
| 108 |
+
return RDB.Del(ctx, key).Err()
|
| 109 |
+
}
|
| 110 |
+
|
| 111 |
+
func RedisHDelObj(key string) error {
|
| 112 |
+
if DebugEnabled {
|
| 113 |
+
SysLog(fmt.Sprintf("Redis HDEL: key=%s", key))
|
| 114 |
+
}
|
| 115 |
+
ctx := context.Background()
|
| 116 |
+
return RDB.Del(ctx, key).Err()
|
| 117 |
+
// TODO: Use RedisDel.
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
func RedisHSetObj(key string, obj interface{}, expiration time.Duration) error {
|
| 121 |
+
if DebugEnabled {
|
| 122 |
+
SysLog(fmt.Sprintf("Redis HSET: key=%s, obj=%+v, expiration=%v", key, obj, expiration))
|
| 123 |
+
}
|
| 124 |
+
ctx := context.Background()
|
| 125 |
+
|
| 126 |
+
data := make(map[string]interface{})
|
| 127 |
+
|
| 128 |
+
// 使用反射遍历结构体字段
|
| 129 |
+
v := reflect.ValueOf(obj).Elem()
|
| 130 |
+
t := v.Type()
|
| 131 |
+
for i := 0; i < v.NumField(); i++ {
|
| 132 |
+
field := t.Field(i)
|
| 133 |
+
value := v.Field(i)
|
| 134 |
+
|
| 135 |
+
// Skip DeletedAt field
|
| 136 |
+
if field.Type.String() == "gorm.DeletedAt" {
|
| 137 |
+
continue
|
| 138 |
+
}
|
| 139 |
+
|
| 140 |
+
// 处理指针类型
|
| 141 |
+
if value.Kind() == reflect.Ptr {
|
| 142 |
+
if value.IsNil() {
|
| 143 |
+
data[field.Name] = ""
|
| 144 |
+
continue
|
| 145 |
+
}
|
| 146 |
+
value = value.Elem()
|
| 147 |
+
}
|
| 148 |
+
|
| 149 |
+
// 处理布尔类型
|
| 150 |
+
if value.Kind() == reflect.Bool {
|
| 151 |
+
data[field.Name] = strconv.FormatBool(value.Bool())
|
| 152 |
+
continue
|
| 153 |
+
}
|
| 154 |
+
|
| 155 |
+
// 其他类型直接转换为字符串
|
| 156 |
+
data[field.Name] = fmt.Sprintf("%v", value.Interface())
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
txn := RDB.TxPipeline()
|
| 160 |
+
txn.HSet(ctx, key, data)
|
| 161 |
+
txn.Expire(ctx, key, expiration)
|
| 162 |
+
|
| 163 |
+
_, err := txn.Exec(ctx)
|
| 164 |
+
if err != nil {
|
| 165 |
+
return fmt.Errorf("failed to execute transaction: %w", err)
|
| 166 |
+
}
|
| 167 |
+
return nil
|
| 168 |
+
}
|
| 169 |
+
|
| 170 |
+
func RedisHGetObj(key string, obj interface{}) error {
|
| 171 |
+
if DebugEnabled {
|
| 172 |
+
SysLog(fmt.Sprintf("Redis HGETALL: key=%s", key))
|
| 173 |
+
}
|
| 174 |
+
ctx := context.Background()
|
| 175 |
+
|
| 176 |
+
result, err := RDB.HGetAll(ctx, key).Result()
|
| 177 |
+
if err != nil {
|
| 178 |
+
return fmt.Errorf("failed to load hash from Redis: %w", err)
|
| 179 |
+
}
|
| 180 |
+
|
| 181 |
+
if len(result) == 0 {
|
| 182 |
+
return fmt.Errorf("key %s not found in Redis", key)
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
// Handle both pointer and non-pointer values
|
| 186 |
+
val := reflect.ValueOf(obj)
|
| 187 |
+
if val.Kind() != reflect.Ptr {
|
| 188 |
+
return fmt.Errorf("obj must be a pointer to a struct, got %T", obj)
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
v := val.Elem()
|
| 192 |
+
if v.Kind() != reflect.Struct {
|
| 193 |
+
return fmt.Errorf("obj must be a pointer to a struct, got pointer to %T", v.Interface())
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
t := v.Type()
|
| 197 |
+
for i := 0; i < v.NumField(); i++ {
|
| 198 |
+
field := t.Field(i)
|
| 199 |
+
fieldName := field.Name
|
| 200 |
+
if value, ok := result[fieldName]; ok {
|
| 201 |
+
fieldValue := v.Field(i)
|
| 202 |
+
|
| 203 |
+
// Handle pointer types
|
| 204 |
+
if fieldValue.Kind() == reflect.Ptr {
|
| 205 |
+
if value == "" {
|
| 206 |
+
continue
|
| 207 |
+
}
|
| 208 |
+
if fieldValue.IsNil() {
|
| 209 |
+
fieldValue.Set(reflect.New(fieldValue.Type().Elem()))
|
| 210 |
+
}
|
| 211 |
+
fieldValue = fieldValue.Elem()
|
| 212 |
+
}
|
| 213 |
+
|
| 214 |
+
// Enhanced type handling for Token struct
|
| 215 |
+
switch fieldValue.Kind() {
|
| 216 |
+
case reflect.String:
|
| 217 |
+
fieldValue.SetString(value)
|
| 218 |
+
case reflect.Int, reflect.Int64:
|
| 219 |
+
intValue, err := strconv.ParseInt(value, 10, 64)
|
| 220 |
+
if err != nil {
|
| 221 |
+
return fmt.Errorf("failed to parse int field %s: %w", fieldName, err)
|
| 222 |
+
}
|
| 223 |
+
fieldValue.SetInt(intValue)
|
| 224 |
+
case reflect.Bool:
|
| 225 |
+
boolValue, err := strconv.ParseBool(value)
|
| 226 |
+
if err != nil {
|
| 227 |
+
return fmt.Errorf("failed to parse bool field %s: %w", fieldName, err)
|
| 228 |
+
}
|
| 229 |
+
fieldValue.SetBool(boolValue)
|
| 230 |
+
case reflect.Struct:
|
| 231 |
+
// Special handling for gorm.DeletedAt
|
| 232 |
+
if fieldValue.Type().String() == "gorm.DeletedAt" {
|
| 233 |
+
if value != "" {
|
| 234 |
+
timeValue, err := time.Parse(time.RFC3339, value)
|
| 235 |
+
if err != nil {
|
| 236 |
+
return fmt.Errorf("failed to parse DeletedAt field %s: %w", fieldName, err)
|
| 237 |
+
}
|
| 238 |
+
fieldValue.Set(reflect.ValueOf(gorm.DeletedAt{Time: timeValue, Valid: true}))
|
| 239 |
+
}
|
| 240 |
+
}
|
| 241 |
+
default:
|
| 242 |
+
return fmt.Errorf("unsupported field type: %s for field %s", fieldValue.Kind(), fieldName)
|
| 243 |
+
}
|
| 244 |
+
}
|
| 245 |
+
}
|
| 246 |
+
|
| 247 |
+
return nil
|
| 248 |
+
}
|
| 249 |
+
|
| 250 |
+
// RedisIncr Add this function to handle atomic increments
|
| 251 |
+
func RedisIncr(key string, delta int64) error {
|
| 252 |
+
if DebugEnabled {
|
| 253 |
+
SysLog(fmt.Sprintf("Redis INCR: key=%s, delta=%d", key, delta))
|
| 254 |
+
}
|
| 255 |
+
// 检查键的剩余生存时间
|
| 256 |
+
ttlCmd := RDB.TTL(context.Background(), key)
|
| 257 |
+
ttl, err := ttlCmd.Result()
|
| 258 |
+
if err != nil && !errors.Is(err, redis.Nil) {
|
| 259 |
+
return fmt.Errorf("failed to get TTL: %w", err)
|
| 260 |
+
}
|
| 261 |
+
|
| 262 |
+
// 只有在 key 存在且有 TTL 时才需要特殊处理
|
| 263 |
+
if ttl > 0 {
|
| 264 |
+
ctx := context.Background()
|
| 265 |
+
// 开始一个Redis事务
|
| 266 |
+
txn := RDB.TxPipeline()
|
| 267 |
+
|
| 268 |
+
// 减少余额
|
| 269 |
+
decrCmd := txn.IncrBy(ctx, key, delta)
|
| 270 |
+
if err := decrCmd.Err(); err != nil {
|
| 271 |
+
return err // 如果减少失败,则直接返回错误
|
| 272 |
+
}
|
| 273 |
+
|
| 274 |
+
// 重新设置过期时间,使用原来的过期时间
|
| 275 |
+
txn.Expire(ctx, key, ttl)
|
| 276 |
+
|
| 277 |
+
// 执行事务
|
| 278 |
+
_, err = txn.Exec(ctx)
|
| 279 |
+
return err
|
| 280 |
+
}
|
| 281 |
+
return nil
|
| 282 |
+
}
|
| 283 |
+
|
| 284 |
+
func RedisHIncrBy(key, field string, delta int64) error {
|
| 285 |
+
if DebugEnabled {
|
| 286 |
+
SysLog(fmt.Sprintf("Redis HINCRBY: key=%s, field=%s, delta=%d", key, field, delta))
|
| 287 |
+
}
|
| 288 |
+
ttlCmd := RDB.TTL(context.Background(), key)
|
| 289 |
+
ttl, err := ttlCmd.Result()
|
| 290 |
+
if err != nil && !errors.Is(err, redis.Nil) {
|
| 291 |
+
return fmt.Errorf("failed to get TTL: %w", err)
|
| 292 |
+
}
|
| 293 |
+
|
| 294 |
+
if ttl > 0 {
|
| 295 |
+
ctx := context.Background()
|
| 296 |
+
txn := RDB.TxPipeline()
|
| 297 |
+
|
| 298 |
+
incrCmd := txn.HIncrBy(ctx, key, field, delta)
|
| 299 |
+
if err := incrCmd.Err(); err != nil {
|
| 300 |
+
return err
|
| 301 |
+
}
|
| 302 |
+
|
| 303 |
+
txn.Expire(ctx, key, ttl)
|
| 304 |
+
|
| 305 |
+
_, err = txn.Exec(ctx)
|
| 306 |
+
return err
|
| 307 |
+
}
|
| 308 |
+
return nil
|
| 309 |
+
}
|
| 310 |
+
|
| 311 |
+
func RedisHSetField(key, field string, value interface{}) error {
|
| 312 |
+
if DebugEnabled {
|
| 313 |
+
SysLog(fmt.Sprintf("Redis HSET field: key=%s, field=%s, value=%v", key, field, value))
|
| 314 |
+
}
|
| 315 |
+
ttlCmd := RDB.TTL(context.Background(), key)
|
| 316 |
+
ttl, err := ttlCmd.Result()
|
| 317 |
+
if err != nil && !errors.Is(err, redis.Nil) {
|
| 318 |
+
return fmt.Errorf("failed to get TTL: %w", err)
|
| 319 |
+
}
|
| 320 |
+
|
| 321 |
+
if ttl > 0 {
|
| 322 |
+
ctx := context.Background()
|
| 323 |
+
txn := RDB.TxPipeline()
|
| 324 |
+
|
| 325 |
+
hsetCmd := txn.HSet(ctx, key, field, value)
|
| 326 |
+
if err := hsetCmd.Err(); err != nil {
|
| 327 |
+
return err
|
| 328 |
+
}
|
| 329 |
+
|
| 330 |
+
txn.Expire(ctx, key, ttl)
|
| 331 |
+
|
| 332 |
+
_, err = txn.Exec(ctx)
|
| 333 |
+
return err
|
| 334 |
+
}
|
| 335 |
+
return nil
|
| 336 |
+
}
|
common/str.go
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"encoding/json"
|
| 21 |
+
"math/rand"
|
| 22 |
+
"strconv"
|
| 23 |
+
"unsafe"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
func GetStringIfEmpty(str string, defaultValue string) string {
|
| 27 |
+
if str == "" {
|
| 28 |
+
return defaultValue
|
| 29 |
+
}
|
| 30 |
+
return str
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
func GetRandomString(length int) string {
|
| 34 |
+
//rand.Seed(time.Now().UnixNano())
|
| 35 |
+
key := make([]byte, length)
|
| 36 |
+
for i := 0; i < length; i++ {
|
| 37 |
+
key[i] = keyChars[rand.Intn(len(keyChars))]
|
| 38 |
+
}
|
| 39 |
+
return string(key)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func MapToJsonStr(m map[string]interface{}) string {
|
| 43 |
+
bytes, err := json.Marshal(m)
|
| 44 |
+
if err != nil {
|
| 45 |
+
return ""
|
| 46 |
+
}
|
| 47 |
+
return string(bytes)
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func StrToMap(str string) map[string]interface{} {
|
| 51 |
+
m := make(map[string]interface{})
|
| 52 |
+
err := json.Unmarshal([]byte(str), &m)
|
| 53 |
+
if err != nil {
|
| 54 |
+
return nil
|
| 55 |
+
}
|
| 56 |
+
return m
|
| 57 |
+
}
|
| 58 |
+
|
| 59 |
+
func IsJsonStr(str string) bool {
|
| 60 |
+
var js map[string]interface{}
|
| 61 |
+
return json.Unmarshal([]byte(str), &js) == nil
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
func String2Int(str string) int {
|
| 65 |
+
num, err := strconv.Atoi(str)
|
| 66 |
+
if err != nil {
|
| 67 |
+
return 0
|
| 68 |
+
}
|
| 69 |
+
return num
|
| 70 |
+
}
|
| 71 |
+
|
| 72 |
+
func StringsContains(strs []string, str string) bool {
|
| 73 |
+
for _, s := range strs {
|
| 74 |
+
if s == str {
|
| 75 |
+
return true
|
| 76 |
+
}
|
| 77 |
+
}
|
| 78 |
+
return false
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
// StringToByteSlice []byte only read, panic on append
|
| 82 |
+
func StringToByteSlice(s string) []byte {
|
| 83 |
+
tmp1 := (*[2]uintptr)(unsafe.Pointer(&s))
|
| 84 |
+
tmp2 := [3]uintptr{tmp1[0], tmp1[1], tmp1[1]}
|
| 85 |
+
return *(*[]byte)(unsafe.Pointer(&tmp2))
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
// IsEmptyOrWhitespace checks if a string is empty or contains only whitespace characters
|
| 89 |
+
func IsEmptyOrWhitespace(s string) bool {
|
| 90 |
+
if s == "" {
|
| 91 |
+
return true
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
for _, char := range s {
|
| 95 |
+
if char != ' ' && char != '\t' && char != '\n' && char != '\r' {
|
| 96 |
+
return false
|
| 97 |
+
}
|
| 98 |
+
}
|
| 99 |
+
return true
|
| 100 |
+
}
|
common/topup-ratio.go
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"encoding/json"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
var TopupGroupRatio = map[string]float64{
|
| 24 |
+
"default": 1,
|
| 25 |
+
"vip": 1,
|
| 26 |
+
"svip": 1,
|
| 27 |
+
}
|
| 28 |
+
|
| 29 |
+
func TopupGroupRatio2JSONString() string {
|
| 30 |
+
jsonBytes, err := json.Marshal(TopupGroupRatio)
|
| 31 |
+
if err != nil {
|
| 32 |
+
SysError("error marshalling model ratio: " + err.Error())
|
| 33 |
+
}
|
| 34 |
+
return string(jsonBytes)
|
| 35 |
+
}
|
| 36 |
+
|
| 37 |
+
func UpdateTopupGroupRatioByJSONString(jsonStr string) error {
|
| 38 |
+
TopupGroupRatio = make(map[string]float64)
|
| 39 |
+
return json.Unmarshal([]byte(jsonStr), &TopupGroupRatio)
|
| 40 |
+
}
|
| 41 |
+
|
| 42 |
+
func GetTopupGroupRatio(name string) float64 {
|
| 43 |
+
ratio, ok := TopupGroupRatio[name]
|
| 44 |
+
if !ok {
|
| 45 |
+
SysError("topup group ratio not found: " + name)
|
| 46 |
+
return 1
|
| 47 |
+
}
|
| 48 |
+
return ratio
|
| 49 |
+
}
|
common/utils.go
ADDED
|
@@ -0,0 +1,403 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"bytes"
|
| 21 |
+
"context"
|
| 22 |
+
crand "crypto/rand"
|
| 23 |
+
"encoding/base64"
|
| 24 |
+
"encoding/json"
|
| 25 |
+
"fmt"
|
| 26 |
+
"github.com/pkg/errors"
|
| 27 |
+
"html/template"
|
| 28 |
+
"io"
|
| 29 |
+
"log"
|
| 30 |
+
"math/big"
|
| 31 |
+
"math/rand"
|
| 32 |
+
"net"
|
| 33 |
+
"os"
|
| 34 |
+
"os/exec"
|
| 35 |
+
"runtime"
|
| 36 |
+
"strconv"
|
| 37 |
+
"strings"
|
| 38 |
+
"time"
|
| 39 |
+
|
| 40 |
+
"github.com/gin-gonic/gin"
|
| 41 |
+
"github.com/google/uuid"
|
| 42 |
+
)
|
| 43 |
+
|
| 44 |
+
func OpenBrowser(url string) {
|
| 45 |
+
var err error
|
| 46 |
+
|
| 47 |
+
switch runtime.GOOS {
|
| 48 |
+
case "linux":
|
| 49 |
+
err = exec.Command("xdg-open", url).Start()
|
| 50 |
+
case "windows":
|
| 51 |
+
err = exec.Command("rundll32", "url.dll,FileProtocolHandler", url).Start()
|
| 52 |
+
case "darwin":
|
| 53 |
+
err = exec.Command("open", url).Start()
|
| 54 |
+
}
|
| 55 |
+
if err != nil {
|
| 56 |
+
log.Println(err)
|
| 57 |
+
}
|
| 58 |
+
}
|
| 59 |
+
|
| 60 |
+
func GetIp() (ip string) {
|
| 61 |
+
ips, err := net.InterfaceAddrs()
|
| 62 |
+
if err != nil {
|
| 63 |
+
log.Println(err)
|
| 64 |
+
return ip
|
| 65 |
+
}
|
| 66 |
+
|
| 67 |
+
for _, a := range ips {
|
| 68 |
+
if ipNet, ok := a.(*net.IPNet); ok && !ipNet.IP.IsLoopback() {
|
| 69 |
+
if ipNet.IP.To4() != nil {
|
| 70 |
+
ip = ipNet.IP.String()
|
| 71 |
+
if strings.HasPrefix(ip, "10") {
|
| 72 |
+
return
|
| 73 |
+
}
|
| 74 |
+
if strings.HasPrefix(ip, "172") {
|
| 75 |
+
return
|
| 76 |
+
}
|
| 77 |
+
if strings.HasPrefix(ip, "192.168") {
|
| 78 |
+
return
|
| 79 |
+
}
|
| 80 |
+
ip = ""
|
| 81 |
+
}
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
return
|
| 85 |
+
}
|
| 86 |
+
|
| 87 |
+
var sizeKB = 1024
|
| 88 |
+
var sizeMB = sizeKB * 1024
|
| 89 |
+
var sizeGB = sizeMB * 1024
|
| 90 |
+
|
| 91 |
+
func Bytes2Size(num int64) string {
|
| 92 |
+
numStr := ""
|
| 93 |
+
unit := "B"
|
| 94 |
+
if num/int64(sizeGB) > 1 {
|
| 95 |
+
numStr = fmt.Sprintf("%.2f", float64(num)/float64(sizeGB))
|
| 96 |
+
unit = "GB"
|
| 97 |
+
} else if num/int64(sizeMB) > 1 {
|
| 98 |
+
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeMB)))
|
| 99 |
+
unit = "MB"
|
| 100 |
+
} else if num/int64(sizeKB) > 1 {
|
| 101 |
+
numStr = fmt.Sprintf("%d", int(float64(num)/float64(sizeKB)))
|
| 102 |
+
unit = "KB"
|
| 103 |
+
} else {
|
| 104 |
+
numStr = fmt.Sprintf("%d", num)
|
| 105 |
+
}
|
| 106 |
+
return numStr + " " + unit
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
func Seconds2Time(num int) (time string) {
|
| 110 |
+
if num/31104000 > 0 {
|
| 111 |
+
time += strconv.Itoa(num/31104000) + " 年 "
|
| 112 |
+
num %= 31104000
|
| 113 |
+
}
|
| 114 |
+
if num/2592000 > 0 {
|
| 115 |
+
time += strconv.Itoa(num/2592000) + " 个月 "
|
| 116 |
+
num %= 2592000
|
| 117 |
+
}
|
| 118 |
+
if num/86400 > 0 {
|
| 119 |
+
time += strconv.Itoa(num/86400) + " 天 "
|
| 120 |
+
num %= 86400
|
| 121 |
+
}
|
| 122 |
+
if num/3600 > 0 {
|
| 123 |
+
time += strconv.Itoa(num/3600) + " 小时 "
|
| 124 |
+
num %= 3600
|
| 125 |
+
}
|
| 126 |
+
if num/60 > 0 {
|
| 127 |
+
time += strconv.Itoa(num/60) + " 分钟 "
|
| 128 |
+
num %= 60
|
| 129 |
+
}
|
| 130 |
+
time += strconv.Itoa(num) + " 秒"
|
| 131 |
+
return
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
func Interface2String(inter interface{}) string {
|
| 135 |
+
switch inter.(type) {
|
| 136 |
+
case string:
|
| 137 |
+
return inter.(string)
|
| 138 |
+
case int:
|
| 139 |
+
return fmt.Sprintf("%d", inter.(int))
|
| 140 |
+
case float64:
|
| 141 |
+
return fmt.Sprintf("%f", inter.(float64))
|
| 142 |
+
}
|
| 143 |
+
return "Not Implemented"
|
| 144 |
+
}
|
| 145 |
+
|
| 146 |
+
func UnescapeHTML(x string) interface{} {
|
| 147 |
+
return template.HTML(x)
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
func IntMax(a int, b int) int {
|
| 151 |
+
if a >= b {
|
| 152 |
+
return a
|
| 153 |
+
} else {
|
| 154 |
+
return b
|
| 155 |
+
}
|
| 156 |
+
}
|
| 157 |
+
|
| 158 |
+
func IsIP(s string) bool {
|
| 159 |
+
ip := net.ParseIP(s)
|
| 160 |
+
return ip != nil
|
| 161 |
+
}
|
| 162 |
+
|
| 163 |
+
func GetUUID() string {
|
| 164 |
+
code := uuid.New().String()
|
| 165 |
+
code = strings.Replace(code, "-", "", -1)
|
| 166 |
+
return code
|
| 167 |
+
}
|
| 168 |
+
|
| 169 |
+
const keyChars = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
| 170 |
+
|
| 171 |
+
func init() {
|
| 172 |
+
rand.New(rand.NewSource(time.Now().UnixNano()))
|
| 173 |
+
}
|
| 174 |
+
|
| 175 |
+
func GenerateRandomCharsKey(length int) (string, error) {
|
| 176 |
+
b := make([]byte, length)
|
| 177 |
+
maxI := big.NewInt(int64(len(keyChars)))
|
| 178 |
+
|
| 179 |
+
for i := range b {
|
| 180 |
+
n, err := crand.Int(crand.Reader, maxI)
|
| 181 |
+
if err != nil {
|
| 182 |
+
return "", err
|
| 183 |
+
}
|
| 184 |
+
b[i] = keyChars[n.Int64()]
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
return string(b), nil
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
func GenerateRandomKey(length int) (string, error) {
|
| 191 |
+
bytes := make([]byte, length*3/4) // 对于48位的输出,这里应该是36
|
| 192 |
+
if _, err := crand.Read(bytes); err != nil {
|
| 193 |
+
return "", err
|
| 194 |
+
}
|
| 195 |
+
return base64.StdEncoding.EncodeToString(bytes), nil
|
| 196 |
+
}
|
| 197 |
+
|
| 198 |
+
func GenerateKey() (string, error) {
|
| 199 |
+
//rand.Seed(time.Now().UnixNano())
|
| 200 |
+
return GenerateRandomCharsKey(48)
|
| 201 |
+
}
|
| 202 |
+
|
| 203 |
+
func GetRandomInt(max int) int {
|
| 204 |
+
//rand.Seed(time.Now().UnixNano())
|
| 205 |
+
return rand.Intn(max)
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// RandomInt returns a random integer in the range [0, max)
|
| 209 |
+
func RandomInt(max int) int {
|
| 210 |
+
return rand.Intn(max)
|
| 211 |
+
}
|
| 212 |
+
|
| 213 |
+
func GetTimestamp() int64 {
|
| 214 |
+
return time.Now().Unix()
|
| 215 |
+
}
|
| 216 |
+
|
| 217 |
+
func GetTimeString() string {
|
| 218 |
+
now := time.Now()
|
| 219 |
+
return fmt.Sprintf("%s%d", now.Format("20060102150405"), now.UnixNano()%1e9)
|
| 220 |
+
}
|
| 221 |
+
|
| 222 |
+
func Max(a int, b int) int {
|
| 223 |
+
if a >= b {
|
| 224 |
+
return a
|
| 225 |
+
} else {
|
| 226 |
+
return b
|
| 227 |
+
}
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
func MessageWithRequestId(message string, id string) string {
|
| 231 |
+
return fmt.Sprintf("%s (request id: %s)", message, id)
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
func RandomSleep() {
|
| 235 |
+
// Sleep for 0-3000 ms
|
| 236 |
+
time.Sleep(time.Duration(rand.Intn(3000)) * time.Millisecond)
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
func GetPointer[T any](v T) *T {
|
| 240 |
+
return &v
|
| 241 |
+
}
|
| 242 |
+
|
| 243 |
+
func Any2Type[T any](data any) (T, error) {
|
| 244 |
+
var zero T
|
| 245 |
+
bytes, err := json.Marshal(data)
|
| 246 |
+
if err != nil {
|
| 247 |
+
return zero, err
|
| 248 |
+
}
|
| 249 |
+
var res T
|
| 250 |
+
err = json.Unmarshal(bytes, &res)
|
| 251 |
+
if err != nil {
|
| 252 |
+
return zero, err
|
| 253 |
+
}
|
| 254 |
+
return res, nil
|
| 255 |
+
}
|
| 256 |
+
|
| 257 |
+
// SaveTmpFile saves data to a temporary file. The filename would be apppended with a random string.
|
| 258 |
+
func SaveTmpFile(filename string, data io.Reader) (string, error) {
|
| 259 |
+
f, err := os.CreateTemp(os.TempDir(), filename)
|
| 260 |
+
if err != nil {
|
| 261 |
+
return "", errors.Wrapf(err, "failed to create temporary file %s", filename)
|
| 262 |
+
}
|
| 263 |
+
defer f.Close()
|
| 264 |
+
|
| 265 |
+
_, err = io.Copy(f, data)
|
| 266 |
+
if err != nil {
|
| 267 |
+
return "", errors.Wrapf(err, "failed to copy data to temporary file %s", filename)
|
| 268 |
+
}
|
| 269 |
+
|
| 270 |
+
return f.Name(), nil
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
// GetAudioDuration returns the duration of an audio file in seconds.
|
| 274 |
+
func GetAudioDuration(ctx context.Context, filename string) (float64, error) {
|
| 275 |
+
// ffprobe -v error -show_entries format=duration -of default=noprint_wrappers=1:nokey=1 {{input}}
|
| 276 |
+
c := exec.CommandContext(ctx, "ffprobe", "-v", "error", "-show_entries", "format=duration", "-of", "default=noprint_wrappers=1:nokey=1", filename)
|
| 277 |
+
output, err := c.Output()
|
| 278 |
+
if err != nil {
|
| 279 |
+
return 0, errors.Wrap(err, "failed to get audio duration")
|
| 280 |
+
}
|
| 281 |
+
|
| 282 |
+
return strconv.ParseFloat(string(bytes.TrimSpace(output)), 64)
|
| 283 |
+
}
|
| 284 |
+
|
| 285 |
+
// GetClientIP detects the client IP address based on reverse proxy configuration
|
| 286 |
+
// This function handles different proxy configurations (Cloudflare, Nginx) and falls back to direct connection IP
|
| 287 |
+
func GetClientIP(c *gin.Context) string {
|
| 288 |
+
// If reverse proxy is not enabled, use direct connection IP
|
| 289 |
+
if !ReverseProxyEnabled {
|
| 290 |
+
return c.ClientIP()
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
var ip string
|
| 294 |
+
|
| 295 |
+
// Handle different proxy providers
|
| 296 |
+
switch ReverseProxyProvider {
|
| 297 |
+
case "cloudflare":
|
| 298 |
+
// Cloudflare uses CF-Connecting-IP header
|
| 299 |
+
ip = c.GetHeader("CF-Connecting-IP")
|
| 300 |
+
if ip != "" && IsValidIP(ip) {
|
| 301 |
+
return SanitizeIP(ip)
|
| 302 |
+
}
|
| 303 |
+
case "nginx":
|
| 304 |
+
// Nginx/OpenResty typically uses X-Real-IP header first
|
| 305 |
+
ip = c.GetHeader("X-Real-IP")
|
| 306 |
+
if ip != "" && IsValidIP(ip) {
|
| 307 |
+
return SanitizeIP(ip)
|
| 308 |
+
}
|
| 309 |
+
|
| 310 |
+
// Fallback to X-Forwarded-For header (get first IP in chain)
|
| 311 |
+
forwardedFor := c.GetHeader("X-Forwarded-For")
|
| 312 |
+
if forwardedFor != "" {
|
| 313 |
+
// X-Forwarded-For can contain multiple IPs separated by commas
|
| 314 |
+
// The first IP is typically the original client IP
|
| 315 |
+
ips := strings.Split(forwardedFor, ",")
|
| 316 |
+
if len(ips) > 0 {
|
| 317 |
+
firstIP := strings.TrimSpace(ips[0])
|
| 318 |
+
if IsValidIP(firstIP) {
|
| 319 |
+
return SanitizeIP(firstIP)
|
| 320 |
+
}
|
| 321 |
+
}
|
| 322 |
+
}
|
| 323 |
+
}
|
| 324 |
+
|
| 325 |
+
// Fallback to connection IP if proxy headers are missing or invalid
|
| 326 |
+
return c.ClientIP()
|
| 327 |
+
}
|
| 328 |
+
|
| 329 |
+
// DetectProxyHeaders analyzes request headers to determine proxy configuration
|
| 330 |
+
// Returns the detected provider and the IP address found
|
| 331 |
+
func DetectProxyHeaders(c *gin.Context) (provider string, ip string) {
|
| 332 |
+
// Check for Cloudflare headers first
|
| 333 |
+
if cfIP := c.GetHeader("CF-Connecting-IP"); cfIP != "" && IsValidIP(cfIP) {
|
| 334 |
+
return "cloudflare", SanitizeIP(cfIP)
|
| 335 |
+
}
|
| 336 |
+
|
| 337 |
+
// Check for Nginx/OpenResty headers
|
| 338 |
+
if realIP := c.GetHeader("X-Real-IP"); realIP != "" && IsValidIP(realIP) {
|
| 339 |
+
return "nginx", SanitizeIP(realIP)
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
// Check X-Forwarded-For as secondary option for Nginx
|
| 343 |
+
if forwardedFor := c.GetHeader("X-Forwarded-For"); forwardedFor != "" {
|
| 344 |
+
ips := strings.Split(forwardedFor, ",")
|
| 345 |
+
if len(ips) > 0 {
|
| 346 |
+
firstIP := strings.TrimSpace(ips[0])
|
| 347 |
+
if IsValidIP(firstIP) {
|
| 348 |
+
return "nginx", SanitizeIP(firstIP)
|
| 349 |
+
}
|
| 350 |
+
}
|
| 351 |
+
}
|
| 352 |
+
|
| 353 |
+
// No proxy headers detected
|
| 354 |
+
return "", ""
|
| 355 |
+
}
|
| 356 |
+
|
| 357 |
+
// IsValidIP validates if a string is a valid IP address (IPv4 or IPv6)
|
| 358 |
+
func IsValidIP(ip string) bool {
|
| 359 |
+
if ip == "" {
|
| 360 |
+
return false
|
| 361 |
+
}
|
| 362 |
+
|
| 363 |
+
// Remove any surrounding whitespace
|
| 364 |
+
ip = strings.TrimSpace(ip)
|
| 365 |
+
|
| 366 |
+
// Use net.ParseIP to validate the IP address
|
| 367 |
+
parsedIP := net.ParseIP(ip)
|
| 368 |
+
if parsedIP == nil {
|
| 369 |
+
return false
|
| 370 |
+
}
|
| 371 |
+
|
| 372 |
+
// Additional validation: reject obviously invalid IPs
|
| 373 |
+
// Reject localhost/loopback addresses in proxy context
|
| 374 |
+
if parsedIP.IsLoopback() {
|
| 375 |
+
return false
|
| 376 |
+
}
|
| 377 |
+
|
| 378 |
+
// Reject unspecified addresses (0.0.0.0 or ::)
|
| 379 |
+
if parsedIP.IsUnspecified() {
|
| 380 |
+
return false
|
| 381 |
+
}
|
| 382 |
+
|
| 383 |
+
return true
|
| 384 |
+
}
|
| 385 |
+
|
| 386 |
+
// SanitizeIP cleans and validates an IP address string
|
| 387 |
+
func SanitizeIP(ip string) string {
|
| 388 |
+
if ip == "" {
|
| 389 |
+
return ""
|
| 390 |
+
}
|
| 391 |
+
|
| 392 |
+
// Remove surrounding whitespace
|
| 393 |
+
ip = strings.TrimSpace(ip)
|
| 394 |
+
|
| 395 |
+
// Parse and reformat the IP to ensure it's in standard format
|
| 396 |
+
parsedIP := net.ParseIP(ip)
|
| 397 |
+
if parsedIP == nil {
|
| 398 |
+
return ""
|
| 399 |
+
}
|
| 400 |
+
|
| 401 |
+
// Return the standardized string representation
|
| 402 |
+
return parsedIP.String()
|
| 403 |
+
}
|
common/validate.go
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import "github.com/go-playground/validator/v10"
|
| 20 |
+
|
| 21 |
+
var Validate *validator.Validate
|
| 22 |
+
|
| 23 |
+
func init() {
|
| 24 |
+
Validate = validator.New()
|
| 25 |
+
}
|
common/verification.go
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package common
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"github.com/google/uuid"
|
| 21 |
+
"strings"
|
| 22 |
+
"sync"
|
| 23 |
+
"time"
|
| 24 |
+
)
|
| 25 |
+
|
| 26 |
+
type verificationValue struct {
|
| 27 |
+
code string
|
| 28 |
+
time time.Time
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const (
|
| 32 |
+
EmailVerificationPurpose = "v"
|
| 33 |
+
PasswordResetPurpose = "r"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
var verificationMutex sync.Mutex
|
| 37 |
+
var verificationMap map[string]verificationValue
|
| 38 |
+
var verificationMapMaxSize = 10
|
| 39 |
+
var VerificationValidMinutes = 10
|
| 40 |
+
|
| 41 |
+
func GenerateVerificationCode(length int) string {
|
| 42 |
+
code := uuid.New().String()
|
| 43 |
+
code = strings.Replace(code, "-", "", -1)
|
| 44 |
+
if length == 0 {
|
| 45 |
+
return code
|
| 46 |
+
}
|
| 47 |
+
return code[:length]
|
| 48 |
+
}
|
| 49 |
+
|
| 50 |
+
func RegisterVerificationCodeWithKey(key string, code string, purpose string) {
|
| 51 |
+
verificationMutex.Lock()
|
| 52 |
+
defer verificationMutex.Unlock()
|
| 53 |
+
verificationMap[purpose+key] = verificationValue{
|
| 54 |
+
code: code,
|
| 55 |
+
time: time.Now(),
|
| 56 |
+
}
|
| 57 |
+
if len(verificationMap) > verificationMapMaxSize {
|
| 58 |
+
removeExpiredPairs()
|
| 59 |
+
}
|
| 60 |
+
}
|
| 61 |
+
|
| 62 |
+
func VerifyCodeWithKey(key string, code string, purpose string) bool {
|
| 63 |
+
verificationMutex.Lock()
|
| 64 |
+
defer verificationMutex.Unlock()
|
| 65 |
+
value, okay := verificationMap[purpose+key]
|
| 66 |
+
now := time.Now()
|
| 67 |
+
if !okay || int(now.Sub(value.time).Seconds()) >= VerificationValidMinutes*60 {
|
| 68 |
+
return false
|
| 69 |
+
}
|
| 70 |
+
return code == value.code
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
func DeleteKey(key string, purpose string) {
|
| 74 |
+
verificationMutex.Lock()
|
| 75 |
+
defer verificationMutex.Unlock()
|
| 76 |
+
delete(verificationMap, purpose+key)
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
// no lock inside, so the caller must lock the verificationMap before calling!
|
| 80 |
+
func removeExpiredPairs() {
|
| 81 |
+
now := time.Now()
|
| 82 |
+
for key := range verificationMap {
|
| 83 |
+
if int(now.Sub(verificationMap[key].time).Seconds()) >= VerificationValidMinutes*60 {
|
| 84 |
+
delete(verificationMap, key)
|
| 85 |
+
}
|
| 86 |
+
}
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
func init() {
|
| 90 |
+
verificationMutex.Lock()
|
| 91 |
+
defer verificationMutex.Unlock()
|
| 92 |
+
verificationMap = make(map[string]verificationValue)
|
| 93 |
+
}
|
constant/azure.go
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package constant
|
| 18 |
+
|
| 19 |
+
import "time"
|
| 20 |
+
|
| 21 |
+
var AzureNoRemoveDotTime = time.Date(2025, time.May, 10, 0, 0, 0, 0, time.UTC).Unix()
|
constant/cache_key.go
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package constant
|
| 18 |
+
|
| 19 |
+
import "veloera/common"
|
| 20 |
+
|
| 21 |
+
var (
|
| 22 |
+
TokenCacheSeconds = common.SyncFrequency
|
| 23 |
+
UserId2GroupCacheSeconds = common.SyncFrequency
|
| 24 |
+
UserId2QuotaCacheSeconds = common.SyncFrequency
|
| 25 |
+
UserId2StatusCacheSeconds = common.SyncFrequency
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
// Cache keys
|
| 29 |
+
const (
|
| 30 |
+
UserGroupKeyFmt = "user_group:%d"
|
| 31 |
+
UserQuotaKeyFmt = "user_quota:%d"
|
| 32 |
+
UserEnabledKeyFmt = "user_enabled:%d"
|
| 33 |
+
UserUsernameKeyFmt = "user_name:%d"
|
| 34 |
+
)
|
| 35 |
+
|
| 36 |
+
const (
|
| 37 |
+
TokenFiledRemainQuota = "RemainQuota"
|
| 38 |
+
TokenFieldGroup = "Group"
|
| 39 |
+
)
|
constant/channel_setting.go
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package constant
|
| 18 |
+
|
| 19 |
+
var (
|
| 20 |
+
ForceFormat = "force_format" // ForceFormat 强制格式化为OpenAI格式
|
| 21 |
+
ChanelSettingProxy = "proxy" // Proxy 代理
|
| 22 |
+
ChannelSettingThinkingToContent = "thinking_to_content" // ThinkingToContent
|
| 23 |
+
ChannelSettingStreamSupport = "stream_support" // StreamSupport 控制上游流式请求行为
|
| 24 |
+
StreamSupportNonStreamOnly = "NON_STREAM_ONLY" // StreamSupport 仅非流式请求
|
| 25 |
+
)
|
constant/context_key.go
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package constant
|
| 18 |
+
|
| 19 |
+
const (
|
| 20 |
+
ContextKeyRequestStartTime = "request_start_time"
|
| 21 |
+
ContextKeyUserSetting = "user_setting"
|
| 22 |
+
ContextKeyUserQuota = "user_quota"
|
| 23 |
+
ContextKeyUserStatus = "user_status"
|
| 24 |
+
ContextKeyUserEmail = "user_email"
|
| 25 |
+
ContextKeyUserGroup = "user_group"
|
| 26 |
+
)
|
constant/env.go
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package constant
|
| 18 |
+
|
| 19 |
+
import (
|
| 20 |
+
"veloera/common"
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
var StreamingTimeout int
|
| 24 |
+
var DifyDebug bool
|
| 25 |
+
var MaxFileDownloadMB int
|
| 26 |
+
var ForceStreamOption bool
|
| 27 |
+
var GetMediaToken bool
|
| 28 |
+
var GetMediaTokenNotStream bool
|
| 29 |
+
var UpdateTask bool
|
| 30 |
+
var AzureDefaultAPIVersion string
|
| 31 |
+
var GeminiVisionMaxImageNum int
|
| 32 |
+
var NotifyLimitCount int
|
| 33 |
+
var NotificationLimitDurationMinute int
|
| 34 |
+
var GenerateDefaultToken bool
|
| 35 |
+
|
| 36 |
+
//var GeminiModelMap = map[string]string{
|
| 37 |
+
// "gemini-1.0-pro": "v1",
|
| 38 |
+
//}
|
| 39 |
+
|
| 40 |
+
func InitEnv() {
|
| 41 |
+
StreamingTimeout = common.GetEnvOrDefault("STREAMING_TIMEOUT", 60)
|
| 42 |
+
DifyDebug = common.GetEnvOrDefaultBool("DIFY_DEBUG", true)
|
| 43 |
+
MaxFileDownloadMB = common.GetEnvOrDefault("MAX_FILE_DOWNLOAD_MB", 20)
|
| 44 |
+
// ForceStreamOption 覆盖请求参数,强制返回usage信息
|
| 45 |
+
ForceStreamOption = common.GetEnvOrDefaultBool("FORCE_STREAM_OPTION", true)
|
| 46 |
+
GetMediaToken = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN", true)
|
| 47 |
+
GetMediaTokenNotStream = common.GetEnvOrDefaultBool("GET_MEDIA_TOKEN_NOT_STREAM", true)
|
| 48 |
+
UpdateTask = common.GetEnvOrDefaultBool("UPDATE_TASK", true)
|
| 49 |
+
AzureDefaultAPIVersion = common.GetEnvOrDefaultString("AZURE_DEFAULT_API_VERSION", "2024-12-01-preview")
|
| 50 |
+
GeminiVisionMaxImageNum = common.GetEnvOrDefault("GEMINI_VISION_MAX_IMAGE_NUM", 16)
|
| 51 |
+
NotifyLimitCount = common.GetEnvOrDefault("NOTIFY_LIMIT_COUNT", 2)
|
| 52 |
+
NotificationLimitDurationMinute = common.GetEnvOrDefault("NOTIFICATION_LIMIT_DURATION_MINUTE", 10)
|
| 53 |
+
// GenerateDefaultToken 是否生成初始令牌,默认关闭。
|
| 54 |
+
GenerateDefaultToken = common.GetEnvOrDefaultBool("GENERATE_DEFAULT_TOKEN", false)
|
| 55 |
+
|
| 56 |
+
//modelVersionMapStr := strings.TrimSpace(os.Getenv("GEMINI_MODEL_MAP"))
|
| 57 |
+
//if modelVersionMapStr == "" {
|
| 58 |
+
// return
|
| 59 |
+
//}
|
| 60 |
+
//for _, pair := range strings.Split(modelVersionMapStr, ",") {
|
| 61 |
+
// parts := strings.Split(pair, ":")
|
| 62 |
+
// if len(parts) == 2 {
|
| 63 |
+
// GeminiModelMap[parts[0]] = parts[1]
|
| 64 |
+
// } else {
|
| 65 |
+
// common.SysError(fmt.Sprintf("invalid model version map: %s", pair))
|
| 66 |
+
// }
|
| 67 |
+
//}
|
| 68 |
+
}
|
constant/finish_reason.go
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Copyright (c) 2025 Tethys Plex
|
| 2 |
+
//
|
| 3 |
+
// This file is part of Veloera.
|
| 4 |
+
//
|
| 5 |
+
// This program is free software: you can redistribute it and/or modify
|
| 6 |
+
// it under the terms of the GNU General Public License as published by
|
| 7 |
+
// the Free Software Foundation, either version 3 of the License, or
|
| 8 |
+
// (at your option) any later version.
|
| 9 |
+
//
|
| 10 |
+
// This program is distributed in the hope that it will be useful,
|
| 11 |
+
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
| 12 |
+
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
| 13 |
+
// GNU General Public License for more details.
|
| 14 |
+
//
|
| 15 |
+
// You should have received a copy of the GNU General Public License
|
| 16 |
+
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
| 17 |
+
package constant
|
| 18 |
+
|
| 19 |
+
var (
|
| 20 |
+
FinishReasonStop = "stop"
|
| 21 |
+
FinishReasonToolCalls = "tool_calls"
|
| 22 |
+
FinishReasonLength = "length"
|
| 23 |
+
FinishReasonFunctionCall = "function_call"
|
| 24 |
+
FinishReasonContentFilter = "content_filter"
|
| 25 |
+
)
|