Compare commits

..
Author SHA1 Message Date
wanghe-fit2cloud 3a888d289f fix app image pull check 2026-06-12 14:10:20 +08:00
803 changed files with 82457 additions and 130369 deletions
+25
View File
@@ -0,0 +1,25 @@
name: LLM Code Review
permissions:
contents: read
pull-requests: write
on:
pull_request:
types: [opened, reopened, synchronize]
jobs:
llm-code-review:
runs-on: ubuntu-latest
steps:
- uses: fit2cloud/LLM-CodeReview-Action@main
env:
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
LANGUAGE: English
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
MODEL: qwen2.5-coder-3b-instruct
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
top_p: 1
temperature: 1
# max_tokens: 10000
MAX_PATCH_LENGTH: 10000
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
+21
View File
@@ -0,0 +1,21 @@
name: SonarCloud Scan
on:
push:
branches:
- dev
pull_request:
types: [opened, synchronize, reopened]
jobs:
sonarcloud:
name: SonarCloud
if: github.repository == '1Panel-dev/1Panel'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
with:
fetch-depth: 0
- name: SonarCloud Scan
uses: SonarSource/sonarcloud-github-action@master
env:
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
+1 -1
View File
@@ -6,7 +6,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Mirror the Github organization repos to Gitee. - name: Mirror the Github organization repos to Gitee.
uses: Yikun/hub-mirror-action@ba51c01b28a6c9f95a25d4f1bcf6af2a147c0e18 # master uses: Yikun/hub-mirror-action@master
with: with:
src: 'github/1Panel-dev' src: 'github/1Panel-dev'
dst: 'gitee/fit2cloud-feizhiyun' dst: 'gitee/fit2cloud-feizhiyun'
+11
View File
@@ -0,0 +1,11 @@
name: Typos Check
on: pull_request
jobs:
run:
name: Spell Check with Typos
runs-on: ubuntu-latest
steps:
- name: Checkout Actions Repository
uses: actions/checkout@v2
- name: Check spelling
uses: crate-ci/typos@master
+38 -26
View File
@@ -1,6 +1,9 @@
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p> <p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
<h3 align="center">The open-source VPS control panel with native AI agent support</h3>
<p align="center"> <p align="center">
Loved by a global community of <strong>2.5M+</strong> self-hosters. Trusted by <strong>2,000,000+</strong> self-hosters worldwide
</p> </p>
<p align="center"> <p align="center">
@@ -9,6 +12,7 @@
<p align="center"> <p align="center">
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a> <a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
<a href="https://app.codacy.com/gh/1Panel-dev/1Panel"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a> <a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a> <a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a> <a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a>
@@ -21,7 +25,7 @@
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a> <a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a> <a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a> <a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
<a href="/docs/README.es-es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a> <a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a> <a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a> <a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a> <a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
@@ -29,36 +33,40 @@
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a> <a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a> <a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a> <a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
<a href="/docs/README.fa.md"><img alt="Persian" src="https://img.shields.io/badge/%D9%81%D8%A7%D8%B1%D8%B3%DB%8C-d9d9d9"></a>
<a href="/docs/README.lo.md"><img alt="ພາສາລາວ" src="https://img.shields.io/badge/%E0%BA%9E%E0%BA%B2%E0%BA%AA%E0%BA%B2%E0%BA%A5%E0%BA%B2%E0%BA%A7-d9d9d9"></a>
</p> </p>
--- ---
## What is 1Panel? ## What is 1Panel?
1Panel is a modern, open-source Linux server management panel and a lightweight AI management platform. Through an intuitive web interface, it provides users with comprehensive, one-stop server management capabilities: 1Panel is a modern, open-source VPS control panel — and the only one with **native AI agent support**. Run Ollama models, deploy OpenClaw agents, and manage your entire server stack from one clean web interface. No CLI memorization required.
- **AI Management**: Offers a unified management platform from bare metal to agents (Metal-to-Agent). It integrates an AI gateway, and Skills Hub, while supporting centralized management of agents and models.
- **Efficient Visual Operations**: Easily manage Linux servers through a web-based GUI, streamlining tasks such as host monitoring, file management, database management, and container management. 👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
- **Rapid Website Deployment**: Deeply integrates with popular website builders like WordPress and Halo. It enables one-click domain binding and SSL certificate configuration, significantly lowering the barrier to website creation.
- **Curated App Store**: Features a built-in store of high-quality open-source applications, providing one-click installation and upgrade services to effortlessly extend server capabilities.
- **Enterprise-Grade Security**: Deploys applications based on container technology to effectively minimize vulnerability exposure. It also provides security features such as WAF and log auditing to ensure comprehensive server protection.
- **One-Click Data Backup**: Supports one-click backup and restoration, and integrates with various cloud storage solutions to ensure data security and prevent loss.
## Why 1Panel? ## Why 1Panel?
| | 1Panel | cPanel / Plesk | aaPanel | Webmin | | | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|--|--------|----------------|---------|--------| |--|--------|----------------|---------|--------|
| Free & open source | ✅ | ❌ | Partial | ✅ | | Free & open source | ✅ | ❌ | Partial | ✅ |
| AI management | ✅ | ❌ | ❌ | ❌ | | Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ | | One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ | | Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
| Docker / container management | ✅ | ❌ | ❌ | ❌ | | Docker / container management | ✅ | ❌ | ❌ | ❌ |
| Active development | ✅ | ✅ | ✅ | Slow | | Active development | ✅ | ✅ | ✅ | Slow |
## Key Features
- **AI Agent Runtime**: Deploy Ollama LLMs, spin up OpenClaw personal agents, and monitor GPU utilization — all from the dashboard. No separate AI stack to manage.
- **One-Click Website Deployment**: Launch production-ready websites with automatic domain binding, SSL provisioning, and Nginx config — zero manual setup.
- **App Marketplace**: 165+ trusted open-source apps (Nextcloud, Bitwarden, Umami, NocoBase, and more) installed and updated with a single click.
- **Docker & Container Management**: Create, start, stop, and inspect containers, images, networks, and volumes through a visual UI — no CLI juggling.
- **Security Out of the Box**: Firewall rules, fail2ban, container isolation, WAF, and audit logs — configured and running from day one.
- **Backup & Restore**: Schedule automated backups to AWS S3, Cloudflare R2, or local storage. Restore any snapshot in one click.
## Quick Start ## Quick Start
Prepare your Linux server and run the following script: > **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
> Takes ~60 seconds.
```bash ```bash
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)" bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
@@ -73,20 +81,24 @@ Run `1pctl user-info` via SSH if you need to retrieve your access credentials.
## Pro Edition ## Pro Edition
1Panel OSS is free forever. 1Panel Pro and Ent adds features built for teams and production workloads: 1Panel OSS is free forever. Pro adds features built for teams and production workloads:
| Feature | OSS | Pro | Ent | | Feature | OSS | Pro |
|---------|:---:|:---:|:---:| |---------|:---:|:---:|
| One-click app installs | ✅ | ✅ | ✅ | | One-click app installs | ✅ | ✅ |
| AI agents (OpenClaw) | 5 agent | Unlimited | ✅ | | AI agents (OpenClaw) | 1 agent | Unlimited |
| WAF & advanced security | Basic | ✅ | ✅ | | WAF & advanced security | Basic | ✅ |
| Website tamper protection | ❌ | ✅ | ✅ | | Website tamper protection | ❌ | ✅ |
| Website uptime monitoring | ❌ | ✅ | ✅ | | Website uptime monitoring | ❌ | ✅ |
| Multi-node management | ❌ | ✅ | ✅ | | Multi-node management | ❌ | ✅ |
| Custom logo & theme | ❌ | ✅ | ✅ | | Custom logo & theme | ❌ | ✅ |
| KVM Web UI | ❌ | ❌ | ✅ | | Priority support | ❌ | ✅ |
| AI Gateway | ❌ | ❌ | ✅ |
| Priority support | ❌ | ❌ | ✅ | **From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/1Panel&type=Date)](https://star-history.com/#1Panel-dev/1Panel&Date)
## Community & Support ## Community & Support
-103
View File
@@ -500,27 +500,6 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags AI
// @Summary Discover custom provider models
// @Accept json
// @Param request body dto.AgentAccountModelDiscoverReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/accounts/models/discover [post]
func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelDiscoverReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
list, err := agentService.DiscoverAccountModels(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, list)
}
// @Tags AI // @Tags AI
// @Summary Create model account model // @Summary Create model account model
// @Accept json // @Accept json
@@ -1378,88 +1357,6 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags AI
// @Summary List OpenClaw plugins
// @Accept json
// @Param request body dto.AgentPluginsReq true "request"
// @Success 200 {array} dto.AgentPluginItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/list [post]
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
var req dto.AgentPluginsReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.ListPlugins(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Search OpenClaw plugins
// @Accept json
// @Param request body dto.AgentPluginSearchReq true "request"
// @Success 200 {array} dto.AgentPluginSearchItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/search [post]
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
var req dto.AgentPluginSearchReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.SearchPlugins(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Install an OpenClaw marketplace plugin
// @Accept json
// @Param request body dto.AgentPluginMarketInstallReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/install [post]
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
var req dto.AgentPluginMarketInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.InstallMarketPlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Operate an OpenClaw plugin
// @Accept json
// @Param request body dto.AgentPluginOperateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/plugins/operate [post]
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
var req dto.AgentPluginOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.OperatePlugin(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI // @Tags AI
// @Summary Login Agent Weixin channel // @Summary Login Agent Weixin channel
// @Accept json // @Accept json
-40
View File
@@ -2,14 +2,11 @@ package v2
import ( import (
"errors" "errors"
"net/http"
"net/url" "net/url"
"strings" "strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -297,34 +294,6 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
return return
} }
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil { if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
switch {
case errors.Is(err, repo.ErrAlertConfigRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrAlertConfigRevisionRequired):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_REQUIRED", "ErrInvalidParams", err)
default:
helper.InternalServer(c, err)
}
return
}
helper.Success(c)
}
// @Tags Alert
// @Summary Update alert config status
// @Accept json
// @Param request body dto.AlertConfigStatusUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/status [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
var req dto.AlertConfigStatusUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -377,15 +346,6 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if req.Type == constant.Custom {
result, err := alertService.TestCustomAlertConfig(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
return
}
flag, err := alertService.TestAlertConfig(req) flag, err := alertService.TestAlertConfig(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
+1 -6
View File
@@ -413,15 +413,10 @@ func (b *BaseApi) Backup(c *gin.Context) {
switch req.Type { switch req.Type {
case "app": case "app":
record, err := backupService.AppBackup(req) if _, err := backupService.AppBackup(req); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
if req.IsImmediate {
helper.SuccessWithData(c, record)
return
}
case "mysql", "mariadb", constant.AppMysqlCluster: case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil { if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
+1 -21
View File
@@ -439,7 +439,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
return return
} }
data, err := containerService.ContainerItemStats(c.Request.Context(), req) data, err := containerService.ContainerItemStats(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -881,26 +881,6 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Container Compose
// @Summary Pin compose
// @Accept json
// @Param request body dto.ComposePin true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/compose/pin [post]
func (b *BaseApi) ComposePin(c *gin.Context) {
var req dto.ComposePin
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := containerService.ComposePin(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Container Compose // @Tags Container Compose
// @Summary Load compose environment variables // @Summary Load compose environment variables
// @Accept json // @Accept json
+12 -215
View File
@@ -41,40 +41,20 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary List mysql users // @Summary Bind user of mysql database
// @Accept json // @Accept json
// @Param request body dto.MysqlUserSearch true "request" // @Param request body dto.BindUser true "request"
// @Success 200 {array} dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/search [post]
func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListUsers(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Create mysql user
// @Accept json
// @Param request body dto.MysqlUserCreate true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/users [post] // @Router /databases/bind [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"create mysql database [database] user [username]@[host]"} // @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"}
func (b *BaseApi) CreateMysqlUser(c *gin.Context) { func (b *BaseApi) BindUser(c *gin.Context) {
var req dto.MysqlUserCreate var req dto.BindUser
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if len(req.Password) != 0 { if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password) password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil { if err != nil {
@@ -83,191 +63,8 @@ func (b *BaseApi) CreateMysqlUser(c *gin.Context) {
} }
req.Password = string(password) req.Password = string(password)
} }
if err := mysqlService.CreateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql if err := mysqlService.BindUser(req); err != nil {
// @Summary Delete mysql user
// @Accept json
// @Param request body dto.MysqlUserDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/del [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"delete mysql database [database] user [username]@[host]"}
func (b *BaseApi) DeleteMysqlUser(c *gin.Context) {
var req dto.MysqlUserDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.DeleteUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Update mysql user
// @Accept json
// @Param request body dto.MysqlUserUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/update [post]
// @x-panel-log {"bodyKeys":["database","username","host","newHost","description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]","formatEN":"update mysql database [database] user [username] access [host] => [newHost] description [description]"}
func (b *BaseApi) UpdateMysqlUser(c *gin.Context) {
var req dto.MysqlUserUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.UpdateUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Change mysql user password
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"update mysql database [database] user [username]@[host] password"}
func (b *BaseApi) ChangeMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.ChangeUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Save mysql user password locally
// @Accept json
// @Param request body dto.MysqlUserPassword true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/users/password/save [post]
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"补充 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"save mysql database [database] user [username]@[host] password locally"}
func (b *BaseApi) SaveMysqlUserPassword(c *gin.Context) {
var req dto.MysqlUserPassword
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if len(req.Password) != 0 {
password, err := base64.StdEncoding.DecodeString(req.Password)
if err != nil {
helper.BadRequest(c, err)
return
}
req.Password = string(password)
}
if err := mysqlService.SaveUserPassword(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary List mysql grants
// @Accept json
// @Param request body dto.MysqlUserSearch true "request"
// @Success 200 {array} dto.MysqlGrant
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/search [post]
func (b *BaseApi) ListMysqlGrants(c *gin.Context) {
var req dto.MysqlUserSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrants(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary List mysql grant summary
// @Accept json
// @Param request body dto.MysqlGrantSummarySearch true "request"
// @Success 200 {object} map[string][]dto.MysqlUser
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/summary [post]
func (b *BaseApi) ListMysqlGrantSummary(c *gin.Context) {
var req dto.MysqlGrantSummarySearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := mysqlService.ListGrantSummary(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Database Mysql
// @Summary Grant mysql user
// @Accept json
// @Param request body dto.MysqlGrantCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]","formatEN":"grant mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) GrantMysqlUser(c *gin.Context) {
var req dto.MysqlGrantCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.GrantUser(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Database Mysql
// @Summary Revoke mysql grant
// @Accept json
// @Param request body dto.MysqlGrantDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/grants/del [post]
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权","formatEN":"revoke mysql database [database] user [username]@[host] access to [db]"}
func (b *BaseApi) RevokeMysqlGrant(c *gin.Context) {
var req dto.MysqlGrantDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := mysqlService.RevokeGrant(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -297,14 +94,14 @@ func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Change mysql root password // @Summary Change mysql password
// @Accept json // @Accept json
// @Param request body dto.ChangeDBInfo true "request" // @Param request body dto.ChangeDBInfo true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/change/password [post] // @Router /databases/change/password [post]
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 密码","formatEN":"Update database [database] root password"} // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"}
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) { func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
var req dto.ChangeDBInfo var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -328,14 +125,14 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
} }
// @Tags Database Mysql // @Tags Database Mysql
// @Summary Change mysql root access // @Summary Change mysql access
// @Accept json // @Accept json
// @Param request body dto.ChangeDBInfo true "request" // @Param request body dto.ChangeDBInfo true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/change/access [post] // @Router /databases/change/access [post]
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 访问权限","formatEN":"Update database [database] root access"} // @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"}
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) { func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
var req dto.ChangeDBInfo var req dto.ChangeDBInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+8 -12
View File
@@ -37,17 +37,14 @@ var (
cronjobService = service.NewICronjobService() cronjobService = service.NewICronjobService()
fileService = service.NewIFileService() fileService = service.NewIFileService()
fileHistoryService = service.NewIFileHistoryService() fileHistoryService = service.NewIFileHistoryService()
fileShareService = service.NewIFileShareService() fileShareService = service.NewIFileShareService()
sshService = service.NewISSHService() sshService = service.NewISSHService()
firewallService = service.NewIFirewallService() firewallService = service.NewIFirewallService()
firewallSettingService = service.NewIFirewallSettingService() iptablesService = service.NewIIptablesService()
forwardingService = service.NewIForwardingService() monitorService = service.NewIMonitorService()
dockerPortGuardService = service.NewIDockerPortGuardService() systemService = service.NewISystemService()
monitorService = service.NewIMonitorService()
systemService = service.NewISystemService()
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
deviceService = service.NewIDeviceService() deviceService = service.NewIDeviceService()
fail2banService = service.NewIFail2BanService() fail2banService = service.NewIFail2BanService()
@@ -62,7 +59,6 @@ var (
websiteDnsAccountService = service.NewIWebsiteDnsAccountService() websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
websiteSSLService = service.NewIWebsiteSSLService() websiteSSLService = service.NewIWebsiteSSLService()
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService() websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
websiteTemplateService = service.NewIWebsiteTemplateService()
nginxService = service.NewINginxService() nginxService = service.NewINginxService()
+21 -535
View File
@@ -12,9 +12,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"strings" "strings"
"sync"
"syscall" "syscall"
"time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,86 +28,6 @@ import (
qrcode "github.com/skip2/go-qrcode" qrcode "github.com/skip2/go-qrcode"
) )
var cancelledChunkUploads = struct {
sync.RWMutex
ids map[string]struct{}
}{ids: make(map[string]struct{})}
type chunkUploadLock struct {
mutex sync.Mutex
refs int
}
var chunkUploadLocks = struct {
sync.Mutex
items map[string]*chunkUploadLock
}{items: make(map[string]*chunkUploadLock)}
type completedChunkUpload struct {
dstDir string
filename string
fileSize int64
}
var completedChunkUploads = struct {
sync.RWMutex
items map[string]completedChunkUpload
}{items: make(map[string]completedChunkUpload)}
var activeChunkUploadTTL = 24 * time.Hour
var (
errChunkUploadCancelled = errors.New("upload cancelled")
errInvalidChunkUpload = errors.New("invalid chunk upload")
)
type activeChunkUpload struct {
upload completedChunkUpload
expiresAt time.Time
timer *time.Timer
}
var activeChunkUploads = struct {
sync.RWMutex
items map[string]activeChunkUpload
}{items: make(map[string]activeChunkUpload)}
type resumableUploadChunk struct {
UploadID string
Filename string
DstDir string
ChunkIndex int
ChunkCount int
Offset int64
FileSize int64
Overwrite bool
}
func invalidChunkUploadError(message string) error {
return fmt.Errorf("%w: %s", errInvalidChunkUpload, message)
}
func isRetryableChunkUploadError(err error) bool {
if err == nil {
return false
}
if errors.Is(err, errChunkUploadCancelled) ||
errors.Is(err, errInvalidChunkUpload) ||
errors.Is(err, os.ErrExist) ||
errors.Is(err, os.ErrPermission) ||
errors.Is(err, os.ErrInvalid) ||
errors.Is(err, syscall.ENOSPC) ||
errors.Is(err, syscall.EDQUOT) ||
errors.Is(err, syscall.EROFS) ||
errors.Is(err, syscall.EFBIG) ||
errors.Is(err, syscall.ENAMETOOLONG) ||
errors.Is(err, syscall.ENOTDIR) ||
errors.Is(err, syscall.EISDIR) {
return false
}
return true
}
// @Tags File // @Tags File
// @Summary List files // @Summary List files
// @Accept json // @Accept json
@@ -549,7 +467,11 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
continue continue
} }
dstInfo, statErr := os.Stat(dstFilename) dstInfo, statErr := os.Stat(dstFilename)
err = finalizeUploadedFile(tmpFilename, dstFilename, overwrite) if overwrite {
_ = os.Remove(dstFilename)
}
err = os.Rename(tmpFilename, dstFilename)
if err != nil { if err != nil {
_ = os.Remove(tmpFilename) _ = os.Remove(tmpFilename)
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
@@ -684,35 +606,10 @@ func (b *BaseApi) StopWget(c *gin.Context) {
return return
} }
if err := files.CancelDownload(req.Key); err != nil { files.CancelDownload(req.Key)
helper.InternalServer(c, err)
return
}
helper.Success(c) helper.Success(c)
} }
// @Tags File
// @Summary Remove finished download progress records without deleting files
// @Accept json
// @Param request body request.FileProcessRemoveReq true "request"
// @Success 200 {object} response.FileProcessKeys
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/process/remove [post]
// @x-panel-log {"bodyKeys":["keys"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除已结束下载记录 [keys]","formatEN":"Remove finished download records [keys]"}
func (b *BaseApi) RemoveWgetRecords(c *gin.Context) {
var req request.FileProcessRemoveReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
keys, err := files.RemoveDownloadRecords(req.Keys)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, response.FileProcessKeys{Keys: keys})
}
// @Tags File // @Tags File
// @Summary Move file // @Summary Move file
// @Accept json // @Accept json
@@ -734,26 +631,6 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags File
// @Summary Stop file move task
// @Accept json
// @Param request body request.FileMoveStopReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/move/stop [post]
func (b *BaseApi) StopMoveFile(c *gin.Context) {
var req request.FileMoveStopReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := fileService.StopMvFile(req.TaskID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags File // @Tags File
// @Summary Download file // @Summary Download file
// @Accept json // @Accept json
@@ -908,289 +785,6 @@ func (b *BaseApi) DepthDirSize(c *gin.Context) {
helper.SuccessWithData(c, res) helper.SuccessWithData(c, res)
} }
func lockChunkUpload(uploadID string) func() {
chunkUploadLocks.Lock()
lock, ok := chunkUploadLocks.items[uploadID]
if !ok {
lock = &chunkUploadLock{}
chunkUploadLocks.items[uploadID] = lock
}
lock.refs++
chunkUploadLocks.Unlock()
lock.mutex.Lock()
return func() {
lock.mutex.Unlock()
chunkUploadLocks.Lock()
lock.refs--
if lock.refs == 0 {
delete(chunkUploadLocks.items, uploadID)
}
chunkUploadLocks.Unlock()
}
}
func resumableUploadPartPath(dstDir, uploadID string) string {
return filepath.Join(dstDir, fmt.Sprintf(".1panel-upload-%s.part", uploadID))
}
func finalizeUploadedFile(tmpFile, dstFile string, overwrite bool) error {
if overwrite {
return os.Rename(tmpFile, dstFile)
}
if err := os.Link(tmpFile, dstFile); err != nil {
return err
}
if err := os.Remove(tmpFile); err != nil {
if rollbackErr := os.Remove(dstFile); rollbackErr != nil {
return fmt.Errorf("remove upload temporary file failed: %v, rollback destination failed: %w", err, rollbackErr)
}
return err
}
return nil
}
func registerActiveChunkUpload(uploadID string, upload completedChunkUpload) error {
activeChunkUploads.Lock()
defer activeChunkUploads.Unlock()
if active, ok := activeChunkUploads.items[uploadID]; ok {
if active.upload != upload {
return invalidChunkUploadError("upload ID is already used by another file")
}
active.timer.Stop()
}
expiresAt := time.Now().Add(activeChunkUploadTTL)
timer := time.AfterFunc(activeChunkUploadTTL, func() {
expireActiveChunkUpload(uploadID, expiresAt)
})
activeChunkUploads.items[uploadID] = activeChunkUpload{
upload: upload,
expiresAt: expiresAt,
timer: timer,
}
return nil
}
func loadActiveChunkUpload(uploadID string) (completedChunkUpload, bool) {
activeChunkUploads.RLock()
active, ok := activeChunkUploads.items[uploadID]
activeChunkUploads.RUnlock()
return active.upload, ok
}
func deleteActiveChunkUpload(uploadID string) {
activeChunkUploads.Lock()
if active, ok := activeChunkUploads.items[uploadID]; ok {
active.timer.Stop()
}
delete(activeChunkUploads.items, uploadID)
activeChunkUploads.Unlock()
}
func discardActiveChunkUpload(uploadID, partFile string) error {
deleteActiveChunkUpload(uploadID)
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("remove upload temporary file failed: %w", err)
}
return nil
}
func finalizeActiveChunkUpload(uploadID, partFile, dstFile string, overwrite bool) error {
if err := finalizeUploadedFile(partFile, dstFile, overwrite); err != nil {
if removeErr := discardActiveChunkUpload(uploadID, partFile); removeErr != nil {
return errors.Join(err, removeErr)
}
return err
}
return nil
}
func expireActiveChunkUpload(uploadID string, expiresAt time.Time) {
unlock := lockChunkUpload(uploadID)
defer unlock()
activeChunkUploads.Lock()
active, ok := activeChunkUploads.items[uploadID]
if !ok || !active.expiresAt.Equal(expiresAt) {
activeChunkUploads.Unlock()
return
}
delete(activeChunkUploads.items, uploadID)
activeChunkUploads.Unlock()
partFile := resumableUploadPartPath(active.upload.dstDir, uploadID)
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
global.LOG.Warnf("remove inactive upload part [%s] failed: %v", partFile, err)
}
}
func removeActiveResumableUploadPart(uploadID string) error {
unlock := lockChunkUpload(uploadID)
defer unlock()
upload, ok := loadActiveChunkUpload(uploadID)
if !ok {
return nil
}
err := os.Remove(resumableUploadPartPath(upload.dstDir, uploadID))
if err == nil || os.IsNotExist(err) {
deleteActiveChunkUpload(uploadID)
return nil
}
return err
}
func loadCompletedChunkUpload(uploadID string) (completedChunkUpload, bool) {
completedChunkUploads.RLock()
completed, ok := completedChunkUploads.items[uploadID]
completedChunkUploads.RUnlock()
return completed, ok
}
func markChunkUploadCompleted(uploadID string, completed completedChunkUpload) {
completedChunkUploads.Lock()
completedChunkUploads.items[uploadID] = completed
completedChunkUploads.Unlock()
time.AfterFunc(10*time.Minute, func() {
completedChunkUploads.Lock()
delete(completedChunkUploads.items, uploadID)
completedChunkUploads.Unlock()
})
}
func writeResumableUploadChunk(chunk resumableUploadChunk, chunkData []byte) error {
unlock := lockChunkUpload(chunk.UploadID)
defer unlock()
if chunkUploadCancelled(chunk.UploadID) {
return errChunkUploadCancelled
}
if chunk.UploadID == "" || filepath.Base(chunk.UploadID) != chunk.UploadID || strings.ContainsAny(chunk.UploadID, `/\`) {
return invalidChunkUploadError("invalid upload ID")
}
if chunk.Filename == "" || filepath.Base(chunk.Filename) != chunk.Filename || strings.ContainsAny(chunk.Filename, `/\`) {
return invalidChunkUploadError("invalid filename")
}
if strings.TrimSpace(chunk.DstDir) == "" {
return invalidChunkUploadError("upload destination is required")
}
dstDir := filepath.Clean(strings.TrimSpace(chunk.DstDir))
if chunk.ChunkCount <= 0 || chunk.ChunkIndex < 0 || chunk.ChunkIndex >= chunk.ChunkCount {
return invalidChunkUploadError("invalid chunk index")
}
if chunk.FileSize <= 0 || chunk.Offset < 0 || chunk.Offset > chunk.FileSize {
return invalidChunkUploadError("invalid upload offset")
}
chunkEnd := chunk.Offset + int64(len(chunkData))
if chunkEnd > chunk.FileSize {
return invalidChunkUploadError("chunk exceeds file size")
}
if chunk.ChunkIndex+1 == chunk.ChunkCount {
if chunkEnd != chunk.FileSize {
return invalidChunkUploadError("final chunk does not match file size")
}
} else if chunkEnd >= chunk.FileSize {
return invalidChunkUploadError("non-final chunk reaches file size")
}
if completed, ok := loadCompletedChunkUpload(chunk.UploadID); ok {
if completed.dstDir == dstDir && completed.filename == chunk.Filename && completed.fileSize == chunk.FileSize {
return nil
}
return invalidChunkUploadError("upload ID has already completed another file")
}
upload := completedChunkUpload{dstDir: dstDir, filename: chunk.Filename, fileSize: chunk.FileSize}
if err := registerActiveChunkUpload(chunk.UploadID, upload); err != nil {
return err
}
mode, err := files.GetParentMode(dstDir)
if err != nil {
return err
}
if err = os.MkdirAll(dstDir, mode); err != nil {
return err
}
dstDirInfo, err := os.Stat(dstDir)
if err != nil {
return err
}
if !dstDirInfo.IsDir() {
return invalidChunkUploadError(fmt.Sprintf("upload destination [%s] is not a directory", dstDir))
}
dstFile := filepath.Join(dstDir, chunk.Filename)
partFile := resumableUploadPartPath(dstDir, chunk.UploadID)
if dstFile == partFile {
return invalidChunkUploadError("filename conflicts with upload temporary file")
}
fileMode := dstDirInfo.Mode().Perm()
ownerInfo := dstDirInfo
if dstInfo, statErr := os.Stat(dstFile); statErr == nil {
if !chunk.Overwrite {
if err := discardActiveChunkUpload(chunk.UploadID, partFile); err != nil {
return errors.Join(os.ErrExist, err)
}
return os.ErrExist
}
fileMode = dstInfo.Mode().Perm()
ownerInfo = dstInfo
} else if !os.IsNotExist(statErr) {
return statErr
}
part, err := os.OpenFile(partFile, os.O_CREATE|os.O_RDWR, fileMode)
if err != nil {
return err
}
partClosed := false
defer func() {
if !partClosed {
_ = part.Close()
}
}()
if stat, statErr := part.Stat(); statErr != nil {
return statErr
} else if chunk.Offset > stat.Size() {
return invalidChunkUploadError(fmt.Sprintf("unexpected upload offset %d, current size is %d", chunk.Offset, stat.Size()))
} else if chunk.Offset < stat.Size() && chunkEnd > stat.Size() {
if err = part.Truncate(chunk.Offset); err != nil {
return err
}
}
if _, err = part.WriteAt(chunkData, chunk.Offset); err != nil {
return err
}
if chunk.ChunkIndex+1 != chunk.ChunkCount {
return nil
}
partInfo, err := part.Stat()
if err != nil {
return err
}
if partInfo.Size() != chunk.FileSize {
return invalidChunkUploadError(fmt.Sprintf("uploaded file size mismatch: expected %d, got %d", chunk.FileSize, partInfo.Size()))
}
if err = part.Close(); err != nil {
return err
}
partClosed = true
if err = os.Chmod(partFile, fileMode); err != nil {
return err
}
if stat, ok := ownerInfo.Sys().(*syscall.Stat_t); ok {
if err = os.Chown(partFile, int(stat.Uid), int(stat.Gid)); err != nil {
return err
}
}
if chunkUploadCancelled(chunk.UploadID) {
return errChunkUploadCancelled
}
if err = finalizeActiveChunkUpload(chunk.UploadID, partFile, dstFile, chunk.Overwrite); err != nil {
return err
}
markChunkUploadCompleted(chunk.UploadID, upload)
deleteActiveChunkUpload(chunk.UploadID)
return nil
}
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error { func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
defer func() { defer func() {
_ = os.RemoveAll(fileDir) _ = os.RemoveAll(fileDir)
@@ -1270,10 +864,6 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
if chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount {
helper.BadRequest(c, errors.New("invalid chunk index"))
return
}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
tmpDir := path.Join(global.Dir.TmpDir, "upload") tmpDir := path.Join(global.Dir.TmpDir, "upload")
if !fileOp.Stat(tmpDir) { if !fileOp.Stat(tmpDir) {
@@ -1283,30 +873,8 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
} }
} }
filename := c.PostForm("filename") filename := c.PostForm("filename")
if filename == "" || filepath.Base(filename) != filename || strings.ContainsAny(filename, `/\\`) { fileDir := filepath.Join(tmpDir, filename)
helper.BadRequest(c, errors.New("invalid filename")) if chunkIndex == 0 {
return
}
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
resumable := c.PostForm("fileSize") != "" || c.PostForm("offset") != ""
cancellable := uploadID != ""
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
if resumable && !cancellable {
helper.BadRequest(c, errors.New("upload ID is required"))
return
}
if !cancellable {
uploadID = filename
}
fileDir := filepath.Join(tmpDir, uploadID)
if cancellable && chunkUploadCancelled(uploadID) {
helper.BadRequest(c, errChunkUploadCancelled)
return
}
if !resumable && chunkIndex == 0 {
if fileOp.Stat(fileDir) { if fileOp.Stat(fileDir) {
_ = fileOp.DeleteDir(fileDir) _ = fileOp.DeleteDir(fileDir)
} }
@@ -1315,68 +883,28 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
filePath := filepath.Join(fileDir, filename) filePath := filepath.Join(fileDir, filename)
defer func() { defer func() {
if !resumable && err != nil { if err != nil {
_ = os.RemoveAll(fileDir) _ = os.Remove(fileDir)
} }
}() }()
chunkData, err := io.ReadAll(uploadFile) var (
if err != nil { emptyFile *os.File
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)) chunkData []byte
return )
}
if cancellable && chunkUploadCancelled(uploadID) {
err = errChunkUploadCancelled
helper.BadRequest(c, err)
return
}
if resumable {
offset, parseErr := strconv.ParseInt(c.PostForm("offset"), 10, 64)
if parseErr != nil {
helper.BadRequest(c, parseErr)
return
}
fileSize, parseErr := strconv.ParseInt(c.PostForm("fileSize"), 10, 64)
if parseErr != nil {
helper.BadRequest(c, parseErr)
return
}
overwrite := true
if ow := c.PostForm("overwrite"); ow != "" {
overwrite, _ = strconv.ParseBool(ow)
}
err = writeResumableUploadChunk(resumableUploadChunk{
UploadID: uploadID,
Filename: filename,
DstDir: c.PostForm("path"),
ChunkIndex: chunkIndex,
ChunkCount: chunkCount,
Offset: offset,
FileSize: fileSize,
Overwrite: overwrite,
}, chunkData)
if err != nil {
uploadErr := buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)
helper.ErrorWithDetailAndData(c, http.StatusInternalServerError, "ErrInternalServer", uploadErr, gin.H{
"retryable": isRetryableChunkUploadError(err),
})
return
}
if chunkIndex+1 == chunkCount {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
}
helper.SuccessWithData(c, true)
return
}
emptyFile, err := os.Create(filePath) emptyFile, err = os.Create(filePath)
if err != nil { if err != nil {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
defer emptyFile.Close() defer emptyFile.Close()
chunkData, err = io.ReadAll(uploadFile)
if err != nil {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return
}
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex)) chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm) err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
if err != nil { if err != nil {
@@ -1394,54 +922,12 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)) helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
return return
} }
if cancellable {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
}
helper.SuccessWithData(c, true) helper.SuccessWithData(c, true)
} else { } else {
return return
} }
} }
// StopChunkUpload removes temporary chunks left by a cancelled upload.
func (b *BaseApi) StopChunkUpload(c *gin.Context) {
var req request.FileProcessReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
uploadID := strings.TrimSpace(req.Key)
if uploadID == "" || filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`) {
helper.BadRequest(c, errors.New("invalid upload ID"))
return
}
cancelledChunkUploads.Lock()
cancelledChunkUploads.ids[uploadID] = struct{}{}
cancelledChunkUploads.Unlock()
time.AfterFunc(10*time.Minute, func() {
cancelledChunkUploads.Lock()
delete(cancelledChunkUploads.ids, uploadID)
cancelledChunkUploads.Unlock()
})
if err := os.RemoveAll(filepath.Join(global.Dir.TmpDir, "upload", uploadID)); err != nil {
helper.InternalServer(c, err)
return
}
if err := removeActiveResumableUploadPart(uploadID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func chunkUploadCancelled(uploadID string) bool {
cancelledChunkUploads.RLock()
_, ok := cancelledChunkUploads.ids[uploadID]
cancelledChunkUploads.RUnlock()
return ok
}
var wsUpgrade = websocket.Upgrader{ var wsUpgrade = websocket.Upgrader{
CheckOrigin: func(r *http.Request) bool { CheckOrigin: func(r *http.Request) bool {
return true return true
+224 -569
View File
@@ -1,53 +1,26 @@
package v2 package v2
import ( import (
"errors"
"net/http"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
func (b *BaseApi) UpdatePanelFirewallPort(c *gin.Context) {
if !global.IsMaster {
c.AbortWithStatus(http.StatusForbidden)
return
}
var request struct {
OldPort uint `json:"oldPort" validate:"required,min=1,max=65535"`
NewPort uint `json:"newPort" validate:"required,min=1,max=65535"`
}
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallService.UpdatePanelPort(c.Request.Context(), request.OldPort, request.NewPort); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall // @Tags Firewall
// @Summary Load firewall base info // @Summary Load firewall base info
// @Accept json // @Accept json
// @Param request body dto.OperationWithName true "request" // @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.FirewallSubsystemStatus // @Success 200 {object} dto.FirewallBaseInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/base [post] // @Router /hosts/firewall/base [post]
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) { func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
var request dto.OperationWithName var req dto.OperationWithName
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
data, err := firewallService.LoadBaseInfo(request.Name) data, err := firewallService.LoadBaseInfo(req.Name)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -56,22 +29,48 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Firewall
// @Summary Page firewall rules
// @Accept json
// @Param request body dto.RuleSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/search [post]
func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
var req dto.RuleSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, list, err := firewallService.SearchWithPage(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
}
// @Tags Firewall // @Tags Firewall
// @Summary Operate firewall // @Summary Operate firewall
// @Accept json // @Accept json
// @Param request body dto.FirewallLifecycleOperation true "request" // @Param request body dto.FirewallOperation true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/operate [post] // @Router /hosts/firewall/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"} // @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
func (b *BaseApi) OperateFirewall(c *gin.Context) { func (b *BaseApi) OperateFirewall(c *gin.Context) {
var request dto.FirewallLifecycleOperation var req dto.FirewallOperation
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := firewallService.OperateFirewall(request); err != nil { if err := firewallService.OperateFirewall(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -80,605 +79,261 @@ func (b *BaseApi) OperateFirewall(c *gin.Context) {
} }
// @Tags Firewall // @Tags Firewall
// @Summary Load forwarding base info // @Summary Create group
// @Accept json // @Accept json
// @Success 200 {object} dto.FirewallSubsystemStatus // @Param request body dto.PortRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/base [post] // @Router /hosts/firewall/port [post]
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) { // @x-panel-log {"bodyKeys":["port","strategy"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加端口规则 [strategy] [port]","formatEN":"create port rules [strategy][port]"}
data, err := forwardingService.LoadBaseInfo() func (b *BaseApi) OperatePortRule(c *gin.Context) {
if err != nil { var req dto.PortRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.OperatePortRule(req, true); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, data) helper.Success(c)
}
// OperateForwardRule
// @Tags Firewall
// @Summary Operate forward rule
// @Accept json
// @Param request body dto.ForwardRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/forward [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
func (b *BaseApi) OperateForwardRule(c *gin.Context) {
var req dto.ForwardRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.OperateForwardRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Page forwarding rules // @Summary Operate Ip rule
// @Accept json // @Accept json
// @Param request body dto.ForwardRuleSearch true "request" // @Param request body dto.AddrRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/ip [post]
// @x-panel-log {"bodyKeys":["strategy","address"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加 ip 规则 [strategy] [address]","formatEN":"create address rules [strategy][address]"}
func (b *BaseApi) OperateIPRule(c *gin.Context) {
var req dto.AddrRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.OperateAddressRule(req, true); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Batch operate rule
// @Accept json
// @Param request body dto.BatchRuleOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/batch [post]
func (b *BaseApi) BatchOperateRule(c *gin.Context) {
var req dto.BatchRuleOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.BatchOperateRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update rule description
// @Accept json
// @Param request body dto.UpdateFirewallDescription true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/update/description [post]
func (b *BaseApi) UpdateFirewallDescription(c *gin.Context) {
var req dto.UpdateFirewallDescription
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.UpdateDescription(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update port rule
// @Accept json
// @Param request body dto.PortRuleUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/update/port [post]
func (b *BaseApi) UpdatePortRule(c *gin.Context) {
var req dto.PortRuleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.UpdatePortRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Update Ip rule
// @Accept json
// @Param request body dto.AddrRuleUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/update/addr [post]
func (b *BaseApi) UpdateAddrRule(c *gin.Context) {
var req dto.AddrRuleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := firewallService.UpdateAddrRule(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary search iptables filter rules
// @Accept json
// @Param request body dto.SearchPageWithType true "request"
// @Success 200 {object} dto.PageResult // @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/search [post] // @Router /hosts/firewall/filter/rule/search [post]
func (b *BaseApi) SearchForwardingRules(c *gin.Context) { func (b *BaseApi) SearchFilterRules(c *gin.Context) {
var request dto.ForwardRuleSearch var req dto.SearchPageWithType
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
total, items, err := forwardingService.SearchRules(request)
total, list, err := iptablesService.Search(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total}) helper.SuccessWithData(c, dto.PageResult{
Items: list,
Total: total,
})
} }
// @Tags Firewall // @Tags Firewall
// @Summary Operate forwarding rules // @Summary Operate iptables filter rule
// @Accept json // @Accept json
// @Param request body dto.ForwardRuleOperate true "request" // @Param request body dto.IptablesRuleOp true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/operate [post] // @Router /hosts/firewall/filter/rule/operate [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"} // @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
func (b *BaseApi) OperateForwardingRules(c *gin.Context) { func (b *BaseApi) OperateFilterRule(c *gin.Context) {
var request dto.ForwardRuleOperate var req dto.IptablesRuleOp
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := iptablesService.OperateRule(req, true); err != nil {
result, err := forwardingService.OperateRules(request)
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, result)
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Enable forwarding // @Summary Batch operate iptables filter rules
// @Accept json // @Accept json
// @Param request body dto.FirewallInitializationTask true "request" // @Param request body dto.IptablesBatchOperate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/forward/enable [post] // @Router /hosts/firewall/filter/rule/batch [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化并启用端口转发","formatEN":"initialize and enable port forwarding"} func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
func (b *BaseApi) EnableForwarding(c *gin.Context) { var req dto.IptablesBatchOperate
var request dto.FirewallInitializationTask if err := helper.CheckBindAndValidate(&req, c); err != nil {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
result, err := forwardingService.QueueInitialization(request)
if err != nil { if err := iptablesService.BatchOperate(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, result)
helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Apply/Unload/Init firewall filter chain // @Summary Apply/Unload/Init iptables filter
// @Accept json // @Accept json
// @Param request body dto.FilterChainOperation true "request" // @Param request body dto.IptablesOp true "request"
// @Success 200 {object} dto.FilterChainOperationResponse // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/filter/operate [post] // @Router /hosts/firewall/filter/operate [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 防火墙过滤链","formatEN":"[operate] firewall filter chain"} // @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] iptables filter 防火墙","formatEN":"[operate] iptables filter firewall"}
func (b *BaseApi) OperateFilterChain(c *gin.Context) { func (b *BaseApi) OperateFilterChain(c *gin.Context) {
var request dto.FilterChainOperation var req dto.IptablesOp
if err := helper.CheckBindAndValidate(&request, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if request.Operate == "init-base" { if err := iptablesService.Operate(req); err != nil {
result, err := firewallService.QueueFilterChainInitialization(request)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
return
}
if err := firewallService.OperateFilterChain(request); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, dto.FilterChainOperationResponse{})
}
// @Tags Firewall
// @Summary List unified firewall v2 rules
// @Accept json
// @Param request body dto.FirewallRuleInventory true "request"
// @Success 200 {object} dto.FirewallRuleInventoryResponse
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/search [post]
func (b *BaseApi) SearchFirewallRules(c *gin.Context) {
var request dto.FirewallRuleInventory
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
inventory, err := firewallService.Inventory(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, inventory)
}
// @Tags Firewall
// @Summary Reset firewall rules
// @Accept json
// @Param request body dto.FirewallRuleReset true "request"
// @Success 200 {object} dto.FirewallRuleResetResponse
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/reset [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重置防火墙规则","formatEN":"reset firewall rules"}
func (b *BaseApi) ResetFirewallRules(c *gin.Context) {
var request dto.FirewallRuleReset
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Reset(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Load one provider-native firewall object definition
// @Accept json
// @Param request body dto.FirewallNativeDetail true "request"
// @Success 200 {string} string
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/native/detail [post]
func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
var request dto.FirewallNativeDetail
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
info, err := firewallService.LoadFirewallNativeDetail(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, info)
}
// @Tags Firewall
// @Summary Adopt an external firewall rule
// @Accept json
// @Param request body dto.FirewallRuleAdopt true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/adopt [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
var request dto.FirewallRuleAdopt
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c) helper.Success(c)
} }
// @Tags Firewall // @Tags Firewall
// @Summary Queue firewall rule creation // @Summary load chain status with name
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
// @Accept json // @Accept json
// @Param request body dto.FirewallRuleCreate true "request" // @Param request body dto.OperationWithName true "request"
// @Success 200 {object} dto.FirewallRuleCreateResponse
// @Failure 400 {object} dto.Response
// @Failure 409 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加防火墙规则","formatEN":"create firewall rules"}
func (b *BaseApi) CreateFirewallRules(c *gin.Context) {
var request dto.FirewallRuleCreate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Create(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Preview firewall rule synchronization
// @Accept json
// @Param request body dto.FirewallRuleSyncRequest true "request"
// @Success 200 {object} dto.FirewallRuleSyncPreview
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/sync/preview [post]
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
var request dto.FirewallRuleSyncRequest
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Load the currently executing firewall rule synchronization task
// @Success 200 {object} dto.FirewallRuleSyncTask
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/sync/task [get]
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
result, err := firewallService.CurrentRuleSyncTask()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Synchronize firewall rules to a target backend
// @Accept json
// @Param request body dto.FirewallRuleSyncRequest true "request"
// @Success 200 {object} dto.FirewallRuleSyncResult
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/sync [post]
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
var request dto.FirewallRuleSyncRequest
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Delete managed unified firewall v2 rules
// @Accept json
// @Param request body dto.FirewallRuleDelete true "request"
// @Success 200 {object} dto.FirewallRuleDeleteResponse
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/delete [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙规则","formatEN":"delete firewall rules"}
func (b *BaseApi) DeleteFirewallRules(c *gin.Context) {
var request dto.FirewallRuleDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallService.Delete(c.Request.Context(), request)
if err != nil {
handleFirewallRuleError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Update a managed unified firewall v2 rule
// @Accept json
// @Param request body dto.FirewallRuleUpdate true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/update [post]
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
var request dto.FirewallRuleUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if !normalizeFirewallRuleUUID(c, &request.UUID) {
return
}
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Reorder a managed unified firewall v2 rule
// @Accept json
// @Param request body dto.FirewallRuleReorder true "request"
// @Success 200
// @Failure 400 {object} dto.Response
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/rules/reorder [post]
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
var request dto.FirewallRuleReorder
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if !normalizeFirewallRuleUUID(c, &request.UUID) {
return
}
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
handleFirewallRuleError(c, err)
return
}
helper.Success(c)
}
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
if value == nil {
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
return false
}
*value = strings.TrimSpace(*value)
if *value == "" {
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
return false
}
return true
}
func handleFirewallRuleError(c *gin.Context, err error) {
switch {
case errors.Is(err, filter.ErrProtectedRule):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_LOCKOUT_RISK", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrRuleStale):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_STALE", "ErrInvalidParams", err)
case errors.Is(err, repo.ErrFirewallRuleRevisionConflict):
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrManagedScopeChange):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
case errors.Is(err, filter.ErrVerificationFailed):
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
default:
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
}
}
// @Tags Firewall
// @Summary Load firewall settings
// @Success 200 {object} dto.FirewallSettings
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings [get]
func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
data, err := firewallSettingService.Load(c.Request.Context())
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Queue firewall port whitelist update
// @Description Returns a taskID; configuration save and per-rule results are recorded in the task log.
// @Accept json
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/settings/whitelist [post]
// @x-panel-log {"bodyKeys":["value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙端口白名单 [value]","formatEN":"update firewall port whitelist [value]"}
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
var request dto.FirewallPortWhitelistUpdate
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := firewallSettingService.QueuePortWhitelist(request.Value)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Operate firewall backend
// @Accept json
// @Param request body dto.FirewallBackendOperation true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/settings/operate [post] // @Router /hosts/firewall/filter/chain/status [post]
// @x-panel-log {"bodyKeys":["subsystem","backend","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"防火墙子系统 [subsystem] 后端 [operation] [backend]","formatEN":"[operation] firewall [subsystem] backend [backend]"} func (b *BaseApi) LoadChainStatus(c *gin.Context) {
func (b *BaseApi) OperateFirewallBackend(c *gin.Context) { var req dto.OperationWithName
var request dto.FirewallBackendOperation if err := helper.CheckBindAndValidate(&req, c); err != nil {
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return return
} }
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
if errors.Is(err, service.ErrFirewallBackendCleanupRequired) {
helper.ErrorWithBusinessCode(
c,
http.StatusConflict,
"FW_BACKEND_CLEANUP_REQUIRED",
"ErrInvalidParams",
err,
)
return
}
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall helper.SuccessWithData(c, iptablesService.LoadChainStatus(req))
// @Summary List Docker port guard status and policies
// @Success 200 {object} dto.DockerPortGuardList
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/ports [get]
func (b *BaseApi) ListDockerPortGuard(c *gin.Context) {
data, err := dockerPortGuardService.LoadOverview(c.Request.Context())
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary List Docker published ports
// @Success 200 {array} dto.DockerPortGuardContainer
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/endpoints [get]
func (b *BaseApi) ListDockerPublishedPorts(c *gin.Context) {
data, err := dockerPortGuardService.LoadPublishedPorts(c.Request.Context())
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Firewall
// @Summary Sync Docker port guard rules
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/sync [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Operate Docker port guard
// @Accept json
// @Param request body dto.DockerPortGuardOperation true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Docker 端口防护","formatEN":"[operation] Docker port guard"}
func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
var request dto.DockerPortGuardOperation
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
if request.Operation == "initialize" {
result, err := dockerPortGuardService.QueueInitialization(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
return
}
if err := dockerPortGuardService.Operate(c.Request.Context(), request); err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.Success(c)
}
// @Tags Firewall
// @Summary Delete Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/delete/batch [post]
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
var request dto.DockerPortGuardPolicyBatchDelete
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := dockerPortGuardService.DeletePolicies(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
}
// @Tags Firewall
// @Summary Batch upsert Docker port guard policies
// @Accept json
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
// @Success 200 {object} dto.FilterChainOperationResponse
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/docker/policies/batch [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"}
func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
var request dto.DockerPortGuardPolicyBatch
if err := helper.CheckBindAndValidate(&request, c); err != nil {
return
}
result, err := dockerPortGuardService.UpsertPolicies(request)
if err != nil {
handleDockerPortGuardError(c, err)
return
}
helper.SuccessWithData(c, result)
}
func handleDockerPortGuardError(c *gin.Context, err error) {
if errors.Is(err, service.ErrDockerIptablesChainUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE", "ErrDockerIptablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerNftablesChainUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE", "ErrDockerNftablesChainUnavailable", err)
return
}
if errors.Is(err, service.ErrDockerGuardInvalid) {
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID", "ErrInvalidParams", err)
return
}
if errors.Is(err, service.ErrDockerUnavailable) {
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
return
}
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
} }
+17 -9
View File
@@ -3,8 +3,9 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator" "github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -16,20 +17,27 @@ import (
// @Security Timestamp // @Security Timestamp
// @Router /ai/gpu/load [get] // @Router /ai/gpu/load [get]
func (b *BaseApi) LoadGpuInfo(c *gin.Context) { func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
ok, client := accelerator.New() ok, client := gpu.New()
if ok { if ok {
snapshot, err := client.Collect(c.Request.Context()) info, err := client.LoadGpuInfo()
if err != nil { if err != nil {
helper.BadRequest(c, err) helper.BadRequest(c, err)
return return
} }
if warning := snapshot.Warning(); warning != nil { helper.SuccessWithData(c, info)
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
}
helper.SuccessWithData(c, &snapshot.Info)
return return
} }
helper.SuccessWithData(c, &accelerator.Info{}) xpuOK, xpuClient := xpu.New()
if xpuOK {
info, err := xpuClient.LoadGpuInfo()
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, info)
return
}
helper.SuccessWithData(c, &common.GpuInfo{})
} }
// @Tags AI // @Tags AI
-20
View File
@@ -30,26 +30,6 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
ctx.Abort() ctx.Abort()
} }
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
res := dto.Response{
Code: code,
ErrorCode: businessCode,
Message: i18n.GetMsgWithDetail(msgKey, err.Error()),
}
ctx.JSON(http.StatusOK, res)
ctx.Abort()
}
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
res := dto.Response{
Code: code,
Data: data,
}
res.Message = i18n.GetMsgWithDetail(msgKey, err.Error())
ctx.JSON(http.StatusOK, res)
ctx.Abort()
}
func InternalServer(ctx *gin.Context, err error) { func InternalServer(ctx *gin.Context, err error) {
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err) ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
} }
-55
View File
@@ -2,7 +2,6 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -21,57 +20,3 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Logs
// @Summary Get host system log status
// @Produce json
// @Success 200 {object} dto.SystemLogStatus
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/status [get]
func (b *BaseApi) GetSystemLogStatus(c *gin.Context) {
data, err := logService.GetSystemLogStatus()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary Read host logs
// @Accept json
// @Param request body dto.SystemLogReq true "request"
// @Produce json
// @Success 200 {object} dto.SystemLogRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/read [post]
func (b *BaseApi) ReadSystemLog(c *gin.Context) {
var req dto.SystemLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := logService.ReadSystemLog(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Logs
// @Summary List running host services
// @Produce json
// @Success 200 {array} string
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/system/services [get]
func (b *BaseApi) ListRunningServices(c *gin.Context) {
data, err := logService.ListRunningServices()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
-63
View File
@@ -23,27 +23,6 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags McpServer
// @Summary Load mcp server detail
// @Accept json
// @Param request body request.McpServerDetail true "request"
// @Success 200 {object} response.McpServerDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/detail [post]
func (b *BaseApi) LoadMcpServerDetail(c *gin.Context) {
var req request.McpServerDetail
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.Detail(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Create mcp server // @Summary Create mcp server
// @Accept json // @Accept json
@@ -128,48 +107,6 @@ func (b *BaseApi) OperateMcpServer(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags McpServer
// @Summary Sync mcp server status
// @Accept json
// @Param request body request.McpServerStatusSync true "request"
// @Success 200 {array} response.McpServerStatusDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/status/sync [post]
func (b *BaseApi) SyncMcpServerStatus(c *gin.Context) {
var req request.McpServerStatusSync
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.SyncStatus(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Test mcp server connection
// @Accept json
// @Param request body request.McpServerConnectionTest true "request"
// @Success 200 {object} response.McpServerConnectionTestRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/connection/test [post]
func (b *BaseApi) TestMcpServerConnection(c *gin.Context) {
var req request.McpServerConnectionTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.TestConnection(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Bind Domain for mcp server // @Summary Bind Domain for mcp server
// @Accept json // @Accept json
-63
View File
@@ -1,63 +0,0 @@
package v2
import (
"os"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
// @Tags RuntimeDiagnostics
// @Summary Load runtime diagnostics summary
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/summary [get]
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
data, err := runtimeDiagnosticsService.Summary()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Load grouped goroutine snapshot
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/goroutines [get]
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
data, err := runtimeDiagnosticsService.Goroutines()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithDataGzipped(c, data)
}
// @Tags RuntimeDiagnostics
// @Summary Capture runtime profile
// @Param request body dto.RuntimeProfileCreate true "request"
// @Success 200 {file} file
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/diagnostics/profiles [post]
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
var req dto.RuntimeProfileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
profile, err := runtimeDiagnosticsService.CreateProfile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
defer os.Remove(profile.Path)
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
c.Header("Content-Type", "application/octet-stream")
c.File(profile.Path)
c.Abort()
}
-15
View File
@@ -224,21 +224,6 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
helper.SuccessWithData(c, tmpFile) helper.SuccessWithData(c, tmpFile)
} }
// @Tags SSH
// @Summary Clean host SSH logs
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/ssh/log/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 SSH 登录日志","formatEN":"clean SSH login logs"}
func (b *BaseApi) CleanSSHLogs(c *gin.Context) {
if err := sshService.CleanLog(); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags SSH // @Tags SSH
// @Summary Load host SSH conf // @Summary Load host SSH conf
// @Accept json // @Accept json
+31 -140
View File
@@ -1,14 +1,11 @@
package v2 package v2
import ( import (
"crypto/sha256"
"encoding/base64" "encoding/base64"
"encoding/hex"
"encoding/json" "encoding/json"
"fmt" "fmt"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
@@ -22,33 +19,29 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/gorilla/websocket" "github.com/gorilla/websocket"
"github.com/pkg/errors" "github.com/pkg/errors"
gossh "golang.org/x/crypto/ssh"
) )
// @Tags Terminal // @Tags Terminal
// @Summary Ws local terminal // @Summary Ws local terminal
// @Param command query string false "command" // @Param command query string false "command"
// @Param session query string false "session id to reattach"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/terminal/local [get] // @Router /hosts/terminal/local [get]
func (b *BaseApi) WsLocalTerminal(c *gin.Context) { func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", "")) b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
} }
// @Tags Terminal // @Tags Terminal
// @Summary Ws host SSH // @Summary Ws host SSH
// @Param id query integer false "id" // @Param id query integer false "id"
// @Param command query string false "command" // @Param command query string false "command"
// @Param session query string false "session id to reattach"
// @Param title query string false "session title shown in the session list"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/terminal/ssh [get] // @Router /hosts/terminal/ssh [get]
func (b *BaseApi) WsHostSSH(c *gin.Context) { func (b *BaseApi) WsHostSSH(c *gin.Context) {
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) { b.runSSHSession(c, func() (*ssh.SSHClient, error) {
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0")) hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
if hostID <= 0 { if hostID <= 0 {
return nil, errors.New("missing host id") return nil, errors.New("missing host id")
@@ -72,33 +65,26 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
return return
} }
defer wsConn.Close() defer wsConn.Close()
identity, ok := loadTerminalIdentity(c)
if !ok { slave, err := loadContainerTerminalCommand(c)
_ = wshandleError(wsConn, errors.New("missing terminal identity")) if wshandleError(wsConn, err) {
return
}
defer slave.Close()
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
if wshandleError(wsConn, err) {
return return
} }
opts := terminal.SessionOptions{ quitChan := make(chan bool, 3)
Identity: identity, tty.Start(quitChan)
Kind: "container", go slave.Wait(quitChan)
Target: containerTerminalTarget(c),
Cols: cols,
Rows: rows,
}
if err := terminal.ServeCommand(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*terminal.LocalCommand, error) {
return loadContainerTerminalCommand(c)
}); err != nil {
_ = wshandleError(wsConn, err)
}
}
func containerTerminalTarget(c *gin.Context) string { <-quitChan
query := c.Request.URL.Query()
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} { global.LOG.Info("websocket finished")
query.Del(key) closeTerminalConn(wsConn)
}
sum := sha256.Sum256([]byte(query.Encode()))
return hex.EncodeToString(sum[:])
} }
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) { func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
@@ -129,127 +115,32 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
return wsConn, cols, rows, true return wsConn, cols, rows, true
} }
func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ssh.SSHClient, error), command string) { func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
wsConn, cols, rows, ok := prepareTerminalSession(c) wsConn, cols, rows, ok := prepareTerminalSession(c)
if !ok { if !ok {
return return
} }
defer wsConn.Close() defer wsConn.Close()
identity, ok := loadTerminalIdentity(c)
if !ok {
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
return
}
hostID := 0 client, clientErr := connect()
if kind == "ssh" { if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0")) return
} }
opts := terminal.SessionOptions{ defer client.Close()
Identity: identity,
Kind: kind,
Title: sanitizeTerminalTitle(c.Query("title")),
HostID: uint(max(hostID, 0)),
Cols: cols,
Rows: rows,
InitCmd: command,
}
err := terminal.Serve(wsConn, strings.TrimSpace(c.Query("session")), opts, func() (*gossh.Client, error) {
client, err := connect()
if err != nil {
return nil, errors.WithMessage(err, "failed to set up the connection. Please check the host information")
}
return client.Client, nil
})
if err != nil {
_ = wshandleError(wsConn, err)
}
}
// @Tags Terminal sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
// @Summary List the caller's live terminal sessions if wshandleError(wsConn, err) {
// @Success 200 {array} terminal.Info
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/sessions/search [post]
func (b *BaseApi) SearchTerminalSessions(c *gin.Context) {
identity, ok := loadTerminalIdentity(c)
if !ok {
helper.BadRequest(c, errors.New("missing terminal identity"))
return return
} }
helper.SuccessWithData(c, terminal.List(identity)) defer sws.Close()
}
// @Tags Terminal quitChan := make(chan bool, 3)
// @Summary Close a terminal session sws.Start(quitChan)
// @Accept json go sws.Wait(quitChan)
// @Param request body dto.TerminalSessionClose true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/sessions/close [post]
func (b *BaseApi) CloseTerminalSession(c *gin.Context) {
var req dto.TerminalSessionClose
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
identity, ok := loadTerminalIdentity(c)
if !ok {
helper.BadRequest(c, errors.New("missing terminal identity"))
return
}
if err := terminal.CloseSession(req.ID, identity); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags Terminal <-quitChan
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/sessions/closeAll [post]
func (b *BaseApi) CloseAllTerminalSessions(c *gin.Context) {
identity, ok := loadTerminalIdentity(c)
if !ok {
helper.BadRequest(c, errors.New("missing terminal identity"))
return
}
terminal.Revoke("auth_session", identity.UserID, identity.AuthSessionID)
helper.Success(c)
}
func (b *BaseApi) RevokeTerminalSessions(c *gin.Context) { closeTerminalConn(wsConn)
var req dto.TerminalSessionRevoke
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if (req.Scope == "auth_session" && (req.UserID == "" || req.AuthSessionID == "")) ||
(req.Scope == "user" && req.UserID == "") {
helper.BadRequest(c, errors.New("missing terminal revocation identity"))
return
}
terminal.Revoke(req.Scope, req.UserID, req.AuthSessionID)
helper.Success(c)
}
func loadTerminalIdentity(c *gin.Context) (terminal.Identity, bool) {
identity := terminal.Identity{
UserID: strings.TrimSpace(c.GetHeader(terminal.HeaderUserID)),
AuthSessionID: strings.TrimSpace(c.GetHeader(terminal.HeaderAuthSessionID)),
}
return identity, identity.Valid()
}
// sanitizeTerminalTitle keeps the title a short single line.
func sanitizeTerminalTitle(title string) string {
title = strings.Join(strings.Fields(title), " ")
if r := []rune(title); len(r) > 64 {
title = string(r[:64])
}
return title
} }
func closeTerminalConn(wsConn *websocket.Conn) { func closeTerminalConn(wsConn *websocket.Conn) {
-25
View File
@@ -210,27 +210,6 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Website SSL
// @Summary Push ssl to nodes
// @Accept json
// @Param request body request.WebsiteSSLPush true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/push [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
var req request.WebsiteSSLPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.PushToNode(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website SSL // @Tags Website SSL
// @Summary Upload ssl // @Summary Upload ssl
// @Accept json // @Accept json
@@ -269,8 +248,6 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload var req request.WebsiteSSLFileUpload
req.Description = c.PostForm("description") req.Description = c.PostForm("description")
req.Nodes = c.PostForm("nodes")
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
sslID := c.PostForm("sslID") sslID := c.PostForm("sslID")
if sslID != "" { if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64) req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
@@ -306,8 +283,6 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
Certificate: string(certificateContent), Certificate: string(certificateContent),
Description: req.Description, Description: req.Description,
SSLID: uint(req.SSLID), SSLID: uint(req.SSLID),
PushNode: req.PushNode,
Nodes: req.Nodes,
} }
if err := websiteSSLService.Upload(uploadReq); err != nil { if err := websiteSSLService.Upload(uploadReq); err != nil {
-259
View File
@@ -1,259 +0,0 @@
package v2
import (
"io"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/gin-gonic/gin"
)
// @Tags Website Template
// @Summary Page website templates
// @Accept json
// @Param request body request.WebsiteTemplateSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/search [post]
func (b *BaseApi) PageWebsiteTemplate(c *gin.Context) {
var req request.WebsiteTemplateSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, templates, err := websiteTemplateService.PageTemplate(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: templates,
})
}
// @Tags Website Template
// @Summary Create website template
// @Accept json
// @Param request body request.WebsiteTemplateCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建网站模板 [name]","formatEN":"Create website template [name]"}
func (b *BaseApi) CreateWebsiteTemplate(c *gin.Context) {
var req request.WebsiteTemplateCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.CreateTemplate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Update website template
// @Accept json
// @Param request body request.WebsiteTemplateUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/update [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新网站模板 [name]","formatEN":"Update website template [name]"}
func (b *BaseApi) UpdateWebsiteTemplate(c *gin.Context) {
var req request.WebsiteTemplateUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.UpdateTemplate(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Delete website template
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_templates","output_column":"name","output_value":"name"}],"formatZH":"删除网站模板 [name]","formatEN":"Delete website template [name]"}
func (b *BaseApi) DeleteWebsiteTemplate(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.DeleteTemplate(req.ID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Get website template
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200 {object} response.WebsiteTemplateDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/get [post]
func (b *BaseApi) GetWebsiteTemplate(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
template, err := websiteTemplateService.GetTemplate(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, template)
}
// @Tags Website Template
// @Summary Upload website template zip
// @Accept multipart/form-data
// @Param file formData file true "file"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/upload [post]
func (b *BaseApi) UploadTemplateZip(c *gin.Context) {
fileHeader, err := c.FormFile("file")
if err != nil {
helper.BadRequest(c, err)
return
}
file, err := fileHeader.Open()
if err != nil {
helper.InternalServer(c, err)
return
}
defer file.Close()
content, err := io.ReadAll(file)
if err != nil {
helper.InternalServer(c, err)
return
}
filePath, variables, err := websiteTemplateService.SaveUploadZip(fileHeader.Filename, content)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, gin.H{"filePath": filePath, "variables": variables})
}
// @Tags Website Template
// @Summary Preview website template
// @Accept json
// @Param request body request.WebsitePreviewReq true "request"
// @Success 200 {object} response.WebsitePreviewDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/preview [post]
func (b *BaseApi) PreviewWebsiteTemplate(c *gin.Context) {
var req request.WebsitePreviewReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
preview, err := websiteTemplateService.Preview(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, preview)
}
// @Tags Website Template
// @Summary Page website template outputs
// @Accept json
// @Param request body request.WebsiteTemplateOutputSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs/search [post]
func (b *BaseApi) PageWebsiteTemplateOutput(c *gin.Context) {
var req request.WebsiteTemplateOutputSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, outputs, err := websiteTemplateService.PageOutput(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: outputs,
})
}
// @Tags Website Template
// @Summary Create website template output
// @Accept json
// @Param request body request.WebsiteTemplateOutputCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"生成模板产物 [name]","formatEN":"Generate template output [name]"}
func (b *BaseApi) CreateWebsiteTemplateOutput(c *gin.Context) {
var req request.WebsiteTemplateOutputCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.CreateOutput(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Delete website template output
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_template_outputs","output_column":"name","output_value":"name"}],"formatZH":"删除模板产物 [name]","formatEN":"Delete template output [name]"}
func (b *BaseApi) DeleteWebsiteTemplateOutput(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteTemplateService.DeleteOutput(req.ID); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website Template
// @Summary Get website template output
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200 {object} response.WebsiteTemplateOutputDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/templates/outputs/get [post]
func (b *BaseApi) GetWebsiteTemplateOutput(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
output, err := websiteTemplateService.GetOutput(req.ID)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, output)
}
+70 -143
View File
@@ -3,30 +3,28 @@ package dto
import "time" import "time"
type AgentCreateReq struct { type AgentCreateReq struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Remark string `json:"remark"` Remark string `json:"remark"`
AppVersion string `json:"appVersion" validate:"required"` AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"` WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"` BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"` AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"` TaskID string `json:"taskID"`
DashboardPassword string `json:"dashboardPassword"` Advanced bool `json:"advanced"`
TaskID string `json:"taskID"` ContainerName string `json:"containerName"`
Advanced bool `json:"advanced"` AllowPort bool `json:"allowPort"`
ContainerName string `json:"containerName"` SpecifyIP string `json:"specifyIP"`
AllowPort bool `json:"allowPort"` RestartPolicy string `json:"restartPolicy"`
SpecifyIP string `json:"specifyIP"` CpuQuota float64 `json:"cpuQuota"`
RestartPolicy string `json:"restartPolicy"` MemoryLimit float64 `json:"memoryLimit"`
CpuQuota float64 `json:"cpuQuota"` MemoryUnit string `json:"memoryUnit"`
MemoryLimit float64 `json:"memoryLimit"` PullImage bool `json:"pullImage"`
MemoryUnit string `json:"memoryUnit"` EditCompose bool `json:"editCompose"`
PullImage bool `json:"pullImage"` DockerCompose string `json:"dockerCompose"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
} }
type AgentBatchInstallReq struct { type AgentBatchInstallReq struct {
@@ -40,8 +38,6 @@ type AgentBatchInstallReq struct {
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Advanced bool `json:"advanced"` Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"` ContainerName string `json:"containerName"`
@@ -118,11 +114,11 @@ type AgentItem struct {
ProviderName string `json:"providerName"` ProviderName string `json:"providerName"`
Model string `json:"model"` Model string `json:"model"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
MaxTokens int `json:"maxTokens"`
ContextWindow int `json:"contextWindow"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
AppInstallID uint `json:"appInstallId"` AppInstallID uint `json:"appInstallId"`
@@ -290,22 +286,19 @@ type AgentOverviewSnapshot struct {
} }
type AgentAccountModel struct { type AgentAccountModel struct {
RecordID uint `json:"recordId"` RecordID uint `json:"recordId"`
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
} }
type AgentAccountModelReq struct { type AgentAccountModelReq struct {
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
} }
type AgentAccountModelDiscoverReq struct {
Provider string `json:"provider" validate:"required"`
BaseURL string `json:"baseURL" validate:"required"`
APIKey string `json:"apiKey" validate:"required"`
APIType string `json:"apiType" validate:"required"`
}
type AgentAccountModelCreateReq struct { type AgentAccountModelCreateReq struct {
AccountID uint `json:"accountId" validate:"required"` AccountID uint `json:"accountId" validate:"required"`
Model AgentAccountModel `json:"model" validate:"required"` Model AgentAccountModel `json:"model" validate:"required"`
@@ -322,40 +315,31 @@ type AgentAccountModelDeleteReq struct {
} }
type AgentAccountCreateReq struct { type AgentAccountCreateReq struct {
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
Models []AgentAccountModel `json:"models"` Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType" validate:"required"` APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"` Remark string `json:"remark"`
VerifyModel string `json:"verifyModel"`
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
} }
type AgentAccountUpdateReq struct { type AgentAccountUpdateReq struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"` APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"` Remark string `json:"remark"`
VerifyModel string `json:"verifyModel"` SyncAgents bool `json:"syncAgents"`
ValidateAvailability *bool `json:"validateAvailability"`
Remark string `json:"remark"`
SyncAgents bool `json:"syncAgents"`
} }
type AgentAccountVerifyReq struct { type AgentAccountVerifyReq struct {
Provider string `json:"provider" validate:"required"` Provider string `json:"provider" validate:"required"`
APIKey string `json:"apiKey" validate:"required"` APIKey string `json:"apiKey" validate:"required"`
BaseURL string `json:"baseURL"` BaseURL string `json:"baseURL"`
APIType string `json:"apiType" validate:"required"`
AuthMode string `json:"authMode"`
Model string `json:"model"`
} }
type AgentAccountDeleteReq struct { type AgentAccountDeleteReq struct {
@@ -365,8 +349,6 @@ type AgentAccountDeleteReq struct {
type AgentAccountSearch struct { type AgentAccountSearch struct {
PageInfo PageInfo
Provider string `json:"provider"` Provider string `json:"provider"`
APIType string `json:"apiType"`
TextOnly bool `json:"textOnly"`
Name string `json:"name"` Name string `json:"name"`
} }
@@ -385,36 +367,26 @@ type AgentAccountInfo struct {
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"` Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
} }
type ProviderModelInfo struct { type ProviderModelInfo struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
} ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
type ProviderAPIInfo struct { Reasoning bool `json:"reasoning"`
APIType string `json:"apiType"` Input []string `json:"input"`
BaseURL string `json:"baseUrl"`
EditableBaseURL bool `json:"editableBaseUrl"`
SupportsModelDiscovery bool `json:"supportsModelDiscovery"`
DefaultAuthMode string `json:"defaultAuthMode"`
AuthModes []string `json:"authModes"`
Models []ProviderModelInfo `json:"models"`
} }
type ProviderInfo struct { type ProviderInfo struct {
Sort uint `json:"-"` Sort uint `json:"-"`
Provider string `json:"provider"` Provider string `json:"provider"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
DefaultAPIType string `json:"defaultApiType"` Models []ProviderModelInfo `json:"models"`
APITypes []ProviderAPIInfo `json:"apiTypes"`
Models []ProviderModelInfo `json:"models"`
} }
type AgentFeishuConfigReq struct { type AgentFeishuConfigReq struct {
@@ -435,6 +407,11 @@ type AgentFeishuConfigUpdateReq struct {
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
} }
type AgentFeishuPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentFeishuConfig struct { type AgentFeishuConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"` ThreadSession bool `json:"threadSession"`
@@ -604,52 +581,6 @@ type AgentPluginStatus struct {
Upgradable bool `json:"upgradable"` Upgradable bool `json:"upgradable"`
} }
type AgentPluginsReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentPluginSearchReq struct {
AgentID uint `json:"agentId" validate:"required"`
Keyword string `json:"keyword" validate:"required,max=100"`
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
}
type AgentPluginMarketInstallReq struct {
AgentID uint `json:"agentId" validate:"required"`
Package string `json:"package" validate:"required,max=200"`
Version string `json:"version" validate:"required,max=100"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginOperateReq struct {
AgentID uint `json:"agentId" validate:"required"`
PluginID string `json:"pluginId" validate:"required,max=200"`
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
TaskID string `json:"taskID" validate:"required"`
}
type AgentPluginItem struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
}
type AgentPluginSearchItem struct {
Package string `json:"package"`
PluginID string `json:"pluginId"`
Name string `json:"name"`
Description string `json:"description"`
Version string `json:"version"`
Channel string `json:"channel"`
VerificationTier string `json:"verificationTier"`
Categories []string `json:"categories"`
Official bool `json:"official"`
Downloads int64 `json:"downloads"`
Score float64 `json:"score"`
}
type AgentDiscordConfigUpdateReq struct { type AgentDiscordConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
@@ -729,20 +660,16 @@ type AgentSecurityConfig struct {
} }
type AgentOtherConfigUpdateReq struct { type AgentOtherConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone"` UserTimezone string `json:"userTimezone" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"` NPMRegistry string `json:"npmRegistry" validate:"required"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentOtherConfig struct { type AgentOtherConfig struct {
UserTimezone string `json:"userTimezone"` UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"` NPMRegistry string `json:"npmRegistry"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentConfigFileReq struct { type AgentConfigFileReq struct {
+18 -33
View File
@@ -113,10 +113,8 @@ type DiskDTO struct {
type AlertLogSearch struct { type AlertLogSearch struct {
PageInfo PageInfo
Count uint `json:"count"` Count uint `json:"count"`
Status string `json:"status"` Status string `json:"status"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
} }
type AlertLogDTO struct { type AlertLogDTO struct {
@@ -151,25 +149,16 @@ type AlertLog struct {
} }
type AlertDetail struct { type AlertDetail struct {
LicenseId string `json:"licenseId"` LicenseId string `json:"licenseId"`
Type string `json:"type"` Type string `json:"type"`
SubType string `json:"subType"` SubType string `json:"subType"`
Title string `json:"title"` Title string `json:"title"`
Method string `json:"method"` Method string `json:"method"`
LicenseCode string `json:"licenseCode"` LicenseCode string `json:"licenseCode"`
DeviceId string `json:"deviceId"` DeviceId string `json:"deviceId"`
Project string `json:"project"` Project string `json:"project"`
Params []Param `json:"params"` Params []Param `json:"params"`
Phone string `json:"phone"` Phone string `json:"phone"`
Task *AlertTaskMetadata `json:"task,omitempty"`
}
type AlertTaskMetadata struct {
AlertID uint `json:"alertId"`
Type string `json:"type"`
Quota string `json:"quota"`
QuotaType string `json:"quotaType"`
Method string `json:"method"`
} }
type AlertRule struct { type AlertRule struct {
@@ -304,19 +293,15 @@ type OfflineQueryRequest struct {
} }
type AlertConfigUpdate struct { type AlertConfigUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Status string `json:"status"` Status string `json:"status"`
Config string `json:"config"` Config string `json:"config"`
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
Revision *time.Time `json:"revision"`
} }
type AlertConfigTest struct { type AlertConfigTest struct {
ID uint `json:"id"`
Type string `json:"type"`
Config string `json:"config"`
Host string `json:"host"` Host string `json:"host"`
Port int `json:"port"` Port int `json:"port"`
Sender string `json:"sender"` Sender string `json:"sender"`
-80
View File
@@ -1,80 +0,0 @@
package dto
const AlertCustomWebhookSchemaVersion = 1
type AlertConfigStatusUpdate struct {
ID uint `json:"id" validate:"required"`
Status string `json:"status" validate:"required,oneof=Enable Disable"`
}
type AlertCustomWebhookSecretMutation struct {
Action string `json:"action,omitempty"`
Value string `json:"value,omitempty"`
}
type AlertCustomWebhookURL struct {
AlertCustomWebhookSecretMutation
Configured bool `json:"configured"`
Masked string `json:"masked,omitempty"`
}
type AlertCustomWebhookBody struct {
Type string `json:"type"`
Template string `json:"template,omitempty"`
Fields []AlertCustomWebhookFormField `json:"fields,omitempty"`
}
type AlertCustomWebhookFormField struct {
Key string `json:"key"`
Value string `json:"value"`
}
type AlertCustomWebhookHeader struct {
UID string `json:"uid"`
Key string `json:"key"`
Secret bool `json:"secret"`
Action string `json:"action,omitempty"`
Value string `json:"value,omitempty"`
Configured bool `json:"configured,omitempty"`
Masked string `json:"masked,omitempty"`
}
type AlertCustomWebhookConfig struct {
SchemaVersion int `json:"schemaVersion"`
State string `json:"state,omitempty"`
DisplayName string `json:"displayName"`
Preset string `json:"preset"`
Method string `json:"method"`
URL AlertCustomWebhookURL `json:"url"`
Body AlertCustomWebhookBody `json:"body"`
Headers []AlertCustomWebhookHeader `json:"headers"`
}
type AlertCustomWebhookSecretConfig struct {
SchemaVersion int `json:"schemaVersion"`
URL string `json:"url"`
Headers map[string]string `json:"headers,omitempty"`
}
type AlertCustomWebhookResolvedConfig struct {
SchemaVersion int
DisplayName string
Preset string
Method string
URL string
Body AlertCustomWebhookBody
Headers []AlertCustomWebhookResolvedHeader
}
type AlertCustomWebhookResolvedHeader struct {
Key string
Value string
}
type AlertConfigTestResult struct {
Success bool `json:"success"`
StatusCode int `json:"statusCode,omitempty"`
Duration int64 `json:"duration,omitempty"` // milliseconds
Message string `json:"message,omitempty"`
Response string `json:"response,omitempty"`
}
-3
View File
@@ -2,7 +2,6 @@ package dto
import ( import (
"context" "context"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/app/task"
) )
@@ -133,8 +132,6 @@ type Locale struct {
Ko string `json:"ko"` Ko string `json:"ko"`
Tr string `json:"tr"` Tr string `json:"tr"`
Es string `json:"es-es" yaml:"es-es"` Es string `json:"es-es" yaml:"es-es"`
Fa string `json:"fa"`
Lo string `json:"lo"`
} }
type AppForm struct { type AppForm struct {
+8 -9
View File
@@ -65,15 +65,14 @@ type UploadForRecover struct {
} }
type CommonBackup struct { type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"` Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"` Name string `json:"name"`
DetailName string `json:"detailName"` DetailName string `json:"detailName"`
Secret string `json:"secret"` Secret string `json:"secret"`
IsImmediate bool `json:"isImmediate"` StopBefore bool `json:"stopBefore"`
StopBefore bool `json:"stopBefore"` TaskID string `json:"taskID"`
TaskID string `json:"taskID"` FileName string `json:"fileName"`
FileName string `json:"fileName"` Args []string `json:"args"`
Args []string `json:"args"`
Description string `json:"description"` Description string `json:"description"`
} }
+1 -2
View File
@@ -2,8 +2,7 @@ package dto
type SearchWithPage struct { type SearchWithPage struct {
PageInfo PageInfo
Info string `json:"info"` Info string `json:"info"`
ExcludeAppStore bool `json:"excludeAppStore"`
} }
type SearchPageWithType struct { type SearchPageWithType struct {
+3 -4
View File
@@ -6,10 +6,9 @@ type PageResult struct {
} }
type Response struct { type Response struct {
Code int `json:"code"` Code int `json:"code"`
ErrorCode string `json:"errorCode,omitempty"` Message string `json:"message"`
Message string `json:"message"` Data interface{} `json:"data"`
Data interface{} `json:"data"`
} }
type Options struct { type Options struct {
+5 -16
View File
@@ -134,15 +134,10 @@ type ExtraHost struct {
IP string `json:"ip"` IP string `json:"ip"`
} }
type ContainerNetwork struct { type ContainerNetwork struct {
Network string `json:"network"` Network string `json:"network"`
Ipv4 string `json:"ipv4"` Ipv4 string `json:"ipv4"`
Ipv6 string `json:"ipv6"` Ipv6 string `json:"ipv6"`
MacAddr string `json:"macAddr"` MacAddr string `json:"macAddr"`
Links []string `json:"links"`
Aliases []string `json:"aliases"`
DriverOpts map[string]string `json:"driverOpts"`
GwPriority int `json:"gwPriority"`
LinkLocalIPs []string `json:"linkLocalIPs"`
} }
type ContainerCreateByCommand struct { type ContainerCreateByCommand struct {
@@ -300,7 +295,6 @@ type ComposeInfo struct {
ConfigFile string `json:"configFile"` ConfigFile string `json:"configFile"`
Workdir string `json:"workdir"` Workdir string `json:"workdir"`
ComposeFileExists bool `json:"composeFileExists"` ComposeFileExists bool `json:"composeFileExists"`
IsPinned bool `json:"isPinned"`
Path string `json:"path"` Path string `json:"path"`
Containers []ComposeContainer `json:"containers"` Containers []ComposeContainer `json:"containers"`
Env string `json:"env"` Env string `json:"env"`
@@ -315,7 +309,6 @@ type ComposeContainer struct {
type ComposeCreate struct { type ComposeCreate struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
Name string `json:"name"` Name string `json:"name"`
DirName string `json:"dirName"`
From string `json:"from" validate:"required,oneof=edit path template"` From string `json:"from" validate:"required,oneof=edit path template"`
File string `json:"file"` File string `json:"file"`
Path string `json:"path"` Path string `json:"path"`
@@ -326,7 +319,7 @@ type ComposeCreate struct {
type ComposeOperation struct { type ComposeOperation struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Path string `json:"path"` Path string `json:"path"`
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete rebuild"` Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"`
WithFile bool `json:"withFile"` WithFile bool `json:"withFile"`
Force bool `json:"force"` Force bool `json:"force"`
} }
@@ -339,10 +332,6 @@ type ComposeUpdate struct {
Env string `json:"env"` Env string `json:"env"`
ForcePull bool `json:"forcePull"` ForcePull bool `json:"forcePull"`
} }
type ComposePin struct {
Name string `json:"name" validate:"required"`
IsPinned bool `json:"isPinned"`
}
type ComposeLogClean struct { type ComposeLogClean struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
-1
View File
@@ -197,7 +197,6 @@ type SearchRecord struct {
type Record struct { type Record struct {
ID uint `json:"id"` ID uint `json:"id"`
CronjobID uint `json:"cronjobID"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
StartTime string `json:"startTime"` StartTime string `json:"startTime"`
Records string `json:"records"` Records string `json:"records"`
+9 -45
View File
@@ -79,9 +79,8 @@ type NodeCurrent struct {
} }
type DashboardCurrent struct { type DashboardCurrent struct {
Uptime uint64 `json:"uptime"` Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"` TimeSinceUptime string `json:"timeSinceUptime"`
RunningTime RunningTime `json:"runningTime"`
Procs uint64 `json:"procs"` Procs uint64 `json:"procs"`
@@ -121,7 +120,6 @@ type DashboardCurrent struct {
NetBytesRecv uint64 `json:"netBytesRecv"` NetBytesRecv uint64 `json:"netBytesRecv"`
GPUData []GPUInfo `json:"gpuData"` GPUData []GPUInfo `json:"gpuData"`
NPUData []NPUInfo `json:"npuData"`
XPUData []XPUInfo `json:"xpuData"` XPUData []XPUInfo `json:"xpuData"`
TopCPUItems []Process `json:"topCPUItems"` TopCPUItems []Process `json:"topCPUItems"`
@@ -130,13 +128,6 @@ type DashboardCurrent struct {
ShotTime time.Time `json:"shotTime"` ShotTime time.Time `json:"shotTime"`
} }
type RunningTime struct {
Days uint64 `json:"days"`
Hours uint64 `json:"hours"`
Minutes uint64 `json:"minutes"`
Seconds uint64 `json:"seconds"`
}
type AppLauncherSync struct { type AppLauncherSync struct {
Keys []string `json:"keys"` Keys []string `json:"keys"`
} }
@@ -157,12 +148,8 @@ type DiskInfo struct {
} }
type GPUInfo struct { type GPUInfo struct {
Type string `json:"type"`
Index uint `json:"index"` Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
ChipIndex uint `json:"chipIndex"`
ProductName string `json:"productName"` ProductName string `json:"productName"`
BusID string `json:"busID"`
GPUUtil string `json:"gpuUtil"` GPUUtil string `json:"gpuUtil"`
Temperature string `json:"temperature"` Temperature string `json:"temperature"`
PerformanceState string `json:"performanceState"` PerformanceState string `json:"performanceState"`
@@ -175,27 +162,6 @@ type GPUInfo struct {
FanSpeed string `json:"fanSpeed"` FanSpeed string `json:"fanSpeed"`
} }
type NPUInfo struct {
Type string `json:"type"`
Index uint `json:"index"`
NPUIndex uint `json:"npuIndex"`
ChipIndex uint `json:"chipIndex"`
ProductName string `json:"productName"`
BusID string `json:"busID"`
Health string `json:"health"`
Temperature string `json:"temperature"`
PowerDraw string `json:"powerDraw"`
AICore string `json:"aiCore"`
MemUsed string `json:"memUsed"`
MemTotal string `json:"memTotal"`
MemoryUsed string `json:"memoryUsed"`
MemoryTotal string `json:"memoryTotal"`
HBMUsed string `json:"hbmUsed"`
HBMTotal string `json:"hbmTotal"`
HugepagesUsed string `json:"hugepagesUsed"`
HugepagesTotal string `json:"hugepagesTotal"`
}
type AppLauncher struct { type AppLauncher struct {
Key string `json:"key"` Key string `json:"key"`
Type string `json:"type"` Type string `json:"type"`
@@ -228,13 +194,11 @@ type LauncherOption struct {
} }
type XPUInfo struct { type XPUInfo struct {
DeviceID int `json:"deviceID"` DeviceID int `json:"deviceID"`
DeviceName string `json:"deviceName"` DeviceName string `json:"deviceName"`
PciBdfAddress string `json:"pciBdfAddress"` Memory string `json:"memory"`
Memory string `json:"memory"` Temperature string `json:"temperature"`
Temperature string `json:"temperature"` MemoryUsed string `json:"memoryUsed"`
GPUUtil string `json:"gpuUtil"` Power string `json:"power"`
MemoryUsed string `json:"memoryUsed"` MemoryUtil string `json:"memoryUtil"`
Power string `json:"power"`
MemoryUtil string `json:"memoryUtil"`
} }
+10 -69
View File
@@ -60,84 +60,25 @@ type MysqlDBCreate struct {
Database string `json:"database" validate:"required"` Database string `json:"database" validate:"required"`
Format string `json:"format" validate:"required"` Format string `json:"format" validate:"required"`
Collation string `json:"collation"` Collation string `json:"collation"`
Username string `json:"username"` Username string `json:"username" validate:"required"`
Password string `json:"password"` Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"` Permission string `json:"permission" validate:"required"`
Description string `json:"description"` Description string `json:"description"`
} }
type MysqlUser struct {
Username string `json:"username"`
Host string `json:"host"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type MysqlGrant struct {
Database string `json:"database"`
Username string `json:"username"`
Host string `json:"host"`
}
type MysqlGrantSummarySearch struct {
Database string `json:"database" validate:"required"`
DBs []string `json:"dbs" validate:"required"`
}
type MysqlUserSearch struct {
Database string `json:"database" validate:"required"`
}
type MysqlUserCreate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Host string `json:"host" validate:"required"`
Description string `json:"description"`
DBs []string `json:"dbs"`
}
type MysqlUserDelete struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlUserUpdate struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
NewHost string `json:"newHost" validate:"required"`
Description string `json:"description"`
}
type MysqlUserPassword struct {
Database string `json:"database" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
Password string `json:"password" validate:"required"`
}
type MysqlGrantCreate struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlGrantDelete struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Host string `json:"host" validate:"required"`
}
type MysqlFormatCollationOption struct { type MysqlFormatCollationOption struct {
Format string `json:"format"` Format string `json:"format"`
Collations []string `json:"collations"` Collations []string `json:"collations"`
} }
type BindUser struct {
Database string `json:"database" validate:"required"`
DB string `json:"db" validate:"required"`
Username string `json:"username" validate:"required"`
Password string `json:"password" validate:"required"`
Permission string `json:"permission" validate:"required"`
}
type MysqlLoadDB struct { type MysqlLoadDB struct {
From string `json:"from" validate:"required,oneof=local remote"` From string `json:"from" validate:"required,oneof=local remote"`
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"` Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
+84 -333
View File
@@ -1,362 +1,113 @@
package dto package dto
import ( type FirewallBaseInfo struct {
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter" Name string `json:"name"`
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync" IsExist bool `json:"isExist"`
) IsActive bool `json:"isActive"`
IsInit bool `json:"isInit"`
type FirewallSubsystemStatus struct { IsBind bool `json:"isBind"`
Name string `json:"name"` Version string `json:"version"`
Backend string `json:"backend"` PingStatus string `json:"pingStatus"`
ConflictBackend string `json:"conflictBackend,omitempty"`
IsExist bool `json:"isExist"`
IsActive bool `json:"isActive"`
IsInit bool `json:"isInit"`
IsBind bool `json:"isBind"`
Version string `json:"version"`
PingStatus string `json:"pingStatus"`
Message string `json:"message,omitempty"`
Reason string `json:"reason,omitempty"`
SyncError string `json:"syncError,omitempty"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
} }
type FirewallLifecycleOperation struct { type RuleSearch struct {
PageInfo
Info string `json:"info"`
Status string `json:"status"`
Strategy string `json:"strategy"`
Type string `json:"type" validate:"required"`
}
type FirewallOperation struct {
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"` Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
WithDockerRestart bool `json:"withDockerRestart"` WithDockerRestart bool `json:"withDockerRestart"`
} }
type FirewallBackendOption struct { type PortRuleOperate struct {
Name string `json:"name"` ID uint `json:"id"`
Installed bool `json:"installed"` Operation string `json:"operation" validate:"required,oneof=add remove"`
Active bool `json:"active"` Chain string `json:"chain"`
Initialized bool `json:"initialized"` Address string `json:"address"`
Bound bool `json:"bound"` Port string `json:"port" validate:"required"`
Supported bool `json:"supported"` Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
SupportReason string `json:"supportReason,omitempty"` Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
Implementation string `json:"implementation,omitempty"`
Message string `json:"message,omitempty"` Description string `json:"description"`
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
} }
type FirewallBackendFamilyStatus struct { type ForwardRuleOperate struct {
Available bool `json:"available"` ForceDelete bool `json:"forceDelete"`
Initialized bool `json:"initialized"` Rules []struct {
Bound bool `json:"bound"` Operation string `json:"operation" validate:"required,oneof=add remove"`
Reason string `json:"reason,omitempty"` Num string `json:"num"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
} `json:"rules"`
} }
type FirewallBackendGroup struct { type UpdateFirewallDescription struct {
Selected string `json:"selected"` Type string `json:"type"`
Current string `json:"current,omitempty"` Chain string `json:"chain"`
Options []FirewallBackendOption `json:"options"` SrcIP string `json:"srcIP"`
DstIP string `json:"dstIP"`
SrcPort string `json:"srcPort"`
DstPort string `json:"dstPort"`
Protocol string `json:"protocol"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
Description string `json:"description"`
} }
type FirewallSettings struct { type AddrRuleOperate struct {
System FirewallBackendGroup `json:"system"` ID uint `json:"id"`
Forwarding FirewallBackendGroup `json:"forwarding"` Operation string `json:"operation" validate:"required,oneof=add remove"`
Docker FirewallBackendGroup `json:"docker"` Address string `json:"address" validate:"required"`
PingStatus string `json:"pingStatus"` Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
PortWhitelist string `json:"portWhiteList"`
Description string `json:"description"`
} }
type FirewallPortWhitelistUpdate struct { type PortRuleUpdate struct {
Value string `json:"value" validate:"required"` OldRule PortRuleOperate `json:"oldRule"`
NewRule PortRuleOperate `json:"newRule"`
} }
type FirewallBackendOperation struct { type AddrRuleUpdate struct {
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"` OldRule AddrRuleOperate `json:"oldRule"`
Backend string `json:"backend" validate:"required,oneof=firewalld ufw iptables nftables"` NewRule AddrRuleOperate `json:"newRule"`
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
} }
type FilterChainOperation struct { type BatchRuleOperate struct {
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"` Type string `json:"type" validate:"required"`
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"` Rules []PortRuleOperate `json:"rules"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
} }
type FilterChainOperationResponse struct { type IptablesOp struct {
TaskID string `json:"taskID"` Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
Queued bool `json:"queued"` Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
} }
type FirewallInitializationTask struct { type IptablesRuleOp struct {
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"` Operation string `json:"operation" validate:"required,oneof=add remove"`
ID uint `json:"id"`
Chain string `json:"chain" validate:"required,oneof=1PANEL_BASIC 1PANEL_BASIC_BEFORE 1PANEL_INPUT 1PANEL_OUTPUT"`
Protocol string `json:"protocol"`
SrcIP string `json:"srcIP"`
SrcPort uint `json:"srcPort"`
DstIP string `json:"dstIP"`
DstPort uint `json:"dstPort"`
Strategy string `json:"strategy" validate:"required,oneof=accept drop reject"`
Description string `json:"description"`
} }
type FirewallSystemPort struct { type IptablesBatchOperate struct {
Family string Rules []IptablesRuleOp `json:"rules"`
Port string
Protocol string
} }
type FirewallRuleInventoryResponse struct { type IptablesChainStatus struct {
IPv4Range filter.PositionRange `json:"ipv4Range"` IsBind bool `json:"isBind"`
IPv6Range filter.PositionRange `json:"ipv6Range"` DefaultStrategy string `json:"defaultStrategy"`
Total int64 `json:"total"`
AllTotal int64 `json:"allTotal"`
ManagedTotal int64 `json:"managedTotal"`
Items []filter.InventoryItem `json:"items"`
Notices []filter.ScopeNotice `json:"notices,omitempty"`
}
type FirewallRuleResetResponse struct {
Removed int `json:"removed"`
Disabled bool `json:"disabled"`
}
type FirewallRuleReset struct {
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
WithDockerRestart bool `json:"withDockerRestart"`
}
type FirewallRuleInventory struct {
PageInfo
Scope filter.Scope `json:"scope,omitempty"`
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
All bool `json:"all,omitempty"`
Info string `json:"info"`
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
}
type FirewallNativeDetail struct {
Provider filter.Provider `json:"provider" validate:"required,oneof=firewalld ufw"`
NativeKind filter.NativeKind `json:"nativeKind" validate:"required,oneof=zone_service ufw_application"`
Name string `json:"name" validate:"required"`
Permanent bool `json:"permanent"`
}
type DockerPortGuardBase struct {
Name string `json:"name"`
Version string `json:"version"`
IsExist bool `json:"isExist"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
IPv4 DockerPortGuardFamilyStatus `json:"ipv4"`
IPv6 DockerPortGuardFamilyStatus `json:"ipv6"`
Backend string `json:"backend"`
Message string `json:"message,omitempty"`
}
type DockerPortGuardFamilyStatus struct {
State string `json:"state"`
Reason string `json:"reason,omitempty"`
Initialized bool `json:"initialized"`
Bound bool `json:"bound"`
Effective bool `json:"effective"`
}
type DockerPortGuardEndpoint struct {
Family string `json:"family"`
HostIP string `json:"hostIP"`
HostPort uint16 `json:"hostPort"`
Protocol string `json:"protocol"`
ContainerID string `json:"containerID"`
ContainerName string `json:"containerName"`
ContainerState string `json:"containerState,omitempty"`
ContainerPort uint16 `json:"containerPort"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
PolicyUUID string `json:"policyUUID,omitempty"`
Mode string `json:"mode,omitempty"`
NativeAction string `json:"nativeAction,omitempty"`
ReadOnly bool `json:"readOnly,omitempty"`
Sources []string `json:"sources"`
Effective bool `json:"effective"`
Description string `json:"description,omitempty"`
TrafficPath string `json:"trafficPath"`
ManagementTarget string `json:"managementTarget"`
ManagementReason string `json:"managementReason,omitempty"`
}
type DockerPortGuardPortGroup struct {
Key string `json:"key"`
Label string `json:"label"`
Endpoint DockerPortGuardEndpoint `json:"endpoint"`
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
}
type DockerPortGuardContainer struct {
Key string `json:"key"`
Name string `json:"name"`
Compose string `json:"compose,omitempty"`
Application string `json:"application,omitempty"`
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
PortGroups []DockerPortGuardPortGroup `json:"portGroups"`
}
type DockerPortGuardList struct {
Base DockerPortGuardBase `json:"base"`
Containers []DockerPortGuardContainer `json:"containers"`
OrphanPolicies []DockerPortGuardEndpoint `json:"orphanPolicies"`
}
type DockerPortGuardEndpointIdentity struct {
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
HostIP string `json:"hostIP" validate:"required,max=45"`
HostPort uint16 `json:"hostPort" validate:"required,min=1"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp"`
}
type DockerPortGuardPolicyBatch struct {
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
}
type DockerPortGuardPolicyBatchDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
}
type DockerPortGuardPolicy struct {
DockerPortGuardEndpointIdentity
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
Sources []string `json:"sources" validate:"dive,required,max=64"`
Description string `json:"description" validate:"max=256"`
}
type DockerPortGuardOperation struct {
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleAdopt struct {
Scope filter.Scope `json:"scope" validate:"required"`
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
}
type FirewallRuleCreateItem struct {
Rule filter.FirewallRule `json:"rule" validate:"required"`
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
SourceID string `json:"sourceID"`
}
type FirewallRuleCreate struct {
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
}
type FirewallRuleCreateResponse struct {
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Skipped int `json:"skipped"`
Errors []FirewallRuleCreateFailure `json:"errors,omitempty"`
}
type FirewallRuleCreateFailure struct {
Index int `json:"index"`
Status string `json:"status"`
Rule filter.FirewallRule `json:"rule"`
Error string `json:"error,omitempty"`
}
type FirewallRuleSyncRequest struct {
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
ResetSource bool `json:"resetSource"`
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
}
type FirewallRuleSyncItem struct {
SourceUUID string `json:"sourceUUID"`
Rule *filter.FirewallRule `json:"rule,omitempty"`
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
Status firewallsync.Status `json:"status"`
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
Reason string `json:"reason,omitempty"`
}
type FirewallRuleSyncPreview struct {
Subsystem string `json:"subsystem"`
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
TargetProvider filter.Provider `json:"targetProvider"`
Total int `json:"total"`
Ready int `json:"ready"`
Existing int `json:"existing"`
Removed int `json:"removed"`
Blocked int `json:"blocked"`
Items []FirewallRuleSyncItem `json:"items"`
}
type FirewallRuleSyncResult struct {
Subsystem string `json:"subsystem"`
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
TargetProvider filter.Provider `json:"targetProvider"`
Total int `json:"total"`
Succeeded int `json:"succeeded"`
Skipped int `json:"skipped"`
Removed int `json:"removed"`
Failed int `json:"failed"`
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
TaskID string `json:"taskID,omitempty"`
Queued bool `json:"queued,omitempty"`
}
type FirewallRuleSyncTask struct {
TaskID string `json:"taskID,omitempty"`
Executing bool `json:"executing"`
}
type FirewallRuleSyncFailure struct {
SourceUUID string `json:"sourceUUID"`
Rule *filter.FirewallRule `json:"rule,omitempty"`
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
Error string `json:"error"`
}
type FirewallRuleDelete struct {
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
}
type FirewallRuleDeleteResponse struct {
Succeeded int `json:"succeeded"`
Failed int `json:"failed"`
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
}
type FirewallRuleDeleteFailure struct {
Index int `json:"index"`
UUID string `json:"uuid"`
Error string `json:"error"`
}
type FirewallRuleUpdate struct {
UUID string `json:"uuid" validate:"required,max=64"`
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
Priority *int `json:"priority,omitempty" validate:"excluded_with=Rule OrderIndex"`
}
type FirewallRuleReorder struct {
UUID string `json:"uuid" validate:"required,max=64"`
TargetPosition *int64 `json:"targetPosition"`
Priority *int `json:"priority"`
}
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
p.Items = append(p.Items, item)
switch item.Status {
case firewallsync.StatusReady:
p.Ready++
p.Total++
case firewallsync.StatusExisting:
p.Existing++
p.Total++
case firewallsync.StatusRemove:
p.Removed++
case firewallsync.StatusBlocked:
p.Blocked++
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
p.Total++
}
}
} }
-47
View File
@@ -1,47 +0,0 @@
package dto
type ForwardRuleSearch struct {
PageInfo
All bool `json:"all,omitempty"`
Info string `json:"info"`
Status string `json:"status"`
Strategy string `json:"strategy"`
}
type ForwardRule struct {
ID uint `json:"id"`
Chain string `json:"chain"`
Family string `json:"family"`
Address string `json:"address"`
Port string `json:"port"`
Protocol string `json:"protocol"`
Strategy string `json:"strategy"`
Num string `json:"num"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort"`
Interface string `json:"interface"`
UsedStatus string `json:"usedStatus"`
Description string `json:"description"`
IsDesired bool `json:"isDesired"`
IsRuntime bool `json:"isRuntime"`
SyncStatus string `json:"syncStatus"`
}
type ForwardRuleOperate struct {
ForceDelete bool `json:"forceDelete"`
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
}
type ForwardRuleOperation struct {
Operation string `json:"operation" validate:"required,oneof=add remove"`
Num string `json:"num"`
Family string `json:"family" validate:"omitempty,oneof=ipv4 ipv6"`
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
Interface string `json:"interface"`
Port string `json:"port" validate:"required"`
TargetIP string `json:"targetIP"`
TargetPort string `json:"targetPort" validate:"required"`
}
-36
View File
@@ -1,8 +1,6 @@
package dto package dto
import ( import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
) )
@@ -16,37 +14,3 @@ type SearchTaskLogReq struct {
type TaskDTO struct { type TaskDTO struct {
model.Task model.Task
} }
type SystemLogReq struct {
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=500"`
Cursor string `json:"cursor"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
Keyword string `json:"keyword"`
Priority string `json:"priority"`
Service string `json:"service"`
}
type SystemLogRes struct {
Source string `json:"source"`
Items []SystemLogItem `json:"items"`
HasMore bool `json:"hasMore"`
NextCursor string `json:"nextCursor"`
}
type SystemLogStatus struct {
Source string `json:"source"`
Version string `json:"version"`
KeywordFilterSupported bool `json:"keywordFilterSupported"`
Message string `json:"message"`
}
type SystemLogItem struct {
Timestamp int64 `json:"-"`
Cursor string `json:"-"`
Time string `json:"time"`
Priority string `json:"priority"`
Service string `json:"service"`
Message string `json:"message"`
Raw string `json:"raw"`
}
-2
View File
@@ -45,12 +45,10 @@ type MonitorGPUOptions struct {
} }
type GPUChartHide struct { type GPUChartHide struct {
ProductName string `json:"productName"` ProductName string `json:"productName"`
Type string `json:"type"`
Process bool `json:"process"` Process bool `json:"process"`
GPU bool `json:"gpu"` GPU bool `json:"gpu"`
Memory bool `json:"memory"` Memory bool `json:"memory"`
Power bool `json:"power"` Power bool `json:"power"`
PowerLimit bool `json:"powerLimit"`
Temperature bool `json:"temperature"` Temperature bool `json:"temperature"`
Speed bool `json:"speed"` Speed bool `json:"speed"`
} }
+6 -46
View File
@@ -1,8 +1,6 @@
package dto package dto
import ( import (
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components" "github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
) )
@@ -51,19 +49,13 @@ const (
CACHE NginxKey = "cache" CACHE NginxKey = "cache"
HttpPer NginxKey = "http-per" HttpPer NginxKey = "http-per"
ProxyCache NginxKey = "proxy-cache" ProxyCache NginxKey = "proxy-cache"
Brotli NginxKey = "brotli"
) )
// BrotliKeys are served from the panel-managed http.d file rather than
// nginx.conf, because the module is optional: its directives must disappear
// together with the module, otherwise nginx refuses to start.
var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}
var ScopeKeyMap = map[NginxKey][]string{ var ScopeKeyMap = map[NginxKey][]string{
Index: {"index"}, Index: {"index"},
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"}, LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
SSL: {"ssl_certificate", "ssl_certificate_key"}, SSL: {"ssl_certificate", "ssl_certificate_key"},
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level", "gzip_types", "gzip_vary", "gzip_proxied"}, HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
} }
var StaticFileKeyMap = map[NginxKey]struct { var StaticFileKeyMap = map[NginxKey]struct {
@@ -95,41 +87,9 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}} var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
type NginxModule struct { type NginxModule struct {
Name string `json:"name"` Name string `json:"name"`
Custom bool `json:"custom,omitempty"` Script string `json:"script"`
Script string `json:"script"` Packages []string `json:"packages"`
Packages []string `json:"packages"` Params string `json:"params"`
Params string `json:"params"` Enable bool `json:"enable"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode,omitempty"`
Provider string `json:"provider,omitempty"`
LoadOrder int `json:"loadOrder,omitempty"`
Builds []NginxModuleBuild `json:"builds,omitempty"`
LastError string `json:"lastError,omitempty"`
}
type NginxModuleBuild struct {
Provider string `json:"provider"`
BuildMode string `json:"buildMode"`
Status string `json:"status"`
Hash string `json:"hash"`
Target NginxModuleTarget `json:"target"`
Artifacts []NginxModuleArtifact `json:"artifacts,omitempty"`
Error string `json:"error,omitempty"`
BuiltAt time.Time `json:"builtAt,omitempty"`
}
type NginxModuleTarget struct {
Key string `json:"key"`
OpenRestyVersion string `json:"openrestyVersion"`
Architecture string `json:"architecture"`
Image string `json:"image,omitempty"`
ImageDigest string `json:"imageDigest,omitempty"`
BuilderDigest string `json:"builderDigest,omitempty"`
}
type NginxModuleArtifact struct {
Name string `json:"name"`
Path string `json:"path"`
Checksum string `json:"checksum"`
} }
-10
View File
@@ -50,10 +50,6 @@ type AppContainerConfig struct {
Type string `json:"type"` Type string `json:"type"`
SpecifyIP string `json:"specifyIP"` SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"` RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
KeepServiceName bool `json:"-"`
SkipComposeCommonConfig bool `json:"-"`
UseLifecycleScripts bool `json:"-"`
} }
type AppInstalledSearch struct { type AppInstalledSearch struct {
@@ -96,8 +92,6 @@ type AppInstalledOperate struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
DeleteImage bool `json:"deleteImage"` DeleteImage bool `json:"deleteImage"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
UseLifecycleScripts bool `json:"-"`
} }
type AppInstallUpgrade struct { type AppInstallUpgrade struct {
@@ -105,7 +99,6 @@ type AppInstallUpgrade struct {
DetailID uint `json:"detailId"` DetailID uint `json:"detailId"`
Backup bool `json:"backup"` Backup bool `json:"backup"`
PullImage bool `json:"pullImage"` PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
} }
@@ -117,14 +110,11 @@ type AppInstallDelete struct {
DeleteDB bool `json:"deleteDB"` DeleteDB bool `json:"deleteDB"`
DeleteImage bool `json:"deleteImage"` DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
UseLifecycleScripts bool `json:"-"`
} }
type AppInstalledUpdate struct { type AppInstalledUpdate struct {
InstallId uint `json:"installId" validate:"required"` InstallId uint `json:"installId" validate:"required"`
Params map[string]interface{} `json:"params" validate:"required"` Params map[string]interface{} `json:"params" validate:"required"`
TaskID string `json:"-"`
AppContainerConfig AppContainerConfig
} }
-9
View File
@@ -131,11 +131,6 @@ type FileMove struct {
Name string `json:"name"` Name string `json:"name"`
Cover bool `json:"cover"` Cover bool `json:"cover"`
CoverPaths []string `json:"coverPaths"` CoverPaths []string `json:"coverPaths"`
TaskID string `json:"taskID"`
}
type FileMoveStopReq struct {
TaskID string `json:"taskID" validate:"required"`
} }
type FileDownload struct { type FileDownload struct {
@@ -158,10 +153,6 @@ type FileProcessReq struct {
Key string `json:"key"` Key string `json:"key"`
} }
type FileProcessRemoveReq struct {
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
}
type FileRoleUpdate struct { type FileRoleUpdate struct {
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
User string `json:"user" validate:"required"` User string `json:"user" validate:"required"`
-16
View File
@@ -21,10 +21,6 @@ type McpServerCreate struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"` OutputTransport string `json:"outputTransport" validate:"required"`
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs" validate:"max=4096"`
TaskID string `json:"taskID"`
} }
type McpServerUpdate struct { type McpServerUpdate struct {
@@ -36,23 +32,11 @@ type McpServerDelete struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
} }
type McpServerDetail struct {
ID uint `json:"id" validate:"required"`
}
type McpServerStatusSync struct {
IDs []uint `json:"ids"`
}
type McpServerOperate struct { type McpServerOperate struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` Operate string `json:"operate" validate:"required"`
} }
type McpServerConnectionTest struct {
ID uint `json:"id" validate:"required"`
}
type McpBindDomain struct { type McpBindDomain struct {
Domain string `json:"domain" validate:"required"` Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
+8 -13
View File
@@ -114,22 +114,17 @@ type NginxRedirectUpdate struct {
} }
type NginxBuildReq struct { type NginxBuildReq struct {
TaskID string `json:"taskID" validate:"required"` TaskID string `json:"taskID" validate:"required"`
Mirror string `json:"mirror" validate:"required"` Mirror string `json:"mirror" validate:"required"`
Modules []string `json:"modules"`
Force bool `json:"force"`
} }
type NginxModuleUpdate struct { type NginxModuleUpdate struct {
Operate string `json:"operate" validate:"required,oneof=create delete update"` Operate string `json:"operate" validate:"required,oneof=create delete update"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Script string `json:"script"` Script string `json:"script"`
Packages string `json:"packages"` Packages string `json:"packages"`
Enable bool `json:"enable"` Enable bool `json:"enable"`
Params string `json:"params"` Params string `json:"params"`
BuildMode string `json:"buildMode" validate:"omitempty,oneof=dynamic static"`
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
} }
type NginxOperateReq struct { type NginxOperateReq struct {
+11 -17
View File
@@ -21,14 +21,13 @@ type RuntimeCreate struct {
Source string `json:"source"` Source string `json:"source"`
CodeDir string `json:"codeDir"` CodeDir string `json:"codeDir"`
Remark string `json:"remark"` Remark string `json:"remark"`
TaskID string `json:"taskID"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
NodeConfig NodeConfig
} }
type NodeConfig struct { type NodeConfig struct {
Install *bool `json:"install"` Install bool `json:"install"`
Clean bool `json:"clean"` Clean bool `json:"clean"`
ExposedPorts []ExposedPort `json:"exposedPorts"` ExposedPorts []ExposedPort `json:"exposedPorts"`
Environments []Environment `json:"environments"` Environments []Environment `json:"environments"`
@@ -43,14 +42,12 @@ type Environment struct {
type Volume struct { type Volume struct {
Source string `json:"source"` Source string `json:"source"`
Target string `json:"target"` Target string `json:"target"`
Mode string `json:"mode"`
} }
type ExposedPort struct { type ExposedPort struct {
HostPort int `json:"hostPort"` HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"` ContainerPort int `json:"containerPort"`
HostIP string `json:"hostIP"` HostIP string `json:"hostIP"`
Protocol string `json:"protocol"`
} }
type ExtraHost struct { type ExtraHost struct {
@@ -59,22 +56,19 @@ type ExtraHost struct {
} }
type RuntimeDelete struct { type RuntimeDelete struct {
ID uint `json:"id"` ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"` ForceDelete bool `json:"forceDelete"`
DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"`
} }
type RuntimeUpdate struct { type RuntimeUpdate struct {
AppDetailID uint `json:"appDetailId"` Name string `json:"name"`
Name string `json:"name"` ID uint `json:"id"`
ID uint `json:"id"` Image string `json:"image"`
Image string `json:"image"` Version string `json:"version"`
Version string `json:"version"` Rebuild bool `json:"rebuild"`
Rebuild bool `json:"rebuild"` Source string `json:"source"`
Source string `json:"source"` CodeDir string `json:"codeDir"`
CodeDir string `json:"codeDir"` Remark string `json:"remark"`
Remark string `json:"remark"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
NodeConfig NodeConfig
-2
View File
@@ -34,8 +34,6 @@ type WebsiteCreate struct {
SiteDir string `json:"siteDir"` SiteDir string `json:"siteDir"`
TemplateOutputID uint `json:"templateOutputID"`
RuntimeConfig RuntimeConfig
FtpConfig FtpConfig
DataBaseConfig DataBaseConfig
-14
View File
@@ -126,16 +126,6 @@ type WebsiteSSLUpload struct {
Type string `json:"type" validate:"required,oneof=paste local"` Type string `json:"type" validate:"required,oneof=paste local"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
Description string `json:"description"` Description string `json:"description"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteSSLPush struct {
ID uint `json:"id" validate:"required"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
TaskID string `json:"taskID" validate:"required"`
Sync bool `json:"sync"`
} }
type WebsiteCASearch struct { type WebsiteCASearch struct {
@@ -167,8 +157,6 @@ type WebsiteCAObtain struct {
Description string `json:"description"` Description string `json:"description"`
ExecShell bool `json:"execShell"` ExecShell bool `json:"execShell"`
Shell string `json:"shell"` Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
type WebsiteCARenew struct { type WebsiteCARenew struct {
@@ -179,6 +167,4 @@ type WebsiteSSLFileUpload struct {
Type string `json:"type"` Type string `json:"type"`
Description string `json:"description"` Description string `json:"description"`
SSLID uint64 `json:"sslID"` SSLID uint64 `json:"sslID"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
-46
View File
@@ -1,46 +0,0 @@
package request
import (
"github.com/1Panel-dev/1Panel/agent/app/dto"
)
type WebsiteTemplateSearch struct {
dto.PageInfo
Name string `json:"name"`
Type string `json:"type"`
}
type WebsiteTemplateCreate struct {
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required,oneof=single multi"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Variables string `json:"variables"`
Remark string `json:"remark"`
}
type WebsiteTemplateUpdate struct {
ID uint `json:"id" validate:"required"`
Name string `json:"name" validate:"required"`
Type string `json:"type" validate:"required,oneof=single multi"`
Content string `json:"content"`
FilePath string `json:"filePath"`
Variables string `json:"variables"`
Remark string `json:"remark"`
}
type WebsiteTemplateOutputSearch struct {
dto.PageInfo
TemplateID uint `json:"templateID"`
}
type WebsiteTemplateOutputCreate struct {
TemplateID uint `json:"templateID" validate:"required"`
Name string `json:"name" validate:"required"`
VariableValues map[string]string `json:"variableValues"`
}
type WebsitePreviewReq struct {
TemplateID uint `json:"templateID" validate:"required"`
VariableValues map[string]string `json:"variableValues"`
}
+28 -30
View File
@@ -104,36 +104,34 @@ type AppDetail struct {
} }
type AppInstallDTO struct { type AppInstallDTO struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AppID uint `json:"appID"` AppID uint `json:"appID"`
AppDetailID uint `json:"appDetailID"` AppDetailID uint `json:"appDetailID"`
Version string `json:"version"` Version string `json:"version"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
HttpPort int `json:"httpPort"` HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"` HttpsPort int `json:"httpsPort"`
Path string `json:"path"` Path string `json:"path"`
CanUpdate bool `json:"canUpdate"` CanUpdate bool `json:"canUpdate"`
Icon string `json:"icon"` Icon string `json:"icon"`
AppName string `json:"appName"` AppName string `json:"appName"`
Ready int `json:"ready"` Ready int `json:"ready"`
Total int `json:"total"` Total int `json:"total"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
AppType string `json:"appType"` AppType string `json:"appType"`
AppStatus string `json:"appStatus"` AppStatus string `json:"appStatus"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
WebUI string `json:"webUI"` WebUI string `json:"webUI"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder"` SortOrder int `json:"sortOrder"`
App AppDetail `json:"app"` App AppDetail `json:"app"`
Container string `json:"container"` Container string `json:"container"`
IsEdit bool `json:"isEdit"` IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"` LinkDB bool `json:"linkDB"`
ResourceKeys []string `json:"resourceKeys"` ServiceName string `json:"serviceName"`
ServiceName string `json:"serviceName"`
Env map[string]interface{} `json:"env"`
} }
type AppInstallInfo struct { type AppInstallInfo struct {
-14
View File
@@ -16,12 +16,6 @@ type McpServerDTO struct {
Volumes []request.Volume `json:"volumes"` Volumes []request.Volume `json:"volumes"`
} }
type McpServerStatusDTO struct {
ID uint `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}
type McpBindDomainRes struct { type McpBindDomainRes struct {
Domain string `json:"domain"` Domain string `json:"domain"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
@@ -30,11 +24,3 @@ type McpBindDomainRes struct {
WebsiteID uint `json:"websiteID"` WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"` ConnUrl string `json:"connUrl"`
} }
type McpServerConnectionTestRes struct {
Success bool `json:"success"`
Endpoint string `json:"endpoint"`
OutputTransport string `json:"outputTransport"`
ProtocolVersion string `json:"protocolVersion,omitempty"`
Message string `json:"message"`
}
+7 -27
View File
@@ -17,17 +17,6 @@ type NginxParam struct {
Params []string `json:"params"` Params []string `json:"params"`
} }
// NginxBrotliRes carries the brotli settings together with where they live.
// ManagedExternally is true when the user defined brotli by hand, in which
// case the panel only reports the values and must not write its own copy.
// ManagedUnavailable is true when the panel could not wire the managed
// configuration into nginx.conf at all, so the reported values are inert.
type NginxBrotliRes struct {
Params []NginxParam `json:"params"`
ManagedExternally bool `json:"managedExternally"`
ManagedUnavailable bool `json:"managedUnavailable"`
}
type NginxAuthRes struct { type NginxAuthRes struct {
Enable bool `json:"enable"` Enable bool `json:"enable"`
Items []dto.NginxAuth `json:"items"` Items []dto.NginxAuth `json:"items"`
@@ -80,25 +69,16 @@ type NginxProxyCache struct {
} }
type NginxModule struct { type NginxModule struct {
Name string `json:"name"` Name string `json:"name"`
Custom bool `json:"custom"` Script string `json:"script"`
Script string `json:"script"` Packages string `json:"packages"`
Packages string `json:"packages"` Params string `json:"params"`
Params string `json:"params"` Enable bool `json:"enable"`
Enable bool `json:"enable"`
BuildMode string `json:"buildMode"`
Provider string `json:"provider"`
LoadOrder int `json:"loadOrder"`
BuildStatus string `json:"buildStatus"`
LoadStatus string `json:"loadStatus"`
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
LastError string `json:"lastError"`
} }
type NginxBuildConfig struct { type NginxBuildConfig struct {
Mirror string `json:"mirror"` Mirror string `json:"mirror"`
DynamicSupported bool `json:"dynamicSupported"` Modules []NginxModule `json:"modules"`
Modules []NginxModule `json:"modules"`
} }
type NginxConfigRes struct { type NginxConfigRes struct {
@@ -1,18 +0,0 @@
package response
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type WebsiteTemplateDTO struct {
model.WebsiteTemplate
}
type WebsiteTemplateOutputDTO struct {
model.WebsiteTemplateOutput
TemplateName string `json:"templateName"`
}
type WebsitePreviewDTO struct {
HTML string `json:"html"`
}
-30
View File
@@ -1,30 +0,0 @@
package dto
import "time"
type RuntimeDiagnosticsSummary struct {
RSS uint64 `json:"rss"`
HeapAlloc uint64 `json:"heapAlloc"`
HeapObjects uint64 `json:"heapObjects"`
Goroutines int `json:"goroutines"`
}
type RuntimeGoroutineGroup struct {
State string `json:"state"`
Top string `json:"top"`
Count int `json:"count"`
Stack []string `json:"stack"`
}
type RuntimeGoroutineSnapshot struct {
Total int `json:"total"`
GroupCount int `json:"groupCount"`
Truncated bool `json:"truncated"`
CapturedAt time.Time `json:"capturedAt"`
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
}
type RuntimeProfileCreate struct {
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
}
+1 -1
View File
@@ -35,7 +35,7 @@ type SettingUpdate struct {
} }
type AgentSettingUpdate struct { type AgentSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin"` Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
Value string `json:"value"` Value string `json:"value"`
} }
-11
View File
@@ -1,11 +0,0 @@
package dto
type TerminalSessionClose struct {
ID string `json:"id" validate:"required"`
}
type TerminalSessionRevoke struct {
Scope string `json:"scope" validate:"required,oneof=auth_session user all"`
UserID string `json:"userId"`
AuthSessionID string `json:"authSessionId"`
}
-2
View File
@@ -7,8 +7,6 @@ type AgentAccount struct {
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
AuthMode string `json:"authMode"`
VerifyModel string `json:"verifyModel"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
+8 -4
View File
@@ -2,10 +2,14 @@ package model
type AgentAccountModel struct { type AgentAccountModel struct {
BaseModel BaseModel
AccountID uint `json:"accountId" gorm:"index"` AccountID uint `json:"accountId" gorm:"index"`
Model string `json:"model" gorm:"index"` Model string `json:"model" gorm:"index"`
Name string `json:"name"` Name string `json:"name"`
SortOrder int `json:"sortOrder" gorm:"index"` ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Reasoning bool `json:"reasoning"`
Input string `json:"input" gorm:"type:text"`
SortOrder int `json:"sortOrder" gorm:"index"`
} }
func (AgentAccountModel) TableName() string { func (AgentAccountModel) TableName() string {
+10 -27
View File
@@ -1,12 +1,5 @@
package model package model
import (
"strings"
"github.com/google/uuid"
"gorm.io/gorm"
)
type Alert struct { type Alert struct {
BaseModel BaseModel
@@ -25,11 +18,10 @@ type Alert struct {
type AlertTask struct { type AlertTask struct {
BaseModel BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"` Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"` Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"` QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"` Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
} }
type AlertLog struct { type AlertLog struct {
@@ -49,21 +41,12 @@ type AlertLog struct {
type AlertConfig struct { type AlertConfig struct {
BaseModel BaseModel
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"` Type string `gorm:"type:varchar(64);not null" json:"type"`
Type string `gorm:"type:varchar(64);not null" json:"type"` Title string `gorm:"type:varchar(64);not null" json:"title"`
Title string `gorm:"type:varchar(64);not null" json:"title"` Status string `gorm:"type:varchar(64);not null" json:"status"`
Status string `gorm:"type:varchar(64);not null" json:"status"` Config string `gorm:"type:varchar(256);not null" json:"config"`
Config string `gorm:"type:text;not null" json:"config"` CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
SecretConfig string `gorm:"type:text;not null;default:''" json:"-"` UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
func (a *AlertConfig) BeforeCreate(_ *gorm.DB) error {
if strings.TrimSpace(a.UID) == "" {
a.UID = uuid.NewString()
}
return nil
} }
type LoginLog struct { type LoginLog struct {
-1
View File
@@ -31,5 +31,4 @@ type BackupRecord struct {
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
Description string `json:"description"` Description string `json:"description"`
Args string `gorm:"not null;default:''" json:"args"`
} }
+2 -3
View File
@@ -11,7 +11,6 @@ type ComposeTemplate struct {
type Compose struct { type Compose struct {
BaseModel BaseModel
Name string `json:"name"` Name string `json:"name"`
Path string `json:"path"` Path string `json:"path"`
IsPinned bool `json:"isPinned"`
} }
-21
View File
@@ -1,21 +0,0 @@
package model
type DatabaseUser struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user"`
Host string `json:"host" gorm:"uniqueIndex:idx_database_user"`
Password string `json:"password"`
Description string `json:"description"`
IsDelete bool `json:"isDelete"`
}
type DatabaseUserGrant struct {
BaseModel
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user_grant"`
DBName string `json:"dbName" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user_grant"`
Host string `json:"host" gorm:"not null;uniqueIndex:idx_database_user_grant"`
}
+13 -217
View File
@@ -1,222 +1,18 @@
package model package model
import ( type Firewall struct {
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"sort"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
)
const FirewallRuleSequenceStep int64 = 1 << 32
type DockerPortGuardPolicy struct {
BaseModel BaseModel
UUID string `gorm:"size:64;not null;uniqueIndex" json:"uuid"` Type string `json:"type"`
ReadOnly bool `gorm:"not null;default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"` Port string `json:"port"` // Deprecated
Family string `gorm:"size:16;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"family"` Address string `json:"address"` // Deprecated
HostIP string `gorm:"size:64;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
HostPort uint16 `gorm:"not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"` Chain string `json:"chain"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"` Protocol string `json:"protocol"`
Mode string `gorm:"size:32;not null" json:"mode"` SrcIP string `json:"srcIP"`
Sources string `gorm:"type:text" json:"-"` SrcPort string `json:"srcPort"`
Description string `gorm:"type:text" json:"description"` DstIP string `json:"dstIP"`
NativeAction string `gorm:"size:32;not null;default:''" json:"-"` DstPort string `json:"dstPort"`
NativeRules string `gorm:"type:text" json:"-"` Strategy string `gorm:"not null" json:"strategy"`
Sequence int64 `gorm:"not null;default:0" json:"-"` Description string `json:"description"`
}
type ForwardingRule struct {
BaseModel
Family string `gorm:"size:16;not null;uniqueIndex:idx_forwarding_rule_identity" json:"family"`
Protocol string `gorm:"size:8;not null;uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
Port string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"port"`
TargetIP string `gorm:"size:64;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
TargetPort string `gorm:"size:32;not null;uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
Interface string `gorm:"size:32;not null;default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
}
type FirewallRule struct {
UUID string `gorm:"size:64;primaryKey" json:"uuid"`
Family string `gorm:"size:16;not null" json:"family"`
Protocol string `gorm:"size:32;not null" json:"protocol"`
SourceAddress string `gorm:"size:255" json:"sourceAddress"`
SourcePort string `gorm:"size:64" json:"sourcePort"`
DestinationAddress string `gorm:"size:255" json:"destinationAddress"`
DestinationPort string `gorm:"size:64" json:"destinationPort"`
Interface string `gorm:"size:128" json:"interface"`
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
Action string `gorm:"size:32;not null" json:"action"`
Description string `gorm:"type:text" json:"description"`
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
Priority *int `json:"priority,omitempty"`
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
Origin string `gorm:"size:32;not null" json:"origin"`
Owner string `gorm:"size:320;not null" json:"owner"`
Revision uint `gorm:"not null;default:1" json:"revision"`
}
func FirewallRuleOwner(sourceKind, sourceID string) string {
sourceKind = strings.TrimSpace(sourceKind)
sourceID = strings.TrimSpace(sourceID)
if sourceID == "" {
return sourceKind
}
return sourceKind + ":" + sourceID
}
func FirewallRuleFromDomain(rule filter.FirewallRule) (FirewallRule, error) {
normalized, err := filter.NormalizeRule(rule)
if err != nil {
return FirewallRule{}, err
}
switch normalized.NativeKind {
case "", filter.NativeKindRule, filter.NativeKindZonePort, filter.NativeKindRichRule, filter.NativeKindUFWRule:
default:
return FirewallRule{}, fmt.Errorf("%w: native rule %q cannot be stored as a provider-neutral policy", filter.ErrUnsupportedScope, normalized.NativeKind)
}
record := FirewallRule{
Family: string(normalized.Scope.Family),
Protocol: normalized.Protocol,
SourceAddress: normalized.SourceAddress,
SourcePort: normalized.SourcePort,
DestinationAddress: normalized.DestinationAddress,
DestinationPort: normalized.DestinationPort,
Interface: normalized.Interface,
ConnectionStates: strings.Join(normalized.ConnectionStates, ","),
Action: string(normalized.Action),
Description: normalized.Description,
}
if normalized.Scope.Provider == filter.ProviderFirewalld {
record.Priority = normalized.Priority
}
return record, nil
}
func (rule FirewallRule) PolicyKey() string {
payload, _ := json.Marshal(struct {
Family string `json:"family"`
Protocol string `json:"protocol"`
SourceAddress string `json:"sourceAddress,omitempty"`
SourcePort string `json:"sourcePort,omitempty"`
DestinationAddress string `json:"destinationAddress,omitempty"`
DestinationPort string `json:"destinationPort,omitempty"`
Interface string `json:"interface,omitempty"`
ConnectionStates string `json:"connectionStates,omitempty"`
Action string `json:"action"`
}{
Family: rule.Family, Protocol: rule.Protocol,
SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: rule.ConnectionStates, Action: rule.Action,
})
sum := sha256.Sum256(payload)
return hex.EncodeToString(sum[:])
}
func (rule FirewallRule) RulesForProvider(provider filter.Provider) ([]filter.FirewallRule, error) {
if rule.CompatibilityError != "" {
return nil, fmt.Errorf("%w: %s", filter.ErrUnsupportedScope, rule.CompatibilityError)
}
connectionStates := make([]string, 0)
if rule.ConnectionStates != "" {
connectionStates = strings.Split(rule.ConnectionStates, ",")
}
base := filter.FirewallRule{
Protocol: rule.Protocol, SourceAddress: rule.SourceAddress, SourcePort: rule.SourcePort,
DestinationAddress: rule.DestinationAddress, DestinationPort: rule.DestinationPort,
Interface: rule.Interface, ConnectionStates: connectionStates,
Action: filter.Action(rule.Action), Description: rule.Description,
}
if provider != filter.ProviderUFW && strings.EqualFold(strings.TrimSpace(base.Protocol), "all") &&
strings.TrimSpace(base.SourcePort) == "" && strings.TrimSpace(base.DestinationPort) != "" {
base.Protocol = "tcp/udp"
}
if provider == filter.ProviderFirewalld {
base.Priority = rule.Priority
}
families := []filter.Family{filter.Family(rule.Family)}
if provider != filter.ProviderFirewalld && families[0] == filter.FamilyInet {
hasIPv4, hasIPv6 := ruleAddressFamilies(base)
switch {
case hasIPv4 && hasIPv6:
return nil, fmt.Errorf("%w: inet policy contains both IPv4 and IPv6 addresses", filter.ErrUnsupportedScope)
case hasIPv6 || strings.EqualFold(base.Protocol, "icmpv6"):
families = []filter.Family{filter.FamilyIPv6}
case hasIPv4:
families = []filter.Family{filter.FamilyIPv4}
default:
families = []filter.Family{filter.FamilyIPv4, filter.FamilyIPv6}
}
}
result := make([]filter.FirewallRule, 0, len(families))
for _, family := range families {
compiled := base
compiled.Scope = filter.Scope{Provider: provider, Family: family, Direction: filter.DirectionInput}
switch provider {
case filter.ProviderIptables, filter.ProviderNftables:
compiled.Scope.Table, compiled.Scope.Chain = "filter", filter.IptablesInputChain
case filter.ProviderFirewalld:
compiled.Scope.Zone = filter.FirewalldInputZone
case filter.ProviderUFW:
compiled.Scope.Chain = filter.UFWInputChain
default:
return nil, fmt.Errorf("%w: unsupported firewall provider %q", filter.ErrProviderUnavailable, provider)
}
expanded, err := filter.ExpandAtomicRules(compiled)
if err != nil {
return nil, err
}
result = append(result, expanded...)
}
return result, nil
}
func SortFirewallRules(rules []FirewallRule, provider filter.Provider) {
sort.SliceStable(rules, func(i, j int) bool {
left, right := rules[i], rules[j]
if provider == filter.ProviderFirewalld {
switch {
case left.Priority == nil && right.Priority != nil:
return false
case left.Priority != nil && right.Priority == nil:
return true
case left.Priority != nil && right.Priority != nil && *left.Priority != *right.Priority:
return *left.Priority < *right.Priority
}
} else {
switch {
case left.Sequence == nil && right.Sequence != nil:
return false
case left.Sequence != nil && right.Sequence == nil:
return true
case left.Sequence != nil && right.Sequence != nil && *left.Sequence != *right.Sequence:
return *left.Sequence < *right.Sequence
}
}
return left.UUID < right.UUID
})
}
func ruleAddressFamilies(rule filter.FirewallRule) (bool, bool) {
hasIPv4, hasIPv6 := false, false
for _, address := range []string{rule.SourceAddress, rule.DestinationAddress} {
address = strings.TrimSpace(address)
if address == "" {
continue
}
if strings.Contains(address, ":") {
hasIPv6 = true
} else {
hasIPv4 = true
}
}
return hasIPv4, hasIPv6
} }
-2
View File
@@ -8,6 +8,4 @@ type Ftp struct {
Status string `gorm:"not null" json:"status"` Status string `gorm:"not null" json:"status"`
Path string `gorm:"not null" json:"path"` Path string `gorm:"not null" json:"path"`
Description string `gorm:"not null" json:"description"` Description string `gorm:"not null" json:"description"`
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
} }
-3
View File
@@ -18,7 +18,4 @@ type McpServer struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"` OutputTransport string `json:"outputTransport"`
Type string `json:"type"` Type string `json:"type"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
GatewayArgs string `json:"gatewayArgs"`
} }
-28
View File
@@ -1,28 +0,0 @@
package model
type WebsiteTemplate struct {
BaseModel
Name string `gorm:"not null" json:"name"`
Type string `gorm:"not null" json:"type"` // single | multi
Content string `gorm:"type:longtext" json:"content"`
FilePath string `json:"filePath"`
Variables string `gorm:"type:text" json:"variables"`
Remark string `json:"remark"`
}
func (w WebsiteTemplate) TableName() string {
return "website_templates"
}
type WebsiteTemplateOutput struct {
BaseModel
Name string `gorm:"not null" json:"name"`
TemplateID uint `gorm:"not null" json:"templateID"`
TemplateType string `json:"templateType"`
VariableValues string `gorm:"type:text" json:"variableValues"`
OutputPath string `json:"outputPath"`
}
func (w WebsiteTemplateOutput) TableName() string {
return "website_template_outputs"
}
+327 -379
View File
@@ -1,262 +1,319 @@
package provider package provider
import ( import (
"fmt"
"net/url"
"strings" "strings"
) )
type APIConfig struct { type Model struct {
APIType string ID string
BaseURL string Name string
EditableBaseURL bool ContextWindow int
DiscoverModels bool MaxTokens int
DefaultAuthMode string Reasoning bool
AuthModes []string Input []string
Models []Model
} }
const ( type RuntimeDefault struct {
AuthModeBearer = "bearer" APIType string
AuthModeXAPIKey = "x-api-key" ContextWindow int
) MaxTokens int
Input []string
type Model struct {
ID string
Name string
} }
type Meta struct { type Meta struct {
Key string Key string
DisplayName string DisplayName string
DisplayNameKey string
Sort uint Sort uint
DefaultAPIType string DefaultBaseURL string
APIConfigs []APIConfig
EnvKey string EnvKey string
Default RuntimeDefault
Models []Model Models []Model
} }
var catalog = map[string]Meta{ var catalog = map[string]Meta{
"custom": { "custom": {
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY", Key: "custom",
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"), DisplayName: "Custom",
Sort: 10,
DefaultBaseURL: "",
EnvKey: "CUSTOM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
}, },
"ollama": { "ollama": {
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses", Key: "ollama",
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"), DisplayName: "Ollama",
}, Sort: 15,
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434. Default: RuntimeDefault{
"llmman": { APIType: "openai-responses",
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses", ContextWindow: 160000,
APIConfigs: []APIConfig{ MaxTokens: 8192,
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true}, Input: []string{"text"},
{APIType: "openai-completions", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
{APIType: "openai-embeddings", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
}, },
}, },
"vllm": { "vllm": {
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY", Key: "vllm",
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"), DisplayName: "vLLM",
Sort: 20,
DefaultBaseURL: "",
EnvKey: "VLLM_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{},
}, },
"deepseek": { "deepseek": {
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY", Key: "deepseek",
APIConfigs: []APIConfig{ DisplayName: "DeepSeek",
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"}, Sort: 25,
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"}, DefaultBaseURL: "https://api.deepseek.com/v1",
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey), EnvKey: "DEEPSEEK_API_KEY",
}, Default: RuntimeDefault{
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}}, APIType: "openai-completions",
}, ContextWindow: 131072,
"bailian-coding-plan": { MaxTokens: 8192,
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY", Input: []string{"text"},
APIConfigs: []APIConfig{
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
}, },
Models: []Model{ Models: []Model{
{ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"}, {ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
{ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"}, {ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
{ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"}, },
{ID: "glm-4.7", Name: "GLM-4.7"}, },
{ID: "kimi-k2.5", Name: "Kimi K2.5"}, "bailian-coding-plan": {
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"}, Key: "bailian-coding-plan",
{ID: "glm-5", Name: "GLM-5"}, DisplayName: "阿里云百炼 Coding Plan",
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"}, Sort: 30,
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"}, DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"}, EnvKey: "QWEN_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
}, },
}, },
"ark-coding-plan": { "ark-coding-plan": {
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY", Key: "ark-coding-plan",
APIConfigs: []APIConfig{ DisplayName: "方舟 Coding Plan",
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"}, Sort: 35,
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer), DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
EnvKey: "ARK_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 4096,
Input: []string{"text"},
}, },
Models: []Model{ Models: []Model{
{ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"}, {ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096},
{ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}, {ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096},
{ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"}, {ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096},
{ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096},
}, },
}, },
"zai": { "zai": {
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY", Key: "zai",
APIConfigs: []APIConfig{ DisplayName: "Z.ai",
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}, Sort: 40,
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}, DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
EnvKey: "ZAI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 204800,
MaxTokens: 131072,
Input: []string{"text"},
},
Models: []Model{
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
}, },
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
}, },
"minimax": { "minimax": {
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY", Key: "minimax",
APIConfigs: []APIConfig{ DisplayName: "MiniMax (CN)",
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer), Sort: 45,
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"}, DefaultBaseURL: "https://api.minimaxi.com/anthropic",
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"}, EnvKey: "MINIMAX_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 200000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
}, },
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
}, },
"xiaomi": { "xiaomi": {
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY", Key: "xiaomi",
APIConfigs: []APIConfig{ DisplayName: "Xiaomi",
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"}, Sort: 46,
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"}, DefaultBaseURL: "https://api.xiaomimimo.com/v1",
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer), EnvKey: "XIAOMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 262144,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 1048576, MaxTokens: 32000, Reasoning: true, Input: []string{"text"}},
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 32000, Reasoning: true, Input: []string{"text", "image"}},
}, },
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
}, },
"kimi": { "kimi": {
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY", Key: "kimi",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}}, DisplayName: "Kimi (CN)",
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}}, Sort: 50,
DefaultBaseURL: "https://api.moonshot.cn/v1",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
},
}, },
"kimi-coding": { "kimi-coding": {
Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY", Key: "kimi-coding",
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)}, DisplayName: "Kimi Coding",
Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}}, Sort: 51,
DefaultBaseURL: "https://api.kimi.com/coding/",
EnvKey: "KIMI_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 262144,
MaxTokens: 32768,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "kimi-coding/kimi-code", Name: "Kimi Code", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
},
}, },
"openai": { "openai": {
Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY", Key: "openai",
APIConfigs: []APIConfig{ DisplayName: "OpenAI",
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"}, Sort: 55,
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"}, DefaultBaseURL: "https://api.openai.com/v1",
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"}, EnvKey: "OPENAI_API_KEY",
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{ Default: RuntimeDefault{
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"}, APIType: "openai-responses",
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"}, ContextWindow: 272000,
}}, MaxTokens: 128000,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "openai/gpt-5.4", Name: "gpt-5.4", ContextWindow: 272000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-pro", Name: "gpt-5.4-pro", ContextWindow: 1050000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-mini", Name: "gpt-5.4-mini", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "openai/gpt-5.4-nano", Name: "gpt-5.4-nano", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
}, },
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
}, },
"openrouter": { "openrouter": {
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY", Key: "openrouter",
APIConfigs: []APIConfig{ DisplayName: "OpenRouter",
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}, Sort: 56,
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"}, DefaultBaseURL: "https://openrouter.ai/api/v1",
EnvKey: "OPENROUTER_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 128000,
MaxTokens: 8192,
Input: []string{"text"},
},
Models: []Model{
{ID: "openrouter/free", Name: "openrouter/free"},
{ID: "openrouter/auto", Name: "openrouter/auto"},
}, },
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
}, },
"anthropic": { "anthropic": {
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY", Key: "anthropic",
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)}, DisplayName: "Anthropic",
Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}}, Sort: 60,
DefaultBaseURL: "https://api.anthropic.com",
EnvKey: "ANTHROPIC_API_KEY",
Default: RuntimeDefault{
APIType: "anthropic-messages",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "anthropic/claude-sonnet-4-6", Name: "Claude Sonnet 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-6", Name: "Claude Opus 4.6", Reasoning: true},
{ID: "anthropic/claude-opus-4-5", Name: "Claude Opus 4.5"},
{ID: "anthropic/claude-sonnet-4-5", Name: "Claude Sonnet 4.5"},
{ID: "anthropic/claude-haiku-4-5", Name: "Claude Haiku 4.5"},
},
}, },
"gemini": { "gemini": {
Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY", Key: "gemini",
APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}}, DisplayName: "Gemini",
Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}}, Sort: 65,
DefaultBaseURL: "https://generativelanguage.googleapis.com",
EnvKey: "GEMINI_API_KEY",
Default: RuntimeDefault{
APIType: "openai-completions",
ContextWindow: 256000,
MaxTokens: 8192,
Input: []string{"text", "image"},
},
Models: []Model{
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
},
}, },
"moonshot": { "moonshot": {
Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY", Key: "moonshot",
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}}, DisplayName: "Moonshot (Global)",
Models: []Model{{ID: "kimi-k2.5", Name: "Kimi K2.5"}, {ID: "kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"}}, Sort: 70,
}, DefaultBaseURL: "https://api.moonshot.ai/v1",
"bailian": { EnvKey: "MOONSHOT_API_KEY",
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY", Default: RuntimeDefault{
APIConfigs: []APIConfig{ APIType: "openai-completions",
{ ContextWindow: 256000,
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", MaxTokens: 8192,
DiscoverModels: true, Input: []string{"text"},
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}}, },
}, Models: []Model{
{ {ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
APIType: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1", {ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
DiscoverModels: true, {ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
},
{
APIType: "anthropic-messages", BaseURL: "https://dashscope.aliyuncs.com/apps/anthropic",
DefaultAuthMode: AuthModeBearer,
AuthModes: []string{AuthModeBearer},
Models: []Model{{ID: "qwen3.7-plus", Name: "qwen3.7-plus"}, {ID: "qwen3.6-plus", Name: "qwen3.6-plus"}, {ID: "qwen3.6-flash", Name: "qwen3.6-flash"}},
},
{
APIType: "dashscope-images", BaseURL: "https://dashscope.aliyuncs.com",
Models: []Model{
{ID: "qwen-image-2.0-pro", Name: "qwen-image-2.0-pro"},
{ID: "qwen-image-2.0", Name: "qwen-image-2.0"},
{ID: "wan2.7-image-pro", Name: "wan2.7-image-pro"},
{ID: "wan2.7-image", Name: "wan2.7-image"},
},
},
}, },
}, },
"ark": {
Key: "ark", DisplayNameKey: "AIProviderArk", Sort: 36, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
APIConfigs: []APIConfig{
{
APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
DiscoverModels: true,
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
},
{
APIType: "openai-responses", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
DiscoverModels: true,
Models: []Model{{ID: "doubao-seed-2-0-pro-260215", Name: "doubao-seed-2-0-pro-260215"}, {ID: "doubao-seed-2-0-lite-260215", Name: "doubao-seed-2-0-lite-260215"}},
},
{
APIType: "openai-images", BaseURL: "https://ark.cn-beijing.volces.com/api/v3",
Models: []Model{
{ID: "doubao-seedream-5-0-260128", Name: "doubao-seedream-5-0-260128"},
{ID: "doubao-seedream-5-0-lite-260128", Name: "doubao-seedream-5-0-lite-260128"},
{ID: "doubao-seedream-4-5-251128", Name: "doubao-seedream-4-5-251128"},
},
},
},
},
}
func editableAPIConfigs(discoverModels bool, apiTypes ...string) []APIConfig {
configs := make([]APIConfig, 0, len(apiTypes))
for _, apiType := range apiTypes {
if apiType == "anthropic-messages" {
config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer)
config.EditableBaseURL = true
configs = append(configs, config)
continue
}
configs = append(configs, APIConfig{
APIType: apiType,
EditableBaseURL: true,
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
})
}
return configs
}
func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig {
authModes := []string{defaultAuthMode}
for _, authMode := range additionalAuthModes {
if authMode != defaultAuthMode {
authModes = append(authModes, authMode)
}
}
return APIConfig{
APIType: "anthropic-messages",
BaseURL: baseURL,
DefaultAuthMode: defaultAuthMode,
AuthModes: authModes,
}
} }
func Get(key string) (Meta, bool) { func Get(key string) (Meta, bool) {
@@ -275,148 +332,12 @@ func All() map[string]Meta {
return result return result
} }
func FindAPIConfig(key, apiType string) (APIConfig, bool) {
meta, ok := catalog[key]
if !ok {
return APIConfig{}, false
}
target := strings.TrimSpace(apiType)
if target == "" {
target = meta.DefaultAPIType
}
for _, config := range meta.APIConfigs {
if config.APIType == target {
config.AuthModes = append([]string(nil), config.AuthModes...)
config.Models = append([]Model(nil), config.Models...)
return config, true
}
}
return APIConfig{}, false
}
func DefaultModels(key, apiType string) []Model {
meta, ok := catalog[key]
if !ok {
return nil
}
target := strings.TrimSpace(apiType)
if target == "" {
target = meta.DefaultAPIType
}
for _, config := range meta.APIConfigs {
if config.APIType != target {
continue
}
if len(config.Models) > 0 {
return append([]Model(nil), config.Models...)
}
if IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType) {
return nil
}
break
}
return append([]Model(nil), meta.Models...)
}
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(provider, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType)
}
if config.APIType != "anthropic-messages" {
return "", nil
}
authMode := strings.TrimSpace(requested)
if authMode == "" {
authMode = config.DefaultAuthMode
}
for _, allowed := range config.AuthModes {
if authMode == allowed {
return authMode, nil
}
}
return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode)
}
func DefaultBaseURL(key string) (string, bool) { func DefaultBaseURL(key string) (string, bool) {
config, ok := FindAPIConfig(key, "") meta, ok := catalog[key]
if !ok || strings.TrimSpace(config.BaseURL) == "" { if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
return "", false return "", false
} }
return config.BaseURL, true return meta.DefaultBaseURL, true
}
func DefaultAPIType(key string) string {
meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" {
return "openai-completions"
}
return meta.DefaultAPIType
}
func ResolveBaseURL(key, apiType, requested string) (string, error) {
config, ok := FindAPIConfig(key, apiType)
if !ok {
return "", fmt.Errorf("provider %s does not support api type %s", key, apiType)
}
if !config.EditableBaseURL {
return strings.TrimRight(config.BaseURL, "/"), nil
}
baseURL := strings.TrimSpace(requested)
if baseURL == "" {
baseURL = config.BaseURL
}
if baseURL == "" {
return "", fmt.Errorf("base url is required")
}
parsed, err := url.Parse(baseURL)
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
return "", fmt.Errorf("invalid base url")
}
if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
return baseURL, nil
}
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
parsed.RawQuery = ""
parsed.Fragment = ""
return strings.TrimRight(parsed.String(), "/"), nil
}
func normalizeEndpointPath(apiType, value string) string {
if IsImageAPIType(apiType) {
return strings.TrimRight(value, "/")
}
path := strings.TrimRight(value, "/")
suffixes := []string{}
switch apiType {
case "openai-completions":
suffixes = []string{"/chat/completions"}
case "openai-responses":
suffixes = []string{"/responses"}
case "anthropic-messages":
suffixes = []string{"/v1/messages", "/messages"}
case "openai-embeddings":
suffixes = []string{"/v1/embeddings", "/embeddings"}
}
for _, suffix := range suffixes {
if strings.HasSuffix(strings.ToLower(path), suffix) {
return path[:len(path)-len(suffix)]
}
}
return path
}
func IsEmbeddingAPIType(apiType string) bool {
return apiType == "openai-embeddings"
}
func IsImageAPIType(apiType string) bool {
switch apiType {
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
return true
default:
return false
}
} }
func EnvKey(key string) string { func EnvKey(key string) string {
@@ -429,62 +350,89 @@ func EnvKey(key string) string {
func DisplayName(key string) string { func DisplayName(key string) string {
meta, ok := catalog[key] meta, ok := catalog[key]
if !ok || strings.TrimSpace(meta.DisplayName) == "" { if !ok {
return key
}
if strings.TrimSpace(meta.DisplayName) == "" {
return key return key
} }
return meta.DisplayName return meta.DisplayName
} }
func DisplayNameKey(key string) string { func FindModel(key, modelID string) (Model, bool) {
meta, ok := catalog[key] meta, ok := Get(key)
if !ok { if !ok {
return "" return Model{}, false
} }
return meta.DisplayNameKey for _, item := range meta.Models {
} if item.ID == modelID {
return item, true
func NormalizeModelID(provider, modelID string) string {
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
for _, prefix := range legacyModelPrefixes[provider] {
legacyPrefix := prefix + "/"
if !strings.HasPrefix(target, legacyPrefix) {
continue
}
candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix))
if candidate != "" {
return candidate
} }
} }
return target return Model{}, false
}
var legacyModelPrefixes = map[string][]string{
"custom": {"custom"},
"vllm": {"custom"},
"ollama": {"ollama"},
"llmman": {"llmman"},
"deepseek": {"deepseek"},
"bailian-coding-plan": {"bailian-coding-plan"},
"ark-coding-plan": {"ark-coding-plan"},
"zai": {"zai"},
"minimax": {"minimax"},
"xiaomi": {"xiaomi"},
"kimi": {"kimi", "moonshot"},
"kimi-coding": {"kimi-coding"},
"openai": {"openai"},
"anthropic": {"anthropic"},
"gemini": {"google", "gemini"},
"moonshot": {"moonshot"},
} }
func cloneMeta(meta Meta) Meta { func cloneMeta(meta Meta) Meta {
clone := meta clone := meta
clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs)) if len(meta.Default.Input) > 0 {
for index, config := range meta.APIConfigs { clone.Default.Input = make([]string, len(meta.Default.Input))
clone.APIConfigs[index] = config copy(clone.Default.Input, meta.Default.Input)
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...) }
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...) if len(meta.Models) > 0 {
clone.Models = make([]Model, len(meta.Models))
for i, item := range meta.Models {
clone.Models[i] = normalizeModel(meta, item)
}
} }
clone.Models = append([]Model(nil), meta.Models...)
return clone return clone
} }
func normalizeModel(meta Meta, model Model) Model {
clone := model
clone.ID = strings.TrimSpace(clone.ID)
clone.Name = strings.TrimSpace(clone.Name)
if clone.Name == "" {
clone.Name = clone.ID
}
if clone.MaxTokens <= 0 {
clone.MaxTokens = meta.Default.MaxTokens
}
if clone.ContextWindow <= 0 {
clone.ContextWindow = meta.Default.ContextWindow
}
if len(clone.Input) == 0 && len(meta.Default.Input) > 0 {
clone.Input = make([]string, len(meta.Default.Input))
copy(clone.Input, meta.Default.Input)
}
return clone
}
func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
defaultAPIType := "openai-completions"
defaultMaxTokens := 8192
defaultContextWindow := 256000
if meta, ok := Get(provider); ok {
if meta.Default.APIType != "" {
defaultAPIType = meta.Default.APIType
}
if meta.Default.MaxTokens > 0 {
defaultMaxTokens = meta.Default.MaxTokens
}
if meta.Default.ContextWindow > 0 {
defaultContextWindow = meta.Default.ContextWindow
}
}
resolvedAPI := apiType
if strings.TrimSpace(apiType) == "" {
resolvedAPI = defaultAPIType
}
resolvedMaxTokens := defaultMaxTokens
resolvedContextWindow := defaultContextWindow
if maxTokens > 0 {
resolvedMaxTokens = maxTokens
}
if contextWindow > 0 {
resolvedContextWindow = contextWindow
}
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
}
-74
View File
@@ -1,74 +0,0 @@
package provider
import (
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
)
func DiscoverModels(baseURL, apiKey string) ([]string, error) {
req, err := http.NewRequest(http.MethodGet, buildModelDiscoveryURL(baseURL), nil)
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode >= http.StatusBadRequest {
return nil, fmt.Errorf("request failed: %s", resp.Status)
}
var payload struct {
Data []struct {
ID string `json:"id"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
return nil, err
}
models := make([]string, 0, len(payload.Data))
seen := make(map[string]struct{}, len(payload.Data))
for _, item := range payload.Data {
id := strings.TrimSpace(item.ID)
if id == "" {
continue
}
if _, ok := seen[id]; ok {
continue
}
seen[id] = struct{}{}
models = append(models, id)
}
if len(models) == 0 {
return nil, fmt.Errorf("no models found")
}
return models, nil
}
func buildModelDiscoveryURL(baseURL string) string {
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
for _, apiType := range []string{"openai-completions", "openai-responses", "anthropic-messages"} {
base = normalizeEndpointPath(apiType, base)
}
switch {
case strings.HasSuffix(base, "/models"):
return base
case hasAPIVersionSuffix(base):
return base + "/models"
default:
return base + "/v1/models"
}
}
func hasAPIVersionSuffix(value string) bool {
segment := value[strings.LastIndex(value, "/")+1:]
if len(segment) < 2 || segment[0] != 'v' {
return false
}
_, err := strconv.Atoi(segment[1:])
return err == nil
}
+53 -33
View File
@@ -15,52 +15,72 @@ type OpenClawProviderPatch struct {
AuthHeader bool AuthHeader bool
} }
func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) { func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
if modelName == "" { if modelName == "" {
return nil, fmt.Errorf("model is required") return nil, fmt.Errorf("model is required")
} }
resolvedAPIType := apiType resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0)
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok { modelID := resolveOpenClawModelID(provider, modelName)
resolvedAPIType = DefaultAPIType(provider)
}
if IsImageAPIType(resolvedAPIType) || IsEmbeddingAPIType(resolvedAPIType) {
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
}
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
if err != nil {
return nil, err
}
usesBearer := resolvedAuthMode == AuthModeBearer
modelID := NormalizeModelID(provider, modelName)
providerKey := provider
preserveQualifiedModel := false
switch provider { switch provider {
case "deepseek":
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
case "gemini": case "gemini":
providerKey = "google" return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
resolvedAPIType = "google-generative-ai"
usesBearer = false
case "moonshot", "kimi": case "moonshot", "kimi":
providerKey = "moonshot" return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
resolvedAPIType = "openai-completions" case "bailian-coding-plan":
usesBearer = false return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
case "ollama", "llmman": case "ark-coding-plan":
apiKey = provider return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
usesBearer = false case "minimax":
case "openai", "openrouter", "anthropic": return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
preserveQualifiedModel = strings.Contains(modelName, "/") case "xiaomi":
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
case "custom", "vllm":
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
case "ollama":
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
case "kimi-coding":
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
case "zai":
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
default:
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
} }
}
primaryModel := providerKey + "/" + modelID func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch {
if preserveQualifiedModel { providerKey := provider
primaryModel = modelName primaryModel := modelName
if provider == "kimi" {
providerKey = "moonshot"
primaryModel = "moonshot/" + modelID
} }
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
}
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
return &OpenClawProviderPatch{ return &OpenClawProviderPatch{
PrimaryModel: primaryModel, PrimaryModel: primaryModel,
ProviderKey: providerKey, ProviderKey: providerKey,
ModelID: modelID, ModelID: modelID,
APIKey: apiKey, APIKey: apiKey,
BaseURL: baseURL, BaseURL: baseURL,
APIType: resolvedAPIType, APIType: apiType,
AuthHeader: usesBearer, AuthHeader: authHeader,
}, nil }
}
func resolveOpenClawModelID(provider, modelName string) string {
if provider == "custom" || provider == "vllm" {
target := strings.TrimLeft(modelName, "/")
if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" {
return strings.TrimLeft(parts[1], "/")
}
return target
}
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
return parts[1]
}
return modelName
} }
+119 -119
View File
@@ -3,9 +3,7 @@ package provider
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"io"
"net/http" "net/http"
"strings" "strings"
"time" "time"
@@ -20,158 +18,160 @@ type VerifyRequest struct {
Body []byte Body []byte
} }
type verifyErrorResponse struct {
Error struct {
Message string `json:"message"`
} `json:"error"`
Message string `json:"message"`
}
const ( const (
defaultVerifyTimeout = 30 * time.Second defaultVerifyTimeout = 30 * time.Second
defaultVerifyMaxTokens = 16
) )
func SkipVerification(provider string) bool { func SkipVerification(key string) bool {
switch provider { switch key {
case "vllm", "ollama", "llmman", "kimi-coding": case "custom", "vllm", "ollama", "kimi-coding":
return true return true
default: default:
return false return false
} }
} }
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error { func VerifyAccount(provider, baseURL, apiKey string) error {
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model) req := BuildVerifyRequest(provider, baseURL, apiKey)
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body)) var body *bytes.Buffer
if len(req.Body) > 0 {
body = bytes.NewBuffer(req.Body)
} else {
body = bytes.NewBuffer(nil)
}
httpReq, err := http.NewRequest(req.Method, req.URL, body)
if err != nil { if err != nil {
return err return err
} }
for key, value := range req.Headers { for key, value := range req.Headers {
httpReq.Header.Set(key, value) httpReq.Header.Set(key, value)
} }
httpReq.Header.Set("Accept", "application/json") resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
if err != nil { if err != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", err) return buserr.WithErr("ErrAgentAccountUnavailable", err)
} }
defer resp.Body.Close() defer resp.Body.Close()
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices { if resp.StatusCode >= 400 {
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024)) return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
if readErr != nil {
return buserr.WithErr("ErrAgentAccountUnavailable", readErr)
}
return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body)))
} }
return nil return nil
} }
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest { func verifyTimeout() time.Duration {
baseURL = strings.TrimSpace(baseURL) return defaultVerifyTimeout
if provider != "custom" || !IsImageAPIType(apiType) { }
baseURL = strings.TrimRight(baseURL, "/")
}
headers := map[string]string{"Content-Type": "application/json"}
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
if provider == "gemini" { func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent" base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
headers["x-goog-api-key"] = apiKey headers := map[string]string{}
request.Body = mustJSON(map[string]interface{}{ request := VerifyRequest{Method: http.MethodGet, Headers: headers}
"contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
})
return request
}
switch apiType { switch provider {
case "openai-embeddings": case "anthropic", "kimi-coding":
request.URL = embeddingVerifyURL(baseURL) headers["x-api-key"] = apiKey
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "ping"})
case "openai-images":
request.URL = imageVerifyURL(provider, baseURL, "/images/generations")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
case "dashscope-images":
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/services/aigc/multimodal-generation/generation")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{
"model": model,
"input": map[string]interface{}{"messages": []map[string]interface{}{
{"role": "user", "content": []map[string]string{{"text": "test"}}},
}},
"parameters": map[string]interface{}{"n": 1},
})
case "minimax-images":
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
case "openrouter-images":
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
headers["Authorization"] = "Bearer " + apiKey
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
case "anthropic-messages":
request.URL = baseURL + "/v1/messages"
if authMode == AuthModeBearer {
headers["Authorization"] = "Bearer " + apiKey
} else {
headers["x-api-key"] = apiKey
}
headers["anthropic-version"] = "2023-06-01" headers["anthropic-version"] = "2023-06-01"
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
case "gemini":
request.Method = http.MethodPost
if strings.Contains(base, "/v1beta") {
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
} else {
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
}
headers["x-goog-api-key"] = apiKey
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false, "contents": []map[string]interface{}{{
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}}, "parts": []map[string]string{{
"text": "Explain how AI works in a few words",
}},
}},
}) })
case "openai-responses": case "zai":
request.URL = baseURL + "/responses" headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Authorization"] = "Bearer " + apiKey request.URL = base + "/models"
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false}) case "bailian-coding-plan":
default: request.Method = http.MethodPost
request.URL = baseURL + "/chat/completions" if !strings.Contains(base, "/v1") {
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" { base = base + "/v1"
headers["Authorization"] = "Bearer " + apiKey }
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "qwen3.5-plus",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "ark-coding-plan":
request.Method = http.MethodPost
if !strings.Contains(base, "/api/coding/v3") {
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
}
request.URL = base + "/chat/completions"
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
request.Body = mustJSON(map[string]interface{}{
"model": "ark-code-latest",
"messages": []map[string]string{{"role": "user", "content": "test"}},
"max_tokens": 1,
})
case "minimax":
request.Method = http.MethodPost
headers["x-api-key"] = apiKey
headers["anthropic-version"] = "2023-06-01"
headers["Content-Type"] = "application/json"
if strings.Contains(base, "/v1") {
request.URL = base + "/messages"
} else {
request.URL = base + "/v1/messages"
} }
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false, "model": "MiniMax-M2.5",
"max_tokens": 1,
"messages": []map[string]interface{}{{
"role": "user",
"content": []map[string]string{{
"type": "text",
"text": "test",
}},
}},
}) })
case "xiaomi":
request.Method = http.MethodPost
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
headers["Content-Type"] = "application/json"
if !strings.Contains(base, "/v1") {
base = base + "/v1"
}
request.URL = base + "/chat/completions"
request.Body = mustJSON(map[string]interface{}{
"model": "mimo-v2-flash",
"max_tokens": 1,
"messages": []map[string]string{{"role": "user", "content": "test"}},
})
case "openrouter":
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/key"
} else {
request.URL = base + "/v1/key"
}
default:
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
if strings.Contains(base, "/v1") {
request.URL = base + "/models"
} else {
request.URL = base + "/v1/models"
}
} }
return request return request
} }
func embeddingVerifyURL(baseURL string) string {
lowerBaseURL := strings.ToLower(baseURL)
if strings.HasSuffix(lowerBaseURL, "/embeddings") {
return baseURL
}
if strings.HasSuffix(lowerBaseURL, "/v1") {
return baseURL + "/embeddings"
}
return baseURL + "/v1/embeddings"
}
func imageVerifyURL(provider, baseURL, endpoint string) string {
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
return baseURL
}
return baseURL + endpoint
}
func verifyHTTPError(statusCode int, body []byte) string {
message := strings.TrimSpace(string(body))
var payload verifyErrorResponse
if err := json.Unmarshal(body, &payload); err == nil {
if value := strings.TrimSpace(payload.Error.Message); value != "" {
message = value
} else if value := strings.TrimSpace(payload.Message); value != "" {
message = value
}
}
if message == "" {
return fmt.Sprintf("validation request returned status %d", statusCode)
}
return message
}
func mustJSON(value interface{}) []byte { func mustJSON(value interface{}) []byte {
payload, err := json.Marshal(value) payload, err := json.Marshal(value)
if err != nil { if err != nil {
+2 -3
View File
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
Save(account *model.AgentAccount) error Save(account *model.AgentAccount) error
DeleteByID(id uint) error DeleteByID(id uint) error
List(opts ...DBOption) ([]model.AgentAccount, error) List(opts ...DBOption) ([]model.AgentAccount, error)
CountTextByProviders(providers []string) (map[string]int64, error) CountByProviders(providers []string) (map[string]int64, error)
} }
func NewIAgentAccountRepo() IAgentAccountRepo { func NewIAgentAccountRepo() IAgentAccountRepo {
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
return accounts, nil return accounts, nil
} }
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) { func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
normalizedProviders := normalizeProviders(providers) normalizedProviders := normalizeProviders(providers)
counts := make(map[string]int64, len(normalizedProviders)) counts := make(map[string]int64, len(normalizedProviders))
for _, provider := range normalizedProviders { for _, provider := range normalizedProviders {
@@ -86,7 +86,6 @@ func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]i
Model(&model.AgentAccount{}). Model(&model.AgentAccount{}).
Select("provider, COUNT(*) as count"). Select("provider, COUNT(*) as count").
Where("provider IN ?", normalizedProviders). Where("provider IN ?", normalizedProviders).
Scopes(WithTextAPIType()).
Group("provider"). Group("provider").
Scan(&rows).Error; err != nil { Scan(&rows).Error; err != nil {
return nil, err return nil, err
+9 -217
View File
@@ -1,30 +1,20 @@
package repo package repo
import ( import (
"encoding/base64"
"encoding/json" "encoding/json"
"errors"
"fmt"
"strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid"
"google.golang.org/genproto/googleapis/type/date" "google.golang.org/genproto/googleapis/type/date"
"gorm.io/gorm" "gorm.io/gorm"
"gorm.io/gorm/clause" "strconv"
"time"
) )
type AlertRepo struct{} type AlertRepo struct{}
var (
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
)
type IAlertRepo interface { type IAlertRepo interface {
WithByType(alertType string) DBOption WithByType(alertType string) DBOption
WithByStatusIn(status []string) DBOption WithByStatusIn(status []string) DBOption
@@ -34,7 +24,6 @@ type IAlertRepo interface {
WithByCreateAt(date *date.Date) DBOption WithByCreateAt(date *date.Date) DBOption
WithByLicenseId(licenseId string) DBOption WithByLicenseId(licenseId string) DBOption
WithByRecordId(recordId uint) DBOption WithByRecordId(recordId uint) DBOption
WithByDeliveryLogID(logID uint) DBOption
WithByAlertMethodContainsConfigID(id uint) DBOption WithByAlertMethodContainsConfigID(id uint) DBOption
WithByMethodConfigIDs(ids []uint) DBOption WithByMethodConfigIDs(ids []uint) DBOption
@@ -56,8 +45,6 @@ type IAlertRepo interface {
CleanAlertLogs() error CleanAlertLogs() error
CreateAlertTask(alertTaskBase *model.AlertTask) error CreateAlertTask(alertTaskBase *model.AlertTask) error
CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error)
FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error)
DeleteAlertTask(opts ...DBOption) error DeleteAlertTask(opts ...DBOption) error
GetAlertTask(opts ...DBOption) (model.AlertTask, error) GetAlertTask(opts ...DBOption) (model.AlertTask, error)
LoadTaskCount(alertType string, project string, method string) (uint, uint, error) LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
@@ -68,7 +55,6 @@ type IAlertRepo interface {
GetConfigById(id uint) (model.AlertConfig, error) GetConfigById(id uint) (model.AlertConfig, error)
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error) AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error
CreateAlertConfig(config *model.AlertConfig) error CreateAlertConfig(config *model.AlertConfig) error
DeleteAlertConfig(opts ...DBOption) error DeleteAlertConfig(opts ...DBOption) error
@@ -237,78 +223,13 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
} }
func (a *AlertRepo) CleanAlertLogs() error { func (a *AlertRepo) CleanAlertLogs() error {
return global.AlertDB.Where("status <> ?", constant.AlertPushing).Delete(&model.AlertLog{}).Error return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
} }
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error { func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
} }
func (a *AlertRepo) CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error) {
if alertTask == nil {
return false, fmt.Errorf("pending alert task is required")
}
created := false
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
var log model.AlertLog
if err := tx.Where("id = ? AND status = ?", logID, constant.AlertPushing).First(&log).Error; err != nil {
return err
}
if log.AlertId != alertID || log.Type != alertTask.Type || log.Method != alertTask.Method {
return fmt.Errorf("pending alert task does not match delivery log %d", logID)
}
alertTask.DeliveryLogID = &logID
result := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "delivery_log_id"}},
DoNothing: true,
}).Create(alertTask)
if result.Error != nil {
return result.Error
}
created = result.RowsAffected > 0
return nil
})
return created, err
}
func (a *AlertRepo) FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error) {
finalized := false
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
status := constant.AlertError
if succeeded {
status = constant.AlertSuccess
message = ""
}
result := tx.Model(&model.AlertLog{}).
Where("id = ? AND status = ?", logID, constant.AlertPushing).
Updates(map[string]interface{}{"status": status, "message": message})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return nil
}
finalized = true
if !succeeded {
return tx.Where("delivery_log_id = ?", logID).Delete(&model.AlertTask{}).Error
}
var count int64
if err := tx.Model(&model.AlertTask{}).Where("delivery_log_id = ?", logID).Count(&count).Error; err != nil {
return err
}
if count > 0 {
return nil
}
if fallback == nil {
return fmt.Errorf("pending alert task metadata is unavailable for delivery log %d", logID)
}
fallback.DeliveryLogID = &logID
return tx.Create(fallback).Error
})
return finalized, err
}
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error { func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
db, _ := getAlertDB(opts...) db, _ := getAlertDB(opts...)
return db.Delete(&model.AlertTask{}).Error return db.Delete(&model.AlertTask{}).Error
@@ -389,23 +310,7 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
return db.Model(&model.AlertConfig{}).Updates(maps).Error return db.Model(&model.AlertConfig{}).Updates(maps).Error
} }
func (a *AlertRepo) UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error {
if revision == nil {
return a.UpdateAlertConfig(maps, opts...)
}
db, _ := getAlertDB(opts...)
result := db.Model(&model.AlertConfig{}).Where("updated_at = ?", *revision).Updates(maps)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrAlertConfigRevisionConflict
}
return nil
}
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error { func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
ensureAlertConfigUID(config)
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
} }
@@ -433,12 +338,6 @@ func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
} }
} }
func (a *AlertRepo) WithByDeliveryLogID(logID uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("delivery_log_id = ?", logID)
}
}
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) { func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
var configs []model.AlertConfig var configs []model.AlertConfig
db := global.AlertDB.Model(&model.AlertConfig{}) db := global.AlertDB.Model(&model.AlertConfig{})
@@ -479,44 +378,26 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
return err return err
} }
oldConfigMap := make(map[string]model.AlertConfig) oldConfigMap := make(map[string]uint)
oldConfigByUID := make(map[string]model.AlertConfig)
oldConfigByType := make(map[string][]model.AlertConfig) oldConfigByType := make(map[string][]model.AlertConfig)
oldConfigByKey := make(map[string][]model.AlertConfig) oldConfigByKey := make(map[string][]model.AlertConfig)
consumedConfigIDs := make(map[uint]struct{}) consumedConfigIDs := make(map[uint]struct{})
for _, item := range oldConfigs { for _, item := range oldConfigs {
if strings.TrimSpace(item.UID) != "" {
oldConfigByUID[item.UID] = item
}
if singletonTypes[item.Type] { if singletonTypes[item.Type] {
oldConfigMap[item.Type] = item oldConfigMap[item.Type] = item.ID
continue continue
} }
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item) oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item) oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
} }
for _, item := range data { for _, item := range data {
if uid := strings.TrimSpace(item.UID); uid != "" {
if matched, ok := oldConfigByUID[uid]; ok && matched.Type != item.Type {
tx.Rollback()
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", uid, matched.Type, item.Type)
}
}
if singletonTypes[item.Type] { if singletonTypes[item.Type] {
if matched, ok := oldConfigMap[item.Type]; ok { if val, ok := oldConfigMap[item.Type]; ok {
if err := inheritAlertConfigSyncState(&item, matched); err != nil { item.ID = val
tx.Rollback()
return err
}
delete(oldConfigMap, item.Type) delete(oldConfigMap, item.Type)
consumedConfigIDs[item.ID] = struct{}{} consumedConfigIDs[item.ID] = struct{}{}
} else { } else {
item.ID = 0 item.ID = 0
ensureAlertConfigUID(&item)
if err := validateAlertConfigSyncSecret(&item); err != nil {
tx.Rollback()
return err
}
} }
if item.ID == 0 { if item.ID == 0 {
if err := tx.Create(&item).Error; err != nil { if err := tx.Create(&item).Error; err != nil {
@@ -530,31 +411,9 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
continue continue
} }
if strings.TrimSpace(item.UID) != "" {
if matched, ok := oldConfigByUID[item.UID]; ok {
delete(oldConfigByUID, item.UID)
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
return err
}
deleteAlertConfigByID(oldConfigByType, matched.ID)
deleteAlertConfigByID(oldConfigByKey, matched.ID)
continue
}
}
key := alertConfigSyncKey(item) key := alertConfigSyncKey(item)
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok { if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
delete(oldConfigByUID, matched.UID) item.ID = matched.ID
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
consumedConfigIDs[item.ID] = struct{}{} consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil { if err := tx.Save(&item).Error; err != nil {
tx.Rollback() tx.Rollback()
@@ -565,12 +424,7 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
} }
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok { if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
delete(oldConfigByUID, matched.UID) item.ID = matched.ID
deleteAlertConfigByID(oldConfigByKey, matched.ID)
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
tx.Rollback()
return err
}
consumedConfigIDs[item.ID] = struct{}{} consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil { if err := tx.Save(&item).Error; err != nil {
tx.Rollback() tx.Rollback()
@@ -580,11 +434,6 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
} }
item.ID = 0 item.ID = 0
ensureAlertConfigUID(&item)
if err := validateAlertConfigSyncSecret(&item); err != nil {
tx.Rollback()
return err
}
if err := tx.Create(&item).Error; err != nil { if err := tx.Create(&item).Error; err != nil {
tx.Rollback() tx.Rollback()
return err return err
@@ -609,63 +458,6 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
return nil return nil
} }
func ensureAlertConfigUID(config *model.AlertConfig) {
if config != nil && strings.TrimSpace(config.UID) == "" {
config.UID = uuid.NewString()
}
}
func inheritAlertConfigSyncState(incoming *model.AlertConfig, existing model.AlertConfig) error {
if incoming.Type != existing.Type {
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", incoming.UID, existing.Type, incoming.Type)
}
preserveExistingCustom := incoming.Type == constant.Custom &&
existing.Status == constant.AlertDisable &&
incoming.Title == existing.Title &&
incoming.Status == existing.Status &&
incoming.Config == existing.Config &&
(incoming.SecretConfig == "" || incoming.SecretConfig == existing.SecretConfig)
incoming.ID = existing.ID
if strings.TrimSpace(incoming.UID) == "" {
incoming.UID = existing.UID
}
if incoming.Type == constant.Custom && incoming.SecretConfig == "" {
incoming.SecretConfig = existing.SecretConfig
}
if preserveExistingCustom {
return nil
}
return validateAlertConfigSyncSecret(incoming)
}
func validateAlertConfigSyncSecret(incoming *model.AlertConfig) error {
if incoming.Type != constant.Custom {
incoming.SecretConfig = ""
return nil
}
if strings.TrimSpace(incoming.SecretConfig) == "" {
return fmt.Errorf("custom webhook sync secret is missing")
}
var version struct {
SchemaVersion int `json:"schemaVersion"`
}
if err := json.Unmarshal([]byte(incoming.Config), &version); err != nil || version.SchemaVersion != 1 {
return fmt.Errorf("custom webhook sync config must use schemaVersion 1")
}
secret := incoming.SecretConfig
for _, prefix := range []string{"core:v1:", "agent:v1:"} {
if !strings.HasPrefix(secret, prefix) {
continue
}
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, prefix))
if err != nil || len(ciphertext) < 32 || len(ciphertext)%16 != 0 {
return fmt.Errorf("custom webhook sync secret envelope is invalid")
}
return nil
}
return fmt.Errorf("custom webhook sync secret must use a versioned envelope")
}
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) { func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
var alerts []model.Alert var alerts []model.Alert
if err := tx.Select("method").Find(&alerts).Error; err != nil { if err := tx.Select("method").Find(&alerts).Error; err != nil {
-7
View File
@@ -15,7 +15,6 @@ type IAppInstallResourceRpo interface {
WithAppInstallId(appInstallId uint) DBOption WithAppInstallId(appInstallId uint) DBOption
WithLinkId(linkId uint) DBOption WithLinkId(linkId uint) DBOption
WithResourceId(resourceId uint) DBOption WithResourceId(resourceId uint) DBOption
WithResourceIds(resourceIds []uint) DBOption
GetBy(opts ...DBOption) ([]model.AppInstallResource, error) GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
GetFirst(opts ...DBOption) (model.AppInstallResource, error) GetFirst(opts ...DBOption) (model.AppInstallResource, error)
Create(ctx context.Context, resource *model.AppInstallResource) error Create(ctx context.Context, resource *model.AppInstallResource) error
@@ -45,12 +44,6 @@ func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption {
} }
} }
func (a AppInstallResourceRpo) WithResourceIds(resourceIds []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("resource_id IN ?", resourceIds)
}
}
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) { func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
db := global.DB.Model(&model.AppInstallResource{}) db := global.DB.Model(&model.AppInstallResource{})
var resources []model.AppInstallResource var resources []model.AppInstallResource
-18
View File
@@ -49,12 +49,6 @@ func WithByName(name string) DBOption {
} }
} }
func WithByPath(path string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("path = ?", path)
}
}
func WithByAddr(addr string) DBOption { func WithByAddr(addr string) DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
return g.Where("addr = ?", addr) return g.Where("addr = ?", addr)
@@ -100,18 +94,6 @@ func WithByProvider(provider string) DBOption {
} }
} }
func WithByAPIType(apiType string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("api_type = ?", apiType)
}
}
func WithTextAPIType() DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("api_type NOT LIKE ? AND api_type <> ?", "%-images", "openai-embeddings")
}
}
func WithByModel(model string) DBOption { func WithByModel(model string) DBOption {
return func(g *gorm.DB) *gorm.DB { return func(g *gorm.DB) *gorm.DB {
if len(model) == 0 { if len(model) == 0 {
-121
View File
@@ -1,121 +0,0 @@
package repo
import (
"context"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"gorm.io/gorm"
)
type DatabaseUserRepo struct{}
type IDatabaseUserRepo interface {
Get(opts ...DBOption) (model.DatabaseUser, error)
List(opts ...DBOption) ([]model.DatabaseUser, error)
Save(user *model.DatabaseUser) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByUser(username, host string) DBOption
WithByUserList(users [][2]string) DBOption
}
func NewIDatabaseUserRepo() IDatabaseUserRepo {
return &DatabaseUserRepo{}
}
func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
var user model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.First(&user).Error; err != nil {
return user, err
}
password, err := encrypt.StringDecrypt(user.Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
return user, nil
}
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
var users []model.DatabaseUser
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
if err := db.Find(&users).Error; err != nil {
return users, err
}
for i := 0; i < len(users); i++ {
password, err := encrypt.StringDecrypt(users[i].Password)
if err != nil {
global.LOG.Errorf("decrypt database user %s password failed, err: %v", users[i].Username, err)
}
users[i].Password = password
}
return users, nil
}
func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
if len(user.Password) != 0 {
password, err := encrypt.StringEncrypt(user.Password)
if err != nil {
return fmt.Errorf("encrypt database user %s password failed, err: %v", user.Username, err)
}
user.Password = password
}
return global.DB.Save(user).Error
}
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUser{}).Error
}
func (u *DatabaseUserRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUser{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserRepo) WithByDatabase(database string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("database = ?", database)
}
}
func (u *DatabaseUserRepo) WithByUser(username, host string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("username = ? AND host = ?", username, host)
}
}
func (u *DatabaseUserRepo) WithByUserList(users [][2]string) DBOption {
return func(g *gorm.DB) *gorm.DB {
if len(users) == 0 {
return g.Where("1 = 0")
}
values := make([][]interface{}, 0, len(users))
for _, user := range users {
values = append(values, []interface{}{user[0], user[1]})
}
return g.Where("(username, host) IN ?", values)
}
}
-109
View File
@@ -1,109 +0,0 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type DatabaseUserGrantRepo struct{}
type IDatabaseUserGrantRepo interface {
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
Save(grant *model.DatabaseUserGrant) error
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
Delete(opts ...DBOption) error
DeleteBy(ctx context.Context, opts ...DBOption) error
Update(vars map[string]interface{}, opts ...DBOption) error
WithByDatabase(database string) DBOption
WithByDBName(dbName string) DBOption
WithByDBNames(dbNames []string) DBOption
WithByUser(username, host string) DBOption
}
func (u *DatabaseUserGrantRepo) Get(opts ...DBOption) (model.DatabaseUserGrant, error) {
var grant model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.First(&grant).Error
return grant, err
}
func NewIDatabaseUserGrantRepo() IDatabaseUserGrantRepo {
return &DatabaseUserGrantRepo{}
}
func (u *DatabaseUserGrantRepo) List(opts ...DBOption) ([]model.DatabaseUserGrant, error) {
var grants []model.DatabaseUserGrant
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
err := db.Find(&grants).Error
return grants, err
}
func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
return global.DB.Save(grant).Error
}
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
return global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
return err
}
if len(grants) != 0 {
return tx.Create(&grants).Error
}
return nil
})
}
func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
db := global.DB
for _, opt := range opts {
db = opt(db)
}
return db.Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
return getTx(ctx, opts...).Delete(&model.DatabaseUserGrant{}).Error
}
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
db := global.DB.Model(&model.DatabaseUserGrant{})
for _, opt := range opts {
db = opt(db)
}
return db.Updates(vars).Error
}
func (u *DatabaseUserGrantRepo) WithByDatabase(database string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("database = ?", database)
}
}
func (u *DatabaseUserGrantRepo) WithByDBName(dbName string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name = ?", dbName)
}
}
func (u *DatabaseUserGrantRepo) WithByDBNames(dbNames []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_name IN ?", dbNames)
}
}
func (u *DatabaseUserGrantRepo) WithByUser(username, host string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("username = ? AND host = ?", username, host)
}
}
-77
View File
@@ -1,77 +0,0 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
"gorm.io/gorm/clause"
)
type IDockerPortGuardRepo interface {
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
ListRuntimeReadOnly(context.Context) ([]model.DockerPortGuardPolicy, error)
DeleteBatch(context.Context, []string) error
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
ReplaceRuntimeReadOnly(context.Context, []model.DockerPortGuardPolicy) error
}
type DockerPortGuardRepo struct{}
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", false).
Order("family, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) ListRuntimeReadOnly(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
var policies []model.DockerPortGuardPolicy
err := global.DB.WithContext(ctx).
Where("read_only = ?", true).
Order("family, sequence, host_ip, host_port, protocol").
Find(&policies).Error
return policies, err
}
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
return global.DB.WithContext(ctx).
Where("read_only = ? AND uuid IN ?", false, uuids).
Delete(&model.DockerPortGuardPolicy{}).Error
}
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
for i := range policies {
policies[i].ReadOnly = false
if err := tx.Clauses(clause.OnConflict{
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
}).Create(&policies[i]).Error; err != nil {
return err
}
}
return nil
})
}
func (r *DockerPortGuardRepo) ReplaceRuntimeReadOnly(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Where("read_only = ?", true).
Delete(&model.DockerPortGuardPolicy{}).Error; err != nil {
return err
}
if len(policies) == 0 {
return nil
}
for i := range policies {
policies[i].ReadOnly = true
}
return tx.Create(&policies).Error
})
}
-150
View File
@@ -1,150 +0,0 @@
package repo
import (
"context"
"errors"
"fmt"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/google/uuid"
"gorm.io/gorm"
)
var (
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
)
type IFirewallRuleRepo interface {
Create(context.Context, *model.FirewallRule) error
GetByUUID(context.Context, string) (model.FirewallRule, error)
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
DeleteWithRevision(context.Context, string, uint) error
}
type FirewallRuleRepo struct {
db *gorm.DB
}
func WithFirewallRuleSource(kind, id string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("owner = ?", model.FirewallRuleOwner(kind, id))
}
}
func NewIFirewallRuleRepo() IFirewallRuleRepo {
return &FirewallRuleRepo{}
}
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
return &FirewallRuleRepo{db: db}
}
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
if err := prepareFirewallRule(rule); err != nil {
return err
}
return r.dbFor(ctx).Create(rule).Error
}
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
var rule model.FirewallRule
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
return rule, err
}
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
var rules []model.FirewallRule
db := r.dbFor(ctx).Model(&model.FirewallRule{})
for _, opt := range opts {
db = opt(db)
}
return rules, db.Find(&rules).Error
}
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
updates = sanitizeRuleUpdates(updates)
updates["revision"] = gorm.Expr("revision + 1")
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
Updates(updates)
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrFirewallRuleRevisionConflict
}
return nil
}
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
result := r.dbFor(ctx).
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
Delete(&model.FirewallRule{})
if result.Error != nil {
return result.Error
}
if result.RowsAffected == 0 {
return ErrFirewallRuleRevisionConflict
}
return nil
}
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
return firewallDB(ctx, r.db)
}
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
if ctx == nil {
ctx = context.Background()
}
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
return tx.WithContext(ctx)
}
if fallback == nil {
fallback = global.DB
}
return fallback.WithContext(ctx)
}
func prepareFirewallRule(rule *model.FirewallRule) error {
if rule == nil {
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
}
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
}
if rule.UUID == "" {
rule.UUID = uuid.NewString()
}
if rule.Revision == 0 {
rule.Revision = 1
}
if rule.Origin == "" {
rule.Origin = constant.FirewallRuleOriginCreated
}
if rule.Owner == "" {
rule.Owner = constant.FirewallRuleSourceUser
}
return nil
}
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
result := cloneUpdates(updates)
delete(result, "id")
delete(result, "uuid")
delete(result, "revision")
delete(result, "created_at")
return result
}
func cloneUpdates(updates map[string]interface{}) map[string]interface{} {
result := make(map[string]interface{}, len(updates)+1)
for key, value := range updates {
result[key] = value
}
return result
}
-36
View File
@@ -1,36 +0,0 @@
package repo
import (
"context"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global"
"gorm.io/gorm"
)
type IForwardingRuleRepo interface {
List(context.Context) ([]model.ForwardingRule, error)
ReplaceAll(context.Context, []model.ForwardingRule) error
}
type ForwardingRuleRepo struct{}
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
var rules []model.ForwardingRule
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
return rules, err
}
func (r *ForwardingRuleRepo) ReplaceAll(ctx context.Context, rules []model.ForwardingRule) error {
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
if err := tx.Session(&gorm.Session{AllowGlobalUpdate: true}).Delete(&model.ForwardingRule{}).Error; err != nil {
return err
}
if len(rules) == 0 {
return nil
}
return tx.Create(&rules).Error
})
}
+72
View File
@@ -22,6 +22,11 @@ type IHostRepo interface {
WithByPort(port uint) DBOption WithByPort(port uint) DBOption
WithByUser(user string) DBOption WithByUser(user string) DBOption
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
SaveFirewallRecord(firewall *model.Firewall) error
DeleteFirewallRecordByID(id uint) error
SyncCert(data []model.RootCert) error SyncCert(data []model.RootCert) error
GetCert(opts ...DBOption) (model.RootCert, error) GetCert(opts ...DBOption) (model.RootCert, error)
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error) PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
@@ -29,6 +34,8 @@ type IHostRepo interface {
SaveCert(cert *model.RootCert) error SaveCert(cert *model.RootCert) error
UpdateCert(id uint, vars map[string]interface{}) error UpdateCert(id uint, vars map[string]interface{}) error
DeleteCert(opts ...DBOption) error DeleteCert(opts ...DBOption) error
WithByChain(chain string) DBOption
} }
func NewIHostRepo() IHostRepo { func NewIHostRepo() IHostRepo {
@@ -109,6 +116,65 @@ func (h *HostRepo) Delete(opts ...DBOption) error {
return db.Delete(&model.Host{}).Error return db.Delete(&model.Host{}).Error
} }
func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
var firewall model.Firewall
db := global.DB
for _, opt := range opts {
db = opt(db)
}
err := db.First(&firewall).Error
return firewall, err
}
func (h *HostRepo) ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error) {
var firewalls []model.Firewall
db := global.DB
for _, opt := range opts {
db = opt(db)
}
if err := global.DB.Find(&firewalls).Error; err != nil {
return firewalls, nil
}
return firewalls, nil
}
func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
if firewall.ID != 0 {
return global.DB.Save(firewall).Error
}
var data model.Firewall
switch firewall.Type {
case "port":
_ = global.DB.Where("type = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND strategy = ?", "port",
firewall.DstPort,
firewall.Protocol,
firewall.SrcIP,
firewall.Strategy,
).First(&data).Error
case "ip":
_ = global.DB.Where("type = ? AND src_ip = ? AND strategy = ?", "address", firewall.SrcIP, firewall.Strategy).First(&data)
default:
_ = global.DB.Where("type = ? AND chain = ? AND src_port = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND dst_ip = ? AND strategy = ?",
firewall.Type,
firewall.Chain,
firewall.SrcPort,
firewall.DstPort,
firewall.Protocol,
firewall.SrcIP,
firewall.DstIP,
firewall.Strategy,
).First(&data).Error
}
if data.ID != 0 {
firewall.ID = data.ID
}
return global.DB.Save(firewall).Error
}
func (h *HostRepo) DeleteFirewallRecordByID(id uint) error {
return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error
}
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) { func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
var cert model.RootCert var cert model.RootCert
db := global.DB db := global.DB
@@ -187,3 +253,9 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
tx.Commit() tx.Commit()
return nil return nil
} }
func (u *HostRepo) WithByChain(chain string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("chain = ?", chain)
}
}
-3
View File
@@ -73,9 +73,6 @@ func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
return global.MonitorDB.Create(&model).Error return global.MonitorDB.Create(&model).Error
} }
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error { func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
if len(list) == 0 {
return nil
}
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
} }
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error { func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
-21
View File
@@ -17,12 +17,10 @@ type IWebsiteRepo interface {
WithGroupID(groupId uint) DBOption WithGroupID(groupId uint) DBOption
WithDefaultServer() DBOption WithDefaultServer() DBOption
WithDomainLike(domain string) DBOption WithDomainLike(domain string) DBOption
WithSearchKeyword(keyword string, ids []uint) DBOption
WithRuntimeID(runtimeID uint) DBOption WithRuntimeID(runtimeID uint) DBOption
WithParentID(websiteID uint) DBOption WithParentID(websiteID uint) DBOption
WithType(websiteType string) DBOption WithType(websiteType string) DBOption
WithDBType(dbType string) DBOption WithDBType(dbType string) DBOption
WithDBTypes(dbTypes []string) DBOption
WithDBID(dbID uint) DBOption WithDBID(dbID uint) DBOption
Page(page, size int, opts ...DBOption) (int64, []model.Website, error) Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
@@ -78,19 +76,6 @@ func (w *WebsiteRepo) WithDomainLike(domain string) DBOption {
} }
} }
func (w *WebsiteRepo) WithSearchKeyword(keyword string, ids []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
if keyword == "" {
return db
}
keyword = "%" + keyword + "%"
if len(ids) == 0 {
return db.Where("(primary_domain like ? OR alias like ?)", keyword, keyword)
}
return db.Where("(primary_domain like ? OR alias like ? OR id in (?))", keyword, keyword, ids)
}
}
func (w *WebsiteRepo) WithAlias(alias string) DBOption { func (w *WebsiteRepo) WithAlias(alias string) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("alias = ?", alias) return db.Where("alias = ?", alias)
@@ -133,12 +118,6 @@ func (w *WebsiteRepo) WithDBType(dbType string) DBOption {
} }
} }
func (w *WebsiteRepo) WithDBTypes(dbTypes []string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("db_type IN ?", dbTypes)
}
}
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption { func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("db_id = ?", dbID) return db.Where("db_id = ?", dbID)
-131
View File
@@ -1,131 +0,0 @@
package repo
import (
"github.com/1Panel-dev/1Panel/agent/app/model"
"gorm.io/gorm"
)
type IWebsiteTemplateRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error)
GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error)
List(opts ...DBOption) ([]model.WebsiteTemplate, error)
Create(template *model.WebsiteTemplate) error
Save(template *model.WebsiteTemplate) error
DeleteBy(opts ...DBOption) error
WithName(name string) DBOption
WithType(templateType string) DBOption
}
func NewIWebsiteTemplateRepo() IWebsiteTemplateRepo {
return &WebsiteTemplateRepo{}
}
type WebsiteTemplateRepo struct {
}
func (w *WebsiteTemplateRepo) WithName(name string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("name like ?", "%"+name+"%")
}
}
func (w *WebsiteTemplateRepo) WithType(templateType string) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("type = ?", templateType)
}
}
func (w *WebsiteTemplateRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplate, error) {
var templates []model.WebsiteTemplate
db := getDb(opts...).Model(&model.WebsiteTemplate{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&templates).Error
return count, templates, err
}
func (w *WebsiteTemplateRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplate, error) {
var template model.WebsiteTemplate
db := getDb(opts...).Model(&model.WebsiteTemplate{})
if err := db.First(&template).Error; err != nil {
return nil, err
}
return &template, nil
}
func (w *WebsiteTemplateRepo) List(opts ...DBOption) ([]model.WebsiteTemplate, error) {
var templates []model.WebsiteTemplate
err := getDb(opts...).Model(&model.WebsiteTemplate{}).Find(&templates).Error
return templates, err
}
func (w *WebsiteTemplateRepo) Create(template *model.WebsiteTemplate) error {
return getDb().Create(template).Error
}
func (w *WebsiteTemplateRepo) Save(template *model.WebsiteTemplate) error {
return getDb().Save(template).Error
}
func (w *WebsiteTemplateRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.WebsiteTemplate{}).Error
}
type IWebsiteTemplateOutputRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error)
GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error)
List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error)
Create(output *model.WebsiteTemplateOutput) error
Save(output *model.WebsiteTemplateOutput) error
DeleteBy(opts ...DBOption) error
WithByTemplateID(templateID uint) DBOption
}
func NewIWebsiteTemplateOutputRepo() IWebsiteTemplateOutputRepo {
return &WebsiteTemplateOutputRepo{}
}
type WebsiteTemplateOutputRepo struct {
}
func (w *WebsiteTemplateOutputRepo) WithByTemplateID(templateID uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
return db.Where("template_id = ?", templateID)
}
}
func (w *WebsiteTemplateOutputRepo) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteTemplateOutput, error) {
var outputs []model.WebsiteTemplateOutput
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&outputs).Error
return count, outputs, err
}
func (w *WebsiteTemplateOutputRepo) GetFirst(opts ...DBOption) (*model.WebsiteTemplateOutput, error) {
var output model.WebsiteTemplateOutput
db := getDb(opts...).Model(&model.WebsiteTemplateOutput{})
if err := db.First(&output).Error; err != nil {
return nil, err
}
return &output, nil
}
func (w *WebsiteTemplateOutputRepo) List(opts ...DBOption) ([]model.WebsiteTemplateOutput, error) {
var outputs []model.WebsiteTemplateOutput
err := getDb(opts...).Model(&model.WebsiteTemplateOutput{}).Find(&outputs).Error
return outputs, err
}
func (w *WebsiteTemplateOutputRepo) Create(output *model.WebsiteTemplateOutput) error {
return getDb().Create(output).Error
}
func (w *WebsiteTemplateOutputRepo) Save(output *model.WebsiteTemplateOutput) error {
return getDb().Save(output).Error
}
func (w *WebsiteTemplateOutputRepo) DeleteBy(opts ...DBOption) error {
return getDb(opts...).Delete(&model.WebsiteTemplateOutput{}).Error
}
+141 -280
View File
@@ -8,8 +8,8 @@ import (
"os" "os"
"path" "path"
"sort" "sort"
"strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -30,8 +30,6 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
) )
var agentAccountMutationMu sync.Mutex
type IAgentService interface { type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error) Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
@@ -79,7 +77,6 @@ type IAgentService interface {
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error CreateAccountModel(req dto.AgentAccountModelCreateReq) error
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
@@ -105,10 +102,6 @@ type IAgentService interface {
UpgradePlugin(req dto.AgentPluginUpgradeReq) error UpgradePlugin(req dto.AgentPluginUpgradeReq) error
UninstallPlugin(req dto.AgentPluginUninstallReq) error UninstallPlugin(req dto.AgentPluginUninstallReq) error
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
OperatePlugin(req dto.AgentPluginOperateReq) error
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
} }
@@ -151,7 +144,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
return nil, buserr.New("ErrNameIsExist") return nil, buserr.New("ErrNameIsExist")
} }
if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() { if !xpack.MultiNodeProvider.IsXpack() {
count, _, err := agentRepo.Page(1, 1) count, _, err := agentRepo.Page(1, 1)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -172,6 +165,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
provider := "" provider := ""
baseURL := "" baseURL := ""
apiType := "" apiType := ""
maxTokens := 0
contextWindow := 0
apiKey := "" apiKey := ""
runtimeModel := "" runtimeModel := ""
accountID := uint(0) accountID := uint(0)
@@ -181,7 +176,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
var allowedOrigins []string var allowedOrigins []string
var account *model.AgentAccount var account *model.AgentAccount
var installHooks *appInstallHooks var installHooks *appInstallHooks
var dashboardAuth agentDashboardAuth
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent { if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
if req.AccountID == 0 { if req.AccountID == 0 {
@@ -202,6 +196,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
} }
storedModel = resolvedRuntime.StoredModel storedModel = resolvedRuntime.StoredModel
apiType = resolvedRuntime.APIType apiType = resolvedRuntime.APIType
maxTokens = resolvedRuntime.MaxTokens
contextWindow = resolvedRuntime.ContextWindow
runtimeModel = resolvedRuntime.PrimaryModel runtimeModel = resolvedRuntime.PrimaryModel
apiKey = account.APIKey apiKey = account.APIKey
accountID = account.ID accountID = account.ID
@@ -226,17 +222,11 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}, },
} }
} else if agentType == constant.AppHermesAgent { } else if agentType == constant.AppHermesAgent {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
installHooks = &appInstallHooks{ installHooks = &appInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error { AfterCopyData: func(appInstall *model.AppInstall) error {
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil { return prepareHermesInstallFiles(appInstall, account, storedModel)
return err
}
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
}, },
} }
} else if agentType == constant.AppCopaw {
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
} }
params := map[string]interface{}{ params := map[string]interface{}{
@@ -252,17 +242,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
params["PROVIDER"] = provider params["PROVIDER"] = provider
params["MODEL"] = runtimeModel params["MODEL"] = runtimeModel
params["API_TYPE"] = apiType params["API_TYPE"] = apiType
params["MAX_TOKENS"] = maxTokens
params["CONTEXT_WINDOW"] = contextWindow
params["BASE_URL"] = baseURL params["BASE_URL"] = baseURL
params["API_KEY"] = apiKey params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token params["OPENCLAW_GATEWAY_TOKEN"] = token
} }
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
params[usernameKey] = dashboardAuth.Username
params[passwordKey] = dashboardAuth.Password
if agentType == constant.AppCopaw {
params[qwenPawAuthEnabledEnvKey] = "true"
}
}
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
return nil, buserr.New("ErrAgentComposeRequired") return nil, buserr.New("ErrAgentComposeRequired")
@@ -297,20 +282,22 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml") configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
} }
agent := &model.Agent{ agent := &model.Agent{
Name: req.Name, Name: req.Name,
Remark: req.Remark, Remark: req.Remark,
AgentType: agentType, AgentType: agentType,
Provider: provider, Provider: provider,
Model: storedModel, Model: storedModel,
APIType: apiType, APIType: apiType,
BaseURL: baseURL, MaxTokens: maxTokens,
APIKey: apiKey, ContextWindow: contextWindow,
Token: token, BaseURL: baseURL,
Status: appInstall.Status, APIKey: apiKey,
Message: appInstall.Message, Token: token,
AppInstallID: appInstall.ID, Status: appInstall.Status,
AccountID: accountID, Message: appInstall.Message,
ConfigPath: configPath, AppInstallID: appInstall.ID,
AccountID: accountID,
ConfigPath: configPath,
} }
if err := agentRepo.Create(agent); err != nil { if err := agentRepo.Create(agent); err != nil {
return nil, err return nil, err
@@ -408,7 +395,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt
results = append(results, result) results = append(results, result)
continue continue
} }
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID) installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID)
if err != nil { if err != nil {
result.Message = err.Error() result.Message = err.Error()
results = append(results, result) results = append(results, result)
@@ -450,7 +437,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if operate == constant.Delete { if operate == constant.Delete {
if err := a.Delete(dto.AgentDeleteReq{ if err := a.Delete(dto.AgentDeleteReq{
ID: agent.ID, ID: agent.ID,
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
ForceDelete: req.ForceDelete, ForceDelete: req.ForceDelete,
}); err != nil { }); err != nil {
result.Message = err.Error() result.Message = err.Error()
@@ -478,7 +465,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{ if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: operate, Operate: operate,
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID), TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
}); err != nil { }); err != nil {
result.Message = err.Error() result.Message = err.Error()
} else { } else {
@@ -562,19 +549,35 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [
DetailID: detail.ID, DetailID: detail.ID,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID), TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID),
}, },
}) })
} }
return plans, results, nil return plans, results, nil
} }
func buildBatchTaskID(taskID, prefix string, id uint) string { func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string {
taskID = strings.TrimSpace(taskID) taskID = strings.TrimSpace(taskID)
if taskID == "" { if taskID == "" {
taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano()) taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano())
} }
return fmt.Sprintf("%s-%d", taskID, id) return fmt.Sprintf("%s-%d", taskID, appInstallID)
}
func buildBatchSkillInstallTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func buildBatchOperateTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
} }
func batchOperateSkipMessage(operate constant.AppOperate, status string) string { func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
@@ -601,30 +604,28 @@ func batchOperateSkipMessage(operate constant.AppOperate, status string) string
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq { func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
return dto.AgentCreateReq{ return dto.AgentCreateReq{
Name: req.Name, Name: req.Name,
Remark: req.Remark, Remark: req.Remark,
AppVersion: req.AppVersion, AppVersion: req.AppVersion,
WebUIPort: req.WebUIPort, WebUIPort: req.WebUIPort,
BridgePort: req.BridgePort, BridgePort: req.BridgePort,
AllowedOrigins: req.AllowedOrigins, AllowedOrigins: req.AllowedOrigins,
AgentType: req.AgentType, AgentType: req.AgentType,
Model: req.Model, Model: req.Model,
AccountID: req.AccountID, AccountID: req.AccountID,
Token: req.Token, Token: req.Token,
DashboardUsername: req.DashboardUsername, TaskID: req.TaskID,
DashboardPassword: req.DashboardPassword, Advanced: req.Advanced,
TaskID: req.TaskID, ContainerName: req.ContainerName,
Advanced: req.Advanced, AllowPort: req.AllowPort,
ContainerName: req.ContainerName, SpecifyIP: req.SpecifyIP,
AllowPort: req.AllowPort, RestartPolicy: req.RestartPolicy,
SpecifyIP: req.SpecifyIP, CpuQuota: req.CpuQuota,
RestartPolicy: req.RestartPolicy, MemoryLimit: req.MemoryLimit,
CpuQuota: req.CpuQuota, MemoryUnit: req.MemoryUnit,
MemoryLimit: req.MemoryLimit, PullImage: req.PullImage,
MemoryUnit: req.MemoryUnit, EditCompose: req.EditCompose,
PullImage: req.PullImage, DockerCompose: req.DockerCompose,
EditCompose: req.EditCompose,
DockerCompose: req.DockerCompose,
} }
} }
@@ -703,7 +704,6 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
account.RememberAPIKey = snapshot.RememberAPIKey account.RememberAPIKey = snapshot.RememberAPIKey
account.BaseURL = snapshot.BaseURL account.BaseURL = snapshot.BaseURL
account.APIType = snapshot.APIType account.APIType = snapshot.APIType
account.AuthMode = snapshot.AuthMode
account.Remark = snapshot.Remark account.Remark = snapshot.Remark
account.Verified = true account.Verified = true
@@ -711,11 +711,6 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
if err != nil { if err != nil {
return 0, err return 0, err
} }
verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels)
if err != nil {
return 0, err
}
account.VerifyModel = verifyModel
if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := global.DB.Transaction(func(tx *gorm.DB) error {
if account.ID == 0 { if account.ID == 0 {
if err := tx.Create(account).Error; err != nil { if err := tx.Create(account).Error; err != nil {
@@ -953,7 +948,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
return err return err
} }
modelName := resolvedRuntime.StoredModel modelName := resolvedRuntime.StoredModel
apiType := resolvedRuntime.APIType apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath) confDir := path.Dir(agent.ConfigPath)
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
cfg, err := readHermesConfig(agent.ConfigPath) cfg, err := readHermesConfig(agent.ConfigPath)
@@ -974,6 +969,8 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
agent.Provider = account.Provider agent.Provider = account.Provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
agent.BaseURL = account.BaseURL agent.BaseURL = account.BaseURL
agent.APIKey = account.APIKey agent.APIKey = account.APIKey
agent.AccountID = account.ID agent.AccountID = account.ID
@@ -987,38 +984,20 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
models := make([]dto.ProviderModelInfo, 0, len(def.Models)) models := make([]dto.ProviderModelInfo, 0, len(def.Models))
for _, item := range def.Models { for _, item := range def.Models {
models = append(models, dto.ProviderModelInfo{ models = append(models, dto.ProviderModelInfo{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
for _, item := range def.APIConfigs {
apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models))
for _, model := range item.Models {
apiModels = append(apiModels, dto.ProviderModelInfo{
ID: model.ID,
Name: model.Name,
})
}
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
APIType: item.APIType,
BaseURL: item.BaseURL,
EditableBaseURL: item.EditableBaseURL,
SupportsModelDiscovery: item.DiscoverModels,
DefaultAuthMode: item.DefaultAuthMode,
AuthModes: item.AuthModes,
Models: apiModels,
})
}
baseURL, _ := providercatalog.DefaultBaseURL(key)
providers = append(providers, dto.ProviderInfo{ providers = append(providers, dto.ProviderInfo{
Sort: def.Sort, Sort: def.Sort,
Provider: key, Provider: key,
DisplayName: localizedAgentProviderName(key), DisplayName: def.DisplayName,
BaseURL: baseURL, BaseURL: def.DefaultBaseURL,
DefaultAPIType: def.DefaultAPIType, Models: models,
APITypes: apiTypes,
Models: models,
}) })
} }
sort.Slice(providers, func(i, j int) bool { sort.Slice(providers, func(i, j int) bool {
@@ -1028,22 +1007,11 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
} }
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
provider := req.Provider provider := req.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil { if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
return err return buserr.New("ErrRecordExist")
} }
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models) resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
if err != nil {
return err
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
if err != nil {
return err
}
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
if err != nil { if err != nil {
return err return err
} }
@@ -1053,12 +1021,14 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
APIKey: resolvedInput.APIKey, APIKey: resolvedInput.APIKey,
RememberAPIKey: req.RememberAPIKey, RememberAPIKey: req.RememberAPIKey,
BaseURL: resolvedInput.BaseURL, BaseURL: resolvedInput.BaseURL,
APIType: resolvedInput.APIType, APIType: req.APIType,
AuthMode: resolvedInput.AuthMode,
VerifyModel: verifyModel,
Verified: true, Verified: true,
Remark: req.Remark, Remark: req.Remark,
} }
initialModels, err := buildInitialAgentAccountModels(account, req.Models)
if err != nil {
return err
}
if err := global.DB.Transaction(func(tx *gorm.DB) error { if err := global.DB.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(account).Error; err != nil { if err := tx.Create(account).Error; err != nil {
return err return err
@@ -1075,33 +1045,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
} }
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
agentAccountMutationMu.Lock()
defer agentAccountMutationMu.Unlock()
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
if err != nil { if err != nil {
return err return err
} }
if req.APIType != account.APIType {
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
}
provider := account.Provider provider := account.Provider
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil { resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
return err
}
models, err := loadAgentAccountModels(account)
if err != nil {
return err
}
requestedVerifyModel := req.VerifyModel
if strings.TrimSpace(requestedVerifyModel) == "" {
requestedVerifyModel = account.VerifyModel
}
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
if err != nil {
return err
}
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
if err != nil { if err != nil {
return err return err
} }
@@ -1109,9 +1058,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
account.APIKey = resolvedInput.APIKey account.APIKey = resolvedInput.APIKey
account.RememberAPIKey = req.RememberAPIKey account.RememberAPIKey = req.RememberAPIKey
account.BaseURL = resolvedInput.BaseURL account.BaseURL = resolvedInput.BaseURL
account.APIType = resolvedInput.APIType account.APIType = req.APIType
account.AuthMode = resolvedInput.AuthMode
account.VerifyModel = verifyModel
account.Remark = req.Remark account.Remark = req.Remark
account.Verified = true account.Verified = true
@@ -1133,12 +1080,6 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
if strings.TrimSpace(req.Provider) != "" { if strings.TrimSpace(req.Provider) != "" {
opts = append(opts, repo.WithByProvider(req.Provider)) opts = append(opts, repo.WithByProvider(req.Provider))
} }
if apiType := strings.TrimSpace(req.APIType); apiType != "" {
opts = append(opts, repo.WithByAPIType(apiType))
}
if req.TextOnly {
opts = append(opts, repo.WithTextAPIType())
}
if strings.TrimSpace(req.Name) != "" { if strings.TrimSpace(req.Name) != "" {
opts = append(opts, repo.WithByLikeName(req.Name)) opts = append(opts, repo.WithByLikeName(req.Name))
} }
@@ -1156,46 +1097,30 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
ID: item.ID, ID: item.ID,
MasterAccountID: item.MasterAccountID, MasterAccountID: item.MasterAccountID,
Provider: item.Provider, Provider: item.Provider,
ProviderName: localizedAgentProviderName(item.Provider), ProviderName: providercatalog.DisplayName(item.Provider),
Name: item.Name, Name: item.Name,
APIKey: apiKey, APIKey: apiKey,
RememberAPIKey: item.RememberAPIKey, RememberAPIKey: item.RememberAPIKey,
BaseURL: item.BaseURL, BaseURL: item.BaseURL,
Models: nil, Models: nil,
APIType: item.APIType, APIType: item.APIType,
AuthMode: item.AuthMode,
VerifyModel: item.VerifyModel,
Verified: item.Verified, Verified: item.Verified,
Remark: item.Remark, Remark: item.Remark,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
}) })
} }
if len(list) > 0 { for i := range items {
accountIDs := make([]uint, 0, len(list)) models, err := loadAgentAccountModels(&list[i])
for _, account := range list { if err != nil {
accountIDs = append(accountIDs, account.ID)
}
var rows []model.AgentAccountModel
if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil {
return 0, nil, err return 0, nil, err
} }
modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list)) items[i].Models = models
for _, row := range rows {
modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{
RecordID: row.ID,
ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name),
})
}
for index, account := range list {
items[index].Models = modelsByAccount[account.ID]
}
} }
return count, items, nil return count, items, nil
} }
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) { func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
return agentAccountRepo.CountTextByProviders(req.Providers) return agentAccountRepo.CountByProviders(req.Providers)
} }
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) { func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
@@ -1206,22 +1131,6 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
return loadAgentAccountModels(account) return loadAgentAccountModels(account)
} }
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType)
if !ok || !config.DiscoverModels {
return nil, buserr.New("ErrAgentAccountModelsRequired")
}
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
models, err := providercatalog.DiscoverModels(baseURL, req.APIKey)
if err != nil {
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
}
return buildDiscoveredAgentAccountModels(models), nil
}
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error { func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil { if err != nil {
@@ -1231,19 +1140,24 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
if err != nil { if err != nil {
return err return err
} }
nextModel, err := normalizeAgentAccountModel(account, req.Model) nextModel := cloneAgentAccountModel(req.Model)
if err != nil {
return err
}
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
return buserr.New("ErrRecordExist") return buserr.New("ErrRecordExist")
} }
inputPayload, err := json.Marshal(nextModel.Input)
if err != nil {
return err
}
sortOrder := len(models) + 1 sortOrder := len(models) + 1
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: account.ID, AccountID: account.ID,
Model: nextModel.ID, Model: nextModel.ID,
Name: nextModel.Name, Name: nextModel.Name,
SortOrder: sortOrder, ContextWindow: nextModel.ContextWindow,
MaxTokens: nextModel.MaxTokens,
Reasoning: nextModel.Reasoning,
Input: string(inputPayload),
SortOrder: sortOrder,
} }
if err := agentAccountModelRepo.Create(record); err != nil { if err := agentAccountModelRepo.Create(record); err != nil {
return err return err
@@ -1264,11 +1178,7 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err != nil { if err != nil {
return err return err
} }
nextModel, err := normalizeAgentAccountModel(account, req.Model) nextModel := cloneAgentAccountModel(req.Model)
if err != nil {
return err
}
nextModel.RecordID = req.Model.RecordID
for _, item := range models { for _, item := range models {
if item.RecordID == req.Model.RecordID { if item.RecordID == req.Model.RecordID {
continue continue
@@ -1288,17 +1198,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
if err := ensureAccountModelsNotBound(account, nextModels); err != nil { if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
return err return err
} }
previousModelID := record.Model inputPayload, err := json.Marshal(nextModel.Input)
record.Model = nextModel.ID if err != nil {
record.Name = nextModel.Name
if err := agentAccountModelRepo.Save(record); err != nil {
return err return err
} }
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) { record.Model = nextModel.ID
account.VerifyModel = nextModel.ID record.Name = nextModel.Name
if err := agentAccountRepo.Save(account); err != nil { record.ContextWindow = nextModel.ContextWindow
return err record.MaxTokens = nextModel.MaxTokens
} record.Reasoning = nextModel.Reasoning
record.Input = string(inputPayload)
if err := agentAccountModelRepo.Save(record); err != nil {
return err
} }
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache() terminalai.InvalidateFileAIRuntimeCache()
@@ -1310,13 +1221,9 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
if err != nil { if err != nil {
return err return err
} }
record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)) if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil {
if err != nil {
return err return err
} }
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
return buserr.New("ErrAgentVerifyModelInUse")
}
models, err := loadAgentAccountModels(account) models, err := loadAgentAccountModels(account)
if err != nil { if err != nil {
return err return err
@@ -1350,45 +1257,25 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
} }
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true) _, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL)
return err return err
} }
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
agentAccountMutationMu.Lock() if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
defer agentAccountMutationMu.Unlock() return buserr.New("ErrAgentAccountBound")
if err := global.DB.Transaction(func(tx *gorm.DB) error { }
var agentCount int64 if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) {
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil { if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) {
return err
}
if agentCount > 0 {
return buserr.New("ErrAgentAccountBound")
}
used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID")
if err != nil {
return err
}
if used {
return buserr.New("ErrTerminalAIAccountInUse") return buserr.New("ErrTerminalAIAccountInUse")
} }
used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID") }
if err != nil { if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil {
return err
}
if used {
return buserr.New("ErrFileAIAccountInUse")
}
if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
return err
}
return tx.Delete(&model.AgentAccount{}, req.ID).Error
}); err != nil {
return err return err
} }
terminalai.InvalidateTerminalRuntimeCache() terminalai.InvalidateTerminalRuntimeCache()
terminalai.InvalidateFileAIRuntimeCache() terminalai.InvalidateFileAIRuntimeCache()
return nil return agentAccountRepo.DeleteByID(req.ID)
} }
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) { func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
@@ -1440,20 +1327,10 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
if err != nil { if err != nil {
return nil, err return nil, err
} }
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
return &dto.AgentOtherConfig{ return &dto.AgentOtherConfig{
UserTimezone: cfg.Timezone, UserTimezone: cfg.Timezone,
BrowserEnabled: true, BrowserEnabled: true,
NPMRegistry: "https://registry.npmjs.org/", NPMRegistry: "https://registry.npmjs.org/",
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
}, nil
}
if agent.AgentType == constant.AppCopaw {
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
return &dto.AgentOtherConfig{
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
}, nil }, nil
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -1474,36 +1351,18 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
return err return err
} }
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
if strings.TrimSpace(req.UserTimezone) == "" {
return buserr.New("ErrInvalidParams")
}
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
if err != nil { if err != nil {
return err return err
} }
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil { if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
return err return err
} }
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
return err
}
operate := constant.Restart
if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
operate = constant.Rebuild
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{ return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: operate, Operate: constant.Restart,
}) })
} }
if agent.AgentType == constant.AppCopaw {
return updateQwenPawDashboardAuth(install, normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword))
}
if strings.TrimSpace(req.UserTimezone) == "" || strings.TrimSpace(req.NPMRegistry) == "" {
return buserr.New("ErrInvalidParams")
}
if err := ensureContainerRunning(install.ContainerName); err != nil { if err := ensureContainerRunning(install.ContainerName); err != nil {
return err return err
} }
@@ -1675,7 +1534,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
return err return err
} }
modelName := resolvedRuntime.StoredModel modelName := resolvedRuntime.StoredModel
apiType := resolvedRuntime.APIType apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
confDir := path.Dir(agent.ConfigPath) confDir := path.Dir(agent.ConfigPath)
switch agent.AgentType { switch agent.AgentType {
case constant.AppOpenclaw: case constant.AppOpenclaw:
@@ -1703,6 +1562,8 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
agent.Provider = account.Provider agent.Provider = account.Provider
agent.Model = modelName agent.Model = modelName
agent.APIType = apiType agent.APIType = apiType
agent.MaxTokens = maxTokens
agent.ContextWindow = contextWindow
_ = agentRepo.Save(&agent) _ = agentRepo.Save(&agent)
} }
return nil return nil
+7 -41
View File
@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"os" "os"
"path" "path"
"slices"
"sort" "sort"
"strings" "strings"
"time" "time"
@@ -812,7 +811,9 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
result.RequireMention = result.GroupPolicy == "allowlist" result.RequireMention = result.GroupPolicy == "allowlist"
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"]) result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
result.Proxy = extractStringValue(telegram["proxy"]) result.Proxy = extractStringValue(telegram["proxy"])
result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming) if streaming := extractStringValue(telegram["streaming"]); streaming != "" {
result.Streaming = streaming
}
accounts := childMap(telegram, "accounts") accounts := childMap(telegram, "accounts")
if len(accounts) == 0 { if len(accounts) == 0 {
botToken := extractStringValue(telegram["botToken"]) botToken := extractStringValue(telegram["botToken"])
@@ -838,7 +839,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
BotToken: extractStringValue(account["botToken"]), BotToken: extractStringValue(account["botToken"]),
DmPolicy: extractStringValue(account["dmPolicy"]), DmPolicy: extractStringValue(account["dmPolicy"]),
GroupPolicy: extractStringValue(account["groupPolicy"]), GroupPolicy: extractStringValue(account["groupPolicy"]),
Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming), Streaming: extractStringValue(account["streaming"]),
}) })
} }
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots)) result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
@@ -878,7 +879,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
} else { } else {
delete(telegram, "proxy") delete(telegram, "proxy")
} }
telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming) telegram["streaming"] = config.Streaming
accounts := make(map[string]interface{}, len(config.Bots)) accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots { for _, bot := range config.Bots {
account := map[string]interface{}{ account := map[string]interface{}{
@@ -887,7 +888,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
"botToken": bot.BotToken, "botToken": bot.BotToken,
"dmPolicy": bot.DmPolicy, "dmPolicy": bot.DmPolicy,
"groupPolicy": bot.GroupPolicy, "groupPolicy": bot.GroupPolicy,
"streaming": buildTelegramStreamingConfig(bot.Streaming), "streaming": bot.Streaming,
} }
if bot.DmPolicy == "open" { if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"} account["allowFrom"] = []string{"*"}
@@ -898,29 +899,6 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
delete(telegram, "botToken") delete(telegram, "botToken")
} }
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
mode := defaultMode
switch typed := value.(type) {
case string:
mode = typed
case map[string]interface{}:
mode = extractStringValue(typed["mode"])
}
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "off", "partial", "block", "progress":
return mode
default:
return "partial"
}
}
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
return map[string]interface{}{
"mode": normalizeTelegramStreamingMode(mode, "partial"),
}
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"} result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
discord := getChannelConfig(conf, "discord") discord := getChannelConfig(conf, "discord")
@@ -1321,10 +1299,6 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
} }
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error { func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
help, err := cmd.RunDockerExecWithStdout(time.Minute, containerName, "openclaw", "plugins", "install", "--help")
if err != nil {
return err
}
workdir := path.Join(openclawPluginPackageTmpDir, pluginID) workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
defer func() { defer func() {
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir) _ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
@@ -1346,15 +1320,7 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
if pkgPath == "" { if pkgPath == "" {
return fmt.Errorf("openclaw plugin package not found") return fmt.Errorf("openclaw plugin package not found")
} }
args := []string{"exec", containerName, "openclaw", "plugins", "install", pkgPath} return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
// Newer CLIs require source confirmation; older releases do not support --force.
options := strings.Fields(help)
if slices.Contains(options, "--force") {
args = append(args, "--force")
} else if slices.Contains(options, "--dangerously-force-unsafe-install") {
args = append(args, "--dangerously-force-unsafe-install")
}
return mgr.Run("docker", args...)
} }
func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error { func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
-128
View File
@@ -1,128 +0,0 @@
package service
import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
)
type qwenPawAuthStatus struct {
Enabled bool `json:"enabled"`
HasUsers bool `json:"has_users"`
}
type qwenPawLoginResponse struct {
Token string `json:"token"`
}
func updateQwenPawDashboardAuth(install *model.AppInstall, next agentDashboardAuth) error {
if install == nil || install.ID == 0 {
return buserr.New("ErrRecordNotFound")
}
current, err := readAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw)
if err != nil {
return err
}
if current == next {
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return err
}
baseURL := fmt.Sprintf("http://127.0.0.1:%d/api/auth", install.HttpPort)
var status qwenPawAuthStatus
if _, err := requestQwenPawAuth(http.MethodGet, baseURL+"/status", nil, "", &status); err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
if !status.Enabled {
return buserr.New("ErrQwenPawAuthDisabled")
}
if !status.HasUsers {
payload := map[string]string{"username": next.Username, "password": next.Password}
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/register", payload, "", nil); err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
} else {
var login qwenPawLoginResponse
payload := map[string]string{"username": current.Username, "password": current.Password}
statusCode, err := requestQwenPawAuth(http.MethodPost, baseURL+"/login", payload, "", &login)
if statusCode == http.StatusUnauthorized {
return buserr.New("ErrQwenPawAuthOutOfSync")
}
if err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
payload = map[string]string{"current_password": current.Password}
if current.Username != next.Username {
payload["new_username"] = next.Username
}
if current.Password != next.Password {
payload["new_password"] = next.Password
}
if _, err := requestQwenPawAuth(http.MethodPost, baseURL+"/update-profile", payload, login.Token, nil); err != nil {
return buserr.WithMap("ErrQwenPawAuthRequest", map[string]interface{}{"err": err.Error()}, err)
}
}
return writeAgentDashboardAuthEnv(install.GetEnvPath(), constant.AppCopaw, next, true)
}
func requestQwenPawAuth(method, reqURL string, payload interface{}, token string, result interface{}) (int, error) {
var body io.Reader
if payload != nil {
data, err := json.Marshal(payload)
if err != nil {
return 0, err
}
body = bytes.NewReader(data)
}
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
req, err := http.NewRequestWithContext(ctx, method, reqURL, body)
if err != nil {
return 0, err
}
req.Header.Set("Content-Type", "application/json")
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := (&http.Client{Timeout: 10 * time.Second}).Do(req)
if err != nil {
return 0, err
}
defer resp.Body.Close()
data, err := io.ReadAll(io.LimitReader(resp.Body, 1<<20))
if err != nil {
return resp.StatusCode, err
}
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
detail := strings.TrimSpace(string(data))
var errorResponse struct {
Detail string `json:"detail"`
}
if json.Unmarshal(data, &errorResponse) == nil && strings.TrimSpace(errorResponse.Detail) != "" {
detail = strings.TrimSpace(errorResponse.Detail)
}
if detail == "" {
detail = resp.Status
}
return resp.StatusCode, errors.New(detail)
}
if result != nil && len(data) > 0 {
if err := json.Unmarshal(data, result); err != nil {
return resp.StatusCode, err
}
}
return resp.StatusCode, nil
}
+68 -31
View File
@@ -13,7 +13,9 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
@@ -69,7 +71,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
} }
} }
provider := resolveHermesProvider(account.Provider, account.APIType) provider := resolveHermesProvider(account.Provider)
configPath := path.Join(confDir, "config.yaml") configPath := path.Join(confDir, "config.yaml")
cfg, err := readHermesConfigMap(configPath) cfg, err := readHermesConfigMap(configPath)
if err != nil { if err != nil {
@@ -150,7 +152,7 @@ func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
} }
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) { func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -207,7 +209,7 @@ func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig,
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error { func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -227,7 +229,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
envMap["TELEGRAM_ALLOWED_USERS"] = allow envMap["TELEGRAM_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN",
"TELEGRAM_ALLOWED_USERS", "TELEGRAM_ALLOWED_USERS",
"TELEGRAM_ALLOW_ALL_USERS", "TELEGRAM_ALLOW_ALL_USERS",
@@ -246,7 +248,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
} }
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) { func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -298,7 +300,7 @@ func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, er
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error { func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -318,7 +320,7 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
envMap["DISCORD_ALLOWED_USERS"] = allow envMap["DISCORD_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"DISCORD_BOT_TOKEN", "DISCORD_BOT_TOKEN",
"DISCORD_ALLOWED_USERS", "DISCORD_ALLOWED_USERS",
"DISCORD_ALLOW_ALL_USERS", "DISCORD_ALLOW_ALL_USERS",
@@ -338,14 +340,14 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
func deleteHermesEnvKeys(confDir string, keys ...string) error { func deleteHermesEnvKeys(confDir string, keys ...string) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
for _, key := range keys { for _, key := range keys {
delete(envMap, key) delete(envMap, key)
} }
return writeAgentEnvMap(envPath, envMap, keys) return writeHermesEnvMap(envPath, envMap, keys)
} }
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error { func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
@@ -405,18 +407,11 @@ func normalizeHermesTimezone(timezone string) string {
return timezone return timezone
} }
func resolveHermesProvider(provider, apiType string) string { func resolveHermesProvider(provider string) string {
if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") {
return "anthropic"
}
switch provider { switch provider {
case "": case "":
return "custom" return "custom"
case "moonshot": case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
return "kimi-coding"
case "kimi":
return "kimi-coding-cn"
case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi":
return provider return provider
case "minimax": case "minimax":
return "minimax-cn" return "minimax-cn"
@@ -433,7 +428,17 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
if targetProvider != "custom" { if targetProvider != "custom" {
return target return target
} }
return providercatalog.NormalizeModelID(sourceProvider, target) if sourceProvider == "custom" || sourceProvider == "vllm" {
return normalizeCustomModel(target)
}
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
model := strings.TrimSpace(parts[1])
if model != "" {
return model
}
}
return target
} }
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) { func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
@@ -444,7 +449,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
if configuredModel == "" { if configuredModel == "" {
return "", buserr.New("ErrAgentModelNotInAccount") return "", buserr.New("ErrAgentModelNotInAccount")
} }
provider := resolveHermesProvider(account.Provider, account.APIType) provider := resolveHermesProvider(account.Provider)
for _, item := range accountModels { for _, item := range accountModels {
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel { if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
return item.ID, nil return item.ID, nil
@@ -457,8 +462,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil { if account == nil {
return nil return nil
} }
resolvedProvider := resolveHermesProvider(account.Provider, account.APIType) if resolveHermesProvider(account.Provider) == "custom" {
if resolvedProvider == "custom" {
if account.APIKey == "" { if account.APIKey == "" {
return nil return nil
} }
@@ -476,11 +480,6 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
} }
entries = append(entries, hermesEnvEntry{Key: key, Value: value}) entries = append(entries, hermesEnvEntry{Key: key, Value: value})
} }
if resolvedProvider == "anthropic" {
appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
return entries
}
switch account.Provider { switch account.Provider {
case "openrouter": case "openrouter":
@@ -488,7 +487,6 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
appendEntry("OPENROUTER_BASE_URL", baseURL) appendEntry("OPENROUTER_BASE_URL", baseURL)
case "anthropic": case "anthropic":
appendEntry("ANTHROPIC_API_KEY", apiKey) appendEntry("ANTHROPIC_API_KEY", apiKey)
appendEntry("ANTHROPIC_BASE_URL", baseURL)
case "gemini": case "gemini":
appendEntry("GOOGLE_API_KEY", apiKey) appendEntry("GOOGLE_API_KEY", apiKey)
appendEntry("GEMINI_API_KEY", apiKey) appendEntry("GEMINI_API_KEY", apiKey)
@@ -534,7 +532,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
} }
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error { func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -550,7 +548,7 @@ func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap[entry.Key] = entry.Value envMap[entry.Key] = entry.Value
order = append(order, entry.Key) order = append(order, entry.Key)
} }
return writeAgentEnvMap(envPath, envMap, order) return writeHermesEnvMap(envPath, envMap, order)
} }
func hermesManagedModelEnvKeys() []string { func hermesManagedModelEnvKeys() []string {
@@ -558,7 +556,6 @@ func hermesManagedModelEnvKeys() []string {
"OPENROUTER_API_KEY", "OPENROUTER_API_KEY",
"OPENROUTER_BASE_URL", "OPENROUTER_BASE_URL",
"ANTHROPIC_API_KEY", "ANTHROPIC_API_KEY",
"ANTHROPIC_BASE_URL",
"GOOGLE_API_KEY", "GOOGLE_API_KEY",
"GEMINI_API_KEY", "GEMINI_API_KEY",
"GEMINI_BASE_URL", "GEMINI_BASE_URL",
@@ -601,6 +598,46 @@ func hermesManagedModelEnvKeys() []string {
return result return result
} }
func writeHermesEnv(envPath string, entries []hermesEnvEntry) error {
envMap, err := readHermesEnvMap(envPath)
if err != nil {
return err
}
for _, entry := range entries {
if entry.Key == "" || entry.Value == "" {
continue
}
envMap[entry.Key] = entry.Value
}
order := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.Key == "" {
continue
}
order = append(order, entry.Key)
}
return writeHermesEnvMap(envPath, envMap, order)
}
func readHermesEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeHermesEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func splitHermesEnvList(value string) []string { func splitHermesEnvList(value string) []string {
if value == "" { if value == "" {
return []string{} return []string{}
+13 -13
View File
@@ -34,7 +34,7 @@ print('Restart the Hermes-Agent container to apply the new Weixin settings.')
` `
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) { func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -78,7 +78,7 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error { func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +102,7 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
envMap["QQ_GROUP_ALLOWED_USERS"] = allow envMap["QQ_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"QQ_APP_ID", "QQ_APP_ID",
"QQ_CLIENT_SECRET", "QQ_CLIENT_SECRET",
"QQ_ALLOW_ALL_USERS", "QQ_ALLOW_ALL_USERS",
@@ -163,7 +163,7 @@ func deleteHermesQQBotChannelConfig(confDir string) error {
} }
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) { func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -194,7 +194,7 @@ func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error)
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error { func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +229,7 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"WECOM_BOT_ID", "WECOM_BOT_ID",
"WECOM_SECRET", "WECOM_SECRET",
"WECOM_DM_POLICY", "WECOM_DM_POLICY",
@@ -282,7 +282,7 @@ func deleteHermesWecomChannelConfig(confDir string) error {
} }
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) { func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -332,7 +332,7 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error { func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -356,7 +356,7 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
} else if config.DmPolicy == "open" { } else if config.DmPolicy == "open" {
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true" envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET", "DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS", "DINGTALK_ALLOW_ALL_USERS",
@@ -398,7 +398,7 @@ func deleteHermesDingTalkChannelConfig(confDir string) error {
} }
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) { func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -452,7 +452,7 @@ func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, erro
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error { func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readAgentEnvMap(envPath) envMap, err := readHermesEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -488,7 +488,7 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
envMap["FEISHU_ALLOWED_USERS"] = allow envMap["FEISHU_ALLOWED_USERS"] = allow
} }
} }
if err := writeAgentEnvMap(envPath, envMap, []string{ if err := writeHermesEnvMap(envPath, envMap, []string{
"FEISHU_APP_ID", "FEISHU_APP_ID",
"FEISHU_APP_SECRET", "FEISHU_APP_SECRET",
"FEISHU_DOMAIN", "FEISHU_DOMAIN",
@@ -529,7 +529,7 @@ func deleteHermesFeishuChannelConfig(confDir string) error {
} }
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) { func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
envMap, err := readAgentEnvMap(path.Join(confDir, ".env")) envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
-315
View File
@@ -1,315 +0,0 @@
package service
import (
"database/sql"
"encoding/json"
"fmt"
"path/filepath"
"regexp"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
)
var (
openclawPluginPackagePattern = regexp.MustCompile(`^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$`)
openclawPluginVersionPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z._-]*$`)
openclawPluginIDPattern = regexp.MustCompile(`^(@[A-Za-z0-9][A-Za-z0-9._-]*/)?[A-Za-z0-9][A-Za-z0-9._-]*$`)
)
type openclawPluginListOutput struct {
Plugins []struct {
ID string `json:"id"`
Name string `json:"name"`
Version string `json:"version"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
} `json:"plugins"`
}
type openclawPluginIndexItem struct {
PluginID string `json:"pluginId"`
PackageName string `json:"packageName"`
PackageVersion string `json:"packageVersion"`
Origin string `json:"origin"`
Enabled bool `json:"enabled"`
}
type openclawPluginSearchOutput struct {
Results []struct {
Score float64 `json:"score"`
Package struct {
Name string `json:"name"`
RuntimeID string `json:"runtimeId"`
DisplayName string `json:"displayName"`
Summary string `json:"summary"`
LatestVersion string `json:"latestVersion"`
Categories []string `json:"categories"`
Channel string `json:"channel"`
IsOfficial bool `json:"isOfficial"`
VerificationTier string `json:"verificationTier"`
Stats struct {
Downloads int64 `json:"downloads"`
} `json:"stats"`
} `json:"package"`
} `json:"results"`
}
func (a AgentService) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) {
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if plugins, err := readOpenclawPluginIndex(filepath.Join(filepath.Dir(agent.ConfigPath), "state", "openclaw.sqlite")); err == nil {
return plugins, nil
}
output, err := cmd.RunDockerExecWithStdout(2*time.Minute, install.ContainerName, "openclaw", "plugins", "list", "--json")
if err != nil {
return nil, err
}
return parseOpenclawPluginList([]byte(output))
}
func (a AgentService) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) {
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
limit := req.Limit
if limit == 0 {
limit = 20
}
output, err := cmd.RunDockerExecWithStdout(
2*time.Minute,
install.ContainerName,
"openclaw", "plugins", "search", strings.TrimSpace(req.Keyword), "--limit", fmt.Sprint(limit), "--json",
)
if err != nil {
return nil, err
}
return parseOpenclawPluginSearch([]byte(output))
}
func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error {
spec, err := buildOpenclawPluginInstallSpec(req.Package, req.Version)
if err != nil {
return err
}
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey("AgentPluginInstall"), req.Package)
installTask, err := task.NewTask(taskName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask(taskName, func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", "install", spec)
}, nil)
addOpenclawPluginRestartTask(installTask, install)
go executeAgentPluginTask(installTask)
return nil
}
func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
if !openclawPluginIDPattern.MatchString(req.PluginID) {
return buserr.New("ErrInvalidChar")
}
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
return err
}
if req.Operate == "update" || req.Operate == "uninstall" {
plugins, err := a.ListPlugins(dto.AgentPluginsReq{AgentID: req.AgentID})
if err != nil {
return err
}
for _, plugin := range plugins {
if plugin.ID == req.PluginID && plugin.Origin == "bundled" {
return buserr.WithName("ErrNotSupportType", req.Operate)
}
}
}
taskType := map[string]string{
"enable": task.TaskUpdate,
"disable": task.TaskUpdate,
"update": task.TaskUpgrade,
"uninstall": task.TaskUninstall,
}[req.Operate]
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey(map[string]string{
"enable": "AgentPluginEnable",
"disable": "AgentPluginDisable",
"update": "AgentPluginUpdate",
"uninstall": "AgentPluginUninstall",
}[req.Operate]), req.PluginID)
operateTask, err := task.NewTask(taskName, taskType, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
operateTask.AddSubTask(taskName, func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Operate == "uninstall" {
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, req.PluginID); err != nil {
return err
}
return cleanupManagedOpenclawPlugin(agent, req.PluginID)
}
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", req.Operate, req.PluginID)
}, nil)
addOpenclawPluginRestartTask(operateTask, install)
go executeAgentPluginTask(operateTask)
return nil
}
func parseOpenclawPluginList(raw []byte) ([]dto.AgentPluginItem, error) {
payload, err := extractEmbeddedJSON(string(raw))
if err != nil {
return nil, err
}
if len(payload) == 0 {
return []dto.AgentPluginItem{}, nil
}
var output openclawPluginListOutput
if err := json.Unmarshal(payload, &output); err != nil {
return nil, err
}
items := make([]dto.AgentPluginItem, 0, len(output.Plugins))
for _, plugin := range output.Plugins {
items = append(items, dto.AgentPluginItem{
ID: plugin.ID,
Name: plugin.Name,
Version: plugin.Version,
Origin: plugin.Origin,
Enabled: plugin.Enabled,
})
}
return items, nil
}
func readOpenclawPluginIndex(dbPath string) ([]dto.AgentPluginItem, error) {
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(dbPath)+"?mode=ro")
if err != nil {
return nil, err
}
defer db.Close()
var raw []byte
if err := db.QueryRow(
"SELECT plugins_json FROM installed_plugin_index WHERE index_key = ?",
"installed-plugin-index",
).Scan(&raw); err != nil {
return nil, err
}
var plugins []openclawPluginIndexItem
if err := json.Unmarshal(raw, &plugins); err != nil {
return nil, err
}
items := make([]dto.AgentPluginItem, 0, len(plugins))
for _, plugin := range plugins {
name := plugin.PackageName
if name == "" {
name = plugin.PluginID
}
items = append(items, dto.AgentPluginItem{
ID: plugin.PluginID,
Name: name,
Version: plugin.PackageVersion,
Origin: plugin.Origin,
Enabled: plugin.Enabled,
})
}
return items, nil
}
func parseOpenclawPluginSearch(raw []byte) ([]dto.AgentPluginSearchItem, error) {
payload, err := extractEmbeddedJSON(string(raw))
if err != nil {
return nil, err
}
if len(payload) == 0 {
return []dto.AgentPluginSearchItem{}, nil
}
var output openclawPluginSearchOutput
if err := json.Unmarshal(payload, &output); err != nil {
return nil, err
}
items := make([]dto.AgentPluginSearchItem, 0, len(output.Results))
for _, result := range output.Results {
items = append(items, dto.AgentPluginSearchItem{
Package: result.Package.Name,
PluginID: result.Package.RuntimeID,
Name: result.Package.DisplayName,
Description: result.Package.Summary,
Version: result.Package.LatestVersion,
Channel: result.Package.Channel,
VerificationTier: result.Package.VerificationTier,
Categories: append([]string{}, result.Package.Categories...),
Official: result.Package.IsOfficial,
Downloads: result.Package.Stats.Downloads,
Score: result.Score,
})
}
return items, nil
}
func buildOpenclawPluginInstallSpec(packageName, version string) (string, error) {
packageName = strings.TrimSpace(packageName)
version = strings.TrimSpace(version)
if !openclawPluginPackagePattern.MatchString(packageName) || !openclawPluginVersionPattern.MatchString(version) {
return "", buserr.New("ErrInvalidChar")
}
return "clawhub:" + packageName + "@" + version, nil
}
func cleanupManagedOpenclawPlugin(agent *model.Agent, pluginID string) error {
pluginType := map[string]string{
"openclaw-lark": "feishu",
"openclaw-qqbot": "qqbot",
"wecom-openclaw-plugin": "wecom",
"dingtalk-connector": "dingtalk",
"openclaw-weixin": "weixin",
}[pluginID]
if pluginType == "" {
return nil
}
conf, err := readOpenclawConfig(agent.ConfigPath)
if err != nil {
return err
}
cleanupOpenclawPluginConfig(conf, pluginType)
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
}
func addOpenclawPluginRestartTask(t *task.Task, install *model.AppInstall) {
t.AddSubTask(task.GetTaskName("OpenClaw", task.TaskRestart, task.TaskScopeAI), func(t *task.Task) error {
output, err := compose.Restart(install.GetComposePath())
if output != "" {
t.Log(output)
}
return err
}, nil)
}
func executeAgentPluginTask(t *task.Task) {
if err := t.Execute(); err != nil {
global.LOG.Errorf("operate openclaw plugin failed: %v", err)
}
}
+273 -277
View File
@@ -4,12 +4,10 @@ import (
"crypto/rand" "crypto/rand"
"encoding/hex" "encoding/hex"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"net/http" "net/http"
"net/url" "net/url"
"path" "path"
"regexp"
"strconv" "strconv"
"strings" "strings"
"time" "time"
@@ -21,12 +19,9 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/joho/godotenv"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -40,39 +35,6 @@ type resolvedAgentAccountInput struct {
Provider string Provider string
APIKey string APIKey string
BaseURL string BaseURL string
APIType string
AuthMode string
}
func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error {
opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)}
if excludeID > 0 {
opts = append(opts, repo.WithByNOTID(excludeID))
}
account, err := agentAccountRepo.GetFirst(opts...)
if errors.Is(err, gorm.ErrRecordNotFound) {
return nil
}
if err != nil {
return err
}
if account != nil && account.ID > 0 {
return buserr.New("ErrRecordExist")
}
return nil
}
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
var settings []model.Setting
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
return false, err
}
values := make(map[string]string, len(settings))
for _, setting := range settings {
values[setting.Key] = setting.Value
}
return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) &&
strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil
} }
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) { func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
@@ -97,27 +59,19 @@ func ensureContainerRunning(containerName string) error {
return nil return nil
} }
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) { func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
resolvedAPIKey := strings.TrimSpace(apiKey) resolvedAPIKey := strings.TrimSpace(apiKey)
resolvedAPIType := strings.TrimSpace(apiType) resolvedBaseURL := strings.TrimSpace(baseURL)
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode) if resolvedBaseURL == "" {
if err != nil { if requiresInitialAgentAccountModels(provider) {
return resolvedAgentAccountInput{}, err
}
resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL)
if err != nil {
if strings.Contains(err.Error(), "base url is required") {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired") return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
} }
return resolvedAgentAccountInput{}, err if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
resolvedBaseURL = defaultURL
}
} }
modelID = strings.TrimSpace(modelID) if !providercatalog.SkipVerification(provider) {
if modelID == "" { if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil {
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
}
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
return resolvedAgentAccountInput{}, err return resolvedAgentAccountInput{}, err
} }
} }
@@ -125,8 +79,6 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
Provider: provider, Provider: provider,
APIKey: resolvedAPIKey, APIKey: resolvedAPIKey,
BaseURL: resolvedBaseURL, BaseURL: resolvedBaseURL,
APIType: resolvedAPIType,
AuthMode: resolvedAuthMode,
}, nil }, nil
} }
@@ -383,24 +335,26 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
agentType := agent.AgentType agentType := agent.AgentType
item := dto.AgentItem{ item := dto.AgentItem{
ID: agent.ID, ID: agent.ID,
Name: agent.Name, Name: agent.Name,
Remark: agent.Remark, Remark: agent.Remark,
AgentType: agentType, AgentType: agentType,
Provider: agent.Provider, Provider: agent.Provider,
ProviderName: localizedAgentProviderName(agent.Provider), ProviderName: providercatalog.DisplayName(agent.Provider),
Model: agent.Model, Model: agent.Model,
APIType: agent.APIType, APIType: agent.APIType,
BaseURL: agent.BaseURL, MaxTokens: agent.MaxTokens,
APIKey: maskKey(agent.APIKey), ContextWindow: agent.ContextWindow,
Token: agent.Token, BaseURL: agent.BaseURL,
Status: agent.Status, APIKey: maskKey(agent.APIKey),
Message: agent.Message, Token: agent.Token,
AppInstallID: agent.AppInstallID, Status: agent.Status,
WebsiteID: agent.WebsiteID, Message: agent.Message,
AccountID: agent.AccountID, AppInstallID: agent.AppInstallID,
ConfigPath: agent.ConfigPath, WebsiteID: agent.WebsiteID,
CreatedAt: agent.CreatedAt, AccountID: agent.AccountID,
ConfigPath: agent.ConfigPath,
CreatedAt: agent.CreatedAt,
} }
if appInstall != nil && appInstall.ID > 0 { if appInstall != nil && appInstall.ID > 0 {
item.Container = appInstall.ContainerName item.Container = appInstall.ContainerName
@@ -422,24 +376,10 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
item.BridgePort = toInt(bridge) item.BridgePort = toInt(bridge)
} }
} }
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
item.DashboardUsername = auth.Username
item.DashboardPassword = auth.Password
}
} }
return item return item
} }
func localizedAgentProviderName(provider string) string {
if key := providercatalog.DisplayNameKey(provider); key != "" {
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
return name
}
}
return providercatalog.DisplayName(provider)
}
func isAgentAppKey(appKey string) bool { func isAgentAppKey(appKey string) bool {
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
} }
@@ -737,9 +677,13 @@ type modelProvider struct {
} }
type modelEntry struct { type modelEntry struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Input []string `json:"input,omitempty"` Reasoning bool `json:"reasoning"`
Input []string `json:"input"`
ContextWindow int `json:"contextWindow"`
MaxTokens int `json:"maxTokens"`
Cost modelCost `json:"cost"`
} }
func requiresOpenclawProviderModels(provider string) bool { func requiresOpenclawProviderModels(provider string) bool {
@@ -759,6 +703,13 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig
return nil return nil
} }
type modelCost struct {
Input float64 `json:"input"`
Output float64 `json:"output"`
CacheRead float64 `json:"cacheRead"`
CacheWrite float64 `json:"cacheWrite"`
}
type browserConfig struct { type browserConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ExecutablePath string `json:"executablePath"` ExecutablePath string `json:"executablePath"`
@@ -909,15 +860,13 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if err := writeOpenclawConfigRaw(configPath, conf); err != nil { if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err return err
} }
envMap := map[string]string{ envPath := path.Join(confDir, ".env")
"OPENCLAW_GATEWAY_TOKEN": token, lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
}
order := []string{"OPENCLAW_GATEWAY_TOKEN"}
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" { if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
envMap[envKey] = account.APIKey lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
order = append(order, envKey)
} }
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order) content := strings.Join(lines, "\n") + "\n"
return fileOp.SaveFile(envPath, content, 0600)
} }
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
@@ -1104,7 +1053,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
} }
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) { func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey) providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey)
if err != nil { if err != nil {
return "", modelEntry{}, "", modelProvider{}, err return "", modelEntry{}, "", modelProvider{}, err
} }
@@ -1116,24 +1065,28 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
}, nil }, nil
} }
var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`)
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry { func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
name := strings.TrimSpace(model.Name) name := strings.TrimSpace(model.Name)
if name == "" { if name == "" {
name = strings.TrimSpace(modelID) name = strings.TrimSpace(modelID)
} }
entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name} return modelEntry{
if openclawVisionModelPattern.MatchString(modelID) { ID: strings.TrimSpace(modelID),
entry.Input = []string{"text", "image"} Name: name,
Reasoning: model.Reasoning,
Input: sanitizeAgentAccountModelInputs(model.Input),
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Cost: modelCost{},
} }
return entry
} }
type openclawAccountModelRuntime struct { type openclawAccountModelRuntime struct {
StoredModel string StoredModel string
PrimaryModel string PrimaryModel string
APIType string APIType string
MaxTokens int
ContextWindow int
} }
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) { func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
@@ -1142,9 +1095,11 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age
return openclawAccountModelRuntime{}, err return openclawAccountModelRuntime{}, err
} }
return openclawAccountModelRuntime{ return openclawAccountModelRuntime{
StoredModel: model.ID, StoredModel: model.ID,
PrimaryModel: primaryModel, PrimaryModel: primaryModel,
APIType: account.APIType, APIType: account.APIType,
MaxTokens: model.MaxTokens,
ContextWindow: model.ContextWindow,
}, nil }, nil
} }
@@ -1164,38 +1119,35 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
} }
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 { if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
return nil, buserr.New("ErrAgentAccountSingleInitialModel") return nil, buserr.New("ErrAgentAccountSingleInitialModel")
} }
if len(requested) > 0 { if len(requested) > 0 {
return normalizeAgentAccountModels(account, requested) models := make([]dto.AgentAccountModel, 0, len(requested))
for _, item := range requested {
models = append(models, cloneAgentAccountModel(item))
}
return models, nil
} }
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType) meta, ok := providercatalog.Get(account.Provider)
if len(defaultModels) == 0 { if !ok || len(meta.Models) == 0 {
if requiresInitialAgentAccountModels(account.Provider) { if requiresInitialAgentAccountModels(account.Provider) {
return nil, buserr.New("ErrAgentAccountModelsRequired") return nil, buserr.New("ErrAgentAccountModelsRequired")
} }
return nil, nil return nil, nil
} }
requested = make([]dto.AgentAccountModel, 0, len(defaultModels)) requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
for _, item := range defaultModels { for _, item := range meta.Models {
requested = append(requested, dto.AgentAccountModel{ requested = append(requested, dto.AgentAccountModel{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
return normalizeAgentAccountModels(account, requested) return requested, nil
}
func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel {
models := make([]dto.AgentAccountModel, 0, len(modelIDs))
for _, modelID := range modelIDs {
models = append(models, dto.AgentAccountModel{
ID: modelID,
Name: modelID,
})
}
return models
} }
func compactPersistedAgentAccountModelSortOrder(accountID uint) error { func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
@@ -1223,6 +1175,18 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode
return listPersistedAgentAccountModels(account.ID) return listPersistedAgentAccountModels(account.ID)
} }
func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel {
return dto.AgentAccountModel{
RecordID: model.RecordID,
ID: model.ID,
Name: model.Name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: append([]string(nil), model.Input...),
}
}
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
if account == nil { if account == nil {
return nil, fmt.Errorf("account is required") return nil, fmt.Errorf("account is required")
@@ -1246,8 +1210,12 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi
continue continue
} }
requested = append(requested, dto.AgentAccountModel{ requested = append(requested, dto.AgentAccountModel{
ID: item.ID, ID: item.ID,
Name: item.Name, Name: item.Name,
ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: append([]string(nil), item.Input...),
}) })
} }
if len(requested) == len(existing) { if len(requested) == len(existing) {
@@ -1266,10 +1234,18 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e
} }
result := make([]dto.AgentAccountModel, 0, len(rows)) result := make([]dto.AgentAccountModel, 0, len(rows))
for _, row := range rows { for _, row := range rows {
inputs := []string{}
if strings.TrimSpace(row.Input) != "" {
_ = json.Unmarshal([]byte(row.Input), &inputs)
}
result = append(result, dto.AgentAccountModel{ result = append(result, dto.AgentAccountModel{
RecordID: row.ID, RecordID: row.ID,
ID: strings.TrimSpace(row.Model), ID: strings.TrimSpace(row.Model),
Name: strings.TrimSpace(row.Name), Name: strings.TrimSpace(row.Name),
ContextWindow: row.ContextWindow,
MaxTokens: row.MaxTokens,
Reasoning: row.Reasoning,
Input: sanitizeAgentAccountModelInputs(inputs),
}) })
} }
return result, nil return result, nil
@@ -1280,11 +1256,19 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
return err return err
} }
for index, item := range models { for index, item := range models {
inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
if err != nil {
return err
}
record := &model.AgentAccountModel{ record := &model.AgentAccountModel{
AccountID: accountID, AccountID: accountID,
Model: strings.TrimSpace(item.ID), Model: strings.TrimSpace(item.ID),
Name: strings.TrimSpace(item.Name), Name: strings.TrimSpace(item.Name),
SortOrder: index + 1, ContextWindow: item.ContextWindow,
MaxTokens: item.MaxTokens,
Reasoning: item.Reasoning,
Input: string(inputPayload),
SortOrder: index + 1,
} }
if err := tx.Create(record).Error; err != nil { if err := tx.Create(record).Error; err != nil {
return err return err
@@ -1325,34 +1309,125 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
if modelID == "" { if modelID == "" {
return dto.AgentAccountModel{}, fmt.Errorf("model is required") return dto.AgentAccountModel{}, fmt.Errorf("model is required")
} }
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
name := strings.TrimSpace(model.Name) name := strings.TrimSpace(model.Name)
if name == "" { if name == "" {
name = modelID name = modelID
} }
inputs := sanitizeAgentAccountModelInputs(model.Input)
return dto.AgentAccountModel{ return dto.AgentAccountModel{
ID: modelID, ID: normalizeAgentAccountModelID(account.Provider, modelID),
Name: name, Name: name,
ContextWindow: model.ContextWindow,
MaxTokens: model.MaxTokens,
Reasoning: model.Reasoning,
Input: inputs,
}, nil }, nil
} }
func normalizeAgentAccountModelID(provider, requestedID string) string {
switch provider {
case "custom", "vllm":
return normalizeCustomModel(requestedID)
case "ollama":
target := strings.TrimSpace(requestedID)
if strings.HasPrefix(target, "ollama/") {
return target
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if target == "" {
return ""
}
return "ollama/" + target
default:
target := strings.TrimSpace(requestedID)
if target == "" {
return ""
}
prefix := poolModelPrefix(provider)
if strings.Contains(target, "/") {
parts := strings.SplitN(target, "/", 2)
targetPrefix := parts[0]
targetModel := strings.TrimSpace(parts[1])
if targetModel == "" {
return strings.TrimSpace(target)
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == targetPrefix {
if prefix != "" {
return prefix + "/" + targetModel
}
return strings.TrimSpace(target)
}
}
return strings.TrimSpace(target)
}
target = strings.TrimLeft(strings.TrimSpace(target), "/")
if prefix == "" {
return target
}
return prefix + "/" + target
}
}
func sanitizeAgentAccountModelInputs(values []string) []string {
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
normalized := value
if normalized != "text" && normalized != "image" {
continue
}
if _, ok := seen[normalized]; ok {
continue
}
seen[normalized] = struct{}{}
result = append(result, normalized)
}
if len(result) == 0 {
return []string{"text"}
}
return result
}
func requiresInitialAgentAccountModels(provider string) bool { func requiresInitialAgentAccountModels(provider string) bool {
switch provider { switch provider {
case "custom", "vllm", "ollama", "llmman": case "custom", "vllm", "ollama":
return true return true
default: default:
return false return false
} }
} }
func normalizeComparableProviderModelID(provider, modelID string) string {
target := strings.TrimSpace(modelID)
if target == "" {
return ""
}
if !strings.Contains(target, "/") {
return target
}
parts := strings.SplitN(target, "/", 2)
prefix := parts[0]
model := strings.TrimSpace(parts[1])
if model == "" {
return target
}
for _, item := range supportedProviderModelPrefixes(provider) {
if item == prefix {
return model
}
}
return target
}
func sameProviderModelID(provider, left, right string) bool { func sameProviderModelID(provider, left, right string) bool {
leftTrimmed := strings.TrimSpace(left) leftTrimmed := strings.TrimSpace(left)
rightTrimmed := strings.TrimSpace(right) rightTrimmed := strings.TrimSpace(right)
if leftTrimmed == rightTrimmed { if leftTrimmed == rightTrimmed {
return true return true
} }
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed) leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed) rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed)
return leftComparable != "" && leftComparable == rightComparable return leftComparable != "" && leftComparable == rightComparable
} }
@@ -1373,20 +1448,6 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco
return selectedAccountModel, nil return selectedAccountModel, nil
} }
func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) {
if len(models) == 0 {
return "", buserr.New("ErrAgentAccountModelsRequired")
}
if strings.TrimSpace(requested) == "" {
return models[0].ID, nil
}
selected, ok := findAgentAccountModelForProvider(provider, models, requested)
if !ok {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return selected.ID, nil
}
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error { func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
if account == nil || account.ID == 0 { if account == nil || account.ID == 0 {
return nil return nil
@@ -1463,122 +1524,6 @@ func readInstallEnv(envStr string) map[string]interface{} {
return data return data
} }
const (
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
)
type agentDashboardAuth struct {
Username string
Password string
}
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
auth := agentDashboardAuth{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}
if auth.Username == "" {
auth.Username = "admin"
}
if auth.Password == "" {
auth.Password = common.RandStr(8)
}
return auth
}
func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
switch agentType {
case constant.AppHermesAgent:
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
case constant.AppCopaw:
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
default:
return "", "", false
}
}
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
if !ok {
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
}
values := map[string]string{
usernameKey: auth.Username,
passwordKey: auth.Password,
}
order := []string{usernameKey, passwordKey}
if agentType == constant.AppCopaw {
values[qwenPawAuthEnabledEnvKey] = "true"
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
}
return upsertAgentEnv(envPath, values, order, overwrite)
}
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
if !ok {
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
}
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return agentDashboardAuth{}, err
}
return agentDashboardAuth{
Username: strings.TrimSpace(envMap[usernameKey]),
Password: strings.TrimSpace(envMap[passwordKey]),
}, nil
}
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
if appInstall == nil || appInstall.ID == 0 {
return agentDashboardAuth{}
}
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
if err != nil {
return agentDashboardAuth{}
}
return auth
}
func readAgentEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return err
}
for key, value := range values {
if key == "" {
continue
}
if overwrite || strings.TrimSpace(envMap[key]) == "" {
envMap[key] = value
}
}
return writeAgentEnvMap(envPath, envMap, order)
}
func maskKey(value string) string { func maskKey(value string) string {
trim := strings.TrimSpace(value) trim := strings.TrimSpace(value)
if len(trim) <= 6 { if len(trim) <= 6 {
@@ -1606,6 +1551,57 @@ func toInt(value interface{}) int {
} }
} }
func normalizeCustomModel(modelName string) string {
trim := strings.TrimSpace(modelName)
trim = strings.TrimLeft(trim, "/")
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
if strings.EqualFold(parts[0], "custom") {
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
}
}
return trim
}
func runtimeProviderModelPrefix(provider string) string {
switch provider {
case "gemini":
return "google"
case "kimi":
return "moonshot"
default:
return provider
}
}
func poolModelPrefix(provider string) string {
meta, ok := providercatalog.Get(provider)
if ok && len(meta.Models) > 0 {
parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2)
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
return parts[0]
}
}
return provider
}
func supportedProviderModelPrefixes(provider string) []string {
values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
result := make([]string, 0, len(values))
seen := make(map[string]struct{}, len(values))
for _, value := range values {
target := value
if target == "" {
continue
}
if _, ok := seen[target]; ok {
continue
}
seen[target] = struct{}{}
result = append(result, target)
}
return result
}
func generateToken() string { func generateToken() string {
bytes := make([]byte, 24) bytes := make([]byte, 24)
if _, err := rand.Read(bytes); err != nil { if _, err := rand.Read(bytes); err != nil {
+5 -1
View File
@@ -162,7 +162,7 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
} }
} }
func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website { func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
websiteMap := make(map[uint]model.Website) websiteMap := make(map[uint]model.Website)
duplicateAppInstallIDs := make(map[uint]struct{}) duplicateAppInstallIDs := make(map[uint]struct{})
for _, website := range websites { for _, website := range websites {
@@ -182,6 +182,10 @@ func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m
return websiteMap return websiteMap
} }
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
return uniqueDeploymentWebsiteMapByAppInstall(websites)
}
func uniqueUintList(items []uint) []uint { func uniqueUintList(items []uint) []uint {
itemMap := make(map[uint]struct{}, len(items)) itemMap := make(map[uint]struct{}, len(items))
uniq := make([]uint, 0, len(items)) uniq := make([]uint, 0, len(items))
-139
View File
@@ -1,139 +0,0 @@
package service
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
const (
vllmAppKeyForUpgrade = "vllm"
vllmImageEnvKey = "IMAGE"
vllmImageTypeNvidia = "nvidia"
vllmImageTypeIntel = "intel"
vllmImageTypeAscend = "ascend"
)
func resolveVllmVersionFamily(version, image string) string {
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
return vllmImageTypeIntel
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
return vllmImageTypeAscend
}
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
return vllmImageTypeNvidia
}
normalizedImage := strings.ToLower(strings.TrimSpace(image))
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
return vllmImageTypeIntel
}
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
return vllmImageTypeAscend
}
return vllmImageTypeNvidia
}
func trimVllmVersionFamily(version string) string {
trimmed := strings.TrimSpace(version)
normalized := strings.ToLower(trimmed)
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
prefix := family + "-"
if strings.HasPrefix(normalized, prefix) {
return strings.TrimSpace(trimmed[len(prefix):])
}
}
return trimmed
}
func buildDefaultVllmImageByVersion(version string) string {
tag := trimVllmVersionFamily(version)
family := resolveVllmVersionFamily(version, "")
if family == vllmImageTypeIntel {
return "intel/llm-scaler-vllm:" + tag
}
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
tag = "v" + tag
}
if family == vllmImageTypeAscend {
return "quay.io/ascend/vllm-ascend:" + tag
}
return "vllm/vllm-openai:" + tag
}
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
targetFamily := resolveVllmVersionFamily(targetVersion, "")
return currentFamily == targetFamily
}
func hasVllmVersionFamilyPrefix(version string) bool {
normalized := strings.ToLower(strings.TrimSpace(version))
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
}
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
if strings.TrimSpace(currentVersion) == strings.TrimSpace(targetVersion) {
return false
}
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
return false
}
if common.CompareVersion(targetVersion, currentVersion) {
return true
}
return !hasVllmVersionFamilyPrefix(currentVersion) &&
resolveVllmVersionFamily(targetVersion, "") == vllmImageTypeNvidia &&
trimVllmVersionFamily(currentVersion) == trimVllmVersionFamily(targetVersion)
}
func buildVllmUpgradeImage(currentImage, currentVersion, targetVersion string) string {
trimmedImage := strings.TrimSpace(currentImage)
if trimmedImage == "" || trimmedImage == buildDefaultVllmImageByVersion(currentVersion) {
return buildDefaultVllmImageByVersion(targetVersion)
}
return trimmedImage
}
func loadVllmImageFromEnv(raw string) string {
envs := make(map[string]interface{})
if strings.TrimSpace(raw) == "" {
return ""
}
if err := json.Unmarshal([]byte(raw), &envs); err != nil {
return ""
}
if image, ok := envs[vllmImageEnvKey].(string); ok {
return strings.TrimSpace(image)
}
return ""
}
func setVllmImageInEnvContent(content []byte, image string) []byte {
normalizedImage := strings.TrimSpace(image)
if normalizedImage == "" {
return content
}
lines := strings.Split(string(content), "\n")
replaced := false
for index, line := range lines {
if strings.HasPrefix(line, vllmImageEnvKey+"=") {
lines[index] = vllmImageEnvKey + "=" + normalizedImage
replaced = true
break
}
}
if !replaced {
if len(lines) > 0 && lines[len(lines)-1] == "" {
lines[len(lines)-1] = vllmImageEnvKey + "=" + normalizedImage
lines = append(lines, "")
} else {
lines = append(lines, vllmImageEnvKey+"="+normalizedImage)
}
}
return []byte(strings.Join(lines, "\n"))
}
+27 -330
View File
@@ -17,13 +17,10 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n" "github.com/1Panel-dev/1Panel/agent/i18n"
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/copier"
"github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
) )
@@ -37,28 +34,6 @@ var communityAlertMethodTypeNames = map[string]string{
constant.SMS: "SMS", constant.SMS: "SMS",
} }
var legacyAlertMethodTypeMap = map[string]string{
"mail": constant.Email,
constant.Email: constant.Email,
constant.SMS: constant.SMS,
constant.Bark: constant.Bark,
constant.WeChat: constant.WeCom,
constant.WeCom: constant.WeCom,
constant.DingTalk: constant.DingTalk,
constant.FeiShu: constant.FeiShu,
constant.Custom: constant.Custom,
}
var supportedAlertMethodTypes = map[string]struct{}{
constant.Email: {},
constant.SMS: {},
constant.Bark: {},
constant.WeCom: {},
constant.DingTalk: {},
constant.FeiShu: {},
constant.Custom: {},
}
type IAlertService interface { type IAlertService interface {
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
GetAlerts() ([]dto.AlertDTO, error) GetAlerts() ([]dto.AlertDTO, error)
@@ -78,10 +53,8 @@ type IAlertService interface {
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
DeleteAlertConfig(id uint) error DeleteAlertConfig(id uint) error
TestAlertConfig(req dto.AlertConfigTest) (bool, error) TestAlertConfig(req dto.AlertConfigTest) (bool, error)
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
} }
func NewIAlertService() IAlertService { func NewIAlertService() IAlertService {
@@ -207,15 +180,9 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
} }
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error { func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
methodTypes, err := a.validateAlertMethodReferences(req.Method) if err := a.validateCommunityAlertMethod(req.Method); err != nil {
if err != nil {
return err return err
} }
if req.Status != constant.AlertDisable {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
}
upMap := make(map[string]interface{}) upMap := make(map[string]interface{})
upMap["id"] = req.ID upMap["id"] = req.ID
@@ -273,16 +240,7 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
if alertInfo.ID == 0 { if alertInfo.ID == 0 {
return buserr.New("ErrRecordNotFound") return buserr.New("ErrRecordNotFound")
} }
methodTypes, err := a.validateAlertMethodReferences(alertInfo.Method) err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
if err != nil {
return err
}
if status == constant.AlertEnable {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
}
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
if err != nil { if err != nil {
return err return err
} }
@@ -426,9 +384,6 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
if search.Count != 0 { if search.Count != 0 {
opts = append(opts, alertRepo.WithByCount(search.Count)) opts = append(opts, alertRepo.WithByCount(search.Count))
} }
if !search.StartTime.IsZero() && !search.EndTime.IsZero() {
opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime))
}
opts = append(opts, repo.WithOrderDesc("created_at")) opts = append(opts, repo.WithOrderDesc("created_at"))
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...) total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
@@ -454,7 +409,6 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil { if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
return dto.AlertLogDTO{}, err return dto.AlertLogDTO{}, err
} }
alertDetail.Task = nil
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil { if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
return dto.AlertLogDTO{}, err return dto.AlertLogDTO{}, err
} }
@@ -537,13 +491,7 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
} }
opts = append(opts, repo.WithByStatus(constant.AlertEnable)) opts = append(opts, repo.WithByStatus(constant.AlertEnable))
configs, err := alertRepo.AlertConfigList(opts...) configs, err := alertRepo.AlertConfigList(opts...)
if err != nil { return configs, err
return nil, err
}
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
return nil, err
}
return configs, nil
} }
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) { func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
@@ -554,49 +502,13 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
if len(req.ExcludeTypes) > 0 { if len(req.ExcludeTypes) > 0 {
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes)) opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
} }
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...) return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
if err != nil {
return 0, nil, err
}
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
return 0, nil, err
}
return total, configs, nil
} }
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error { func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
if req.Type == constant.Custom {
if req.ID != 0 && req.Revision == nil {
return repo.ErrAlertConfigRevisionRequired
}
return a.updateCustomAlertConfig(req, operator)
}
usesMutation, err := alertconfig.UsesMutation(req.Type, req.Config)
if err != nil {
return err
}
if req.ID != 0 && usesMutation && req.Revision == nil {
return repo.ErrAlertConfigRevisionRequired
}
var existing *model.AlertConfig
if req.ID != 0 {
stored, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
}
if stored.Type != req.Type {
return fmt.Errorf("alert config %d has type %s, not %s", req.ID, stored.Type, req.Type)
}
existing = &stored
}
if err := a.validateCommunityAlertConfigType(req.Type); err != nil { if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
return err return err
} }
prepared, err := alertconfig.Prepare(req.Type, req.Config, req.Status, existing)
if err != nil {
return err
}
req.Config = prepared
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil { if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
return err return err
} }
@@ -611,7 +523,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
upMap["status"] = req.Status upMap["status"] = req.Status
upMap["config"] = req.Config upMap["config"] = req.Config
upMap["update_user"] = operator upMap["update_user"] = operator
if err := alertRepo.UpdateAlertConfigWithRevision(upMap, req.Revision, repo.WithByID(req.ID)); err != nil { if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
return err return err
} }
} else { } else {
@@ -629,99 +541,6 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
return nil return nil
} }
func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operator string) error {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
}
var existing *model.AlertConfig
if req.ID != 0 {
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
}
if config.Type != constant.Custom {
return fmt.Errorf("alert config %d is not a custom webhook", req.ID)
}
existing = &config
}
prepared, err := alertwebhook.Prepare(req.Config, req.Status, existing)
if err != nil {
return err
}
validatedReq := req
validatedReq.Config = prepared.Config
if err := a.checkAlertConfigDisplayNameUnique(validatedReq); err != nil {
return err
}
if existing != nil {
return alertRepo.UpdateAlertConfigWithRevision(map[string]interface{}{
"type": constant.Custom,
"title": req.Title,
"status": req.Status,
"config": prepared.Config,
"secret_config": prepared.SecretConfig,
"update_user": operator,
}, req.Revision, repo.WithByID(req.ID))
}
return alertRepo.CreateAlertConfig(&model.AlertConfig{
Type: constant.Custom,
Title: req.Title,
Status: req.Status,
Config: prepared.Config,
SecretConfig: prepared.SecretConfig,
CreateUser: operator,
UpdateUser: operator,
})
}
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
if err := validateAlertConfigStatus(req.Status); err != nil {
return err
}
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return err
}
if req.Status == constant.AlertEnable {
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
return err
}
if config.Type == constant.Custom {
if _, err := alertwebhook.Resolve(config); err != nil {
return err
}
}
}
return alertRepo.UpdateAlertConfig(map[string]interface{}{
"status": req.Status,
"update_user": operator,
}, repo.WithByID(req.ID))
}
func validateAlertConfigStatus(status string) error {
if status != constant.AlertEnable && status != constant.AlertDisable {
return fmt.Errorf("alert config status must be Enable or Disable")
}
return nil
}
func exposeCustomAlertConfigSecrets(configs []model.AlertConfig) error {
for index := range configs {
if configs[index].Type != constant.Custom {
continue
}
view, err := alertwebhook.PlainView(configs[index])
if err != nil {
return fmt.Errorf("build editable custom alert config %d: %w", configs[index].ID, err)
}
configs[index].Config = view
}
return nil
}
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error { func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
if req.Type != constant.SMSConfig { if req.Type != constant.SMSConfig {
return nil return nil
@@ -746,9 +565,6 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
} }
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error { func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
if req.Type != constant.Custom && (global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn") {
return nil
}
displayName := alertConfigDisplayName(req.Type, req.Config) displayName := alertConfigDisplayName(req.Type, req.Config)
if displayName == "" { if displayName == "" {
return nil return nil
@@ -772,67 +588,37 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
} }
func (a AlertService) validateCommunityAlertMethod(method string) error { func (a AlertService) validateCommunityAlertMethod(method string) error {
methodTypes, err := a.validateAlertMethodReferences(method) if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
if err != nil { return nil
return err
} }
return a.validateAlertMethodEntitlement(methodTypes)
}
func (a AlertService) validateAlertMethodReferences(method string) ([]string, error) {
if strings.TrimSpace(method) == "" { if strings.TrimSpace(method) == "" {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil) return nil
} }
methodTypes := make([]string, 0)
for _, item := range strings.Split(method, ",") { for _, item := range strings.Split(method, ",") {
item = strings.TrimSpace(item) item = strings.TrimSpace(item)
if item == "" { if item == "" {
continue continue
} }
configType := ""
if configID, err := strconv.ParseUint(item, 10, 64); err == nil { if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
config, err := alertRepo.GetConfigById(uint(configID)) config, err := alertRepo.GetConfigById(uint(configID))
if err != nil { if err != nil {
return nil, err return err
} }
configType = config.Type if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
} else { return buserr.WithErr("ErrAlertMethodNotSupported", nil)
var ok bool
configType, ok = legacyAlertMethodTypeMap[item]
if !ok {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
} }
}
if _, ok := supportedAlertMethodTypes[configType]; !ok {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
methodTypes = append(methodTypes, configType)
}
if len(methodTypes) == 0 {
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
return methodTypes, nil
}
func (a AlertService) validateAlertMethodEntitlement(methodTypes []string) error {
for _, configType := range methodTypes {
if configType == constant.Custom {
continue continue
} }
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" { if _, ok := communityAlertMethodTypeNames[item]; ok {
continue
}
if _, ok := communityAlertMethodTypeNames[configType]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil) return buserr.WithErr("ErrAlertMethodNotSupported", nil)
} }
} }
return nil return nil
} }
func (a AlertService) validateCommunityAlertConfigType(configType string) error { func (a AlertService) validateCommunityAlertConfigType(configType string) error {
if configType == constant.Custom {
return nil
}
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" { if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
return nil return nil
} }
@@ -844,7 +630,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
func alertConfigDisplayName(configType, configData string) string { func alertConfigDisplayName(configType, configData string) string {
switch configType { switch configType {
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS, constant.Custom: case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
var cfg struct { var cfg struct {
DisplayName string `json:"displayName"` DisplayName string `json:"displayName"`
} }
@@ -883,24 +669,20 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
} }
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) { func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
emailConfig, err := resolveEmailTestConfig(req) username := req.UserName
if err != nil {
return false, err
}
username := emailConfig.UserName
if username == "" { if username == "" {
username = emailConfig.Sender username = req.Sender
} }
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailConfig.DisplayName) encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
cfg := email.SMTPConfig{ cfg := email.SMTPConfig{
Host: emailConfig.Host, Host: req.Host,
Port: emailConfig.Port, Port: req.Port,
Sender: emailConfig.Sender, Sender: req.Sender,
Username: username, Username: username,
Password: emailConfig.Password, Password: req.Password,
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender), From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
Encryption: emailConfig.Encryption, Encryption: req.Encryption,
Recipient: emailConfig.Recipient, Recipient: req.Recipient,
} }
msg := email.EmailMessage{ msg := email.EmailMessage{
@@ -915,94 +697,9 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
return true, nil return true, nil
} }
func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, error) {
emailConfig := dto.AlertEmailConfig{
Host: req.Host,
Port: req.Port,
Sender: req.Sender,
UserName: req.UserName,
Password: req.Password,
DisplayName: req.DisplayName,
Encryption: req.Encryption,
Recipient: req.Recipient,
}
if strings.TrimSpace(req.Config) != "" {
configType := req.Type
if configType == "" {
configType = constant.EmailConfig
}
if configType != constant.EmailConfig {
return dto.AlertEmailConfig{}, fmt.Errorf("alert config test type must be email")
}
var existing *model.AlertConfig
if req.ID != 0 {
stored, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return dto.AlertEmailConfig{}, err
}
existing = &stored
}
prepared, err := alertconfig.Prepare(configType, req.Config, constant.AlertEnable, existing)
if err != nil {
return dto.AlertEmailConfig{}, err
}
if err := json.Unmarshal([]byte(prepared), &emailConfig); err != nil {
return dto.AlertEmailConfig{}, fmt.Errorf("decode email alert config: %w", err)
}
}
return emailConfig, nil
}
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
if req.Type != constant.Custom {
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
}
var existing *model.AlertConfig
if req.ID != 0 {
config, err := alertRepo.GetConfigById(req.ID)
if err != nil {
return dto.AlertConfigTestResult{}, err
}
if config.Type != constant.Custom {
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config %d is not a custom webhook", req.ID)
}
existing = &config
}
prepared, err := alertwebhook.Prepare(req.Config, constant.AlertEnable, existing)
if err != nil {
return dto.AlertConfigTestResult{}, err
}
resolved, err := alertwebhook.Resolve(model.AlertConfig{
Type: constant.Custom,
Config: prepared.Config,
SecretConfig: prepared.SecretConfig,
})
if err != nil {
return dto.AlertConfigTestResult{}, err
}
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
if !ok {
return dto.AlertConfigTestResult{
Success: false,
Message: providers.ErrCustomWebhookUnsupported.Error(),
}, nil
}
return tester.TestCustomWebhook(resolved)
}
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error { func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
var methodTypes []string if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
if updateAlert.SendCount != 0 || strings.TrimSpace(updateAlert.Method) != "" { return err
var err error
methodTypes, err = a.validateAlertMethodReferences(updateAlert.Method)
if err != nil {
return err
}
}
if updateAlert.SendCount != 0 {
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
return err
}
} }
upMap := make(map[string]interface{}) upMap := make(map[string]interface{})
var newStatus string var newStatus string
+29 -100
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"math" "math"
"net" "net"
@@ -33,7 +32,6 @@ const (
ResourceAlertInterval = 30 ResourceAlertInterval = 30
CheckIntervalSec = 3 CheckIntervalSec = 3
LoadCheckIntervalMin = 5 LoadCheckIntervalMin = 5
sshIPLoginWindow = 30 * time.Minute
) )
type AlertTaskHelper struct { type AlertTaskHelper struct {
@@ -514,28 +512,10 @@ func loadPanelLogin(alert dto.AlertDTO) {
} }
func loadSSHLogin(alert dto.AlertDTO) { func loadSSHLogin(alert dto.AlertDTO) {
now := time.Now() count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
failedWindow := time.Duration(alert.Cycle) * time.Minute
loadWindow := failedWindow
if loadWindow < sshIPLoginWindow {
loadWindow = sshIPLoginWindow
}
location, err := time.LoadLocation(common.LoadTimeZoneByCmd())
if err != nil { if err != nil {
global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err) global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
location = time.Local
} }
histories, err := loadSSHAlertHistories(defaultSSHLogDir, now.Add(-loadWindow), now, location)
if err != nil {
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
}
count, records := summarizeSSHLoginHistories(
histories,
now,
failedWindow,
strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n"),
)
isAlert := count >= int(alert.Count)
if isAlert { if isAlert {
params := []dto.Param{ params := []dto.Param{
{ {
@@ -551,6 +531,12 @@ func loadSSHLogin(alert dto.AlertDTO) {
} }
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params) sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
} }
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
if err != nil {
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
}
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
if len(records) > 0 { if len(records) > 0 {
quota := strings.Join(records, "\n") quota := strings.Join(records, "\n")
params := []dto.Param{ params := []dto.Param{
@@ -579,6 +565,20 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
return filtered return filtered
} }
func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string {
filtered := make([]string, 0, len(records))
for _, record := range records {
ip := record
if idx := strings.Index(record, "-"); idx >= 0 {
ip = record[:idx]
}
if !isIPInWhitelist(ip, whitelist) {
filtered = append(filtered, record)
}
}
return filtered
}
func isIPInWhitelist(ip string, whitelist []string) bool { func isIPInWhitelist(ip string, whitelist []string) bool {
targetIP := net.ParseIP(strings.TrimSpace(ip)) targetIP := net.ParseIP(strings.TrimSpace(ip))
if targetIP == nil { if targetIP == nil {
@@ -695,10 +695,9 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) { func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
typeMap := map[string]string{ typeMap := map[string]string{
"mail": constant.Email, "mail": constant.Email,
constant.Bark: constant.Bark, constant.Bark: constant.Bark,
constant.SMS: constant.SMS, constant.SMS: constant.SMS,
constant.Custom: constant.Custom,
} }
configType, ok := typeMap[method] configType, ok := typeMap[method]
if !ok { if !ok {
@@ -706,6 +705,7 @@ func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType st
} }
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil { if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return return
} }
doSendAlert(alert, alertType, quota, quotaType, params, config) doSendAlert(alert, alertType, quota, quotaType, params, config)
@@ -786,7 +786,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
} }
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr) alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom: case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr) todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid { if !isValid {
return return
@@ -799,31 +799,12 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
var alertErr error
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: alert.ID,
Type: alertType,
Quota: quota,
QuotaType: quotaType,
Method: methodStr,
}
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo, task)
queued, alertErr = result.Queued, deliveryErr
if alertErr == nil && result.Queued {
_, alertErr = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
alertErr = xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
}
if alertErr != nil { if alertErr != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr) global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
return return
} }
if !queued { alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
}
} }
} }
@@ -1117,55 +1098,3 @@ func calculateMinutesDifference(newDate time.Time) int {
minutesDifference := int(now.Sub(newDate).Minutes()) minutesDifference := int(now.Sub(newDate).Minutes())
return minutesDifference return minutesDifference
} }
func loadSSHAlertHistories(
baseDir string,
startTime, endTime time.Time,
location *time.Location,
) ([]dto.SSHHistory, error) {
fileList, err := listSSHLogFiles(baseDir)
if err != nil {
return nil, err
}
var (
histories []dto.SSHHistory
loadErr error
)
for _, file := range fileList {
items, err := loadSSHHistoriesFromFile(file.Name, "", "", startTime, endTime, file.Year, location)
if err != nil {
loadErr = errors.Join(loadErr, fmt.Errorf("load SSH log file %s: %w", file.Name, err))
continue
}
histories = append(histories, items...)
}
return histories, loadErr
}
func summarizeSSHLoginHistories(
histories []dto.SSHHistory,
now time.Time,
failedWindow time.Duration,
whitelist []string,
) (int, []string) {
failedStartTime := now.Add(-failedWindow)
successStartTime := now.Add(-sshIPLoginWindow)
failedCount := 0
var abnormalLogins []string
for _, item := range histories {
switch item.Status {
case constant.StatusFailed:
if isSSHLogWithinTimeRange(item.Date, failedStartTime, now) {
failedCount++
}
case constant.StatusSuccess:
if !isSSHLogWithinTimeRange(item.Date, successStartTime, now) || isIPInWhitelist(item.Address, whitelist) {
continue
}
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", item.Address, item.Date.Format(constant.DateTimeLayout)))
}
}
return failedCount, abnormalLogins
}
+7 -45
View File
@@ -75,7 +75,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
} else { } else {
s.sendBarkWithConfig(config, quota, params) s.sendBarkWithConfig(config, quota, params)
} }
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom: case constant.WeCom, constant.DingTalk, constant.FeiShu:
if isResource { if isResource {
s.sendResourceWebhookWithConfig(config, quota, params) s.sendResourceWebhookWithConfig(config, quota, params)
} else { } else {
@@ -86,13 +86,14 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) { func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo() alertRepo := repo.NewIAlertRepo()
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS, constant.Custom: constant.Custom} typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
configType := method configType := method
if mapped, ok := typeMap[method]; ok { if mapped, ok := typeMap[method]; ok {
configType = mapped configType = mapped
} }
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType)) config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil { if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return return
} }
if !alertUtil.IsAlertConfigEnabled(config) { if !alertUtil.IsAlertConfigEnabled(config) {
@@ -308,31 +309,12 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
var err error
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: s.alert.ID,
Type: s.alert.Type,
Quota: quota,
QuotaType: s.quotaType,
Method: strconv.Itoa(int(config.ID)),
}
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
queued, err = result.Queued, deliveryErr
if err == nil && result.Queued {
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
}
if err != nil { if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err) global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return return
} }
if !queued { alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
} }
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) { func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
@@ -353,31 +335,11 @@ func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, qu
} }
transport := xpack.MultiNodeProvider.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
queued := false if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
var err error
if config.Type == constant.Custom {
task := dto.AlertTaskMetadata{
AlertID: s.alert.ID,
Type: s.alert.Type,
Quota: quota,
QuotaType: s.quotaType,
Method: strconv.Itoa(int(config.ID)),
}
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
queued, err = result.Queued, deliveryErr
if err == nil && result.Queued {
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
}
} else {
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
}
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err) global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return return
} }
if !queued { alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
} }
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) { func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
+14 -29
View File
@@ -223,9 +223,6 @@ func (a AppService) GetAppDetailByKey(appKey, version string) (response.AppDetai
if err != nil { if err != nil {
return appDetailDTO, err return appDetailDTO, err
} }
if err = checkVllmVersionAccess(app.Key, version); err != nil {
return appDetailDTO, err
}
appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version)) appDetail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(app.ID), appDetailRepo.WithVersion(version))
if err != nil { if err != nil {
return appDetailDTO, err return appDetailDTO, err
@@ -244,17 +241,14 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
if err != nil { if err != nil {
return appDetailDTO, err return appDetailDTO, err
} }
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
if err != nil {
return appDetailDTO, err
}
if err = checkVllmVersionAccess(app.Key, detail.Version); err != nil {
return appDetailDTO, err
}
appDetailDTO.AppDetail = detail appDetailDTO.AppDetail = detail
appDetailDTO.Enable = true appDetailDTO.Enable = true
if appType == "runtime" { if appType == "runtime" {
app, err := appRepo.GetFirst(repo.WithByID(appID))
if err != nil {
return appDetailDTO, err
}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version) versionPath := filepath.Join(app.GetAppResourcePath(), detail.Version)
@@ -309,9 +303,6 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
filename := filepath.Base(appDetailDTO.DownloadUrl) filename := filepath.Base(appDetailDTO.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(appDetailDTO.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return appDetailDTO, buserr.New("ErrAppVersionUnavailable")
}
if err != nil { if err != nil {
return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err) return appDetailDTO, buserr.WithDetail("ErrGetCompose", err.Error(), err)
} }
@@ -325,6 +316,10 @@ func (a AppService) GetAppDetail(appID uint, version, appType string) (response.
appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose) appDetailDTO.HostMode = isHostModel(appDetailDTO.DockerCompose)
app, err := appRepo.GetFirst(repo.WithByID(detail.AppId))
if err != nil {
return appDetailDTO, err
}
if err := checkLimit(app); err != nil { if err := checkLimit(app); err != nil {
appDetailDTO.Enable = false appDetailDTO.Enable = false
} }
@@ -376,9 +371,6 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
if err != nil { if err != nil {
return return
} }
if err = checkVllmVersionAccess(app.Key, appDetail.Version); err != nil {
return
}
if DatabaseKeys[app.Key] > 0 { if DatabaseKeys[app.Key] > 0 {
if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 { if existDatabases, _ := databaseRepo.GetList(repo.WithByName(req.Name)); len(existDatabases) > 0 {
err = buserr.New("ErrRemoteExist") err = buserr.New("ErrRemoteExist")
@@ -434,12 +426,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
} else { } else {
if appDetail.DockerCompose == "" { if appDetail.DockerCompose == "" {
dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version) dockerComposeUrl := fmt.Sprintf("%s/%s/1panel/%s/%s/docker-compose.yml", global.AppRepoURL(), global.CONF.Base.Mode, app.Key, appDetail.Version)
var statusCode int _, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
statusCode, composeRes, err = req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
err = buserr.New("ErrAppVersionUnavailable")
return
}
if err != nil { if err != nil {
return return
} }
@@ -488,17 +475,15 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
index++ index++
} }
newServiceName := strings.ToLower(appInstall.Name) newServiceName := strings.ToLower(appInstall.Name)
if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 && !req.KeepServiceName { if app.Limit == 0 && newServiceName != serviceName && len(servicesMap) == 1 {
servicesMap[newServiceName] = servicesMap[serviceName] servicesMap[newServiceName] = servicesMap[serviceName]
delete(servicesMap, serviceName) delete(servicesMap, serviceName)
serviceName = newServiceName serviceName = newServiceName
} }
appInstall.ServiceName = serviceName appInstall.ServiceName = serviceName
if !req.SkipComposeCommonConfig { if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil {
if err = addDockerComposeCommonParam(composeMap, appInstall.ServiceName, req.AppContainerConfig, req.Params); err != nil { return
return
}
} }
var ( var (
composeByte []byte composeByte []byte
@@ -566,7 +551,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return err return err
} }
} }
if executeScript || req.UseLifecycleScripts { if executeScript {
if err = runScript(t, appInstall, "init"); err != nil { if err = runScript(t, appInstall, "init"); err != nil {
return err return err
} }
@@ -579,7 +564,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
return err return err
} }
} }
if err = upApp(t, appInstall, req.PullImage, req.UseLifecycleScripts); err != nil { if err = upApp(t, appInstall, req.PullImage); err != nil {
return err return err
} }
updateToolApp(appInstall) updateToolApp(appInstall)
+13 -114
View File
@@ -4,7 +4,6 @@ import (
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
"maps"
"math" "math"
"net/http" "net/http"
"os" "os"
@@ -14,14 +13,12 @@ import (
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -255,9 +252,6 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return buserr.New("ErrInstallDirNotFound") return buserr.New("ErrInstallDirNotFound")
} }
dockerComposePath := install.GetComposePath() dockerComposePath := install.GetComposePath()
if req.UseLifecycleScripts && (req.Operate == constant.Start || req.Operate == constant.Stop || req.Operate == constant.Restart) {
return operateAppWithLifecycleScripts(install, req, nil)
}
switch req.Operate { switch req.Operate {
case constant.Rebuild: case constant.Rebuild:
return rebuildApp(install) return rebuildApp(install)
@@ -281,13 +275,12 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
return syncAppInstallStatus(&install, false) return syncAppInstallStatus(&install, false)
case constant.Delete: case constant.Delete:
deleteReq := request.AppInstallDelete{ deleteReq := request.AppInstallDelete{
Install: install, Install: install,
DeleteBackup: req.DeleteBackup, DeleteBackup: req.DeleteBackup,
ForceDelete: req.ForceDelete, ForceDelete: req.ForceDelete,
DeleteDB: req.DeleteDB, DeleteDB: req.DeleteDB,
DeleteImage: req.DeleteImage, DeleteImage: req.DeleteImage,
TaskID: req.TaskID, TaskID: req.TaskID,
UseLifecycleScripts: req.UseLifecycleScripts,
} }
if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete { if err = deleteAppInstall(deleteReq); err != nil && !req.ForceDelete {
return err return err
@@ -301,7 +294,6 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
DetailID: req.DetailId, DetailID: req.DetailId,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose, DockerCompose: req.DockerCompose,
TaskID: req.TaskID, TaskID: req.TaskID,
} }
@@ -319,70 +311,6 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
} }
} }
func operateAppWithLifecycleScripts(install model.AppInstall, req request.AppInstalledOperate, onFailure func(error)) error {
taskType := task.TaskUpdate
switch req.Operate {
case constant.Start:
install.Status = constant.StatusStarting
case constant.Restart:
taskType = task.TaskRestart
install.Status = constant.StatusRestarting
case constant.Stop:
install.Status = constant.StatusWaiting
default:
return errors.New("lifecycle script operation not supported")
}
install.Message = ""
if err := appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
operationTask, err := task.NewTaskWithOps(install.Name, taskType, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
return err
}
operation := string(req.Operate)
operationTask.AddSubTaskWithOps(
task.GetTaskName(install.Name, taskType, task.TaskScopeApp),
func(t *task.Task) error {
if err := runScript(t, &install, operation); err != nil {
return err
}
if req.Operate == constant.Stop {
install.Status = constant.StatusStopped
install.Message = ""
return appInstallRepo.Save(context.Background(), &install)
}
containerNames, err := getContainerNames(install)
if err != nil {
return err
}
if len(containerNames) == 0 {
return buserr.WithName("ErrContainerNotFound", install.Name)
}
install.ContainerName = strings.Join(containerNames, ",")
install.Status = constant.StatusRunning
install.Message = ""
return appInstallRepo.Save(context.Background(), &install)
},
nil,
0,
time.Hour,
)
go func() {
if taskErr := operationTask.Execute(); taskErr != nil {
if onFailure != nil {
onFailure(taskErr)
return
}
install.Status = constant.StatusUpErr
install.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), &install)
}
}()
return nil
}
func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error { func (a *AppInstallService) UpdateAppConfig(req request.AppConfigUpdate) error {
installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID)) installed, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if err != nil { if err != nil {
@@ -445,10 +373,8 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
return err return err
} }
} }
if !req.SkipComposeCommonConfig { if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil {
if err = addDockerComposeCommonParam(composeMap, installed.ServiceName, req.AppContainerConfig, req.Params); err != nil { return err
return err
}
} }
composeByte, err := yaml.Marshal(composeMap) composeByte, err := yaml.Marshal(composeMap)
if err != nil { if err != nil {
@@ -481,7 +407,7 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
if err != nil { if err != nil {
return err return err
} }
backupEnvMaps := maps.Clone(oldEnvMaps) backupEnvMaps := oldEnvMaps
handleMap(req.Params, oldEnvMaps) handleMap(req.Params, oldEnvMaps)
paramByte, err := json.Marshal(oldEnvMaps) paramByte, err := json.Marshal(oldEnvMaps)
if err != nil { if err != nil {
@@ -493,32 +419,13 @@ func (a *AppInstallService) Update(req request.AppInstalledUpdate) error {
} }
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm) _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(installed.DockerCompose), constant.DirPerm)
restoreConfig := func(operationErr error) { if err := rebuildApp(installed); err != nil {
_ = env.Write(backupEnvMaps, envPath) _ = env.Write(backupEnvMaps, envPath)
_ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), constant.DirPerm) _ = fileOp.WriteFile(installed.GetComposePath(), strings.NewReader(backupDockerCompose), constant.DirPerm)
failed := oldInstalled
failed.Status = constant.StatusUpErr
failed.Message = operationErr.Error()
_ = appInstallRepo.Save(context.Background(), &failed)
}
if req.UseLifecycleScripts {
err = operateAppWithLifecycleScripts(installed, request.AppInstalledOperate{
InstallId: installed.ID,
Operate: constant.Restart,
TaskID: req.TaskID,
UseLifecycleScripts: true,
}, restoreConfig)
} else {
err = rebuildApp(installed)
}
if err != nil {
restoreConfig(err)
return err return err
} }
if !req.UseLifecycleScripts { installed.Status = constant.StatusRunning
installed.Status = constant.StatusRunning _ = appInstallRepo.Save(context.Background(), &installed)
_ = appInstallRepo.Save(context.Background(), &installed)
}
proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed) proxyChanged := hasAppInstallProxyPassChanged(&oldInstalled, &installed)
currentProxy, currentProxyErr := getAppInstallProxyPass(&installed) currentProxy, currentProxyErr := getAppInstallProxyPass(&installed)
@@ -675,9 +582,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
return versions, err return versions, err
} }
for _, detail := range details { for _, detail := range details {
if !canAccessVllmVersion(app.Key, detail.Version) {
continue
}
ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version")) ignores, _ := appIgnoreUpgradeRepo.List(runtimeRepo.WithDetailId(detail.ID), appIgnoreUpgradeRepo.WithScope("version"))
if len(ignores) > 0 { if len(ignores) > 0 {
continue continue
@@ -696,9 +600,6 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
filename := filepath.Base(detail.DownloadUrl) filename := filepath.Base(detail.DownloadUrl)
dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml") dockerComposeUrl := fmt.Sprintf("%s%s", strings.TrimSuffix(detail.DownloadUrl, filename), "docker-compose.yml")
statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s) statusCode, composeRes, err := req_helper.HandleRequest(dockerComposeUrl, http.MethodGet, constant.TimeOut20s)
if statusCode == http.StatusNotFound {
return versions, buserr.New("ErrAppVersionUnavailable")
}
if err != nil { if err != nil {
return versions, err return versions, err
} }
@@ -931,9 +832,7 @@ func (a *AppInstallService) GetParams(id uint) (*response.AppConfig, error) {
} }
func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error { func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
switch appInstall.Status { if appInstall.Status == constant.StatusInstalling || appInstall.Status == constant.StatusRebuilding || appInstall.Status == constant.StatusUpgrading || appInstall.Status == constant.StatusUninstalling {
case constant.StatusInstalling, constant.StatusRebuilding, constant.StatusUpgrading, constant.StatusUninstalling,
constant.StatusStarting, constant.StatusRestarting, constant.StatusWaiting:
return nil return nil
} }
cli, err := docker.NewClient() cli, err := docker.NewClient()
-951
View File
@@ -1,951 +0,0 @@
package service
import (
"context"
"encoding/json"
"errors"
"fmt"
"maps"
"os"
"path"
"sort"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/i18n"
"github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/compose"
"github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/docker/docker/api/types/container"
"github.com/docker/docker/api/types/filters"
"github.com/joho/godotenv"
)
type appUpgradePhase int
const (
appUpgradePreparing appUpgradePhase = iota
appUpgradePrepared
appUpgradeStopped
appUpgradeBackedUp
appUpgradeDown
appUpgradeMutated
appUpgradeStarted
appUpgradeReady
appUpgradeCommitted
)
const composeServiceLabel = "com.docker.compose.service"
var appUpgradeLocks sync.Map
type appUpgradeSnapshot interface {
Restore() error
Cleanup()
}
type upgradeFileSnapshot struct {
installPath string
backupPath string
paths []string
existing map[string]bool
}
type appUpgradeContext struct {
req request.AppInstallUpgrade
original model.AppInstall
candidate model.AppInstall
detail model.AppDetail
phase appUpgradePhase
stopAttempted bool
downAttempted bool
rollbackErr error
detailDir string
stageDir string
envContent []byte
oldEnvContent []byte
oldDockerCompose string
oldImageIDs []appImageID
backupFile string
snapshot appUpgradeSnapshot
createdPaths []string
}
func upgradeInstall(req request.AppInstallUpgrade) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if err != nil {
return err
}
if install.Status == constant.StatusUpgrading {
return buserr.New("TaskIsExecuting")
}
if err = task.CheckScopeTaskIsExecuting(task.TaskScopeApp, install.ID); err != nil {
return err
}
if _, loaded := appUpgradeLocks.LoadOrStore(install.ID, struct{}{}); loaded {
return buserr.New("TaskIsExecuting")
}
releaseLock := true
defer func() {
if releaseLock {
appUpgradeLocks.Delete(install.ID)
}
}()
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
return err
}
if err = checkVllmVersionAccess(install.App.Key, detail.Version); err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
}
if install.Version == detail.Version {
return errors.New("two version is same")
}
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
return err
}
ctx := &appUpgradeContext{
req: req,
original: install,
candidate: install,
detail: detail,
phase: appUpgradePreparing,
oldDockerCompose: install.DockerCompose,
}
upgradeTask.AddSubTaskWithOps(i18n.GetMsgByKey("UpgradePrepare"), ctx.prepare, nil, 0, 0)
upgradeTask.AddSubTaskWithOps(
task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp),
ctx.cutover,
func(t *task.Task) {
ctx.rollbackErr = ctx.rollback(t)
},
0,
0,
)
upgradingInstall := install
upgradingInstall.Status = constant.StatusUpgrading
upgradingInstall.Message = ""
if err = appInstallRepo.Save(context.Background(), &upgradingInstall); err != nil {
return err
}
releaseLock = false
go func() {
defer appUpgradeLocks.Delete(install.ID)
defer ctx.cleanup()
taskErr := upgradeTask.Execute()
if taskErr == nil {
return
}
if ctx.rollbackErr != nil {
taskErr = fmt.Errorf("%w; %s: %v", taskErr, i18n.GetMsgByKey("UpgradeRollbackFailed"), ctx.rollbackErr)
upgradeTask.Task.ErrorMsg = taskErr.Error()
_ = repo.NewITaskRepo().Update(context.Background(), upgradeTask.Task)
}
if !ctx.stopAttempted || ctx.rollbackErr == nil {
restored := ctx.original
_ = appInstallRepo.Save(context.Background(), &restored)
return
}
failed := ctx.original
failed.Status = constant.StatusUpgradeErr
failed.Message = taskErr.Error()
_ = appInstallRepo.Save(context.Background(), &failed)
}()
return nil
}
func (u *appUpgradeContext) prepare(t *task.Task) error {
fileOp := files.NewFileOp()
u.detailDir = path.Join(u.original.App.GetAppResourcePath(), u.detail.Version)
if u.original.App.Resource == constant.AppResourceRemote {
if err := downloadApp(u.original.App, u.detail, nil, t.Logger); err != nil {
return err
}
}
if !fileOp.Stat(u.detailDir) {
return buserr.WithName("ErrFileNotFound", u.detailDir)
}
if u.detail.DockerCompose == "" {
composeContent, err := fileOp.GetContent(path.Join(u.detailDir, "docker-compose.yml"))
if err != nil {
return err
}
u.detail.DockerCompose = string(composeContent)
_ = appDetailRepo.Update(context.Background(), u.detail)
}
if strings.TrimSpace(u.detail.DockerCompose) == "" && strings.TrimSpace(u.req.DockerCompose) == "" {
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
}
var err error
u.oldEnvContent, err = fileOp.GetContent(u.original.GetEnvPath())
if err != nil {
return err
}
u.stageDir, err = os.MkdirTemp(u.original.GetAppPath(), "."+u.original.Name+"-upgrade-")
if err != nil {
return err
}
if err = fileOp.CopyDirWithNewName(u.detailDir, u.stageDir, "."); err != nil {
return err
}
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, ".env"); err != nil {
return err
}
if u.original.App.Key == constant.AppOpenclaw {
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, path.Join("data", "conf", "openclaw.json")); err != nil {
return err
}
}
if u.original.App.Key == constant.AppOpenresty {
for _, relativePath := range []string{
nginxModuleBuildDir,
nginxModuleModulesDir,
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
} {
if err = copyUpgradeStageFile(u.original.GetPath(), u.stageDir, relativePath); err != nil {
return err
}
}
}
stagedInstall := u.original
stagedInstall.Name = path.Base(u.stageDir)
stagedInstall.Version = u.detail.Version
stagedInstall.AppDetailId = u.req.DetailID
if stagedInstall.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(stagedInstall.Env), &envs); err != nil {
return err
}
image := buildVllmUpgradeImage(loadVllmImageFromEnv(stagedInstall.Env), u.original.Version, u.detail.Version)
envs[vllmImageEnvKey] = image
paramBytes, marshalErr := json.Marshal(envs)
if marshalErr != nil {
return marshalErr
}
stagedInstall.Env = string(paramBytes)
}
if err = migrateOpenclawProtocolUpgrade(&stagedInstall, u.original.Version, u.detail.Version); err != nil {
return err
}
u.candidate = stagedInstall
u.candidate.Name = u.original.Name
u.candidate.DockerCompose, err = renderUpgradeCompose(u.candidate, u.detail, u.req.DockerCompose)
if err != nil {
return err
}
if strings.TrimSpace(u.candidate.DockerCompose) == "" {
return buserr.WithName("ErrFileNotFound", "docker-compose.yml")
}
u.envContent, err = renderUpgradeEnv(&u.candidate, u.oldEnvContent)
if err != nil {
return err
}
if err = writeUpgradeFile(path.Join(u.stageDir, ".env"), u.envContent, constant.FilePerm); err != nil {
return err
}
if err = writeUpgradeFile(path.Join(u.stageDir, "docker-compose.yml"), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
return err
}
project, err := docker.GetComposeProject(u.original.Name, u.stageDir, []byte(u.candidate.DockerCompose), u.envContent, false)
if err != nil {
return err
}
hasBuild := false
for _, service := range project.Services {
if service.Image == "" && service.Build == nil {
return fmt.Errorf("compose service %s has neither image nor build configuration", service.Name)
}
hasBuild = hasBuild || service.Build != nil
}
if u.req.DeleteImage {
dockerClient, clientErr := docker.NewClient()
if clientErr != nil {
return clientErr
}
u.oldImageIDs, err = getAppImageIDsByCompose(dockerClient, u.oldEnvContent, []byte(u.oldDockerCompose))
dockerClient.Close()
if err != nil {
return err
}
}
images := make([]string, 0, len(project.Services))
for _, service := range project.Services {
if service.Image != "" {
images = append(images, service.Image)
}
}
if err = prepareUpgradeImages(t, images, u.req.PullImage); err != nil {
return err
}
if u.candidate.App.Key == constant.AppOpenresty {
if err = u.prepareOpenresty(t, stagedInstall); err != nil {
return err
}
if err = verifyUpgradeImages(images); err != nil {
return err
}
} else if hasBuild {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
t.LogStart(logStr)
if err = compose.BuildWithTask(path.Join(u.stageDir, "docker-compose.yml"), project.Name, t); err != nil {
t.LogFailedWithErr(logStr, err)
return err
}
t.LogSuccess(logStr)
if err = verifyUpgradeImages(images); err != nil {
return err
}
}
if u.original.App.Resource == constant.AppResourceRemote {
go RequestDownloadCallBack(u.detail.DownloadCallBackUrl)
}
u.phase = appUpgradePrepared
return nil
}
func (u *appUpgradeContext) prepareOpenresty(t *task.Task, stagedInstall model.AppInstall) error {
fileOp := files.NewFileOp()
detailBuildDir := path.Join(u.detailDir, nginxModuleBuildDir)
installBuildDir := path.Join(u.stageDir, nginxModuleBuildDir)
if !fileOp.Stat(installBuildDir) {
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
return err
}
}
if err := copyAppDetailMissing(fileOp, detailBuildDir, installBuildDir); err != nil {
return err
}
if err := fileOp.DeleteDir(path.Join(installBuildDir, nginxModuleTmpDir)); err != nil {
return err
}
if err := fileOp.CopyDir(path.Join(detailBuildDir, nginxModuleTmpDir), installBuildDir); err != nil {
return err
}
for _, fileName := range []string{"Dockerfile", "nginx.conf", "nginx.vh.default.conf"} {
if err := fileOp.CopyFile(path.Join(detailBuildDir, fileName), installBuildDir); err != nil {
return err
}
}
if err := syncNginxModuleBuilder(detailBuildDir, installBuildDir); err != nil {
return err
}
targetCatalogSource := path.Join(detailBuildDir, nginxModuleCatalogFile)
if !fileOp.Stat(targetCatalogSource) {
return fmt.Errorf("target OpenResty module catalog not found: %s", targetCatalogSource)
}
targetCatalogPath := path.Join(installBuildDir, nginxModuleCatalogPendingFile)
if err := stageNginxModuleCatalog(targetCatalogSource, targetCatalogPath); err != nil {
return err
}
stagedInstall.Name = path.Base(u.stageDir)
stagedInstall.Version = u.candidate.Version
stagedInstall.Env = u.candidate.Env
stagedInstall.DockerCompose = u.candidate.DockerCompose
return buildNginx(t, stagedInstall, targetCatalogPath)
}
func (u *appUpgradeContext) cutover(t *task.Task) error {
u.stopAttempted = true
t.LogStart(i18n.GetMsgByKey("UpgradeStop"))
if out, err := compose.Stop(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeStop"), err)
return err
}
t.LogSuccess(i18n.GetMsgByKey("UpgradeStop"))
u.phase = appUpgradeStopped
var err error
if u.original.App.Key == constant.AppOpenresty {
u.snapshot, err = createOpenrestyUpgradeSnapshot(u.original.GetPath())
} else {
snapshotPaths := []string{".env", "docker-compose.yml", "scripts"}
if u.original.App.Key == constant.AppOpenclaw {
snapshotPaths = append(snapshotPaths, path.Join("data", "conf", "openclaw.json"))
}
u.snapshot, err = createUpgradeFileSnapshot(u.original.GetPath(), snapshotPaths)
}
if err != nil {
return err
}
if u.req.Backup {
if err = u.backup(t); err != nil {
return err
}
u.phase = appUpgradeBackedUp
} else {
t.Log(i18n.GetMsgByKey("UpgradeBackupDisabled"))
}
u.downAttempted = true
if out, downErr := compose.Down(u.original.GetComposePath()); downErr != nil {
if out != "" {
downErr = fmt.Errorf("%s: %w", out, downErr)
}
return downErr
}
u.phase = appUpgradeDown
u.phase = appUpgradeMutated
if err = u.applyStagedFiles(); err != nil {
return err
}
if err = writeUpgradeFile(u.original.GetEnvPath(), u.envContent, constant.FilePerm); err != nil {
return err
}
if err = runScript(t, &u.candidate, "upgrade"); err != nil {
return err
}
if err = writeUpgradeFile(u.original.GetComposePath(), []byte(u.candidate.DockerCompose), constant.FilePerm); err != nil {
return err
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
t.LogStart(logStr)
if out, upErr := compose.UpWithoutBuild(u.original.GetComposePath()); upErr != nil {
if out != "" {
upErr = fmt.Errorf("%s: %w", out, upErr)
}
t.LogFailedWithErr(logStr, upErr)
return upErr
}
t.LogSuccess(logStr)
u.phase = appUpgradeStarted
t.LogStart(i18n.GetMsgByKey("UpgradeWaitReady"))
containerNames, err := waitAppContainersReady(context.Background(), u.candidate)
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("UpgradeWaitReady"), err)
return err
}
t.LogSuccess(i18n.GetMsgByKey("UpgradeWaitReady"))
u.phase = appUpgradeReady
u.candidate.ContainerName = strings.Join(containerNames, ",")
u.candidate.Status = constant.StatusRunning
u.candidate.Message = ""
if u.candidate.App.Key == constant.AppOpenresty {
liveCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogPendingFile)
if err = commitStaticNginxModuleBuilds(u.candidate, liveCatalogPath, t); err != nil {
return err
}
activeCatalogPath := path.Join(u.candidate.GetPath(), nginxModuleBuildDir, nginxModuleCatalogFile)
if err = activateNginxModuleCatalogAndCommit(liveCatalogPath, activeCatalogPath, func() error {
return appInstallRepo.Save(context.Background(), &u.candidate)
}); err != nil {
return err
}
// Upgrades deliberately keep the user's nginx.conf, so corrected gzip
// defaults shipped with a new version would never reach existing
// installations. Rewrite only an untouched factory configuration, and
// never fail the upgrade over it.
if gzipErr := upgradeStockNginxGzipConfig(u.candidate); gzipErr != nil {
t.Logf("WARNING: update stock gzip configuration failed, keeping the current one: %v", gzipErr)
}
} else if err = appInstallRepo.Save(context.Background(), &u.candidate); err != nil {
return err
}
u.phase = appUpgradeCommitted
u.deleteOldImages(t)
return nil
}
func (u *appUpgradeContext) backup(t *task.Task) error {
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", u.original.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
record, err := backupAppWithParentTask(&u.original, t, fileName)
if err != nil {
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
}
u.backupFile = path.Join(global.Dir.LocalBackupDir, record.FileDir, record.FileName)
info, err := os.Stat(u.backupFile)
if err != nil || info.Size() == 0 || record.Status != constant.StatusSuccess {
if err == nil {
err = errors.New("backup archive is empty or incomplete")
}
markBackupFailed(record.ID, err)
return buserr.WithNameAndErr("ErrAppBackup", u.original.Name, err)
}
backupRecordService := NewIBackupRecordService()
backups, _ := backupRecordService.ListAppRecords(u.original.App.Key, u.original.Name, "upgrade_backup")
if len(backups) > 3 {
deleteIDs := make([]uint, 0, len(backups)-3)
for _, backup := range backups[:len(backups)-3] {
deleteIDs = append(deleteIDs, backup.ID)
}
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
}
return nil
}
func (u *appUpgradeContext) applyStagedFiles() error {
fileOp := files.NewFileOp()
if err := copyAppDetailMissingTracked(fileOp, u.detailDir, u.original.GetPath(), &u.createdPaths); err != nil {
return err
}
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), "scripts"); err != nil {
return err
}
if u.original.App.Key == constant.AppOpenclaw {
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), path.Join("data", "conf", "openclaw.json")); err != nil {
return err
}
}
if u.original.App.Key == constant.AppOpenresty {
for _, relativePath := range []string{
nginxModuleBuildDir,
nginxModuleModulesDir,
path.Join(nginxModuleConfDir, nginxModuleEnabledConfDir),
} {
if err := replaceUpgradePath(u.stageDir, u.original.GetPath(), relativePath); err != nil {
return err
}
}
}
return nil
}
func (u *appUpgradeContext) rollback(t *task.Task) (rollbackErr error) {
if !u.stopAttempted {
return nil
}
logStr := i18n.GetWithName("AppRecover", u.original.Name)
t.LogStart(logStr)
defer func() {
if rollbackErr != nil {
t.LogFailedWithErr(logStr, rollbackErr)
} else {
t.LogSuccess(logStr)
}
}()
if !u.downAttempted {
if out, err := compose.Operate(u.original.GetComposePath(), "start"); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
return err
}
return u.finishRollback()
}
if u.phase < appUpgradeMutated {
if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
return err
}
return u.finishRollback()
}
if out, err := compose.Down(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
rollbackErr = err
}
if u.backupFile != "" {
_ = u.restoreManagedFiles()
if err := handleAppRecover(&u.original, t, u.backupFile, true, "", ""); err != nil {
_, _ = compose.UpWithoutBuild(u.original.GetComposePath())
return errors.Join(rollbackErr, err)
}
} else {
if err := u.restoreManagedFiles(); err != nil {
return errors.Join(rollbackErr, err)
}
if out, err := compose.UpWithoutBuild(u.original.GetComposePath()); err != nil {
if out != "" {
err = fmt.Errorf("%s: %w", out, err)
}
return errors.Join(rollbackErr, err)
}
}
return errors.Join(rollbackErr, u.finishRollback())
}
func (u *appUpgradeContext) finishRollback() error {
if _, err := waitAppContainersReady(context.Background(), u.original); err != nil {
return err
}
restored := u.original
if err := appInstallRepo.Save(context.Background(), &restored); err != nil {
return err
}
return nil
}
func (u *appUpgradeContext) restoreManagedFiles() error {
var restoreErr error
if u.snapshot != nil {
restoreErr = u.snapshot.Restore()
}
for index := len(u.createdPaths) - 1; index >= 0; index-- {
if err := os.RemoveAll(u.createdPaths[index]); err != nil {
restoreErr = errors.Join(restoreErr, err)
}
}
return restoreErr
}
func (u *appUpgradeContext) deleteOldImages(t *task.Task) {
if !u.req.DeleteImage {
return
}
excludeImages, err := docker.GetImagesFromDockerCompose(u.envContent, []byte(u.candidate.DockerCompose))
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return
}
dockerClient, err := docker.NewClient()
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return
}
defer dockerClient.Close()
if err = deleteAppImagesByIDs(t, dockerClient, u.oldImageIDs, excludeImages); err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
}
}
func (u *appUpgradeContext) cleanup() {
if u.snapshot != nil {
u.snapshot.Cleanup()
}
if u.stageDir != "" {
_ = os.RemoveAll(u.stageDir)
}
}
type upgradeImageClient interface {
PullImageWithProcess(*task.Task, string) error
ImageExists(string) (bool, error)
Close()
}
func prepareUpgradeImages(t *task.Task, images []string, pull bool) error {
dockerClient, err := docker.NewClient()
if err != nil {
return err
}
return prepareUpgradeImagesWithClient(t, dockerClient, images, pull)
}
func prepareUpgradeImagesWithClient(t *task.Task, dockerClient upgradeImageClient, images []string, pull bool) error {
defer dockerClient.Close()
seen := make(map[string]struct{}, len(images))
for _, image := range images {
image = strings.TrimSpace(image)
if image == "" {
continue
}
if _, ok := seen[image]; ok {
continue
}
seen[image] = struct{}{}
if pull {
if t != nil {
t.Log(i18n.GetWithName("PullImageStart", image))
}
if pullErr := dockerClient.PullImageWithProcess(t, image); pullErr != nil {
if exists, _ := dockerClient.ImageExists(image); exists {
if t != nil {
t.Log(i18n.GetMsgByKey("UseExistImage"))
}
continue
}
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
}
}
exists, inspectErr := dockerClient.ImageExists(image)
if inspectErr != nil || !exists {
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
}
if pull && t != nil {
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
return nil
}
func verifyUpgradeImages(images []string) error {
dockerClient, err := docker.NewClient()
if err != nil {
return err
}
defer dockerClient.Close()
for _, image := range images {
exists, inspectErr := dockerClient.ImageExists(image)
if inspectErr != nil || !exists {
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s is not available locally: %v", image, inspectErr))
}
}
return nil
}
func renderUpgradeEnv(install *model.AppInstall, original []byte) ([]byte, error) {
originalEnv := make(map[string]string)
if len(original) > 0 {
var err error
originalEnv, err = godotenv.UnmarshalBytes(original)
if err != nil {
return nil, err
}
}
params := make(map[string]string, len(originalEnv))
maps.Copy(params, originalEnv)
envs := make(map[string]interface{})
if err := json.Unmarshal([]byte(install.Env), &envs); err != nil {
return nil, err
}
handleMap(envs, params)
if install.App.Key == "openlist" {
// The upgrade script updates this too late for the pre-pull phase.
image := "openlistteam/openlist:v" + strings.TrimPrefix(install.Version, "v")
if preInstalled := params["PRE_INSTALLED"]; preInstalled != "" {
image += "-" + preInstalled
}
params["OPENLIST_IMAGE"] = image
envs["OPENLIST_IMAGE"] = image
content, err := json.Marshal(envs)
if err != nil {
return nil, err
}
install.Env = string(content)
}
if install.App.Key == constant.AppOpenresty {
for _, key := range []string{"CONTAINER_PACKAGE_URL", "RESTY_ADD_PACKAGE_BUILDDEPS", "RESTY_CONFIG_OPTIONS_MORE"} {
if value, ok := originalEnv[key]; ok {
params[key] = value
}
}
if websiteDir := strings.TrimSpace(originalEnv["WEBSITE_DIR"]); websiteDir != "" {
params["WEBSITE_DIR"] = websiteDir
}
websiteDir := strings.TrimSpace(params["WEBSITE_DIR"])
if websiteDir == "" {
websiteDir = NewISettingService().GetWebsiteDir()
}
if !path.IsAbs(websiteDir) {
websiteDir = path.Join(global.Dir.DataDir, websiteDir)
}
params["WEBSITE_DIR"] = websiteDir
envs["WEBSITE_DIR"] = websiteDir
content, marshalErr := json.Marshal(envs)
if marshalErr != nil {
return nil, marshalErr
}
install.Env = string(content)
}
content, err := godotenv.Marshal(params)
if err != nil {
return nil, err
}
return []byte(content), nil
}
func renderUpgradeCompose(install model.AppInstall, detail model.AppDetail, customCompose string) (string, error) {
if customCompose != "" {
return customCompose, nil
}
if install.App.Key == vllmAppKeyForUpgrade {
return install.DockerCompose, nil
}
return getUpgradeCompose(install, detail)
}
func writeUpgradeFile(filePath string, content []byte, mode os.FileMode) error {
tmp, err := os.CreateTemp(path.Dir(filePath), "."+path.Base(filePath)+".*")
if err != nil {
return err
}
tmpPath := tmp.Name()
defer os.Remove(tmpPath)
if err = tmp.Chmod(mode); err == nil {
_, err = tmp.Write(content)
}
if err == nil {
err = tmp.Sync()
}
if closeErr := tmp.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return os.Rename(tmpPath, filePath)
}
func copyUpgradeStageFile(sourceRoot, targetRoot, relativePath string) error {
source := path.Join(sourceRoot, relativePath)
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
target := path.Join(targetRoot, relativePath)
_ = os.RemoveAll(target)
return copyOpenrestyUpgradeSnapshotEntry(source, target)
}
func replaceUpgradePath(sourceRoot, targetRoot, relativePath string) error {
source := path.Join(sourceRoot, relativePath)
if _, err := os.Stat(source); err != nil {
if os.IsNotExist(err) {
return nil
}
return err
}
target := path.Join(targetRoot, relativePath)
if err := os.RemoveAll(target); err != nil {
return err
}
return copyOpenrestyUpgradeSnapshotEntry(source, target)
}
func createUpgradeFileSnapshot(installPath string, paths []string) (*upgradeFileSnapshot, error) {
backupPath, err := os.MkdirTemp("", "1panel-app-upgrade-*")
if err != nil {
return nil, err
}
snapshot := &upgradeFileSnapshot{
installPath: installPath,
backupPath: backupPath,
paths: paths,
existing: make(map[string]bool, len(paths)),
}
for _, relativePath := range paths {
source := path.Join(installPath, relativePath)
if _, err = os.Stat(source); err != nil {
if os.IsNotExist(err) {
continue
}
snapshot.Cleanup()
return nil, err
}
snapshot.existing[relativePath] = true
if err = copyOpenrestyUpgradeSnapshotEntry(source, path.Join(backupPath, relativePath)); err != nil {
snapshot.Cleanup()
return nil, err
}
}
return snapshot, nil
}
func (s *upgradeFileSnapshot) Restore() error {
for _, relativePath := range s.paths {
target := path.Join(s.installPath, relativePath)
if err := os.RemoveAll(target); err != nil {
return err
}
if !s.existing[relativePath] {
continue
}
if err := copyOpenrestyUpgradeSnapshotEntry(path.Join(s.backupPath, relativePath), target); err != nil {
return err
}
}
return nil
}
func (s *upgradeFileSnapshot) Cleanup() {
if s != nil && s.backupPath != "" {
_ = os.RemoveAll(s.backupPath)
}
}
type appContainerReadinessClient interface {
ContainerList(context.Context, container.ListOptions) ([]container.Summary, error)
ContainerInspect(context.Context, string) (container.InspectResponse, error)
}
func waitAppContainersReady(ctx context.Context, install model.AppInstall) ([]string, error) {
client, err := docker.NewDockerClient()
if err != nil {
return nil, err
}
defer client.Close()
return waitAppContainersReadyWithClient(ctx, client, install)
}
func waitAppContainersReadyWithClient(ctx context.Context, client appContainerReadinessClient, install model.AppInstall) ([]string, error) {
envContent, err := os.ReadFile(install.GetEnvPath())
if err != nil {
envContent, err = renderUpgradeEnv(&install, nil)
if err != nil {
return nil, err
}
}
project, err := docker.GetComposeProject(install.Name, install.GetPath(), []byte(install.DockerCompose), envContent, false)
if err != nil {
return nil, err
}
expectedServices := make(map[string]struct{})
for _, service := range project.Services {
if !skipCheckStatus(service) {
expectedServices[service.Name] = struct{}{}
}
}
if len(expectedServices) == 0 {
return strings.Split(install.ContainerName, ","), nil
}
options := container.ListOptions{
All: true,
Filters: filters.NewArgs(
filters.Arg("label", composeWorkdirLabel+"="+install.GetPath()),
),
}
containers, err := client.ContainerList(ctx, options)
if err != nil {
return nil, err
}
foundServices := make(map[string]bool, len(expectedServices))
containerNames := make([]string, 0, len(containers))
for _, item := range containers {
serviceName := item.Labels[composeServiceLabel]
if _, ok := expectedServices[serviceName]; !ok {
continue
}
if err = waitContainerReady(ctx, client, item.ID); err != nil {
return nil, fmt.Errorf("container %s is not ready: %w", serviceName, err)
}
foundServices[serviceName] = true
if len(item.Names) > 0 {
containerNames = append(containerNames, strings.TrimPrefix(item.Names[0], "/"))
}
}
for serviceName := range expectedServices {
if !foundServices[serviceName] {
return nil, fmt.Errorf("container for service %s was not created", serviceName)
}
}
sort.Strings(containerNames)
return containerNames, nil
}
+378 -269
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"bufio"
"context" "context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
@@ -9,6 +10,7 @@ import (
"math" "math"
"net/http" "net/http"
"os" "os"
"os/exec"
"path" "path"
"path/filepath" "path/filepath"
"reflect" "reflect"
@@ -87,15 +89,7 @@ func checkPort(key string, params map[string]interface{}) (int, error) {
return 0, nil return 0, nil
} }
func isPortInUse(port int, protocol string) bool {
return common.ScanPortWithProto(port, normalizeComposeProtocol(protocol))
}
func checkPortExist(port int) error { func checkPortExist(port int) error {
return checkPortExistWithProtocol(port, "")
}
func checkPortExistWithProtocol(port int, protocol string) error {
errMap := make(map[string]interface{}) errMap := make(map[string]interface{})
errMap["port"] = port errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -116,7 +110,7 @@ func checkPortExistWithProtocol(port int, protocol string) error {
errMap["name"] = domain.Domain errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil) return buserr.WithMap("ErrPortExist", errMap, nil)
} }
if isPortInUse(port, protocol) { if common.ScanPort(port) {
return buserr.WithDetail("ErrPortInUsed", port, nil) return buserr.WithDetail("ErrPortInUsed", port, nil)
} }
return nil return nil
@@ -297,8 +291,8 @@ func createLink(ctx context.Context, installTask *task.Task, app model.App, appI
} }
resourceId = oldMysqlDb.ID resourceId = oldMysqlDb.ID
if oldMysqlDb.ID > 0 { if oldMysqlDb.ID > 0 {
if err := ensureAppMysqlDBUser(database, dbConfig); err != nil { if oldMysqlDb.Username != dbConfig.DbUser || oldMysqlDb.Password != dbConfig.Password {
return err return buserr.New("ErrDbUserNotValid")
} }
} else { } else {
var createMysql dto.MysqlDBCreate var createMysql dto.MysqlDBCreate
@@ -353,29 +347,43 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App") logStr := i18n.GetMsgByKey("Stop") + i18n.GetMsgByKey("App")
t.Log(logStr) t.Log(logStr)
if deleteReq.UseLifecycleScripts { out, err := compose.Down(install.GetComposePath())
if err = runScript(t, &install, "uninstall"); err != nil { if err != nil && !deleteReq.ForceDelete {
return err return handleErr(install, err, out)
}
} else {
out, err := compose.Down(install.GetComposePath())
if err != nil && !deleteReq.ForceDelete {
return handleErr(install, err, out)
}
if err = runScript(t, &install, "uninstall"); err != nil {
_, _ = compose.Up(install.GetComposePath())
return err
}
} }
t.LogSuccess(logStr) t.LogSuccess(logStr)
if err = runScript(t, &install, "uninstall"); err != nil {
_, _ = compose.Up(install.GetComposePath())
return err
}
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
return err return err
} }
if err = deleteAppImagesByCompose(t, content, []byte(install.DockerCompose), nil); err != nil { images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
if err != nil {
return err return err
} }
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil {
imgStr := delImageStr + image
t.Log(imgStr)
if err = client.DeleteImage(imageID); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(delImageStr + image)
}
}
} }
} }
tx, ctx := helper.GetTxAndContext() tx, ctx := helper.GetTxAndContext()
@@ -416,20 +424,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
switch install.App.Key { switch install.App.Key {
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster: case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
if err = databaseUserGrantRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserGrantRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
if err = databaseUserRepo.DeleteBy(
ctx,
repo.WithByType(install.App.Key),
databaseUserRepo.WithByDatabase(install.Name),
); err != nil {
return err
}
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name)) _ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
case constant.AppMongodb: case constant.AppMongodb:
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name)) _ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
@@ -474,107 +468,6 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return nil return nil
} }
type appImageID struct {
name string
id string
}
func getAppImageIDsByCompose(client docker.Client, envContent, composeContent []byte) ([]appImageID, error) {
images, err := docker.GetImagesFromDockerCompose(envContent, composeContent)
if err != nil {
return nil, err
}
imageIDs := make([]appImageID, 0, len(images))
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
imageIDs = append(imageIDs, appImageID{name: image, id: imageID})
}
}
return imageIDs, nil
}
func deleteAppImagesByCompose(t *task.Task, envContent, composeContent []byte, excludeImages []string) error {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageIDs, err := getAppImageIDsByCompose(client, envContent, composeContent)
if err != nil {
return err
}
return deleteAppImagesByIDs(t, client, imageIDs, excludeImages)
}
func deleteAppImagesByIDs(t *task.Task, client docker.Client, imageIDs []appImageID, excludeImages []string) error {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
excludeImageIDs := make(map[string]struct{}, len(excludeImages))
for _, image := range excludeImages {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
excludeImageIDs[imageID] = struct{}{}
}
}
deletedImageIDs := make(map[string]struct{}, len(imageIDs))
for _, image := range imageIDs {
if _, ok := excludeImageIDs[image.id]; ok {
continue
}
if _, ok := deletedImageIDs[image.id]; ok {
continue
}
deletedImageIDs[image.id] = struct{}{}
imgStr := delImageStr + image.name
t.Log(imgStr)
if err := client.DeleteImage(image.id); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(imgStr)
}
return nil
}
func ensureAppMysqlDBUser(database model.Database, dbConfig dto.AppDatabase) error {
const host = "%"
mysqlService := NewIMysqlService()
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
userExists := false
passwordValid := false
for _, user := range users {
if user.Username != dbConfig.DbUser || user.Host != host || user.IsDelete {
continue
}
userExists = true
passwordValid = user.Password == dbConfig.Password
break
}
if !userExists || !passwordValid {
return buserr.New("ErrDbUserNotValid")
}
grants, err := mysqlService.ListGrants(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
for _, grant := range grants {
if grant.Database == dbConfig.DbName && grant.Username == dbConfig.DbUser && grant.Host == host {
return nil
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbConfig.DbName,
Username: dbConfig.DbUser,
Host: host,
})
}
func deleteLink(del dto.DelAppLink) error { func deleteLink(del dto.DelAppLink) error {
install := del.Install install := del.Install
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
@@ -584,20 +477,19 @@ func deleteLink(del dto.DelAppLink) error {
for _, re := range resources { for _, re := range resources {
switch re.Key { switch re.Key {
case constant.AppMysql, constant.AppMariaDB: case constant.AppMysql, constant.AppMariaDB:
mysqlService := NewIMysqlService()
database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId)) database, _ := mysqlRepo.Get(repo.WithByID(re.ResourceId))
if reflect.DeepEqual(database, model.DatabaseMysql{}) { if reflect.DeepEqual(database, model.DatabaseMysql{}) {
continue continue
} }
if err := deleteMysqlDatabaseForResourceOwner(del.Ctx, dto.MysqlDBDelete{ if err := mysqlService.Delete(del.Ctx, dto.MysqlDBDelete{
ID: database.ID, ID: database.ID,
ForceDelete: del.ForceDelete, ForceDelete: del.ForceDelete,
DeleteBackup: true, DeleteBackup: true,
Type: re.Key, Type: re.Key,
Database: database.MysqlName, Database: database.MysqlName,
}, dto.DBResource{Type: constant.TypeApp, Name: install.Name}); err != nil { }); err != nil && !del.ForceDelete {
if isMysqlDatabaseResourceInUseError(err) || !del.ForceDelete { return err
return err
}
} }
case constant.AppPostgresql: case constant.AppPostgresql:
pgsqlService := NewIPostgresqlService() pgsqlService := NewIPostgresqlService()
@@ -672,58 +564,11 @@ func handleUpgradeCompose(install model.AppInstall, detail model.AppDetail) (map
if oldServiceValue["restart"] != nil { if oldServiceValue["restart"] != nil {
serviceValue["restart"] = oldServiceValue["restart"] serviceValue["restart"] = oldServiceValue["restart"]
} }
if install.App.Key == constant.AppOpenresty {
mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue)
}
servicesMap[install.ServiceName] = serviceValue servicesMap[install.ServiceName] = serviceValue
composeMap["services"] = servicesMap composeMap["services"] = servicesMap
return composeMap, nil return composeMap, nil
} }
// mergeOpenrestyModuleVolumes carries the dynamic module mounts of the old
// compose over to the upgraded one when it does not declare them, so built
// module artifacts and their load configuration stay mounted across upgrades.
func mergeOpenrestyModuleVolumes(serviceValue, oldServiceValue map[string]interface{}) {
oldVolumes, ok := oldServiceValue["volumes"].([]interface{})
if !ok {
return
}
newVolumes, _ := serviceValue["volumes"].([]interface{})
existing := make(map[string]struct{}, len(newVolumes))
for _, volume := range newVolumes {
if containerPath, ok := composeVolumeContainerPath(volume); ok {
existing[containerPath] = struct{}{}
}
}
for _, volume := range oldVolumes {
containerPath, ok := composeVolumeContainerPath(volume)
if !ok {
continue
}
if !strings.Contains(containerPath, nginxModuleEnabledConfDir) && !strings.Contains(containerPath, "nginx/modules/1panel") {
continue
}
if _, ok = existing[containerPath]; ok {
continue
}
newVolumes = append(newVolumes, volume)
existing[containerPath] = struct{}{}
}
serviceValue["volumes"] = newVolumes
}
func composeVolumeContainerPath(volume interface{}) (string, bool) {
volumeStr, ok := volume.(string)
if !ok {
return "", false
}
parts := strings.Split(volumeStr, ":")
if len(parts) < 2 {
return "", false
}
return parts[1], true
}
func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) { func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string, error) {
if detail.DockerCompose == "" { if detail.DockerCompose == "" {
return "", nil return "", nil
@@ -757,35 +602,340 @@ func getUpgradeCompose(install model.AppInstall, detail model.AppDetail) (string
return string(composeByte), nil return string(composeByte), nil
} }
func buildNginx(parentTask *task.Task, nginxInstall model.AppInstall, catalogPath string) error { func buildNginx(parentTask *task.Task) error {
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
if err != nil {
return err
}
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
buildPath := path.Join(nginxInstall.GetPath(), nginxModuleBuildDir) buildPath := path.Join(nginxInstall.GetPath(), "build")
if !fileOp.Stat(buildPath) { if !fileOp.Stat(buildPath) {
return buserr.New("ErrBuildDirNotFound") return buserr.New("ErrBuildDirNotFound")
} }
modules, err := loadNginxModulesWithCatalog(nginxInstall, catalogPath) moduleConfigPath := path.Join(buildPath, "module.json")
moduleContent, err := fileOp.GetContent(moduleConfigPath)
if err != nil { if err != nil {
return err return err
} }
previousModules := cloneNginxModules(modules) var (
staticBuild := hasEnabledStaticNginxModules(modules) modules []dto.NginxModule
if err = configureStaticNginxModules(nginxInstall, modules, ""); err != nil { addModuleParams []string
return err addPackages []string
} )
if staticBuild { if len(moduleContent) > 0 {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image")) _ = json.Unmarshal(moduleContent, &modules)
parentTask.LogStart(logStr) bashFile, err := os.OpenFile(path.Join(buildPath, "tmp", "pre.sh"), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.DirPerm)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(120*time.Minute)) if err != nil {
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil { return err
}
defer bashFile.Close()
bashFileWriter := bufio.NewWriter(bashFile)
for _, module := range modules {
if !module.Enable {
continue
}
_, err = bashFileWriter.WriteString(module.Script + "\n")
if err != nil {
return err
}
addModuleParams = append(addModuleParams, module.Params)
addPackages = append(addPackages, module.Packages...)
}
err = bashFileWriter.Flush()
if err != nil {
return err return err
} }
parentTask.LogSuccess(logStr)
} }
modules, err = buildDynamicNginxModules(nginxInstall, modules, nil, false, "", catalogPath, parentTask) envs, err := gotenv.Read(nginxInstall.GetEnvPath())
if err != nil { if err != nil {
return err return err
} }
return commitNginxModuleBuilds(nginxInstall, previousModules, modules, false, catalogPath) envs["RESTY_CONFIG_OPTIONS_MORE"] = ""
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = ""
if len(addModuleParams) > 0 {
envs["RESTY_CONFIG_OPTIONS_MORE"] = strings.Join(addModuleParams, " ")
}
if len(addPackages) > 0 {
envs["RESTY_ADD_PACKAGE_BUILDDEPS"] = strings.Join(addPackages, " ")
}
_ = gotenv.Write(envs, nginxInstall.GetEnvPath())
if len(addModuleParams) == 0 && len(addPackages) == 0 {
return nil
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute))
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
parentTask.LogSuccess(logStr)
return nil
}
func upgradeInstall(req request.AppInstallUpgrade) error {
install, err := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if err != nil {
return err
}
oldVersion := install.Version
detail, err := appDetailRepo.GetFirst(repo.WithByID(req.DetailID))
if err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
}
if install.Version == detail.Version {
return errors.New("two version is same")
}
upgradeTask, err := task.NewTaskWithOps(install.Name, task.TaskUpgrade, task.TaskScopeApp, req.TaskID, install.ID)
if err != nil {
return err
}
install.Status = constant.StatusUpgrading
var (
upErr error
backupFile string
)
backUpApp := func(t *task.Task) error {
backupService := NewIBackupService()
backupRecordService := NewIBackupRecordService()
fileName := fmt.Sprintf("upgrade_backup_%s_%s.tar.gz", install.Name, time.Now().Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5))
backupRecord, err := backupService.AppBackup(dto.CommonBackup{Name: install.App.Key, DetailName: install.Name, FileName: fileName})
if err == nil {
backups, _ := backupRecordService.ListAppRecords(install.App.Key, install.Name, "upgrade_backup")
if len(backups) > 3 {
backupsToDelete := backups[:len(backups)-3]
var deleteIDs []uint
for _, backup := range backupsToDelete {
deleteIDs = append(deleteIDs, backup.ID)
}
_ = backupRecordService.BatchDeleteRecord(deleteIDs)
}
backupFile = path.Join(global.Dir.LocalBackupDir, backupRecord.FileDir, backupRecord.FileName)
} else {
return buserr.WithNameAndErr("ErrAppBackup", install.Name, err)
}
return nil
}
if req.Backup {
upgradeTask.AddSubTask(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeApp), backUpApp, nil)
}
upgradeApp := func(t *task.Task) error {
fileOp := files.NewFileOp()
detailDir := path.Join(global.Dir.ResourceDir, "apps", install.App.Resource, install.App.Key, detail.Version)
if install.App.Resource == constant.AppResourceRemote {
if err = downloadApp(install.App, detail, &install, t.Logger); err != nil {
return err
}
if detail.DockerCompose == "" {
composeDetail, err := fileOp.GetContent(path.Join(detailDir, "docker-compose.yml"))
if err != nil {
return err
}
detail.DockerCompose = string(composeDetail)
_ = appDetailRepo.Update(context.Background(), detail)
}
go func() {
RequestDownloadCallBack(detail.DownloadCallBackUrl)
}()
}
if install.App.Resource == constant.AppResourceLocal {
detailDir = path.Join(global.Dir.ResourceDir, "apps", "local", strings.TrimPrefix(install.App.Key, "local"), detail.Version)
}
content, err := fileOp.GetContent(install.GetEnvPath())
if err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
return err
}
image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version)
envs[vllmImageEnvKey] = image
paramByte, err := json.Marshal(envs)
if err != nil {
return err
}
install.Env = string(paramByte)
content = setVllmImageInEnvContent(content, image)
}
if req.PullImage {
composeContent := []byte(detail.DockerCompose)
if install.App.Key == vllmAppKeyForUpgrade {
composeContent = []byte(install.DockerCompose)
}
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
images, err := docker.GetImagesFromDockerCompose(content, composeContent)
if err != nil {
return err
}
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
t.Log(i18n.GetWithName("PullImageStart", image))
if err = dockerCLi.PullImageWithProcess(t, image); err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
exist, err := dockerCLi.ImageExists(image)
if err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
if !exist {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s not found", image))
return err
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
if install.App.Key == constant.AppOpenresty {
installBuildDir := path.Join(install.GetPath(), "build")
detailBuildDir := path.Join(detailDir, "build")
if !fileOp.Stat(installBuildDir) {
if err := fileOp.CreateDir(installBuildDir, constant.DirPerm); err != nil {
return err
}
}
if err := fileOp.DeleteDir(path.Join(installBuildDir, "tmp")); err != nil {
return err
}
if err := fileOp.CopyDir(path.Join(detailBuildDir, "tmp"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "Dockerfile"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.conf"), installBuildDir); err != nil {
return err
}
if err := fileOp.CopyFile(path.Join(detailBuildDir, "nginx.vh.default.conf"), installBuildDir); err != nil {
return err
}
}
sourceScripts := path.Join(detailDir, "scripts")
if fileOp.Stat(sourceScripts) {
dstScripts := path.Join(install.GetPath(), "scripts")
_ = fileOp.DeleteDir(dstScripts)
_ = fileOp.CreateDir(dstScripts, constant.DirPerm)
scriptCmd := exec.Command("cp", "-rf", sourceScripts+"/.", dstScripts+"/")
_, _ = scriptCmd.CombinedOutput()
}
var newCompose string
if err = migrateOpenclawProtocolUpgrade(&install, oldVersion, detail.Version); err != nil {
return err
}
if req.DockerCompose == "" {
if install.App.Key == vllmAppKeyForUpgrade {
newCompose = install.DockerCompose
} else {
newCompose, err = getUpgradeCompose(install, detail)
if err != nil {
return err
}
}
} else {
newCompose = req.DockerCompose
}
install.DockerCompose = newCompose
install.Version = detail.Version
install.AppDetailId = req.DetailID
if out, err := compose.Down(install.GetComposePath()); err != nil {
if out != "" {
upErr = errors.New(out)
return upErr
}
return err
}
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
return err
}
envParams := make(map[string]string, len(envs))
if install.App.Key == constant.AppOpenresty {
packageUrl, _ := env.GetEnvValueByKey(install.GetEnvPath(), "CONTAINER_PACKAGE_URL")
addPackage, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_ADD_PACKAGE_BUILDDEPS")
options, _ := env.GetEnvValueByKey(install.GetEnvPath(), "RESTY_CONFIG_OPTIONS_MORE")
envParams["CONTAINER_PACKAGE_URL"] = packageUrl
envParams["RESTY_ADD_PACKAGE_BUILDDEPS"] = addPackage
envParams["RESTY_CONFIG_OPTIONS_MORE"] = options
}
handleMap(envs, envParams)
if err = env.Write(envParams, install.GetEnvPath()); err != nil {
return err
}
if err = runScript(t, &install, "upgrade"); err != nil {
return err
}
if err = fileOp.WriteFile(install.GetComposePath(), strings.NewReader(install.DockerCompose), constant.FilePerm); err != nil {
return err
}
if install.App.Key == constant.AppOpenresty {
if err = buildNginx(t); err != nil {
t.Log(err.Error())
return err
}
}
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("Run"), i18n.GetMsgByKey("App"))
t.Log(logStr)
if out, err := compose.Up(install.GetComposePath()); err != nil {
if out != "" {
return errors.New(out)
}
return err
}
t.LogSuccess(logStr)
install.Status = constant.StatusRunning
return appInstallRepo.Save(context.Background(), &install)
}
rollBackApp := func(t *task.Task) {
if req.Backup {
t.Log(i18n.GetWithName("AppRecover", install.Name))
if err := NewIBackupService().AppRecover(dto.CommonRecover{Name: install.App.Key, DetailName: install.Name, Type: "app", DownloadAccountID: 1, File: backupFile}); err != nil {
t.LogFailedWithErr(i18n.GetWithName("AppRecover", install.Name), err)
return
}
t.LogSuccess(i18n.GetWithName("AppRecover", install.Name))
return
}
}
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour)
go func() {
err = upgradeTask.Execute()
if err != nil {
existInstall, _ := appInstallRepo.GetFirst(repo.WithByID(req.InstallID))
if existInstall.ID > 0 && existInstall.Status != constant.StatusRunning {
existInstall.Status = constant.StatusUpgradeErr
existInstall.Message = err.Error()
_ = appInstallRepo.Save(context.Background(), &existInstall)
}
}
}()
return appInstallRepo.Save(context.Background(), &install)
} }
func skipCheckStatus(service types.ServiceConfig) bool { func skipCheckStatus(service types.ServiceConfig) bool {
@@ -1005,12 +1155,6 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
scriptPath = path.Join(workDir, "scripts", "upgrade.sh") scriptPath = path.Join(workDir, "scripts", "upgrade.sh")
case "uninstall": case "uninstall":
scriptPath = path.Join(workDir, "scripts", "uninstall.sh") scriptPath = path.Join(workDir, "scripts", "uninstall.sh")
case "start":
scriptPath = path.Join(workDir, "scripts", "start.sh")
case "stop":
scriptPath = path.Join(workDir, "scripts", "stop.sh")
case "restart":
scriptPath = path.Join(workDir, "scripts", "restart.sh")
} }
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if !fileOp.Stat(scriptPath) { if !fileOp.Stat(scriptPath) {
@@ -1020,11 +1164,7 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
logStr := i18n.GetWithName("ExecShell", operate) logStr := i18n.GetWithName("ExecShell", operate)
task.LogStart(logStr) task.LogStart(logStr)
timeout := 10 * time.Minute cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute), cmd.WithWorkDir(workDir))
if operate == "start" || operate == "restart" {
timeout = time.Hour
}
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(timeout), cmd.WithWorkDir(workDir), cmd.WithTask(*task))
if err := cmdMgr.Run("bash", scriptPath); err != nil { if err := cmdMgr.Run("bash", scriptPath); err != nil {
task.LogFailedWithErr(logStr, err) task.LogFailedWithErr(logStr, err)
return err return err
@@ -1059,15 +1199,12 @@ func checkContainerNameIsExist(containerName, appDir string) (bool, error) {
return false, nil return false, nil
} }
func upApp(task *task.Task, appInstall *model.AppInstall, pullImages, useLifecycleScripts bool) error { func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error {
upProject := func(appInstall *model.AppInstall) (err error) { upProject := func(appInstall *model.AppInstall) (err error) {
var ( var (
out string out string
errMsg string errMsg string
) )
if useLifecycleScripts {
return runScript(task, appInstall, "start")
}
if pullImages && appInstall.App.Type != "php" { if pullImages && appInstall.App.Type != "php" {
envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath()) envByte, err := files.NewFileOp().GetContent(appInstall.GetEnvPath())
if err != nil { if err != nil {
@@ -1394,8 +1531,7 @@ func handleErr(install model.AppInstall, err error, out string) error {
func doNotNeedSync(installed model.AppInstall) bool { func doNotNeedSync(installed model.AppInstall) bool {
return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading || return installed.Status == constant.StatusInstalling || installed.Status == constant.StatusRebuilding || installed.Status == constant.StatusUpgrading ||
installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr || installed.Status == constant.StatusSyncing || installed.Status == constant.StatusUninstalling || installed.Status == constant.StatusInstallErr
installed.Status == constant.StatusStarting || installed.Status == constant.StatusRestarting || installed.Status == constant.StatusWaiting
} }
func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) { func synAppInstall(containers map[string]container.Summary, appInstall *model.AppInstall, force bool) {
@@ -1497,9 +1633,6 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
synAppInstall(containersMap, &installed, false) synAppInstall(containersMap, &installed, false)
} }
resourceKeys := getAppInstallResourceKeys(installed.ID)
envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(installed.Env), &envMap)
installDTO := response.AppInstallDTO{ installDTO := response.AppInstallDTO{
ID: installed.ID, ID: installed.ID,
Name: installed.Name, Name: installed.Name,
@@ -1521,16 +1654,14 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
Website: installed.App.Website, Website: installed.App.Website,
Document: installed.App.Document, Document: installed.App.Document,
}, },
Favorite: installed.Favorite, Favorite: installed.Favorite,
SortOrder: installed.SortOrder, SortOrder: installed.SortOrder,
Container: installed.ContainerName, Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName), ServiceName: strings.ToLower(installed.ServiceName),
ResourceKeys: resourceKeys,
Env: envMap,
} }
if !updated && !checkUpdate { if !updated && !checkUpdate {
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
continue continue
} }
@@ -1538,7 +1669,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if installed.Version == "latest" { if installed.Version == "latest" {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1567,7 +1698,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if len(versions) == 0 { if len(versions) == 0 {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1599,7 +1730,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res = append(res, installDTO) res = append(res, installDTO)
} }
} else if checkUpdate { } else if checkUpdate {
installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys) installDTO.LinkDB = hasLinkDB(installed.ID)
res = append(res, installDTO) res = append(res, installDTO)
} }
} }
@@ -1842,10 +1973,6 @@ func isHostModel(dockerCompose string) bool {
} }
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error { func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
return copyAppDetailMissingTracked(fileOp, srcDir, dstDir, nil)
}
func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, createdPaths *[]string) error {
entries, err := os.ReadDir(srcDir) entries, err := os.ReadDir(srcDir)
if err != nil { if err != nil {
return err return err
@@ -1857,9 +1984,6 @@ func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, cre
srcPath := path.Join(srcDir, entry.Name()) srcPath := path.Join(srcDir, entry.Name())
dstPath := path.Join(dstDir, entry.Name()) dstPath := path.Join(dstDir, entry.Name())
if !fileOp.Stat(dstPath) { if !fileOp.Stat(dstPath) {
if createdPaths != nil {
*createdPaths = append(*createdPaths, dstPath)
}
if entry.IsDir() { if entry.IsDir() {
if err := fileOp.CopyDir(srcPath, dstDir); err != nil { if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
return err return err
@@ -1874,7 +1998,7 @@ func copyAppDetailMissingTracked(fileOp files.FileOp, srcDir, dstDir string, cre
if !entry.IsDir() { if !entry.IsDir() {
continue continue
} }
if err := copyAppDetailMissingTracked(fileOp, srcPath, dstPath, createdPaths); err != nil { if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
return err return err
} }
} }
@@ -2037,7 +2161,7 @@ func handleOpenrestyFile(appInstall *model.AppInstall) error {
func handleDefaultServer(appInstall *model.AppInstall) error { func handleDefaultServer(appInstall *model.AppInstall) error {
installDir := appInstall.GetPath() installDir := appInstall.GetPath()
defaultConfigPath := path.Join(installDir, nginxModuleConfDir, "default", "00.default.conf") defaultConfigPath := path.Join(installDir, "conf", "default", "00.default.conf")
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
content, err := fileOp.GetContent(defaultConfigPath) content, err := fileOp.GetContent(defaultConfigPath)
if err != nil { if err != nil {
@@ -2051,7 +2175,7 @@ func handleDefaultServer(appInstall *model.AppInstall) error {
} }
func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error { func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRejectHandshake bool) error {
sslDir := path.Join(appInstall.GetPath(), nginxModuleConfDir, "ssl") sslDir := path.Join(appInstall.GetPath(), "conf", "ssl")
fileOp := files.NewFileOp() fileOp := files.NewFileOp()
if !fileOp.Stat(sslDir) { if !fileOp.Stat(sslDir) {
return errors.New("ssl dir not found") return errors.New("ssl dir not found")
@@ -2081,7 +2205,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
_ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID}) _ = NewIWebsiteSSLService().Delete([]uint{websiteSSL.ID})
}() }()
} }
defaultConfigPath := path.Join(appInstall.GetPath(), nginxModuleConfDir, "default", "00.default.conf") defaultConfigPath := path.Join(appInstall.GetPath(), "conf", "default", "00.default.conf")
content, err := os.ReadFile(defaultConfigPath) content, err := os.ReadFile(defaultConfigPath)
if err != nil { if err != nil {
return err return err
@@ -2173,25 +2297,13 @@ func needsUpdate(localTag *model.Tag, remoteTag dto.Tag, translations string) bo
} }
func hasLinkDB(installID uint) bool { func hasLinkDB(installID uint) bool {
return hasLinkDBFromKeys(getAppInstallResourceKeys(installID))
}
func getAppInstallResourceKeys(installID uint) []string {
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID))
keys := make([]string, 0, len(resources))
for _, resource := range resources {
keys = append(keys, resource.Key)
}
return keys
}
func hasLinkDBFromKeys(resourceKeys []string) bool {
hasDB := false hasDB := false
if len(resourceKeys) > 0 { if len(resources) > 0 {
for _, resourceKey := range resourceKeys { for _, resource := range resources {
if resourceKey == constant.AppPostgres || resourceKey == constant.AppMysql || if resource.Key == constant.AppPostgres || resource.Key == constant.AppMysql ||
resourceKey == constant.AppMariaDB || resourceKey == constant.AppMysqlCluster || resource.Key == constant.AppMariaDB || resource.Key == constant.AppMysqlCluster ||
resourceKey == constant.AppPostgresql || resourceKey == constant.AppPostgresqlCluster { resource.Key == constant.AppPostgresql || resource.Key == constant.AppPostgresqlCluster {
hasDB = true hasDB = true
break break
} }
@@ -2249,9 +2361,6 @@ func getAppVersions(key string, details []model.AppDetail) []string {
hasLatest := false hasLatest := false
latestVersion := "" latestVersion := ""
for _, detail := range details { for _, detail := range details {
if !canAccessVllmVersion(key, detail.Version) {
continue
}
if key != "mssql" && strings.Contains(detail.Version, "latest") { if key != "mssql" && strings.Contains(detail.Version, "latest") {
hasLatest = true hasLatest = true
latestVersion = detail.Version latestVersion = detail.Version
+6 -78
View File
@@ -2,7 +2,6 @@ package service
import ( import (
"bufio" "bufio"
"context"
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -373,7 +372,7 @@ func (u *BackupService) checkBackupConn(backup *model.BackupAccount) (bool, erro
targetPath = strings.TrimPrefix(targetPath, "/") targetPath = strings.TrimPrefix(targetPath, "/")
} }
if _, err := client.Upload(context.Background(), fileItem, targetPath); err != nil { if _, err := client.Upload(fileItem, targetPath); err != nil {
return false, err return false, err
} }
_, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel")) _, _ = client.Delete(path.Join(backup.BackupPath, "test/1panel"))
@@ -438,10 +437,6 @@ type backupClientHelper struct {
} }
func NewBackupClientMap(ids []string) map[string]backupClientHelper { func NewBackupClientMap(ids []string) map[string]backupClientHelper {
return NewBackupClientMapWithContext(context.Background(), ids)
}
func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string]backupClientHelper {
var accounts []model.BackupAccount var accounts []model.BackupAccount
var idItems []uint var idItems []uint
for i := 0; i < len(ids); i++ { for i := 0; i < len(ids); i++ {
@@ -451,7 +446,7 @@ func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string
accounts, _ = backupRepo.List(repo.WithByIDs(idItems)) accounts, _ = backupRepo.List(repo.WithByIDs(idItems))
clientMap := make(map[string]backupClientHelper) clientMap := make(map[string]backupClientHelper)
for _, item := range accounts { for _, item := range accounts {
backClient, err := newClientWithContext(ctx, &item, true) backClient, err := newClient(&item, true)
itemHelper := backupClientHelper{ itemHelper := backupClientHelper{
client: backClient, client: backClient,
name: item.Name, name: item.Name,
@@ -468,11 +463,7 @@ func NewBackupClientMapWithContext(ctx context.Context, ids []string) map[string
return clientMap return clientMap
} }
func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure bool) error { func uploadWithMap(taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint) error {
return uploadWithMapWithContext(context.Background(), taskItem, accountMap, src, dst, accountIDs, downloadAccountID, retry, cleanOnFailure, true)
}
func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMap map[string]backupClientHelper, src, dst, accountIDs string, downloadAccountID, retry uint, cleanOnFailure, removeSrc bool) error {
accounts := strings.Split(accountIDs, ",") accounts := strings.Split(accountIDs, ",")
for _, account := range accounts { for _, account := range accounts {
if len(account) == 0 { if len(account) == 0 {
@@ -498,13 +489,10 @@ func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMa
"backup": name, "backup": name,
})) }))
for i := 0; i < int(retry)+1; i++ { for i := 0; i < int(retry)+1; i++ {
_, err := itemBackup.client.Upload(ctx, src, path.Join(itemBackup.backupPath, dst)) _, err := itemBackup.client.Upload(src, path.Join(itemBackup.backupPath, dst))
taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err) taskItem.LogWithStatus(i18n.GetMsgByKey("Upload"), err)
if err != nil { if err != nil {
if account == fmt.Sprintf("%d", downloadAccountID) { if account == fmt.Sprintf("%d", downloadAccountID) {
if cleanOnFailure {
cleanupCronjobBackupArtifacts(accountMap, src, dst)
}
return err return err
} }
} else { } else {
@@ -514,71 +502,11 @@ func uploadWithMapWithContext(ctx context.Context, taskItem task.Task, accountMa
itemBackup.hasBackup = true itemBackup.hasBackup = true
accountMap[account] = itemBackup accountMap[account] = itemBackup
} }
if removeSrc { os.RemoveAll(src)
os.RemoveAll(src)
}
return nil return nil
} }
func cleanupCronjobBackupArtifacts(accountMap map[string]backupClientHelper, src, dst string) {
if err := os.RemoveAll(src); err != nil {
global.LOG.Errorf("remove failed local cronjob backup file %s failed, err: %v", src, err)
}
for _, account := range accountMap {
if !account.isOk {
continue
}
if _, err := account.client.Delete(path.Join(account.backupPath, dst)); err != nil {
global.LOG.Errorf("remove failed cronjob backup file %s failed, err: %v", dst, err)
}
}
}
func markBackupFailed(recordID uint, backupErr error) {
_ = backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": backupErr.Error()})
record, err := backupRepo.GetRecord(repo.WithByID(recordID))
if err != nil || record.ID == 0 {
global.LOG.Errorf("load failed backup record %d for cleanup failed, err: %v", recordID, err)
return
}
filePath := path.Join(record.FileDir, record.FileName)
if err := os.Remove(path.Join(global.Dir.LocalBackupDir, filePath)); err != nil && !os.IsNotExist(err) {
global.LOG.Errorf("remove failed local backup file %s failed, err: %v", filePath, err)
}
cleaned := make(map[string]struct{})
for _, accountID := range strings.Split(record.SourceAccountIDs, ",") {
if accountID == "" {
continue
}
if _, ok := cleaned[accountID]; ok {
continue
}
cleaned[accountID] = struct{}{}
id, err := strconv.Atoi(accountID)
if err != nil {
global.LOG.Errorf("parse backup account %s for failed backup cleanup failed, err: %v", accountID, err)
continue
}
account, storageClient, err := NewBackupClientWithID(uint(id))
if err != nil {
global.LOG.Errorf("new backup client for failed backup cleanup failed, err: %v", err)
continue
}
if _, err := storageClient.Delete(path.Join(account.BackupPath, filePath)); err != nil {
global.LOG.Errorf("remove failed backup file %s failed, err: %v", filePath, err)
}
}
}
func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) { func newClient(account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
return newClientWithContext(context.Background(), account, isEncrypt)
}
func newClientWithContext(ctx context.Context, account *model.BackupAccount, isEncrypt bool) (cloud_storage.CloudStorageClient, error) {
varMap := make(map[string]interface{}) varMap := make(map[string]interface{})
if len(account.Vars) != 0 { if len(account.Vars) != 0 {
if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil { if err := json.Unmarshal([]byte(account.Vars), &varMap); err != nil {
@@ -603,7 +531,7 @@ func newClientWithContext(ctx context.Context, account *model.BackupAccount, isE
varMap["password"] = account.Credential varMap["password"] = account.Credential
} }
client, err := cloud_storage.NewCloudStorageClientWithContext(ctx, account.Type, varMap) client, err := cloud_storage.NewCloudStorageClient(account.Type, varMap)
if err != nil { if err != nil {
return nil, err return nil, err
} }
+10 -107
View File
@@ -63,58 +63,11 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
return nil, err return nil, err
} }
if !req.IsImmediate { if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil { global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
markBackupFailed(record.ID, err) return nil, err
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
return record, nil
} }
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
if err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil
}
func backupAppWithParentTask(install *model.AppInstall, parentTask *task.Task, fileName string) (*model.BackupRecord, error) {
itemDir := fmt.Sprintf("app/%s/%s", install.App.Key, install.Name)
backupDir := path.Join(global.Dir.LocalBackupDir, itemDir)
record := &model.BackupRecord{
Type: "app",
Name: install.App.Key,
DetailName: install.Name,
SourceAccountIDs: "1",
DownloadAccountID: 1,
FileDir: itemDir,
FileName: fileName,
TaskID: parentTask.TaskID,
Status: constant.StatusWaiting,
}
if err := backupRepo.CreateRecord(record); err != nil {
return nil, err
}
if err := handleAppBackup(install, parentTask, record.ID, backupDir, fileName, "", "", parentTask.TaskID); err != nil {
markBackupFailed(record.ID, err)
record.Status = constant.StatusFailed
record.Message = err.Error()
return record, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil return record, nil
} }
@@ -190,7 +143,7 @@ func handleAppBackup(install *model.AppInstall, parentTask *task.Task, recordID
backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour) backupTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskBackup, task.TaskScopeBackup), func(t *task.Task) error { return itemHandler() }, nil, 3, time.Hour)
go func() { go func() {
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -231,11 +184,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
return err return err
} }
defer func() { defer func() {
if isRollback { _, _ = compose.Up(install.GetComposePath())
_, _ = compose.UpWithoutBuild(install.GetComposePath())
} else {
_, _ = compose.Up(install.GetComposePath())
}
_ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", "")) _ = os.RemoveAll(strings.ReplaceAll(recoverFile, ".tar.gz", ""))
}() }()
@@ -312,7 +261,7 @@ func handleAppRecover(install *model.AppInstall, parentTask *task.Task, recoverF
if err != nil { if err != nil {
return err return err
} }
newDB, err := reCreateDB(db.ID, database, backupEnvMap, install.Name) newDB, err := reCreateDB(db.ID, database, backupEnvMap)
if err != nil { if err != nil {
return err return err
} }
@@ -440,16 +389,10 @@ func doAppBackup(install *model.AppInstall, parentTask *task.Task, backupDir, fi
return nil return nil
} }
func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}, appInstallName string) (*model.DatabaseMysql, error) { func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabaseMysql, error) {
mysqlService := NewIMysqlService() mysqlService := NewIMysqlService()
ctx := context.Background() ctx := context.Background()
if err := deleteMysqlDatabaseForResourceOwner( _ = mysqlService.Delete(ctx, dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true})
ctx,
dto.MysqlDBDelete{ID: dbID, Database: database.Name, Type: database.Type, DeleteBackup: false, ForceDelete: true},
dto.DBResource{Type: constant.TypeApp, Name: appInstallName},
); err != nil {
return nil, err
}
dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4") dbInfo := getDBCreateInfoFromEnv(envMap, "utf8mb4")
createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{ createDB, err := mysqlService.Create(context.Background(), dto.MysqlDBCreate{
@@ -457,57 +400,17 @@ func reCreateDB(dbID uint, database model.Database, envMap map[string]interface{
From: database.From, From: database.From,
Database: database.Name, Database: database.Name,
Format: dbInfo.Format, Format: dbInfo.Format,
Username: dbInfo.User,
Password: dbInfo.Password,
Permission: "%", Permission: "%",
}) })
if err != nil { if err != nil {
return nil, err return nil, err
} }
if len(dbInfo.User) != 0 {
if err := ensureMysqlDBUser(mysqlService, database, dbInfo); err != nil {
return nil, err
}
}
updateCronjobsDBRef(dbID, createDB.ID) updateCronjobsDBRef(dbID, createDB.ID)
return createDB, nil return createDB, nil
} }
func ensureMysqlDBUser(mysqlService IMysqlService, database model.Database, dbInfo dbRecreateInfo) error {
const host = "%"
users, err := mysqlService.ListUsers(dto.MysqlUserSearch{Database: database.Name})
if err != nil {
return err
}
var oldUser dto.MysqlUser
exists := false
for _, user := range users {
if user.Username == dbInfo.User && user.Host == host && !user.IsDelete {
oldUser = user
exists = true
break
}
}
if exists {
if len(oldUser.Password) != 0 && oldUser.Password != dbInfo.Password {
return buserr.New("ErrDbUserNotValid")
}
} else {
if err := mysqlService.CreateUser(dto.MysqlUserCreate{
Database: database.Name,
Username: dbInfo.User,
Host: host,
Password: dbInfo.Password,
}); err != nil {
return err
}
}
return mysqlService.GrantUser(dto.MysqlGrantCreate{
Database: database.Name,
DB: dbInfo.Name,
Username: dbInfo.User,
Host: host,
})
}
func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) { func reCreatePostgresqlDB(dbID uint, database model.Database, envMap map[string]interface{}) (*model.DatabasePostgresql, error) {
postgresqlService := NewIPostgresqlService() postgresqlService := NewIPostgresqlService()
_ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{ _ = postgresqlService.Delete(context.Background(), dto.PostgresqlDBDelete{
+2 -2
View File
@@ -98,7 +98,7 @@ func (u *BackupService) ComposeBackup(req dto.CommonBackup) error {
return err return err
} }
if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil { if err := handleComposeBackup(req, nil, record.ID, backupDir, fileName); err != nil {
markBackupFailed(record.ID, err) backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err return err
} }
return nil return nil
@@ -163,7 +163,7 @@ func handleComposeBackup(req dto.CommonBackup, parentTask *task.Task, recordID u
go func() { go func() {
defer composeCtx.close() defer composeCtx.close()
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
+34 -72
View File
@@ -106,7 +106,7 @@ func (u *BackupService) ContainerBackup(req dto.CommonBackup) error {
return err return err
} }
if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil { if err := handleContainerBackup(req.Name, nil, record.ID, backupDir, fileName, req.TaskID, req.Secret, req.StopBefore); err != nil {
markBackupFailed(record.ID, err) backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return err return err
} }
return nil return nil
@@ -158,7 +158,7 @@ func handleContainerBackup(containerName string, parentTask *task.Task, recordID
go func() { go func() {
defer backupCtx.close() defer backupCtx.close()
if err := backupTask.Execute(); err != nil { if err := backupTask.Execute(); err != nil {
markBackupFailed(recordID, err) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
return return
} }
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess}) backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
@@ -582,10 +582,6 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
if config.Image == "" { if config.Image == "" {
return fmt.Errorf("container image not found in backup file") return fmt.Errorf("container image not found in backup file")
} }
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
if err := normalizeContainerEndpointSettings(ctx, recoverCtx.client, networkConf, extraNetworks); err != nil {
return err
}
if !checkImageExist(recoverCtx.client, config.Image) { if !checkImageExist(recoverCtx.client, config.Image) {
if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil { if err := pullImages(taskItem, recoverCtx.client, config.Image); err != nil {
return err return err
@@ -600,7 +596,7 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return err return err
} }
createRes, err := createContainerWithNetworks(ctx, recoverCtx.client, config, hostConfig, networkConf, extraNetworks, recoverCtx.targetName) createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
if err != nil { if err != nil {
return err return err
} }
@@ -608,68 +604,44 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return nil return nil
} }
func normalizeContainerEndpointSettings(ctx context.Context, cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) error { func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if cli.NewVersionError(ctx, "1.44", "specify mac-address per network") != nil {
removeEndpointMacAddresses(primary, extras)
}
endpointGroups := []map[string]*network.EndpointSettings{extras}
if primary != nil { if primary != nil {
endpointGroups = append(endpointGroups, primary.EndpointsConfig) removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig)
} }
for _, endpoints := range endpointGroups { removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, extras)
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
}
info, err := cli.NetworkInspect(ctx, netName, network.InspectOptions{})
if err != nil {
return fmt.Errorf("inspect network %s failed: %w", netName, err)
}
if err := validateContainerEndpointStaticIP(netName, info, endpoint); err != nil {
return err
}
}
}
return nil
} }
func removeEndpointMacAddresses(primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) { func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
if primary != nil { for netName, endpoint := range endpoints {
for _, endpoint := range primary.EndpointsConfig { if endpoint == nil || endpoint.IPAMConfig == nil {
if endpoint != nil { continue
endpoint.MacAddress = ""
}
} }
} info, err := cli.NetworkInspect(context.Background(), netName, network.InspectOptions{})
for _, endpoint := range extras { if err != nil {
if endpoint != nil { continue
endpoint.MacAddress = ""
} }
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
} }
} }
func validateContainerEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) error { func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) {
if endpoint == nil || endpoint.IPAMConfig == nil { if endpoint == nil || endpoint.IPAMConfig == nil {
return nil return
} }
ipam := endpoint.IPAMConfig if isDefaultBridgeNetwork(netName, info) {
if err := ipam.Validate(); err != nil { endpoint.IPAMConfig = nil
return fmt.Errorf("invalid IP configuration for network %s: %w", netName, err) return
}
if ipam.IPv4Address == "" && ipam.IPv6Address == "" {
return nil
}
if netName == "host" || netName == "none" || isDefaultBridgeNetwork(netName, info) {
return fmt.Errorf("network %s does not support static IP configuration", netName)
} }
if ipam.IPv4Address != "" && !networkSupportsStaticIP(info, ipam.IPv4Address, false) { if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) {
return fmt.Errorf("static IPv4 address %s is not in a configured subnet of network %s", ipam.IPv4Address, netName) endpoint.IPAMConfig.IPv4Address = ""
} }
if ipam.IPv6Address != "" && !networkSupportsStaticIP(info, ipam.IPv6Address, true) { if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
return fmt.Errorf("static IPv6 address %s is not in a configured subnet of network %s", ipam.IPv6Address, netName) endpoint.IPAMConfig.IPv6Address = ""
}
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" {
endpoint.IPAMConfig = nil
} }
return nil
} }
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool { func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
@@ -684,7 +656,6 @@ func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool
if err != nil { if err != nil {
return false return false
} }
addr = addr.Unmap()
if addr.Is6() != isIPv6 { if addr.Is6() != isIPv6 {
return false return false
} }
@@ -805,25 +776,16 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
if name == "host" || name == "none" { if name == "host" || name == "none" {
continue continue
} }
if endpoint == nil { endpointSetting := &network.EndpointSettings{Aliases: append([]string(nil), endpoint.Aliases...), MacAddress: endpoint.MacAddress}
if name == primaryName {
config.EndpointsConfig[name] = &network.EndpointSettings{}
} else {
extraNetworks[name] = &network.EndpointSettings{}
}
continue
}
endpointSetting := &network.EndpointSettings{
Links: append([]string(nil), endpoint.Links...),
Aliases: append([]string(nil), endpoint.Aliases...),
DriverOpts: cloneStringMap(endpoint.DriverOpts),
GwPriority: endpoint.GwPriority,
}
if endpoint.IPAMConfig != nil { if endpoint.IPAMConfig != nil {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{ endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAMConfig.IPv4Address, IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address, IPv6Address: endpoint.IPAMConfig.IPv6Address,
LinkLocalIPs: append([]string(nil), endpoint.IPAMConfig.LinkLocalIPs...), }
} else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address,
} }
} }
if name == primaryName { if name == primaryName {

Some files were not shown because too many files have changed in this diff Show More