mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-28 16:00:18 +00:00
Compare commits
305
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c1a3827f18 | ||
|
|
ecedaf8c21 | ||
|
|
d7c42e3e8b | ||
|
|
d32cbb40f6 | ||
|
|
8684a26459 | ||
|
|
1798f9768e | ||
|
|
eb4beb50c4 | ||
|
|
3a7cd211af | ||
|
|
bd0bc0079e | ||
|
|
93ed58d784 | ||
|
|
8d320ef2ca | ||
|
|
b3c1e0df42 | ||
|
|
5346d5f58a | ||
|
|
ac09426f10 | ||
|
|
19707a68dd | ||
|
|
7c7fd492c4 | ||
|
|
fbf66766c2 | ||
|
|
d7355f3ccc | ||
|
|
93ca724985 | ||
|
|
9481a7535d | ||
|
|
14be5b7870 | ||
|
|
c4b76a8e89 | ||
|
|
9295f4ef8a | ||
|
|
c925184bf6 | ||
|
|
87828b03ae | ||
|
|
e7b4146e1b | ||
|
|
ec998552b9 | ||
|
|
1af237944d | ||
|
|
f9f9169a4d | ||
|
|
1e01761ba3 | ||
|
|
15283fde34 | ||
|
|
49f8e7f040 | ||
|
|
98b27af6a0 | ||
|
|
a393224729 | ||
|
|
b851c84290 | ||
|
|
372855157d | ||
|
|
577d771495 | ||
|
|
ca3e6511e5 | ||
|
|
8af14eae25 | ||
|
|
175fbaf3f2 | ||
|
|
13199efd62 | ||
|
|
04099ae2a2 | ||
|
|
c695a2a408 | ||
|
|
690cb48280 | ||
|
|
f226fed8cd | ||
|
|
110c292856 | ||
|
|
8e865c2c46 | ||
|
|
17b8f472e7 | ||
|
|
d9dc5d7aa9 | ||
|
|
547f9775b0 | ||
|
|
f37afb1a37 | ||
|
|
3b89d83554 | ||
|
|
12bfe6a1bd | ||
|
|
aaf70cfc41 | ||
|
|
1ecd4f7f0c | ||
|
|
a8ccdcade7 | ||
|
|
4a89c0c031 | ||
|
|
f21ebb1a36 | ||
|
|
3011383b07 | ||
|
|
fd12866897 | ||
|
|
bc6cf519ce | ||
|
|
f9c43989fe | ||
|
|
e90b201001 | ||
|
|
f7394938f7 | ||
|
|
dada7294dd | ||
|
|
afcb94f6ef | ||
|
|
5a2e430dff | ||
|
|
f2ca4f56e6 | ||
|
|
b6f609aaee | ||
|
|
c960c0137d | ||
|
|
09a901294e | ||
|
|
4d481a8f31 | ||
|
|
f1e0aa3391 | ||
|
|
f475500543 | ||
|
|
8415a1ce28 | ||
|
|
faf4933a02 | ||
|
|
c692a9f5e3 | ||
|
|
e7f0c21715 | ||
|
|
da7e8f1dbf | ||
|
|
486cd5d0ad | ||
|
|
00ce5a7b36 | ||
|
|
c17f055e06 | ||
|
|
f22df4b29c | ||
|
|
b9bab90a14 | ||
|
|
ce14a104cc | ||
|
|
2eaabccf94 | ||
|
|
e119239d3d | ||
|
|
14a57af4e9 | ||
|
|
2701af9054 | ||
|
|
4954067736 | ||
|
|
b962144e7d | ||
|
|
8f1be42b6b | ||
|
|
c6969f231b | ||
|
|
3571fb8d86 | ||
|
|
d21288788e | ||
|
|
f65e4b06ab | ||
|
|
d26bc9a3a4 | ||
|
|
7088a903fe | ||
|
|
864ebeed82 | ||
|
|
c1b2b92708 | ||
|
|
2189d9229a | ||
|
|
d0183f460e | ||
|
|
d27d6db1ea | ||
|
|
d2bb3813d9 | ||
|
|
cac73e6d45 | ||
|
|
4712ceaf2c | ||
|
|
65243c68c4 | ||
|
|
256e79ca81 | ||
|
|
4861eb69cb | ||
|
|
fb8cf15537 | ||
|
|
c4a6791271 | ||
|
|
f58e147636 | ||
|
|
387e9fbeed | ||
|
|
4eb627bc79 | ||
|
|
850c86229c | ||
|
|
f984917a66 | ||
|
|
8588217fbf | ||
|
|
a2307c5f64 | ||
|
|
5923290de8 | ||
|
|
1c994fba4a | ||
|
|
415ab96aab | ||
|
|
19bb823b05 | ||
|
|
3a5371652e | ||
|
|
a267b4148a | ||
|
|
36a01eb60d | ||
|
|
65f6fdd045 | ||
|
|
75b60b32e4 | ||
|
|
6cb65e2290 | ||
|
|
0bad1b471f | ||
|
|
3814525edd | ||
|
|
8162dd1856 | ||
|
|
e833787020 | ||
|
|
673ffac516 | ||
|
|
e864610015 | ||
|
|
78402e1b7d | ||
|
|
782bc1e67c | ||
|
|
86e4ed6f64 | ||
|
|
ee8bac39af | ||
|
|
fe742b9f41 | ||
|
|
9a5bd9bcba | ||
|
|
b9c8e39560 | ||
|
|
6b20ff0b13 | ||
|
|
89bd32b6d4 | ||
|
|
005f240fb7 | ||
|
|
75da53e374 | ||
|
|
2485b0aa5e | ||
|
|
ed51a5e1fa | ||
|
|
56870504ac | ||
|
|
7aefb47cc3 | ||
|
|
aba41c0aea | ||
|
|
9300bf4141 | ||
|
|
b7ec17b3e3 | ||
|
|
2fcfe56a30 | ||
|
|
63b2d4e4d5 | ||
|
|
4cd77d8ee1 | ||
|
|
53a7347bea | ||
|
|
a02c25ebcc | ||
|
|
605c8cc6db | ||
|
|
033cc7c2d1 | ||
|
|
7c1ddb5b4c | ||
|
|
eb0f5264d7 | ||
|
|
5ad12c6fe4 | ||
|
|
61dacce5e0 | ||
|
|
e3f0381a26 | ||
|
|
6bc9dd96af | ||
|
|
e23f338b31 | ||
|
|
6e08b50e3c | ||
|
|
536712cd55 | ||
|
|
191ff0cda4 | ||
|
|
671f781564 | ||
|
|
30dc36b95d | ||
|
|
fac4aec680 | ||
|
|
8eac9a1808 | ||
|
|
ce74d96617 | ||
|
|
a15e77d605 | ||
|
|
bad022f524 | ||
|
|
50a54d0613 | ||
|
|
a47e41a8b7 | ||
|
|
f938443e55 | ||
|
|
b90abd2b28 | ||
|
|
da5682a600 | ||
|
|
81b72d9b7d | ||
|
|
6e13143286 | ||
|
|
70fc628c81 | ||
|
|
9858881ce6 | ||
|
|
eb6a8c7646 | ||
|
|
a71aea8aec | ||
|
|
918c441f88 | ||
|
|
960b4b0345 | ||
|
|
3aa4bfaa82 | ||
|
|
b3bdf9ef7e | ||
|
|
2948b8ffe8 | ||
|
|
e99c6c08a5 | ||
|
|
6fb389b2ed | ||
|
|
c09833cbde | ||
|
|
fa2ad69154 | ||
|
|
51d84455a3 | ||
|
|
d88d98d8a8 | ||
|
|
5aec466c8e | ||
|
|
0ee93774d5 | ||
|
|
7be7368bb9 | ||
|
|
eab0bb4a94 | ||
|
|
9f74f2077a | ||
|
|
a6e2efa6c9 | ||
|
|
205ef3009a | ||
|
|
d7edbd1e95 | ||
|
|
b361f464c5 | ||
|
|
fb377d2e99 | ||
|
|
deddd392ba | ||
|
|
3ab10848c8 | ||
|
|
433f1a940f | ||
|
|
1f12c09eb5 | ||
|
|
31e6d523f9 | ||
|
|
3c0bd051bf | ||
|
|
3c2d92dc5f | ||
|
|
262bd14bc8 | ||
|
|
f15ff46e34 | ||
|
|
fc1ec4e1b0 | ||
|
|
53f75826d8 | ||
|
|
ddfb816ef1 | ||
|
|
18428d108e | ||
|
|
3506c5dd3b | ||
|
|
2dffd06b1b | ||
|
|
12f2484d12 | ||
|
|
2dea44acf6 | ||
|
|
205f76c65d | ||
|
|
86af4fbd4d | ||
|
|
7915230121 | ||
|
|
1b27db7daa | ||
|
|
7370dcaa55 | ||
|
|
6a378b6863 | ||
|
|
6f6747a584 | ||
|
|
825221b2bb | ||
|
|
1e9d4b592e | ||
|
|
4a51db4764 | ||
|
|
afea71c81c | ||
|
|
9c8ca2ab3c | ||
|
|
a04875f64b | ||
|
|
7ec0bdb3f7 | ||
|
|
d2dbb6486e | ||
|
|
14728f889e | ||
|
|
ff199245b0 | ||
|
|
667807e249 | ||
|
|
63e09c8c47 | ||
|
|
17a8835d59 | ||
|
|
b306bfa77a | ||
|
|
b1eff2a893 | ||
|
|
5ac7c80881 | ||
|
|
d402f67fc3 | ||
|
|
c13793c445 | ||
|
|
daa3f6b206 | ||
|
|
a2d85c911d | ||
|
|
1b76c91e1b | ||
|
|
d663a4397a | ||
|
|
d1558c5eae | ||
|
|
0da4f77a2e | ||
|
|
e7ef35740c | ||
|
|
b0d561e33b | ||
|
|
75b362fa9b | ||
|
|
466f373ef6 | ||
|
|
4489641b54 | ||
|
|
1971d9dec2 | ||
|
|
c38d741770 | ||
|
|
122a474032 | ||
|
|
54854e99e7 | ||
|
|
e01fb7c905 | ||
|
|
83a3675a0c | ||
|
|
9dfa451fae | ||
|
|
9204a287fd | ||
|
|
f027507f9a | ||
|
|
01aee89f3b | ||
|
|
17285d4397 | ||
|
|
91ae846418 | ||
|
|
1eb429631d | ||
|
|
6584e3b868 | ||
|
|
14e2294db9 | ||
|
|
26b69bc208 | ||
|
|
2111d4c16b | ||
|
|
b682835b4e | ||
|
|
c34ac2f31e | ||
|
|
c28047374a | ||
|
|
60d16609f7 | ||
|
|
02ca9347dc | ||
|
|
d0187994ee | ||
|
|
be672d604f | ||
|
|
16e3d496eb | ||
|
|
9159ab842d | ||
|
|
0d8835d494 | ||
|
|
9f9e3aacfc | ||
|
|
7bd11fe73e | ||
|
|
e11dc5fadd | ||
|
|
f35b0deb29 | ||
|
|
33b3eecb95 | ||
|
|
6ac7a5f167 | ||
|
|
a5fbbfc460 | ||
|
|
563df3da71 | ||
|
|
13e6bc4fac | ||
|
|
357d77856a | ||
|
|
4279339189 | ||
|
|
380033dfe0 | ||
|
|
97d383ed12 | ||
|
|
52e6a63ebc | ||
|
|
7506e709e2 | ||
|
|
cf37de66fc | ||
|
|
e2754b447d |
@@ -1,9 +1,6 @@
|
||||
<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">
|
||||
Trusted by <strong>2,000,000+</strong> self-hosters worldwide
|
||||
Loved by a global community of <strong>2.5M+</strong> self-hosters.
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
@@ -12,7 +9,6 @@
|
||||
|
||||
<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://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://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>
|
||||
@@ -41,34 +37,28 @@
|
||||
|
||||
## What is 1Panel?
|
||||
|
||||
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.
|
||||
|
||||
👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
|
||||
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:
|
||||
- **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.
|
||||
- **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?
|
||||
|
||||
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
||||
|--|--------|----------------|---------|--------|
|
||||
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
||||
| Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
|
||||
| AI management | ✅ | ❌ | ❌ | ❌ |
|
||||
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
||||
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
||||
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
||||
| 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
|
||||
|
||||
> **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
|
||||
> Takes ~60 seconds.
|
||||
Prepare your Linux server and run the following script:
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
||||
@@ -83,24 +73,20 @@ Run `1pctl user-info` via SSH if you need to retrieve your access credentials.
|
||||
|
||||
## Pro Edition
|
||||
|
||||
1Panel OSS is free forever. Pro adds features built for teams and production workloads:
|
||||
1Panel OSS is free forever. 1Panel Pro and Ent adds features built for teams and production workloads:
|
||||
|
||||
| Feature | OSS | Pro |
|
||||
|---------|:---:|:---:|
|
||||
| One-click app installs | ✅ | ✅ |
|
||||
| AI agents (OpenClaw) | 1 agent | Unlimited |
|
||||
| WAF & advanced security | Basic | ✅ |
|
||||
| Website tamper protection | ❌ | ✅ |
|
||||
| Website uptime monitoring | ❌ | ✅ |
|
||||
| Multi-node management | ❌ | ✅ |
|
||||
| Custom logo & theme | ❌ | ✅ |
|
||||
| Priority support | ❌ | ✅ |
|
||||
|
||||
**From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#1Panel-dev/1Panel&Date)
|
||||
| Feature | OSS | Pro | Ent |
|
||||
|---------|:---:|:---:|:---:|
|
||||
| One-click app installs | ✅ | ✅ | ✅ |
|
||||
| AI agents (OpenClaw) | 5 agent | Unlimited | ✅ |
|
||||
| WAF & advanced security | Basic | ✅ | ✅ |
|
||||
| Website tamper protection | ❌ | ✅ | ✅ |
|
||||
| Website uptime monitoring | ❌ | ✅ | ✅ |
|
||||
| Multi-node management | ❌ | ✅ | ✅ |
|
||||
| Custom logo & theme | ❌ | ✅ | ✅ |
|
||||
| KVM Web UI | ❌ | ❌ | ✅ |
|
||||
| AI Gateway | ❌ | ❌ | ✅ |
|
||||
| Priority support | ❌ | ❌ | ✅ |
|
||||
|
||||
## Community & Support
|
||||
|
||||
|
||||
@@ -826,6 +826,26 @@ func (b *BaseApi) UpdateAgentFeishuConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Switch to the bundled OpenClaw Feishu plugin
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentFeishuSwitchReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/feishu/switch [post]
|
||||
func (b *BaseApi) SwitchAgentFeishuPlugin(c *gin.Context) {
|
||||
var req dto.AgentFeishuSwitchReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.SwitchFeishuPlugin(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Telegram channel config
|
||||
// @Accept json
|
||||
@@ -1378,6 +1398,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
||||
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
|
||||
// @Summary Login Agent Weixin channel
|
||||
// @Accept json
|
||||
|
||||
+12
-16
@@ -72,22 +72,6 @@ func (b *BaseApi) CloseOllamaModel(c *gin.Context) {
|
||||
helper.SuccessWithData(c, nil)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Sync Ollama model list
|
||||
// @Success 200 {array} dto.OllamaModelDropList
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/ollama/model/sync [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Ollama 模型列表","formatEN":"sync Ollama model list"}
|
||||
func (b *BaseApi) SyncOllamaModel(c *gin.Context) {
|
||||
list, err := aiToolService.Sync()
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page Ollama models
|
||||
// @Accept json
|
||||
@@ -114,6 +98,18 @@ func (b *BaseApi) SearchOllamaModel(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
func (b *BaseApi) SyncOllamaModelAccount(c *gin.Context) {
|
||||
var req dto.OllamaModelAccountSync
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := aiToolService.SyncAccount(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Page Ollama models
|
||||
// @Accept json
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) AIBenchmarkSources(c *gin.Context) {
|
||||
sources, err := service.AIBenchmarkSources(c.Query("kind"), c.Query("selected"))
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, sources)
|
||||
}
|
||||
@@ -2,11 +2,17 @@ package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"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/repo"
|
||||
"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/webhook_sender"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -287,13 +293,43 @@ func (b *BaseApi) PageAlertConfig(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/update [post]
|
||||
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
|
||||
// @x-panel-log {"bodyKeys":["displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [displayName]","formatEN":"update alert config [displayName]"}
|
||||
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
||||
var req dto.AlertConfigUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
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":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [name][status]","formatEN":"update alert config status [name][status]"}
|
||||
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||
var req dto.AlertConfigStatusUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
name, err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c))
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -319,13 +355,14 @@ func loadAuditUser(c *gin.Context) string {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /alert/config/del [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [name]","formatEN":"delete alert config [name]"}
|
||||
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
|
||||
var req dto.DeleteRequest
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := alertService.DeleteAlertConfig(req.ID)
|
||||
name, err := alertService.DeleteAlertConfig(req.ID)
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -346,6 +383,29 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if req.Type == constant.Custom {
|
||||
lang := strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||
if lang == "" {
|
||||
lang = i18n.GetLanguageFromDB()
|
||||
}
|
||||
result, err := alertService.TestCustomAlertConfig(req, lang)
|
||||
if err != nil {
|
||||
if global.LOG != nil {
|
||||
global.LOG.Warnf("webhook test preparation failed: configID=%d error=%q", req.ID,
|
||||
webhook_sender.DiagnosticText(err.Error(), webhook_sender.Request{
|
||||
Format: webhook_sender.BodyJSON, Body: []byte(req.Config),
|
||||
}))
|
||||
}
|
||||
result = dto.AlertConfigTestResult{Success: false}
|
||||
}
|
||||
result.Response = ""
|
||||
result.Message = ""
|
||||
if !result.Success {
|
||||
result.Message = i18n.GetMsgByKeyAndLang(lang, "ErrWebhookTestFailed")
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
return
|
||||
}
|
||||
flag, err := alertService.TestAlertConfig(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := containerService.Page(req)
|
||||
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
||||
// @Success 200 {object} dto.ContainerStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param containersOnly query boolean false "Only count containers"
|
||||
// @Router /containers/status [get]
|
||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
||||
data, err := containerService.LoadStatus()
|
||||
data, err := containerService.LoadStatus(c.Request.Context(), c.Query("containersOnly") == "true")
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
||||
// @Success 200 {array} dto.ContainerListStats
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||
// @Router /containers/list/stats [get]
|
||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
||||
data, err := containerService.ContainerListStats()
|
||||
var ids []string
|
||||
if _, supplied := c.Request.URL.Query()["ids"]; supplied {
|
||||
ids = strings.FieldsFunc(c.Query("ids"), func(r rune) bool { return r == ',' })
|
||||
}
|
||||
data, err := containerService.ContainerListStats(c.Request.Context(), ids)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -439,7 +445,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := containerService.ContainerItemStats(req)
|
||||
data, err := containerService.ContainerItemStats(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -881,6 +887,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
||||
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
|
||||
// @Summary Load compose environment variables
|
||||
// @Accept json
|
||||
|
||||
@@ -32,7 +32,6 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
||||
}
|
||||
req.Password = string(password)
|
||||
}
|
||||
|
||||
if _, err := mysqlService.Create(context.Background(), req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
|
||||
@@ -86,17 +86,23 @@ func (b *BaseApi) CheckHasCli(c *gin.Context) {
|
||||
|
||||
// @Tags Database Redis
|
||||
// @Summary Install redis-cli
|
||||
// @Success 200
|
||||
// @Accept json
|
||||
// @Param request body dto.RedisCliInstall true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/redis/install/cli [post]
|
||||
func (b *BaseApi) InstallCli(c *gin.Context) {
|
||||
if err := redisService.InstallCli(); err != nil {
|
||||
var req dto.RedisCliInstall
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := redisService.InstallCli(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Database Redis
|
||||
|
||||
@@ -42,7 +42,9 @@ var (
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
firewallSettingService = service.NewIFirewallSettingService()
|
||||
forwardingService = service.NewIForwardingService()
|
||||
dockerPortGuardService = service.NewIDockerPortGuardService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||
@@ -60,6 +62,7 @@ var (
|
||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||
websiteSSLService = service.NewIWebsiteSSLService()
|
||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||
|
||||
nginxService = service.NewINginxService()
|
||||
|
||||
|
||||
+470
-23
@@ -35,6 +35,81 @@ var cancelledChunkUploads = struct {
|
||||
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
|
||||
// @Summary List files
|
||||
// @Accept json
|
||||
@@ -474,11 +549,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
||||
continue
|
||||
}
|
||||
dstInfo, statErr := os.Stat(dstFilename)
|
||||
if overwrite {
|
||||
_ = os.Remove(dstFilename)
|
||||
}
|
||||
|
||||
err = os.Rename(tmpFilename, dstFilename)
|
||||
err = finalizeUploadedFile(tmpFilename, dstFilename, overwrite)
|
||||
if err != nil {
|
||||
_ = os.Remove(tmpFilename)
|
||||
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
||||
@@ -613,10 +684,35 @@ func (b *BaseApi) StopWget(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
files.CancelDownload(req.Key)
|
||||
if err := files.CancelDownload(req.Key); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
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
|
||||
// @Summary Move file
|
||||
// @Accept json
|
||||
@@ -638,6 +734,26 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
|
||||
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
|
||||
// @Summary Download file
|
||||
// @Accept json
|
||||
@@ -792,6 +908,289 @@ func (b *BaseApi) DepthDirSize(c *gin.Context) {
|
||||
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 {
|
||||
defer func() {
|
||||
_ = os.RemoveAll(fileDir)
|
||||
@@ -871,6 +1270,10 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount {
|
||||
helper.BadRequest(c, errors.New("invalid chunk index"))
|
||||
return
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
||||
if !fileOp.Stat(tmpDir) {
|
||||
@@ -885,20 +1288,25 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||
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, errors.New("upload cancelled"))
|
||||
helper.BadRequest(c, errChunkUploadCancelled)
|
||||
return
|
||||
}
|
||||
if chunkIndex == 0 {
|
||||
if !resumable && chunkIndex == 0 {
|
||||
if fileOp.Stat(fileDir) {
|
||||
_ = fileOp.DeleteDir(fileDir)
|
||||
}
|
||||
@@ -907,32 +1315,67 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
||||
filePath := filepath.Join(fileDir, filename)
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
if !resumable && err != nil {
|
||||
_ = os.RemoveAll(fileDir)
|
||||
}
|
||||
}()
|
||||
var (
|
||||
emptyFile *os.File
|
||||
chunkData []byte
|
||||
)
|
||||
|
||||
emptyFile, err = os.Create(filePath)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
defer emptyFile.Close()
|
||||
|
||||
chunkData, err = io.ReadAll(uploadFile)
|
||||
chunkData, err := io.ReadAll(uploadFile)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||
return
|
||||
}
|
||||
if cancellable && chunkUploadCancelled(uploadID) {
|
||||
err = errors.New("upload cancelled")
|
||||
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)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
defer emptyFile.Close()
|
||||
|
||||
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
||||
@@ -985,6 +1428,10 @@ func (b *BaseApi) StopChunkUpload(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if err := removeActiveResumableUploadPart(uploadID); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
|
||||
+587
-246
@@ -1,26 +1,53 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"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
|
||||
// @Summary Load firewall base info
|
||||
// @Accept json
|
||||
// @Param request body dto.OperationWithName true "request"
|
||||
// @Success 200 {object} dto.FirewallBaseInfo
|
||||
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/base [post]
|
||||
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
var req dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
var request dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
data, err := firewallService.LoadBaseInfo(req.Name)
|
||||
data, err := firewallService.LoadBaseInfo(request.Name)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -29,311 +56,625 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||
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
|
||||
// @Summary Operate firewall
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallOperation true "request"
|
||||
// @Success 200
|
||||
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
|
||||
func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||
var req dto.FirewallOperation
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
var request dto.FirewallLifecycleOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := firewallService.OperateFirewall(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Create group
|
||||
// @Accept json
|
||||
// @Param request body dto.PortRuleOperate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/port [post]
|
||||
// @x-panel-log {"bodyKeys":["port","strategy"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"添加端口规则 [strategy] [port]","formatEN":"create port rules [strategy][port]"}
|
||||
func (b *BaseApi) OperatePortRule(c *gin.Context) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
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
|
||||
// @Summary Operate Ip rule
|
||||
// @Accept json
|
||||
// @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
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/rule/search [post]
|
||||
func (b *BaseApi) SearchFilterRules(c *gin.Context) {
|
||||
var req dto.SearchPageWithType
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := iptablesService.Search(req)
|
||||
result, err := firewallService.QueueFirewallOperation(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate iptables filter rule
|
||||
// @Summary Load forwarding base info
|
||||
// @Accept json
|
||||
// @Param request body dto.IptablesRuleOp true "request"
|
||||
// @Success 200
|
||||
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/rule/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
|
||||
func (b *BaseApi) OperateFilterRule(c *gin.Context) {
|
||||
var req dto.IptablesRuleOp
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
// @Router /hosts/firewall/forward/base [post]
|
||||
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||
data, err := forwardingService.LoadBaseInfo(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if err := iptablesService.OperateRule(req, true); err != nil {
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Page forwarding rules
|
||||
// @Accept json
|
||||
// @Param request body dto.ForwardRuleSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/search [post]
|
||||
func (b *BaseApi) SearchForwardingRules(c *gin.Context) {
|
||||
var request dto.ForwardRuleSearch
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := forwardingService.SearchRules(c.Request.Context(), request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Batch operate iptables filter rules
|
||||
// @Summary Operate forwarding rules
|
||||
// @Accept json
|
||||
// @Param request body dto.IptablesBatchOperate true "request"
|
||||
// @Success 200
|
||||
// @Param request body dto.ForwardRuleOperate true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/rule/batch [post]
|
||||
func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
|
||||
var req dto.IptablesBatchOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
// @Router /hosts/firewall/forward/operate [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
|
||||
func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
|
||||
var request dto.ForwardRuleOperate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := iptablesService.BatchOperate(req); err != nil {
|
||||
result, err := forwardingService.OperateRules(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.Success(c)
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Apply/Unload/Init iptables filter
|
||||
// @Summary Enable forwarding
|
||||
// @Accept json
|
||||
// @Param request body dto.IptablesOp true "request"
|
||||
// @Success 200
|
||||
// @Param request body dto.FirewallInitializationTask true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/forward/enable [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"初始化并启用端口转发","formatEN":"initialize and enable port forwarding"}
|
||||
func (b *BaseApi) EnableForwarding(c *gin.Context) {
|
||||
var request dto.FirewallInitializationTask
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := forwardingService.QueueInitialization(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Apply/Unload/Init firewall filter chain
|
||||
// @Accept json
|
||||
// @Param request body dto.FilterChainOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] iptables filter 防火墙","formatEN":"[operate] iptables filter firewall"}
|
||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 防火墙过滤链","formatEN":"[operate] firewall filter chain"}
|
||||
func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
||||
var req dto.IptablesOp
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
var request dto.FilterChainOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := iptablesService.Operate(req); err != nil {
|
||||
if request.Operate == "init-base" {
|
||||
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)
|
||||
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 Queue firewall rule creation
|
||||
// @Description Creation and import return a taskID immediately; validation and execution results are written to the task log.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallRuleCreate 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 Queue firewall rule deletion
|
||||
// @Description Deletes non-whitelist rules by scope and instance key. Returns a taskID immediately; results are written to the task log.
|
||||
// @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 firewall 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":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则","formatEN":"update firewall rule"}
|
||||
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Update(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary load chain status with name
|
||||
// @Summary Reorder a firewall rule
|
||||
// @Accept json
|
||||
// @Param request body dto.OperationWithName true "request"
|
||||
// @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":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序","formatEN":"reorder firewall rule"}
|
||||
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||
var request dto.FirewallRuleReorder
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallService.Reorder(c.Request.Context(), request); err != nil {
|
||||
handleFirewallRuleError(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||
var businessErr buserr.BusinessError
|
||||
isBusinessError := errors.As(err, &businessErr)
|
||||
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, 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):
|
||||
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||
case isBusinessError && businessErr.Msg == "ErrRecordExist":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_DUPLICATE", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrFirewallRuleConflict":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_RULE_CONFLICT", Message: err.Error()})
|
||||
c.Abort()
|
||||
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||
c.Abort()
|
||||
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 Create firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/filter/chain/status [post]
|
||||
func (b *BaseApi) LoadChainStatus(c *gin.Context) {
|
||||
var req dto.OperationWithName
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
// @Router /hosts/firewall/settings/whitelist [post]
|
||||
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistCreate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.CreatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, iptablesService.LoadChainStatus(req))
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall port whitelist rules
|
||||
// @Description Saves whitelist configuration and applies missing allowances; existing rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist/update [post]
|
||||
// @x-panel-log {"bodyKeys":["oldRule","rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"编辑防火墙端口白名单","formatEN":"update firewall port whitelist"}
|
||||
func (b *BaseApi) UpdateFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistUpdate
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.UpdatePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Delete firewall port whitelist rules
|
||||
// @Description Removes whitelist configuration; existing firewall rules are not removed.
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallPortWhitelistDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/whitelist/delete [post]
|
||||
// @x-panel-log {"bodyKeys":["rules"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除防火墙端口白名单","formatEN":"delete firewall port whitelist"}
|
||||
func (b *BaseApi) DeleteFirewallPortWhitelist(c *gin.Context) {
|
||||
var request dto.FirewallPortWhitelistDelete
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.DeletePortWhitelist(c.Request.Context(), request); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Operate firewall backend
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallBackendOperation true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","backend","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"防火墙子系统 [subsystem] 后端 [operation] [backend]","formatEN":"[operation] firewall [subsystem] backend [backend]"}
|
||||
func (b *BaseApi) OperateFirewallBackend(c *gin.Context) {
|
||||
var request dto.FirewallBackendOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := firewallSettingService.Operate(c.Request.Context(), request); err != nil {
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) && businessErr.Msg == "ErrFirewallBackendCleanupRequired" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: http.StatusConflict, ErrorCode: "FW_BACKEND_CLEANUP_REQUIRED", Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @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 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":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略","formatEN":"delete Docker port guard policies"}
|
||||
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) {
|
||||
var businessErr buserr.BusinessError
|
||||
if errors.As(err, &businessErr) {
|
||||
code, errorCode := http.StatusInternalServerError, ""
|
||||
switch businessErr.Msg {
|
||||
case "ErrDockerIptablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_IPTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrDockerNftablesChainUnavailable":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_NFTABLES_CHAIN_UNAVAILABLE"
|
||||
case "ErrInvalidParams":
|
||||
code, errorCode = http.StatusBadRequest, "FW_DOCKER_GUARD_INVALID"
|
||||
case "ErrDockerFailed":
|
||||
code, errorCode = http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE"
|
||||
}
|
||||
if errorCode != "" {
|
||||
c.JSON(http.StatusOK, dto.Response{Code: code, ErrorCode: errorCode, Message: err.Error()})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
if errors.Is(err, docker.ErrUnavailable) {
|
||||
helper.ErrorWithBusinessCode(c, http.StatusServiceUnavailable, "FW_DOCKER_UNAVAILABLE", "ErrDockerFailed", err)
|
||||
return
|
||||
}
|
||||
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_DOCKER_GUARD_FAILED", "ErrInternalServer", err)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary List firewall rule backups
|
||||
// @Param subsystem query string false "Firewall subsystem" Enums(system,forwarding,docker) default(system)
|
||||
// @Success 200 {object} dto.FirewallRuleBackups
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/rules/backups [get]
|
||||
func (b *BaseApi) ListFirewallRuleBackups(c *gin.Context) {
|
||||
result, err := firewallService.ListRuleBackups(c.Request.Context(), c.DefaultQuery("subsystem", "system"))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Initialize, repair or bind one firewall address family
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallFamilyOperation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/family/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["subsystem","family","operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] [subsystem] [family] 防火墙链","formatEN":"[operation] [subsystem] [family] firewall chains"}
|
||||
func (b *BaseApi) OperateFirewallFamily(c *gin.Context) {
|
||||
var request dto.FirewallFamilyOperation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateFamily(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
// @Tags Firewall
|
||||
// @Summary Update firewall IPv6 support
|
||||
// @Accept json
|
||||
// @Param request body dto.FirewallIPv6Operation true "request"
|
||||
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/firewall/settings/ipv6 [post]
|
||||
// @x-panel-log {"bodyKeys":["status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"设置防火墙 IPv6 支持为 [status]","formatEN":"Set firewall IPv6 support to [status]"}
|
||||
func (b *BaseApi) OperateFirewallIPv6(c *gin.Context) {
|
||||
var request dto.FirewallIPv6Operation
|
||||
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||
return
|
||||
}
|
||||
result, err := firewallSettingService.OperateIPv6(request)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, result)
|
||||
}
|
||||
|
||||
+9
-17
@@ -3,9 +3,8 @@ package v2
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -17,27 +16,20 @@ import (
|
||||
// @Security Timestamp
|
||||
// @Router /ai/gpu/load [get]
|
||||
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
||||
ok, client := gpu.New()
|
||||
ok, client := accelerator.New()
|
||||
if ok {
|
||||
info, err := client.LoadGpuInfo()
|
||||
snapshot, err := client.Collect(c.Request.Context())
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
if warning := snapshot.Warning(); warning != nil {
|
||||
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
|
||||
}
|
||||
helper.SuccessWithData(c, &snapshot.Info)
|
||||
return
|
||||
}
|
||||
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{})
|
||||
helper.SuccessWithData(c, &accelerator.Info{})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
|
||||
@@ -3,6 +3,7 @@ package helper
|
||||
import (
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
@@ -11,6 +12,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
@@ -21,6 +23,7 @@ import (
|
||||
)
|
||||
|
||||
func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
res := dto.Response{
|
||||
Code: code,
|
||||
Message: "",
|
||||
@@ -30,6 +33,28 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
||||
ctx.Abort()
|
||||
}
|
||||
|
||||
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
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{}) {
|
||||
mcpapi.SetBusinessErrorHeader(ctx.Request, ctx.Writer.Header(), err)
|
||||
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) {
|
||||
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
||||
}
|
||||
@@ -164,3 +189,23 @@ func GetParamInt32(paramName string, c *gin.Context) (int32, error) {
|
||||
intNum, _ := strconv.Atoi(idParam)
|
||||
return int32(intNum), nil
|
||||
}
|
||||
|
||||
// SetOperationLogValues supplies server-resolved values to Core's operation log.
|
||||
// Call before writing the response. Only pass public audit fields, never credentials.
|
||||
func SetOperationLogValues(c *gin.Context, values map[string]string) {
|
||||
const header = "X-Op-Resolved"
|
||||
resolved := make(map[string]string)
|
||||
if data, err := base64.RawURLEncoding.DecodeString(c.Writer.Header().Get(header)); err == nil {
|
||||
_ = json.Unmarshal(data, &resolved)
|
||||
}
|
||||
if resolved == nil {
|
||||
resolved = make(map[string]string)
|
||||
}
|
||||
for key, value := range values {
|
||||
resolved[key] = value
|
||||
}
|
||||
data, err := json.Marshal(resolved)
|
||||
if err == nil {
|
||||
c.Header(header, base64.RawURLEncoding.EncodeToString(data))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,137 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net/http"
|
||||
|
||||
"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/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/mcpapi"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type mcpBusinessAPI struct {
|
||||
installApp func(request.AppInstallCreate, bool) (*model.AppInstall, error)
|
||||
}
|
||||
|
||||
func MCPBusinessDispatch() gin.HandlerFunc {
|
||||
return (&mcpBusinessAPI{installApp: (&service.AppService{}).InstallForMCP}).dispatch()
|
||||
}
|
||||
|
||||
func (api *mcpBusinessAPI) dispatch() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
var handler gin.HandlerFunc
|
||||
switch c.Request.Method + " " + c.FullPath() {
|
||||
case "POST /api/v2/databases":
|
||||
handler = api.createMysql
|
||||
case "POST /api/v2/databases/status":
|
||||
handler = api.mysqlStatus
|
||||
case "GET /api/v2/databases/db/list/:type":
|
||||
handler = api.databases
|
||||
case "POST /api/v2/apps/install":
|
||||
handler = api.install
|
||||
case "GET /api/v2/apps/installed/info/:appInstallId":
|
||||
handler = api.appInfo
|
||||
default:
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
present, trusted := mcpapi.Check(c.Request)
|
||||
if !present {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
if !trusted {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
handler(c)
|
||||
c.Abort()
|
||||
}
|
||||
}
|
||||
|
||||
func (api *mcpBusinessAPI) createMysql(c *gin.Context) {
|
||||
var req dto.MysqlDBCreate
|
||||
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)
|
||||
}
|
||||
created, err := (&service.MysqlService{}).CreateForMCP(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
nodeID, _ := mcpapi.NodeID(c.Request)
|
||||
helper.SuccessWithData(c, map[string]any{"id": created.ID, "database": created.MysqlName, "name": created.Name, "nodeId": nodeID})
|
||||
}
|
||||
|
||||
func (api *mcpBusinessAPI) mysqlStatus(c *gin.Context) {
|
||||
var req dto.OperationWithNameAndType
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := (&service.MysqlService{}).LoadStatusForMCP(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (api *mcpBusinessAPI) databases(c *gin.Context) {
|
||||
dbType, err := helper.GetStrParamByKey(c, "type")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
list, err := (&service.DatabaseService{}).ListForMCP(c.Request.Context(), dbType)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, list)
|
||||
}
|
||||
|
||||
func (api *mcpBusinessAPI) install(c *gin.Context) {
|
||||
var req request.AppInstallCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if value, exists := req.Params["WEBSITE_DIR"]; exists {
|
||||
directory, ok := value.(string)
|
||||
if !ok || directory == "" || directory != settingService.GetWebsiteDir() {
|
||||
c.AbortWithStatus(http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
}
|
||||
installed, err := api.installApp(req, true)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, installed)
|
||||
}
|
||||
|
||||
func (api *mcpBusinessAPI) appInfo(c *gin.Context) {
|
||||
id, err := helper.GetIntParamByKey(c, "appInstallId")
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
info, err := (&service.AppInstallService{}).GetAppInstallInfoForMCP(c.Request.Context(), id)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/taskscope"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func PanelMCPTaskAccess() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
if hasPanelMCPTaskAccess(c.Request) {
|
||||
c.Request = c.Request.WithContext(taskscope.WithGrants(c.Request.Context(), constant.TaskScopePanelMCP))
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func hasPanelMCPTaskAccess(req *http.Request) bool {
|
||||
if !global.IsMaster {
|
||||
return false
|
||||
}
|
||||
local, ok := req.Context().Value(http.LocalAddrContextKey).(net.Addr)
|
||||
values := req.Header.Values("X-Panel-MCP-Task-Access")
|
||||
return ok && local.Network() == "unix" && len(values) == 1 && values[0] == "1"
|
||||
}
|
||||
+10
-23
@@ -1,13 +1,9 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"sort"
|
||||
|
||||
"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/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/net"
|
||||
)
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
// @Summary Clean monitor data
|
||||
// @Summary Clean host or GPU monitor data
|
||||
// @Param request body dto.MonitorClean true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/clean [post]
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空监控数据","formatEN":"clean monitor datas"}
|
||||
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 [type] 监控数据","formatEN":"clean [type] monitoring data"}
|
||||
func (b *BaseApi) CleanMonitor(c *gin.Context) {
|
||||
if err := monitorService.CleanData(); err != nil {
|
||||
var req dto.MonitorClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanData(req.Type); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/netoptions [get]
|
||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
netStat, _ := net.IOCounters(true)
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range netStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||
}
|
||||
|
||||
// @Tags Monitor
|
||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/monitor/iooptions [get]
|
||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||
diskStat, _ := disk.IOCounters()
|
||||
var options []string
|
||||
options = append(options, "all")
|
||||
for _, net := range diskStat {
|
||||
options = append(options, net.Name)
|
||||
}
|
||||
sort.Strings(options)
|
||||
helper.SuccessWithData(c, options)
|
||||
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) LoadVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMMonitorData(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (b *BaseApi) LoadVLLMCurrent(c *gin.Context) {
|
||||
var req dto.MonitorVLLMCurrent
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := monitorService.LoadVLLMCurrent(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
func (b *BaseApi) CleanVLLMMonitor(c *gin.Context) {
|
||||
var req dto.MonitorVLLMClean
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := monitorService.CleanVLLMMonitor(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary List quick directories
|
||||
// @Success 200 {array} model.QuickDirectory
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory [get]
|
||||
func (b *BaseApi) ListQuickDirectories(c *gin.Context) {
|
||||
items, err := dashboardService.ListQuickDirectories()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, items)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Create quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory [post]
|
||||
func (b *BaseApi) CreateQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := dashboardService.CreateQuickDirectory(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Update quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory/update [post]
|
||||
func (b *BaseApi) UpdateQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := dashboardService.UpdateQuickDirectory(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags Dashboard
|
||||
// @Summary Delete quick directory
|
||||
// @Accept json
|
||||
// @Param request body dto.QuickDirectoryDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /dashboard/quick/directory/del [post]
|
||||
func (b *BaseApi) DeleteQuickDirectory(c *gin.Context) {
|
||||
var req dto.QuickDirectoryDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := dashboardService.DeleteQuickDirectory(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -61,13 +61,14 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/del [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [id]","formatEN":"Delete runtime [id]"}
|
||||
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
|
||||
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
|
||||
var req request.RuntimeDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
err := runtimeService.Delete(req)
|
||||
name, err := runtimeService.Delete(req)
|
||||
helper.SetOperationLogValues(c, map[string]string{"name": name})
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -169,7 +170,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /runtimes/operate [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
|
||||
// @x-panel-log {"bodyKeys":["ID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ID","isList":false,"db":"runtimes","output_column":"name","output_value":"name"}],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
|
||||
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||
var req request.RuntimeOperate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.CreateRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
||||
}
|
||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
if err := sshService.EditRootCert(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -5,6 +5,8 @@ import (
|
||||
"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/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/service"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/taskscope"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -21,7 +23,7 @@ func (b *BaseApi) PageTasks(c *gin.Context) {
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, list, err := taskService.Page(req)
|
||||
total, list, err := taskServiceForRequest(c).Page(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -45,7 +47,7 @@ func (b *BaseApi) ReadTaskLogByLine(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
var res *response.FileLineContent
|
||||
res, err := taskService.ReadByLine(req)
|
||||
res, err := taskServiceForRequest(c).ReadByLine(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
@@ -64,10 +66,17 @@ func (b *BaseApi) ReadTaskLogByLine(c *gin.Context) {
|
||||
// @Security Timestamp
|
||||
// @Router /logs/tasks/executing/count [get]
|
||||
func (b *BaseApi) CountExecutingTasks(c *gin.Context) {
|
||||
count, err := taskService.CountExecutingTask()
|
||||
count, err := taskServiceForRequest(c).CountExecutingTask()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, count)
|
||||
}
|
||||
|
||||
func taskServiceForRequest(c *gin.Context) service.ITaskLogService {
|
||||
if granted := taskscope.Grants(c.Request.Context()); len(granted) != 0 {
|
||||
return service.NewITaskService(granted...)
|
||||
}
|
||||
return taskService
|
||||
}
|
||||
|
||||
+153
-31
@@ -1,11 +1,14 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
@@ -19,29 +22,35 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
"github.com/pkg/errors"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary Ws local terminal
|
||||
// @Param command query string false "command"
|
||||
// @Param session query string false "session id to reattach"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/local [get]
|
||||
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
|
||||
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", ""))
|
||||
}
|
||||
|
||||
// @Tags Terminal
|
||||
// @Summary Ws host SSH
|
||||
// @Param id query integer false "id"
|
||||
// @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"
|
||||
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/terminal/ssh [get]
|
||||
func (b *BaseApi) WsHostSSH(c *gin.Context) {
|
||||
b.runSSHSession(c, func() (*ssh.SSHClient, error) {
|
||||
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) {
|
||||
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
if hostID <= 0 {
|
||||
return nil, errors.New("missing host id")
|
||||
@@ -65,26 +74,33 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
slave, err := loadContainerTerminalCommand(c)
|
||||
if wshandleError(wsConn, err) {
|
||||
return
|
||||
}
|
||||
defer slave.Close()
|
||||
|
||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
||||
if wshandleError(wsConn, err) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
tty.Start(quitChan)
|
||||
go slave.Wait(quitChan)
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: "container",
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
|
||||
global.LOG.Info("websocket finished")
|
||||
closeTerminalConn(wsConn)
|
||||
func containerTerminalTarget(c *gin.Context) string {
|
||||
query := c.Request.URL.Query()
|
||||
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} {
|
||||
query.Del(key)
|
||||
}
|
||||
sum := sha256.Sum256([]byte(query.Encode()))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||
@@ -115,32 +131,138 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||
return wsConn, cols, rows, true
|
||||
}
|
||||
|
||||
func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
|
||||
func (b *BaseApi) runSSHSession(c *gin.Context, kind string, connect func() (*ssh.SSHClient, error), command string) {
|
||||
wsConn, cols, rows, ok := prepareTerminalSession(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
defer wsConn.Close()
|
||||
|
||||
client, clientErr := connect()
|
||||
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
|
||||
identity, ok := loadTerminalIdentity(c)
|
||||
if !ok {
|
||||
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||
return
|
||||
}
|
||||
defer client.Close()
|
||||
|
||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
|
||||
if wshandleError(wsConn, err) {
|
||||
hostID := 0
|
||||
if kind == "ssh" {
|
||||
hostID, _ = strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||
}
|
||||
opts := terminal.SessionOptions{
|
||||
Identity: identity,
|
||||
Kind: kind,
|
||||
Title: sanitizeTerminalTitle(c.Query("title")),
|
||||
Persistent: c.Query("terminalPersistent") == "true",
|
||||
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
|
||||
// @Summary List the caller's live terminal sessions
|
||||
// @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
|
||||
}
|
||||
defer sws.Close()
|
||||
helper.SuccessWithData(c, terminal.List(identity))
|
||||
}
|
||||
|
||||
quitChan := make(chan bool, 3)
|
||||
sws.Start(quitChan)
|
||||
go sws.Wait(quitChan)
|
||||
// @Tags Terminal
|
||||
// @Summary Close a terminal session
|
||||
// @Accept json
|
||||
// @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)
|
||||
}
|
||||
|
||||
<-quitChan
|
||||
// @Tags Terminal
|
||||
// @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)
|
||||
}
|
||||
|
||||
closeTerminalConn(wsConn)
|
||||
func (b *BaseApi) RevokeTerminalSessions(c *gin.Context) {
|
||||
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)),
|
||||
}
|
||||
if value := c.GetHeader(terminal.HeaderAuthLeaseUntil); value != "" {
|
||||
millis, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil || millis <= 0 {
|
||||
return terminal.Identity{}, false
|
||||
}
|
||||
identity.AuthLeaseUntil = time.UnixMilli(millis)
|
||||
if maximum := time.Now().Add(90 * time.Second); identity.AuthLeaseUntil.After(maximum) {
|
||||
identity.AuthLeaseUntil = maximum
|
||||
}
|
||||
}
|
||||
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) {
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
func (b *BaseApi) TerminalCapabilities(c *gin.Context) {
|
||||
helper.SuccessWithData(c, gin.H{"apiKeyLeaseVersion": 1})
|
||||
}
|
||||
@@ -402,7 +402,7 @@ func (b *BaseApi) ChangeDefaultServer(c *gin.Context) {
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /websites/php/version [post]
|
||||
// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||
// @x-panel-log {"bodyKeys":["websiteID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteID","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"php 版本变更 [domain]","formatEN":"php version update [domain]"}
|
||||
func (b *BaseApi) ChangePHPVersion(c *gin.Context) {
|
||||
var req request.WebsitePHPVersionReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"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/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
@@ -176,3 +179,64 @@ func (b *BaseApi) DownloadCAFile(c *gin.Context) {
|
||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(info.Name()))
|
||||
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
|
||||
}
|
||||
|
||||
// @Tags Website CA
|
||||
// @Summary Upload website CA archive
|
||||
// @Accept multipart/form-data
|
||||
// @Param file formData file true "ZIP containing ca.crt and ca.key"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /websites/ca/upload [post]
|
||||
func (b *BaseApi) UploadWebsiteCA(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 11<<20)
|
||||
if err := c.Request.ParseMultipartForm(11 << 20); err != nil {
|
||||
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||
return
|
||||
}
|
||||
if c.Request.MultipartForm != nil {
|
||||
defer c.Request.MultipartForm.RemoveAll()
|
||||
}
|
||||
file, err := c.FormFile("file")
|
||||
if err != nil || file.Size > 10<<20 || !strings.HasSuffix(strings.ToLower(file.Filename), ".zip") {
|
||||
helper.InternalServer(c, buserr.New("ErrCAUpload"))
|
||||
return
|
||||
}
|
||||
stream, err := file.Open()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
defer stream.Close()
|
||||
data, err := io.ReadAll(io.LimitReader(stream, (10<<20)+1))
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
if err = websiteCAService.Upload(file.Filename[:len(file.Filename)-len(".zip")], data); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Website CA
|
||||
// @Summary Import website CA from PEM or server files
|
||||
// @Accept json
|
||||
// @Param request body request.WebsiteCAImport true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /websites/ca/import [post]
|
||||
func (b *BaseApi) ImportWebsiteCA(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 3<<20)
|
||||
var req request.WebsiteCAImport
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := websiteCAService.Import(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
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)
|
||||
}
|
||||
+88
-3
@@ -9,7 +9,8 @@ type AgentCreateReq struct {
|
||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
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 deepseek-harness octop"`
|
||||
AccessHost string `json:"accessHost"`
|
||||
Model string `json:"model"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Token string `json:"token"`
|
||||
@@ -110,6 +111,7 @@ type AgentBatchOperateResult struct {
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
AccessHost string `json:"accessHost"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
@@ -166,12 +168,21 @@ type AgentModelConfigUpdateReq struct {
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||
}
|
||||
|
||||
type AgentModelConfig struct {
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
Metadata []AgentModelMetadata `json:"metadata"`
|
||||
}
|
||||
|
||||
type AgentModelMetadata struct {
|
||||
Model string `json:"model" validate:"required"`
|
||||
InputMode string `json:"inputMode" validate:"required,oneof=auto text image"`
|
||||
ContextWindow int `json:"contextWindow" validate:"min=0"`
|
||||
MaxTokens int `json:"maxTokens" validate:"min=0"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionItem struct {
|
||||
@@ -331,6 +342,7 @@ type AgentAccountCreateReq struct {
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
ValidateAvailability *bool `json:"validateAvailability"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
@@ -343,6 +355,7 @@ type AgentAccountUpdateReq struct {
|
||||
APIType string `json:"apiType" validate:"required"`
|
||||
AuthMode string `json:"authMode"`
|
||||
VerifyModel string `json:"verifyModel"`
|
||||
ValidateAvailability *bool `json:"validateAvailability"`
|
||||
Remark string `json:"remark"`
|
||||
SyncAgents bool `json:"syncAgents"`
|
||||
}
|
||||
@@ -363,6 +376,8 @@ type AgentAccountDeleteReq struct {
|
||||
type AgentAccountSearch struct {
|
||||
PageInfo
|
||||
Provider string `json:"provider"`
|
||||
APIType string `json:"apiType"`
|
||||
TextOnly bool `json:"textOnly"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
@@ -371,6 +386,7 @@ type AgentAccountProviderCountReq struct {
|
||||
}
|
||||
|
||||
type AgentAccountInfo struct {
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
ID uint `json:"id"`
|
||||
MasterAccountID uint `json:"masterAccountId"`
|
||||
Provider string `json:"provider"`
|
||||
@@ -397,8 +413,10 @@ type ProviderAPIInfo struct {
|
||||
APIType string `json:"apiType"`
|
||||
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 {
|
||||
@@ -415,7 +433,23 @@ type AgentFeishuConfigReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentFeishuTransition struct {
|
||||
Confirmed bool `json:"confirmed"`
|
||||
Streaming bool `json:"streaming"`
|
||||
GroupSessionScope string `json:"groupSessionScope"`
|
||||
RequireMention string `json:"requireMention"`
|
||||
}
|
||||
|
||||
type AgentFeishuSwitchReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
Streaming bool `json:"streaming"`
|
||||
GroupSessionScope string `json:"groupSessionScope" validate:"required,oneof=group group_sender group_topic group_topic_sender"`
|
||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false"`
|
||||
}
|
||||
|
||||
type AgentFeishuConfigUpdateReq struct {
|
||||
GroupSessionScope string `json:"groupSessionScope" validate:"omitempty,oneof=group group_sender group_topic group_topic_sender"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
@@ -430,6 +464,11 @@ type AgentFeishuConfigUpdateReq struct {
|
||||
}
|
||||
|
||||
type AgentFeishuConfig struct {
|
||||
BuiltinManaged bool `json:"builtinManaged"`
|
||||
LegacyConfigured bool `json:"legacyConfigured"`
|
||||
GroupSessionScope string `json:"groupSessionScope"`
|
||||
BuiltinAvailable bool `json:"builtinAvailable"`
|
||||
Legacy bool `json:"legacy"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode"`
|
||||
@@ -598,6 +637,52 @@ type AgentPluginStatus struct {
|
||||
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 {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
@@ -678,9 +763,9 @@ type AgentSecurityConfig struct {
|
||||
|
||||
type AgentOtherConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
||||
UserTimezone string `json:"userTimezone"`
|
||||
BrowserEnabled bool `json:"browserEnabled"`
|
||||
NPMRegistry string `json:"npmRegistry" validate:"required"`
|
||||
NPMRegistry string `json:"npmRegistry"`
|
||||
DashboardUsername string `json:"dashboardUsername"`
|
||||
DashboardPassword string `json:"dashboardPassword"`
|
||||
}
|
||||
|
||||
+6
-5
@@ -14,16 +14,17 @@ type OllamaModelInfo struct {
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type OllamaModelDropList struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type OllamaModelName struct {
|
||||
Name string `json:"name"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type OllamaModelAccountSync struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"required,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
}
|
||||
|
||||
type OllamaBindDomain struct {
|
||||
Domain string `json:"domain" validate:"required"`
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
|
||||
@@ -0,0 +1,13 @@
|
||||
package dto
|
||||
|
||||
type AIBenchmarkSource struct {
|
||||
ID string `json:"id"`
|
||||
Kind string `json:"kind"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
Models []string `json:"models"`
|
||||
Tokenizer string `json:"tokenizer"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
@@ -21,6 +21,7 @@ type AlertBase struct {
|
||||
}
|
||||
|
||||
type PushAlert struct {
|
||||
Result string `json:"result,omitempty"`
|
||||
TaskName string `json:"taskName"`
|
||||
AlertType string `json:"alertType"`
|
||||
EntryID uint `json:"entryID"`
|
||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
||||
Method string `json:"method"`
|
||||
Title string `json:"title"`
|
||||
Project string `json:"project"`
|
||||
TaskName string `json:"taskName,omitempty"`
|
||||
Status string `json:"status"`
|
||||
SendCount uint `json:"sendCount"`
|
||||
AdvancedParams string `json:"advancedParams"`
|
||||
@@ -115,6 +117,8 @@ type AlertLogSearch struct {
|
||||
PageInfo
|
||||
Count uint `json:"count"`
|
||||
Status string `json:"status"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
|
||||
type AlertLogDTO struct {
|
||||
@@ -159,6 +163,15 @@ type AlertDetail struct {
|
||||
Project string `json:"project"`
|
||||
Params []Param `json:"params"`
|
||||
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 {
|
||||
@@ -299,9 +312,13 @@ type AlertConfigUpdate struct {
|
||||
Status string `json:"status"`
|
||||
Config string `json:"config"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Revision *time.Time `json:"revision"`
|
||||
}
|
||||
|
||||
type AlertConfigTest struct {
|
||||
ID uint `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Config string `json:"config"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Sender string `json:"sender"`
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
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"`
|
||||
}
|
||||
@@ -43,6 +43,8 @@ type AppOssConfig struct {
|
||||
}
|
||||
|
||||
type AppVersion struct {
|
||||
FeishuLegacy bool `json:"feishuLegacy,omitempty"`
|
||||
FeishuConfigured bool `json:"feishuConfigured,omitempty"`
|
||||
Version string `json:"version"`
|
||||
DetailId uint `json:"detailId"`
|
||||
DockerCompose string `json:"dockerCompose"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package dto
|
||||
type SearchWithPage struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||
}
|
||||
|
||||
type SearchPageWithType struct {
|
||||
|
||||
@@ -7,6 +7,7 @@ type PageResult struct {
|
||||
|
||||
type Response struct {
|
||||
Code int `json:"code"`
|
||||
ErrorCode string `json:"errorCode,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Data interface{} `json:"data"`
|
||||
}
|
||||
|
||||
@@ -172,6 +172,7 @@ type ContainerItemStats struct {
|
||||
}
|
||||
type ContainerListStats struct {
|
||||
ContainerID string `json:"containerID"`
|
||||
ContainerName string `json:"containerName"`
|
||||
|
||||
CPUTotalUsage uint64 `json:"cpuTotalUsage"`
|
||||
SystemUsage uint64 `json:"systemUsage"`
|
||||
@@ -300,6 +301,7 @@ type ComposeInfo struct {
|
||||
ConfigFile string `json:"configFile"`
|
||||
Workdir string `json:"workdir"`
|
||||
ComposeFileExists bool `json:"composeFileExists"`
|
||||
IsPinned bool `json:"isPinned"`
|
||||
Path string `json:"path"`
|
||||
Containers []ComposeContainer `json:"containers"`
|
||||
Env string `json:"env"`
|
||||
@@ -314,6 +316,7 @@ type ComposeContainer struct {
|
||||
type ComposeCreate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Name string `json:"name"`
|
||||
DirName string `json:"dirName"`
|
||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||
File string `json:"file"`
|
||||
Path string `json:"path"`
|
||||
@@ -337,6 +340,10 @@ type ComposeUpdate struct {
|
||||
Env string `json:"env"`
|
||||
ForcePull bool `json:"forcePull"`
|
||||
}
|
||||
type ComposePin struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IsPinned bool `json:"isPinned"`
|
||||
}
|
||||
type ComposeLogClean struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
|
||||
@@ -54,6 +54,7 @@ type CronjobOperate struct {
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
|
||||
CleanLogConfig
|
||||
}
|
||||
@@ -127,6 +128,7 @@ type CronjobInfo struct {
|
||||
Args string `json:"args"`
|
||||
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||
}
|
||||
|
||||
type CronjobImport struct {
|
||||
@@ -172,6 +174,7 @@ type CronjobTrans struct {
|
||||
AlertCount uint `json:"alertCount"`
|
||||
AlertTitle string `json:"alertTitle"`
|
||||
AlertMethod string `json:"alertMethod"`
|
||||
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||
}
|
||||
type TransHelper struct {
|
||||
Name string `json:"name"`
|
||||
@@ -197,6 +200,7 @@ type SearchRecord struct {
|
||||
|
||||
type Record struct {
|
||||
ID uint `json:"id"`
|
||||
CronjobID uint `json:"cronjobID"`
|
||||
TaskID string `json:"taskID"`
|
||||
StartTime string `json:"startTime"`
|
||||
Records string `json:"records"`
|
||||
|
||||
@@ -31,7 +31,27 @@ type DashboardBase struct {
|
||||
}
|
||||
|
||||
type ChangeQuicks struct {
|
||||
Quicks []QuickJump `json:"quicks"`
|
||||
Quicks []QuickVisibility `json:"quicks" validate:"required,dive"`
|
||||
}
|
||||
|
||||
type QuickVisibility struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
IsShow bool `json:"isShow"`
|
||||
}
|
||||
|
||||
type QuickDirectoryCreate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Alias string `json:"alias"`
|
||||
}
|
||||
|
||||
type QuickDirectoryDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Legacy bool `json:"legacy"`
|
||||
}
|
||||
|
||||
type QuickDirectoryUpdate struct {
|
||||
QuickDirectoryDelete
|
||||
QuickDirectoryCreate
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
@@ -121,6 +141,7 @@ type DashboardCurrent struct {
|
||||
NetBytesRecv uint64 `json:"netBytesRecv"`
|
||||
|
||||
GPUData []GPUInfo `json:"gpuData"`
|
||||
NPUData []NPUInfo `json:"npuData"`
|
||||
XPUData []XPUInfo `json:"xpuData"`
|
||||
|
||||
TopCPUItems []Process `json:"topCPUItems"`
|
||||
@@ -156,8 +177,12 @@ type DiskInfo struct {
|
||||
}
|
||||
|
||||
type GPUInfo 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"`
|
||||
GPUUtil string `json:"gpuUtil"`
|
||||
Temperature string `json:"temperature"`
|
||||
PerformanceState string `json:"performanceState"`
|
||||
@@ -170,6 +195,27 @@ type GPUInfo struct {
|
||||
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 {
|
||||
Key string `json:"key"`
|
||||
Type string `json:"type"`
|
||||
@@ -204,8 +250,10 @@ type LauncherOption struct {
|
||||
type XPUInfo struct {
|
||||
DeviceID int `json:"deviceID"`
|
||||
DeviceName string `json:"deviceName"`
|
||||
PciBdfAddress string `json:"pciBdfAddress"`
|
||||
Memory string `json:"memory"`
|
||||
Temperature string `json:"temperature"`
|
||||
GPUUtil string `json:"gpuUtil"`
|
||||
MemoryUsed string `json:"memoryUsed"`
|
||||
Power string `json:"power"`
|
||||
MemoryUtil string `json:"memoryUtil"`
|
||||
|
||||
@@ -22,6 +22,10 @@ type DBBaseInfo struct {
|
||||
Port int64 `json:"port"`
|
||||
}
|
||||
|
||||
type RedisCliInstall struct {
|
||||
TaskID string `json:"taskID" validate:"omitempty,uuid"`
|
||||
}
|
||||
|
||||
// mysql
|
||||
type MysqlDBSearch struct {
|
||||
PageInfo
|
||||
|
||||
+317
-76
@@ -1,113 +1,354 @@
|
||||
package dto
|
||||
|
||||
type FirewallBaseInfo struct {
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||
dockerfirewall "github.com/1Panel-dev/1Panel/agent/utils/firewall/docker_guard"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/firewall/forwarding"
|
||||
)
|
||||
|
||||
type FirewallSubsystemStatus struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
Name string `json:"name"`
|
||||
Backend string `json:"backend"`
|
||||
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"`
|
||||
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
|
||||
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 {
|
||||
type FirewallLifecycleOperation struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||
WithDockerRestart bool `json:"withDockerRestart"`
|
||||
}
|
||||
|
||||
type PortRuleOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
Chain string `json:"chain"`
|
||||
Address string `json:"address"`
|
||||
Port string `json:"port" validate:"required"`
|
||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
||||
|
||||
Description string `json:"description"`
|
||||
type FirewallLifecycleOperationResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperate struct {
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
Rules []struct {
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
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 FirewallBackendOption struct {
|
||||
Name string `json:"name"`
|
||||
Installed bool `json:"installed"`
|
||||
Active bool `json:"active"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Supported bool `json:"supported"`
|
||||
SupportReason string `json:"supportReason,omitempty"`
|
||||
Implementation string `json:"implementation,omitempty"`
|
||||
Message string `json:"message,omitempty"`
|
||||
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||
}
|
||||
|
||||
type UpdateFirewallDescription struct {
|
||||
Type string `json:"type"`
|
||||
Chain string `json:"chain"`
|
||||
SrcIP string `json:"srcIP"`
|
||||
DstIP string `json:"dstIP"`
|
||||
SrcPort string `json:"srcPort"`
|
||||
DstPort string `json:"dstPort"`
|
||||
type FirewallBackendFamilyStatus struct {
|
||||
Partial bool `json:"partial"`
|
||||
Available bool `json:"available"`
|
||||
Initialized bool `json:"initialized"`
|
||||
Bound bool `json:"bound"`
|
||||
Reason string `json:"reason,omitempty"`
|
||||
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallBackendGroup struct {
|
||||
Selected string `json:"selected"`
|
||||
Current string `json:"current,omitempty"`
|
||||
Options []FirewallBackendOption `json:"options"`
|
||||
}
|
||||
|
||||
type FirewallSettings struct {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
System FirewallBackendGroup `json:"system"`
|
||||
Forwarding FirewallBackendGroup `json:"forwarding"`
|
||||
Docker FirewallBackendGroup `json:"docker"`
|
||||
PingStatus string `json:"pingStatus"`
|
||||
PortWhitelist []filter.PortWhitelist `json:"portWhiteList"`
|
||||
PanelPort string `json:"panelPort"`
|
||||
SSHPort string `json:"sshPort"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistCreate struct {
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistUpdate struct {
|
||||
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallPortWhitelistDelete struct {
|
||||
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallBackendOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||
}
|
||||
|
||||
type FirewallIPv6Operation struct {
|
||||
Status string `json:"status" validate:"required,oneof=Enable Disable"`
|
||||
}
|
||||
|
||||
type FirewallFamilyOperation struct {
|
||||
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||
Backend string `json:"backend" validate:"required,oneof=iptables nftables"`
|
||||
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize repair bind"`
|
||||
}
|
||||
|
||||
type FilterChainOperation struct {
|
||||
Name string `json:"name" validate:"required,eq=1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base bind-base unbind-base"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FilterChainOperationResponse struct {
|
||||
TaskID string `json:"taskID"`
|
||||
Queued bool `json:"queued"`
|
||||
}
|
||||
|
||||
type FirewallInitializationTask struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallSystemPort = firewall.SystemPort
|
||||
|
||||
type FirewallRuleInventoryResponse struct {
|
||||
IPv4Range filter.PositionRange `json:"ipv4Range"`
|
||||
IPv6Range filter.PositionRange `json:"ipv6Range"`
|
||||
Total int64 `json:"total"`
|
||||
AllTotal int64 `json:"allTotal"`
|
||||
Items []filter.InventoryItem `json:"items"`
|
||||
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackup struct {
|
||||
Name string `json:"name"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
RuleCount int `json:"ruleCount"`
|
||||
ModifiedAt int64 `json:"modifiedAt"`
|
||||
}
|
||||
|
||||
type FirewallRuleBackups struct {
|
||||
Directory string `json:"directory"`
|
||||
Files []FirewallRuleBackup `json:"files"`
|
||||
}
|
||||
|
||||
type FirewallRuleResetResponse struct {
|
||||
BackupPath string `json:"backupPath"`
|
||||
Removed int `json:"removed"`
|
||||
Disabled bool `json:"disabled"`
|
||||
}
|
||||
|
||||
type FirewallRuleReset struct {
|
||||
Subsystem string `json:"subsystem,omitempty" validate:"omitempty,oneof=system forwarding docker"`
|
||||
Backup *bool `json:"backup,omitempty" default:"true"`
|
||||
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"`
|
||||
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 {
|
||||
IPv6Enabled bool `json:"ipv6Enabled"`
|
||||
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 {
|
||||
Partial bool `json:"partial"`
|
||||
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"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
||||
|
||||
Description string `json:"description"`
|
||||
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"`
|
||||
Sources []string `json:"sources"`
|
||||
Effective bool `json:"effective"`
|
||||
TrafficPath string `json:"trafficPath"`
|
||||
ManagementTarget string `json:"managementTarget"`
|
||||
ManagementReason string `json:"managementReason,omitempty"`
|
||||
}
|
||||
|
||||
type AddrRuleOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||
Address string `json:"address" validate:"required"`
|
||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
||||
|
||||
Description string `json:"description"`
|
||||
type DockerPortGuardPortGroup struct {
|
||||
Key string `json:"key"`
|
||||
Label string `json:"label"`
|
||||
Endpoint DockerPortGuardEndpoint `json:"endpoint"`
|
||||
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
|
||||
}
|
||||
|
||||
type PortRuleUpdate struct {
|
||||
OldRule PortRuleOperate `json:"oldRule"`
|
||||
NewRule PortRuleOperate `json:"newRule"`
|
||||
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 AddrRuleUpdate struct {
|
||||
OldRule AddrRuleOperate `json:"oldRule"`
|
||||
NewRule AddrRuleOperate `json:"newRule"`
|
||||
type DockerPortGuardList struct {
|
||||
Base DockerPortGuardBase `json:"base"`
|
||||
Containers []DockerPortGuardContainer `json:"containers"`
|
||||
OrphanPolicies []DockerPortGuardEndpoint `json:"orphanPolicies"`
|
||||
}
|
||||
|
||||
type BatchRuleOperate struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Rules []PortRuleOperate `json:"rules"`
|
||||
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 IptablesOp struct {
|
||||
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
|
||||
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
||||
type DockerPortGuardPolicyBatch struct {
|
||||
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||
Import bool `json:"import"`
|
||||
}
|
||||
|
||||
type IptablesRuleOp struct {
|
||||
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 DockerPortGuardPolicyBatchDelete struct {
|
||||
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
||||
}
|
||||
|
||||
type IptablesBatchOperate struct {
|
||||
Rules []IptablesRuleOp `json:"rules"`
|
||||
type DockerPortGuardPolicy struct {
|
||||
DockerPortGuardEndpointIdentity
|
||||
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all accept_sources accept_all"`
|
||||
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||
}
|
||||
|
||||
type IptablesChainStatus struct {
|
||||
IsBind bool `json:"isBind"`
|
||||
DefaultStrategy string `json:"defaultStrategy"`
|
||||
type DockerPortGuardOperation struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreateItem struct {
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||
}
|
||||
|
||||
type FirewallRuleCreate struct {
|
||||
BackupFile string `json:"backupFile,omitempty" validate:"omitempty,max=255"`
|
||||
Initialize bool `json:"initialize"`
|
||||
Items []FirewallRuleCreateItem `json:"items" validate:"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 FirewallRuleDelete struct {
|
||||
Targets []FirewallRuleDeleteItem `json:"targets" validate:"required,min=1,dive"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteItem struct {
|
||||
FirewallRuleDeleteTarget
|
||||
Observed filter.ObservedRule `json:"observed" validate:"required"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteTarget struct {
|
||||
Scope filter.Scope `json:"scope" validate:"required"`
|
||||
InstanceKey string `json:"instanceKey" validate:"required,max=128"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteResponse struct {
|
||||
TaskID string `json:"taskID,omitempty"`
|
||||
Queued bool `json:"queued,omitempty"`
|
||||
Succeeded int `json:"succeeded"`
|
||||
Failed int `json:"failed"`
|
||||
Errors []FirewallRuleDeleteFailure `json:"errors,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallRuleDeleteFailure struct {
|
||||
Index int `json:"index"`
|
||||
InstanceKey string `json:"instanceKey"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
type FirewallRuleUpdate struct {
|
||||
FirewallRuleDeleteTarget
|
||||
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 {
|
||||
FirewallRuleDeleteTarget
|
||||
TargetPosition *int64 `json:"targetPosition"`
|
||||
Priority *int `json:"priority"`
|
||||
}
|
||||
|
||||
type FirewallRuleExportItem struct {
|
||||
filter.FirewallRule
|
||||
Raw string `json:"raw,omitempty"`
|
||||
ParseStatus filter.ParseStatus `json:"parseStatus,omitempty"`
|
||||
}
|
||||
|
||||
type FirewallSubsystemBackup struct {
|
||||
Families []string `json:"families,omitempty"`
|
||||
Subsystem string `json:"subsystem"`
|
||||
Provider filter.Provider `json:"provider"`
|
||||
Forwarding []forwarding.Rule `json:"forwarding"`
|
||||
Docker *dockerfirewall.PolicyInventory `json:"docker,omitempty"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
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"`
|
||||
}
|
||||
|
||||
type ForwardRuleOperate struct {
|
||||
Import bool `json:"import"`
|
||||
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"`
|
||||
}
|
||||
+91
-13
@@ -1,6 +1,11 @@
|
||||
package dto
|
||||
|
||||
import "time"
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
)
|
||||
|
||||
type MonitorSearch struct {
|
||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||
@@ -26,6 +31,12 @@ type Process struct {
|
||||
}
|
||||
|
||||
type MonitorSetting struct {
|
||||
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||
MonitorStatus string `json:"monitorStatus"`
|
||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||
MonitorInterval string `json:"monitorInterval"`
|
||||
@@ -34,42 +45,78 @@ type MonitorSetting struct {
|
||||
}
|
||||
|
||||
type MonitorSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval DefaultNetwork DefaultIO"`
|
||||
Key string `json:"key" validate:"required,oneof=MonitorStatus MonitorStoreDays MonitorInterval GPUMonitorStatus GPUMonitorStoreDays GPUMonitorInterval VLLMMonitorStatus VLLMMonitorStoreDays VLLMMonitorInterval DefaultNetwork DefaultIO"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type MonitorGPUOptions struct {
|
||||
Supported bool `json:"supported"`
|
||||
GPUType string `json:"gpuType"`
|
||||
ChartHide []GPUChartHide `json:"chartHide"`
|
||||
Options []string `json:"options"`
|
||||
}
|
||||
type GPUChartHide struct {
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
Type string `json:"type"`
|
||||
Process bool `json:"process"`
|
||||
GPU bool `json:"gpu"`
|
||||
Memory bool `json:"memory"`
|
||||
Power bool `json:"power"`
|
||||
PowerLimit bool `json:"powerLimit"`
|
||||
Temperature bool `json:"temperature"`
|
||||
Speed bool `json:"speed"`
|
||||
}
|
||||
type MonitorGPUSearch struct {
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
Legacy bool `json:"legacy"`
|
||||
ProductName string `json:"productName"`
|
||||
StartTime time.Time `json:"startTime"`
|
||||
EndTime time.Time `json:"endTime"`
|
||||
}
|
||||
type MonitorGPUData struct {
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []float64 `json:"gpuValue"`
|
||||
TemperatureValue []float64 `json:"temperatureValue"`
|
||||
PowerTotal []float64 `json:"powerTotal"`
|
||||
PowerUsed []float64 `json:"powerUsed"`
|
||||
PowerPercent []float64 `json:"powerPercent"`
|
||||
MemoryTotal []float64 `json:"memoryTotal"`
|
||||
MemoryUsed []float64 `json:"memoryUsed"`
|
||||
MemoryPercent []float64 `json:"memoryPercent"`
|
||||
SpeedValue []int `json:"speedValue"`
|
||||
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||
OFAUtil []*float64 `json:"ofaUtil"`
|
||||
MediaUtil []*float64 `json:"mediaUtil"`
|
||||
ComputeUtil []*float64 `json:"computeUtil"`
|
||||
CopyUtil []*float64 `json:"copyUtil"`
|
||||
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||
FanRPM []*float64 `json:"fanRPM"`
|
||||
AICPUUtil []*float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil []*float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed []*float64 `json:"ddrUsed"`
|
||||
DDRTotal []*float64 `json:"ddrTotal"`
|
||||
HBMUsed []*float64 `json:"hbmUsed"`
|
||||
HBMTotal []*float64 `json:"hbmTotal"`
|
||||
DDRBandwidth []*float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth []*float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth []*float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency []*float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed []*float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal []*float64 `json:"hugepagesTotal"`
|
||||
|
||||
ProcessCount []int `json:"processCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
MemoryTemperatureValue []*float64 `json:"memoryTemperatureValue"`
|
||||
FrequencyValue []*float64 `json:"frequencyValue"`
|
||||
MemoryFrequencyValue []*float64 `json:"memoryFrequencyValue"`
|
||||
Date []time.Time `json:"date"`
|
||||
GPUValue []*float64 `json:"gpuValue"`
|
||||
TemperatureValue []*float64 `json:"temperatureValue"`
|
||||
PowerTotal []*float64 `json:"powerTotal"`
|
||||
PowerUsed []*float64 `json:"powerUsed"`
|
||||
PowerPercent []*float64 `json:"powerPercent"`
|
||||
MemoryTotal []*float64 `json:"memoryTotal"`
|
||||
MemoryUsed []*float64 `json:"memoryUsed"`
|
||||
MemoryPercent []*float64 `json:"memoryPercent"`
|
||||
SpeedValue []*float64 `json:"speedValue"`
|
||||
|
||||
ProcessCount []*float64 `json:"processCount"`
|
||||
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||
}
|
||||
|
||||
@@ -79,3 +126,34 @@ type GPUProcess struct {
|
||||
ProcessName string `json:"processName"`
|
||||
UsedMemory string `json:"usedMemory"`
|
||||
}
|
||||
|
||||
type MonitorVLLMSearch struct {
|
||||
Group string `json:"group" validate:"omitempty,oneof=overview latency requests"`
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
StartTime time.Time `json:"startTime" validate:"required"`
|
||||
EndTime time.Time `json:"endTime" validate:"required"`
|
||||
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||
}
|
||||
|
||||
type MonitorVLLMData struct {
|
||||
SampleCount int64 `json:"sampleCount"`
|
||||
BucketSeconds int64 `json:"bucketSeconds"`
|
||||
Points []model.MonitorVLLM `json:"points"`
|
||||
}
|
||||
|
||||
type MonitorVLLMCurrent struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorVLLMCurrentData struct {
|
||||
model.MonitorVLLM
|
||||
Metrics map[string]json.RawMessage `json:"metrics,omitempty"`
|
||||
}
|
||||
|
||||
type MonitorVLLMClean struct {
|
||||
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||
}
|
||||
|
||||
type MonitorClean struct {
|
||||
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||
}
|
||||
|
||||
@@ -51,13 +51,19 @@ const (
|
||||
CACHE NginxKey = "cache"
|
||||
HttpPer NginxKey = "http-per"
|
||||
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{
|
||||
Index: {"index"},
|
||||
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
||||
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"},
|
||||
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"},
|
||||
}
|
||||
|
||||
var StaticFileKeyMap = map[NginxKey]struct {
|
||||
|
||||
@@ -23,6 +23,10 @@ type AppInstallCreate struct {
|
||||
Services map[string]string `json:"services"`
|
||||
TaskID string `json:"taskID"`
|
||||
|
||||
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
|
||||
AppContainerConfig
|
||||
NodePushConfig
|
||||
}
|
||||
@@ -50,6 +54,10 @@ type AppContainerConfig struct {
|
||||
Type string `json:"type"`
|
||||
SpecifyIP string `json:"specifyIP"`
|
||||
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 {
|
||||
@@ -79,6 +87,7 @@ type AppBackupDelete struct {
|
||||
}
|
||||
|
||||
type AppInstalledOperate struct {
|
||||
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
BackupId uint `json:"backupId"`
|
||||
DetailId uint `json:"detailId"`
|
||||
@@ -92,9 +101,12 @@ type AppInstalledOperate struct {
|
||||
TaskID string `json:"taskID"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
Favorite bool `json:"favorite"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstallUpgrade struct {
|
||||
FeishuTransition *dto.AgentFeishuTransition `json:"feishuTransition,omitempty"`
|
||||
InstallID uint `json:"installId"`
|
||||
DetailID uint `json:"detailId"`
|
||||
Backup bool `json:"backup"`
|
||||
@@ -111,11 +123,17 @@ type AppInstallDelete struct {
|
||||
DeleteDB bool `json:"deleteDB"`
|
||||
DeleteImage bool `json:"deleteImage"`
|
||||
TaskID string `json:"taskID"`
|
||||
|
||||
UseLifecycleScripts bool `json:"-"`
|
||||
}
|
||||
|
||||
type AppInstalledUpdate struct {
|
||||
SyncModelAccount *bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType" validate:"omitempty,oneof=container localhost systemIP custom"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
InstallId uint `json:"installId" validate:"required"`
|
||||
Params map[string]interface{} `json:"params" validate:"required"`
|
||||
TaskID string `json:"-"`
|
||||
AppContainerConfig
|
||||
}
|
||||
|
||||
|
||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||
UseProxy bool `json:"useProxy"`
|
||||
UseServerFilename bool `json:"useServerFilename"`
|
||||
}
|
||||
|
||||
type FileMove struct {
|
||||
@@ -131,6 +132,11 @@ type FileMove struct {
|
||||
Name string `json:"name"`
|
||||
Cover bool `json:"cover"`
|
||||
CoverPaths []string `json:"coverPaths"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type FileMoveStopReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type FileDownload struct {
|
||||
@@ -153,6 +159,10 @@ type FileProcessReq struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type FileProcessRemoveReq struct {
|
||||
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
|
||||
}
|
||||
|
||||
type FileRoleUpdate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
User string `json:"user" validate:"required"`
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
package request
|
||||
|
||||
type MCPWebsiteBinding struct {
|
||||
Operation string `json:"operation"`
|
||||
RequestID string `json:"requestId"`
|
||||
BindingID string `json:"bindingId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
Domain string `json:"domain"`
|
||||
EnableSSL bool `json:"enableSSL"`
|
||||
SSLID uint `json:"sslID"`
|
||||
Upstream string `json:"upstream"`
|
||||
CoreHost string `json:"coreHost"`
|
||||
ProxyKey string `json:"proxyKey"`
|
||||
}
|
||||
@@ -66,6 +66,7 @@ type RuntimeDelete struct {
|
||||
}
|
||||
|
||||
type RuntimeUpdate struct {
|
||||
AppDetailID uint `json:"appDetailId"`
|
||||
Name string `json:"name"`
|
||||
ID uint `json:"id"`
|
||||
Image string `json:"image"`
|
||||
|
||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
||||
|
||||
SiteDir string `json:"siteDir"`
|
||||
|
||||
TemplateOutputID uint `json:"templateOutputID"`
|
||||
|
||||
RuntimeConfig
|
||||
FtpConfig
|
||||
DataBaseConfig
|
||||
|
||||
@@ -182,3 +182,12 @@ type WebsiteSSLFileUpload struct {
|
||||
PushNode bool `json:"pushNode"`
|
||||
Nodes string `json:"nodes"`
|
||||
}
|
||||
|
||||
type WebsiteCAImport struct {
|
||||
Name string `json:"name" validate:"required,max=128"`
|
||||
Type string `json:"type" validate:"required,oneof=paste local"`
|
||||
Certificate string `json:"certificate" validate:"required_if=Type paste"`
|
||||
PrivateKey string `json:"privateKey" validate:"required_if=Type paste"`
|
||||
CertificatePath string `json:"certificatePath" validate:"required_if=Type local"`
|
||||
PrivateKeyPath string `json:"privateKeyPath" validate:"required_if=Type local"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
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"`
|
||||
}
|
||||
@@ -50,6 +50,8 @@ type TagDTO struct {
|
||||
}
|
||||
|
||||
type AppInstalledCheck struct {
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
IsExist bool `json:"isExist"`
|
||||
Name string `json:"name"`
|
||||
App string `json:"app"`
|
||||
@@ -184,6 +186,12 @@ type AppParam struct {
|
||||
}
|
||||
|
||||
type AppConfig struct {
|
||||
AppKey string `json:"appKey"`
|
||||
Name string `json:"name"`
|
||||
HttpPort int `json:"httpPort"`
|
||||
SyncModelAccount bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
Params []AppParam `json:"params"`
|
||||
RawCompose string `json:"rawCompose"`
|
||||
request.AppContainerConfig
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
package response
|
||||
|
||||
type MCPWebsiteBinding struct {
|
||||
State string `json:"state"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
BindingID string `json:"bindingId"`
|
||||
RequestID string `json:"requestId"`
|
||||
Domain string `json:"domain"`
|
||||
Endpoint string `json:"endpoint"`
|
||||
EnableSSL bool `json:"enableSSL"`
|
||||
SSLID uint `json:"sslID"`
|
||||
WebsiteStatus string `json:"websiteStatus"`
|
||||
Reason string `json:"reason"`
|
||||
Code string `json:"code,omitempty"`
|
||||
Error string `json:"error,omitempty"`
|
||||
Definitive bool `json:"definitive,omitempty"`
|
||||
}
|
||||
@@ -17,6 +17,17 @@ type NginxParam struct {
|
||||
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 {
|
||||
Enable bool `json:"enable"`
|
||||
Items []dto.NginxAuth `json:"items"`
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
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"`
|
||||
}
|
||||
@@ -35,7 +35,7 @@ type SettingUpdate struct {
|
||||
}
|
||||
|
||||
type AgentSettingUpdate struct {
|
||||
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
|
||||
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
|
||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Mode string `json:"mode"`
|
||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
||||
EncryptionMode string `json:"encryptionMode"`
|
||||
PassPhrase string `json:"passPhrase"`
|
||||
PublicKey string `json:"publicKey"`
|
||||
PrivateKey string `json:"privateKey"`
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
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,6 +2,7 @@ package model
|
||||
|
||||
type AgentAccount struct {
|
||||
BaseModel
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
APIKey string `json:"apiKey"`
|
||||
|
||||
@@ -1,5 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type Alert struct {
|
||||
BaseModel
|
||||
|
||||
@@ -22,6 +29,7 @@ type AlertTask struct {
|
||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
|
||||
}
|
||||
|
||||
type AlertLog struct {
|
||||
@@ -41,14 +49,23 @@ type AlertLog struct {
|
||||
|
||||
type AlertConfig struct {
|
||||
BaseModel
|
||||
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"`
|
||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||
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"`
|
||||
SecretConfig string `gorm:"type:text;not null;default:''" json:"-"`
|
||||
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 {
|
||||
BaseModel
|
||||
IP string `json:"ip"`
|
||||
|
||||
@@ -28,6 +28,10 @@ type AppInstall struct {
|
||||
Favorite bool `json:"favorite"`
|
||||
SortOrder int `json:"sortOrder" gorm:"default:0"`
|
||||
|
||||
SyncModelAccount bool `json:"syncModelAccount"`
|
||||
ModelAccountBaseURLType string `json:"modelAccountBaseURLType"`
|
||||
ModelAccountBaseURL string `json:"modelAccountBaseURL"`
|
||||
|
||||
App App `json:"app" gorm:"-:migration"`
|
||||
}
|
||||
|
||||
|
||||
@@ -5,6 +5,13 @@ type AppLauncher struct {
|
||||
Key string `json:"key"`
|
||||
}
|
||||
|
||||
type QuickDirectory struct {
|
||||
BaseModel
|
||||
Path string `json:"path"`
|
||||
Alias string `json:"alias"`
|
||||
Legacy bool `json:"legacy" gorm:"-"`
|
||||
}
|
||||
|
||||
type QuickJump struct {
|
||||
BaseModel
|
||||
Name string `json:"name"`
|
||||
|
||||
@@ -31,4 +31,5 @@ type BackupRecord struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
Description string `json:"description"`
|
||||
Args string `gorm:"not null;default:''" json:"args"`
|
||||
}
|
||||
|
||||
@@ -13,4 +13,5 @@ type Compose struct {
|
||||
|
||||
Name string `json:"name"`
|
||||
Path string `json:"path"`
|
||||
IsPinned bool `json:"isPinned"`
|
||||
}
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
package model
|
||||
|
||||
type Firewall struct {
|
||||
BaseModel
|
||||
|
||||
Type string `json:"type"`
|
||||
Port string `json:"port"` // Deprecated
|
||||
Address string `json:"address"` // Deprecated
|
||||
|
||||
Chain string `json:"chain"`
|
||||
Protocol string `json:"protocol"`
|
||||
SrcIP string `json:"srcIP"`
|
||||
SrcPort string `json:"srcPort"`
|
||||
DstIP string `json:"dstIP"`
|
||||
DstPort string `json:"dstPort"`
|
||||
Strategy string `gorm:"not null" json:"strategy"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -8,4 +8,6 @@ type Ftp struct {
|
||||
Status string `gorm:"not null" json:"status"`
|
||||
Path string `gorm:"not null" json:"path"`
|
||||
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:"-"`
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package model
|
||||
|
||||
import "time"
|
||||
|
||||
type MonitorBase struct {
|
||||
BaseModel
|
||||
Cpu float64 `json:"cpu"`
|
||||
@@ -33,14 +35,88 @@ type MonitorNetwork struct {
|
||||
}
|
||||
|
||||
type MonitorGPU struct {
|
||||
MemoryUtil *float64 `json:"memoryUtil"`
|
||||
MemoryActivity *float64 `json:"memoryActivity"`
|
||||
EncoderUtil *float64 `json:"encoderUtil"`
|
||||
DecoderUtil *float64 `json:"decoderUtil"`
|
||||
JPEGUtil *float64 `json:"jpegUtil"`
|
||||
OFAUtil *float64 `json:"ofaUtil"`
|
||||
MediaUtil *float64 `json:"mediaUtil"`
|
||||
ComputeUtil *float64 `json:"computeUtil"`
|
||||
CopyUtil *float64 `json:"copyUtil"`
|
||||
HotspotTemperature *float64 `json:"hotspotTemperature"`
|
||||
FanRPM *float64 `json:"fanRPM"`
|
||||
AICPUUtil *float64 `json:"aiCPUUtil"`
|
||||
CtrlCPUUtil *float64 `json:"ctrlCPUUtil"`
|
||||
DDRUsed *float64 `json:"ddrUsed"`
|
||||
DDRTotal *float64 `json:"ddrTotal"`
|
||||
HBMUsed *float64 `json:"hbmUsed"`
|
||||
HBMTotal *float64 `json:"hbmTotal"`
|
||||
DDRBandwidth *float64 `json:"ddrBandwidth"`
|
||||
HBMBandwidth *float64 `json:"hbmBandwidth"`
|
||||
MemoryBandwidth *float64 `json:"memoryBandwidth"`
|
||||
MediaFrequency *float64 `json:"mediaFrequency"`
|
||||
HugepagesUsed *float64 `json:"hugepagesUsed"`
|
||||
HugepagesTotal *float64 `json:"hugepagesTotal"`
|
||||
|
||||
MemoryTemperature *float64 `json:"memoryTemperature"`
|
||||
DeviceID string `json:"deviceID"`
|
||||
DeviceType string `json:"deviceType"`
|
||||
ProcessStatus string `json:"processStatus"`
|
||||
Frequency *float64 `json:"frequency"`
|
||||
MemoryFrequency *float64 `json:"memoryFrequency"`
|
||||
IntervalSeconds int `json:"intervalSeconds"`
|
||||
BaseModel
|
||||
ProductName string `json:"productName"`
|
||||
GPUUtil float64 `json:"gpuUtil"`
|
||||
Temperature float64 `json:"temperature"`
|
||||
PowerDraw float64 `json:"powerDraw"`
|
||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
||||
MemUsed float64 `json:"memUsed"`
|
||||
MemTotal float64 `json:"memTotal"`
|
||||
FanSpeed int `json:"fanSpeed"`
|
||||
GPUUtil *float64 `json:"gpuUtil"`
|
||||
Temperature *float64 `json:"temperature"`
|
||||
PowerDraw *float64 `json:"powerDraw"`
|
||||
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||
MemUsed *float64 `json:"memUsed"`
|
||||
MemTotal *float64 `json:"memTotal"`
|
||||
FanSpeed *float64 `json:"fanSpeed"`
|
||||
Processes string `json:"processes"`
|
||||
}
|
||||
|
||||
type MonitorVLLM struct {
|
||||
ID uint `json:"-" gorm:"primarykey;autoIncrement"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
AppInstallID uint `json:"appInstallID"`
|
||||
Status string `json:"status"`
|
||||
RawMetrics string `json:"-"`
|
||||
HistogramDeltas string `json:"-"`
|
||||
IntervalSeconds float64 `json:"intervalSeconds" gorm:"-"`
|
||||
SampleIntervalSeconds int `json:"-"`
|
||||
RequestDeltas string `json:"-"`
|
||||
CompletedRequests *float64 `json:"completedRequests" gorm:"-"`
|
||||
QueueTime *float64 `json:"queueTime"`
|
||||
AverageGenerationTokens *float64 `json:"averageGenerationTokens"`
|
||||
PrefixCacheHitRate *float64 `json:"prefixCacheHitRate"`
|
||||
FinishReasons map[string]float64 `json:"finishReasons" gorm:"-"`
|
||||
PromptDistribution map[string]float64 `json:"promptDistribution" gorm:"-"`
|
||||
GenerationDistribution map[string]float64 `json:"generationDistribution" gorm:"-"`
|
||||
|
||||
Running *float64 `json:"running"`
|
||||
Waiting *float64 `json:"waiting"`
|
||||
CacheUsage *float64 `json:"cacheUsage"`
|
||||
PromptThroughput *float64 `json:"promptThroughput"`
|
||||
GenerationThroughput *float64 `json:"generationThroughput"`
|
||||
RequestThroughput *float64 `json:"requestThroughput"`
|
||||
TimeToFirstToken *float64 `json:"timeToFirstToken"`
|
||||
TimePerOutputToken *float64 `json:"timePerOutputToken"`
|
||||
RequestLatency *float64 `json:"requestLatency"`
|
||||
PrefillTime *float64 `json:"prefillTime"`
|
||||
DecodeTime *float64 `json:"decodeTime"`
|
||||
TimeToFirstTokenP50 *float64 `json:"timeToFirstTokenP50"`
|
||||
TimeToFirstTokenP90 *float64 `json:"timeToFirstTokenP90"`
|
||||
TimeToFirstTokenP95 *float64 `json:"timeToFirstTokenP95"`
|
||||
TimeToFirstTokenP99 *float64 `json:"timeToFirstTokenP99"`
|
||||
TimePerOutputTokenP50 *float64 `json:"timePerOutputTokenP50"`
|
||||
TimePerOutputTokenP90 *float64 `json:"timePerOutputTokenP90"`
|
||||
TimePerOutputTokenP95 *float64 `json:"timePerOutputTokenP95"`
|
||||
TimePerOutputTokenP99 *float64 `json:"timePerOutputTokenP99"`
|
||||
RequestLatencyP50 *float64 `json:"requestLatencyP50"`
|
||||
RequestLatencyP90 *float64 `json:"requestLatencyP90"`
|
||||
RequestLatencyP95 *float64 `json:"requestLatencyP95"`
|
||||
RequestLatencyP99 *float64 `json:"requestLatencyP99"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
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"
|
||||
}
|
||||
+153
-14
@@ -10,8 +10,10 @@ type APIConfig struct {
|
||||
APIType string
|
||||
BaseURL string
|
||||
EditableBaseURL bool
|
||||
DiscoverModels bool
|
||||
DefaultAuthMode string
|
||||
AuthModes []string
|
||||
Models []Model
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -27,6 +29,7 @@ type Model struct {
|
||||
type Meta struct {
|
||||
Key string
|
||||
DisplayName string
|
||||
DisplayNameKey string
|
||||
Sort uint
|
||||
DefaultAPIType string
|
||||
APIConfigs []APIConfig
|
||||
@@ -37,26 +40,36 @@ type Meta struct {
|
||||
var catalog = map[string]Meta{
|
||||
"custom": {
|
||||
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
||||
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
|
||||
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"),
|
||||
},
|
||||
"ollama": {
|
||||
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: editableAPIConfigs("openai-responses", "openai-completions"),
|
||||
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
|
||||
},
|
||||
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
||||
"llmman": {
|
||||
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-responses", BaseURL: "http://127.0.0.1:17434/v1", EditableBaseURL: true},
|
||||
{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": {
|
||||
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||
APIConfigs: editableAPIConfigs("openai-completions", "openai-responses", "anthropic-messages"),
|
||||
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||
},
|
||||
"deepseek": {
|
||||
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com/v1"},
|
||||
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
|
||||
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||
},
|
||||
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||
Models: []Model{{ID: "deepseek-flash", Name: "deepseek-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||
},
|
||||
"bailian-coding-plan": {
|
||||
Key: "bailian-coding-plan", DisplayName: "阿里云百炼 Coding Plan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
||||
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
||||
@@ -69,13 +82,12 @@ var catalog = map[string]Meta{
|
||||
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||
{ID: "glm-5", Name: "GLM-5"},
|
||||
{ID: "MiniMax-M2.5", Name: "MiniMax M2.5"},
|
||||
{ID: "qwen3.6-plus", Name: "Qwen3.6-Plus"},
|
||||
{ID: "qwen3.7-plus", Name: "Qwen3.7-Plus"},
|
||||
},
|
||||
},
|
||||
"ark-coding-plan": {
|
||||
Key: "ark-coding-plan", DisplayName: "方舟 Coding Plan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
||||
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
||||
@@ -88,7 +100,10 @@ var catalog = map[string]Meta{
|
||||
},
|
||||
"zai": {
|
||||
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true}},
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: 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": {
|
||||
@@ -96,16 +111,18 @@ var catalog = map[string]Meta{
|
||||
APIConfigs: []APIConfig{
|
||||
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
||||
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||
},
|
||||
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"}},
|
||||
Models: []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}},
|
||||
},
|
||||
"xiaomi": {
|
||||
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
||||
},
|
||||
Models: []Model{{ID: "mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, {ID: "mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, {ID: "mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}},
|
||||
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
|
||||
},
|
||||
"kimi": {
|
||||
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
||||
@@ -122,12 +139,20 @@ var catalog = map[string]Meta{
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
||||
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
||||
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
|
||||
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{
|
||||
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"},
|
||||
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"},
|
||||
}},
|
||||
},
|
||||
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": {
|
||||
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"}},
|
||||
APIConfigs: []APIConfig{
|
||||
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"},
|
||||
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"},
|
||||
},
|
||||
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
||||
},
|
||||
"anthropic": {
|
||||
@@ -145,9 +170,62 @@ var catalog = map[string]Meta{
|
||||
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
||||
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"}},
|
||||
},
|
||||
"bailian": {
|
||||
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY",
|
||||
APIConfigs: []APIConfig{
|
||||
{
|
||||
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
DiscoverModels: 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: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||
DiscoverModels: 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(apiTypes ...string) []APIConfig {
|
||||
func editableAPIConfigs(discoverModels bool, apiTypes ...string) []APIConfig {
|
||||
configs := make([]APIConfig, 0, len(apiTypes))
|
||||
for _, apiType := range apiTypes {
|
||||
if apiType == "anthropic-messages" {
|
||||
@@ -156,7 +234,11 @@ func editableAPIConfigs(apiTypes ...string) []APIConfig {
|
||||
configs = append(configs, config)
|
||||
continue
|
||||
}
|
||||
configs = append(configs, APIConfig{APIType: apiType, EditableBaseURL: true})
|
||||
configs = append(configs, APIConfig{
|
||||
APIType: apiType,
|
||||
EditableBaseURL: true,
|
||||
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
|
||||
})
|
||||
}
|
||||
return configs
|
||||
}
|
||||
@@ -203,12 +285,38 @@ func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
||||
}
|
||||
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 {
|
||||
@@ -264,6 +372,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
||||
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 = ""
|
||||
@@ -271,6 +382,9 @@ func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
||||
}
|
||||
|
||||
func normalizeEndpointPath(apiType, value string) string {
|
||||
if IsImageAPIType(apiType) {
|
||||
return strings.TrimRight(value, "/")
|
||||
}
|
||||
path := strings.TrimRight(value, "/")
|
||||
suffixes := []string{}
|
||||
switch apiType {
|
||||
@@ -280,6 +394,8 @@ func normalizeEndpointPath(apiType, value string) string {
|
||||
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) {
|
||||
@@ -289,6 +405,19 @@ func normalizeEndpointPath(apiType, value string) string {
|
||||
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 {
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
@@ -305,6 +434,14 @@ func DisplayName(key string) string {
|
||||
return meta.DisplayName
|
||||
}
|
||||
|
||||
func DisplayNameKey(key string) string {
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
return meta.DisplayNameKey
|
||||
}
|
||||
|
||||
func NormalizeModelID(provider, modelID string) string {
|
||||
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
||||
for _, prefix := range legacyModelPrefixes[provider] {
|
||||
@@ -324,6 +461,7 @@ 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"},
|
||||
@@ -344,6 +482,7 @@ func cloneMeta(meta Meta) Meta {
|
||||
for index, config := range meta.APIConfigs {
|
||||
clone.APIConfigs[index] = config
|
||||
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
||||
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...)
|
||||
}
|
||||
clone.Models = append([]Model(nil), meta.Models...)
|
||||
return clone
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -54,11 +55,20 @@ func buildModelDiscoveryURL(baseURL string) string {
|
||||
base = normalizeEndpointPath(apiType, base)
|
||||
}
|
||||
switch {
|
||||
case strings.HasSuffix(base, "/v1/models"):
|
||||
case strings.HasSuffix(base, "/models"):
|
||||
return base
|
||||
case strings.HasSuffix(base, "/v1"):
|
||||
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
|
||||
}
|
||||
|
||||
@@ -0,0 +1,100 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMiniMaxCatalog(t *testing.T) {
|
||||
meta, ok := Get("minimax")
|
||||
if !ok || !reflect.DeepEqual(meta.Models, []Model{{ID: "MiniMax-M3.1-Flash", Name: "MiniMax M3.1 Flash"}, {ID: "MiniMax-M3", Name: "MiniMax M3"}}) {
|
||||
t.Fatalf("unexpected MiniMax models: %+v", meta.Models)
|
||||
}
|
||||
if meta.DefaultAPIType != "anthropic-messages" {
|
||||
t.Fatalf("changed default protocol: %s", meta.DefaultAPIType)
|
||||
}
|
||||
for _, provider := range All() {
|
||||
for _, model := range provider.Models {
|
||||
if strings.Contains(strings.ToLower(model.ID), "minimax") && model.ID != "MiniMax-M3.1-Flash" && model.ID != "MiniMax-M3" {
|
||||
t.Errorf("legacy suggestion in %s: %s", provider.Key, model.ID)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxVerificationRequests(t *testing.T) {
|
||||
for _, region := range []string{"https://api.minimaxi.com", "https://api.minimax.io"} {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
for _, auth := range []string{AuthModeXAPIKey, AuthModeBearer} {
|
||||
for _, model := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.7-highspeed", "custom-model"} {
|
||||
t.Run(region+"/"+protocol+"/"+auth+"/"+model, func(t *testing.T) {
|
||||
base, endpoint := region+"/v1", "/chat/completions"
|
||||
if protocol == "anthropic-messages" {
|
||||
base, endpoint = region+"/anthropic", "/v1/messages"
|
||||
}
|
||||
built := BuildVerifyRequest("minimax", protocol, auth, base, "test-key", model)
|
||||
if built.URL != base+endpoint {
|
||||
t.Fatalf("URL = %s", built.URL)
|
||||
}
|
||||
var captured map[string]interface{}
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost || r.URL.Path != strings.TrimPrefix(base, region)+endpoint {
|
||||
t.Errorf("unexpected request: %s %s", r.Method, r.URL.Path)
|
||||
}
|
||||
if protocol == "anthropic-messages" && auth == AuthModeXAPIKey {
|
||||
if r.Header.Get("x-api-key") != "test-key" || r.Header.Get("Authorization") != "" {
|
||||
t.Error("incorrect API key authentication")
|
||||
}
|
||||
} else if r.Header.Get("Authorization") != "Bearer test-key" {
|
||||
t.Error("incorrect bearer authentication")
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&captured); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
w.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
defer server.Close()
|
||||
if err := VerifyAccount("minimax", protocol, auth, server.URL+strings.TrimPrefix(base, region), "test-key", model); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if captured["model"] != model || captured["stream"] != false || captured["max_tokens"] != float64(defaultVerifyMaxTokens) {
|
||||
t.Fatalf("unexpected serialized body: %#v", captured)
|
||||
}
|
||||
if model == "MiniMax-M3.1-Flash" {
|
||||
if protocol == "anthropic-messages" {
|
||||
if !reflect.DeepEqual(captured["thinking"], map[string]interface{}{"type": "adaptive"}) || !reflect.DeepEqual(captured["output_config"], map[string]interface{}{"effort": "max"}) || captured["reasoning_effort"] != nil {
|
||||
t.Fatalf("invalid Anthropic Flash reasoning: %#v", captured)
|
||||
}
|
||||
} else if captured["reasoning_effort"] != "max" || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||
t.Fatalf("invalid OpenAI Flash reasoning: %#v", captured)
|
||||
}
|
||||
} else if captured["reasoning_effort"] != nil || captured["thinking"] != nil || captured["output_config"] != nil {
|
||||
t.Fatalf("changed legacy/custom reasoning: %#v", captured)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxExplicitGatewayAndCustomModels(t *testing.T) {
|
||||
for _, provider := range []string{"bailian-coding-plan", "custom", "openrouter"} {
|
||||
for _, protocol := range []string{"openai-completions", "anthropic-messages"} {
|
||||
for _, model := range []string{"MiniMax-M2.5", "MiniMax-M3.1-Flash", "my-custom-model"} {
|
||||
request := BuildVerifyRequest(provider, protocol, AuthModeBearer, "https://example.test/v1", "test-key", model)
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(request.Body, &body); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if body["model"] != model || body["reasoning_effort"] != nil || body["output_config"] != nil || body["thinking"] != nil {
|
||||
t.Fatalf("changed explicit %s request: %#v", provider, body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,9 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
||||
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
||||
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
|
||||
@@ -40,8 +43,8 @@ func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL,
|
||||
providerKey = "moonshot"
|
||||
resolvedAPIType = "openai-completions"
|
||||
usesBearer = false
|
||||
case "ollama":
|
||||
apiKey = "ollama"
|
||||
case "ollama", "llmman":
|
||||
apiKey = provider
|
||||
usesBearer = false
|
||||
case "openai", "openrouter", "anthropic":
|
||||
preserveQualifiedModel = strings.Contains(modelName, "/")
|
||||
|
||||
@@ -27,11 +27,14 @@ type verifyErrorResponse struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
const defaultVerifyTimeout = 30 * time.Second
|
||||
const (
|
||||
defaultVerifyTimeout = 30 * time.Second
|
||||
defaultVerifyMaxTokens = 16
|
||||
)
|
||||
|
||||
func SkipVerification(provider string) bool {
|
||||
switch provider {
|
||||
case "vllm", "ollama", "kimi-coding":
|
||||
case "vllm", "ollama", "llmman", "kimi-coding":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -64,7 +67,10 @@ func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) e
|
||||
}
|
||||
|
||||
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
||||
baseURL = strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
baseURL = strings.TrimSpace(baseURL)
|
||||
if provider != "custom" || !IsImageAPIType(apiType) {
|
||||
baseURL = strings.TrimRight(baseURL, "/")
|
||||
}
|
||||
headers := map[string]string{"Content-Type": "application/json"}
|
||||
request := VerifyRequest{Method: http.MethodPost, Headers: headers}
|
||||
|
||||
@@ -78,6 +84,32 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
}
|
||||
|
||||
switch apiType {
|
||||
case "openai-embeddings":
|
||||
request.URL = embeddingVerifyURL(baseURL)
|
||||
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 {
|
||||
@@ -86,26 +118,53 @@ func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model stri
|
||||
headers["x-api-key"] = apiKey
|
||||
}
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "max_tokens": 1, "stream": false,
|
||||
body := map[string]interface{}{
|
||||
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||
})
|
||||
}
|
||||
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||
body["thinking"] = map[string]string{"type": "adaptive"}
|
||||
body["output_config"] = map[string]string{"effort": "max"}
|
||||
}
|
||||
request.Body = mustJSON(body)
|
||||
case "openai-responses":
|
||||
request.URL = baseURL + "/responses"
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": 1, "stream": false})
|
||||
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
|
||||
default:
|
||||
request.URL = baseURL + "/chat/completions"
|
||||
if provider != "ollama" || strings.TrimSpace(apiKey) != "" {
|
||||
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||
headers["Authorization"] = "Bearer " + apiKey
|
||||
}
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": 1, "stream": false,
|
||||
})
|
||||
body := map[string]interface{}{
|
||||
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||
}
|
||||
if provider == "minimax" && model == "MiniMax-M3.1-Flash" {
|
||||
body["reasoning_effort"] = "max"
|
||||
}
|
||||
request.Body = mustJSON(body)
|
||||
}
|
||||
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
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
@@ -17,7 +18,8 @@ type IAgentAccountRepo interface {
|
||||
Save(account *model.AgentAccount) error
|
||||
DeleteByID(id uint) error
|
||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||
CountByProviders(providers []string) (map[string]int64, error)
|
||||
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||
SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error
|
||||
}
|
||||
|
||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||
@@ -67,7 +69,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
||||
return accounts, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
|
||||
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||
normalizedProviders := normalizeProviders(providers)
|
||||
counts := make(map[string]int64, len(normalizedProviders))
|
||||
for _, provider := range normalizedProviders {
|
||||
@@ -86,6 +88,7 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
||||
Model(&model.AgentAccount{}).
|
||||
Select("provider, COUNT(*) as count").
|
||||
Where("provider IN ?", normalizedProviders).
|
||||
Scopes(WithTextAPIType()).
|
||||
Group("provider").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -96,6 +99,25 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
||||
return counts, nil
|
||||
}
|
||||
|
||||
func (a AgentAccountRepo) SaveLocal(account *model.AgentAccount, models []model.AgentAccountModel) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Save(account).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if err := tx.Where("account_id = ?", account.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(models) == 0 {
|
||||
return nil
|
||||
}
|
||||
for i := range models {
|
||||
models[i].AccountID = account.ID
|
||||
models[i].SortOrder = i + 1
|
||||
}
|
||||
return tx.Create(&models).Error
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeProviders(providers []string) []string {
|
||||
seen := make(map[string]struct{}, len(providers))
|
||||
result := make([]string, 0, len(providers))
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
)
|
||||
@@ -14,6 +16,7 @@ type IAiRepo interface {
|
||||
Create(cronjob *model.OllamaModel) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteAll(ctx context.Context) error
|
||||
}
|
||||
|
||||
func NewIAiRepo() IAiRepo {
|
||||
@@ -67,3 +70,7 @@ func (u *AiRepo) Delete(opts ...DBOption) error {
|
||||
}
|
||||
return db.Delete(&model.OllamaModel{}).Error
|
||||
}
|
||||
|
||||
func (u *AiRepo) DeleteAll(ctx context.Context) error {
|
||||
return getTx(ctx).Where("1 = 1").Delete(&model.OllamaModel{}).Error
|
||||
}
|
||||
|
||||
+217
-9
@@ -1,20 +1,30 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"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"
|
||||
"google.golang.org/genproto/googleapis/type/date"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
"time"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type AlertRepo struct{}
|
||||
|
||||
var (
|
||||
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
|
||||
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
|
||||
)
|
||||
|
||||
type IAlertRepo interface {
|
||||
WithByType(alertType string) DBOption
|
||||
WithByStatusIn(status []string) DBOption
|
||||
@@ -24,6 +34,7 @@ type IAlertRepo interface {
|
||||
WithByCreateAt(date *date.Date) DBOption
|
||||
WithByLicenseId(licenseId string) DBOption
|
||||
WithByRecordId(recordId uint) DBOption
|
||||
WithByDeliveryLogID(logID uint) DBOption
|
||||
WithByAlertMethodContainsConfigID(id uint) DBOption
|
||||
WithByMethodConfigIDs(ids []uint) DBOption
|
||||
|
||||
@@ -45,6 +56,8 @@ type IAlertRepo interface {
|
||||
CleanAlertLogs() 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
|
||||
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
||||
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
||||
@@ -55,6 +68,7 @@ type IAlertRepo interface {
|
||||
GetConfigById(id uint) (model.AlertConfig, error)
|
||||
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, 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
|
||||
DeleteAlertConfig(opts ...DBOption) error
|
||||
|
||||
@@ -223,13 +237,78 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CleanAlertLogs() error {
|
||||
return global.AlertDB.Where("1 = 1").Delete(&model.AlertLog{}).Error
|
||||
return global.AlertDB.Where("status <> ?", constant.AlertPushing).Delete(&model.AlertLog{}).Error
|
||||
}
|
||||
|
||||
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) 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 {
|
||||
db, _ := getAlertDB(opts...)
|
||||
return db.Delete(&model.AlertTask{}).Error
|
||||
@@ -310,7 +389,23 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
|
||||
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 {
|
||||
ensureAlertConfigUID(config)
|
||||
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
|
||||
}
|
||||
|
||||
@@ -338,6 +433,12 @@ 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) {
|
||||
var configs []model.AlertConfig
|
||||
db := global.AlertDB.Model(&model.AlertConfig{})
|
||||
@@ -378,26 +479,44 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
oldConfigMap := make(map[string]uint)
|
||||
oldConfigMap := make(map[string]model.AlertConfig)
|
||||
oldConfigByUID := make(map[string]model.AlertConfig)
|
||||
oldConfigByType := make(map[string][]model.AlertConfig)
|
||||
oldConfigByKey := make(map[string][]model.AlertConfig)
|
||||
consumedConfigIDs := make(map[uint]struct{})
|
||||
for _, item := range oldConfigs {
|
||||
if strings.TrimSpace(item.UID) != "" {
|
||||
oldConfigByUID[item.UID] = item
|
||||
}
|
||||
if singletonTypes[item.Type] {
|
||||
oldConfigMap[item.Type] = item.ID
|
||||
oldConfigMap[item.Type] = item
|
||||
continue
|
||||
}
|
||||
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
||||
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
||||
}
|
||||
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 val, ok := oldConfigMap[item.Type]; ok {
|
||||
item.ID = val
|
||||
if matched, ok := oldConfigMap[item.Type]; ok {
|
||||
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
delete(oldConfigMap, item.Type)
|
||||
consumedConfigIDs[item.ID] = struct{}{}
|
||||
} else {
|
||||
item.ID = 0
|
||||
ensureAlertConfigUID(&item)
|
||||
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if item.ID == 0 {
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
@@ -411,9 +530,31 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
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)
|
||||
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
|
||||
item.ID = matched.ID
|
||||
delete(oldConfigByUID, matched.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()
|
||||
@@ -424,7 +565,12 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
}
|
||||
|
||||
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
|
||||
item.ID = matched.ID
|
||||
delete(oldConfigByUID, matched.UID)
|
||||
deleteAlertConfigByID(oldConfigByKey, matched.ID)
|
||||
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()
|
||||
@@ -434,6 +580,11 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
}
|
||||
|
||||
item.ID = 0
|
||||
ensureAlertConfigUID(&item)
|
||||
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
if err := tx.Create(&item).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
@@ -458,6 +609,63 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
||||
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) {
|
||||
var alerts []model.Alert
|
||||
if err := tx.Select("method").Find(&alerts).Error; err != nil {
|
||||
|
||||
@@ -38,6 +38,7 @@ type IAppInstallRepo interface {
|
||||
LoadBaseInfo(key string, name string) (*RootInfo, error)
|
||||
LoadInstallAppByKeyAndName(key string, name string) (*model.AppInstall, error)
|
||||
GetFirstByCtx(ctx context.Context, opts ...DBOption) (model.AppInstall, error)
|
||||
GetSnapshot(ctx context.Context, id uint) (model.AppInstall, error)
|
||||
}
|
||||
|
||||
func NewIAppInstallRepo() IAppInstallRepo {
|
||||
@@ -131,6 +132,12 @@ func (a *AppInstallRepo) GetFirstByCtx(ctx context.Context, opts ...DBOption) (m
|
||||
return install, err
|
||||
}
|
||||
|
||||
func (a *AppInstallRepo) GetSnapshot(ctx context.Context, id uint) (model.AppInstall, error) {
|
||||
var install model.AppInstall
|
||||
err := getTx(ctx).WithContext(ctx).Preload("App").Where("id = ?", id).First(&install).Error
|
||||
return install, err
|
||||
}
|
||||
|
||||
func (a *AppInstallRepo) Create(ctx context.Context, install *model.AppInstall) error {
|
||||
db := getTx(ctx).Model(&model.AppInstall{})
|
||||
return db.Omit(clause.Associations).Create(&install).Error
|
||||
|
||||
@@ -3,6 +3,7 @@ package repo
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type LauncherRepo struct{}
|
||||
@@ -17,6 +18,11 @@ type ILauncherRepo interface {
|
||||
GetQuickJump(opts ...DBOption) (model.QuickJump, error)
|
||||
ListQuickJump(withAll bool) []model.QuickJump
|
||||
UpdateQuicks(quicks []model.QuickJump) error
|
||||
ListQuickDirectories() ([]model.QuickDirectory, error)
|
||||
CountQuickDirectories() (int64, error)
|
||||
CreateQuickDirectory(item *model.QuickDirectory) error
|
||||
UpdateQuickDirectory(item *model.QuickDirectory) error
|
||||
DeleteQuickDirectory(id uint, legacy bool) error
|
||||
}
|
||||
|
||||
func NewILauncherRepo() ILauncherRepo {
|
||||
@@ -95,13 +101,73 @@ func (u *LauncherRepo) UpdateQuicks(quicks []model.QuickJump) error {
|
||||
for _, item := range quicks {
|
||||
if err := tx.Model(&model.QuickJump{}).Where("id = ?", item.ID).Updates(map[string]interface{}{
|
||||
"is_show": item.IsShow,
|
||||
"detail": item.Detail,
|
||||
"alias": item.Alias,
|
||||
}).Error; err != nil {
|
||||
tx.Rollback()
|
||||
return err
|
||||
}
|
||||
}
|
||||
tx.Commit()
|
||||
return tx.Commit().Error
|
||||
}
|
||||
|
||||
// Legacy entries remain in the overview configuration until explicitly deleted.
|
||||
func legacyQuickDirectories(db *gorm.DB) *gorm.DB {
|
||||
return db.Model(&model.QuickJump{}).Where("name = ? AND is_show = ? AND detail <> ?", "File", true, "")
|
||||
}
|
||||
|
||||
func (u *LauncherRepo) ListQuickDirectories() ([]model.QuickDirectory, error) {
|
||||
items := make([]model.QuickDirectory, 0)
|
||||
if err := global.DB.Order("id ASC").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var quicks []model.QuickJump
|
||||
if err := legacyQuickDirectories(global.DB).Order("id ASC").Find(&quicks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, quick := range quicks {
|
||||
items = append(items, model.QuickDirectory{BaseModel: quick.BaseModel, Path: quick.Detail, Alias: quick.Alias, Legacy: true})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
func (u *LauncherRepo) CountQuickDirectories() (int64, error) {
|
||||
var count, legacyCount int64
|
||||
if err := global.DB.Model(&model.QuickDirectory{}).Count(&count).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
if err := legacyQuickDirectories(global.DB).Count(&legacyCount).Error; err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return count + legacyCount, nil
|
||||
}
|
||||
func (u *LauncherRepo) CreateQuickDirectory(item *model.QuickDirectory) error {
|
||||
return global.DB.Create(item).Error
|
||||
}
|
||||
func (u *LauncherRepo) UpdateQuickDirectory(item *model.QuickDirectory) error {
|
||||
var result *gorm.DB
|
||||
if item.Legacy {
|
||||
result = legacyQuickDirectories(global.DB).Where("id = ?", item.ID).Updates(map[string]interface{}{"detail": item.Path, "alias": item.Alias})
|
||||
} else {
|
||||
result = global.DB.Model(&model.QuickDirectory{}).Where("id = ?", item.ID).Updates(map[string]interface{}{"path": item.Path, "alias": item.Alias})
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
func (u *LauncherRepo) DeleteQuickDirectory(id uint, legacy bool) error {
|
||||
var result *gorm.DB
|
||||
if legacy {
|
||||
result = legacyQuickDirectories(global.DB).Where("id = ?", id).Updates(map[string]interface{}{"detail": nil, "alias": nil})
|
||||
} else {
|
||||
result = global.DB.Delete(&model.QuickDirectory{}, id)
|
||||
}
|
||||
if result.Error != nil {
|
||||
return result.Error
|
||||
}
|
||||
if result.RowsAffected == 0 {
|
||||
return gorm.ErrRecordNotFound
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -49,6 +49,12 @@ 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 {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("addr = ?", addr)
|
||||
@@ -94,6 +100,18 @@ 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 {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if len(model) == 0 {
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"gorm.io/gorm"
|
||||
@@ -20,6 +21,8 @@ type IDatabaseRepo interface {
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
Delete(ctx context.Context, opts ...DBOption) error
|
||||
Get(opts ...DBOption) (model.Database, error)
|
||||
ListInstalledLocalMysql(ctx context.Context) ([]InstalledLocalMysql, error)
|
||||
GetInstalledLocalMysql(ctx context.Context, name string) (InstalledLocalMysql, error)
|
||||
WithoutByFrom(from string) DBOption
|
||||
WithAppInstallID(appInstallID uint) DBOption
|
||||
WithTypeList(dbType string) DBOption
|
||||
@@ -29,6 +32,32 @@ func NewIDatabaseRepo() IDatabaseRepo {
|
||||
return &DatabaseRepo{}
|
||||
}
|
||||
|
||||
type InstalledLocalMysql struct {
|
||||
ID uint
|
||||
Type, From, Database string
|
||||
ContainerName, Version, Env string
|
||||
}
|
||||
|
||||
func installedLocalMysqlQuery(ctx context.Context) *gorm.DB {
|
||||
return getTx(ctx).WithContext(ctx).Table("databases AS d").
|
||||
Joins("JOIN app_installs AS ai ON ai.id = d.app_install_id AND ai.name = d.name").
|
||||
Joins("JOIN apps AS a ON a.id = ai.app_id").
|
||||
Where("d.type = ? AND d.\"from\" = ? AND a.key = ?", constant.AppMysql, "local", constant.AppMysql).
|
||||
Where("ai.status <> ? AND ai.container_name <> ?", constant.StatusDeleted, "")
|
||||
}
|
||||
|
||||
func (d *DatabaseRepo) ListInstalledLocalMysql(ctx context.Context) ([]InstalledLocalMysql, error) {
|
||||
result := make([]InstalledLocalMysql, 0)
|
||||
err := installedLocalMysqlQuery(ctx).Select("d.id, d.type, d.\"from\", d.name AS database, ai.version").Order("d.id").Scan(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *DatabaseRepo) GetInstalledLocalMysql(ctx context.Context, name string) (InstalledLocalMysql, error) {
|
||||
var result InstalledLocalMysql
|
||||
err := installedLocalMysqlQuery(ctx).Where("d.name = ?", name).Select("d.id, ai.container_name, ai.version, ai.env").Take(&result).Error
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (d *DatabaseRepo) Get(opts ...DBOption) (model.Database, error) {
|
||||
var database model.Database
|
||||
db := global.DB
|
||||
|
||||
@@ -18,6 +18,7 @@ type IMysqlRepo interface {
|
||||
List(opts ...DBOption) ([]model.DatabaseMysql, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMysql, error)
|
||||
Create(ctx context.Context, mysql *model.DatabaseMysql) error
|
||||
CreateMetadata(ctx context.Context, mysql *model.DatabaseMysql) error
|
||||
Delete(ctx context.Context, opts ...DBOption) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
DeleteLocal(ctx context.Context) error
|
||||
@@ -94,6 +95,10 @@ func (u *MysqlRepo) Create(ctx context.Context, mysql *model.DatabaseMysql) erro
|
||||
return getTx(ctx).Create(mysql).Error
|
||||
}
|
||||
|
||||
func (u *MysqlRepo) CreateMetadata(ctx context.Context, mysql *model.DatabaseMysql) error {
|
||||
return getTx(ctx).WithContext(ctx).Create(mysql).Error
|
||||
}
|
||||
|
||||
func (u *MysqlRepo) Delete(ctx context.Context, opts ...DBOption) error {
|
||||
return getTx(ctx, opts...).Delete(&model.DatabaseMysql{}).Error
|
||||
}
|
||||
|
||||
@@ -14,8 +14,10 @@ type DatabaseUserRepo struct{}
|
||||
|
||||
type IDatabaseUserRepo interface {
|
||||
Get(opts ...DBOption) (model.DatabaseUser, error)
|
||||
GetEncryptedWithContext(ctx context.Context, opts ...DBOption) (model.DatabaseUser, error)
|
||||
List(opts ...DBOption) ([]model.DatabaseUser, error)
|
||||
Save(user *model.DatabaseUser) error
|
||||
SaveEncryptedWithContext(ctx context.Context, user *model.DatabaseUser) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteBy(ctx context.Context, opts ...DBOption) error
|
||||
Update(vars map[string]interface{}, opts ...DBOption) error
|
||||
@@ -45,6 +47,13 @@ func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// GetEncryptedWithContext leaves credential transformation to the caller.
|
||||
func (u *DatabaseUserRepo) GetEncryptedWithContext(ctx context.Context, opts ...DBOption) (model.DatabaseUser, error) {
|
||||
var user model.DatabaseUser
|
||||
err := getTx(ctx, opts...).WithContext(ctx).First(&user).Error
|
||||
return user, err
|
||||
}
|
||||
|
||||
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
|
||||
var users []model.DatabaseUser
|
||||
db := global.DB.Model(&model.DatabaseUser{})
|
||||
@@ -75,6 +84,10 @@ func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
|
||||
return global.DB.Save(user).Error
|
||||
}
|
||||
|
||||
func (u *DatabaseUserRepo) SaveEncryptedWithContext(ctx context.Context, user *model.DatabaseUser) error {
|
||||
return getTx(ctx).WithContext(ctx).Save(user).Error
|
||||
}
|
||||
|
||||
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
|
||||
@@ -14,6 +14,7 @@ type IDatabaseUserGrantRepo interface {
|
||||
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
|
||||
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
|
||||
Save(grant *model.DatabaseUserGrant) error
|
||||
CreateIfAbsent(ctx context.Context, grant *model.DatabaseUserGrant) error
|
||||
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteBy(ctx context.Context, opts ...DBOption) error
|
||||
@@ -52,6 +53,12 @@ func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
|
||||
return global.DB.Save(grant).Error
|
||||
}
|
||||
|
||||
func (u *DatabaseUserGrantRepo) CreateIfAbsent(ctx context.Context, grant *model.DatabaseUserGrant) error {
|
||||
return getTx(ctx).WithContext(ctx).
|
||||
Where("type = ? AND database = ? AND db_name = ? AND username = ? AND host = ?", grant.Type, grant.Database, grant.DBName, grant.Username, grant.Host).
|
||||
FirstOrCreate(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 {
|
||||
|
||||
@@ -22,11 +22,6 @@ type IHostRepo interface {
|
||||
WithByPort(port uint) 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
|
||||
GetCert(opts ...DBOption) (model.RootCert, error)
|
||||
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
|
||||
@@ -34,8 +29,6 @@ type IHostRepo interface {
|
||||
SaveCert(cert *model.RootCert) error
|
||||
UpdateCert(id uint, vars map[string]interface{}) error
|
||||
DeleteCert(opts ...DBOption) error
|
||||
|
||||
WithByChain(chain string) DBOption
|
||||
}
|
||||
|
||||
func NewIHostRepo() IHostRepo {
|
||||
@@ -116,65 +109,6 @@ func (h *HostRepo) Delete(opts ...DBOption) 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) {
|
||||
var cert model.RootCert
|
||||
db := global.DB
|
||||
@@ -253,9 +187,3 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *HostRepo) WithByChain(chain string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("chain = ?", chain)
|
||||
}
|
||||
}
|
||||
|
||||
+106
-13
@@ -1,6 +1,8 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -10,11 +12,25 @@ import (
|
||||
|
||||
type MonitorRepo struct{}
|
||||
|
||||
type GPUHistoryPoint struct {
|
||||
model.MonitorGPU
|
||||
Bucket int64
|
||||
PowerPercent *float64
|
||||
MemoryPercent *float64
|
||||
ProcessCount *float64
|
||||
}
|
||||
|
||||
type IMonitorRepo interface {
|
||||
CleanHost() error
|
||||
CleanGPU() error
|
||||
GetBase(opts ...DBOption) ([]model.MonitorBase, error)
|
||||
GetGPU(opts ...DBOption) ([]model.MonitorGPU, error)
|
||||
CountGPU(opts ...DBOption) (int64, error)
|
||||
GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error)
|
||||
GetGPUDevices() ([]model.MonitorGPU, error)
|
||||
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||
GetIONames() ([]string, error)
|
||||
GetNetworkNames() ([]string, error)
|
||||
|
||||
CreateMonitorBase(model model.MonitorBase) error
|
||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
||||
DelMonitorIO(timeForDelete time.Time) error
|
||||
DelMonitorNet(timeForDelete time.Time) error
|
||||
|
||||
WithByProductName(name string) DBOption
|
||||
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||
}
|
||||
|
||||
func NewIMonitorRepo() IMonitorRepo {
|
||||
return &MonitorRepo{}
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanHost() error {
|
||||
for _, item := range []interface{}{&model.MonitorBase{}, &model.MonitorIO{}, &model.MonitorNetwork{}} {
|
||||
if err := global.MonitorDB.Where("1 = 1").Delete(item).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) CleanGPU() error {
|
||||
return global.GPUMonitorDB.Where("1 = 1").Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||
var data []model.MonitorBase
|
||||
db := global.MonitorDB
|
||||
@@ -59,20 +88,25 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
db := global.GPUMonitorDB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
err := db.Find(&data).Error
|
||||
return data, err
|
||||
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||
var names []string
|
||||
err := global.MonitorDB.Model(&model.MonitorNetwork{}).Distinct().Pluck("name", &names).Error
|
||||
return names, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||
return global.MonitorDB.Create(&model).Error
|
||||
}
|
||||
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
||||
if len(list) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
||||
}
|
||||
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
||||
@@ -94,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||
}
|
||||
|
||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("product_name = ?", name)
|
||||
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||
var data []model.MonitorGPU
|
||||
err := global.GPUMonitorDB.Model(&model.MonitorGPU{}).Select("device_id, product_name, device_type").Group("device_id, product_name, device_type").Order("product_name, device_id").Find(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) WithByGPUDevice(deviceID, name string, legacy bool) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if deviceID != "" {
|
||||
return db.Where("device_id = ?", deviceID)
|
||||
}
|
||||
db = db.Where("product_name = ?", name)
|
||||
if legacy {
|
||||
db = db.Where("device_id IS NULL OR device_id = ''")
|
||||
}
|
||||
return db
|
||||
}
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) CountGPU(opts ...DBOption) (int64, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
var count int64
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (u *MonitorRepo) GetGPUHistory(start time.Time, bucketSeconds int64, aggregation string, opts ...DBOption) ([]GPUHistoryPoint, error) {
|
||||
db := global.GPUMonitorDB.Model(&model.MonitorGPU{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
expressions := []string{
|
||||
"CASE WHEN max_power_limit > 0 THEN 100.0 * power_draw / max_power_limit END",
|
||||
"CASE WHEN mem_total > 0 AND mem_used IS NOT NULL THEN 100.0 * mem_used / mem_total ELSE memory_util END",
|
||||
"CASE WHEN (process_status = 'ok' OR process_status IS NULL OR process_status = '') AND json_valid(processes) THEN CASE WHEN json_type(processes) = 'array' THEN json_array_length(processes) END END",
|
||||
}
|
||||
aliases := []string{"power_percent", "memory_percent", "process_count"}
|
||||
columns := []string{"*"}
|
||||
if bucketSeconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), bucketSeconds)}
|
||||
for _, column := range []string{"memory_activity", "encoder_util", "decoder_util", "jpeg_util", "ofa_util", "media_util", "compute_util", "copy_util", "hotspot_temperature", "fan_rpm", "ai_cpu_util", "ctrl_cpu_util", "ddr_used", "ddr_total", "hbm_used", "hbm_total", "ddr_bandwidth", "hbm_bandwidth", "memory_bandwidth", "media_frequency", "hugepages_used", "hugepages_total", "gpu_util", "temperature", "memory_temperature", "power_draw", "max_power_limit", "mem_used", "mem_total", "frequency", "memory_frequency", "fan_speed"} {
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
for i := range expressions {
|
||||
expressions[i] = operation + "(" + expressions[i] + ")"
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
for i, expression := range expressions {
|
||||
columns = append(columns, expression+" AS "+aliases[i])
|
||||
}
|
||||
var data []GPUHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&data).Error
|
||||
return data, err
|
||||
}
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type VLLMMonitorRepo struct{}
|
||||
|
||||
type VLLMHistoryPoint struct {
|
||||
model.MonitorVLLM
|
||||
Bucket int64
|
||||
CompletedRequests *float64
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Create(point *model.MonitorVLLM) error {
|
||||
return global.VLLMMonitorDB.Create(point).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Latest(id uint) (model.MonitorVLLM, error) {
|
||||
var point model.MonitorVLLM
|
||||
db := global.VLLMMonitorDB.Where("app_install_id = ?", id)
|
||||
err := db.Order("created_at DESC, id DESC").First(&point).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return point, nil
|
||||
}
|
||||
return point, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) CleanTarget(id uint) error {
|
||||
return global.VLLMMonitorDB.Where("app_install_id = ?", id).Delete(&model.MonitorVLLM{}).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) DeleteBefore(before time.Time) error {
|
||||
return global.VLLMMonitorDB.Where("created_at < ?", before).Delete(&model.MonitorVLLM{}).Error
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) Count(ctx context.Context, id uint, start, end time.Time) (int64, error) {
|
||||
var count int64
|
||||
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||
err := db.Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) History(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string) ([]VLLMHistoryPoint, error) {
|
||||
db := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||
var metrics []string
|
||||
if group == "" || group == "overview" {
|
||||
metrics = append(metrics, "running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput")
|
||||
if seconds <= 0 || aggregation == "max" {
|
||||
metrics = append(metrics, "prefix_cache_hit_rate")
|
||||
}
|
||||
}
|
||||
if seconds <= 0 || aggregation == "max" {
|
||||
if group == "" || group == "latency" {
|
||||
metrics = append(metrics, "queue_time", "time_to_first_token", "time_per_output_token", "request_latency", "prefill_time", "decode_time", "time_to_first_token_p50", "time_to_first_token_p90", "time_to_first_token_p95", "time_to_first_token_p99", "time_per_output_token_p50", "time_per_output_token_p90", "time_per_output_token_p95", "time_per_output_token_p99", "request_latency_p50", "request_latency_p90", "request_latency_p95", "request_latency_p99")
|
||||
}
|
||||
if group == "" || group == "requests" {
|
||||
metrics = append(metrics, "average_generation_tokens")
|
||||
}
|
||||
}
|
||||
var columns []string
|
||||
if seconds > 0 {
|
||||
operation := "AVG"
|
||||
if aggregation == "max" {
|
||||
operation = "MAX"
|
||||
}
|
||||
columns = []string{fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d AS bucket", start.Unix(), seconds)}
|
||||
for _, column := range metrics {
|
||||
columns = append(columns, operation+"("+column+") AS "+column)
|
||||
}
|
||||
db = db.Group("bucket").Order("bucket ASC")
|
||||
} else {
|
||||
columns = append([]string{"id", "created_at", "app_install_id", "status", "sample_interval_seconds"}, metrics...)
|
||||
if group == "" || group == "requests" {
|
||||
columns = append(columns, "histogram_deltas", "request_deltas")
|
||||
}
|
||||
db = db.Order("created_at ASC, id ASC")
|
||||
}
|
||||
if group == "" || group == "overview" {
|
||||
count := "CASE WHEN json_valid(request_deltas) THEN CASE WHEN json_extract(request_deltas, '$.seconds') > 0 THEN ROUND(request_throughput * json_extract(request_deltas, '$.seconds')) END END"
|
||||
if seconds > 0 {
|
||||
count = "SUM(" + count + ")"
|
||||
}
|
||||
columns = append(columns, count+" AS completed_requests")
|
||||
}
|
||||
var points []VLLMHistoryPoint
|
||||
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||
return points, err
|
||||
}
|
||||
|
||||
func (r *VLLMMonitorRepo) WalkDeltas(ctx context.Context, id uint, start, end time.Time, seconds int64, aggregation, group string, visit func(int64, string, string) error) error {
|
||||
histograms, requests := "''", "''"
|
||||
if group == "" || group == "requests" || (group == "latency" && aggregation != "max") {
|
||||
histograms = "COALESCE(histogram_deltas, '')"
|
||||
}
|
||||
if group == "" || group == "requests" || (group == "overview" && aggregation != "max") {
|
||||
requests = "COALESCE(request_deltas, '')"
|
||||
}
|
||||
if histograms == "''" && requests == "''" {
|
||||
return ctx.Err()
|
||||
}
|
||||
bucket := fmt.Sprintf("(CAST(strftime('%%s', created_at) AS INTEGER) - %d) / %d", start.Unix(), seconds)
|
||||
rows, err := global.VLLMMonitorDB.WithContext(ctx).Model(&model.MonitorVLLM{}).
|
||||
Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end).
|
||||
Select(bucket + ", " + histograms + ", " + requests).Rows()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer rows.Close()
|
||||
for rows.Next() {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
var index int64
|
||||
var histograms, requests string
|
||||
if err := rows.Scan(&index, &histograms, &requests); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := visit(index, histograms, requests); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return rows.Err()
|
||||
}
|
||||
@@ -1,11 +1,13 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
)
|
||||
|
||||
type SettingRepo struct{}
|
||||
@@ -13,18 +15,26 @@ type SettingRepo struct{}
|
||||
type ISettingRepo interface {
|
||||
GetList(opts ...DBOption) ([]model.Setting, error)
|
||||
Get(opts ...DBOption) (model.Setting, error)
|
||||
GetWithContext(ctx context.Context, opts ...DBOption) (model.Setting, error)
|
||||
GetValueByKey(key string) (string, error)
|
||||
GetCoreValueByKey(key string) (string, error)
|
||||
Create(key, value string) error
|
||||
CreateWithContext(ctx context.Context, setting *model.Setting) error
|
||||
Update(key, value string) error
|
||||
UpdateIfMatch(ctx context.Context, id uint, expectedValue, value string) (int64, error)
|
||||
WithByKey(key string) DBOption
|
||||
|
||||
UpdateOrCreate(key, value string) error
|
||||
UpdateValues(map[string]string) error
|
||||
|
||||
GetDescription(opts ...DBOption) (model.CommonDescription, error)
|
||||
GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error)
|
||||
CreateDescription(data *model.CommonDescription) error
|
||||
SaveDescriptions(context.Context, []model.CommonDescription) error
|
||||
UpdateDescription(id string, val map[string]interface{}) error
|
||||
DelDescription(id string) error
|
||||
DeleteDescriptions(context.Context, string, []string, bool) (int64, error)
|
||||
WithDescriptionIDs(ids []string) DBOption
|
||||
WithByDescriptionID(id string) DBOption
|
||||
}
|
||||
|
||||
@@ -60,6 +70,22 @@ func (s *SettingRepo) Get(opts ...DBOption) (model.Setting, error) {
|
||||
return settings, err
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetWithContext(ctx context.Context, opts ...DBOption) (model.Setting, error) {
|
||||
var setting model.Setting
|
||||
err := getTx(ctx, opts...).WithContext(ctx).First(&setting).Error
|
||||
return setting, err
|
||||
}
|
||||
|
||||
func (s *SettingRepo) CreateWithContext(ctx context.Context, setting *model.Setting) error {
|
||||
return getTx(ctx).WithContext(ctx).Create(setting).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateIfMatch(ctx context.Context, id uint, expectedValue, value string) (int64, error) {
|
||||
result := getTx(ctx).WithContext(ctx).Model(&model.Setting{}).
|
||||
Where("id = ? AND value = ?", id, expectedValue).Update("value", value)
|
||||
return result.RowsAffected, result.Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
||||
var setting model.Setting
|
||||
if err := global.DB.Model(&model.Setting{}).Where("key = ?", key).First(&setting).Error; err != nil {
|
||||
@@ -68,6 +94,17 @@ func (s *SettingRepo) GetValueByKey(key string) (string, error) {
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetCoreValueByKey(key string) (string, error) {
|
||||
if global.CoreDB == nil {
|
||||
return "", errors.New("core database is unavailable")
|
||||
}
|
||||
var setting model.Setting
|
||||
if err := global.CoreDB.Where("key = ?", key).First(&setting).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
return setting.Value, nil
|
||||
}
|
||||
|
||||
func (s *SettingRepo) WithByKey(key string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("key = ?", key)
|
||||
@@ -90,6 +127,25 @@ func (s *SettingRepo) UpdateOrCreate(key, value string) error {
|
||||
return global.DB.Model(&setting).UpdateColumn("value", value).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateValues(values map[string]string) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
for key, value := range values {
|
||||
var setting model.Setting
|
||||
err := tx.Where("key = ?", key).First(&setting).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if err := tx.Create(&model.Setting{Key: key, Value: value}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
} else if err != nil {
|
||||
return err
|
||||
} else if err := tx.Model(&setting).UpdateColumn("value", value).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (s *SettingRepo) GetDescriptionList(opts ...DBOption) ([]model.CommonDescription, error) {
|
||||
var lists []model.CommonDescription
|
||||
db := global.DB.Model(&model.CommonDescription{})
|
||||
@@ -111,14 +167,42 @@ func (s *SettingRepo) GetDescription(opts ...DBOption) (model.CommonDescription,
|
||||
func (s *SettingRepo) CreateDescription(data *model.CommonDescription) error {
|
||||
return global.DB.Create(data).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) SaveDescriptions(ctx context.Context, descriptions []model.CommonDescription) error {
|
||||
return global.DB.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "id"}},
|
||||
DoUpdates: clause.AssignmentColumns([]string{"description"}),
|
||||
}).CreateInBatches(&descriptions, 100).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) UpdateDescription(id string, val map[string]interface{}) error {
|
||||
return global.DB.Model(&model.CommonDescription{}).Where("id = ?", id).Updates(val).Error
|
||||
}
|
||||
func (s *SettingRepo) DelDescription(id string) error {
|
||||
return global.DB.Where("id = ?", id).Delete(&model.CommonDescription{}).Error
|
||||
}
|
||||
|
||||
func (s *SettingRepo) DeleteDescriptions(ctx context.Context, kind string, ids []string, emptyOnly bool) (int64, error) {
|
||||
var deleted int64
|
||||
for start := 0; start < len(ids); start += 500 {
|
||||
query := global.DB.WithContext(ctx).Where("type = ? AND id IN ?", kind, ids[start:min(start+500, len(ids))])
|
||||
if emptyOnly {
|
||||
query = query.Where("description = ? AND is_pinned = ?", "", false)
|
||||
}
|
||||
result := query.Delete(&model.CommonDescription{})
|
||||
deleted += result.RowsAffected
|
||||
if result.Error != nil {
|
||||
return deleted, result.Error
|
||||
}
|
||||
}
|
||||
return deleted, nil
|
||||
}
|
||||
func (s *SettingRepo) WithByDescriptionID(id string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SettingRepo) WithDescriptionIDs(ids []string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB { return db.Where("id IN ?", ids) }
|
||||
}
|
||||
|
||||
+22
-6
@@ -2,6 +2,7 @@ package repo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
@@ -18,8 +19,8 @@ type ITaskRepo interface {
|
||||
GetFirst(opts ...DBOption) (model.Task, error)
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.Task, error)
|
||||
Update(ctx context.Context, task *model.Task) error
|
||||
UpdateRunningTaskToFailed() error
|
||||
CountExecutingTask() (int64, error)
|
||||
UpdateRunningTaskToFailed(opts ...DBOption) error
|
||||
CountExecutingTask(opts ...DBOption) (int64, error)
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteAll() error
|
||||
|
||||
@@ -28,6 +29,7 @@ type ITaskRepo interface {
|
||||
WithResourceID(id uint) DBOption
|
||||
WithOperate(taskOperate string) DBOption
|
||||
WithByStatus(status string) DBOption
|
||||
WithoutScopes(scopes ...string) DBOption
|
||||
}
|
||||
|
||||
func NewITaskRepo() ITaskRepo {
|
||||
@@ -109,16 +111,30 @@ func (t TaskRepo) Update(ctx context.Context, task *model.Task) error {
|
||||
return getTaskTx(ctx).Save(&task).Error
|
||||
}
|
||||
|
||||
func (t TaskRepo) UpdateRunningTaskToFailed() error {
|
||||
return getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Updates(map[string]interface{}{"status": constant.StatusFailed, "error_msg": "1Panel restart causes failure"}).Error
|
||||
func (t TaskRepo) UpdateRunningTaskToFailed(opts ...DBOption) error {
|
||||
opts = append(opts, t.WithByStatus(constant.StatusExecuting))
|
||||
return getTaskDb(opts...).Model(&model.Task{}).Updates(map[string]interface{}{
|
||||
"status": constant.StatusFailed, "error_msg": constant.InterruptedMsg, "end_at": time.Now(),
|
||||
}).Error
|
||||
}
|
||||
|
||||
func (t TaskRepo) CountExecutingTask() (int64, error) {
|
||||
func (t TaskRepo) CountExecutingTask(opts ...DBOption) (int64, error) {
|
||||
var count int64
|
||||
err := getTaskDb(t.WithByStatus(constant.StatusExecuting)).Model(&model.Task{}).Count(&count).Error
|
||||
opts = append(opts, t.WithByStatus(constant.StatusExecuting))
|
||||
err := getTaskDb(opts...).Model(&model.Task{}).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (t TaskRepo) WithoutScopes(scopes ...string) DBOption {
|
||||
excluded := append([]string(nil), scopes...)
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if len(excluded) == 0 {
|
||||
return db
|
||||
}
|
||||
return db.Where("type IS NULL OR type NOT IN ?", excluded)
|
||||
}
|
||||
}
|
||||
|
||||
func (t TaskRepo) Delete(opts ...DBOption) error {
|
||||
db := global.TaskDB
|
||||
for _, opt := range opts {
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
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
|
||||
}
|
||||
+124
-23
@@ -88,6 +88,7 @@ type IAgentService interface {
|
||||
|
||||
GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error)
|
||||
UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error
|
||||
SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error
|
||||
GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error)
|
||||
UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error
|
||||
GetDiscordConfig(req dto.AgentIDReq) (*dto.AgentDiscordConfig, error)
|
||||
@@ -105,6 +106,10 @@ type IAgentService interface {
|
||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||
UninstallPlugin(req dto.AgentPluginUninstallReq) 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
|
||||
}
|
||||
|
||||
@@ -138,6 +143,9 @@ const (
|
||||
|
||||
func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
agentType := req.AgentType
|
||||
if err := validateNativeAgentCreate(req); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := checkPortExist(req.WebUIPort); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -177,7 +185,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
var allowedOrigins []string
|
||||
var account *model.AgentAccount
|
||||
var installHooks *appInstallHooks
|
||||
var hermesAuth hermesDashboardAuth
|
||||
var dashboardAuth agentDashboardAuth
|
||||
|
||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||
if req.AccountID == 0 {
|
||||
@@ -222,15 +230,19 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
},
|
||||
}
|
||||
} else if agentType == constant.AppHermesAgent {
|
||||
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
installHooks = &appInstallHooks{
|
||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
|
||||
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
|
||||
},
|
||||
}
|
||||
} else if agentType == constant.AppCopaw {
|
||||
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
} else if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOctop {
|
||||
dashboardAuth = agentDashboardAuth{Username: req.DashboardUsername, Password: req.DashboardPassword}
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -239,6 +251,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
constant.HostIP: "",
|
||||
}
|
||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||
if agentType == constant.AppDeepSeekHarness {
|
||||
params["HTTPS_ACCESS_HOST"] = req.AccessHost
|
||||
}
|
||||
if agentType == constant.AppOctop {
|
||||
params["OCTOP_LOG_LEVEL"] = "info"
|
||||
}
|
||||
if agentType == constant.AppOpenclaw {
|
||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||
@@ -250,9 +268,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
params["API_KEY"] = apiKey
|
||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||
}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
|
||||
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
|
||||
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) == "" {
|
||||
@@ -729,7 +750,7 @@ func requiresAgentAccount(agentType string) bool {
|
||||
}
|
||||
|
||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
if agentType == constant.AppDeepSeekHarness || agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||
return
|
||||
}
|
||||
@@ -927,6 +948,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -958,7 +980,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks, req.Metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
@@ -984,19 +1006,28 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
||||
}
|
||||
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{
|
||||
Sort: def.Sort,
|
||||
Provider: key,
|
||||
DisplayName: def.DisplayName,
|
||||
DisplayName: localizedAgentProviderName(key),
|
||||
BaseURL: baseURL,
|
||||
DefaultAPIType: def.DefaultAPIType,
|
||||
APITypes: apiTypes,
|
||||
@@ -1016,7 +1047,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider}, req.Models)
|
||||
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1024,7 +1055,8 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, false)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1062,6 +1094,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.APIType != account.APIType {
|
||||
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
|
||||
}
|
||||
provider := account.Provider
|
||||
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
||||
return err
|
||||
@@ -1074,11 +1109,15 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||
requestedVerifyModel = account.VerifyModel
|
||||
}
|
||||
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
verifyModel := ""
|
||||
if account.AppInstallID == 0 || len(models) > 0 {
|
||||
verifyModel, err = resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel)
|
||||
}
|
||||
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability, account.AppInstallID != 0 && len(models) == 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1092,7 +1131,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
account.Remark = req.Remark
|
||||
account.Verified = true
|
||||
|
||||
if err := global.DB.Save(account).Error; err != nil {
|
||||
if err := agentAccountRepo.Save(account); err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
@@ -1110,6 +1149,12 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
if strings.TrimSpace(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) != "" {
|
||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||
}
|
||||
@@ -1117,17 +1162,38 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
installIDs := make([]uint, 0, len(list))
|
||||
for _, account := range list {
|
||||
if account.AppInstallID != 0 {
|
||||
installIDs = append(installIDs, account.AppInstallID)
|
||||
}
|
||||
}
|
||||
localInstalls := make(map[uint]bool, len(installIDs))
|
||||
if len(installIDs) > 0 {
|
||||
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByIDs(installIDs))
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, install := range installs {
|
||||
localInstalls[install.ID] = true
|
||||
}
|
||||
}
|
||||
items := make([]dto.AgentAccountInfo, 0, len(list))
|
||||
for _, item := range list {
|
||||
apiKey := ""
|
||||
if item.RememberAPIKey {
|
||||
apiKey = item.APIKey
|
||||
}
|
||||
appInstallID := uint(0)
|
||||
if localInstalls[item.AppInstallID] {
|
||||
appInstallID = item.AppInstallID
|
||||
}
|
||||
items = append(items, dto.AgentAccountInfo{
|
||||
AppInstallID: appInstallID,
|
||||
ID: item.ID,
|
||||
MasterAccountID: item.MasterAccountID,
|
||||
Provider: item.Provider,
|
||||
ProviderName: providercatalog.DisplayName(item.Provider),
|
||||
ProviderName: localizedAgentProviderName(item.Provider),
|
||||
Name: item.Name,
|
||||
APIKey: apiKey,
|
||||
RememberAPIKey: item.RememberAPIKey,
|
||||
@@ -1166,7 +1232,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
||||
}
|
||||
|
||||
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
||||
return agentAccountRepo.CountByProviders(req.Providers)
|
||||
return agentAccountRepo.CountTextByProviders(req.Providers)
|
||||
}
|
||||
|
||||
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
||||
@@ -1178,7 +1244,8 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
||||
}
|
||||
|
||||
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
||||
if req.APIType != "openai-completions" && req.APIType != "openai-responses" {
|
||||
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)
|
||||
@@ -1320,13 +1387,27 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
||||
}
|
||||
|
||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model)
|
||||
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true, false)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if account.AppInstallID != 0 && account.Provider != "ollama" {
|
||||
_, err := appInstallRepo.GetFirst(repo.WithByID(account.AppInstallID))
|
||||
if err == nil {
|
||||
return buserr.WithDetail("ErrInvalidParams", "Delete the local inference engine instead", nil)
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
var agentCount int64
|
||||
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||
@@ -1410,7 +1491,7 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
auth := readHermesDashboardAuthFromInstall(install)
|
||||
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||
return &dto.AgentOtherConfig{
|
||||
UserTimezone: cfg.Timezone,
|
||||
BrowserEnabled: true,
|
||||
@@ -1419,6 +1500,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
||||
DashboardPassword: auth.Password,
|
||||
}, nil
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||
return &dto.AgentOtherConfig{
|
||||
DashboardUsername: auth.Username,
|
||||
DashboardPassword: auth.Password,
|
||||
}, nil
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1437,16 +1525,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
if strings.TrimSpace(req.UserTimezone) == "" {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
previousAuth := readHermesDashboardAuthFromInstall(install)
|
||||
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
|
||||
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
|
||||
return err
|
||||
}
|
||||
operate := constant.Restart
|
||||
@@ -1458,6 +1549,12 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
Operate: operate,
|
||||
})
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
@@ -1495,6 +1592,10 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer feishuSwitches.Delete(agent.ConfigPath)
|
||||
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1638,7 +1739,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
return err
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"slices"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -44,9 +45,29 @@ func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentF
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw || !isOpenclawBundledFeishuVersion(install.Version) {
|
||||
result := extractFeishuConfig(conf)
|
||||
installed, _ := checkPluginInstalled(install.GetPath(), "feishu")
|
||||
result.Installed = installed
|
||||
result.Installed, _ = checkPluginInstalled(install.GetPath(), "feishu")
|
||||
return &result, nil
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := extractFeishuConfig(conf)
|
||||
if !legacy {
|
||||
result = extractBundledFeishuConfig(conf)
|
||||
}
|
||||
result.BuiltinManaged = true
|
||||
result.BuiltinAvailable = hasBundledFeishu(install.ContainerName)
|
||||
result.Installed = result.BuiltinAvailable
|
||||
result.Legacy, result.LegacyConfigured = legacy, configured
|
||||
if legacy {
|
||||
result.GroupSessionScope = "group"
|
||||
if result.ThreadSession {
|
||||
result.GroupSessionScope = "group_topic"
|
||||
}
|
||||
}
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
@@ -71,6 +92,9 @@ func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) err
|
||||
})
|
||||
})
|
||||
}
|
||||
if agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return saveBundledFeishuConfig(agent, install, req)
|
||||
}
|
||||
return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error {
|
||||
config := dto.AgentFeishuConfig{
|
||||
Enabled: req.Enabled,
|
||||
@@ -357,6 +381,9 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -402,6 +429,9 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -442,6 +472,9 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -537,10 +570,13 @@ func (a AgentService) DeleteChannelConfig(req dto.AgentChannelDeleteReq) error {
|
||||
}
|
||||
|
||||
func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) {
|
||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if req.Type == "feishu" && agent.AgentType == constant.AppOpenclaw && isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return &dto.AgentPluginStatus{Installed: hasBundledFeishu(install.ContainerName)}, nil
|
||||
}
|
||||
installed, err := checkPluginInstalled(install.GetPath(), req.Type)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1320,6 +1356,10 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
|
||||
}
|
||||
|
||||
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)
|
||||
defer func() {
|
||||
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
||||
@@ -1341,7 +1381,19 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
||||
if pkgPath == "" {
|
||||
return fmt.Errorf("openclaw plugin package not found")
|
||||
}
|
||||
return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
|
||||
args := []string{"exec", containerName, "openclaw", "plugins", "install", pkgPath}
|
||||
// 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")
|
||||
}
|
||||
// Source confirmation does not grant the selected channel plugin's capabilities.
|
||||
if slices.Contains(options, "--accept-capabilities") {
|
||||
args = append(args, "--accept-capabilities")
|
||||
}
|
||||
return mgr.Run("docker", args...)
|
||||
}
|
||||
|
||||
func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
|
||||
|
||||
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,677 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"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/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||
)
|
||||
|
||||
// Keep panel configuration writes out of the backup/activation/recovery window.
|
||||
var feishuSwitches sync.Map
|
||||
|
||||
func isOpenclawBundledFeishuVersion(version string) bool {
|
||||
return common.CompareAppVersion(version, "2026.10.7")
|
||||
}
|
||||
|
||||
func hasBundledFeishu(container string) bool {
|
||||
image, err := feishuContainerImage(container)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
_, err = bundledFeishuImage(image)
|
||||
return err == nil
|
||||
}
|
||||
|
||||
func usesBundledFeishu(conf map[string]interface{}) bool {
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
for _, key := range []string{"threadSession", "replyMode"} {
|
||||
if _, old := f[key]; old {
|
||||
return false
|
||||
}
|
||||
}
|
||||
if _, old := f["streaming"].(bool); old {
|
||||
return false
|
||||
}
|
||||
entries := childMap(childMap(conf, "plugins"), "entries")
|
||||
if extractBoolValue(childMap(entries, "openclaw-lark")["enabled"], false) {
|
||||
return false
|
||||
}
|
||||
_, streaming := f["streaming"].(map[string]interface{})
|
||||
_, selected := entries["feishu"]
|
||||
return selected && (extractBoolValue(childMap(entries, "feishu")["enabled"], false) || f["groupSessionScope"] != nil || streaming)
|
||||
}
|
||||
|
||||
func hasLegacyFeishu(conf map[string]interface{}, installPath string) bool {
|
||||
legacy, _, err := classifyLegacyFeishu(conf, installPath)
|
||||
return err != nil || legacy
|
||||
}
|
||||
|
||||
func selectBundledFeishu(conf map[string]interface{}, enabled bool) {
|
||||
plugins := ensureChildMap(conf, "plugins")
|
||||
entries := ensureChildMap(plugins, "entries")
|
||||
if legacy, ok := entries["openclaw-lark"]; ok {
|
||||
if entry, ok := legacy.(map[string]interface{}); ok {
|
||||
entry["enabled"] = false
|
||||
}
|
||||
}
|
||||
ensureChildMap(entries, "feishu")["enabled"] = enabled
|
||||
// Preserve an absent allowlist; creating one would disable unrelated plugins.
|
||||
if allow, ok := plugins["allow"]; ok && len(extractStringList(allow)) > 0 {
|
||||
ids := slices.DeleteFunc(extractStringList(allow), func(id string) bool { return id == "openclaw-lark" })
|
||||
if !slices.Contains(ids, "feishu") {
|
||||
ids = append(ids, "feishu")
|
||||
}
|
||||
plugins["allow"] = ids
|
||||
}
|
||||
if deny, ok := plugins["deny"]; ok {
|
||||
plugins["deny"] = slices.DeleteFunc(extractStringList(deny), func(id string) bool { return id == "feishu" })
|
||||
}
|
||||
}
|
||||
|
||||
func feishuConfigError(field string) error { return buserr.WithDetail("ErrFeishuConfig", field, nil) }
|
||||
|
||||
func convertLegacyFeishu(conf map[string]interface{}, req dto.AgentFeishuSwitchReq) error {
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
if len(f) == 0 {
|
||||
if raw, exists := childMap(conf, "channels")["feishu"]; exists && raw != nil {
|
||||
if _, ok := raw.(map[string]interface{}); !ok {
|
||||
return feishuConfigError("channels.feishu")
|
||||
}
|
||||
}
|
||||
f = ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
|
||||
f["enabled"], f["dmPolicy"], f["groupPolicy"] = false, "pairing", "allowlist"
|
||||
}
|
||||
if err := checkLegacyFeishuFields(f, "channels.feishu", false); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := convertFeishuAccess(f, "channels.feishu"); err != nil {
|
||||
return err
|
||||
}
|
||||
delete(f, "threadSession")
|
||||
delete(f, "replyMode")
|
||||
f["streaming"] = map[string]interface{}{"mode": "off"}
|
||||
if req.Streaming {
|
||||
f["streaming"] = map[string]interface{}{"mode": "partial"}
|
||||
}
|
||||
f["groupSessionScope"] = req.GroupSessionScope
|
||||
f["requireMention"] = req.RequireMention == "true"
|
||||
delete(f, "topicSessionMode")
|
||||
// Preserve legacy defaults explicitly; bundled defaults are more restrictive.
|
||||
if _, ok := f["groupPolicy"]; !ok {
|
||||
f["groupPolicy"] = "open"
|
||||
}
|
||||
entries := ensureChildMap(ensureChildMap(conf, "plugins"), "entries")
|
||||
ensureChildMap(entries, "openclaw-lark")["enabled"] = false
|
||||
selectBundledFeishu(conf, extractBoolValue(f["enabled"], true))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Limit automatic conversion to the shared channel fields; custom plugin options require review.
|
||||
func checkLegacyFeishuFields(f map[string]interface{}, field string, group bool) error {
|
||||
allowed := strings.Fields("enabled name appId appSecret encryptKey verificationToken domain connectionMode webhookPath defaultAccount dmPolicy allowFrom groupPolicy groupAllowFrom groupSenderAllowFrom requireMention accounts groups streaming threadSession replyMode groupSessionScope topicSessionMode replyInThread")
|
||||
if group {
|
||||
allowed = strings.Fields("enabled requireMention allowFrom groupSessionScope topicSessionMode replyInThread")
|
||||
}
|
||||
for _, key := range sortedChildKeys(f) {
|
||||
if !slices.Contains(allowed, key) {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
}
|
||||
if enabled, exists := f["enabled"]; exists {
|
||||
if _, ok := enabled.(bool); !ok {
|
||||
return feishuConfigError(field + ".enabled")
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"dmPolicy", "groupPolicy"} {
|
||||
if raw, exists := f[key]; exists {
|
||||
value, ok := raw.(string)
|
||||
options := []string{"open", "allowlist", "disabled"}
|
||||
if key == "dmPolicy" {
|
||||
options = append(options, "pairing")
|
||||
}
|
||||
if !ok || !slices.Contains(options, value) {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, key := range []string{"allowFrom", "groupSenderAllowFrom"} {
|
||||
if raw, exists := f[key]; exists {
|
||||
values, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
for _, value := range values {
|
||||
if _, ok := value.(string); !ok {
|
||||
return feishuConfigError(field + "." + key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, section := range []string{"accounts", "groups"} {
|
||||
raw, exists := f[section]
|
||||
if !exists {
|
||||
continue
|
||||
}
|
||||
entries, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
return feishuConfigError(field + "." + section)
|
||||
}
|
||||
for _, id := range sortedChildKeys(entries) {
|
||||
entry, ok := entries[id].(map[string]interface{})
|
||||
name := field + "." + section + "." + id
|
||||
if !ok {
|
||||
return feishuConfigError(name)
|
||||
}
|
||||
if err := checkLegacyFeishuFields(entry, name, section == "groups"); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func convertFeishuAccess(f map[string]interface{}, field string) error {
|
||||
if raw, ok := f["groupAllowFrom"]; ok {
|
||||
values, ok := raw.([]interface{})
|
||||
if !ok {
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
groups, senders := []string{}, []string{}
|
||||
for _, value := range values {
|
||||
id, ok := value.(string)
|
||||
if !ok {
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
switch {
|
||||
case strings.HasPrefix(id, "oc_"):
|
||||
groups = append(groups, id)
|
||||
case strings.HasPrefix(id, "ou_"):
|
||||
senders = append(senders, id)
|
||||
default:
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
}
|
||||
// A sender-only legacy allowlist has different group admission semantics.
|
||||
if len(senders) > 0 && (len(groups) == 0 || f["groupSenderAllowFrom"] != nil) {
|
||||
return feishuConfigError(field + ".groupAllowFrom")
|
||||
}
|
||||
f["groupAllowFrom"] = groups
|
||||
if len(senders) > 0 {
|
||||
f["groupSenderAllowFrom"] = senders
|
||||
}
|
||||
}
|
||||
for _, section := range []string{"accounts", "groups"} {
|
||||
for id, value := range childMap(f, section) {
|
||||
entry, ok := value.(map[string]interface{})
|
||||
name := field + "." + section + "." + id
|
||||
if !ok {
|
||||
return feishuConfigError(name)
|
||||
}
|
||||
// Account/group overrides must be resolved explicitly, not flattened.
|
||||
for _, key := range []string{"threadSession", "replyMode"} {
|
||||
if _, ok := entry[key]; ok {
|
||||
return feishuConfigError(name + "." + key)
|
||||
}
|
||||
}
|
||||
if mention, ok := entry["requireMention"]; ok {
|
||||
if _, ok := mention.(bool); !ok {
|
||||
return feishuConfigError(name + ".requireMention")
|
||||
}
|
||||
}
|
||||
if raw, exists := entry["streaming"]; exists {
|
||||
streaming, ok := raw.(bool)
|
||||
if !ok || streaming {
|
||||
return feishuConfigError(name + ".streaming")
|
||||
}
|
||||
entry["streaming"] = map[string]interface{}{"mode": "off"}
|
||||
}
|
||||
if err := convertFeishuAccess(entry, name); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractBundledFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig {
|
||||
result := extractFeishuConfig(conf)
|
||||
f := getChannelConfig(conf, "feishu")
|
||||
result.Enabled = extractBoolValue(f["enabled"], true) && extractBoolValue(childMap(childMap(childMap(conf, "plugins"), "entries"), "feishu")["enabled"], true)
|
||||
result.GroupSessionScope = extractStringValue(f["groupSessionScope"])
|
||||
if result.GroupSessionScope == "" {
|
||||
result.GroupSessionScope = "group"
|
||||
}
|
||||
result.Streaming = extractStringValue(childMap(f, "streaming")["mode"]) != "off"
|
||||
result.GroupPolicy = extractStringValue(f["groupPolicy"])
|
||||
if result.GroupPolicy == "" {
|
||||
result.GroupPolicy = "allowlist"
|
||||
}
|
||||
if len(f) == 0 {
|
||||
result.Streaming = false
|
||||
result.RequireMention = "true"
|
||||
} else if _, ok := f["requireMention"]; !ok {
|
||||
result.RequireMention = "open"
|
||||
}
|
||||
accounts := childMap(f, "accounts")
|
||||
ids := sortedChildKeys(accounts)
|
||||
if (f["appId"] != nil || f["appSecret"] != nil) && !slices.Contains(ids, "default") {
|
||||
ids = append([]string{"default"}, ids...)
|
||||
}
|
||||
result.Bots = []dto.AgentFeishuBot{}
|
||||
for _, id := range ids {
|
||||
effective := make(map[string]interface{}, len(f))
|
||||
for k, v := range f {
|
||||
effective[k] = v
|
||||
}
|
||||
for k, v := range childMap(accounts, id) {
|
||||
effective[k] = v
|
||||
}
|
||||
bot := defaultFeishuBot()
|
||||
bot.AccountID, bot.IsDefault = id, id == "default"
|
||||
bot.Name = extractDisplayName(childMap(accounts, id), "", id)
|
||||
bot.Enabled = extractBoolValue(childMap(accounts, id)["enabled"], true)
|
||||
bot.AppID, bot.AppSecret = extractStringValue(effective["appId"]), extractStringValue(effective["appSecret"])
|
||||
bot.DmPolicy = extractStringValue(effective["dmPolicy"])
|
||||
if bot.DmPolicy == "" {
|
||||
bot.DmPolicy = "pairing"
|
||||
}
|
||||
bot.AllowFrom = extractStringList(effective["allowFrom"])
|
||||
result.Bots = append(result.Bots, bot)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func setBundledFeishuConfig(conf map[string]interface{}, req dto.AgentFeishuConfigUpdateReq) error {
|
||||
if !slices.Contains([]string{"group", "group_sender", "group_topic", "group_topic_sender"}, req.GroupSessionScope) {
|
||||
return feishuConfigError("groupSessionScope")
|
||||
}
|
||||
f := ensureChildMap(ensureChildMap(conf, "channels"), "feishu")
|
||||
accounts := childMap(f, "accounts")
|
||||
seen := map[string]bool{}
|
||||
appIDs := map[string]bool{}
|
||||
for _, bot := range req.Bots {
|
||||
if bot.AccountID == "" || bot.AccountID != strings.ToLower(strings.TrimSpace(bot.AccountID)) || seen[bot.AccountID] || appIDs[bot.AppID] || bot.AppID == "" || !slices.Contains([]string{"pairing", "open", "allowlist", "disabled"}, bot.DmPolicy) || (bot.DmPolicy == "allowlist" && len(bot.AllowFrom) == 0) {
|
||||
return feishuConfigError("accounts")
|
||||
}
|
||||
seen[bot.AccountID] = true
|
||||
appIDs[bot.AppID] = true
|
||||
entry := childMap(accounts, bot.AccountID)
|
||||
if _, exists := entry["appSecret"]; !exists && bot.AppSecret == "" {
|
||||
entry["appSecret"] = f["appSecret"]
|
||||
}
|
||||
// SecretRef credentials cannot be represented by the form: keep them intact.
|
||||
if _, secretRef := entry["appSecret"].(map[string]interface{}); !secretRef && bot.AppSecret == "" {
|
||||
return feishuConfigError("accounts." + bot.AccountID + ".appSecret")
|
||||
}
|
||||
entry["appId"], entry["name"], entry["enabled"], entry["dmPolicy"] = bot.AppID, bot.Name, bot.Enabled, bot.DmPolicy
|
||||
if bot.AppSecret != "" {
|
||||
entry["appSecret"] = bot.AppSecret
|
||||
}
|
||||
switch bot.DmPolicy {
|
||||
case "open":
|
||||
entry["allowFrom"] = []string{"*"}
|
||||
case "allowlist":
|
||||
entry["allowFrom"] = bot.AllowFrom
|
||||
default:
|
||||
if len(bot.AllowFrom) > 0 {
|
||||
entry["allowFrom"] = bot.AllowFrom
|
||||
} else {
|
||||
entry["allowFrom"] = []string{}
|
||||
}
|
||||
}
|
||||
accounts[bot.AccountID] = entry
|
||||
}
|
||||
for id := range accounts {
|
||||
if !seen[id] {
|
||||
delete(accounts, id)
|
||||
}
|
||||
}
|
||||
if !seen["default"] {
|
||||
delete(f, "appId")
|
||||
delete(f, "appSecret")
|
||||
}
|
||||
if id := extractStringValue(f["defaultAccount"]); id != "" && !seen[id] {
|
||||
return feishuConfigError("defaultAccount")
|
||||
}
|
||||
if len(accounts) > 0 {
|
||||
f["accounts"] = accounts
|
||||
} else {
|
||||
delete(f, "accounts")
|
||||
}
|
||||
f["enabled"], f["groupSessionScope"], f["groupPolicy"] = req.Enabled, req.GroupSessionScope, req.GroupPolicy
|
||||
if req.RequireMention == "open" {
|
||||
delete(f, "requireMention")
|
||||
} else {
|
||||
f["requireMention"] = req.RequireMention == "true"
|
||||
}
|
||||
if req.GroupPolicy == "allowlist" {
|
||||
f["groupAllowFrom"] = req.GroupAllowFrom
|
||||
} else {
|
||||
delete(f, "groupAllowFrom")
|
||||
}
|
||||
streaming := ensureChildMap(f, "streaming")
|
||||
streaming["mode"] = "off"
|
||||
if req.Streaming {
|
||||
streaming["mode"] = "partial"
|
||||
}
|
||||
selectBundledFeishu(conf, req.Enabled && hasEnabledBots(req.Bots))
|
||||
return nil
|
||||
}
|
||||
|
||||
// Write beside the original so validation and rename preserve the mounted filesystem and owner.
|
||||
func stageFeishuConfig(configPath string, payload []byte) (string, error) {
|
||||
info, err := os.Lstat(configPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return "", feishuConfigError("openclaw.json")
|
||||
}
|
||||
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
name := f.Name()
|
||||
defer func() {
|
||||
f.Close()
|
||||
if err != nil {
|
||||
os.Remove(name)
|
||||
}
|
||||
}()
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
err = f.Chown(int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
if err == nil {
|
||||
err = f.Chmod(info.Mode().Perm())
|
||||
}
|
||||
if err == nil {
|
||||
_, err = f.Write(payload)
|
||||
}
|
||||
if err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
if err == nil {
|
||||
err = f.Close()
|
||||
}
|
||||
return name, err
|
||||
}
|
||||
|
||||
func validateFeishuConfig(container, configPath string) error {
|
||||
out, err := cmd.RunDockerExecWithStdout(time.Minute, container, "env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath), "openclaw", "config", "validate", "--json")
|
||||
return feishuValidationResult(out, err)
|
||||
}
|
||||
|
||||
func replaceFeishuConfig(configPath string, expected, payload []byte) error {
|
||||
tmp, err := stageFeishuConfig(configPath, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(tmp)
|
||||
current, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(current, expected) {
|
||||
return buserr.New("ErrFeishuConfigChanged")
|
||||
}
|
||||
return os.Rename(tmp, configPath)
|
||||
}
|
||||
|
||||
func (a AgentService) SwitchFeishuPlugin(req dto.AgentFeishuSwitchReq) error {
|
||||
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
started := false
|
||||
defer func() {
|
||||
if !started {
|
||||
feishuSwitches.Delete(agent.ConfigPath)
|
||||
}
|
||||
}()
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasBundledFeishu(install.ContainerName) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !legacy {
|
||||
return buserr.New("ErrFeishuAlreadySwitched")
|
||||
}
|
||||
if err := prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
|
||||
return err
|
||||
}
|
||||
switchTask, err := task.NewTaskWithOps("Feishu", task.TaskUpdate, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
switchTask.AddSubTaskWithAlias("AgentFeishuSwitch", func(t *task.Task) error { return a.runFeishuSwitch(agent, install, req, t) }, nil)
|
||||
started = true
|
||||
go func() { defer feishuSwitches.Delete(agent.ConfigPath); executeAgentPluginTask(switchTask) }()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) runFeishuSwitch(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuSwitchReq, t *task.Task) (result error) {
|
||||
original, conf, err := readFeishuConfigBytes(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
legacy, configured, err := classifyLegacyFeishu(conf, install.GetPath())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !legacy {
|
||||
return buserr.New("ErrFeishuAlreadySwitched")
|
||||
}
|
||||
image, err := feishuContainerImage(install.ContainerName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
image, err = bundledFeishuImage(image)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = prepareLegacyFeishu(conf, configured, &dto.AgentFeishuTransition{Confirmed: true, Streaming: req.Streaming, GroupSessionScope: req.GroupSessionScope, RequireMention: req.RequireMention}); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
recovery, err := feishuRecoveryPayload(payload, configured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Validate both candidates without invoking Doctor or mutating retained state.
|
||||
for _, candidate := range [][]byte{payload, recovery} {
|
||||
staged, stageErr := stageFeishuConfig(agent.ConfigPath, candidate)
|
||||
if stageErr != nil {
|
||||
return stageErr
|
||||
}
|
||||
validateErr := validateFeishuImageConfig(image, staged)
|
||||
os.Remove(staged)
|
||||
if validateErr != nil {
|
||||
return validateErr
|
||||
}
|
||||
}
|
||||
if _, err = compose.Stop(install.GetComposePath()); err != nil {
|
||||
return err
|
||||
}
|
||||
replaced := false
|
||||
defer func() {
|
||||
if !replaced {
|
||||
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
|
||||
}
|
||||
return
|
||||
}
|
||||
if result == nil {
|
||||
return
|
||||
}
|
||||
if _, stopErr := compose.Stop(install.GetComposePath()); stopErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", stopErr))
|
||||
return
|
||||
}
|
||||
if restoreErr := replaceFeishuConfig(agent.ConfigPath, payload, recovery); restoreErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", restoreErr))
|
||||
return
|
||||
}
|
||||
if _, startErr := compose.Operate(install.GetComposePath(), "start"); startErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", startErr))
|
||||
return
|
||||
}
|
||||
if healthErr := verifyFeishuActivation(install.ContainerName, false); healthErr != nil {
|
||||
result = errors.Join(result, buserr.WithErr("ErrFeishuRestore", healthErr))
|
||||
return
|
||||
}
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
|
||||
}()
|
||||
current, err := os.ReadFile(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !bytes.Equal(current, original) {
|
||||
return buserr.New("ErrFeishuConfigChanged")
|
||||
}
|
||||
backup, err := backupFeishuConfig(agent.ConfigPath, original)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.Log(i18n.GetWithName("AgentFeishuBackup", filepath.Base(backup)))
|
||||
if err = replaceFeishuConfig(agent.ConfigPath, original, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
replaced = true
|
||||
if _, err = compose.Operate(install.GetComposePath(), "start"); err != nil {
|
||||
return err
|
||||
}
|
||||
enabled := configured && extractBoolValue(getChannelConfig(conf, "feishu")["enabled"], true)
|
||||
if err = verifyFeishuActivation(install.ContainerName, enabled); err != nil {
|
||||
return err
|
||||
}
|
||||
if enabled {
|
||||
probeFeishuConnection(install.ContainerName, t)
|
||||
}
|
||||
if configured {
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuVerifyMessages"))
|
||||
} else {
|
||||
t.Log(i18n.GetMsgByKey("AgentFeishuRecoveryDisabled"))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func saveBundledFeishuConfig(agent *model.Agent, install *model.AppInstall, req dto.AgentFeishuConfigUpdateReq) error {
|
||||
if !isOpenclawBundledFeishuVersion(install.Version) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
if _, busy := feishuSwitches.LoadOrStore(agent.ConfigPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer feishuSwitches.Delete(agent.ConfigPath)
|
||||
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||
return err
|
||||
}
|
||||
if !hasBundledFeishu(install.ContainerName) {
|
||||
return buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
original, err := os.ReadFile(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var conf map[string]interface{}
|
||||
if err = json.Unmarshal(original, &conf); err != nil {
|
||||
return err
|
||||
}
|
||||
if hasLegacyFeishu(conf, install.GetPath()) {
|
||||
return buserr.New("ErrFeishuSwitchRequired")
|
||||
}
|
||||
if err = setBundledFeishuConfig(conf, req); err != nil {
|
||||
return err
|
||||
}
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
staged, err := stageFeishuConfig(agent.ConfigPath, payload)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(staged)
|
||||
if err = validateFeishuConfig(install.ContainerName, staged); err != nil {
|
||||
return err
|
||||
}
|
||||
return replaceFeishuConfig(agent.ConfigPath, original, payload)
|
||||
}
|
||||
|
||||
func feishuProbeOK(output string) bool {
|
||||
raw, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var status struct {
|
||||
ChannelAccounts map[string][]struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Configured bool `json:"configured"`
|
||||
Running bool `json:"running"`
|
||||
Probe struct {
|
||||
OK bool `json:"ok"`
|
||||
} `json:"probe"`
|
||||
} `json:"channelAccounts"`
|
||||
}
|
||||
if json.Unmarshal(raw, &status) != nil {
|
||||
return false
|
||||
}
|
||||
accounts, ok := status.ChannelAccounts["feishu"]
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if account.Enabled && (!account.Configured || !account.Running || !account.Probe.OK) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func feishuGatewayReady(output string, enabled bool) bool {
|
||||
raw, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
var health struct {
|
||||
OK bool `json:"ok"`
|
||||
Plugins struct {
|
||||
Loaded []string `json:"loaded"`
|
||||
} `json:"plugins"`
|
||||
}
|
||||
return json.Unmarshal(raw, &health) == nil && health.OK && !slices.Contains(health.Plugins.Loaded, "openclaw-lark") && (!enabled || slices.Contains(health.Plugins.Loaded, "feishu"))
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
)
|
||||
|
||||
// Inspect a stopped disposable container; its entrypoint never touches retained state.
|
||||
func bundledFeishuImage(image string) (string, error) {
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
info, _, err := cli.ImageInspectWithRaw(ctx, image)
|
||||
if err != nil {
|
||||
return "", buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
created, err := cli.ContainerCreate(ctx, &container.Config{Image: info.ID, Entrypoint: []string{"/bin/true"}}, &container.HostConfig{NetworkMode: "none"}, nil, nil, "")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer func() {
|
||||
cleanup, done := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer done()
|
||||
_ = cli.ContainerRemove(cleanup, created.ID, container.RemoveOptions{Force: true, RemoveVolumes: true})
|
||||
}()
|
||||
if _, err = cli.ContainerStatPath(ctx, created.ID, "/app/.1panel-feishu-bundled-v1"); err != nil {
|
||||
return "", buserr.New("ErrFeishuImageRequired")
|
||||
}
|
||||
return info.ID, nil
|
||||
}
|
||||
|
||||
func feishuContainerImage(name string) (string, error) {
|
||||
cli, err := docker.NewDockerClient()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cli.Close()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
|
||||
defer cancel()
|
||||
info, err := cli.ContainerInspect(ctx, name)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return info.Image, nil
|
||||
}
|
||||
|
||||
// The original state is read-only and the normal Doctor entrypoint is bypassed.
|
||||
func validateFeishuImageConfig(image, configPath string) error {
|
||||
info, err := os.Stat(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
uid, gid := uint32(1000), uint32(1000)
|
||||
if stat, ok := info.Sys().(*syscall.Stat_t); ok {
|
||||
uid, gid = stat.Uid, stat.Gid
|
||||
}
|
||||
user := fmt.Sprintf("%d:%d", uid, gid)
|
||||
tmpfs := fmt.Sprintf("/tmp:rw,mode=0700,uid=%d,gid=%d", uid, gid)
|
||||
name := "1panel-feishu-" + strings.TrimSuffix(filepath.Base(configPath), ".json")
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Minute))
|
||||
defer func() { _ = cmd.NewCommandMgr(cmd.WithTimeout(15*time.Second)).Run("docker", "rm", "-f", "-v", name) }()
|
||||
out, runErr := mgr.RunWithStdout("docker", "run", "--name", name, "--rm", "--network", "none", "--read-only", "--user", user,
|
||||
"--tmpfs", tmpfs,
|
||||
"--mount", "type=bind,src="+filepath.Dir(configPath)+",dst=/home/node/.openclaw,readonly",
|
||||
"--env", "HOME=/home/node", "--env", "TMPDIR=/tmp", "--env", "OPENCLAW_STATE_DIR=/tmp/state",
|
||||
"--env", "OPENCLAW_CONFIG_PATH=/home/node/.openclaw/"+filepath.Base(configPath),
|
||||
"--entrypoint", "sh", image, "-c", "mkdir -m 700 /tmp/state && ln -s /home/node/.openclaw/extensions /tmp/state/extensions && exec node /app/openclaw.mjs config validate --json")
|
||||
return feishuValidationResult(out, runErr)
|
||||
}
|
||||
|
||||
func feishuValidationResult(out string, runErr error) error {
|
||||
raw, _ := extractEmbeddedJSON(out)
|
||||
var result struct {
|
||||
Valid bool `json:"valid"`
|
||||
Issues []struct {
|
||||
Path string `json:"path"`
|
||||
} `json:"issues"`
|
||||
}
|
||||
if json.Unmarshal(raw, &result) == nil && result.Valid && runErr == nil {
|
||||
return nil
|
||||
}
|
||||
fields := []string{}
|
||||
for _, issue := range result.Issues {
|
||||
if issue.Path != "" {
|
||||
fields = append(fields, issue.Path)
|
||||
}
|
||||
}
|
||||
if len(fields) == 0 {
|
||||
fields = append(fields, "openclaw.json")
|
||||
}
|
||||
return feishuConfigError(strings.Join(fields, ", "))
|
||||
}
|
||||
|
||||
func backupFeishuConfig(configPath string, original []byte) (string, error) {
|
||||
f, err := os.CreateTemp(filepath.Dir(configPath), ".feishu-backup-*.json")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if _, err = f.Write(original); err == nil {
|
||||
err = f.Sync()
|
||||
}
|
||||
closeErr := f.Close()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if closeErr != nil {
|
||||
return "", closeErr
|
||||
}
|
||||
return f.Name(), nil
|
||||
}
|
||||
|
||||
// Doctor can spend 94 seconds waiting for the previous container's owner lease.
|
||||
func waitOpenclawReady(name string) error {
|
||||
script := `const fs=require('fs');const c=JSON.parse(fs.readFileSync('/home/node/.openclaw/openclaw.json','utf8'));const tls=c.gateway?.tls?.enabled===true;const h=require(tls?'https':'http');function check(p){const r=h.get({hostname:'127.0.0.1',port:c.gateway?.port||18789,path:p,rejectUnauthorized:false,timeout:3000},s=>{s.resume();if(s.statusCode===404&&p==='/readyz')return check('/healthz');process.exitCode=s.statusCode===200?0:1});r.on('timeout',()=>r.destroy());r.on('error',()=>process.exitCode=1)}check('/readyz')`
|
||||
deadline := time.Now().Add(4 * time.Minute)
|
||||
for time.Now().Before(deadline) {
|
||||
if cmd.RunDockerExec(8*time.Second, name, "node", "-e", script) == nil {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
|
||||
func verifyFeishuActivation(name string, enabled bool) error {
|
||||
if err := waitOpenclawReady(name); err != nil {
|
||||
return err
|
||||
}
|
||||
output, err := cmd.RunDockerExecWithStdout(time.Minute, name, "openclaw", "plugins", "list", "--json")
|
||||
if err != nil {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
raw, _ := extractEmbeddedJSON(output)
|
||||
var result struct {
|
||||
Plugins []struct {
|
||||
ID string `json:"id"`
|
||||
Status string `json:"status"`
|
||||
Origin string `json:"origin"`
|
||||
} `json:"plugins"`
|
||||
}
|
||||
if json.Unmarshal(raw, &result) != nil {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
found := false
|
||||
for _, p := range result.Plugins {
|
||||
if p.ID == "openclaw-lark" && p.Status != "disabled" {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
if p.ID == "feishu" && p.Origin == "bundled" && (p.Status == "loaded" || (!enabled && p.Status == "disabled")) {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
health, err := cmd.RunDockerExecWithStdout(20*time.Second, name, "openclaw", "gateway", "health", "--json")
|
||||
if err != nil || !feishuGatewayReady(health, enabled) {
|
||||
return buserr.New("ErrFeishuActivation")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readFeishuConfigBytes(file string) ([]byte, map[string]interface{}, error) {
|
||||
info, err := os.Lstat(file)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return nil, nil, feishuConfigError("openclaw.json")
|
||||
}
|
||||
f, err := os.Open(file)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
raw, err := io.ReadAll(f)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var conf map[string]interface{}
|
||||
if err = json.Unmarshal(raw, &conf); err != nil || conf == nil {
|
||||
return nil, nil, feishuConfigError("openclaw.json")
|
||||
}
|
||||
return raw, conf, nil
|
||||
}
|
||||
@@ -19,13 +19,6 @@ import (
|
||||
|
||||
const hermesWorkspaceDir = "/opt/data/workspace"
|
||||
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
||||
const hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
||||
const hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
||||
|
||||
type hermesDashboardAuth struct {
|
||||
Username string
|
||||
Password string
|
||||
}
|
||||
|
||||
type hermesConfig struct {
|
||||
Model hermesModelConfig `yaml:"model"`
|
||||
@@ -117,52 +110,6 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
|
||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
||||
}
|
||||
|
||||
func normalizeHermesDashboardAuth(username, password string) hermesDashboardAuth {
|
||||
auth := hermesDashboardAuth{
|
||||
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 writeHermesDashboardAuthEnv(envPath string, auth hermesDashboardAuth, overwrite bool) error {
|
||||
return upsertAgentEnv(envPath, map[string]string{
|
||||
hermesDashboardUsernameEnvKey: auth.Username,
|
||||
hermesDashboardPasswordEnvKey: auth.Password,
|
||||
}, []string{
|
||||
hermesDashboardUsernameEnvKey,
|
||||
hermesDashboardPasswordEnvKey,
|
||||
}, overwrite)
|
||||
}
|
||||
|
||||
func readHermesDashboardAuthEnv(envPath string) (hermesDashboardAuth, error) {
|
||||
envMap, err := readAgentEnvMap(envPath)
|
||||
if err != nil {
|
||||
return hermesDashboardAuth{}, err
|
||||
}
|
||||
return hermesDashboardAuth{
|
||||
Username: strings.TrimSpace(envMap[hermesDashboardUsernameEnvKey]),
|
||||
Password: strings.TrimSpace(envMap[hermesDashboardPasswordEnvKey]),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func readHermesDashboardAuthFromInstall(appInstall *model.AppInstall) hermesDashboardAuth {
|
||||
if appInstall == nil || appInstall.ID == 0 {
|
||||
return hermesDashboardAuth{}
|
||||
}
|
||||
auth, err := readHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"))
|
||||
if err != nil {
|
||||
return hermesDashboardAuth{}
|
||||
}
|
||||
return auth
|
||||
}
|
||||
|
||||
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||
content, err := files.NewFileOp().GetContent(configPath)
|
||||
if err != nil {
|
||||
|
||||
@@ -8,11 +8,15 @@ import (
|
||||
)
|
||||
|
||||
const hermesWeixinLoginScript = `import asyncio
|
||||
from gateway.platforms.weixin import qr_login
|
||||
try:
|
||||
from plugins.platforms.weixin.adapter import qr_login
|
||||
except ModuleNotFoundError as exc:
|
||||
if exc.name not in ('plugins', 'plugins.platforms', 'plugins.platforms.weixin', 'plugins.platforms.weixin.adapter'):
|
||||
raise
|
||||
from gateway.platforms.weixin import qr_login
|
||||
from hermes_cli.config import save_env_value
|
||||
|
||||
creds = asyncio.run(qr_login('/opt/data'))
|
||||
print('RESULT =', creds)
|
||||
|
||||
if not creds:
|
||||
raise SystemExit(1)
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func SyncLocalModelAccount(install model.AppInstall, accountID uint, baseURL string, models []dto.AgentAccountModel, updateBaseURL, createIfMissing bool) (*model.AgentAccount, error) {
|
||||
if install.ID == 0 || (install.App.Key != "ollama" && install.App.Key != "vllm") {
|
||||
return nil, fmt.Errorf("invalid local model installation")
|
||||
}
|
||||
agentAccountMutationMu.Lock()
|
||||
defer agentAccountMutationMu.Unlock()
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) && accountID != 0 {
|
||||
account, err = agentAccountRepo.GetFirst(repo.WithByID(accountID), repo.WithByProvider(install.App.Key))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil && account.AppInstallID != 0 && account.AppInstallID != install.ID {
|
||||
return nil, fmt.Errorf("model account belongs to another installation")
|
||||
}
|
||||
}
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
if !createIfMissing {
|
||||
return nil, nil
|
||||
}
|
||||
name := install.Name
|
||||
for suffix := 1; ; suffix++ {
|
||||
existing, err := agentAccountRepo.List(repo.WithByProvider(install.App.Key), repo.WithByName(name))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(existing) == 0 {
|
||||
break
|
||||
}
|
||||
name = fmt.Sprintf("%s-%d", install.Name, suffix)
|
||||
}
|
||||
apiType, apiKey := "openai-completions", "vllm-local"
|
||||
if install.App.Key == "ollama" {
|
||||
apiType, apiKey = "openai-responses", "ollama-local"
|
||||
}
|
||||
account = &model.AgentAccount{Name: name, Provider: install.App.Key, BaseURL: baseURL, APIType: apiType, APIKey: apiKey, RememberAPIKey: true, Verified: true}
|
||||
}
|
||||
accountChanged := account.ID == 0 || account.AppInstallID != install.ID || (updateBaseURL && account.BaseURL != baseURL)
|
||||
account.AppInstallID = install.ID
|
||||
if updateBaseURL {
|
||||
account.BaseURL = baseURL
|
||||
}
|
||||
rows := make([]model.AgentAccountModel, 0, len(models))
|
||||
verifyModelExists := account.VerifyModel == ""
|
||||
for _, item := range models {
|
||||
rows = append(rows, model.AgentAccountModel{Model: item.ID, Name: item.Name})
|
||||
if item.ID == account.VerifyModel {
|
||||
verifyModelExists = true
|
||||
}
|
||||
}
|
||||
if !verifyModelExists {
|
||||
account.VerifyModel = ""
|
||||
accountChanged = true
|
||||
}
|
||||
var previous []model.AgentAccountModel
|
||||
if account.ID != 0 {
|
||||
previous, err = agentAccountModelRepo.List(repo.WithByAccountID(account.ID), repo.WithOrderAsc("sort_order"), repo.WithOrderAsc("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
modelsChanged := len(previous) != len(rows)
|
||||
if !modelsChanged {
|
||||
for i := range rows {
|
||||
if previous[i].Model != rows[i].Model || previous[i].Name != rows[i].Name {
|
||||
modelsChanged = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if modelsChanged {
|
||||
err = agentAccountRepo.SaveLocal(account, rows)
|
||||
} else if accountChanged {
|
||||
err = agentAccountRepo.Save(account)
|
||||
} else {
|
||||
return account, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return account, nil
|
||||
}
|
||||
|
||||
func SyncOllamaModelAccount(createIfMissing bool) error {
|
||||
app, err := appRepo.GetFirst(appRepo.WithKey("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
install, err := appInstallRepo.GetFirst(appInstallRepo.WithAppId(app.ID))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !install.SyncModelAccount {
|
||||
return nil
|
||||
}
|
||||
return syncOllamaModelAccount(install, fmt.Sprintf("http://%s:11434/v1", install.ContainerName), false, createIfMissing)
|
||||
}
|
||||
|
||||
func syncOllamaModelAccount(install model.AppInstall, baseURL string, updateBaseURL, createIfMissing bool) error {
|
||||
models, err := aiRepo.List(repo.WithByStatus(constant.StatusSuccess), repo.WithOrderAsc("name"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
items := make([]dto.AgentAccountModel, 0, len(models))
|
||||
for _, item := range models {
|
||||
items = append(items, dto.AgentAccountModel{ID: item.Name, Name: item.Name})
|
||||
}
|
||||
_, err = SyncLocalModelAccount(install, 0, baseURL, items, updateBaseURL, createIfMissing)
|
||||
return err
|
||||
}
|
||||
|
||||
func loadOllamaModelAccountAddress(install model.AppInstall) (string, string, error) {
|
||||
addressType := install.ModelAccountBaseURLType
|
||||
if addressType == "" {
|
||||
addressType = "container"
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByAppInstallID(install.ID), repo.WithByProvider("ollama"))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return addressType, install.ModelAccountBaseURL, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if addressType != "custom" {
|
||||
expectedURL, err := ResolveLocalModelAccountBaseURL(addressType, "", install.ContainerName, "11434", install.HttpPort)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if strings.TrimRight(account.BaseURL, "/") != expectedURL {
|
||||
addressType = "custom"
|
||||
}
|
||||
}
|
||||
return addressType, account.BaseURL, nil
|
||||
}
|
||||
|
||||
func CleanupLocalModelAccount(installID uint) {
|
||||
if installID == 0 {
|
||||
return
|
||||
}
|
||||
accounts, err := agentAccountRepo.List(repo.WithByAppInstallID(installID))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load local model accounts: %v", err)
|
||||
return
|
||||
}
|
||||
for _, account := range accounts {
|
||||
if err := (AgentService{}).DeleteAccount(dto.AgentAccountDeleteReq{ID: account.ID}); err != nil {
|
||||
global.LOG.Warnf("keep local model account %d after uninstall: %v", account.ID, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func ResolveLocalModelAccountBaseURL(addressType, customURL, containerName, containerPort string, hostPort int) (string, error) {
|
||||
switch addressType {
|
||||
case "custom":
|
||||
baseURL := strings.TrimRight(strings.TrimSpace(customURL), "/")
|
||||
if !strings.HasPrefix(baseURL, "http://") && !strings.HasPrefix(baseURL, "https://") {
|
||||
return "", fmt.Errorf("model account base URL must start with http:// or https://")
|
||||
}
|
||||
return baseURL, nil
|
||||
case "localhost":
|
||||
return fmt.Sprintf("http://127.0.0.1:%d/v1", hostPort), nil
|
||||
case "systemIP":
|
||||
systemIP, err := settingRepo.GetValueByKey("SystemIP")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
systemIP = strings.TrimSpace(systemIP)
|
||||
if systemIP == "" {
|
||||
systemIP = "127.0.0.1"
|
||||
}
|
||||
return fmt.Sprintf("http://%s:%d/v1", systemIP, hostPort), nil
|
||||
default:
|
||||
return fmt.Sprintf("http://%s:%s/v1", containerName, containerPort), nil
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
|
||||
)
|
||||
|
||||
func TestMiniMaxOpenclawModelMetadata(t *testing.T) {
|
||||
for _, provider := range []string{"minimax", "custom", "bailian-coding-plan"} {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "MiniMax-M2.5", "saved-custom"} {
|
||||
t.Run(provider+"/"+protocol+"/"+id, func(t *testing.T) {
|
||||
account := &model.AgentAccount{Provider: provider, APIType: protocol, BaseURL: "https://example.test/v1", APIKey: "test-key"}
|
||||
_, entry, _, _, err := buildOpenclawAccountModelConfig(account, dto.AgentAccountModel{ID: id, Name: "Saved name"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
payload, err := json.Marshal(entry)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var actual modelEntry
|
||||
if err := json.Unmarshal(payload, &actual); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if actual.ID != id || actual.Name != "Saved name" {
|
||||
t.Fatalf("changed saved identity: %s", payload)
|
||||
}
|
||||
if provider == "minimax" && id == "MiniMax-M3.1-Flash" {
|
||||
if actual.ContextWindow != 1000000 || actual.MaxTokens != 524288 || !reflect.DeepEqual(actual.Input, []string{"text", "image"}) {
|
||||
t.Fatalf("incorrect direct Flash metadata: %s", payload)
|
||||
}
|
||||
} else if actual.ContextWindow != 0 || actual.MaxTokens != 0 {
|
||||
t.Fatalf("changed existing/gateway limits: %s", payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxPreservesSavedOpenclawMetadata(t *testing.T) {
|
||||
current := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text"}, ContextWindow: 32000, MaxTokens: 4096}}},
|
||||
}}
|
||||
conf := map[string]interface{}{}
|
||||
if err := applyOpenclawModelsConfig(conf, current); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
next := &modelsConfig{Mode: "merge", Providers: map[string]modelProvider{
|
||||
"minimax": {Models: []modelEntry{{ID: "MiniMax-M3.1-Flash", Input: []string{"text", "image"}, ContextWindow: 1000000, MaxTokens: 524288}}},
|
||||
}}
|
||||
preserveOpenclawModelMetadata(conf, next)
|
||||
if !reflect.DeepEqual(next.Providers["minimax"].Models, current.Providers["minimax"].Models) {
|
||||
t.Fatalf("saved metadata changed: %+v", next)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxDefaultAccountRequest(t *testing.T) {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
account := &model.AgentAccount{Provider: "minimax", APIType: protocol}
|
||||
models, err := buildInitialAgentAccountModels(account, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
selected, err := resolveAgentAccountVerifyModel("minimax", "", models)
|
||||
if err != nil || selected != "MiniMax-M3.1-Flash" || len(models) != 2 || models[1].ID != "MiniMax-M3" {
|
||||
t.Fatalf("unexpected default: %s, %+v, %v", selected, models, err)
|
||||
}
|
||||
request := providercatalog.BuildVerifyRequest("minimax", protocol, "", "https://example.test", "test-key", selected)
|
||||
var body map[string]interface{}
|
||||
if err := json.Unmarshal(request.Body, &body); err != nil || body["model"] != selected {
|
||||
t.Fatalf("default request: %s, %v", request.Body, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxHermesConfiguration(t *testing.T) {
|
||||
for _, protocol := range []string{"anthropic-messages", "openai-completions"} {
|
||||
for _, id := range []string{"MiniMax-M3.1-Flash", "MiniMax-M3", "MiniMax-M2.7", "my-custom-model"} {
|
||||
dir := t.TempDir()
|
||||
account := &model.AgentAccount{Provider: "minimax", APIType: protocol, BaseURL: "https://api.minimaxi.com/anthropic", APIKey: "test-key"}
|
||||
if err := writeHermesConfig(dir, account, id, ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conf, err := readHermesConfig(filepath.Join(dir, "config.yaml"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if conf.Model.Default != id || conf.Model.Provider != "minimax-cn" || conf.Model.BaseURL != account.BaseURL {
|
||||
t.Fatalf("changed Hermes selection: %+v", conf.Model)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestMiniMaxExplicitAccountModels(t *testing.T) {
|
||||
for _, provider := range []string{"minimax", "bailian-coding-plan", "custom"} {
|
||||
account := &model.AgentAccount{Provider: provider, APIType: "openai-completions"}
|
||||
requested := []dto.AgentAccountModel{{ID: "MiniMax-M2.5", Name: "Saved legacy"}, {ID: "my-custom-model", Name: "Saved custom"}}
|
||||
actual, err := buildInitialAgentAccountModels(account, requested)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(actual, requested) {
|
||||
t.Fatalf("changed explicit %s models: %+v", provider, actual)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"net"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
)
|
||||
|
||||
var deepSeekUsernamePattern = regexp.MustCompile(`^[A-Za-z0-9._-]+$`)
|
||||
var deepSeekHostLabelPattern = regexp.MustCompile(`^[A-Za-z0-9](?:[A-Za-z0-9-]{0,61}[A-Za-z0-9])?$`)
|
||||
|
||||
func isDeepSeekAccessHost(value string) bool {
|
||||
if ip := net.ParseIP(value); ip != nil {
|
||||
return ip.To4() != nil && !strings.Contains(value, ":")
|
||||
}
|
||||
if value == "" || len(value) > 253 || strings.Trim(value, "0123456789.") == "" {
|
||||
return false
|
||||
}
|
||||
for _, label := range strings.Split(value, ".") {
|
||||
if !deepSeekHostLabelPattern.MatchString(label) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateNativeAgentCreate(req dto.AgentCreateReq) error {
|
||||
if req.AgentType != constant.AppDeepSeekHarness && req.AgentType != constant.AppOctop {
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(req.DashboardUsername) == "" || strings.ContainsAny(req.DashboardUsername, "\r\n\x00") {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||
}
|
||||
if strings.ContainsAny(req.DashboardPassword, "\r\n\x00") || req.DashboardPassword != strings.TrimSpace(req.DashboardPassword) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
if req.AgentType == constant.AppDeepSeekHarness {
|
||||
if !isDeepSeekAccessHost(req.AccessHost) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "accessHost", nil)
|
||||
}
|
||||
if !deepSeekUsernamePattern.MatchString(req.DashboardUsername) {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardUsername", nil)
|
||||
}
|
||||
if utf8.RuneCountInString(req.DashboardPassword) < 12 {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
hasLetter, hasDigit := false, false
|
||||
for _, char := range req.DashboardPassword {
|
||||
hasLetter = hasLetter || unicode.IsLetter(char)
|
||||
hasDigit = hasDigit || unicode.IsDigit(char)
|
||||
}
|
||||
if utf8.RuneCountInString(req.DashboardPassword) < 8 || !hasLetter || !hasDigit {
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
switch strings.ToLower(req.DashboardPassword) {
|
||||
case "password", "password1", "password12", "password123", "12345678", "123456789", "qwerty123",
|
||||
"admin123", "welcome1", "letmein1", "changeme1", "octop123", "abc12345", "iloveyou1":
|
||||
return buserr.WithDetail("ErrInvalidParams", "dashboardPassword", nil)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,321 @@
|
||||
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 isOpenclawBundledFeishuVersion(install.Version) && (strings.TrimSpace(req.Package) == "@larksuite/openclaw-lark" || strings.TrimSpace(req.Package) == "@openclaw/feishu") {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
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 isOpenclawBundledFeishuVersion(install.Version) && (req.PluginID == "openclaw-lark" || req.PluginID == "feishu") {
|
||||
return buserr.New("ErrFeishuManaged")
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"net/url"
|
||||
"path"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -21,6 +22,7 @@ import (
|
||||
"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"
|
||||
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
@@ -96,7 +98,7 @@ func ensureContainerRunning(containerName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string) (resolvedAgentAccountInput, error) {
|
||||
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability, allowEmptyModel bool) (resolvedAgentAccountInput, error) {
|
||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||
resolvedAPIType := strings.TrimSpace(apiType)
|
||||
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||
@@ -111,10 +113,11 @@ func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, mode
|
||||
return resolvedAgentAccountInput{}, err
|
||||
}
|
||||
modelID = strings.TrimSpace(modelID)
|
||||
if modelID == "" {
|
||||
if modelID == "" && !allowEmptyModel {
|
||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
if !providercatalog.SkipVerification(provider) {
|
||||
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
|
||||
}
|
||||
@@ -145,6 +148,10 @@ func readOpenclawConfig(configPath string) (map[string]interface{}, error) {
|
||||
}
|
||||
|
||||
func writeOpenclawConfigRaw(configPath string, conf map[string]interface{}) error {
|
||||
if _, busy := feishuSwitches.LoadOrStore(configPath, true); busy {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
defer feishuSwitches.Delete(configPath)
|
||||
ensureGatewaySecurityDefaults(conf)
|
||||
ensureOpenclawUpdateDefaults(conf)
|
||||
payload, err := json.MarshalIndent(conf, "", " ")
|
||||
@@ -386,7 +393,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
Remark: agent.Remark,
|
||||
AgentType: agentType,
|
||||
Provider: agent.Provider,
|
||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
||||
ProviderName: localizedAgentProviderName(agent.Provider),
|
||||
Model: agent.Model,
|
||||
APIType: agent.APIType,
|
||||
BaseURL: agent.BaseURL,
|
||||
@@ -412,6 +419,15 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
} else {
|
||||
item.WebUIPort = appInstall.HttpPort
|
||||
}
|
||||
if agentType == constant.AppDeepSeekHarness {
|
||||
item.WebUIPort = appInstall.HttpsPort
|
||||
if values, err := readAgentEnvMap(appInstall.GetEnvPath()); err == nil {
|
||||
accessHost := values["HTTPS_ACCESS_HOST"]
|
||||
if isDeepSeekAccessHost(accessHost) {
|
||||
item.AccessHost = accessHost
|
||||
}
|
||||
}
|
||||
}
|
||||
item.Path = appInstall.GetPath()
|
||||
item.Status = appInstall.Status
|
||||
item.Message = appInstall.Message
|
||||
@@ -420,8 +436,8 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
item.BridgePort = toInt(bridge)
|
||||
}
|
||||
}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
auth := readHermesDashboardAuthFromInstall(appInstall)
|
||||
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
|
||||
item.DashboardUsername = auth.Username
|
||||
item.DashboardPassword = auth.Password
|
||||
}
|
||||
@@ -429,8 +445,18 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
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 {
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent ||
|
||||
appKey == constant.AppDeepSeekHarness || appKey == constant.AppOctop
|
||||
}
|
||||
|
||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||
@@ -640,7 +666,7 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
||||
if len(ignores) > 0 {
|
||||
continue
|
||||
}
|
||||
if common.IsCrossVersion(install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||
if isCrossAppVersion(install.App.Key, install.Version, item.Version) && !install.App.CrossVersionUpdate {
|
||||
continue
|
||||
}
|
||||
versions = append(versions, item.Version)
|
||||
@@ -648,12 +674,12 @@ func checkAgentUpgradable(install model.AppInstall) bool {
|
||||
if len(versions) == 0 {
|
||||
return false
|
||||
}
|
||||
versions = common.GetSortedVersions(versions)
|
||||
versions = sortAppVersions(install.App.Key, versions)
|
||||
lastVersion := versions[0]
|
||||
if common.IsCrossVersion(install.Version, lastVersion) {
|
||||
if isCrossAppVersion(install.App.Key, install.Version, lastVersion) {
|
||||
return install.App.CrossVersionUpdate
|
||||
}
|
||||
return common.CompareVersion(lastVersion, install.Version)
|
||||
return isNewerAppVersion(install.App.Key, lastVersion, install.Version)
|
||||
}
|
||||
|
||||
type openclawConfig struct {
|
||||
@@ -729,6 +755,8 @@ type modelEntry struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Input []string `json:"input,omitempty"`
|
||||
ContextWindow int `json:"contextWindow,omitempty"`
|
||||
MaxTokens int `json:"maxTokens,omitempty"`
|
||||
}
|
||||
|
||||
func requiresOpenclawProviderModels(provider string) bool {
|
||||
@@ -756,7 +784,7 @@ type browserConfig struct {
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string, metadata []dto.AgentModelMetadata) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -841,6 +869,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
conf = initial
|
||||
} else {
|
||||
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -895,6 +924,9 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
if allowedOrigins != nil {
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
}
|
||||
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -909,6 +941,144 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
|
||||
}
|
||||
|
||||
func readOpenclawModelsConfig(conf map[string]interface{}) *modelsConfig {
|
||||
raw, ok := conf["models"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
payload, err := json.Marshal(raw)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var models modelsConfig
|
||||
if err := json.Unmarshal(payload, &models); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &models
|
||||
}
|
||||
|
||||
func preserveOpenclawModelMetadata(conf map[string]interface{}, next *modelsConfig) {
|
||||
current := readOpenclawModelsConfig(conf)
|
||||
if current == nil || next == nil {
|
||||
return
|
||||
}
|
||||
for providerID, nextProvider := range next.Providers {
|
||||
currentProvider, ok := current.Providers[providerID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
byID := make(map[string]modelEntry, len(currentProvider.Models))
|
||||
for _, entry := range currentProvider.Models {
|
||||
byID[entry.ID] = entry
|
||||
}
|
||||
for index := range nextProvider.Models {
|
||||
currentEntry, ok := byID[nextProvider.Models[index].ID]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
nextProvider.Models[index].Input = currentEntry.Input
|
||||
nextProvider.Models[index].ContextWindow = currentEntry.ContextWindow
|
||||
nextProvider.Models[index].MaxTokens = currentEntry.MaxTokens
|
||||
}
|
||||
next.Providers[providerID] = nextProvider
|
||||
}
|
||||
}
|
||||
|
||||
func extractOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) []dto.AgentModelMetadata {
|
||||
result := make([]dto.AgentModelMetadata, 0, len(accountModels))
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
for _, item := range accountModels {
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
metadata := dto.AgentModelMetadata{Model: item.ID, InputMode: "auto"}
|
||||
if configured != nil {
|
||||
for _, entry := range configured.Providers[providerID].Models {
|
||||
if entry.ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
metadata.ContextWindow = entry.ContextWindow
|
||||
metadata.MaxTokens = entry.MaxTokens
|
||||
if len(entry.Input) > 0 && !slices.Equal(entry.Input, inferred.Input) {
|
||||
if slices.Contains(entry.Input, "image") {
|
||||
metadata.InputMode = "image"
|
||||
} else {
|
||||
metadata.InputMode = "text"
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
result = append(result, metadata)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func applyOpenclawModelMetadata(conf map[string]interface{}, account *model.AgentAccount, requested []dto.AgentModelMetadata) error {
|
||||
if len(requested) == 0 {
|
||||
return nil
|
||||
}
|
||||
configured := readOpenclawModelsConfig(conf)
|
||||
if configured == nil {
|
||||
return fmt.Errorf("model metadata is not supported for provider %s", account.Provider)
|
||||
}
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
available := make(map[string]dto.AgentAccountModel, len(accountModels))
|
||||
for _, item := range accountModels {
|
||||
available[item.ID] = item
|
||||
}
|
||||
seen := make(map[string]struct{}, len(requested))
|
||||
for _, metadata := range requested {
|
||||
item, ok := available[metadata.Model]
|
||||
if !ok {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if _, ok := seen[metadata.Model]; ok {
|
||||
return fmt.Errorf("duplicate model metadata: %s", metadata.Model)
|
||||
}
|
||||
seen[metadata.Model] = struct{}{}
|
||||
_, inferred, providerID, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
provider := configured.Providers[providerID]
|
||||
found := false
|
||||
for index := range provider.Models {
|
||||
if provider.Models[index].ID != inferred.ID {
|
||||
continue
|
||||
}
|
||||
found = true
|
||||
provider.Models[index].ContextWindow = metadata.ContextWindow
|
||||
provider.Models[index].MaxTokens = metadata.MaxTokens
|
||||
switch metadata.InputMode {
|
||||
case "auto":
|
||||
provider.Models[index].Input = inferred.Input
|
||||
case "text":
|
||||
provider.Models[index].Input = []string{"text"}
|
||||
case "image":
|
||||
provider.Models[index].Input = []string{"text", "image"}
|
||||
default:
|
||||
return fmt.Errorf("unsupported model input mode: %s", metadata.InputMode)
|
||||
}
|
||||
break
|
||||
}
|
||||
if !found {
|
||||
return buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
configured.Providers[providerID] = provider
|
||||
}
|
||||
modelsMap, err := structToMap(configured)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf["models"] = modelsMap
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
@@ -1030,7 +1200,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
@@ -1097,7 +1267,12 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
||||
if err != nil {
|
||||
return "", modelEntry{}, "", modelProvider{}, err
|
||||
}
|
||||
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
|
||||
entry := buildOpenclawModelEntry(providerPatch.ModelID, model)
|
||||
if account.Provider == "minimax" && providerPatch.ModelID == "MiniMax-M3.1-Flash" {
|
||||
entry.ContextWindow = 1000000
|
||||
entry.MaxTokens = 524288
|
||||
}
|
||||
return providerPatch.PrimaryModel, entry, providerPatch.ProviderKey, modelProvider{
|
||||
ApiKey: providerPatch.APIKey,
|
||||
BaseUrl: providerPatch.BaseURL,
|
||||
Api: providerPatch.APIType,
|
||||
@@ -1159,15 +1334,15 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
||||
if len(requested) > 0 {
|
||||
return normalizeAgentAccountModels(account, requested)
|
||||
}
|
||||
meta, ok := providercatalog.Get(account.Provider)
|
||||
if !ok || len(meta.Models) == 0 {
|
||||
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
|
||||
if len(defaultModels) == 0 {
|
||||
if requiresInitialAgentAccountModels(account.Provider) {
|
||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
|
||||
for _, item := range meta.Models {
|
||||
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
|
||||
for _, item := range defaultModels {
|
||||
requested = append(requested, dto.AgentAccountModel{
|
||||
ID: item.ID,
|
||||
Name: item.Name,
|
||||
@@ -1327,7 +1502,7 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
||||
|
||||
func requiresInitialAgentAccountModels(provider string) bool {
|
||||
switch provider {
|
||||
case "custom", "vllm", "ollama":
|
||||
case "custom", "vllm", "ollama", "llmman":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
@@ -1452,6 +1627,91 @@ func readInstallEnv(envStr string) map[string]interface{} {
|
||||
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
|
||||
case constant.AppDeepSeekHarness:
|
||||
return "DSH_AUTH_USERNAME", "DSH_AUTH_PASSWORD", true
|
||||
case constant.AppOctop:
|
||||
return "OCTOP_ADMIN_USERNAME", "OCTOP_DEFAULT_PASSWORD", 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) {
|
||||
|
||||
+107
-47
@@ -8,6 +8,7 @@ import (
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
@@ -28,13 +29,15 @@ import (
|
||||
|
||||
type AIToolService struct{}
|
||||
|
||||
var ollamaModelMu sync.Mutex
|
||||
|
||||
type IAIToolService interface {
|
||||
Search(search dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error)
|
||||
Create(req dto.OllamaModelName) error
|
||||
Close(name string) error
|
||||
Recreate(req dto.OllamaModelName) error
|
||||
Delete(req dto.ForceDelete) error
|
||||
Sync() ([]dto.OllamaModelDropList, error)
|
||||
SyncAccount(req dto.OllamaModelAccountSync) error
|
||||
LoadDetail(name string) (string, error)
|
||||
BindDomain(req dto.OllamaBindDomain) error
|
||||
GetBindDomain(req dto.OllamaBindDomainReq) (*dto.OllamaBindDomainRes, error)
|
||||
@@ -46,6 +49,7 @@ func NewIAIToolService() IAIToolService {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModelInfo, error) {
|
||||
syncErr := syncOllamaModels()
|
||||
var options []repo.DBOption
|
||||
if len(req.Info) != 0 {
|
||||
options = append(options, repo.WithByLikeName(req.Info))
|
||||
@@ -60,6 +64,11 @@ func (u *AIToolService) Search(req dto.SearchWithPage) (int64, []dto.OllamaModel
|
||||
if err := copier.Copy(&item, &itemModel); err != nil {
|
||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
if syncErr != nil && item.Status != constant.StatusWaiting {
|
||||
item.Status = constant.StatusUnknown
|
||||
item.Message = syncErr.Error()
|
||||
item.Size = ""
|
||||
}
|
||||
taskModel, _ := taskRepo.GetFirst(taskRepo.WithResourceID(item.ID), repo.WithByType(task.TaskScopeAI))
|
||||
if len(taskModel.ID) != 0 {
|
||||
item.LogFileExist = true
|
||||
@@ -85,6 +94,8 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
if cmd.CheckIllegal(req.Name) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -125,6 +136,9 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = aiRepo.Update(info.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
}
|
||||
if err := SyncOllamaModelAccount(false); err != nil {
|
||||
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
@@ -144,6 +158,8 @@ func (u *AIToolService) Close(name string) error {
|
||||
}
|
||||
|
||||
func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
if cmd.CheckIllegal(req.Name) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
@@ -179,11 +195,16 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
if err := taskItem.Execute(); err != nil {
|
||||
_ = aiRepo.Update(modelInfo.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
}
|
||||
if err := SyncOllamaModelAccount(false); err != nil {
|
||||
global.LOG.Errorf("sync Ollama model account after pull: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
ollamaList, _ := aiRepo.List(repo.WithByIDs(req.IDs))
|
||||
if len(ollamaList) == 0 {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
@@ -202,55 +223,28 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
|
||||
logItem := path.Join(global.Dir.DataDir, "log", "AITools", item.Name)
|
||||
_ = os.Remove(logItem)
|
||||
}
|
||||
return nil
|
||||
return SyncOllamaModelAccount(false)
|
||||
}
|
||||
|
||||
func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
|
||||
containerName, err := LoadContainerName()
|
||||
func (u *AIToolService) SyncAccount(req dto.OllamaModelAccountSync) error {
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||
if install.App.Key != "ollama" {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
baseURL, err := ResolveLocalModelAccountBaseURL(req.ModelAccountBaseURLType, req.ModelAccountBaseURL, install.ContainerName, "11434", install.HttpPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
var list []model.OllamaModel
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
continue
|
||||
if err := syncOllamaModelAccount(install, baseURL, true, true); err != nil {
|
||||
return err
|
||||
}
|
||||
if parts[0] == "NAME" {
|
||||
continue
|
||||
}
|
||||
list = append(list, model.OllamaModel{Name: parts[0], Size: parts[2] + " " + parts[3]})
|
||||
}
|
||||
listInDB, _ := aiRepo.List()
|
||||
var dropList []dto.OllamaModelDropList
|
||||
for _, itemModel := range listInDB {
|
||||
isExit := false
|
||||
for i := 0; i < len(list); i++ {
|
||||
if list[i].Name == itemModel.Name {
|
||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusSuccess, "message": "", "size": list[i].Size})
|
||||
list = append(list[:i], list[(i+1):]...)
|
||||
isExit = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isExit && itemModel.Status != constant.StatusWaiting {
|
||||
_ = aiRepo.Update(itemModel.ID, map[string]interface{}{"status": constant.StatusDeleted, "message": "not exist", "size": ""})
|
||||
dropList = append(dropList, dto.OllamaModelDropList{ID: itemModel.ID, Name: itemModel.Name})
|
||||
continue
|
||||
}
|
||||
}
|
||||
for _, item := range list {
|
||||
item.Status = constant.StatusSuccess
|
||||
item.From = "remote"
|
||||
_ = aiRepo.Create(&item)
|
||||
}
|
||||
|
||||
return dropList, nil
|
||||
return appInstallRepo.BatchUpdateBy(map[string]interface{}{
|
||||
"model_account_base_url_type": req.ModelAccountBaseURLType,
|
||||
"model_account_base_url": baseURL,
|
||||
}, repo.WithByID(install.ID))
|
||||
}
|
||||
|
||||
func (u *AIToolService) BindDomain(req dto.OllamaBindDomain) error {
|
||||
@@ -370,6 +364,72 @@ func (u *AIToolService) UpdateBindDomain(req dto.OllamaBindDomain) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func syncOllamaModels() error {
|
||||
ollamaModelMu.Lock()
|
||||
defer ollamaModelMu.Unlock()
|
||||
containerName, err := LoadContainerName()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
models := make(map[string]string)
|
||||
lines := strings.Split(strings.TrimSpace(stdout), "\n")
|
||||
if len(lines) == 0 || !strings.HasPrefix(strings.TrimSpace(lines[0]), "NAME") {
|
||||
return fmt.Errorf("invalid ollama list output: %s", stdout)
|
||||
}
|
||||
for _, line := range lines[1:] {
|
||||
if strings.TrimSpace(line) == "" {
|
||||
continue
|
||||
}
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
return fmt.Errorf("invalid ollama model entry: %s", line)
|
||||
}
|
||||
models[parts[0]] = parts[2] + " " + parts[3]
|
||||
}
|
||||
list, err := aiRepo.List()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
known := make(map[string]bool, len(list))
|
||||
for _, item := range list {
|
||||
name := item.Name
|
||||
if !strings.Contains(path.Base(name), ":") {
|
||||
name += ":latest"
|
||||
}
|
||||
size, exists := models[name]
|
||||
known[name] = true
|
||||
if item.Status == constant.StatusWaiting {
|
||||
continue
|
||||
}
|
||||
status, message := constant.StatusSuccess, ""
|
||||
if !exists {
|
||||
if item.Status == constant.StatusFailed || item.Status == constant.StatusCanceled {
|
||||
continue
|
||||
}
|
||||
status, message = constant.StatusDeleted, "not exist"
|
||||
}
|
||||
if item.Status != status || item.Size != size || item.Message != message {
|
||||
if err := aiRepo.Update(item.ID, map[string]interface{}{"status": status, "size": size, "message": message}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, size := range models {
|
||||
if known[name] {
|
||||
continue
|
||||
}
|
||||
item := model.OllamaModel{Name: name, Size: size, From: "remote", Status: constant.StatusSuccess}
|
||||
if err := aiRepo.Create(&item); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func LoadContainerName() (string, error) {
|
||||
ollamaBaseInfo, err := appInstallRepo.LoadBaseInfo("ollama", "")
|
||||
if err != nil {
|
||||
@@ -387,12 +447,12 @@ func loadModelSize(name string, containerName string) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
lines := strings.Split(stdout, "\n")
|
||||
for _, line := range lines {
|
||||
if !strings.Contains(line, name) {
|
||||
continue
|
||||
if !strings.Contains(path.Base(name), ":") {
|
||||
name += ":latest"
|
||||
}
|
||||
for _, line := range lines {
|
||||
parts := strings.Fields(line)
|
||||
if len(parts) < 5 {
|
||||
if len(parts) < 5 || parts[0] != name {
|
||||
continue
|
||||
}
|
||||
return parts[2] + " " + parts[3], nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user