Jack698 commited on
Commit
adc1e1c
·
verified ·
1 Parent(s): fc4aa48

Upload folder using huggingface_hub

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
.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: Velo
3
- emoji: 📚
4
  colorFrom: blue
5
- colorTo: pink
6
  sdk: docker
7
- pinned: false
8
  ---
9
 
10
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
1
  ---
2
+ title: "velo"
3
+ emoji: "🚀"
4
  colorFrom: blue
5
+ colorTo: green
6
  sdk: docker
7
+ app_port: 3000
8
  ---
9
 
10
+ ### 🚀 一键部署
11
+ [![Deploy with HFSpaceDeploy](https://img.shields.io/badge/Deploy_with-HFSpaceDeploy-green?style=social&logo=rocket)](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
+ )