Compare commits

..
19 Commits
Author SHA1 Message Date
Akizon d19b64ec1d Merge pull request #144 from komari-monitor/dev
feat: 优化静态资源服务
2025-07-24 16:25:33 +08:00
Akizon77 6ea22e3241 feat: 优化静态资源服务 2025-07-24 08:10:09 +00:00
Akizon77 ea99966e7a feat: 配置获取添加并发控制 2025-07-24 08:09:57 +00:00
Akizon77 fa13d4d1f9 feat: 自定义端口 2025-07-23 14:31:45 +00:00
Akizon77 15e4f583e0 chore: 删除待办清单文件 2025-07-23 14:25:40 +00:00
Akizon77 3fbbc7bdce docs: 添加前端构建说明文档 2025-07-23 14:23:54 +00:00
Akizon77 5fcd1d313c fix: #138 Docker容器添加时区数据 2025-07-23 04:00:24 +00:00
Akizon 967b7ab8a7 docs: 更新文档链接 2025-07-22 20:52:24 +08:00
Akizon77 d113dde3cc docs: 添加容器云一键部署 2025-07-21 07:52:12 +00:00
Akizon77 60c24a5ee1 docs: 添加一键脚本的指南 2025-07-21 06:48:51 +00:00
Akizon77 d5b8982763 docs: 添加Telegram群组链接 2025-07-21 06:40:45 +00:00
Akizon77 88a441da23 feat: 添加manifest.json路由 2025-07-20 08:28:15 +00:00
akizon77 03f27e7eb1 docs: 更新鸣谢 2025-07-19 01:01:56 +08:00
Akizon 6bba4526f0 Merge pull request #129 from komari-monitor/dev
feat: 定时自动续费
2025-07-16 11:26:14 +08:00
Akizon77 ebb84862a2 feat: 定时自动续费 2025-07-16 11:16:05 +08:00
Akizon77 28480a93d4 feat: 自动续费 2025-07-16 11:07:27 +08:00
Akizon 5c7c5a4e99 Merge pull request #128 from komari-monitor/main
Sync to dev branch
2025-07-16 10:21:57 +08:00
Akizon 90ee1da9c7 Merge pull request #127 from ours1505/dev
feat: 自动续费
2025-07-16 10:18:56 +08:00
Nya 596dbd0c2d feat: 自动续费 2025-07-16 10:02:26 +08:00
13 changed files with 345 additions and 103 deletions

No files matched your search

+2
View File
@@ -6,6 +6,8 @@ WORKDIR /app
ARG TARGETOS
ARG TARGETARCH
RUN apk add --no-cache tzdata
COPY komari-${TARGETOS}-${TARGETARCH} /app/komari
RUN chmod +x /app/komari
+21 -7
View File
@@ -6,7 +6,7 @@
Komari 是一款轻量级的自托管服务器监控工具,旨在提供简单、高效的服务器性能监控解决方案。它支持通过 Web 界面查看服务器状态,并通过轻量级 Agent 收集数据。
[文档](https://komari-monitor.github.io/komari-document/)
[文档](https://komari-document.pages.dev/) | [Telegram 群组](https://t.me/komari_monitor)
## 特性
- **轻量高效**:低资源占用,适合各种规模的服务器。
@@ -15,11 +15,19 @@ Komari 是一款轻量级的自托管服务器监控工具,旨在提供简单
## 快速开始
### 依赖
- Docker(快速部署)
- 或者 Go 1.18+ 和 Node.js 20+(手工构建)
### 0. 容器云一键部署
### Docker 部署
[![](https://raw.githubusercontent.com/ClawCloud/Run-Template/refs/heads/main/Run-on-ClawCloud.svg)](https://template.run.claw.cloud/?openapp=system-fastdeploy%3FtemplateName%3Dkomari)
### 1. 使用一键安装脚本
适用于使用了 systemd 的发行版(Ubuntu、Debian...)。
```bash
curl -fsSL https://raw.githubusercontent.com/komari-monitor/komari/main/install-komari.sh -o install-komari.sh
chmod +x install-komari.sh
sudo ./install-komari.sh
```
### 2. Docker 部署
1. 创建数据目录:
```bash
mkdir -p ./data
@@ -41,7 +49,7 @@ Komari 是一款轻量级的自托管服务器监控工具,旨在提供简单
> [!NOTE]
> 你也可以通过环境变量 `ADMIN_USERNAME` 和 `ADMIN_PASSWORD` 自定义初始用户名和密码。
### 二进制文件部署
### 3. 二进制文件部署
1. 访问 Komari 的 [GitHub Release 页面](https://github.com/komari-monitor/komari/releases) 下载适用于你操作系统的最新二进制文件。
2. 运行 Komari:
```bash
@@ -55,6 +63,11 @@ Komari 是一款轻量级的自托管服务器监控工具,旨在提供简单
### 手工构建
#### 依赖
- Go 1.18+ 和 Node.js 20+(手工构建)
1. 构建前端静态文件:
```bash
git clone https://github.com/komari-monitor/komari-web
@@ -87,8 +100,9 @@ Komari 是一款轻量级的自托管服务器监控工具,旨在提供简单
欢迎提交 Issue 或 Pull Request!
## 鸣谢
- [DreamCloud - 极高性价比解锁直连亚太高防](https://as211392.com/)
- 感谢我自己能这么闲
- 广告位招租喵
- 提交PR、制作主题的各位开发者
## Star History
+4
View File
@@ -21,6 +21,10 @@ func Log(ip, uuid, message, msgType string) {
db.Create(logEntry)
}
func EventLog(eventType, message string) {
Log("", "", message, eventType)
}
// Delete logs older than 30 days
func RemoveOldLogs() {
db := dbcore.GetDBInstance()
+9
View File
@@ -3,6 +3,7 @@ package config
import (
"errors"
"log"
"sync"
"time"
"github.com/komari-monitor/komari/database/dbcore"
@@ -10,7 +11,15 @@ import (
"gorm.io/gorm"
)
var mu sync.Mutex
func init() {
mu = sync.Mutex{}
}
func Get() (models.Config, error) {
mu.Lock()
defer mu.Unlock()
db := dbcore.GetDBInstance()
var config models.Config
if err := db.First(&config).Error; err != nil {
+1
View File
@@ -29,6 +29,7 @@ type Client struct {
Weight int `json:"weight" gorm:"type:int"`
Price float64 `json:"price"`
BillingCycle int `json:"billing_cycle"`
AutoRenewal bool `json:"auto_renewal" gorm:"default:false"` // 是否自动续费
Currency string `json:"currency" gorm:"type:varchar(20);default:'$'"`
ExpiredAt UTCTime `json:"expired_at" gorm:"type:timestamp"`
Group string `json:"group" gorm:"type:varchar(100)"`
+26 -6
View File
@@ -23,11 +23,14 @@ log_step() {
echo -e "${YELLOW}$1${NC}"
}
# Global variables
INSTALL_DIR="/opt/komari"
DATA_DIR="/opt/komari/data"
SERVICE_NAME="komari"
BINARY_PATH="$INSTALL_DIR/komari"
DEFAULT_PORT="25774"
LISTEN_PORT=""
# Show banner
show_banner() {
@@ -119,6 +122,21 @@ install_binary() {
return
fi
# 监听端口输入,校验范围 1-65535
while true; do
read -p "请输入监听端口 [默认: $DEFAULT_PORT]: " input_port
if [[ -z "$input_port" ]]; then
LISTEN_PORT="$DEFAULT_PORT"
break
elif [[ "$input_port" =~ ^[0-9]+$ ]] && (( input_port >= 1 && input_port <= 65535 )); then
LISTEN_PORT="$input_port"
break
else
log_error "端口号无效,请输入 1-65535 之间的数字。"
fi
done
install_dependencies
local arch=$(detect_arch)
@@ -147,13 +165,13 @@ install_binary() {
if ! check_systemd; then
log_step "警告:未检测到 systemd,跳过服务创建。"
log_step "您可以从命令行手动运行 Komari:"
log_step " $BINARY_PATH server -l 0.0.0.0:25774"
log_step " $BINARY_PATH server -l 0.0.0.0:$LISTEN_PORT"
echo
log_success "安装完成!"
return
fi
create_systemd_service
create_systemd_service "$LISTEN_PORT"
systemctl daemon-reload
systemctl enable ${SERVICE_NAME}.service
@@ -168,7 +186,7 @@ install_binary() {
if [ -z "$password" ]; then
log_error "未能获取初始密码,请检查日志"
fi
show_access_info "$password"
show_access_info "$password" "$LISTEN_PORT"
else
log_error "Komari 服务启动失败"
log_info "查看日志: journalctl -u ${SERVICE_NAME} -f"
@@ -178,6 +196,7 @@ install_binary() {
# Create systemd service file
create_systemd_service() {
local port="$1"
log_step "创建 systemd 服务..."
local service_file="/etc/systemd/system/${SERVICE_NAME}.service"
@@ -188,7 +207,7 @@ After=network.target
[Service]
Type=simple
ExecStart=${BINARY_PATH} server -l 0.0.0.0:25774
ExecStart=${BINARY_PATH} server -l 0.0.0.0:${port}
WorkingDirectory=${DATA_DIR}
Restart=always
User=root
@@ -203,13 +222,14 @@ EOF
# Show access information
show_access_info() {
local password=$1
local port=${2:-$DEFAULT_PORT}
echo
log_success "安装完成!"
echo
log_info "访问信息:"
log_info " URL: http://$(hostname -I | awk '{print $1}'):25774"
log_info " URL: http://$(hostname -I | awk '{print $1}'):${port}"
if [ -n "$password" ]; then
log_info "初始登录信息: $password"
log_info "初始登录信息(仅显示一次): $password"
fi
echo
log_info "服务管理命令:"
+62 -19
View File
@@ -103,6 +103,32 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
}
c.Data(http.StatusOK, "image/x-icon", data)
})
// r.GET("/manifest.json", func(c *gin.Context) {
// cfg, err := config.Get()
// if cfg.Theme == "default" || cfg.Theme == "" || err != nil {
// // 使用默认主题的manifest.json
// f, err := DistFS.Open("manifest.json")
// if err != nil {
// c.Status(http.StatusNotFound)
// return
// }
// defer f.Close()
// data, err := io.ReadAll(f)
// if err != nil {
// c.Status(http.StatusInternalServerError)
// return
// }
// c.Data(http.StatusOK, "application/json", data)
// } else {
// // 使用自定义主题的manifest.json
// themePath := filepath.Join("./data/theme", cfg.Theme, "dist", "manifest.json")
// if _, err := os.Stat(themePath); os.IsNotExist(err) {
// c.JSON(http.StatusNotFound, gin.H{"error": "Manifest not found"})
// } else {
// c.File(themePath)
// }
// }
// })
// Serve theme files from data/theme directory (for theme previews and static assets)
r.Static("/themes", "./data/theme")
@@ -133,26 +159,43 @@ func Static(r *gin.RouterGroup, noRoute func(handlers ...gin.HandlerFunc)) {
// serveFromEmbedded 从嵌入的文件系统服务文件
func serveFromEmbedded(c *gin.Context, path string) {
// 处理静态资源文件夹
folders := []string{"assets", "images", "streamer", "static"}
for _, folder := range folders {
if strings.HasPrefix(path, "/"+folder+"/") {
c.Header("Cache-Control", "public, max-age=15552000")
sub, err := fs.Sub(DistFS, folder)
if err == nil {
relativePath := strings.TrimPrefix(path, "/"+folder+"/")
file, err := sub.Open(relativePath)
if err == nil {
defer file.Close()
data, err := io.ReadAll(file)
if err == nil {
// 设置正确的Content-Type
contentType := getContentType(path)
c.Header("Content-Type", contentType)
c.Data(http.StatusOK, contentType, data)
return
}
}
// folders := []string{"assets", "images", "streamer", "static"}
// for _, folder := range folders {
// if strings.HasPrefix(path, "/"+folder+"/") {
// c.Header("Cache-Control", "public, max-age=15552000")
// sub, err := fs.Sub(DistFS, folder)
// if err == nil {
// relativePath := strings.TrimPrefix(path, "/"+folder+"/")
// file, err := sub.Open(relativePath)
// if err == nil {
// defer file.Close()
// data, err := io.ReadAll(file)
// if err == nil {
// // 设置正确的Content-Type
// contentType := getContentType(path)
// c.Header("Content-Type", contentType)
// c.Data(http.StatusOK, contentType, data)
// return
// }
// }
// }
// }
// }
// 如果文件存在,直接返回文件内容
cleanPath := strings.TrimPrefix(path, "/")
file, err := DistFS.Open(cleanPath)
if err == nil {
defer file.Close()
data, err := io.ReadAll(file)
if err == nil {
contentType := getContentType(path)
c.Header("Content-Type", contentType)
// 静态资源设置缓存
if strings.Contains(path, "/assets/") || strings.Contains(path, "/static/") {
c.Header("Cache-Control", "public, max-age=15552000")
}
c.Data(http.StatusOK, contentType, data)
return
}
}
+61
View File
@@ -0,0 +1,61 @@
# Frontend Build Instructions / 前端构建说明 / フロントエンド構築手順
## English
### Frontend Repository
- **Frontend project repository**: https://github.com/komari-monitor/komari-web
### Build Requirements
1. Clone the frontend repository and build the static files
2. Copy the generated static files to the `dist` folder in the Komari backend project root directory
3. Ensure the `dist` folder contains `index.html`
### Important Note
⚠️ **The projects under Akizon77's personal repository are no longer maintained. Please use the projects under the organization (komari-monitor).**
---
## 中文
### 前端项目仓库
- **前端项目地址**: https://github.com/komari-monitor/komari-web
### 构建要求
1. 克隆前端仓库并构建静态文件
2. 将生成的静态文件复制到 Komari 后端项目根目录下的 `dist` 文件夹
3. 确保 `dist` 文件夹内包含 `index.html`
### 重要提醒
⚠️ **Akizon77 个人仓库的项目已经不再使用,请使用组织(komari-monitor)下的项目。**
---
## 日本語
### フロントエンドプロジェクトリポジトリ
- **フロントエンドプロジェクトアドレス**: https://github.com/komari-monitor/komari-web
### ビルド要件
1. フロントエンドリポジトリをクローンして静的ファイルをビルドする
2. 生成された静的ファイルを Komari バックエンドプロジェクトのルートディレクトリ下の `dist` フォルダーにコピーする
3. `dist` フォルダー内に `index.html` が含まれていることを確認する
### 重要な注意事項
⚠️ **Akizon77 の個人リポジトリのプロジェクトは使用されなくなりました。組織(komari-monitor)下のプロジェクトを使用してください。**
---
## Quick Setup / 快速设置 / クイックセットアップ
```bash
# Clone frontend repository / 克隆前端仓库 / フロントエンドリポジトリをクローン
git clone https://github.com/komari-monitor/komari-web
cd komari-web
# Install dependencies and build / 安装依赖并构建 / 依存関係をインストールしてビルド
npm install
npm run build
# Copy dist folder to Komari backend project / 将 dist 文件夹复制到 Komari 后端项目 / dist フォルダーを Komari バックエンドプロジェクトにコピー
cp -r dist /path/to/komari/public/
```
-39
View File
@@ -1,39 +0,0 @@
# komari 待办清单
## 通用
- [ ] 低带宽模式
- [x] Web终端
- [x] 一键部署脚本
- [x] 远程命令执行
## Serser
- [x] Docker 部署
- [ ] Web前端
- [x] Web前端嵌入二进制
- [x] 命令行指令 chpasswd 强制更换密码
- [x] Github单点登录、绑定、解绑
- [x] 2FA
- [ ] 负载通知
- [x] 离线通知
- [ ] DDNS
- [x] Agent 备注
## Agent
- [x] 自动更新
- [x] 慢速模式(调整interval)
+7
View File
@@ -36,6 +36,13 @@ type MessageSender interface {
func SendTextMessage(message string, title string) error {
var err error
cfg, err := config.Get()
if err != nil {
return err
}
if !cfg.NotificationEnabled {
return nil
}
for i := 0; i < 3; i++ {
err = CurrentProvider.SendTextMessage(message, title)
if err == nil {
+35 -31
View File
@@ -8,6 +8,7 @@ import (
"github.com/komari-monitor/komari/database/clients"
"github.com/komari-monitor/komari/database/config"
"github.com/komari-monitor/komari/utils/messageSender"
"github.com/komari-monitor/komari/utils/renewal"
)
func CheckExpireScheduledWork() {
@@ -25,12 +26,6 @@ func CheckExpireScheduledWork() {
time.Sleep(time.Second)
continue
}
if !cfg.ExpireNotificationEnabled {
time.Sleep(time.Second)
continue
}
notificationLeadDays := cfg.ExpireNotificationLeadDays
clients_all, err := clients.GetAllClientBasicInfo()
if err != nil {
@@ -38,44 +33,53 @@ func CheckExpireScheduledWork() {
continue
}
type clientToExpireInfo struct {
Name string
DaysLeft int
}
var clientLeadToExpire []clientToExpireInfo
checkTime := time.Now()
for _, client := range clients_all {
clientExpireTime := client.ExpiredAt.ToTime()
// 过期提醒检查(仅当启用过期通知时)
if cfg.ExpireNotificationEnabled {
notificationLeadDays := cfg.ExpireNotificationLeadDays
if clientExpireTime.Before(checkTime) {
continue
type clientToExpireInfo struct {
Name string
DaysLeft int
}
notificationThreshold := checkTime.Add(time.Duration(notificationLeadDays) * 24 * time.Hour)
var clientLeadToExpire []clientToExpireInfo
if clientExpireTime.Before(notificationThreshold) || clientExpireTime.Equal(notificationThreshold) {
remainingDuration := clientExpireTime.Sub(checkTime)
daysLeft := int(math.Ceil(remainingDuration.Hours() / 24))
for _, client := range clients_all {
clientExpireTime := client.ExpiredAt.ToTime()
clientLeadToExpire = append(clientLeadToExpire, clientToExpireInfo{
Name: client.Name,
DaysLeft: daysLeft,
})
if clientExpireTime.Before(checkTime) {
continue
}
notificationThreshold := checkTime.Add(time.Duration(notificationLeadDays) * 24 * time.Hour)
if clientExpireTime.Before(notificationThreshold) || clientExpireTime.Equal(notificationThreshold) {
remainingDuration := clientExpireTime.Sub(checkTime)
daysLeft := int(math.Ceil(remainingDuration.Hours() / 24))
clientLeadToExpire = append(clientLeadToExpire, clientToExpireInfo{
Name: client.Name,
DaysLeft: daysLeft,
})
}
}
}
if len(clientLeadToExpire) > 0 {
message := "The following clients are about to expire: \n\n"
for _, clientInfo := range clientLeadToExpire {
message += fmt.Sprintf("%s in %d days.\n", clientInfo.Name, clientInfo.DaysLeft)
if len(clientLeadToExpire) > 0 {
message := "The following clients are about to expire: \n\n"
for _, clientInfo := range clientLeadToExpire {
message += fmt.Sprintf("• %s in %d days\n", clientInfo.Name, clientInfo.DaysLeft)
}
messageSender.SendTextMessage(message, "Komari Expiration Notification")
}
messageSender.SendTextMessage(message, "Komari Expiration Notification")
}
// 等待1秒,防止多次触发
time.Sleep(time.Second)
for _, client := range clients_all {
renewal.CheckAndAutoRenewal(client)
}
}
}
+3 -1
View File
@@ -11,6 +11,7 @@ import (
"github.com/komari-monitor/komari/database/dbcore"
"github.com/komari-monitor/komari/database/models"
"github.com/komari-monitor/komari/utils/messageSender"
"github.com/komari-monitor/komari/utils/renewal"
)
// notificationState 保存单个客户端的通知状态。
@@ -119,7 +120,8 @@ func OnlineNotification(clientID string) {
if err != nil {
return
}
// 上线时检测续费
renewal.CheckAndAutoRenewal(client)
_, enabled := getNotificationConfig(clientID)
if !enabled {
return
+114
View File
@@ -0,0 +1,114 @@
package renewal
import (
"fmt"
"time"
"github.com/komari-monitor/komari/database/auditlog"
"github.com/komari-monitor/komari/database/clients"
"github.com/komari-monitor/komari/database/models"
"github.com/komari-monitor/komari/utils/messageSender"
"github.com/komari-monitor/komari/ws"
)
func CheckAndAutoRenewal(client models.Client) {
// 自动续费检查
type renewedClient struct {
Name string
NewExpireTime time.Time
}
var renewedClients []renewedClient
if !client.AutoRenewal {
return
}
// 不在线则不续费
if _, ok := ws.GetConnectedClients()[client.UUID]; !ok {
return
}
clientExpireTime := client.ExpiredAt.ToTime()
checkTime := time.Now()
// 如果到期时间小于0002年,跳过
if clientExpireTime.Year() < 2 {
return
}
// 检查是否已过期或当天过期
if clientExpireTime.Before(checkTime) || clientExpireTime.Format("2006-01-02") == checkTime.Format("2006-01-02") {
// 计算过期时间距离创建时间的总天数,判断是否为长期账单
now := time.Now()
hundredYearsFromNow := now.AddDate(100, 0, 0)
// 如果过期时间超过当前时间100年,视为长期/一次性账单,不续费
if clientExpireTime.After(hundredYearsFromNow) {
return
}
// 如果有账单周期且不为0,进行自动续费
if client.BillingCycle > 0 {
// 根据账单周期计算新的过期时间
var newExpireTime time.Time
billingCycle := client.BillingCycle
// 如果服务器的过期时间太早了,那么直接设置为从当前时间算的下一个到期时间
baseTime := clientExpireTime
if clientExpireTime.Before(now.AddDate(0, 0, -30)) { // 过期时间超过30天前
baseTime = now
}
if billingCycle >= 27 && billingCycle <= 32 {
// 月度计费 - 加1个月
newExpireTime = baseTime.AddDate(0, 1, 0)
} else if billingCycle >= 87 && billingCycle <= 95 {
// 季度计费 - 加3个月
newExpireTime = baseTime.AddDate(0, 3, 0)
} else if billingCycle >= 175 && billingCycle <= 185 {
// 半年计费 - 加6个月
newExpireTime = baseTime.AddDate(0, 6, 0)
} else if billingCycle >= 360 && billingCycle <= 370 {
// 年度计费 - 加1年
newExpireTime = baseTime.AddDate(1, 0, 0)
} else if billingCycle >= 720 && billingCycle <= 750 {
// 两年计费 - 加2年
newExpireTime = baseTime.AddDate(2, 0, 0)
} else if billingCycle >= 1080 && billingCycle <= 1150 {
// 三年计费 - 加3年
newExpireTime = baseTime.AddDate(3, 0, 0)
} else {
// 其他情况,直接加上账单周期天数
newExpireTime = baseTime.AddDate(0, 0, billingCycle)
}
// 更新客户端过期时间
updates := map[string]interface{}{
"uuid": client.UUID,
"expired_at": models.FromTime(newExpireTime),
}
err := clients.SaveClient(updates)
if err != nil {
auditlog.EventLog("renewal", fmt.Sprintf("Failed to renew client %s (%s): %v", client.Name, client.UUID, err))
return
}
renewedClients = append(renewedClients, renewedClient{
Name: client.Name,
NewExpireTime: newExpireTime,
})
auditlog.EventLog("renewal", fmt.Sprintf("Auto-renewed client: %s until %s",
client.Name, newExpireTime.Format("2006-01-02")))
}
}
// 发送续费通知
if len(renewedClients) > 0 {
message := "The following clients have been automatically renewed: \n\n"
for _, clientInfo := range renewedClients {
message += fmt.Sprintf("• %s until %s\n", clientInfo.Name, clientInfo.NewExpireTime.Format("2006-01-02"))
}
messageSender.SendTextMessage(message, "Komari Auto-Renewal Notification")
}
}