mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +00:00
Compare commits
354
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
262618a4a9 | ||
|
|
b374b70e71 | ||
|
|
4ea3f7e4b8 | ||
|
|
a917880cac | ||
|
|
d41dd382ab | ||
|
|
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 | ||
|
|
c04848a92a | ||
|
|
748f5f070a | ||
|
|
2dcb367622 | ||
|
|
26bd2d08be | ||
|
|
739686ff89 | ||
|
|
68a871a5d7 | ||
|
|
b47bba4827 | ||
|
|
3b91859a49 | ||
|
|
c417bc9c2b | ||
|
|
7c95becfeb | ||
|
|
6b1480c5fa | ||
|
|
7f450544e1 | ||
|
|
0c046edfe5 | ||
|
|
2a2e6607b8 | ||
|
|
f6ed11ac55 | ||
|
|
d330f60b2d | ||
|
|
10c8cafcee | ||
|
|
d1534c88aa | ||
|
|
eb6b4c7cb7 | ||
|
|
c77ddcfebe | ||
|
|
af12526a6c | ||
|
|
b347abe100 | ||
|
|
092b57413b | ||
|
|
e8f35e4e06 | ||
|
|
b019e7526d | ||
|
|
1b06467521 | ||
|
|
7f3fd0cb1d | ||
|
|
3e9848554b | ||
|
|
b02cdbc5f9 | ||
|
|
d7723fd42a | ||
|
|
8b265bb81e | ||
|
|
89b6c94e42 | ||
|
|
219ac160b6 | ||
|
|
51ca1eab18 | ||
|
|
1ab3da1fab | ||
|
|
1d1b12c2fa | ||
|
|
07a658582d | ||
|
|
61da74ae50 | ||
|
|
90334f3cb0 | ||
|
|
fb9278bfc6 | ||
|
|
7750936af6 | ||
|
|
7b695af996 | ||
|
|
2874849557 | ||
|
|
9fd1f3e661 | ||
|
|
85182d9164 | ||
|
|
d9b2ea051f | ||
|
|
097fe6dcfb | ||
|
|
d5beca6d96 | ||
|
|
e3a356f893 | ||
|
|
8be2a9ab02 | ||
|
|
3d7f0f5143 | ||
|
|
1fd62fbaf7 | ||
|
|
f1372dda85 | ||
|
|
64c354adb2 | ||
|
|
a379e1f840 | ||
|
|
270ba799f7 | ||
|
|
60dcfacb88 | ||
|
|
b474d720e9 | ||
|
|
b2192b7252 | ||
|
|
6a988ad96a | ||
|
|
029ac5efee | ||
|
|
582cf1f533 | ||
|
|
b8a4e8e9e0 | ||
|
|
4a462aecc7 | ||
|
|
74f221b895 | ||
|
|
cbcb628bff | ||
|
|
f602645e31 | ||
|
|
ec9e609f10 | ||
|
|
1c1f3c7761 | ||
|
|
fae49284b7 | ||
|
|
072a608a63 | ||
|
|
1e6fd9c3a0 | ||
|
|
23200cbe85 | ||
|
|
08604e8c02 | ||
|
|
62a472d2d1 | ||
|
|
2c84665e8b | ||
|
|
616bd6830c | ||
|
|
e55ea96f00 | ||
|
|
aa676e139c | ||
|
|
7708fea58d | ||
|
|
34bdff01ad | ||
|
|
5a49579ddf | ||
|
|
b98a2b06c9 | ||
|
|
9073b16baa | ||
|
|
2ec4db8d7e | ||
|
|
fe9628f91c | ||
|
|
18a469e4be | ||
|
|
8c691cc074 | ||
|
|
2cd31a47f8 | ||
|
|
d97aa9c2a0 | ||
|
|
f31f0a89ff | ||
|
|
0a1621166e | ||
|
|
8623aa5318 | ||
|
|
ec50599526 | ||
|
|
458a30c520 | ||
|
|
9a660db4c3 | ||
|
|
f4fc71d32b | ||
|
|
e45a8185e3 | ||
|
|
7be9189b3c | ||
|
|
bdf13c01f6 | ||
|
|
902bc90aef | ||
|
|
27ba50d8be | ||
|
|
fb483ecb90 | ||
|
|
2de7079dd2 | ||
|
|
2e4ac604a8 | ||
|
|
46af80142e | ||
|
|
677b47dcdb | ||
|
|
56cccdf7c0 | ||
|
|
3e0d0bb809 | ||
|
|
86aa6c09c6 | ||
|
|
f67e1d897c | ||
|
|
9e518daa62 | ||
|
|
2e5455e655 | ||
|
|
868f1db4c5 | ||
|
|
efd84b1e1b | ||
|
|
5591fe50f0 | ||
|
|
5e0a598799 | ||
|
|
45d7b30abc | ||
|
|
46af88e1d0 | ||
|
|
7f331f5503 | ||
|
|
7b74e09d30 | ||
|
|
01e4455110 | ||
|
|
922a2d21c8 | ||
|
|
439c4794b7 | ||
|
|
5ac7f16b67 | ||
|
|
bfb79066b4 | ||
|
|
19602b42fb | ||
|
|
f987af264f | ||
|
|
a6c76dffc3 | ||
|
|
a2eed66bc4 | ||
|
|
a49b9b132d | ||
|
|
e8505a249a |
@@ -1,25 +0,0 @@
|
|||||||
name: LLM Code Review
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
pull-requests: write
|
|
||||||
on:
|
|
||||||
pull_request:
|
|
||||||
types: [opened, reopened, synchronize]
|
|
||||||
jobs:
|
|
||||||
llm-code-review:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: fit2cloud/LLM-CodeReview-Action@main
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.FIT2CLOUDRD_LLM_CODE_REVIEW_TOKEN }}
|
|
||||||
OPENAI_API_KEY: ${{ secrets.ALIYUN_LLM_API_KEY }}
|
|
||||||
LANGUAGE: English
|
|
||||||
OPENAI_API_ENDPOINT: https://dashscope.aliyuncs.com/compatible-mode/v1
|
|
||||||
MODEL: qwen2.5-coder-3b-instruct
|
|
||||||
PROMPT: "Please check the following code differences for any irregularities, potential issues, or optimization suggestions, and provide your answers in English."
|
|
||||||
top_p: 1
|
|
||||||
temperature: 1
|
|
||||||
# max_tokens: 10000
|
|
||||||
MAX_PATCH_LENGTH: 10000
|
|
||||||
IGNORE_PATTERNS: "/node_modules,*.md,/dist,/.github"
|
|
||||||
FILE_PATTERNS: "*.java,*.go,*.py,*.vue,*.ts,*.js,*.css,*.scss,*.html"
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
name: SonarCloud Scan
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- dev
|
|
||||||
pull_request:
|
|
||||||
types: [opened, synchronize, reopened]
|
|
||||||
jobs:
|
|
||||||
sonarcloud:
|
|
||||||
name: SonarCloud
|
|
||||||
if: github.repository == '1Panel-dev/1Panel'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v3
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
- name: SonarCloud Scan
|
|
||||||
uses: SonarSource/sonarcloud-github-action@master
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUBTOKEN }}
|
|
||||||
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
|
|
||||||
@@ -6,7 +6,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- name: Mirror the Github organization repos to Gitee.
|
- name: Mirror the Github organization repos to Gitee.
|
||||||
uses: Yikun/hub-mirror-action@master
|
uses: Yikun/hub-mirror-action@ba51c01b28a6c9f95a25d4f1bcf6af2a147c0e18 # master
|
||||||
with:
|
with:
|
||||||
src: 'github/1Panel-dev'
|
src: 'github/1Panel-dev'
|
||||||
dst: 'gitee/fit2cloud-feizhiyun'
|
dst: 'gitee/fit2cloud-feizhiyun'
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
name: Typos Check
|
|
||||||
on: pull_request
|
|
||||||
jobs:
|
|
||||||
run:
|
|
||||||
name: Spell Check with Typos
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- name: Checkout Actions Repository
|
|
||||||
uses: actions/checkout@v2
|
|
||||||
- name: Check spelling
|
|
||||||
uses: crate-ci/typos@master
|
|
||||||
@@ -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>
|
<p align="center"><a href="https://1panel.pro"><img src="https://resource.1panel.pro/img/1panel-logo.png" alt="1Panel" width="300" /></a></p>
|
||||||
|
|
||||||
<h3 align="center">The open-source VPS control panel with native AI agent support</h3>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
Trusted by <strong>2,000,000+</strong> self-hosters worldwide
|
Loved by a global community of <strong>2.5M+</strong> self-hosters.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
@@ -12,7 +9,6 @@
|
|||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
|
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
|
||||||
<a href="https://app.codacy.com/gh/1Panel-dev/1Panel"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
|
|
||||||
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
|
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
|
||||||
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
|
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
|
||||||
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a>
|
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a>
|
||||||
@@ -25,7 +21,7 @@
|
|||||||
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
|
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
|
||||||
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
|
||||||
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
|
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
|
||||||
<a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
<a href="/docs/README.es-es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
||||||
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
|
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
|
||||||
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
|
||||||
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
|
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
|
||||||
@@ -33,40 +29,36 @@
|
|||||||
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
|
||||||
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
|
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
|
||||||
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
|
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
|
||||||
|
<a href="/docs/README.fa.md"><img alt="Persian" src="https://img.shields.io/badge/%D9%81%D8%A7%D8%B1%D8%B3%DB%8C-d9d9d9"></a>
|
||||||
|
<a href="/docs/README.lo.md"><img alt="ພາສາລາວ" src="https://img.shields.io/badge/%E0%BA%9E%E0%BA%B2%E0%BA%AA%E0%BA%B2%E0%BA%A5%E0%BA%B2%E0%BA%A7-d9d9d9"></a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## What is 1Panel?
|
## What is 1Panel?
|
||||||
|
|
||||||
1Panel is a modern, open-source 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.
|
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.
|
||||||
👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
|
- **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?
|
## Why 1Panel?
|
||||||
|
|
||||||
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
||||||
|--|--------|----------------|---------|--------|
|
|--|--------|----------------|---------|--------|
|
||||||
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
||||||
| Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
|
| AI management | ✅ | ❌ | ❌ | ❌ |
|
||||||
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
||||||
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
||||||
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
||||||
| Active development | ✅ | ✅ | ✅ | Slow |
|
| Active development | ✅ | ✅ | ✅ | Slow |
|
||||||
|
|
||||||
## Key Features
|
|
||||||
|
|
||||||
- **AI Agent Runtime**: Deploy Ollama LLMs, spin up OpenClaw personal agents, and monitor GPU utilization — all from the dashboard. No separate AI stack to manage.
|
|
||||||
- **One-Click Website Deployment**: Launch production-ready websites with automatic domain binding, SSL provisioning, and Nginx config — zero manual setup.
|
|
||||||
- **App Marketplace**: 165+ trusted open-source apps (Nextcloud, Bitwarden, Umami, NocoBase, and more) installed and updated with a single click.
|
|
||||||
- **Docker & Container Management**: Create, start, stop, and inspect containers, images, networks, and volumes through a visual UI — no CLI juggling.
|
|
||||||
- **Security Out of the Box**: Firewall rules, fail2ban, container isolation, WAF, and audit logs — configured and running from day one.
|
|
||||||
- **Backup & Restore**: Schedule automated backups to AWS S3, Cloudflare R2, or local storage. Restore any snapshot in one click.
|
|
||||||
|
|
||||||
## Quick Start
|
## Quick Start
|
||||||
|
|
||||||
> **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
|
Prepare your Linux server and run the following script:
|
||||||
> Takes ~60 seconds.
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
||||||
@@ -81,24 +73,20 @@ Run `1pctl user-info` via SSH if you need to retrieve your access credentials.
|
|||||||
|
|
||||||
## Pro Edition
|
## 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 |
|
| Feature | OSS | Pro | Ent |
|
||||||
|---------|:---:|:---:|
|
|---------|:---:|:---:|:---:|
|
||||||
| One-click app installs | ✅ | ✅ |
|
| One-click app installs | ✅ | ✅ | ✅ |
|
||||||
| AI agents (OpenClaw) | 1 agent | Unlimited |
|
| AI agents (OpenClaw) | 5 agent | Unlimited | ✅ |
|
||||||
| WAF & advanced security | Basic | ✅ |
|
| WAF & advanced security | Basic | ✅ | ✅ |
|
||||||
| Website tamper protection | ❌ | ✅ |
|
| Website tamper protection | ❌ | ✅ | ✅ |
|
||||||
| Website uptime monitoring | ❌ | ✅ |
|
| Website uptime monitoring | ❌ | ✅ | ✅ |
|
||||||
| Multi-node management | ❌ | ✅ |
|
| Multi-node management | ❌ | ✅ | ✅ |
|
||||||
| Custom logo & theme | ❌ | ✅ |
|
| Custom logo & theme | ❌ | ✅ | ✅ |
|
||||||
| Priority support | ❌ | ✅ |
|
| KVM Web UI | ❌ | ❌ | ✅ |
|
||||||
|
| AI Gateway | ❌ | ❌ | ✅ |
|
||||||
**From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
|
| Priority support | ❌ | ❌ | ✅ |
|
||||||
|
|
||||||
## Star History
|
|
||||||
|
|
||||||
[](https://star-history.com/#1Panel-dev/1Panel&Date)
|
|
||||||
|
|
||||||
## Community & Support
|
## Community & Support
|
||||||
|
|
||||||
|
|||||||
@@ -500,6 +500,27 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, list)
|
helper.SuccessWithData(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Discover custom provider models
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentAccountModelDiscoverReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentAccountModel
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/accounts/models/discover [post]
|
||||||
|
func (b *BaseApi) DiscoverAgentAccountModels(c *gin.Context) {
|
||||||
|
var req dto.AgentAccountModelDiscoverReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
list, err := agentService.DiscoverAccountModels(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, list)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Create model account model
|
// @Summary Create model account model
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -1357,6 +1378,88 @@ func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary List OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginsReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/list [post]
|
||||||
|
func (b *BaseApi) ListAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginsReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.ListPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Search OpenClaw plugins
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginSearchReq true "request"
|
||||||
|
// @Success 200 {array} dto.AgentPluginSearchItem
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/search [post]
|
||||||
|
func (b *BaseApi) SearchAgentPlugins(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginSearchReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := agentService.SearchPlugins(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Install an OpenClaw marketplace plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginMarketInstallReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/install [post]
|
||||||
|
func (b *BaseApi) InstallAgentMarketPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginMarketInstallReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.InstallMarketPlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags AI
|
||||||
|
// @Summary Operate an OpenClaw plugin
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AgentPluginOperateReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/agents/plugins/operate [post]
|
||||||
|
func (b *BaseApi) OperateAgentPlugin(c *gin.Context) {
|
||||||
|
var req dto.AgentPluginOperateReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := agentService.OperatePlugin(req); err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
// @Summary Login Agent Weixin channel
|
// @Summary Login Agent Weixin channel
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
@@ -2,11 +2,14 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -294,6 +297,34 @@ func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
|
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, repo.ErrAlertConfigRevisionConflict):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, repo.ErrAlertConfigRevisionRequired):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "ALERT_CONFIG_REVISION_REQUIRED", "ErrInvalidParams", err)
|
||||||
|
default:
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Alert
|
||||||
|
// @Summary Update alert config status
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.AlertConfigStatusUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /alert/config/status [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置状态 [id][status]","formatEN":"update alert config status [id][status]"}
|
||||||
|
func (b *BaseApi) UpdateAlertConfigStatus(c *gin.Context) {
|
||||||
|
var req dto.AlertConfigStatusUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := alertService.UpdateAlertConfigStatus(req, loadAuditUser(c)); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -346,6 +377,15 @@ func (b *BaseApi) TestAlertConfig(c *gin.Context) {
|
|||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.Type == constant.Custom {
|
||||||
|
result, err := alertService.TestCustomAlertConfig(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
flag, err := alertService.TestAlertConfig(req)
|
flag, err := alertService.TestAlertConfig(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -413,10 +413,15 @@ func (b *BaseApi) Backup(c *gin.Context) {
|
|||||||
|
|
||||||
switch req.Type {
|
switch req.Type {
|
||||||
case "app":
|
case "app":
|
||||||
if _, err := backupService.AppBackup(req); err != nil {
|
record, err := backupService.AppBackup(req)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if req.IsImmediate {
|
||||||
|
helper.SuccessWithData(c, record)
|
||||||
|
return
|
||||||
|
}
|
||||||
case "mysql", "mariadb", constant.AppMysqlCluster:
|
case "mysql", "mariadb", constant.AppMysqlCluster:
|
||||||
if err := backupService.MysqlBackup(req); err != nil {
|
if err := backupService.MysqlBackup(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ func (b *BaseApi) SearchContainer(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
total, list, err := containerService.Page(req)
|
total, list, err := containerService.Page(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -249,9 +249,10 @@ func (b *BaseApi) ListContainerByImage(c *gin.Context) {
|
|||||||
// @Success 200 {object} dto.ContainerStatus
|
// @Success 200 {object} dto.ContainerStatus
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param containersOnly query boolean false "Only count containers"
|
||||||
// @Router /containers/status [get]
|
// @Router /containers/status [get]
|
||||||
func (b *BaseApi) LoadContainerStatus(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -415,9 +416,14 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
|
|||||||
// @Success 200 {array} dto.ContainerListStats
|
// @Success 200 {array} dto.ContainerListStats
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
|
// @Param ids query string false "Comma-separated container IDs; omitted selects all containers"
|
||||||
// @Router /containers/list/stats [get]
|
// @Router /containers/list/stats [get]
|
||||||
func (b *BaseApi) ContainerListStats(c *gin.Context) {
|
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 {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -439,7 +445,7 @@ func (b *BaseApi) ContainerItemStats(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := containerService.ContainerItemStats(req)
|
data, err := containerService.ContainerItemStats(c.Request.Context(), req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -881,6 +887,26 @@ func (b *BaseApi) ComposeUpdate(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Container Compose
|
||||||
|
// @Summary Pin compose
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.ComposePin true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /containers/compose/pin [post]
|
||||||
|
func (b *BaseApi) ComposePin(c *gin.Context) {
|
||||||
|
var req dto.ComposePin
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := containerService.ComposePin(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Container Compose
|
// @Tags Container Compose
|
||||||
// @Summary Load compose environment variables
|
// @Summary Load compose environment variables
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -944,3 +970,12 @@ func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
|
|||||||
|
|
||||||
containerService.StreamLogs(c, streamLog)
|
containerService.StreamLogs(c, streamLog)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *BaseApi) CleanNetworks(c *gin.Context) {
|
||||||
|
result, err := containerService.CleanNetworks()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,20 +41,40 @@ func (b *BaseApi) CreateMysql(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Mysql
|
// @Tags Database Mysql
|
||||||
// @Summary Bind user of mysql database
|
// @Summary List mysql users
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.BindUser true "request"
|
// @Param request body dto.MysqlUserSearch true "request"
|
||||||
// @Success 200
|
// @Success 200 {array} dto.MysqlUser
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/bind [post]
|
// @Router /databases/users/search [post]
|
||||||
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mysql 数据库名 [database] [username]","formatEN":"bind mysql database [database] [username]"}
|
func (b *BaseApi) ListMysqlUsers(c *gin.Context) {
|
||||||
func (b *BaseApi) BindUser(c *gin.Context) {
|
var req dto.MysqlUserSearch
|
||||||
var req dto.BindUser
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
data, err := mysqlService.ListUsers(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary Create mysql user
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlUserCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/users [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"create mysql database [database] user [username]@[host]"}
|
||||||
|
func (b *BaseApi) CreateMysqlUser(c *gin.Context) {
|
||||||
|
var req dto.MysqlUserCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
if len(req.Password) != 0 {
|
if len(req.Password) != 0 {
|
||||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -63,8 +83,191 @@ func (b *BaseApi) BindUser(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
req.Password = string(password)
|
req.Password = string(password)
|
||||||
}
|
}
|
||||||
|
if err := mysqlService.CreateUser(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
if err := mysqlService.BindUser(req); err != nil {
|
// @Tags Database Mysql
|
||||||
|
// @Summary Delete mysql user
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlUserDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/users/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 mysql 数据库 [database] 用户 [username]@[host]","formatEN":"delete mysql database [database] user [username]@[host]"}
|
||||||
|
func (b *BaseApi) DeleteMysqlUser(c *gin.Context) {
|
||||||
|
var req dto.MysqlUserDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mysqlService.DeleteUser(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary Update mysql user
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlUserUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/users/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","username","host","newHost","description"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username] 访问权限 [host] => [newHost] 描述 [description]","formatEN":"update mysql database [database] user [username] access [host] => [newHost] description [description]"}
|
||||||
|
func (b *BaseApi) UpdateMysqlUser(c *gin.Context) {
|
||||||
|
var req dto.MysqlUserUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mysqlService.UpdateUser(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary Change mysql user password
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlUserPassword true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/users/password [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"update mysql database [database] user [username]@[host] password"}
|
||||||
|
func (b *BaseApi) ChangeMysqlUserPassword(c *gin.Context) {
|
||||||
|
var req dto.MysqlUserPassword
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Password) != 0 {
|
||||||
|
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Password = string(password)
|
||||||
|
}
|
||||||
|
if err := mysqlService.ChangeUserPassword(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary Save mysql user password locally
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlUserPassword true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/users/password/save [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"补充 mysql 数据库 [database] 用户 [username]@[host] 密码","formatEN":"save mysql database [database] user [username]@[host] password locally"}
|
||||||
|
func (b *BaseApi) SaveMysqlUserPassword(c *gin.Context) {
|
||||||
|
var req dto.MysqlUserPassword
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(req.Password) != 0 {
|
||||||
|
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
req.Password = string(password)
|
||||||
|
}
|
||||||
|
if err := mysqlService.SaveUserPassword(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary List mysql grants
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlUserSearch true "request"
|
||||||
|
// @Success 200 {array} dto.MysqlGrant
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/grants/search [post]
|
||||||
|
func (b *BaseApi) ListMysqlGrants(c *gin.Context) {
|
||||||
|
var req dto.MysqlUserSearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := mysqlService.ListGrants(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary List mysql grant summary
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlGrantSummarySearch true "request"
|
||||||
|
// @Success 200 {object} map[string][]dto.MysqlUser
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/grants/summary [post]
|
||||||
|
func (b *BaseApi) ListMysqlGrantSummary(c *gin.Context) {
|
||||||
|
var req dto.MysqlGrantSummarySearch
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := mysqlService.ListGrantSummary(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary Grant mysql user
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlGrantCreate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/grants [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"授权 mysql 数据库 [database] 用户 [username]@[host] 访问 [db]","formatEN":"grant mysql database [database] user [username]@[host] access to [db]"}
|
||||||
|
func (b *BaseApi) GrantMysqlUser(c *gin.Context) {
|
||||||
|
var req dto.MysqlGrantCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mysqlService.GrantUser(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Database Mysql
|
||||||
|
// @Summary Revoke mysql grant
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.MysqlGrantDelete true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /databases/grants/del [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["database","db","username","host"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"取消 mysql 数据库 [database] 用户 [username]@[host] 对 [db] 的授权","formatEN":"revoke mysql database [database] user [username]@[host] access to [db]"}
|
||||||
|
func (b *BaseApi) RevokeMysqlGrant(c *gin.Context) {
|
||||||
|
var req dto.MysqlGrantDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := mysqlService.RevokeGrant(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -94,14 +297,14 @@ func (b *BaseApi) UpdateMysqlDescription(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Mysql
|
// @Tags Database Mysql
|
||||||
// @Summary Change mysql password
|
// @Summary Change mysql root password
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.ChangeDBInfo true "request"
|
// @Param request body dto.ChangeDBInfo true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/change/password [post]
|
// @Router /databases/change/password [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 密码","formatEN":"Update database [name] password"}
|
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 密码","formatEN":"Update database [database] root password"}
|
||||||
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
|
func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
|
||||||
var req dto.ChangeDBInfo
|
var req dto.ChangeDBInfo
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
@@ -125,14 +328,14 @@ func (b *BaseApi) ChangeMysqlPassword(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Database Mysql
|
// @Tags Database Mysql
|
||||||
// @Summary Change mysql access
|
// @Summary Change mysql root access
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.ChangeDBInfo true "request"
|
// @Param request body dto.ChangeDBInfo true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /databases/change/access [post]
|
// @Router /databases/change/access [post]
|
||||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mysqls","output_column":"name","output_value":"name"}],"formatZH":"更新数据库 [name] 访问权限","formatEN":"Update database [name] access"}
|
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] root 访问权限","formatEN":"Update database [database] root access"}
|
||||||
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
|
func (b *BaseApi) ChangeMysqlAccess(c *gin.Context) {
|
||||||
var req dto.ChangeDBInfo
|
var req dto.ChangeDBInfo
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -37,14 +37,17 @@ var (
|
|||||||
|
|
||||||
cronjobService = service.NewICronjobService()
|
cronjobService = service.NewICronjobService()
|
||||||
|
|
||||||
fileService = service.NewIFileService()
|
fileService = service.NewIFileService()
|
||||||
fileHistoryService = service.NewIFileHistoryService()
|
fileHistoryService = service.NewIFileHistoryService()
|
||||||
fileShareService = service.NewIFileShareService()
|
fileShareService = service.NewIFileShareService()
|
||||||
sshService = service.NewISSHService()
|
sshService = service.NewISSHService()
|
||||||
firewallService = service.NewIFirewallService()
|
firewallService = service.NewIFirewallService()
|
||||||
iptablesService = service.NewIIptablesService()
|
firewallSettingService = service.NewIFirewallSettingService()
|
||||||
monitorService = service.NewIMonitorService()
|
forwardingService = service.NewIForwardingService()
|
||||||
systemService = service.NewISystemService()
|
dockerPortGuardService = service.NewIDockerPortGuardService()
|
||||||
|
monitorService = service.NewIMonitorService()
|
||||||
|
systemService = service.NewISystemService()
|
||||||
|
runtimeDiagnosticsService = service.NewIRuntimeDiagnosticsService()
|
||||||
|
|
||||||
deviceService = service.NewIDeviceService()
|
deviceService = service.NewIDeviceService()
|
||||||
fail2banService = service.NewIFail2BanService()
|
fail2banService = service.NewIFail2BanService()
|
||||||
@@ -59,6 +62,7 @@ var (
|
|||||||
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
websiteDnsAccountService = service.NewIWebsiteDnsAccountService()
|
||||||
websiteSSLService = service.NewIWebsiteSSLService()
|
websiteSSLService = service.NewIWebsiteSSLService()
|
||||||
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
websiteAcmeAccountService = service.NewIWebsiteAcmeAccountService()
|
||||||
|
websiteTemplateService = service.NewIWebsiteTemplateService()
|
||||||
|
|
||||||
nginxService = service.NewINginxService()
|
nginxService = service.NewINginxService()
|
||||||
|
|
||||||
|
|||||||
+535
-21
@@ -12,7 +12,9 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -28,6 +30,86 @@ import (
|
|||||||
qrcode "github.com/skip2/go-qrcode"
|
qrcode "github.com/skip2/go-qrcode"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var cancelledChunkUploads = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
ids map[string]struct{}
|
||||||
|
}{ids: make(map[string]struct{})}
|
||||||
|
|
||||||
|
type chunkUploadLock struct {
|
||||||
|
mutex sync.Mutex
|
||||||
|
refs int
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkUploadLocks = struct {
|
||||||
|
sync.Mutex
|
||||||
|
items map[string]*chunkUploadLock
|
||||||
|
}{items: make(map[string]*chunkUploadLock)}
|
||||||
|
|
||||||
|
type completedChunkUpload struct {
|
||||||
|
dstDir string
|
||||||
|
filename string
|
||||||
|
fileSize int64
|
||||||
|
}
|
||||||
|
|
||||||
|
var completedChunkUploads = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
items map[string]completedChunkUpload
|
||||||
|
}{items: make(map[string]completedChunkUpload)}
|
||||||
|
|
||||||
|
var activeChunkUploadTTL = 24 * time.Hour
|
||||||
|
|
||||||
|
var (
|
||||||
|
errChunkUploadCancelled = errors.New("upload cancelled")
|
||||||
|
errInvalidChunkUpload = errors.New("invalid chunk upload")
|
||||||
|
)
|
||||||
|
|
||||||
|
type activeChunkUpload struct {
|
||||||
|
upload completedChunkUpload
|
||||||
|
expiresAt time.Time
|
||||||
|
timer *time.Timer
|
||||||
|
}
|
||||||
|
|
||||||
|
var activeChunkUploads = struct {
|
||||||
|
sync.RWMutex
|
||||||
|
items map[string]activeChunkUpload
|
||||||
|
}{items: make(map[string]activeChunkUpload)}
|
||||||
|
|
||||||
|
type resumableUploadChunk struct {
|
||||||
|
UploadID string
|
||||||
|
Filename string
|
||||||
|
DstDir string
|
||||||
|
ChunkIndex int
|
||||||
|
ChunkCount int
|
||||||
|
Offset int64
|
||||||
|
FileSize int64
|
||||||
|
Overwrite bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func invalidChunkUploadError(message string) error {
|
||||||
|
return fmt.Errorf("%w: %s", errInvalidChunkUpload, message)
|
||||||
|
}
|
||||||
|
|
||||||
|
func isRetryableChunkUploadError(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, errChunkUploadCancelled) ||
|
||||||
|
errors.Is(err, errInvalidChunkUpload) ||
|
||||||
|
errors.Is(err, os.ErrExist) ||
|
||||||
|
errors.Is(err, os.ErrPermission) ||
|
||||||
|
errors.Is(err, os.ErrInvalid) ||
|
||||||
|
errors.Is(err, syscall.ENOSPC) ||
|
||||||
|
errors.Is(err, syscall.EDQUOT) ||
|
||||||
|
errors.Is(err, syscall.EROFS) ||
|
||||||
|
errors.Is(err, syscall.EFBIG) ||
|
||||||
|
errors.Is(err, syscall.ENAMETOOLONG) ||
|
||||||
|
errors.Is(err, syscall.ENOTDIR) ||
|
||||||
|
errors.Is(err, syscall.EISDIR) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary List files
|
// @Summary List files
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -467,11 +549,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
dstInfo, statErr := os.Stat(dstFilename)
|
dstInfo, statErr := os.Stat(dstFilename)
|
||||||
if overwrite {
|
err = finalizeUploadedFile(tmpFilename, dstFilename, overwrite)
|
||||||
_ = os.Remove(dstFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Rename(tmpFilename, dstFilename)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
_ = os.Remove(tmpFilename)
|
_ = os.Remove(tmpFilename)
|
||||||
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
||||||
@@ -606,10 +684,35 @@ func (b *BaseApi) StopWget(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
files.CancelDownload(req.Key)
|
if err := files.CancelDownload(req.Key); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags File
|
||||||
|
// @Summary Remove finished download progress records without deleting files
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.FileProcessRemoveReq true "request"
|
||||||
|
// @Success 200 {object} response.FileProcessKeys
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /files/wget/process/remove [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["keys"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"移除已结束下载记录 [keys]","formatEN":"Remove finished download records [keys]"}
|
||||||
|
func (b *BaseApi) RemoveWgetRecords(c *gin.Context) {
|
||||||
|
var req request.FileProcessRemoveReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
keys, err := files.RemoveDownloadRecords(req.Keys)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, response.FileProcessKeys{Keys: keys})
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary Move file
|
// @Summary Move file
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -631,6 +734,26 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags File
|
||||||
|
// @Summary Stop file move task
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.FileMoveStopReq true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /files/move/stop [post]
|
||||||
|
func (b *BaseApi) StopMoveFile(c *gin.Context) {
|
||||||
|
var req request.FileMoveStopReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := fileService.StopMvFile(req.TaskID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags File
|
// @Tags File
|
||||||
// @Summary Download file
|
// @Summary Download file
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -785,6 +908,289 @@ func (b *BaseApi) DepthDirSize(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, res)
|
helper.SuccessWithData(c, res)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func lockChunkUpload(uploadID string) func() {
|
||||||
|
chunkUploadLocks.Lock()
|
||||||
|
lock, ok := chunkUploadLocks.items[uploadID]
|
||||||
|
if !ok {
|
||||||
|
lock = &chunkUploadLock{}
|
||||||
|
chunkUploadLocks.items[uploadID] = lock
|
||||||
|
}
|
||||||
|
lock.refs++
|
||||||
|
chunkUploadLocks.Unlock()
|
||||||
|
|
||||||
|
lock.mutex.Lock()
|
||||||
|
return func() {
|
||||||
|
lock.mutex.Unlock()
|
||||||
|
chunkUploadLocks.Lock()
|
||||||
|
lock.refs--
|
||||||
|
if lock.refs == 0 {
|
||||||
|
delete(chunkUploadLocks.items, uploadID)
|
||||||
|
}
|
||||||
|
chunkUploadLocks.Unlock()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func resumableUploadPartPath(dstDir, uploadID string) string {
|
||||||
|
return filepath.Join(dstDir, fmt.Sprintf(".1panel-upload-%s.part", uploadID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeUploadedFile(tmpFile, dstFile string, overwrite bool) error {
|
||||||
|
if overwrite {
|
||||||
|
return os.Rename(tmpFile, dstFile)
|
||||||
|
}
|
||||||
|
if err := os.Link(tmpFile, dstFile); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.Remove(tmpFile); err != nil {
|
||||||
|
if rollbackErr := os.Remove(dstFile); rollbackErr != nil {
|
||||||
|
return fmt.Errorf("remove upload temporary file failed: %v, rollback destination failed: %w", err, rollbackErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerActiveChunkUpload(uploadID string, upload completedChunkUpload) error {
|
||||||
|
activeChunkUploads.Lock()
|
||||||
|
defer activeChunkUploads.Unlock()
|
||||||
|
if active, ok := activeChunkUploads.items[uploadID]; ok {
|
||||||
|
if active.upload != upload {
|
||||||
|
return invalidChunkUploadError("upload ID is already used by another file")
|
||||||
|
}
|
||||||
|
active.timer.Stop()
|
||||||
|
}
|
||||||
|
expiresAt := time.Now().Add(activeChunkUploadTTL)
|
||||||
|
timer := time.AfterFunc(activeChunkUploadTTL, func() {
|
||||||
|
expireActiveChunkUpload(uploadID, expiresAt)
|
||||||
|
})
|
||||||
|
activeChunkUploads.items[uploadID] = activeChunkUpload{
|
||||||
|
upload: upload,
|
||||||
|
expiresAt: expiresAt,
|
||||||
|
timer: timer,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadActiveChunkUpload(uploadID string) (completedChunkUpload, bool) {
|
||||||
|
activeChunkUploads.RLock()
|
||||||
|
active, ok := activeChunkUploads.items[uploadID]
|
||||||
|
activeChunkUploads.RUnlock()
|
||||||
|
return active.upload, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteActiveChunkUpload(uploadID string) {
|
||||||
|
activeChunkUploads.Lock()
|
||||||
|
if active, ok := activeChunkUploads.items[uploadID]; ok {
|
||||||
|
active.timer.Stop()
|
||||||
|
}
|
||||||
|
delete(activeChunkUploads.items, uploadID)
|
||||||
|
activeChunkUploads.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
func discardActiveChunkUpload(uploadID, partFile string) error {
|
||||||
|
deleteActiveChunkUpload(uploadID)
|
||||||
|
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
return fmt.Errorf("remove upload temporary file failed: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func finalizeActiveChunkUpload(uploadID, partFile, dstFile string, overwrite bool) error {
|
||||||
|
if err := finalizeUploadedFile(partFile, dstFile, overwrite); err != nil {
|
||||||
|
if removeErr := discardActiveChunkUpload(uploadID, partFile); removeErr != nil {
|
||||||
|
return errors.Join(err, removeErr)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func expireActiveChunkUpload(uploadID string, expiresAt time.Time) {
|
||||||
|
unlock := lockChunkUpload(uploadID)
|
||||||
|
defer unlock()
|
||||||
|
activeChunkUploads.Lock()
|
||||||
|
active, ok := activeChunkUploads.items[uploadID]
|
||||||
|
if !ok || !active.expiresAt.Equal(expiresAt) {
|
||||||
|
activeChunkUploads.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
delete(activeChunkUploads.items, uploadID)
|
||||||
|
activeChunkUploads.Unlock()
|
||||||
|
partFile := resumableUploadPartPath(active.upload.dstDir, uploadID)
|
||||||
|
if err := os.Remove(partFile); err != nil && !os.IsNotExist(err) {
|
||||||
|
global.LOG.Warnf("remove inactive upload part [%s] failed: %v", partFile, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeActiveResumableUploadPart(uploadID string) error {
|
||||||
|
unlock := lockChunkUpload(uploadID)
|
||||||
|
defer unlock()
|
||||||
|
upload, ok := loadActiveChunkUpload(uploadID)
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := os.Remove(resumableUploadPartPath(upload.dstDir, uploadID))
|
||||||
|
if err == nil || os.IsNotExist(err) {
|
||||||
|
deleteActiveChunkUpload(uploadID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadCompletedChunkUpload(uploadID string) (completedChunkUpload, bool) {
|
||||||
|
completedChunkUploads.RLock()
|
||||||
|
completed, ok := completedChunkUploads.items[uploadID]
|
||||||
|
completedChunkUploads.RUnlock()
|
||||||
|
return completed, ok
|
||||||
|
}
|
||||||
|
|
||||||
|
func markChunkUploadCompleted(uploadID string, completed completedChunkUpload) {
|
||||||
|
completedChunkUploads.Lock()
|
||||||
|
completedChunkUploads.items[uploadID] = completed
|
||||||
|
completedChunkUploads.Unlock()
|
||||||
|
time.AfterFunc(10*time.Minute, func() {
|
||||||
|
completedChunkUploads.Lock()
|
||||||
|
delete(completedChunkUploads.items, uploadID)
|
||||||
|
completedChunkUploads.Unlock()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeResumableUploadChunk(chunk resumableUploadChunk, chunkData []byte) error {
|
||||||
|
unlock := lockChunkUpload(chunk.UploadID)
|
||||||
|
defer unlock()
|
||||||
|
if chunkUploadCancelled(chunk.UploadID) {
|
||||||
|
return errChunkUploadCancelled
|
||||||
|
}
|
||||||
|
if chunk.UploadID == "" || filepath.Base(chunk.UploadID) != chunk.UploadID || strings.ContainsAny(chunk.UploadID, `/\`) {
|
||||||
|
return invalidChunkUploadError("invalid upload ID")
|
||||||
|
}
|
||||||
|
if chunk.Filename == "" || filepath.Base(chunk.Filename) != chunk.Filename || strings.ContainsAny(chunk.Filename, `/\`) {
|
||||||
|
return invalidChunkUploadError("invalid filename")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(chunk.DstDir) == "" {
|
||||||
|
return invalidChunkUploadError("upload destination is required")
|
||||||
|
}
|
||||||
|
dstDir := filepath.Clean(strings.TrimSpace(chunk.DstDir))
|
||||||
|
|
||||||
|
if chunk.ChunkCount <= 0 || chunk.ChunkIndex < 0 || chunk.ChunkIndex >= chunk.ChunkCount {
|
||||||
|
return invalidChunkUploadError("invalid chunk index")
|
||||||
|
}
|
||||||
|
if chunk.FileSize <= 0 || chunk.Offset < 0 || chunk.Offset > chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("invalid upload offset")
|
||||||
|
}
|
||||||
|
chunkEnd := chunk.Offset + int64(len(chunkData))
|
||||||
|
if chunkEnd > chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("chunk exceeds file size")
|
||||||
|
}
|
||||||
|
if chunk.ChunkIndex+1 == chunk.ChunkCount {
|
||||||
|
if chunkEnd != chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("final chunk does not match file size")
|
||||||
|
}
|
||||||
|
} else if chunkEnd >= chunk.FileSize {
|
||||||
|
return invalidChunkUploadError("non-final chunk reaches file size")
|
||||||
|
}
|
||||||
|
if completed, ok := loadCompletedChunkUpload(chunk.UploadID); ok {
|
||||||
|
if completed.dstDir == dstDir && completed.filename == chunk.Filename && completed.fileSize == chunk.FileSize {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return invalidChunkUploadError("upload ID has already completed another file")
|
||||||
|
}
|
||||||
|
upload := completedChunkUpload{dstDir: dstDir, filename: chunk.Filename, fileSize: chunk.FileSize}
|
||||||
|
if err := registerActiveChunkUpload(chunk.UploadID, upload); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
mode, err := files.GetParentMode(dstDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err = os.MkdirAll(dstDir, mode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
dstDirInfo, err := os.Stat(dstDir)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !dstDirInfo.IsDir() {
|
||||||
|
return invalidChunkUploadError(fmt.Sprintf("upload destination [%s] is not a directory", dstDir))
|
||||||
|
}
|
||||||
|
|
||||||
|
dstFile := filepath.Join(dstDir, chunk.Filename)
|
||||||
|
partFile := resumableUploadPartPath(dstDir, chunk.UploadID)
|
||||||
|
if dstFile == partFile {
|
||||||
|
return invalidChunkUploadError("filename conflicts with upload temporary file")
|
||||||
|
}
|
||||||
|
fileMode := dstDirInfo.Mode().Perm()
|
||||||
|
ownerInfo := dstDirInfo
|
||||||
|
if dstInfo, statErr := os.Stat(dstFile); statErr == nil {
|
||||||
|
if !chunk.Overwrite {
|
||||||
|
if err := discardActiveChunkUpload(chunk.UploadID, partFile); err != nil {
|
||||||
|
return errors.Join(os.ErrExist, err)
|
||||||
|
}
|
||||||
|
return os.ErrExist
|
||||||
|
}
|
||||||
|
fileMode = dstInfo.Mode().Perm()
|
||||||
|
ownerInfo = dstInfo
|
||||||
|
} else if !os.IsNotExist(statErr) {
|
||||||
|
return statErr
|
||||||
|
}
|
||||||
|
|
||||||
|
part, err := os.OpenFile(partFile, os.O_CREATE|os.O_RDWR, fileMode)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
partClosed := false
|
||||||
|
defer func() {
|
||||||
|
if !partClosed {
|
||||||
|
_ = part.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
if stat, statErr := part.Stat(); statErr != nil {
|
||||||
|
return statErr
|
||||||
|
} else if chunk.Offset > stat.Size() {
|
||||||
|
return invalidChunkUploadError(fmt.Sprintf("unexpected upload offset %d, current size is %d", chunk.Offset, stat.Size()))
|
||||||
|
} else if chunk.Offset < stat.Size() && chunkEnd > stat.Size() {
|
||||||
|
if err = part.Truncate(chunk.Offset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, err = part.WriteAt(chunkData, chunk.Offset); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if chunk.ChunkIndex+1 != chunk.ChunkCount {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
partInfo, err := part.Stat()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if partInfo.Size() != chunk.FileSize {
|
||||||
|
return invalidChunkUploadError(fmt.Sprintf("uploaded file size mismatch: expected %d, got %d", chunk.FileSize, partInfo.Size()))
|
||||||
|
}
|
||||||
|
if err = part.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
partClosed = true
|
||||||
|
if err = os.Chmod(partFile, fileMode); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stat, ok := ownerInfo.Sys().(*syscall.Stat_t); ok {
|
||||||
|
if err = os.Chown(partFile, int(stat.Uid), int(stat.Gid)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if chunkUploadCancelled(chunk.UploadID) {
|
||||||
|
return errChunkUploadCancelled
|
||||||
|
}
|
||||||
|
if err = finalizeActiveChunkUpload(chunk.UploadID, partFile, dstFile, chunk.Overwrite); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
markChunkUploadCompleted(chunk.UploadID, upload)
|
||||||
|
deleteActiveChunkUpload(chunk.UploadID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int, overwrite bool) error {
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = os.RemoveAll(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
@@ -864,6 +1270,10 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if chunkCount <= 0 || chunkIndex < 0 || chunkIndex >= chunkCount {
|
||||||
|
helper.BadRequest(c, errors.New("invalid chunk index"))
|
||||||
|
return
|
||||||
|
}
|
||||||
fileOp := files.NewFileOp()
|
fileOp := files.NewFileOp()
|
||||||
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
tmpDir := path.Join(global.Dir.TmpDir, "upload")
|
||||||
if !fileOp.Stat(tmpDir) {
|
if !fileOp.Stat(tmpDir) {
|
||||||
@@ -873,8 +1283,30 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
filename := c.PostForm("filename")
|
filename := c.PostForm("filename")
|
||||||
fileDir := filepath.Join(tmpDir, filename)
|
if filename == "" || filepath.Base(filename) != filename || strings.ContainsAny(filename, `/\\`) {
|
||||||
if chunkIndex == 0 {
|
helper.BadRequest(c, errors.New("invalid filename"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadID := strings.TrimSpace(c.PostForm("uploadID"))
|
||||||
|
resumable := c.PostForm("fileSize") != "" || c.PostForm("offset") != ""
|
||||||
|
cancellable := uploadID != ""
|
||||||
|
if cancellable && (filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`)) {
|
||||||
|
helper.BadRequest(c, errors.New("invalid upload ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resumable && !cancellable {
|
||||||
|
helper.BadRequest(c, errors.New("upload ID is required"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !cancellable {
|
||||||
|
uploadID = filename
|
||||||
|
}
|
||||||
|
fileDir := filepath.Join(tmpDir, uploadID)
|
||||||
|
if cancellable && chunkUploadCancelled(uploadID) {
|
||||||
|
helper.BadRequest(c, errChunkUploadCancelled)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !resumable && chunkIndex == 0 {
|
||||||
if fileOp.Stat(fileDir) {
|
if fileOp.Stat(fileDir) {
|
||||||
_ = fileOp.DeleteDir(fileDir)
|
_ = fileOp.DeleteDir(fileDir)
|
||||||
}
|
}
|
||||||
@@ -883,28 +1315,68 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
filePath := filepath.Join(fileDir, filename)
|
filePath := filepath.Join(fileDir, filename)
|
||||||
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if err != nil {
|
if !resumable && err != nil {
|
||||||
_ = os.Remove(fileDir)
|
_ = os.RemoveAll(fileDir)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
var (
|
chunkData, err := io.ReadAll(uploadFile)
|
||||||
emptyFile *os.File
|
if err != nil {
|
||||||
chunkData []byte
|
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
)
|
return
|
||||||
|
}
|
||||||
|
if cancellable && chunkUploadCancelled(uploadID) {
|
||||||
|
err = errChunkUploadCancelled
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if resumable {
|
||||||
|
offset, parseErr := strconv.ParseInt(c.PostForm("offset"), 10, 64)
|
||||||
|
if parseErr != nil {
|
||||||
|
helper.BadRequest(c, parseErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fileSize, parseErr := strconv.ParseInt(c.PostForm("fileSize"), 10, 64)
|
||||||
|
if parseErr != nil {
|
||||||
|
helper.BadRequest(c, parseErr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
overwrite := true
|
||||||
|
if ow := c.PostForm("overwrite"); ow != "" {
|
||||||
|
overwrite, _ = strconv.ParseBool(ow)
|
||||||
|
}
|
||||||
|
err = writeResumableUploadChunk(resumableUploadChunk{
|
||||||
|
UploadID: uploadID,
|
||||||
|
Filename: filename,
|
||||||
|
DstDir: c.PostForm("path"),
|
||||||
|
ChunkIndex: chunkIndex,
|
||||||
|
ChunkCount: chunkCount,
|
||||||
|
Offset: offset,
|
||||||
|
FileSize: fileSize,
|
||||||
|
Overwrite: overwrite,
|
||||||
|
}, chunkData)
|
||||||
|
if err != nil {
|
||||||
|
uploadErr := buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err)
|
||||||
|
helper.ErrorWithDetailAndData(c, http.StatusInternalServerError, "ErrInternalServer", uploadErr, gin.H{
|
||||||
|
"retryable": isRetryableChunkUploadError(err),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if chunkIndex+1 == chunkCount {
|
||||||
|
cancelledChunkUploads.Lock()
|
||||||
|
delete(cancelledChunkUploads.ids, uploadID)
|
||||||
|
cancelledChunkUploads.Unlock()
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
emptyFile, err = os.Create(filePath)
|
emptyFile, err := os.Create(filePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer emptyFile.Close()
|
defer emptyFile.Close()
|
||||||
|
|
||||||
chunkData, err = io.ReadAll(uploadFile)
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
chunkPath := filepath.Join(fileDir, fmt.Sprintf("%s.%d", filename, chunkIndex))
|
||||||
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
err = os.WriteFile(chunkPath, chunkData, constant.DirPerm)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -922,12 +1394,54 @@ func (b *BaseApi) UploadChunkFiles(c *gin.Context) {
|
|||||||
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
helper.InternalServer(c, buserr.WithMap("ErrFileUpload", map[string]interface{}{"name": filename, "detail": err.Error()}, err))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
if cancellable {
|
||||||
|
cancelledChunkUploads.Lock()
|
||||||
|
delete(cancelledChunkUploads.ids, uploadID)
|
||||||
|
cancelledChunkUploads.Unlock()
|
||||||
|
}
|
||||||
helper.SuccessWithData(c, true)
|
helper.SuccessWithData(c, true)
|
||||||
} else {
|
} else {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// StopChunkUpload removes temporary chunks left by a cancelled upload.
|
||||||
|
func (b *BaseApi) StopChunkUpload(c *gin.Context) {
|
||||||
|
var req request.FileProcessReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
uploadID := strings.TrimSpace(req.Key)
|
||||||
|
if uploadID == "" || filepath.Base(uploadID) != uploadID || strings.ContainsAny(uploadID, `/\\`) {
|
||||||
|
helper.BadRequest(c, errors.New("invalid upload ID"))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cancelledChunkUploads.Lock()
|
||||||
|
cancelledChunkUploads.ids[uploadID] = struct{}{}
|
||||||
|
cancelledChunkUploads.Unlock()
|
||||||
|
time.AfterFunc(10*time.Minute, func() {
|
||||||
|
cancelledChunkUploads.Lock()
|
||||||
|
delete(cancelledChunkUploads.ids, uploadID)
|
||||||
|
cancelledChunkUploads.Unlock()
|
||||||
|
})
|
||||||
|
if err := os.RemoveAll(filepath.Join(global.Dir.TmpDir, "upload", uploadID)); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := removeActiveResumableUploadPart(uploadID); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func chunkUploadCancelled(uploadID string) bool {
|
||||||
|
cancelledChunkUploads.RLock()
|
||||||
|
_, ok := cancelledChunkUploads.ids[uploadID]
|
||||||
|
cancelledChunkUploads.RUnlock()
|
||||||
|
return ok
|
||||||
|
}
|
||||||
|
|
||||||
var wsUpgrade = websocket.Upgrader{
|
var wsUpgrade = websocket.Upgrader{
|
||||||
CheckOrigin: func(r *http.Request) bool {
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
return true
|
return true
|
||||||
|
|||||||
+646
-246
@@ -1,26 +1,55 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func (b *BaseApi) UpdatePanelFirewallPort(c *gin.Context) {
|
||||||
|
if !global.IsMaster {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
var request struct {
|
||||||
|
OldPort uint `json:"oldPort" validate:"required,min=1,max=65535"`
|
||||||
|
NewPort uint `json:"newPort" validate:"required,min=1,max=65535"`
|
||||||
|
}
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.UpdatePanelPort(c.Request.Context(), request.OldPort, request.NewPort); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Load firewall base info
|
// @Summary Load firewall base info
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.OperationWithName true "request"
|
// @Param request body dto.OperationWithName true "request"
|
||||||
// @Success 200 {object} dto.FirewallBaseInfo
|
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/base [post]
|
// @Router /hosts/firewall/base [post]
|
||||||
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
||||||
var req dto.OperationWithName
|
var request dto.OperationWithName
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := firewallService.LoadBaseInfo(req.Name)
|
data, err := firewallService.LoadBaseInfo(request.Name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
@@ -29,311 +58,682 @@ func (b *BaseApi) LoadFirewallBaseInfo(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, data)
|
helper.SuccessWithData(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
|
||||||
// @Summary Page firewall rules
|
|
||||||
// @Accept json
|
|
||||||
// @Param request body dto.RuleSearch true "request"
|
|
||||||
// @Success 200 {object} dto.PageResult
|
|
||||||
// @Security ApiKeyAuth
|
|
||||||
// @Security Timestamp
|
|
||||||
// @Router /hosts/firewall/search [post]
|
|
||||||
func (b *BaseApi) SearchFirewallRule(c *gin.Context) {
|
|
||||||
var req dto.RuleSearch
|
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
total, list, err := firewallService.SearchWithPage(req)
|
|
||||||
if err != nil {
|
|
||||||
helper.InternalServer(c, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
helper.SuccessWithData(c, dto.PageResult{
|
|
||||||
Items: list,
|
|
||||||
Total: total,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Operate firewall
|
// @Summary Operate firewall
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.FirewallOperation true "request"
|
// @Param request body dto.FirewallLifecycleOperation true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.FirewallLifecycleOperationResponse
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/operate [post]
|
// @Router /hosts/firewall/operate [post]
|
||||||
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
|
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] 防火墙","formatEN":"[operation] firewall"}
|
||||||
func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
func (b *BaseApi) OperateFirewall(c *gin.Context) {
|
||||||
var req dto.FirewallOperation
|
var request dto.FirewallLifecycleOperation
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := firewallService.OperateFirewall(req); err != nil {
|
result, err := firewallService.QueueFirewallOperation(request)
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.SuccessWithData(c, dto.PageResult{
|
helper.SuccessWithData(c, result)
|
||||||
Items: list,
|
|
||||||
Total: total,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Operate iptables filter rule
|
// @Summary Load forwarding base info
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.IptablesRuleOp true "request"
|
// @Success 200 {object} dto.FirewallSubsystemStatus
|
||||||
// @Success 200
|
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/rule/operate [post]
|
// @Router /hosts/firewall/forward/base [post]
|
||||||
// @x-panel-log {"bodyKeys":["operation","chain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] filter规则到 [chain]","formatEN":"[operation] filter rule to [chain]"}
|
func (b *BaseApi) LoadForwardingBaseInfo(c *gin.Context) {
|
||||||
func (b *BaseApi) OperateFilterRule(c *gin.Context) {
|
data, err := forwardingService.LoadBaseInfo()
|
||||||
var req dto.IptablesRuleOp
|
if err != nil {
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
helper.InternalServer(c, err)
|
||||||
return
|
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(request)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
helper.Success(c)
|
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Batch operate iptables filter rules
|
// @Summary Operate forwarding rules
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.IptablesBatchOperate true "request"
|
// @Param request body dto.ForwardRuleOperate true "request"
|
||||||
// @Success 200
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/rule/batch [post]
|
// @Router /hosts/firewall/forward/operate [post]
|
||||||
func (b *BaseApi) BatchOperateFilterRule(c *gin.Context) {
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新端口转发规则","formatEN":"update port forward rules"}
|
||||||
var req dto.IptablesBatchOperate
|
func (b *BaseApi) OperateForwardingRules(c *gin.Context) {
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
var request dto.ForwardRuleOperate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := iptablesService.BatchOperate(req); err != nil {
|
result, err := forwardingService.OperateRules(request)
|
||||||
|
if err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
helper.Success(c)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary Apply/Unload/Init iptables filter
|
// @Summary Enable forwarding
|
||||||
// @Accept json
|
// @Accept json
|
||||||
// @Param request body dto.IptablesOp true "request"
|
// @Param request body dto.FirewallInitializationTask true "request"
|
||||||
// @Success 200
|
// @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 ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/operate [post]
|
// @Router /hosts/firewall/filter/operate [post]
|
||||||
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 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) {
|
func (b *BaseApi) OperateFilterChain(c *gin.Context) {
|
||||||
var req dto.IptablesOp
|
var request dto.FilterChainOperation
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
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)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
helper.SuccessWithData(c, dto.FilterChainOperationResponse{})
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary List unified firewall v2 rules
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleInventory true "request"
|
||||||
|
// @Success 200 {object} dto.FirewallRuleInventoryResponse
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/search [post]
|
||||||
|
func (b *BaseApi) SearchFirewallRules(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleInventory
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
inventory, err := firewallService.Inventory(c.Request.Context(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, inventory)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Reset firewall rules
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleReset true "request"
|
||||||
|
// @Success 200 {object} dto.FirewallRuleResetResponse
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/reset [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"重置防火墙规则","formatEN":"reset firewall rules"}
|
||||||
|
func (b *BaseApi) ResetFirewallRules(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleReset
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallService.Reset(c.Request.Context(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Load one provider-native firewall object definition
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallNativeDetail true "request"
|
||||||
|
// @Success 200 {string} string
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/native/detail [post]
|
||||||
|
func (b *BaseApi) LoadFirewallNativeDetail(c *gin.Context) {
|
||||||
|
var request dto.FirewallNativeDetail
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
info, err := firewallService.LoadFirewallNativeDetail(c.Request.Context(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Adopt an external firewall rule
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleAdopt true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/adopt [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"纳管防火墙规则","formatEN":"adopt firewall rule"}
|
||||||
|
func (b *BaseApi) AdoptFirewallRule(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleAdopt
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.Adopt(c.Request.Context(), request); err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Firewall
|
// @Tags Firewall
|
||||||
// @Summary load chain status with name
|
// @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
|
// @Accept json
|
||||||
// @Param request body dto.OperationWithName true "request"
|
// @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 Preview firewall rule synchronization
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||||
|
// @Success 200 {object} dto.FirewallRuleSyncPreview
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/sync/preview [post]
|
||||||
|
func (b *BaseApi) PreviewFirewallRuleSync(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleSyncRequest
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallService.PreviewRuleSync(c.Request.Context(), c.ClientIP(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Load the currently executing firewall rule synchronization task
|
||||||
|
// @Success 200 {object} dto.FirewallRuleSyncTask
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/sync/task [get]
|
||||||
|
func (b *BaseApi) LoadFirewallRuleSyncTask(c *gin.Context) {
|
||||||
|
result, err := firewallService.CurrentRuleSyncTask()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Synchronize firewall rules to a target backend
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleSyncRequest true "request"
|
||||||
|
// @Success 200 {object} dto.FirewallRuleSyncResult
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/sync [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["subsystem","sourceProvider","targetProvider"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 [subsystem] 防火墙规则到 [targetProvider]","formatEN":"sync [subsystem] firewall rules to [targetProvider]"}
|
||||||
|
func (b *BaseApi) SyncFirewallRules(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleSyncRequest
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := firewallService.SyncRules(c.Request.Context(), c.ClientIP(), request)
|
||||||
|
if err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Queue firewall rule deletion
|
||||||
|
// @Description Deletes managed rules by UUID or unprotected before-chain rules by 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 managed unified firewall v2 rule
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleUpdate true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/update [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新防火墙规则 [uuid]","formatEN":"update firewall rule [uuid]"}
|
||||||
|
func (b *BaseApi) UpdateFirewallRule(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleUpdate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.Update(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Reorder a managed unified firewall v2 rule
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallRuleReorder true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Failure 400 {object} dto.Response
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/rules/reorder [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["uuid"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"调整防火墙规则顺序 [uuid]","formatEN":"reorder firewall rule [uuid]"}
|
||||||
|
func (b *BaseApi) ReorderFirewallRule(c *gin.Context) {
|
||||||
|
var request dto.FirewallRuleReorder
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !normalizeFirewallRuleUUID(c, &request.UUID) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := firewallService.Reorder(c.Request.Context(), c.ClientIP(), request); err != nil {
|
||||||
|
handleFirewallRuleError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeFirewallRuleUUID(c *gin.Context, value *string) bool {
|
||||||
|
if value == nil {
|
||||||
|
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
*value = strings.TrimSpace(*value)
|
||||||
|
if *value == "" {
|
||||||
|
helper.BadRequest(c, repo.ErrFirewallPersistenceInvalid)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleFirewallRuleError(c *gin.Context, err error) {
|
||||||
|
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, repo.ErrFirewallRuleRevisionConflict):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusConflict, "FW_RULE_REVISION_CONFLICT", "ErrInvalidParams", err)
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrFirewallRuleScopeChange":
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrFirewallRuleScopeChange", err)
|
||||||
|
case errors.Is(err, filter.ErrUnsupportedScope), errors.Is(err, filter.ErrInvalidScope),
|
||||||
|
errors.Is(err, filter.ErrProviderUnavailable), errors.Is(err, filter.ErrAdapterUnavailable):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_SCOPE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
|
case errors.Is(err, filter.ErrInvalidRule), errors.Is(err, filter.ErrRuleOperation), errors.Is(err, filter.ErrRuleConflict),
|
||||||
|
errors.Is(err, repo.ErrFirewallPersistenceInvalid):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusBadRequest, "FW_RULE_UNSUPPORTED", "ErrInvalidParams", err)
|
||||||
|
case isBusinessError && businessErr.Msg == "ErrInvalidParams":
|
||||||
|
c.JSON(http.StatusOK, dto.Response{Code: http.StatusBadRequest, ErrorCode: "FW_RULE_UNSUPPORTED", Message: err.Error()})
|
||||||
|
c.Abort()
|
||||||
|
case errors.Is(err, filter.ErrVerificationFailed):
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_VERIFY_FAILED", "ErrInternalServer", err)
|
||||||
|
default:
|
||||||
|
helper.ErrorWithBusinessCode(c, http.StatusInternalServerError, "FW_APPLY_FAILED", "ErrInternalServer", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Load firewall settings
|
||||||
|
// @Success 200 {object} dto.FirewallSettings
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/settings [get]
|
||||||
|
func (b *BaseApi) LoadFirewallSettings(c *gin.Context) {
|
||||||
|
data, err := firewallSettingService.Load(c.Request.Context())
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Create firewall port whitelist rules
|
||||||
|
// @Description Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing rules are not removed.
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.FirewallPortWhitelistCreate true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/firewall/filter/chain/status [post]
|
// @Router /hosts/firewall/settings/whitelist [post]
|
||||||
func (b *BaseApi) LoadChainStatus(c *gin.Context) {
|
// @x-panel-log {"bodyKeys":["rule"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建防火墙端口白名单","formatEN":"create firewall port whitelist"}
|
||||||
var req dto.OperationWithName
|
func (b *BaseApi) CreateFirewallPortWhitelist(c *gin.Context) {
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
var request dto.FirewallPortWhitelistCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
return
|
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 only. Missing rules are added on startup, restart, initialization, or synchronization; 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 Saves whitelist configuration only. Missing rules are added on startup, restart, initialization, or synchronization; existing 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 Sync Docker port guard rules
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/docker/sync [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"同步 Docker 端口防护规则","formatEN":"sync Docker port guard rules"}
|
||||||
|
func (b *BaseApi) SyncDockerPortGuard(c *gin.Context) {
|
||||||
|
if err := dockerPortGuardService.Reconcile(c.Request.Context()); err != nil {
|
||||||
|
handleDockerPortGuardError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Operate Docker port guard
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.DockerPortGuardOperation true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/docker/operate [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Docker 端口防护","formatEN":"[operation] Docker port guard"}
|
||||||
|
func (b *BaseApi) OperateDockerPortGuard(c *gin.Context) {
|
||||||
|
var request dto.DockerPortGuardOperation
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if request.Operation == "initialize" {
|
||||||
|
result, err := dockerPortGuardService.QueueInitialization(request)
|
||||||
|
if err != nil {
|
||||||
|
handleDockerPortGuardError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := dockerPortGuardService.Operate(c.Request.Context(), request); err != nil {
|
||||||
|
handleDockerPortGuardError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Delete Docker port guard policies
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.DockerPortGuardPolicyBatchDelete true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/docker/policies/delete/batch [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["uuids"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 Docker 端口防护策略 [uuids]","formatEN":"delete Docker port guard policies [uuids]"}
|
||||||
|
func (b *BaseApi) DeleteDockerPortGuardPolicies(c *gin.Context) {
|
||||||
|
var request dto.DockerPortGuardPolicyBatchDelete
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := dockerPortGuardService.DeletePolicies(request)
|
||||||
|
if err != nil {
|
||||||
|
handleDockerPortGuardError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Firewall
|
||||||
|
// @Summary Batch upsert Docker port guard policies
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.DockerPortGuardPolicyBatch true "request"
|
||||||
|
// @Success 200 {object} dto.FilterChainOperationResponse
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/firewall/docker/policies/batch [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"批量更新 Docker 端口防护策略","formatEN":"batch update Docker port guard policies"}
|
||||||
|
func (b *BaseApi) UpsertDockerPortGuardPolicies(c *gin.Context) {
|
||||||
|
var request dto.DockerPortGuardPolicyBatch
|
||||||
|
if err := helper.CheckBindAndValidate(&request, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := dockerPortGuardService.UpsertPolicies(request)
|
||||||
|
if err != nil {
|
||||||
|
handleDockerPortGuardError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleDockerPortGuardError(c *gin.Context, err error) {
|
||||||
|
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)
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-16
@@ -3,9 +3,8 @@ package v2
|
|||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/accelerator"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,27 +16,20 @@ import (
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /ai/gpu/load [get]
|
// @Router /ai/gpu/load [get]
|
||||||
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
|
||||||
ok, client := gpu.New()
|
ok, client := accelerator.New()
|
||||||
if ok {
|
if ok {
|
||||||
info, err := client.LoadGpuInfo()
|
snapshot, err := client.Collect(c.Request.Context())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, info)
|
if warning := snapshot.Warning(); warning != nil {
|
||||||
return
|
global.LOG.Warnf("load realtime accelerator data partially failed, err: %v", warning)
|
||||||
}
|
|
||||||
xpuOK, xpuClient := xpu.New()
|
|
||||||
if xpuOK {
|
|
||||||
info, err := xpuClient.LoadGpuInfo()
|
|
||||||
if err != nil {
|
|
||||||
helper.BadRequest(c, err)
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, info)
|
helper.SuccessWithData(c, &snapshot.Info)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
helper.SuccessWithData(c, &common.GpuInfo{})
|
helper.SuccessWithData(c, &accelerator.Info{})
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags AI
|
// @Tags AI
|
||||||
|
|||||||
@@ -30,6 +30,26 @@ func ErrorWithDetail(ctx *gin.Context, code int, msgKey string, err error) {
|
|||||||
ctx.Abort()
|
ctx.Abort()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ErrorWithBusinessCode(ctx *gin.Context, code int, businessCode, msgKey string, err error) {
|
||||||
|
res := dto.Response{
|
||||||
|
Code: code,
|
||||||
|
ErrorCode: businessCode,
|
||||||
|
Message: i18n.GetMsgWithDetail(msgKey, err.Error()),
|
||||||
|
}
|
||||||
|
ctx.JSON(http.StatusOK, res)
|
||||||
|
ctx.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func ErrorWithDetailAndData(ctx *gin.Context, code int, msgKey string, err error, data interface{}) {
|
||||||
|
res := dto.Response{
|
||||||
|
Code: code,
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
res.Message = i18n.GetMsgWithDetail(msgKey, err.Error())
|
||||||
|
ctx.JSON(http.StatusOK, res)
|
||||||
|
ctx.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
func InternalServer(ctx *gin.Context, err error) {
|
func InternalServer(ctx *gin.Context, err error) {
|
||||||
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
ErrorWithDetail(ctx, http.StatusInternalServerError, "ErrInternalServer", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package v2
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -20,3 +21,57 @@ func (b *BaseApi) GetSystemFiles(c *gin.Context) {
|
|||||||
|
|
||||||
helper.SuccessWithData(c, data)
|
helper.SuccessWithData(c, data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Logs
|
||||||
|
// @Summary Get host system log status
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} dto.SystemLogStatus
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /logs/system/status [get]
|
||||||
|
func (b *BaseApi) GetSystemLogStatus(c *gin.Context) {
|
||||||
|
data, err := logService.GetSystemLogStatus()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Logs
|
||||||
|
// @Summary Read host logs
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body dto.SystemLogReq true "request"
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {object} dto.SystemLogRes
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /logs/system/read [post]
|
||||||
|
func (b *BaseApi) ReadSystemLog(c *gin.Context) {
|
||||||
|
var req dto.SystemLogReq
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
data, err := logService.ReadSystemLog(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags Logs
|
||||||
|
// @Summary List running host services
|
||||||
|
// @Produce json
|
||||||
|
// @Success 200 {array} string
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /logs/system/services [get]
|
||||||
|
func (b *BaseApi) ListRunningServices(c *gin.Context) {
|
||||||
|
data, err := logService.ListRunningServices()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|||||||
@@ -23,6 +23,27 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, list)
|
helper.SuccessWithData(c, list)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags McpServer
|
||||||
|
// @Summary Load mcp server detail
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.McpServerDetail true "request"
|
||||||
|
// @Success 200 {object} response.McpServerDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/mcp/server/detail [post]
|
||||||
|
func (b *BaseApi) LoadMcpServerDetail(c *gin.Context) {
|
||||||
|
var req request.McpServerDetail
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := mcpServerService.Detail(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, res)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags McpServer
|
// @Tags McpServer
|
||||||
// @Summary Create mcp server
|
// @Summary Create mcp server
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -107,6 +128,48 @@ func (b *BaseApi) OperateMcpServer(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags McpServer
|
||||||
|
// @Summary Sync mcp server status
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.McpServerStatusSync true "request"
|
||||||
|
// @Success 200 {array} response.McpServerStatusDTO
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/mcp/server/status/sync [post]
|
||||||
|
func (b *BaseApi) SyncMcpServerStatus(c *gin.Context) {
|
||||||
|
var req request.McpServerStatusSync
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := mcpServerService.SyncStatus(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, res)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags McpServer
|
||||||
|
// @Summary Test mcp server connection
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.McpServerConnectionTest true "request"
|
||||||
|
// @Success 200 {object} response.McpServerConnectionTestRes
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /ai/mcp/server/connection/test [post]
|
||||||
|
func (b *BaseApi) TestMcpServerConnection(c *gin.Context) {
|
||||||
|
var req request.McpServerConnectionTest
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res, err := mcpServerService.TestConnection(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, res)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags McpServer
|
// @Tags McpServer
|
||||||
// @Summary Bind Domain for mcp server
|
// @Summary Bind Domain for mcp server
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
+10
-23
@@ -1,13 +1,9 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sort"
|
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -32,14 +28,19 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
// @Summary Clean monitor data
|
// @Summary Clean host or GPU monitor data
|
||||||
|
// @Param request body dto.MonitorClean true "request"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/clean [post]
|
// @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) {
|
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)
|
helper.InternalServer(c, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -91,14 +92,7 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/netoptions [get]
|
// @Router /hosts/monitor/netoptions [get]
|
||||||
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
||||||
netStat, _ := net.IOCounters(true)
|
helper.SuccessWithData(c, monitorService.LoadNetworkOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range netStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Monitor
|
// @Tags Monitor
|
||||||
@@ -108,12 +102,5 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
|
|||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/monitor/iooptions [get]
|
// @Router /hosts/monitor/iooptions [get]
|
||||||
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
func (b *BaseApi) GetIOOptions(c *gin.Context) {
|
||||||
diskStat, _ := disk.IOCounters()
|
helper.SuccessWithData(c, monitorService.LoadIOOptions())
|
||||||
var options []string
|
|
||||||
options = append(options, "all")
|
|
||||||
for _, net := range diskStat {
|
|
||||||
options = append(options, net.Name)
|
|
||||||
}
|
|
||||||
sort.Strings(options)
|
|
||||||
helper.SuccessWithData(c, options)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(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)
|
||||||
|
}
|
||||||
@@ -169,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
|
|||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /runtimes/operate [post]
|
// @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) {
|
func (b *BaseApi) OperateRuntime(c *gin.Context) {
|
||||||
var req request.RuntimeOperate
|
var req request.RuntimeOperate
|
||||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
|||||||
@@ -0,0 +1,63 @@
|
|||||||
|
package v2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// @Tags RuntimeDiagnostics
|
||||||
|
// @Summary Load runtime diagnostics summary
|
||||||
|
// @Success 200 {object} dto.RuntimeDiagnosticsSummary
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/diagnostics/summary [get]
|
||||||
|
func (b *BaseApi) LoadRuntimeDiagnosticsSummary(c *gin.Context) {
|
||||||
|
data, err := runtimeDiagnosticsService.Summary()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithData(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags RuntimeDiagnostics
|
||||||
|
// @Summary Load grouped goroutine snapshot
|
||||||
|
// @Success 200 {object} dto.RuntimeGoroutineSnapshot
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/diagnostics/goroutines [get]
|
||||||
|
func (b *BaseApi) LoadRuntimeGoroutines(c *gin.Context) {
|
||||||
|
data, err := runtimeDiagnosticsService.Goroutines()
|
||||||
|
if err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.SuccessWithDataGzipped(c, data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// @Tags RuntimeDiagnostics
|
||||||
|
// @Summary Capture runtime profile
|
||||||
|
// @Param request body dto.RuntimeProfileCreate true "request"
|
||||||
|
// @Success 200 {file} file
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/diagnostics/profiles [post]
|
||||||
|
func (b *BaseApi) CreateRuntimeProfile(c *gin.Context) {
|
||||||
|
var req dto.RuntimeProfileCreate
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
profile, err := runtimeDiagnosticsService.CreateProfile(req)
|
||||||
|
if err != nil {
|
||||||
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer os.Remove(profile.Path)
|
||||||
|
c.Header("Content-Disposition", `attachment; filename="`+profile.Name+`"`)
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.File(profile.Path)
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
@@ -83,6 +83,7 @@ func (b *BaseApi) CreateRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.CreateRootCert(req); err != nil {
|
if err := sshService.CreateRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -107,6 +108,7 @@ func (b *BaseApi) EditRootCert(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
if err := loadCertAfterDecrypt(&req); err != nil {
|
if err := loadCertAfterDecrypt(&req); err != nil {
|
||||||
helper.BadRequest(c, err)
|
helper.BadRequest(c, err)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
if err := sshService.EditRootCert(req); err != nil {
|
if err := sshService.EditRootCert(req); err != nil {
|
||||||
helper.InternalServer(c, err)
|
helper.InternalServer(c, err)
|
||||||
@@ -224,6 +226,21 @@ func (b *BaseApi) ExportSSHLogs(c *gin.Context) {
|
|||||||
helper.SuccessWithData(c, tmpFile)
|
helper.SuccessWithData(c, tmpFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags SSH
|
||||||
|
// @Summary Clean host SSH logs
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /hosts/ssh/log/clean [post]
|
||||||
|
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空 SSH 登录日志","formatEN":"clean SSH login logs"}
|
||||||
|
func (b *BaseApi) CleanSSHLogs(c *gin.Context) {
|
||||||
|
if err := sshService.CleanLog(); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags SSH
|
// @Tags SSH
|
||||||
// @Summary Load host SSH conf
|
// @Summary Load host SSH conf
|
||||||
// @Accept json
|
// @Accept json
|
||||||
|
|||||||
+153
-31
@@ -1,11 +1,14 @@
|
|||||||
package v2
|
package v2
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||||
@@ -19,29 +22,35 @@ import (
|
|||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"github.com/gorilla/websocket"
|
"github.com/gorilla/websocket"
|
||||||
"github.com/pkg/errors"
|
"github.com/pkg/errors"
|
||||||
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// @Tags Terminal
|
// @Tags Terminal
|
||||||
// @Summary Ws local terminal
|
// @Summary Ws local terminal
|
||||||
// @Param command query string false "command"
|
// @Param command query string false "command"
|
||||||
|
// @Param session query string false "session id to reattach"
|
||||||
|
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/terminal/local [get]
|
// @Router /hosts/terminal/local [get]
|
||||||
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
|
||||||
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
|
b.runSSHSession(c, "local", loadLocalConn, c.DefaultQuery("command", ""))
|
||||||
}
|
}
|
||||||
|
|
||||||
// @Tags Terminal
|
// @Tags Terminal
|
||||||
// @Summary Ws host SSH
|
// @Summary Ws host SSH
|
||||||
// @Param id query integer false "id"
|
// @Param id query integer false "id"
|
||||||
// @Param command query string false "command"
|
// @Param command query string false "command"
|
||||||
|
// @Param session query string false "session id to reattach"
|
||||||
|
// @Param title query string false "session title shown in the session list"
|
||||||
|
// @Param terminalPersistent query boolean false "allow recovery after an unexpected disconnect"
|
||||||
// @Success 200
|
// @Success 200
|
||||||
// @Security ApiKeyAuth
|
// @Security ApiKeyAuth
|
||||||
// @Security Timestamp
|
// @Security Timestamp
|
||||||
// @Router /hosts/terminal/ssh [get]
|
// @Router /hosts/terminal/ssh [get]
|
||||||
func (b *BaseApi) WsHostSSH(c *gin.Context) {
|
func (b *BaseApi) WsHostSSH(c *gin.Context) {
|
||||||
b.runSSHSession(c, func() (*ssh.SSHClient, error) {
|
b.runSSHSession(c, "ssh", func() (*ssh.SSHClient, error) {
|
||||||
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
|
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
|
||||||
if hostID <= 0 {
|
if hostID <= 0 {
|
||||||
return nil, errors.New("missing host id")
|
return nil, errors.New("missing host id")
|
||||||
@@ -65,26 +74,33 @@ func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer wsConn.Close()
|
defer wsConn.Close()
|
||||||
|
identity, ok := loadTerminalIdentity(c)
|
||||||
slave, err := loadContainerTerminalCommand(c)
|
if !ok {
|
||||||
if wshandleError(wsConn, err) {
|
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||||
return
|
|
||||||
}
|
|
||||||
defer slave.Close()
|
|
||||||
|
|
||||||
tty, err := terminal.NewLocalWsSession(cols, rows, wsConn, slave, false)
|
|
||||||
if wshandleError(wsConn, err) {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
quitChan := make(chan bool, 3)
|
opts := terminal.SessionOptions{
|
||||||
tty.Start(quitChan)
|
Identity: identity,
|
||||||
go slave.Wait(quitChan)
|
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
|
func containerTerminalTarget(c *gin.Context) string {
|
||||||
|
query := c.Request.URL.Query()
|
||||||
global.LOG.Info("websocket finished")
|
for _, key := range []string{"cols", "rows", "session", "terminalRevalidate"} {
|
||||||
closeTerminalConn(wsConn)
|
query.Del(key)
|
||||||
|
}
|
||||||
|
sum := sha256.Sum256([]byte(query.Encode()))
|
||||||
|
return hex.EncodeToString(sum[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
||||||
@@ -115,32 +131,138 @@ func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
|
|||||||
return wsConn, cols, rows, true
|
return wsConn, cols, rows, true
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *BaseApi) runSSHSession(c *gin.Context, 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)
|
wsConn, cols, rows, ok := prepareTerminalSession(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer wsConn.Close()
|
defer wsConn.Close()
|
||||||
|
identity, ok := loadTerminalIdentity(c)
|
||||||
client, clientErr := connect()
|
if !ok {
|
||||||
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
|
_ = wshandleError(wsConn, errors.New("missing terminal identity"))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer client.Close()
|
|
||||||
|
|
||||||
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
|
hostID := 0
|
||||||
if wshandleError(wsConn, err) {
|
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
|
return
|
||||||
}
|
}
|
||||||
defer sws.Close()
|
helper.SuccessWithData(c, terminal.List(identity))
|
||||||
|
}
|
||||||
|
|
||||||
quitChan := make(chan bool, 3)
|
// @Tags Terminal
|
||||||
sws.Start(quitChan)
|
// @Summary Close a terminal session
|
||||||
go sws.Wait(quitChan)
|
// @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) {
|
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})
|
||||||
|
}
|
||||||
@@ -210,6 +210,27 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
|
|||||||
helper.Success(c)
|
helper.Success(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// @Tags Website SSL
|
||||||
|
// @Summary Push ssl to nodes
|
||||||
|
// @Accept json
|
||||||
|
// @Param request body request.WebsiteSSLPush true "request"
|
||||||
|
// @Success 200
|
||||||
|
// @Security ApiKeyAuth
|
||||||
|
// @Security Timestamp
|
||||||
|
// @Router /websites/ssl/push [post]
|
||||||
|
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
|
||||||
|
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
|
||||||
|
var req request.WebsiteSSLPush
|
||||||
|
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := websiteSSLService.PushToNode(req); err != nil {
|
||||||
|
helper.InternalServer(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
helper.Success(c)
|
||||||
|
}
|
||||||
|
|
||||||
// @Tags Website SSL
|
// @Tags Website SSL
|
||||||
// @Summary Upload ssl
|
// @Summary Upload ssl
|
||||||
// @Accept json
|
// @Accept json
|
||||||
@@ -248,6 +269,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
|
|||||||
var req request.WebsiteSSLFileUpload
|
var req request.WebsiteSSLFileUpload
|
||||||
|
|
||||||
req.Description = c.PostForm("description")
|
req.Description = c.PostForm("description")
|
||||||
|
req.Nodes = c.PostForm("nodes")
|
||||||
|
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
|
||||||
sslID := c.PostForm("sslID")
|
sslID := c.PostForm("sslID")
|
||||||
if sslID != "" {
|
if sslID != "" {
|
||||||
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
|
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
|
||||||
@@ -283,6 +306,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
|
|||||||
Certificate: string(certificateContent),
|
Certificate: string(certificateContent),
|
||||||
Description: req.Description,
|
Description: req.Description,
|
||||||
SSLID: uint(req.SSLID),
|
SSLID: uint(req.SSLID),
|
||||||
|
PushNode: req.PushNode,
|
||||||
|
Nodes: req.Nodes,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := websiteSSLService.Upload(uploadReq); err != nil {
|
if err := websiteSSLService.Upload(uploadReq); err != nil {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
+159
-77
@@ -3,28 +3,30 @@ package dto
|
|||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
type AgentCreateReq struct {
|
type AgentCreateReq struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
AppVersion string `json:"appVersion" validate:"required"`
|
AppVersion string `json:"appVersion" validate:"required"`
|
||||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||||
BridgePort int `json:"bridgePort"`
|
BridgePort int `json:"bridgePort"`
|
||||||
AllowedOrigins []string `json:"allowedOrigins"`
|
AllowedOrigins []string `json:"allowedOrigins"`
|
||||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
TaskID string `json:"taskID"`
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
Advanced bool `json:"advanced"`
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
ContainerName string `json:"containerName"`
|
TaskID string `json:"taskID"`
|
||||||
AllowPort bool `json:"allowPort"`
|
Advanced bool `json:"advanced"`
|
||||||
SpecifyIP string `json:"specifyIP"`
|
ContainerName string `json:"containerName"`
|
||||||
RestartPolicy string `json:"restartPolicy"`
|
AllowPort bool `json:"allowPort"`
|
||||||
CpuQuota float64 `json:"cpuQuota"`
|
SpecifyIP string `json:"specifyIP"`
|
||||||
MemoryLimit float64 `json:"memoryLimit"`
|
RestartPolicy string `json:"restartPolicy"`
|
||||||
MemoryUnit string `json:"memoryUnit"`
|
CpuQuota float64 `json:"cpuQuota"`
|
||||||
PullImage bool `json:"pullImage"`
|
MemoryLimit float64 `json:"memoryLimit"`
|
||||||
EditCompose bool `json:"editCompose"`
|
MemoryUnit string `json:"memoryUnit"`
|
||||||
DockerCompose string `json:"dockerCompose"`
|
PullImage bool `json:"pullImage"`
|
||||||
|
EditCompose bool `json:"editCompose"`
|
||||||
|
DockerCompose string `json:"dockerCompose"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentBatchInstallReq struct {
|
type AgentBatchInstallReq struct {
|
||||||
@@ -38,6 +40,8 @@ type AgentBatchInstallReq struct {
|
|||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
Advanced bool `json:"advanced"`
|
Advanced bool `json:"advanced"`
|
||||||
ContainerName string `json:"containerName"`
|
ContainerName string `json:"containerName"`
|
||||||
@@ -114,11 +118,11 @@ type AgentItem struct {
|
|||||||
ProviderName string `json:"providerName"`
|
ProviderName string `json:"providerName"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
ContextWindow int `json:"contextWindow"`
|
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
Token string `json:"token"`
|
Token string `json:"token"`
|
||||||
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
AppInstallID uint `json:"appInstallId"`
|
AppInstallID uint `json:"appInstallId"`
|
||||||
@@ -158,16 +162,25 @@ type AgentWebsiteBindReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentModelConfigUpdateReq struct {
|
type AgentModelConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
Model string `json:"model" validate:"required"`
|
Model string `json:"model" validate:"required"`
|
||||||
Fallbacks []string `json:"fallbacks"`
|
Fallbacks []string `json:"fallbacks"`
|
||||||
|
Metadata []AgentModelMetadata `json:"metadata" validate:"dive"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentModelConfig struct {
|
type AgentModelConfig struct {
|
||||||
AccountID uint `json:"accountId"`
|
AccountID uint `json:"accountId"`
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
Fallbacks []string `json:"fallbacks"`
|
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 {
|
type AgentHermesChatSessionItem struct {
|
||||||
@@ -286,19 +299,22 @@ type AgentOverviewSnapshot struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModel struct {
|
type AgentAccountModel struct {
|
||||||
RecordID uint `json:"recordId"`
|
RecordID uint `json:"recordId"`
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Reasoning bool `json:"reasoning"`
|
|
||||||
Input []string `json:"input"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountModelReq struct {
|
type AgentAccountModelReq struct {
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentAccountModelDiscoverReq struct {
|
||||||
|
Provider string `json:"provider" validate:"required"`
|
||||||
|
BaseURL string `json:"baseURL" validate:"required"`
|
||||||
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
|
APIType string `json:"apiType" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentAccountModelCreateReq struct {
|
type AgentAccountModelCreateReq struct {
|
||||||
AccountID uint `json:"accountId" validate:"required"`
|
AccountID uint `json:"accountId" validate:"required"`
|
||||||
Model AgentAccountModel `json:"model" validate:"required"`
|
Model AgentAccountModel `json:"model" validate:"required"`
|
||||||
@@ -315,31 +331,40 @@ type AgentAccountModelDeleteReq struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountCreateReq struct {
|
type AgentAccountCreateReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
Remark string `json:"remark"`
|
AuthMode string `json:"authMode"`
|
||||||
|
VerifyModel string `json:"verifyModel"`
|
||||||
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountUpdateReq struct {
|
type AgentAccountUpdateReq struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
APIType string `json:"apiType" validate:"required"`
|
APIType string `json:"apiType" validate:"required"`
|
||||||
Remark string `json:"remark"`
|
AuthMode string `json:"authMode"`
|
||||||
SyncAgents bool `json:"syncAgents"`
|
VerifyModel string `json:"verifyModel"`
|
||||||
|
ValidateAvailability *bool `json:"validateAvailability"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
SyncAgents bool `json:"syncAgents"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountVerifyReq struct {
|
type AgentAccountVerifyReq struct {
|
||||||
Provider string `json:"provider" validate:"required"`
|
Provider string `json:"provider" validate:"required"`
|
||||||
APIKey string `json:"apiKey" validate:"required"`
|
APIKey string `json:"apiKey" validate:"required"`
|
||||||
BaseURL string `json:"baseURL"`
|
BaseURL string `json:"baseURL"`
|
||||||
|
APIType string `json:"apiType" validate:"required"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
Model string `json:"model"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentAccountDeleteReq struct {
|
type AgentAccountDeleteReq struct {
|
||||||
@@ -349,6 +374,8 @@ type AgentAccountDeleteReq struct {
|
|||||||
type AgentAccountSearch struct {
|
type AgentAccountSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
|
APIType string `json:"apiType"`
|
||||||
|
TextOnly bool `json:"textOnly"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,26 +394,36 @@ type AgentAccountInfo struct {
|
|||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Models []AgentAccountModel `json:"models"`
|
Models []AgentAccountModel `json:"models"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
VerifyModel string `json:"verifyModel"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProviderModelInfo struct {
|
type ProviderModelInfo struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
}
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Reasoning bool `json:"reasoning"`
|
type ProviderAPIInfo struct {
|
||||||
Input []string `json:"input"`
|
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 {
|
type ProviderInfo struct {
|
||||||
Sort uint `json:"-"`
|
Sort uint `json:"-"`
|
||||||
Provider string `json:"provider"`
|
Provider string `json:"provider"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
Models []ProviderModelInfo `json:"models"`
|
DefaultAPIType string `json:"defaultApiType"`
|
||||||
|
APITypes []ProviderAPIInfo `json:"apiTypes"`
|
||||||
|
Models []ProviderModelInfo `json:"models"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentFeishuConfigReq struct {
|
type AgentFeishuConfigReq struct {
|
||||||
@@ -407,11 +444,6 @@ type AgentFeishuConfigUpdateReq struct {
|
|||||||
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentFeishuPairingApproveReq struct {
|
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
|
||||||
PairingCode string `json:"pairingCode" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AgentFeishuConfig struct {
|
type AgentFeishuConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
ThreadSession bool `json:"threadSession"`
|
ThreadSession bool `json:"threadSession"`
|
||||||
@@ -581,6 +613,52 @@ type AgentPluginStatus struct {
|
|||||||
Upgradable bool `json:"upgradable"`
|
Upgradable bool `json:"upgradable"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AgentPluginsReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Keyword string `json:"keyword" validate:"required,max=100"`
|
||||||
|
Limit int `json:"limit" validate:"omitempty,min=1,max=100"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginMarketInstallReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
Package string `json:"package" validate:"required,max=200"`
|
||||||
|
Version string `json:"version" validate:"required,max=100"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginOperateReq struct {
|
||||||
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
|
PluginID string `json:"pluginId" validate:"required,max=200"`
|
||||||
|
Operate string `json:"operate" validate:"required,oneof=enable disable update uninstall"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AgentPluginSearchItem struct {
|
||||||
|
Package string `json:"package"`
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Official bool `json:"official"`
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
}
|
||||||
|
|
||||||
type AgentDiscordConfigUpdateReq struct {
|
type AgentDiscordConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
@@ -660,16 +738,20 @@ type AgentSecurityConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentOtherConfigUpdateReq struct {
|
type AgentOtherConfigUpdateReq struct {
|
||||||
AgentID uint `json:"agentId" validate:"required"`
|
AgentID uint `json:"agentId" validate:"required"`
|
||||||
UserTimezone string `json:"userTimezone" validate:"required"`
|
UserTimezone string `json:"userTimezone"`
|
||||||
BrowserEnabled bool `json:"browserEnabled"`
|
BrowserEnabled bool `json:"browserEnabled"`
|
||||||
NPMRegistry string `json:"npmRegistry" validate:"required"`
|
NPMRegistry string `json:"npmRegistry"`
|
||||||
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentOtherConfig struct {
|
type AgentOtherConfig struct {
|
||||||
UserTimezone string `json:"userTimezone"`
|
UserTimezone string `json:"userTimezone"`
|
||||||
BrowserEnabled bool `json:"browserEnabled"`
|
BrowserEnabled bool `json:"browserEnabled"`
|
||||||
NPMRegistry string `json:"npmRegistry"`
|
NPMRegistry string `json:"npmRegistry"`
|
||||||
|
DashboardUsername string `json:"dashboardUsername"`
|
||||||
|
DashboardPassword string `json:"dashboardPassword"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AgentConfigFileReq struct {
|
type AgentConfigFileReq struct {
|
||||||
|
|||||||
+35
-18
@@ -21,6 +21,7 @@ type AlertBase struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PushAlert struct {
|
type PushAlert struct {
|
||||||
|
Result string `json:"result,omitempty"`
|
||||||
TaskName string `json:"taskName"`
|
TaskName string `json:"taskName"`
|
||||||
AlertType string `json:"alertType"`
|
AlertType string `json:"alertType"`
|
||||||
EntryID uint `json:"entryID"`
|
EntryID uint `json:"entryID"`
|
||||||
@@ -53,6 +54,7 @@ type AlertDTO struct {
|
|||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
|
TaskName string `json:"taskName,omitempty"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
SendCount uint `json:"sendCount"`
|
SendCount uint `json:"sendCount"`
|
||||||
AdvancedParams string `json:"advancedParams"`
|
AdvancedParams string `json:"advancedParams"`
|
||||||
@@ -113,8 +115,10 @@ type DiskDTO struct {
|
|||||||
|
|
||||||
type AlertLogSearch struct {
|
type AlertLogSearch struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Count uint `json:"count"`
|
Count uint `json:"count"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertLogDTO struct {
|
type AlertLogDTO struct {
|
||||||
@@ -149,16 +153,25 @@ type AlertLog struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertDetail struct {
|
type AlertDetail struct {
|
||||||
LicenseId string `json:"licenseId"`
|
LicenseId string `json:"licenseId"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SubType string `json:"subType"`
|
SubType string `json:"subType"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Method string `json:"method"`
|
Method string `json:"method"`
|
||||||
LicenseCode string `json:"licenseCode"`
|
LicenseCode string `json:"licenseCode"`
|
||||||
DeviceId string `json:"deviceId"`
|
DeviceId string `json:"deviceId"`
|
||||||
Project string `json:"project"`
|
Project string `json:"project"`
|
||||||
Params []Param `json:"params"`
|
Params []Param `json:"params"`
|
||||||
Phone string `json:"phone"`
|
Phone string `json:"phone"`
|
||||||
|
Task *AlertTaskMetadata `json:"task,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertTaskMetadata struct {
|
||||||
|
AlertID uint `json:"alertId"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Quota string `json:"quota"`
|
||||||
|
QuotaType string `json:"quotaType"`
|
||||||
|
Method string `json:"method"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertRule struct {
|
type AlertRule struct {
|
||||||
@@ -293,15 +306,19 @@ type OfflineQueryRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AlertConfigUpdate struct {
|
type AlertConfigUpdate struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Title string `json:"title"`
|
Title string `json:"title"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Config string `json:"config"`
|
Config string `json:"config"`
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
|
Revision *time.Time `json:"revision"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertConfigTest struct {
|
type AlertConfigTest struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Config string `json:"config"`
|
||||||
Host string `json:"host"`
|
Host string `json:"host"`
|
||||||
Port int `json:"port"`
|
Port int `json:"port"`
|
||||||
Sender string `json:"sender"`
|
Sender string `json:"sender"`
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@ package dto
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
)
|
)
|
||||||
@@ -132,6 +133,8 @@ type Locale struct {
|
|||||||
Ko string `json:"ko"`
|
Ko string `json:"ko"`
|
||||||
Tr string `json:"tr"`
|
Tr string `json:"tr"`
|
||||||
Es string `json:"es-es" yaml:"es-es"`
|
Es string `json:"es-es" yaml:"es-es"`
|
||||||
|
Fa string `json:"fa"`
|
||||||
|
Lo string `json:"lo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppForm struct {
|
type AppForm struct {
|
||||||
|
|||||||
@@ -65,14 +65,15 @@ type UploadForRecover struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CommonBackup struct {
|
type CommonBackup struct {
|
||||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
|
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
DetailName string `json:"detailName"`
|
DetailName string `json:"detailName"`
|
||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
StopBefore bool `json:"stopBefore"`
|
IsImmediate bool `json:"isImmediate"`
|
||||||
TaskID string `json:"taskID"`
|
StopBefore bool `json:"stopBefore"`
|
||||||
FileName string `json:"fileName"`
|
TaskID string `json:"taskID"`
|
||||||
Args []string `json:"args"`
|
FileName string `json:"fileName"`
|
||||||
|
Args []string `json:"args"`
|
||||||
|
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ package dto
|
|||||||
|
|
||||||
type SearchWithPage struct {
|
type SearchWithPage struct {
|
||||||
PageInfo
|
PageInfo
|
||||||
Info string `json:"info"`
|
Info string `json:"info"`
|
||||||
|
ExcludeAppStore bool `json:"excludeAppStore"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SearchPageWithType struct {
|
type SearchPageWithType struct {
|
||||||
|
|||||||
@@ -6,9 +6,10 @@ type PageResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Response struct {
|
type Response struct {
|
||||||
Code int `json:"code"`
|
Code int `json:"code"`
|
||||||
Message string `json:"message"`
|
ErrorCode string `json:"errorCode,omitempty"`
|
||||||
Data interface{} `json:"data"`
|
Message string `json:"message"`
|
||||||
|
Data interface{} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
|
|||||||
@@ -134,10 +134,15 @@ type ExtraHost struct {
|
|||||||
IP string `json:"ip"`
|
IP string `json:"ip"`
|
||||||
}
|
}
|
||||||
type ContainerNetwork struct {
|
type ContainerNetwork struct {
|
||||||
Network string `json:"network"`
|
Network string `json:"network"`
|
||||||
Ipv4 string `json:"ipv4"`
|
Ipv4 string `json:"ipv4"`
|
||||||
Ipv6 string `json:"ipv6"`
|
Ipv6 string `json:"ipv6"`
|
||||||
MacAddr string `json:"macAddr"`
|
MacAddr string `json:"macAddr"`
|
||||||
|
Links []string `json:"links"`
|
||||||
|
Aliases []string `json:"aliases"`
|
||||||
|
DriverOpts map[string]string `json:"driverOpts"`
|
||||||
|
GwPriority int `json:"gwPriority"`
|
||||||
|
LinkLocalIPs []string `json:"linkLocalIPs"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ContainerCreateByCommand struct {
|
type ContainerCreateByCommand struct {
|
||||||
@@ -295,6 +300,7 @@ type ComposeInfo struct {
|
|||||||
ConfigFile string `json:"configFile"`
|
ConfigFile string `json:"configFile"`
|
||||||
Workdir string `json:"workdir"`
|
Workdir string `json:"workdir"`
|
||||||
ComposeFileExists bool `json:"composeFileExists"`
|
ComposeFileExists bool `json:"composeFileExists"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Containers []ComposeContainer `json:"containers"`
|
Containers []ComposeContainer `json:"containers"`
|
||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
@@ -309,6 +315,7 @@ type ComposeContainer struct {
|
|||||||
type ComposeCreate struct {
|
type ComposeCreate struct {
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
|
DirName string `json:"dirName"`
|
||||||
From string `json:"from" validate:"required,oneof=edit path template"`
|
From string `json:"from" validate:"required,oneof=edit path template"`
|
||||||
File string `json:"file"`
|
File string `json:"file"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
@@ -319,7 +326,7 @@ type ComposeCreate struct {
|
|||||||
type ComposeOperation struct {
|
type ComposeOperation struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"`
|
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete rebuild"`
|
||||||
WithFile bool `json:"withFile"`
|
WithFile bool `json:"withFile"`
|
||||||
Force bool `json:"force"`
|
Force bool `json:"force"`
|
||||||
}
|
}
|
||||||
@@ -332,6 +339,10 @@ type ComposeUpdate struct {
|
|||||||
Env string `json:"env"`
|
Env string `json:"env"`
|
||||||
ForcePull bool `json:"forcePull"`
|
ForcePull bool `json:"forcePull"`
|
||||||
}
|
}
|
||||||
|
type ComposePin struct {
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
|
}
|
||||||
type ComposeLogClean struct {
|
type ComposeLogClean struct {
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
|
|||||||
@@ -0,0 +1,17 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type NetworkCleanupReport struct {
|
||||||
|
Deleted []NetworkCleanupItem `json:"deleted"`
|
||||||
|
Skipped []NetworkCleanupItem `json:"skipped"`
|
||||||
|
Failed []NetworkCleanupItem `json:"failed"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkCleanupItem struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NetworkCleanupTask struct {
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
|
}
|
||||||
@@ -51,9 +51,10 @@ type CronjobOperate struct {
|
|||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Args string `json:"args"`
|
Args string `json:"args"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
|
|
||||||
CleanLogConfig
|
CleanLogConfig
|
||||||
}
|
}
|
||||||
@@ -126,7 +127,8 @@ type CronjobInfo struct {
|
|||||||
Secret string `json:"secret"`
|
Secret string `json:"secret"`
|
||||||
Args string `json:"args"`
|
Args string `json:"args"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type CronjobImport struct {
|
type CronjobImport struct {
|
||||||
@@ -169,9 +171,10 @@ type CronjobTrans struct {
|
|||||||
SourceAccounts []string `json:"sourceAccounts"`
|
SourceAccounts []string `json:"sourceAccounts"`
|
||||||
DownloadAccount string `json:"downloadAccount"`
|
DownloadAccount string `json:"downloadAccount"`
|
||||||
|
|
||||||
AlertCount uint `json:"alertCount"`
|
AlertCount uint `json:"alertCount"`
|
||||||
AlertTitle string `json:"alertTitle"`
|
AlertTitle string `json:"alertTitle"`
|
||||||
AlertMethod string `json:"alertMethod"`
|
AlertMethod string `json:"alertMethod"`
|
||||||
|
AlertTriggerMode string `json:"alertTriggerMode" validate:"omitempty,oneof=failed success both"`
|
||||||
}
|
}
|
||||||
type TransHelper struct {
|
type TransHelper struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
@@ -197,6 +200,7 @@ type SearchRecord struct {
|
|||||||
|
|
||||||
type Record struct {
|
type Record struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
|
CronjobID uint `json:"cronjobID"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
StartTime string `json:"startTime"`
|
StartTime string `json:"startTime"`
|
||||||
Records string `json:"records"`
|
Records string `json:"records"`
|
||||||
|
|||||||
@@ -79,8 +79,9 @@ type NodeCurrent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type DashboardCurrent struct {
|
type DashboardCurrent struct {
|
||||||
Uptime uint64 `json:"uptime"`
|
Uptime uint64 `json:"uptime"`
|
||||||
TimeSinceUptime string `json:"timeSinceUptime"`
|
TimeSinceUptime string `json:"timeSinceUptime"`
|
||||||
|
RunningTime RunningTime `json:"runningTime"`
|
||||||
|
|
||||||
Procs uint64 `json:"procs"`
|
Procs uint64 `json:"procs"`
|
||||||
|
|
||||||
@@ -120,6 +121,7 @@ type DashboardCurrent struct {
|
|||||||
NetBytesRecv uint64 `json:"netBytesRecv"`
|
NetBytesRecv uint64 `json:"netBytesRecv"`
|
||||||
|
|
||||||
GPUData []GPUInfo `json:"gpuData"`
|
GPUData []GPUInfo `json:"gpuData"`
|
||||||
|
NPUData []NPUInfo `json:"npuData"`
|
||||||
XPUData []XPUInfo `json:"xpuData"`
|
XPUData []XPUInfo `json:"xpuData"`
|
||||||
|
|
||||||
TopCPUItems []Process `json:"topCPUItems"`
|
TopCPUItems []Process `json:"topCPUItems"`
|
||||||
@@ -128,6 +130,13 @@ type DashboardCurrent struct {
|
|||||||
ShotTime time.Time `json:"shotTime"`
|
ShotTime time.Time `json:"shotTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type RunningTime struct {
|
||||||
|
Days uint64 `json:"days"`
|
||||||
|
Hours uint64 `json:"hours"`
|
||||||
|
Minutes uint64 `json:"minutes"`
|
||||||
|
Seconds uint64 `json:"seconds"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppLauncherSync struct {
|
type AppLauncherSync struct {
|
||||||
Keys []string `json:"keys"`
|
Keys []string `json:"keys"`
|
||||||
}
|
}
|
||||||
@@ -148,8 +157,12 @@ type DiskInfo struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type GPUInfo struct {
|
type GPUInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
Index uint `json:"index"`
|
Index uint `json:"index"`
|
||||||
|
NPUIndex uint `json:"npuIndex"`
|
||||||
|
ChipIndex uint `json:"chipIndex"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
|
BusID string `json:"busID"`
|
||||||
GPUUtil string `json:"gpuUtil"`
|
GPUUtil string `json:"gpuUtil"`
|
||||||
Temperature string `json:"temperature"`
|
Temperature string `json:"temperature"`
|
||||||
PerformanceState string `json:"performanceState"`
|
PerformanceState string `json:"performanceState"`
|
||||||
@@ -162,6 +175,27 @@ type GPUInfo struct {
|
|||||||
FanSpeed string `json:"fanSpeed"`
|
FanSpeed string `json:"fanSpeed"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NPUInfo struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Index uint `json:"index"`
|
||||||
|
NPUIndex uint `json:"npuIndex"`
|
||||||
|
ChipIndex uint `json:"chipIndex"`
|
||||||
|
ProductName string `json:"productName"`
|
||||||
|
BusID string `json:"busID"`
|
||||||
|
Health string `json:"health"`
|
||||||
|
Temperature string `json:"temperature"`
|
||||||
|
PowerDraw string `json:"powerDraw"`
|
||||||
|
AICore string `json:"aiCore"`
|
||||||
|
MemUsed string `json:"memUsed"`
|
||||||
|
MemTotal string `json:"memTotal"`
|
||||||
|
MemoryUsed string `json:"memoryUsed"`
|
||||||
|
MemoryTotal string `json:"memoryTotal"`
|
||||||
|
HBMUsed string `json:"hbmUsed"`
|
||||||
|
HBMTotal string `json:"hbmTotal"`
|
||||||
|
HugepagesUsed string `json:"hugepagesUsed"`
|
||||||
|
HugepagesTotal string `json:"hugepagesTotal"`
|
||||||
|
}
|
||||||
|
|
||||||
type AppLauncher struct {
|
type AppLauncher struct {
|
||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -194,11 +228,13 @@ type LauncherOption struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type XPUInfo struct {
|
type XPUInfo struct {
|
||||||
DeviceID int `json:"deviceID"`
|
DeviceID int `json:"deviceID"`
|
||||||
DeviceName string `json:"deviceName"`
|
DeviceName string `json:"deviceName"`
|
||||||
Memory string `json:"memory"`
|
PciBdfAddress string `json:"pciBdfAddress"`
|
||||||
Temperature string `json:"temperature"`
|
Memory string `json:"memory"`
|
||||||
MemoryUsed string `json:"memoryUsed"`
|
Temperature string `json:"temperature"`
|
||||||
Power string `json:"power"`
|
GPUUtil string `json:"gpuUtil"`
|
||||||
MemoryUtil string `json:"memoryUtil"`
|
MemoryUsed string `json:"memoryUsed"`
|
||||||
|
Power string `json:"power"`
|
||||||
|
MemoryUtil string `json:"memoryUtil"`
|
||||||
}
|
}
|
||||||
|
|||||||
+69
-10
@@ -60,25 +60,84 @@ type MysqlDBCreate struct {
|
|||||||
Database string `json:"database" validate:"required"`
|
Database string `json:"database" validate:"required"`
|
||||||
Format string `json:"format" validate:"required"`
|
Format string `json:"format" validate:"required"`
|
||||||
Collation string `json:"collation"`
|
Collation string `json:"collation"`
|
||||||
Username string `json:"username" validate:"required"`
|
Username string `json:"username"`
|
||||||
Password string `json:"password" validate:"required"`
|
Password string `json:"password"`
|
||||||
Permission string `json:"permission" validate:"required"`
|
Permission string `json:"permission" validate:"required"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MysqlUser struct {
|
||||||
|
Username string `json:"username"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsDelete bool `json:"isDelete"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlGrant struct {
|
||||||
|
Database string `json:"database"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
Host string `json:"host"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlGrantSummarySearch struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
DBs []string `json:"dbs" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlUserSearch struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlUserCreate struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
Host string `json:"host" validate:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
DBs []string `json:"dbs"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlUserDelete struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Host string `json:"host" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlUserUpdate struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Host string `json:"host" validate:"required"`
|
||||||
|
NewHost string `json:"newHost" validate:"required"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlUserPassword struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Host string `json:"host" validate:"required"`
|
||||||
|
Password string `json:"password" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlGrantCreate struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
DB string `json:"db" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Host string `json:"host" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MysqlGrantDelete struct {
|
||||||
|
Database string `json:"database" validate:"required"`
|
||||||
|
DB string `json:"db" validate:"required"`
|
||||||
|
Username string `json:"username" validate:"required"`
|
||||||
|
Host string `json:"host" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type MysqlFormatCollationOption struct {
|
type MysqlFormatCollationOption struct {
|
||||||
Format string `json:"format"`
|
Format string `json:"format"`
|
||||||
Collations []string `json:"collations"`
|
Collations []string `json:"collations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type BindUser struct {
|
|
||||||
Database string `json:"database" validate:"required"`
|
|
||||||
DB string `json:"db" validate:"required"`
|
|
||||||
Username string `json:"username" validate:"required"`
|
|
||||||
Password string `json:"password" validate:"required"`
|
|
||||||
Permission string `json:"permission" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MysqlLoadDB struct {
|
type MysqlLoadDB struct {
|
||||||
From string `json:"from" validate:"required,oneof=local remote"`
|
From string `json:"from" validate:"required,oneof=local remote"`
|
||||||
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
|
Type string `json:"type" validate:"required,oneof=mysql mariadb mysql-cluster"`
|
||||||
|
|||||||
+360
-84
@@ -1,113 +1,389 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
type FirewallBaseInfo struct {
|
import (
|
||||||
Name string `json:"name"`
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
|
||||||
IsExist bool `json:"isExist"`
|
"github.com/1Panel-dev/1Panel/agent/utils/firewall/filter"
|
||||||
IsActive bool `json:"isActive"`
|
firewallsync "github.com/1Panel-dev/1Panel/agent/utils/firewall/sync"
|
||||||
IsInit bool `json:"isInit"`
|
)
|
||||||
IsBind bool `json:"isBind"`
|
|
||||||
Version string `json:"version"`
|
type FirewallSubsystemStatus struct {
|
||||||
PingStatus string `json:"pingStatus"`
|
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"`
|
||||||
|
SyncError string `json:"syncError,omitempty"`
|
||||||
|
LifecycleTaskID string `json:"lifecycleTaskID,omitempty"`
|
||||||
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuleSearch struct {
|
type FirewallLifecycleOperation struct {
|
||||||
PageInfo
|
|
||||||
Info string `json:"info"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Strategy string `json:"strategy"`
|
|
||||||
Type string `json:"type" validate:"required"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type FirewallOperation struct {
|
|
||||||
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
Operation string `json:"operation" validate:"required,oneof=start stop restart disableBanPing enableBanPing"`
|
||||||
WithDockerRestart bool `json:"withDockerRestart"`
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortRuleOperate struct {
|
type FirewallLifecycleOperationResponse struct {
|
||||||
ID uint `json:"id"`
|
TaskID string `json:"taskID,omitempty"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Queued bool `json:"queued"`
|
||||||
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 ForwardRuleOperate struct {
|
type FirewallBackendOption struct {
|
||||||
ForceDelete bool `json:"forceDelete"`
|
Name string `json:"name"`
|
||||||
Rules []struct {
|
Installed bool `json:"installed"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Active bool `json:"active"`
|
||||||
Num string `json:"num"`
|
Initialized bool `json:"initialized"`
|
||||||
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
Bound bool `json:"bound"`
|
||||||
Interface string `json:"interface"`
|
Supported bool `json:"supported"`
|
||||||
Port string `json:"port" validate:"required"`
|
SupportReason string `json:"supportReason,omitempty"`
|
||||||
TargetIP string `json:"targetIP"`
|
Implementation string `json:"implementation,omitempty"`
|
||||||
TargetPort string `json:"targetPort" validate:"required"`
|
Message string `json:"message,omitempty"`
|
||||||
} `json:"rules"`
|
IPv4 FirewallBackendFamilyStatus `json:"ipv4"`
|
||||||
|
IPv6 FirewallBackendFamilyStatus `json:"ipv6"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type UpdateFirewallDescription struct {
|
type FirewallBackendFamilyStatus struct {
|
||||||
Type string `json:"type"`
|
Available bool `json:"available"`
|
||||||
Chain string `json:"chain"`
|
Initialized bool `json:"initialized"`
|
||||||
SrcIP string `json:"srcIP"`
|
Bound bool `json:"bound"`
|
||||||
DstIP string `json:"dstIP"`
|
Reason string `json:"reason,omitempty"`
|
||||||
SrcPort string `json:"srcPort"`
|
ForwardPolicy string `json:"forwardPolicy,omitempty"`
|
||||||
DstPort string `json:"dstPort"`
|
RAInterfaces []string `json:"raInterfaces,omitempty"`
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
|
||||||
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AddrRuleOperate struct {
|
type FirewallBackendGroup struct {
|
||||||
ID uint `json:"id"`
|
Selected string `json:"selected"`
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Current string `json:"current,omitempty"`
|
||||||
Address string `json:"address" validate:"required"`
|
Options []FirewallBackendOption `json:"options"`
|
||||||
Strategy string `json:"strategy" validate:"required,oneof=accept drop"`
|
|
||||||
|
|
||||||
Description string `json:"description"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type PortRuleUpdate struct {
|
type FirewallSettings struct {
|
||||||
OldRule PortRuleOperate `json:"oldRule"`
|
System FirewallBackendGroup `json:"system"`
|
||||||
NewRule PortRuleOperate `json:"newRule"`
|
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 AddrRuleUpdate struct {
|
type FirewallPortWhitelistCreate struct {
|
||||||
OldRule AddrRuleOperate `json:"oldRule"`
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
NewRule AddrRuleOperate `json:"newRule"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type BatchRuleOperate struct {
|
type FirewallPortWhitelistUpdate struct {
|
||||||
Type string `json:"type" validate:"required"`
|
OldRule filter.PortWhitelist `json:"oldRule" validate:"required"`
|
||||||
Rules []PortRuleOperate `json:"rules"`
|
Rule filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type IptablesOp struct {
|
type FirewallPortWhitelistDelete struct {
|
||||||
Name string `json:"name" validate:"required,oneof=1PANEL_INPUT 1PANEL_OUTPUT 1PANEL_BASIC"`
|
Rule *filter.PortWhitelist `json:"rule" validate:"required"`
|
||||||
Operate string `json:"operate" validate:"required,oneof=init-base init-forward init-advance bind-base unbind-base bind unbind"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type IptablesRuleOp struct {
|
type FirewallBackendOperation struct {
|
||||||
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
Subsystem string `json:"subsystem" validate:"required,oneof=system forwarding docker"`
|
||||||
ID uint `json:"id"`
|
Backend string `json:"backend" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||||
Chain string `json:"chain" validate:"required,oneof=1PANEL_BASIC 1PANEL_BASIC_BEFORE 1PANEL_INPUT 1PANEL_OUTPUT"`
|
Operation string `json:"operation" validate:"required,oneof=select initialize cleanup"`
|
||||||
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 IptablesBatchOperate struct {
|
type FilterChainOperation struct {
|
||||||
Rules []IptablesRuleOp `json:"rules"`
|
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 IptablesChainStatus struct {
|
type FilterChainOperationResponse struct {
|
||||||
IsBind bool `json:"isBind"`
|
TaskID string `json:"taskID"`
|
||||||
DefaultStrategy string `json:"defaultStrategy"`
|
Queued bool `json:"queued"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallInitializationTask struct {
|
||||||
|
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"`
|
||||||
|
ManagedTotal int64 `json:"managedTotal"`
|
||||||
|
Items []filter.InventoryItem `json:"items"`
|
||||||
|
Notices []filter.ScopeNotice `json:"notices,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleResetResponse struct {
|
||||||
|
Removed int `json:"removed"`
|
||||||
|
Disabled bool `json:"disabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleReset struct {
|
||||||
|
Provider filter.Provider `json:"provider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
|
WithDockerRestart bool `json:"withDockerRestart"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleInventory struct {
|
||||||
|
Refresh bool `json:"refresh,omitempty"`
|
||||||
|
PageInfo
|
||||||
|
Scope filter.Scope `json:"scope,omitempty"`
|
||||||
|
Scopes []filter.Scope `json:"scopes,omitempty" validate:"max=16"`
|
||||||
|
All bool `json:"all,omitempty"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
Families []filter.Family `json:"families,omitempty" validate:"omitempty,dive,oneof=ipv4 ipv6"`
|
||||||
|
Actions []string `json:"actions,omitempty" validate:"omitempty,dive,oneof=accept deny"`
|
||||||
|
States []filter.InventoryState `json:"states,omitempty" validate:"omitempty,dive,oneof=managed adopted external drifted protected"`
|
||||||
|
ExcludeChains []string `json:"excludeChains,omitempty" validate:"omitempty,dive,oneof=1PANEL_BASIC_BEFORE 1PANEL_BASIC 1PANEL_BASIC_AFTER"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallNativeDetail struct {
|
||||||
|
Provider filter.Provider `json:"provider" validate:"required,oneof=firewalld ufw"`
|
||||||
|
NativeKind filter.NativeKind `json:"nativeKind" validate:"required,oneof=zone_service ufw_application"`
|
||||||
|
Name string `json:"name" validate:"required"`
|
||||||
|
Permanent bool `json:"permanent"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardBase struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
IsExist bool `json:"isExist"`
|
||||||
|
Initialized bool `json:"initialized"`
|
||||||
|
Bound bool `json:"bound"`
|
||||||
|
IPv4 DockerPortGuardFamilyStatus `json:"ipv4"`
|
||||||
|
IPv6 DockerPortGuardFamilyStatus `json:"ipv6"`
|
||||||
|
Backend string `json:"backend"`
|
||||||
|
Message string `json:"message,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardFamilyStatus struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
Initialized bool `json:"initialized"`
|
||||||
|
Bound bool `json:"bound"`
|
||||||
|
Effective bool `json:"effective"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardEndpoint struct {
|
||||||
|
Family string `json:"family"`
|
||||||
|
HostIP string `json:"hostIP"`
|
||||||
|
HostPort uint16 `json:"hostPort"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
ContainerID string `json:"containerID"`
|
||||||
|
ContainerName string `json:"containerName"`
|
||||||
|
ContainerState string `json:"containerState,omitempty"`
|
||||||
|
ContainerPort uint16 `json:"containerPort"`
|
||||||
|
Compose string `json:"compose,omitempty"`
|
||||||
|
Application string `json:"application,omitempty"`
|
||||||
|
PolicyUUID string `json:"policyUUID,omitempty"`
|
||||||
|
Mode string `json:"mode,omitempty"`
|
||||||
|
NativeAction string `json:"nativeAction,omitempty"`
|
||||||
|
ReadOnly bool `json:"readOnly,omitempty"`
|
||||||
|
Sources []string `json:"sources"`
|
||||||
|
Effective bool `json:"effective"`
|
||||||
|
Description string `json:"description,omitempty"`
|
||||||
|
TrafficPath string `json:"trafficPath"`
|
||||||
|
ManagementTarget string `json:"managementTarget"`
|
||||||
|
ManagementReason string `json:"managementReason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardPortGroup struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Label string `json:"label"`
|
||||||
|
Endpoint DockerPortGuardEndpoint `json:"endpoint"`
|
||||||
|
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardContainer struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Compose string `json:"compose,omitempty"`
|
||||||
|
Application string `json:"application,omitempty"`
|
||||||
|
Endpoints []DockerPortGuardEndpoint `json:"endpoints"`
|
||||||
|
PortGroups []DockerPortGuardPortGroup `json:"portGroups"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardList struct {
|
||||||
|
Base DockerPortGuardBase `json:"base"`
|
||||||
|
Containers []DockerPortGuardContainer `json:"containers"`
|
||||||
|
OrphanPolicies []DockerPortGuardEndpoint `json:"orphanPolicies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardEndpointIdentity struct {
|
||||||
|
Family string `json:"family" validate:"required,oneof=ipv4 ipv6"`
|
||||||
|
HostIP string `json:"hostIP" validate:"required,max=45"`
|
||||||
|
HostPort uint16 `json:"hostPort" validate:"required,min=1"`
|
||||||
|
Protocol string `json:"protocol" validate:"required,oneof=tcp udp"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardPolicyBatch struct {
|
||||||
|
Policies []DockerPortGuardPolicy `json:"policies" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardPolicyBatchDelete struct {
|
||||||
|
UUIDs []string `json:"uuids" validate:"required,min=1,dive,required,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardPolicy struct {
|
||||||
|
DockerPortGuardEndpointIdentity
|
||||||
|
Mode string `json:"mode" validate:"required,oneof=deny_sources allow_sources deny_all"`
|
||||||
|
Sources []string `json:"sources" validate:"dive,required,max=64"`
|
||||||
|
Description string `json:"description" validate:"max=256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardOperation struct {
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=initialize bind unbind"`
|
||||||
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleAdopt struct {
|
||||||
|
Scope filter.Scope `json:"scope" validate:"required"`
|
||||||
|
InstanceKey string `json:"instanceKey,omitempty" validate:"omitempty,max=128"`
|
||||||
|
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||||
|
Marker string `json:"marker,omitempty" validate:"max=256"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleCreateItem struct {
|
||||||
|
Rule filter.FirewallRule `json:"rule" validate:"required"`
|
||||||
|
SourceKind string `json:"sourceKind" validate:"omitempty,oneof=user imported"`
|
||||||
|
SourceID string `json:"sourceID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleCreate struct {
|
||||||
|
Items []FirewallRuleCreateItem `json:"items" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleCreateResponse struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued,omitempty"`
|
||||||
|
Succeeded int `json:"succeeded"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Skipped int `json:"skipped"`
|
||||||
|
Errors []FirewallRuleCreateFailure `json:"errors,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleCreateFailure struct {
|
||||||
|
Index int `json:"index"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Rule filter.FirewallRule `json:"rule"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncRequest struct {
|
||||||
|
Subsystem string `json:"subsystem" validate:"omitempty,oneof=system forwarding docker"`
|
||||||
|
SourceProvider filter.Provider `json:"sourceProvider,omitempty" validate:"omitempty,oneof=firewalld ufw iptables nftables"`
|
||||||
|
TargetProvider filter.Provider `json:"targetProvider" validate:"required,oneof=firewalld ufw iptables nftables"`
|
||||||
|
ResetSource bool `json:"resetSource"`
|
||||||
|
TaskID string `json:"taskID,omitempty" validate:"omitempty,max=64"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncItem struct {
|
||||||
|
SourceUUID string `json:"sourceUUID"`
|
||||||
|
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||||
|
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||||
|
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||||
|
Status firewallsync.Status `json:"status"`
|
||||||
|
ReasonCode firewallsync.ReasonCode `json:"reasonCode,omitempty"`
|
||||||
|
Reason string `json:"reason,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncPreview struct {
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||||
|
TargetProvider filter.Provider `json:"targetProvider"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Ready int `json:"ready"`
|
||||||
|
Existing int `json:"existing"`
|
||||||
|
Removed int `json:"removed"`
|
||||||
|
Blocked int `json:"blocked"`
|
||||||
|
Items []FirewallRuleSyncItem `json:"items"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncResult struct {
|
||||||
|
Subsystem string `json:"subsystem"`
|
||||||
|
SourceProvider filter.Provider `json:"sourceProvider,omitempty"`
|
||||||
|
TargetProvider filter.Provider `json:"targetProvider"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Succeeded int `json:"succeeded"`
|
||||||
|
Skipped int `json:"skipped"`
|
||||||
|
Removed int `json:"removed"`
|
||||||
|
Failed int `json:"failed"`
|
||||||
|
Errors []FirewallRuleSyncFailure `json:"errors,omitempty"`
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Queued bool `json:"queued,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncTask struct {
|
||||||
|
TaskID string `json:"taskID,omitempty"`
|
||||||
|
Executing bool `json:"executing"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleSyncFailure struct {
|
||||||
|
SourceUUID string `json:"sourceUUID"`
|
||||||
|
Rule *filter.FirewallRule `json:"rule,omitempty"`
|
||||||
|
ForwardRule *ForwardRule `json:"forwardRule,omitempty"`
|
||||||
|
DockerRule *DockerPortGuardEndpoint `json:"dockerRule,omitempty"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleDelete struct {
|
||||||
|
UUIDs []string `json:"uuids" validate:"omitempty,dive,required,max=64"`
|
||||||
|
BeforeRules []FirewallRuleDeleteTarget `json:"beforeRules,omitempty" validate:"omitempty,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
UUID string `json:"uuid"`
|
||||||
|
Error string `json:"error"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleUpdate struct {
|
||||||
|
UUID string `json:"uuid" validate:"required,max=64"`
|
||||||
|
Rule *filter.FirewallRule `json:"rule,omitempty" validate:"required_without_all=Description OrderIndex Priority,excluded_with=Description OrderIndex Priority"`
|
||||||
|
Description *string `json:"description,omitempty" validate:"excluded_with=Rule"`
|
||||||
|
OrderIndex *int64 `json:"orderIndex,omitempty" validate:"excluded_with=Rule Priority"`
|
||||||
|
Priority *int `json:"priority,omitempty" validate:"excluded_with=Rule OrderIndex"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleReorder struct {
|
||||||
|
UUID string `json:"uuid" validate:"required,max=64"`
|
||||||
|
TargetPosition *int64 `json:"targetPosition"`
|
||||||
|
Priority *int `json:"priority"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *FirewallRuleSyncPreview) Add(item FirewallRuleSyncItem) {
|
||||||
|
p.Items = append(p.Items, item)
|
||||||
|
switch item.Status {
|
||||||
|
case firewallsync.StatusReady:
|
||||||
|
p.Ready++
|
||||||
|
p.Total++
|
||||||
|
case firewallsync.StatusExisting:
|
||||||
|
p.Existing++
|
||||||
|
p.Total++
|
||||||
|
case firewallsync.StatusRemove:
|
||||||
|
p.Removed++
|
||||||
|
case firewallsync.StatusBlocked:
|
||||||
|
p.Blocked++
|
||||||
|
if item.ReasonCode != firewallsync.ReasonReadOnlyRule {
|
||||||
|
p.Total++
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
type ForwardRuleSearch struct {
|
||||||
|
PageInfo
|
||||||
|
All bool `json:"all,omitempty"`
|
||||||
|
Info string `json:"info"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRule struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Chain string `json:"chain"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port string `json:"port"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
Strategy string `json:"strategy"`
|
||||||
|
|
||||||
|
Num string `json:"num"`
|
||||||
|
TargetIP string `json:"targetIP"`
|
||||||
|
TargetPort string `json:"targetPort"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
|
||||||
|
UsedStatus string `json:"usedStatus"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
|
||||||
|
IsDesired bool `json:"isDesired"`
|
||||||
|
IsRuntime bool `json:"isRuntime"`
|
||||||
|
SyncStatus string `json:"syncStatus"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperate struct {
|
||||||
|
ForceDelete bool `json:"forceDelete"`
|
||||||
|
Rules []ForwardRuleOperation `json:"rules" validate:"required,min=1,dive"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardRuleOperation struct {
|
||||||
|
Operation string `json:"operation" validate:"required,oneof=add remove"`
|
||||||
|
Num string `json:"num"`
|
||||||
|
Family string `json:"family" validate:"omitempty,oneof=ipv4 ipv6"`
|
||||||
|
Protocol string `json:"protocol" validate:"required,oneof=tcp udp tcp/udp"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
Port string `json:"port" validate:"required"`
|
||||||
|
TargetIP string `json:"targetIP"`
|
||||||
|
TargetPort string `json:"targetPort" validate:"required"`
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,3 +16,37 @@ type SearchTaskLogReq struct {
|
|||||||
type TaskDTO struct {
|
type TaskDTO struct {
|
||||||
model.Task
|
model.Task
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type SystemLogReq struct {
|
||||||
|
PageSize int `json:"pageSize" validate:"omitempty,min=1,max=500"`
|
||||||
|
Cursor string `json:"cursor"`
|
||||||
|
StartTime time.Time `json:"startTime"`
|
||||||
|
EndTime time.Time `json:"endTime"`
|
||||||
|
Keyword string `json:"keyword"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemLogRes struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Items []SystemLogItem `json:"items"`
|
||||||
|
HasMore bool `json:"hasMore"`
|
||||||
|
NextCursor string `json:"nextCursor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemLogStatus struct {
|
||||||
|
Source string `json:"source"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
KeywordFilterSupported bool `json:"keywordFilterSupported"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type SystemLogItem struct {
|
||||||
|
Timestamp int64 `json:"-"`
|
||||||
|
Cursor string `json:"-"`
|
||||||
|
Time string `json:"time"`
|
||||||
|
Priority string `json:"priority"`
|
||||||
|
Service string `json:"service"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Raw string `json:"raw"`
|
||||||
|
}
|
||||||
|
|||||||
+89
-18
@@ -1,6 +1,10 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import "time"
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
)
|
||||||
|
|
||||||
type MonitorSearch struct {
|
type MonitorSearch struct {
|
||||||
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
Param string `json:"param" validate:"required,oneof=all cpu memory load io network"`
|
||||||
@@ -26,50 +30,92 @@ type Process struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSetting struct {
|
type MonitorSetting struct {
|
||||||
MonitorStatus string `json:"monitorStatus"`
|
GPUMonitorStatus string `json:"gpuMonitorStatus"`
|
||||||
MonitorStoreDays string `json:"monitorStoreDays"`
|
GPUMonitorStoreDays string `json:"gpuMonitorStoreDays"`
|
||||||
MonitorInterval string `json:"monitorInterval"`
|
GPUMonitorInterval string `json:"gpuMonitorInterval"`
|
||||||
DefaultNetwork string `json:"defaultNetwork"`
|
VLLMMonitorStatus string `json:"vllmMonitorStatus"`
|
||||||
DefaultIO string `json:"defaultIO"`
|
VLLMMonitorStoreDays string `json:"vllmMonitorStoreDays"`
|
||||||
|
VLLMMonitorInterval string `json:"vllmMonitorInterval"`
|
||||||
|
MonitorStatus string `json:"monitorStatus"`
|
||||||
|
MonitorStoreDays string `json:"monitorStoreDays"`
|
||||||
|
MonitorInterval string `json:"monitorInterval"`
|
||||||
|
DefaultNetwork string `json:"defaultNetwork"`
|
||||||
|
DefaultIO string `json:"defaultIO"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorSettingUpdate 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"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPUOptions struct {
|
type MonitorGPUOptions struct {
|
||||||
|
Supported bool `json:"supported"`
|
||||||
GPUType string `json:"gpuType"`
|
GPUType string `json:"gpuType"`
|
||||||
ChartHide []GPUChartHide `json:"chartHide"`
|
ChartHide []GPUChartHide `json:"chartHide"`
|
||||||
Options []string `json:"options"`
|
Options []string `json:"options"`
|
||||||
}
|
}
|
||||||
type GPUChartHide struct {
|
type GPUChartHide struct {
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
|
Type string `json:"type"`
|
||||||
Process bool `json:"process"`
|
Process bool `json:"process"`
|
||||||
GPU bool `json:"gpu"`
|
GPU bool `json:"gpu"`
|
||||||
Memory bool `json:"memory"`
|
Memory bool `json:"memory"`
|
||||||
Power bool `json:"power"`
|
Power bool `json:"power"`
|
||||||
|
PowerLimit bool `json:"powerLimit"`
|
||||||
Temperature bool `json:"temperature"`
|
Temperature bool `json:"temperature"`
|
||||||
Speed bool `json:"speed"`
|
Speed bool `json:"speed"`
|
||||||
}
|
}
|
||||||
type MonitorGPUSearch struct {
|
type MonitorGPUSearch struct {
|
||||||
|
Aggregation string `json:"aggregation" validate:"omitempty,oneof=avg max"`
|
||||||
|
DeviceID string `json:"deviceID"`
|
||||||
|
Legacy bool `json:"legacy"`
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
StartTime time.Time `json:"startTime"`
|
StartTime time.Time `json:"startTime"`
|
||||||
EndTime time.Time `json:"endTime"`
|
EndTime time.Time `json:"endTime"`
|
||||||
}
|
}
|
||||||
type MonitorGPUData struct {
|
type MonitorGPUData struct {
|
||||||
Date []time.Time `json:"date"`
|
MemoryActivity []*float64 `json:"memoryActivity"`
|
||||||
GPUValue []float64 `json:"gpuValue"`
|
EncoderUtil []*float64 `json:"encoderUtil"`
|
||||||
TemperatureValue []float64 `json:"temperatureValue"`
|
DecoderUtil []*float64 `json:"decoderUtil"`
|
||||||
PowerTotal []float64 `json:"powerTotal"`
|
JPEGUtil []*float64 `json:"jpegUtil"`
|
||||||
PowerUsed []float64 `json:"powerUsed"`
|
OFAUtil []*float64 `json:"ofaUtil"`
|
||||||
PowerPercent []float64 `json:"powerPercent"`
|
MediaUtil []*float64 `json:"mediaUtil"`
|
||||||
MemoryTotal []float64 `json:"memoryTotal"`
|
ComputeUtil []*float64 `json:"computeUtil"`
|
||||||
MemoryUsed []float64 `json:"memoryUsed"`
|
CopyUtil []*float64 `json:"copyUtil"`
|
||||||
MemoryPercent []float64 `json:"memoryPercent"`
|
HotspotTemperature []*float64 `json:"hotspotTemperature"`
|
||||||
SpeedValue []int `json:"speedValue"`
|
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"`
|
GPUProcesses [][]GPUProcess `json:"gpuProcesses"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,3 +125,28 @@ type GPUProcess struct {
|
|||||||
ProcessName string `json:"processName"`
|
ProcessName string `json:"processName"`
|
||||||
UsedMemory string `json:"usedMemory"`
|
UsedMemory string `json:"usedMemory"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type MonitorVLLMSearch struct {
|
||||||
|
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 MonitorVLLMClean struct {
|
||||||
|
AppInstallID uint `json:"appInstallID" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type MonitorClean struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=host gpu"`
|
||||||
|
}
|
||||||
|
|||||||
+46
-6
@@ -1,6 +1,8 @@
|
|||||||
package dto
|
package dto
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
"github.com/1Panel-dev/1Panel/agent/utils/nginx/components"
|
||||||
)
|
)
|
||||||
@@ -49,13 +51,19 @@ const (
|
|||||||
CACHE NginxKey = "cache"
|
CACHE NginxKey = "cache"
|
||||||
HttpPer NginxKey = "http-per"
|
HttpPer NginxKey = "http-per"
|
||||||
ProxyCache NginxKey = "proxy-cache"
|
ProxyCache NginxKey = "proxy-cache"
|
||||||
|
Brotli NginxKey = "brotli"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// BrotliKeys are served from the panel-managed http.d file rather than
|
||||||
|
// nginx.conf, because the module is optional: its directives must disappear
|
||||||
|
// together with the module, otherwise nginx refuses to start.
|
||||||
|
var BrotliKeys = []string{"brotli", "brotli_comp_level", "brotli_min_length", "brotli_types"}
|
||||||
|
|
||||||
var ScopeKeyMap = map[NginxKey][]string{
|
var ScopeKeyMap = map[NginxKey][]string{
|
||||||
Index: {"index"},
|
Index: {"index"},
|
||||||
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
LimitConn: {"limit_conn", "limit_rate", "limit_conn_zone"},
|
||||||
SSL: {"ssl_certificate", "ssl_certificate_key"},
|
SSL: {"ssl_certificate", "ssl_certificate_key"},
|
||||||
HttpPer: {"server_names_hash_bucket_size", "client_header_buffer_size", "client_max_body_size", "keepalive_timeout", "gzip", "gzip_min_length", "gzip_comp_level"},
|
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 {
|
var StaticFileKeyMap = map[NginxKey]struct {
|
||||||
@@ -87,9 +95,41 @@ var LBAlgorithms = map[string]struct{}{"ip_hash": {}, "least_conn": {}}
|
|||||||
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
|
var RealIPKeys = map[string]struct{}{"X-Forwarded-For": {}, "X-Real-IP": {}, "CF-Connecting-IP": {}}
|
||||||
|
|
||||||
type NginxModule struct {
|
type NginxModule struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Script string `json:"script"`
|
Custom bool `json:"custom,omitempty"`
|
||||||
Packages []string `json:"packages"`
|
Script string `json:"script"`
|
||||||
Params string `json:"params"`
|
Packages []string `json:"packages"`
|
||||||
Enable bool `json:"enable"`
|
Params string `json:"params"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
BuildMode string `json:"buildMode,omitempty"`
|
||||||
|
Provider string `json:"provider,omitempty"`
|
||||||
|
LoadOrder int `json:"loadOrder,omitempty"`
|
||||||
|
Builds []NginxModuleBuild `json:"builds,omitempty"`
|
||||||
|
LastError string `json:"lastError,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NginxModuleBuild struct {
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
BuildMode string `json:"buildMode"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Hash string `json:"hash"`
|
||||||
|
Target NginxModuleTarget `json:"target"`
|
||||||
|
Artifacts []NginxModuleArtifact `json:"artifacts,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
BuiltAt time.Time `json:"builtAt,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NginxModuleTarget struct {
|
||||||
|
Key string `json:"key"`
|
||||||
|
OpenRestyVersion string `json:"openrestyVersion"`
|
||||||
|
Architecture string `json:"architecture"`
|
||||||
|
Image string `json:"image,omitempty"`
|
||||||
|
ImageDigest string `json:"imageDigest,omitempty"`
|
||||||
|
BuilderDigest string `json:"builderDigest,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NginxModuleArtifact struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Path string `json:"path"`
|
||||||
|
Checksum string `json:"checksum"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,6 +50,10 @@ type AppContainerConfig struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
SpecifyIP string `json:"specifyIP"`
|
SpecifyIP string `json:"specifyIP"`
|
||||||
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
|
RestartPolicy string `json:"restartPolicy" validate:"omitempty,oneof=always unless-stopped no on-failure"`
|
||||||
|
|
||||||
|
KeepServiceName bool `json:"-"`
|
||||||
|
SkipComposeCommonConfig bool `json:"-"`
|
||||||
|
UseLifecycleScripts bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledSearch struct {
|
type AppInstalledSearch struct {
|
||||||
@@ -92,6 +96,8 @@ type AppInstalledOperate struct {
|
|||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
DeleteImage bool `json:"deleteImage"`
|
DeleteImage bool `json:"deleteImage"`
|
||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
|
|
||||||
|
UseLifecycleScripts bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallUpgrade struct {
|
type AppInstallUpgrade struct {
|
||||||
@@ -99,6 +105,7 @@ type AppInstallUpgrade struct {
|
|||||||
DetailID uint `json:"detailId"`
|
DetailID uint `json:"detailId"`
|
||||||
Backup bool `json:"backup"`
|
Backup bool `json:"backup"`
|
||||||
PullImage bool `json:"pullImage"`
|
PullImage bool `json:"pullImage"`
|
||||||
|
DeleteImage bool `json:"deleteImage"`
|
||||||
DockerCompose string `json:"dockerCompose"`
|
DockerCompose string `json:"dockerCompose"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
@@ -110,11 +117,14 @@ type AppInstallDelete struct {
|
|||||||
DeleteDB bool `json:"deleteDB"`
|
DeleteDB bool `json:"deleteDB"`
|
||||||
DeleteImage bool `json:"deleteImage"`
|
DeleteImage bool `json:"deleteImage"`
|
||||||
TaskID string `json:"taskID"`
|
TaskID string `json:"taskID"`
|
||||||
|
|
||||||
|
UseLifecycleScripts bool `json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstalledUpdate struct {
|
type AppInstalledUpdate struct {
|
||||||
InstallId uint `json:"installId" validate:"required"`
|
InstallId uint `json:"installId" validate:"required"`
|
||||||
Params map[string]interface{} `json:"params" validate:"required"`
|
Params map[string]interface{} `json:"params" validate:"required"`
|
||||||
|
TaskID string `json:"-"`
|
||||||
AppContainerConfig
|
AppContainerConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -122,6 +122,7 @@ type FileWget struct {
|
|||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
IgnoreCertificate bool `json:"ignoreCertificate"`
|
IgnoreCertificate bool `json:"ignoreCertificate"`
|
||||||
UseProxy bool `json:"useProxy"`
|
UseProxy bool `json:"useProxy"`
|
||||||
|
UseServerFilename bool `json:"useServerFilename"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileMove struct {
|
type FileMove struct {
|
||||||
@@ -131,6 +132,11 @@ type FileMove struct {
|
|||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Cover bool `json:"cover"`
|
Cover bool `json:"cover"`
|
||||||
CoverPaths []string `json:"coverPaths"`
|
CoverPaths []string `json:"coverPaths"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FileMoveStopReq struct {
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type FileDownload struct {
|
type FileDownload struct {
|
||||||
@@ -153,6 +159,10 @@ type FileProcessReq struct {
|
|||||||
Key string `json:"key"`
|
Key string `json:"key"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type FileProcessRemoveReq struct {
|
||||||
|
Keys []string `json:"keys" validate:"required,min=1,max=1000"`
|
||||||
|
}
|
||||||
|
|
||||||
type FileRoleUpdate struct {
|
type FileRoleUpdate struct {
|
||||||
Path string `json:"path" validate:"required"`
|
Path string `json:"path" validate:"required"`
|
||||||
User string `json:"user" validate:"required"`
|
User string `json:"user" validate:"required"`
|
||||||
|
|||||||
@@ -21,6 +21,10 @@ type McpServerCreate struct {
|
|||||||
StreamableHttpPath string `json:"streamableHttpPath"`
|
StreamableHttpPath string `json:"streamableHttpPath"`
|
||||||
OutputTransport string `json:"outputTransport" validate:"required"`
|
OutputTransport string `json:"outputTransport" validate:"required"`
|
||||||
Type string `json:"type" validate:"required"`
|
Type string `json:"type" validate:"required"`
|
||||||
|
GatewayImage string `json:"gatewayImage"`
|
||||||
|
ProtocolVersion string `json:"protocolVersion"`
|
||||||
|
GatewayArgs string `json:"gatewayArgs" validate:"max=4096"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type McpServerUpdate struct {
|
type McpServerUpdate struct {
|
||||||
@@ -32,11 +36,23 @@ type McpServerDelete struct {
|
|||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type McpServerDetail struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type McpServerStatusSync struct {
|
||||||
|
IDs []uint `json:"ids"`
|
||||||
|
}
|
||||||
|
|
||||||
type McpServerOperate struct {
|
type McpServerOperate struct {
|
||||||
ID uint `json:"id" validate:"required"`
|
ID uint `json:"id" validate:"required"`
|
||||||
Operate string `json:"operate" validate:"required"`
|
Operate string `json:"operate" validate:"required"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type McpServerConnectionTest struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
}
|
||||||
|
|
||||||
type McpBindDomain struct {
|
type McpBindDomain struct {
|
||||||
Domain string `json:"domain" validate:"required"`
|
Domain string `json:"domain" validate:"required"`
|
||||||
SSLID uint `json:"sslID"`
|
SSLID uint `json:"sslID"`
|
||||||
|
|||||||
@@ -114,17 +114,22 @@ type NginxRedirectUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NginxBuildReq struct {
|
type NginxBuildReq struct {
|
||||||
TaskID string `json:"taskID" validate:"required"`
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
Mirror string `json:"mirror" validate:"required"`
|
Mirror string `json:"mirror" validate:"required"`
|
||||||
|
Modules []string `json:"modules"`
|
||||||
|
Force bool `json:"force"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxModuleUpdate struct {
|
type NginxModuleUpdate struct {
|
||||||
Operate string `json:"operate" validate:"required,oneof=create delete update"`
|
Operate string `json:"operate" validate:"required,oneof=create delete update"`
|
||||||
Name string `json:"name" validate:"required"`
|
Name string `json:"name" validate:"required"`
|
||||||
Script string `json:"script"`
|
Script string `json:"script"`
|
||||||
Packages string `json:"packages"`
|
Packages string `json:"packages"`
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
Params string `json:"params"`
|
Params string `json:"params"`
|
||||||
|
BuildMode string `json:"buildMode" validate:"omitempty,oneof=dynamic static"`
|
||||||
|
Provider string `json:"provider" validate:"omitempty,oneof=local prebuilt"`
|
||||||
|
LoadOrder int `json:"loadOrder" validate:"omitempty,min=0,max=9999"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxOperateReq struct {
|
type NginxOperateReq struct {
|
||||||
|
|||||||
@@ -21,13 +21,14 @@ type RuntimeCreate struct {
|
|||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
CodeDir string `json:"codeDir"`
|
CodeDir string `json:"codeDir"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
|
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]interface{} `json:"params"`
|
||||||
NodeConfig
|
NodeConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeConfig struct {
|
type NodeConfig struct {
|
||||||
Install bool `json:"install"`
|
Install *bool `json:"install"`
|
||||||
Clean bool `json:"clean"`
|
Clean bool `json:"clean"`
|
||||||
ExposedPorts []ExposedPort `json:"exposedPorts"`
|
ExposedPorts []ExposedPort `json:"exposedPorts"`
|
||||||
Environments []Environment `json:"environments"`
|
Environments []Environment `json:"environments"`
|
||||||
@@ -42,12 +43,14 @@ type Environment struct {
|
|||||||
type Volume struct {
|
type Volume struct {
|
||||||
Source string `json:"source"`
|
Source string `json:"source"`
|
||||||
Target string `json:"target"`
|
Target string `json:"target"`
|
||||||
|
Mode string `json:"mode"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExposedPort struct {
|
type ExposedPort struct {
|
||||||
HostPort int `json:"hostPort"`
|
HostPort int `json:"hostPort"`
|
||||||
ContainerPort int `json:"containerPort"`
|
ContainerPort int `json:"containerPort"`
|
||||||
HostIP string `json:"hostIP"`
|
HostIP string `json:"hostIP"`
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type ExtraHost struct {
|
type ExtraHost struct {
|
||||||
@@ -56,19 +59,22 @@ type ExtraHost struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDelete struct {
|
type RuntimeDelete struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
ForceDelete bool `json:"forceDelete"`
|
ForceDelete bool `json:"forceDelete"`
|
||||||
|
DeleteImage bool `json:"deleteImage"`
|
||||||
|
TaskID string `json:"taskID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeUpdate struct {
|
type RuntimeUpdate struct {
|
||||||
Name string `json:"name"`
|
AppDetailID uint `json:"appDetailId"`
|
||||||
ID uint `json:"id"`
|
Name string `json:"name"`
|
||||||
Image string `json:"image"`
|
ID uint `json:"id"`
|
||||||
Version string `json:"version"`
|
Image string `json:"image"`
|
||||||
Rebuild bool `json:"rebuild"`
|
Version string `json:"version"`
|
||||||
Source string `json:"source"`
|
Rebuild bool `json:"rebuild"`
|
||||||
CodeDir string `json:"codeDir"`
|
Source string `json:"source"`
|
||||||
Remark string `json:"remark"`
|
CodeDir string `json:"codeDir"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
|
||||||
Params map[string]interface{} `json:"params"`
|
Params map[string]interface{} `json:"params"`
|
||||||
NodeConfig
|
NodeConfig
|
||||||
|
|||||||
@@ -34,6 +34,8 @@ type WebsiteCreate struct {
|
|||||||
|
|
||||||
SiteDir string `json:"siteDir"`
|
SiteDir string `json:"siteDir"`
|
||||||
|
|
||||||
|
TemplateOutputID uint `json:"templateOutputID"`
|
||||||
|
|
||||||
RuntimeConfig
|
RuntimeConfig
|
||||||
FtpConfig
|
FtpConfig
|
||||||
DataBaseConfig
|
DataBaseConfig
|
||||||
|
|||||||
@@ -126,6 +126,16 @@ type WebsiteSSLUpload struct {
|
|||||||
Type string `json:"type" validate:"required,oneof=paste local"`
|
Type string `json:"type" validate:"required,oneof=paste local"`
|
||||||
SSLID uint `json:"sslID"`
|
SSLID uint `json:"sslID"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
PushNode bool `json:"pushNode"`
|
||||||
|
Nodes string `json:"nodes"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebsiteSSLPush struct {
|
||||||
|
ID uint `json:"id" validate:"required"`
|
||||||
|
PushNode bool `json:"pushNode"`
|
||||||
|
Nodes string `json:"nodes"`
|
||||||
|
TaskID string `json:"taskID" validate:"required"`
|
||||||
|
Sync bool `json:"sync"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteCASearch struct {
|
type WebsiteCASearch struct {
|
||||||
@@ -157,6 +167,8 @@ type WebsiteCAObtain struct {
|
|||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
ExecShell bool `json:"execShell"`
|
ExecShell bool `json:"execShell"`
|
||||||
Shell string `json:"shell"`
|
Shell string `json:"shell"`
|
||||||
|
PushNode bool `json:"pushNode"`
|
||||||
|
Nodes string `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type WebsiteCARenew struct {
|
type WebsiteCARenew struct {
|
||||||
@@ -167,4 +179,6 @@ type WebsiteSSLFileUpload struct {
|
|||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
SSLID uint64 `json:"sslID"`
|
SSLID uint64 `json:"sslID"`
|
||||||
|
PushNode bool `json:"pushNode"`
|
||||||
|
Nodes string `json:"nodes"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -104,34 +104,36 @@ type AppDetail struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallDTO struct {
|
type AppInstallDTO struct {
|
||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
AppID uint `json:"appID"`
|
AppID uint `json:"appID"`
|
||||||
AppDetailID uint `json:"appDetailID"`
|
AppDetailID uint `json:"appDetailID"`
|
||||||
Version string `json:"version"`
|
Version string `json:"version"`
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
HttpPort int `json:"httpPort"`
|
HttpPort int `json:"httpPort"`
|
||||||
HttpsPort int `json:"httpsPort"`
|
HttpsPort int `json:"httpsPort"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
CanUpdate bool `json:"canUpdate"`
|
CanUpdate bool `json:"canUpdate"`
|
||||||
Icon string `json:"icon"`
|
Icon string `json:"icon"`
|
||||||
AppName string `json:"appName"`
|
AppName string `json:"appName"`
|
||||||
Ready int `json:"ready"`
|
Ready int `json:"ready"`
|
||||||
Total int `json:"total"`
|
Total int `json:"total"`
|
||||||
AppKey string `json:"appKey"`
|
AppKey string `json:"appKey"`
|
||||||
AppType string `json:"appType"`
|
AppType string `json:"appType"`
|
||||||
AppStatus string `json:"appStatus"`
|
AppStatus string `json:"appStatus"`
|
||||||
DockerCompose string `json:"dockerCompose"`
|
DockerCompose string `json:"dockerCompose"`
|
||||||
WebUI string `json:"webUI"`
|
WebUI string `json:"webUI"`
|
||||||
CreatedAt time.Time `json:"createdAt"`
|
CreatedAt time.Time `json:"createdAt"`
|
||||||
Favorite bool `json:"favorite"`
|
Favorite bool `json:"favorite"`
|
||||||
SortOrder int `json:"sortOrder"`
|
SortOrder int `json:"sortOrder"`
|
||||||
App AppDetail `json:"app"`
|
App AppDetail `json:"app"`
|
||||||
Container string `json:"container"`
|
Container string `json:"container"`
|
||||||
IsEdit bool `json:"isEdit"`
|
IsEdit bool `json:"isEdit"`
|
||||||
LinkDB bool `json:"linkDB"`
|
LinkDB bool `json:"linkDB"`
|
||||||
ServiceName string `json:"serviceName"`
|
ResourceKeys []string `json:"resourceKeys"`
|
||||||
|
ServiceName string `json:"serviceName"`
|
||||||
|
Env map[string]interface{} `json:"env"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AppInstallInfo struct {
|
type AppInstallInfo struct {
|
||||||
|
|||||||
@@ -16,6 +16,12 @@ type McpServerDTO struct {
|
|||||||
Volumes []request.Volume `json:"volumes"`
|
Volumes []request.Volume `json:"volumes"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type McpServerStatusDTO struct {
|
||||||
|
ID uint `json:"id"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
type McpBindDomainRes struct {
|
type McpBindDomainRes struct {
|
||||||
Domain string `json:"domain"`
|
Domain string `json:"domain"`
|
||||||
SSLID uint `json:"sslID"`
|
SSLID uint `json:"sslID"`
|
||||||
@@ -24,3 +30,11 @@ type McpBindDomainRes struct {
|
|||||||
WebsiteID uint `json:"websiteID"`
|
WebsiteID uint `json:"websiteID"`
|
||||||
ConnUrl string `json:"connUrl"`
|
ConnUrl string `json:"connUrl"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type McpServerConnectionTestRes struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Endpoint string `json:"endpoint"`
|
||||||
|
OutputTransport string `json:"outputTransport"`
|
||||||
|
ProtocolVersion string `json:"protocolVersion,omitempty"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|||||||
@@ -17,6 +17,17 @@ type NginxParam struct {
|
|||||||
Params []string `json:"params"`
|
Params []string `json:"params"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NginxBrotliRes carries the brotli settings together with where they live.
|
||||||
|
// ManagedExternally is true when the user defined brotli by hand, in which
|
||||||
|
// case the panel only reports the values and must not write its own copy.
|
||||||
|
// ManagedUnavailable is true when the panel could not wire the managed
|
||||||
|
// configuration into nginx.conf at all, so the reported values are inert.
|
||||||
|
type NginxBrotliRes struct {
|
||||||
|
Params []NginxParam `json:"params"`
|
||||||
|
ManagedExternally bool `json:"managedExternally"`
|
||||||
|
ManagedUnavailable bool `json:"managedUnavailable"`
|
||||||
|
}
|
||||||
|
|
||||||
type NginxAuthRes struct {
|
type NginxAuthRes struct {
|
||||||
Enable bool `json:"enable"`
|
Enable bool `json:"enable"`
|
||||||
Items []dto.NginxAuth `json:"items"`
|
Items []dto.NginxAuth `json:"items"`
|
||||||
@@ -69,16 +80,25 @@ type NginxProxyCache struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NginxModule struct {
|
type NginxModule struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Script string `json:"script"`
|
Custom bool `json:"custom"`
|
||||||
Packages string `json:"packages"`
|
Script string `json:"script"`
|
||||||
Params string `json:"params"`
|
Packages string `json:"packages"`
|
||||||
Enable bool `json:"enable"`
|
Params string `json:"params"`
|
||||||
|
Enable bool `json:"enable"`
|
||||||
|
BuildMode string `json:"buildMode"`
|
||||||
|
Provider string `json:"provider"`
|
||||||
|
LoadOrder int `json:"loadOrder"`
|
||||||
|
BuildStatus string `json:"buildStatus"`
|
||||||
|
LoadStatus string `json:"loadStatus"`
|
||||||
|
Artifacts []dto.NginxModuleArtifact `json:"artifacts"`
|
||||||
|
LastError string `json:"lastError"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxBuildConfig struct {
|
type NginxBuildConfig struct {
|
||||||
Mirror string `json:"mirror"`
|
Mirror string `json:"mirror"`
|
||||||
Modules []NginxModule `json:"modules"`
|
DynamicSupported bool `json:"dynamicSupported"`
|
||||||
|
Modules []NginxModule `json:"modules"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type NginxConfigRes struct {
|
type NginxConfigRes struct {
|
||||||
|
|||||||
@@ -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"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
package dto
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type RuntimeDiagnosticsSummary struct {
|
||||||
|
RSS uint64 `json:"rss"`
|
||||||
|
HeapAlloc uint64 `json:"heapAlloc"`
|
||||||
|
HeapObjects uint64 `json:"heapObjects"`
|
||||||
|
Goroutines int `json:"goroutines"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuntimeGoroutineGroup struct {
|
||||||
|
State string `json:"state"`
|
||||||
|
Top string `json:"top"`
|
||||||
|
Count int `json:"count"`
|
||||||
|
Stack []string `json:"stack"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuntimeGoroutineSnapshot struct {
|
||||||
|
Total int `json:"total"`
|
||||||
|
GroupCount int `json:"groupCount"`
|
||||||
|
Truncated bool `json:"truncated"`
|
||||||
|
CapturedAt time.Time `json:"capturedAt"`
|
||||||
|
Goroutines []RuntimeGoroutineGroup `json:"goroutines"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuntimeProfileCreate struct {
|
||||||
|
Type string `json:"type" validate:"required,oneof=cpu heap goroutine mutex block"`
|
||||||
|
Duration int `json:"duration" validate:"omitempty,min=5,max=30"`
|
||||||
|
}
|
||||||
@@ -35,7 +35,7 @@ type SettingUpdate struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AgentSettingUpdate 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"`
|
Value string `json:"value"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ type RootCertOperate struct {
|
|||||||
ID uint `json:"id"`
|
ID uint `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Mode string `json:"mode"`
|
Mode string `json:"mode"`
|
||||||
EncryptionMode string `json:"encryptionMode" validate:"required,oneof=rsa ed25519 ecdsa dsa"`
|
EncryptionMode string `json:"encryptionMode"`
|
||||||
PassPhrase string `json:"passPhrase"`
|
PassPhrase string `json:"passPhrase"`
|
||||||
PublicKey string `json:"publicKey"`
|
PublicKey string `json:"publicKey"`
|
||||||
PrivateKey string `json:"privateKey"`
|
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"`
|
||||||
|
}
|
||||||
@@ -7,6 +7,8 @@ type AgentAccount struct {
|
|||||||
APIKey string `json:"apiKey"`
|
APIKey string `json:"apiKey"`
|
||||||
BaseURL string `json:"baseUrl"`
|
BaseURL string `json:"baseUrl"`
|
||||||
APIType string `json:"apiType"`
|
APIType string `json:"apiType"`
|
||||||
|
AuthMode string `json:"authMode"`
|
||||||
|
VerifyModel string `json:"verifyModel"`
|
||||||
RememberAPIKey bool `json:"rememberApiKey"`
|
RememberAPIKey bool `json:"rememberApiKey"`
|
||||||
Verified bool `json:"verified"`
|
Verified bool `json:"verified"`
|
||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
|
|||||||
@@ -2,14 +2,10 @@ package model
|
|||||||
|
|
||||||
type AgentAccountModel struct {
|
type AgentAccountModel struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
AccountID uint `json:"accountId" gorm:"index"`
|
AccountID uint `json:"accountId" gorm:"index"`
|
||||||
Model string `json:"model" gorm:"index"`
|
Model string `json:"model" gorm:"index"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
SortOrder int `json:"sortOrder" gorm:"index"`
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Reasoning bool `json:"reasoning"`
|
|
||||||
Input string `json:"input" gorm:"type:text"`
|
|
||||||
SortOrder int `json:"sortOrder" gorm:"index"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (AgentAccountModel) TableName() string {
|
func (AgentAccountModel) TableName() string {
|
||||||
|
|||||||
+27
-10
@@ -1,5 +1,12 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
type Alert struct {
|
type Alert struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
@@ -18,10 +25,11 @@ type Alert struct {
|
|||||||
|
|
||||||
type AlertTask struct {
|
type AlertTask struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||||
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
Quota string `gorm:"type:varchar(64)" json:"quota"`
|
||||||
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
|
||||||
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
|
||||||
|
DeliveryLogID *uint `gorm:"uniqueIndex" json:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type AlertLog struct {
|
type AlertLog struct {
|
||||||
@@ -41,12 +49,21 @@ type AlertLog struct {
|
|||||||
|
|
||||||
type AlertConfig struct {
|
type AlertConfig struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
UID string `gorm:"type:varchar(64);not null;uniqueIndex" json:"uid"`
|
||||||
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
Type string `gorm:"type:varchar(64);not null" json:"type"`
|
||||||
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
Title string `gorm:"type:varchar(64);not null" json:"title"`
|
||||||
Config string `gorm:"type:varchar(256);not null" json:"config"`
|
Status string `gorm:"type:varchar(64);not null" json:"status"`
|
||||||
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
|
Config string `gorm:"type:text;not null" json:"config"`
|
||||||
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
|
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 {
|
type LoginLog struct {
|
||||||
|
|||||||
@@ -31,4 +31,5 @@ type BackupRecord struct {
|
|||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Message string `json:"message"`
|
Message string `json:"message"`
|
||||||
Description string `json:"description"`
|
Description string `json:"description"`
|
||||||
|
Args string `gorm:"not null;default:''" json:"args"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ type ComposeTemplate struct {
|
|||||||
type Compose struct {
|
type Compose struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
|
IsPinned bool `json:"isPinned"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
type DatabaseUser struct {
|
||||||
|
BaseModel
|
||||||
|
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user"`
|
||||||
|
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user"`
|
||||||
|
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user"`
|
||||||
|
Host string `json:"host" gorm:"uniqueIndex:idx_database_user"`
|
||||||
|
Password string `json:"password"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
IsDelete bool `json:"isDelete"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DatabaseUserGrant struct {
|
||||||
|
BaseModel
|
||||||
|
Type string `json:"type" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||||
|
Database string `json:"database" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||||
|
DBName string `json:"dbName" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||||
|
Username string `json:"username" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||||
|
Host string `json:"host" gorm:"not null;uniqueIndex:idx_database_user_grant"`
|
||||||
|
}
|
||||||
+46
-13
@@ -1,18 +1,51 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
type Firewall struct {
|
type DockerPortGuardPolicy struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
|
|
||||||
Type string `json:"type"`
|
UUID string `gorm:"uniqueIndex" json:"uuid"`
|
||||||
Port string `json:"port"` // Deprecated
|
ReadOnly bool `gorm:"default:false;uniqueIndex:idx_docker_port_guard_endpoint" json:"-"`
|
||||||
Address string `json:"address"` // Deprecated
|
Family string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"family"`
|
||||||
|
HostIP string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostIP"`
|
||||||
Chain string `json:"chain"`
|
HostPort uint16 `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"hostPort"`
|
||||||
Protocol string `json:"protocol"`
|
Protocol string `gorm:"uniqueIndex:idx_docker_port_guard_endpoint" json:"protocol"`
|
||||||
SrcIP string `json:"srcIP"`
|
Mode string `json:"mode"`
|
||||||
SrcPort string `json:"srcPort"`
|
Sources string `gorm:"type:text" json:"-"`
|
||||||
DstIP string `json:"dstIP"`
|
Description string `gorm:"type:text" json:"description"`
|
||||||
DstPort string `json:"dstPort"`
|
NativeAction string `gorm:"default:''" json:"-"`
|
||||||
Strategy string `gorm:"not null" json:"strategy"`
|
NativeRules string `gorm:"type:text" json:"-"`
|
||||||
Description string `json:"description"`
|
Sequence int64 `gorm:"default:0" json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingRule struct {
|
||||||
|
BaseModel
|
||||||
|
|
||||||
|
Family string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"family"`
|
||||||
|
Protocol string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"protocol"`
|
||||||
|
Port string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"port"`
|
||||||
|
TargetIP string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetIP"`
|
||||||
|
TargetPort string `gorm:"uniqueIndex:idx_forwarding_rule_identity" json:"targetPort"`
|
||||||
|
Interface string `gorm:"default:'';uniqueIndex:idx_forwarding_rule_identity" json:"interface"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRule struct {
|
||||||
|
UUID string `gorm:"primaryKey" json:"uuid"`
|
||||||
|
Family string `json:"family"`
|
||||||
|
|
||||||
|
Protocol string `json:"protocol"`
|
||||||
|
SourceAddress string `json:"sourceAddress"`
|
||||||
|
SourcePort string `json:"sourcePort"`
|
||||||
|
DestinationAddress string `json:"destinationAddress"`
|
||||||
|
DestinationPort string `json:"destinationPort"`
|
||||||
|
Interface string `json:"interface"`
|
||||||
|
ConnectionStates string `gorm:"type:text" json:"connectionStates"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Description string `gorm:"type:text" json:"description"`
|
||||||
|
CompatibilityError string `gorm:"type:text" json:"compatibilityError,omitempty"`
|
||||||
|
Priority *int `json:"priority,omitempty"`
|
||||||
|
Sequence *int64 `gorm:"index" json:"sequence,omitempty"`
|
||||||
|
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Revision uint `gorm:"default:1" json:"revision"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,4 +8,6 @@ type Ftp struct {
|
|||||||
Status string `gorm:"not null" json:"status"`
|
Status string `gorm:"not null" json:"status"`
|
||||||
Path string `gorm:"not null" json:"path"`
|
Path string `gorm:"not null" json:"path"`
|
||||||
Description string `gorm:"not null" json:"description"`
|
Description string `gorm:"not null" json:"description"`
|
||||||
|
UID uint `gorm:"column:uid;not null;default:1000" json:"-"`
|
||||||
|
GID uint `gorm:"column:gid;not null;default:1000" json:"-"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,4 +18,7 @@ type McpServer struct {
|
|||||||
StreamableHttpPath string `json:"streamableHttpPath"`
|
StreamableHttpPath string `json:"streamableHttpPath"`
|
||||||
OutputTransport string `json:"outputTransport"`
|
OutputTransport string `json:"outputTransport"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
|
GatewayImage string `json:"gatewayImage"`
|
||||||
|
ProtocolVersion string `json:"protocolVersion"`
|
||||||
|
GatewayArgs string `json:"gatewayArgs"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
type MonitorBase struct {
|
type MonitorBase struct {
|
||||||
BaseModel
|
BaseModel
|
||||||
Cpu float64 `json:"cpu"`
|
Cpu float64 `json:"cpu"`
|
||||||
@@ -33,14 +35,78 @@ type MonitorNetwork struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type MonitorGPU 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
|
BaseModel
|
||||||
ProductName string `json:"productName"`
|
ProductName string `json:"productName"`
|
||||||
GPUUtil float64 `json:"gpuUtil"`
|
GPUUtil *float64 `json:"gpuUtil"`
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature *float64 `json:"temperature"`
|
||||||
PowerDraw float64 `json:"powerDraw"`
|
PowerDraw *float64 `json:"powerDraw"`
|
||||||
MaxPowerLimit float64 `json:"maxPowerLimit"`
|
MaxPowerLimit *float64 `json:"maxPowerLimit"`
|
||||||
MemUsed float64 `json:"memUsed"`
|
MemUsed *float64 `json:"memUsed"`
|
||||||
MemTotal float64 `json:"memTotal"`
|
MemTotal *float64 `json:"memTotal"`
|
||||||
FanSpeed int `json:"fanSpeed"`
|
FanSpeed *float64 `json:"fanSpeed"`
|
||||||
Processes string `json:"processes"`
|
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:"-"`
|
||||||
|
|
||||||
|
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"
|
||||||
|
}
|
||||||
+376
-324
@@ -1,319 +1,262 @@
|
|||||||
package provider
|
package provider
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Model struct {
|
type APIConfig struct {
|
||||||
ID string
|
APIType string
|
||||||
Name string
|
BaseURL string
|
||||||
ContextWindow int
|
EditableBaseURL bool
|
||||||
MaxTokens int
|
DiscoverModels bool
|
||||||
Reasoning bool
|
DefaultAuthMode string
|
||||||
Input []string
|
AuthModes []string
|
||||||
|
Models []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
type RuntimeDefault struct {
|
const (
|
||||||
APIType string
|
AuthModeBearer = "bearer"
|
||||||
ContextWindow int
|
AuthModeXAPIKey = "x-api-key"
|
||||||
MaxTokens int
|
)
|
||||||
Input []string
|
|
||||||
|
type Model struct {
|
||||||
|
ID string
|
||||||
|
Name string
|
||||||
}
|
}
|
||||||
|
|
||||||
type Meta struct {
|
type Meta struct {
|
||||||
Key string
|
Key string
|
||||||
DisplayName string
|
DisplayName string
|
||||||
|
DisplayNameKey string
|
||||||
Sort uint
|
Sort uint
|
||||||
DefaultBaseURL string
|
DefaultAPIType string
|
||||||
|
APIConfigs []APIConfig
|
||||||
EnvKey string
|
EnvKey string
|
||||||
Default RuntimeDefault
|
|
||||||
Models []Model
|
Models []Model
|
||||||
}
|
}
|
||||||
|
|
||||||
var catalog = map[string]Meta{
|
var catalog = map[string]Meta{
|
||||||
"custom": {
|
"custom": {
|
||||||
Key: "custom",
|
Key: "custom", DisplayName: "Custom", Sort: 10, DefaultAPIType: "openai-completions", EnvKey: "CUSTOM_API_KEY",
|
||||||
DisplayName: "Custom",
|
APIConfigs: editableAPIConfigs(true, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "dashscope-images", "openai-embeddings"),
|
||||||
Sort: 10,
|
|
||||||
DefaultBaseURL: "",
|
|
||||||
EnvKey: "CUSTOM_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 128000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{},
|
|
||||||
},
|
},
|
||||||
"ollama": {
|
"ollama": {
|
||||||
Key: "ollama",
|
Key: "ollama", DisplayName: "Ollama", Sort: 15, DefaultAPIType: "openai-responses",
|
||||||
DisplayName: "Ollama",
|
APIConfigs: editableAPIConfigs(false, "openai-responses", "openai-completions", "openai-embeddings"),
|
||||||
Sort: 15,
|
},
|
||||||
Default: RuntimeDefault{
|
// llmman (https://github.com/llmmanorg/llmman): local runner with Ollama/OpenAI-compatible routes on 127.0.0.1:17434.
|
||||||
APIType: "openai-responses",
|
"llmman": {
|
||||||
ContextWindow: 160000,
|
Key: "llmman", DisplayName: "llmman", Sort: 16, DefaultAPIType: "openai-responses",
|
||||||
MaxTokens: 8192,
|
APIConfigs: []APIConfig{
|
||||||
Input: []string{"text"},
|
{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": {
|
"vllm": {
|
||||||
Key: "vllm",
|
Key: "vllm", DisplayName: "vLLM", Sort: 20, DefaultAPIType: "openai-completions", EnvKey: "VLLM_API_KEY",
|
||||||
DisplayName: "vLLM",
|
APIConfigs: editableAPIConfigs(false, "openai-completions", "openai-responses", "anthropic-messages", "openai-images", "openai-embeddings"),
|
||||||
Sort: 20,
|
|
||||||
DefaultBaseURL: "",
|
|
||||||
EnvKey: "VLLM_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 128000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{},
|
|
||||||
},
|
},
|
||||||
"deepseek": {
|
"deepseek": {
|
||||||
Key: "deepseek",
|
Key: "deepseek", DisplayName: "DeepSeek", Sort: 25, DefaultAPIType: "openai-completions", EnvKey: "DEEPSEEK_API_KEY",
|
||||||
DisplayName: "DeepSeek",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 25,
|
{APIType: "openai-completions", BaseURL: "https://api.deepseek.com"},
|
||||||
DefaultBaseURL: "https://api.deepseek.com/v1",
|
{APIType: "openai-responses", BaseURL: "https://api.deepseek.com"},
|
||||||
EnvKey: "DEEPSEEK_API_KEY",
|
anthropicAPIConfig("https://api.deepseek.com/anthropic", AuthModeXAPIKey),
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 131072,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
|
|
||||||
{ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "deepseek-v4-flash", Name: "deepseek-v4-flash"}, {ID: "deepseek-v4-pro", Name: "deepseek-v4-pro"}},
|
||||||
},
|
},
|
||||||
"bailian-coding-plan": {
|
"bailian-coding-plan": {
|
||||||
Key: "bailian-coding-plan",
|
Key: "bailian-coding-plan", DisplayNameKey: "AIProviderBailianCodingPlan", Sort: 30, DefaultAPIType: "openai-completions", EnvKey: "QWEN_API_KEY",
|
||||||
DisplayName: "阿里云百炼 Coding Plan",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 30,
|
{APIType: "openai-completions", BaseURL: "https://coding.dashscope.aliyuncs.com/v1"},
|
||||||
DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1",
|
anthropicAPIConfig("https://coding.dashscope.aliyuncs.com/apps/anthropic", AuthModeBearer),
|
||||||
EnvKey: "QWEN_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
},
|
||||||
Models: []Model{
|
Models: []Model{
|
||||||
{ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true},
|
{ID: "qwen3-coder-plus", Name: "Qwen3-Coder-Plus"},
|
||||||
{ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true},
|
{ID: "qwen3-max-2026-01-23", Name: "Qwen3-Max-2026-01-23"},
|
||||||
{ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true},
|
{ID: "qwen3-coder-next", Name: "Qwen3-Coder-Next"},
|
||||||
{ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true},
|
{ID: "glm-4.7", Name: "GLM-4.7"},
|
||||||
{ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5"},
|
||||||
{ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true},
|
{ID: "qwen3.5-plus", Name: "Qwen3.5-Plus"},
|
||||||
{ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true},
|
{ID: "glm-5", Name: "GLM-5"},
|
||||||
{ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
|
{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": {
|
"ark-coding-plan": {
|
||||||
Key: "ark-coding-plan",
|
Key: "ark-coding-plan", DisplayNameKey: "AIProviderArkCodingPlan", Sort: 35, DefaultAPIType: "openai-completions", EnvKey: "ARK_API_KEY",
|
||||||
DisplayName: "方舟 Coding Plan",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 35,
|
{APIType: "openai-completions", BaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3"},
|
||||||
DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3",
|
anthropicAPIConfig("https://ark.cn-beijing.volces.com/api/coding", AuthModeBearer),
|
||||||
EnvKey: "ARK_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 4096,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
},
|
||||||
Models: []Model{
|
Models: []Model{
|
||||||
{ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096},
|
{ID: "ark-code-latest", Name: "Ark Coding Plan"}, {ID: "doubao-seed-code", Name: "Doubao Seed Code"},
|
||||||
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096},
|
{ID: "glm-4.7", Name: "GLM 4.7 Coding"}, {ID: "kimi-k2-thinking", Name: "Kimi K2 Thinking"},
|
||||||
{ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096},
|
{ID: "kimi-k2.5", Name: "Kimi K2.5 Coding"}, {ID: "doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview"},
|
||||||
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096},
|
|
||||||
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096},
|
|
||||||
{ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"zai": {
|
"zai": {
|
||||||
Key: "zai",
|
Key: "zai", DisplayName: "Z.ai", Sort: 40, DefaultAPIType: "openai-completions", EnvKey: "ZAI_API_KEY",
|
||||||
DisplayName: "Z.ai",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 40,
|
{APIType: "openai-completions", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||||
DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4",
|
{APIType: "openai-images", BaseURL: "https://open.bigmodel.cn/api/paas/v4", EditableBaseURL: true},
|
||||||
EnvKey: "ZAI_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 204800,
|
|
||||||
MaxTokens: 131072,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "zai/glm-5", Name: "GLM-5", Reasoning: true},
|
|
||||||
{ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true},
|
|
||||||
{ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true},
|
|
||||||
{ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "glm-5", Name: "GLM-5"}, {ID: "glm-4.7", Name: "GLM-4.7"}, {ID: "glm-4.7-flash", Name: "GLM-4.7-Flash"}, {ID: "glm-4.7-flashx", Name: "GLM-4.7-FlashX"}},
|
||||||
},
|
},
|
||||||
"minimax": {
|
"minimax": {
|
||||||
Key: "minimax",
|
Key: "minimax", DisplayName: "MiniMax (CN)", Sort: 45, DefaultAPIType: "anthropic-messages", EnvKey: "MINIMAX_API_KEY",
|
||||||
DisplayName: "MiniMax (CN)",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 45,
|
anthropicAPIConfig("https://api.minimaxi.com/anthropic", AuthModeXAPIKey, AuthModeBearer),
|
||||||
DefaultBaseURL: "https://api.minimaxi.com/anthropic",
|
{APIType: "openai-completions", BaseURL: "https://api.minimaxi.com/v1"},
|
||||||
EnvKey: "MINIMAX_API_KEY",
|
{APIType: "minimax-images", BaseURL: "https://api.minimaxi.com"},
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "anthropic-messages",
|
|
||||||
ContextWindow: 200000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
|
|
||||||
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
|
|
||||||
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
|
|
||||||
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "MiniMax-M3", Name: "MiniMax M3"}, {ID: "MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}},
|
||||||
},
|
},
|
||||||
"xiaomi": {
|
"xiaomi": {
|
||||||
Key: "xiaomi",
|
Key: "xiaomi", DisplayName: "Xiaomi", Sort: 46, DefaultAPIType: "openai-completions", EnvKey: "XIAOMI_API_KEY",
|
||||||
DisplayName: "Xiaomi",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 46,
|
{APIType: "openai-completions", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||||
DefaultBaseURL: "https://api.xiaomimimo.com/v1",
|
{APIType: "openai-responses", BaseURL: "https://api.xiaomimimo.com/v1"},
|
||||||
EnvKey: "XIAOMI_API_KEY",
|
anthropicAPIConfig("https://api.xiaomimimo.com/anthropic", AuthModeBearer),
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 262144,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Input: []string{"text"}},
|
|
||||||
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 1048576, MaxTokens: 32000, Reasoning: true, Input: []string{"text"}},
|
|
||||||
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 32000, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "mimo-v2.5", Name: "Xiaomi MiMo V2.5"}, {ID: "mimo-v2.5-pro", Name: "Xiaomi MiMo V2.5 Pro"}},
|
||||||
},
|
},
|
||||||
"kimi": {
|
"kimi": {
|
||||||
Key: "kimi",
|
Key: "kimi", DisplayName: "Kimi (CN)", Sort: 50, DefaultAPIType: "openai-completions", EnvKey: "KIMI_API_KEY",
|
||||||
DisplayName: "Kimi (CN)",
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.cn/v1"}},
|
||||||
Sort: 50,
|
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"}},
|
||||||
DefaultBaseURL: "https://api.moonshot.cn/v1",
|
|
||||||
EnvKey: "KIMI_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text", "image"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
|
|
||||||
{ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
|
||||||
{ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"kimi-coding": {
|
"kimi-coding": {
|
||||||
Key: "kimi-coding",
|
Key: "kimi-coding", DisplayName: "Kimi Coding", Sort: 51, DefaultAPIType: "anthropic-messages", EnvKey: "KIMI_API_KEY",
|
||||||
DisplayName: "Kimi Coding",
|
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.kimi.com/coding/", AuthModeXAPIKey)},
|
||||||
Sort: 51,
|
Models: []Model{{ID: "kimi-code", Name: "Kimi Code"}, {ID: "k2p5", Name: "Kimi K2.5"}},
|
||||||
DefaultBaseURL: "https://api.kimi.com/coding/",
|
|
||||||
EnvKey: "KIMI_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "anthropic-messages",
|
|
||||||
ContextWindow: 262144,
|
|
||||||
MaxTokens: 32768,
|
|
||||||
Input: []string{"text", "image"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "kimi-coding/kimi-code", Name: "Kimi Code", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", ContextWindow: 262144, MaxTokens: 32768, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"openai": {
|
"openai": {
|
||||||
Key: "openai",
|
Key: "openai", DisplayName: "OpenAI", Sort: 55, DefaultAPIType: "openai-responses", EnvKey: "OPENAI_API_KEY",
|
||||||
DisplayName: "OpenAI",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 55,
|
{APIType: "openai-responses", BaseURL: "https://api.openai.com/v1"},
|
||||||
DefaultBaseURL: "https://api.openai.com/v1",
|
{APIType: "openai-completions", BaseURL: "https://api.openai.com/v1"},
|
||||||
EnvKey: "OPENAI_API_KEY",
|
{APIType: "openai-images", BaseURL: "https://api.openai.com/v1"},
|
||||||
Default: RuntimeDefault{
|
{APIType: "openai-embeddings", BaseURL: "https://api.openai.com/v1", Models: []Model{
|
||||||
APIType: "openai-responses",
|
{ID: "text-embedding-3-small", Name: "text-embedding-3-small"},
|
||||||
ContextWindow: 272000,
|
{ID: "text-embedding-3-large", Name: "text-embedding-3-large"},
|
||||||
MaxTokens: 128000,
|
}},
|
||||||
Input: []string{"text", "image"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "openai/gpt-5.4", Name: "gpt-5.4", ContextWindow: 272000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
{ID: "openai/gpt-5.4-pro", Name: "gpt-5.4-pro", ContextWindow: 1050000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
{ID: "openai/gpt-5.4-mini", Name: "gpt-5.4-mini", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
{ID: "openai/gpt-5.4-nano", Name: "gpt-5.4-nano", ContextWindow: 400000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "gpt-5.4", Name: "gpt-5.4"}, {ID: "gpt-5.4-pro", Name: "gpt-5.4-pro"}, {ID: "gpt-5.4-mini", Name: "gpt-5.4-mini"}, {ID: "gpt-5.4-nano", Name: "gpt-5.4-nano"}},
|
||||||
},
|
},
|
||||||
"openrouter": {
|
"openrouter": {
|
||||||
Key: "openrouter",
|
Key: "openrouter", DisplayName: "OpenRouter", Sort: 56, DefaultAPIType: "openai-completions", EnvKey: "OPENROUTER_API_KEY",
|
||||||
DisplayName: "OpenRouter",
|
APIConfigs: []APIConfig{
|
||||||
Sort: 56,
|
{APIType: "openai-completions", BaseURL: "https://openrouter.ai/api/v1"},
|
||||||
DefaultBaseURL: "https://openrouter.ai/api/v1",
|
{APIType: "openrouter-images", BaseURL: "https://openrouter.ai"},
|
||||||
EnvKey: "OPENROUTER_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 128000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "openrouter/free", Name: "openrouter/free"},
|
|
||||||
{ID: "openrouter/auto", Name: "openrouter/auto"},
|
|
||||||
},
|
},
|
||||||
|
Models: []Model{{ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}},
|
||||||
},
|
},
|
||||||
"anthropic": {
|
"anthropic": {
|
||||||
Key: "anthropic",
|
Key: "anthropic", DisplayName: "Anthropic", Sort: 60, DefaultAPIType: "anthropic-messages", EnvKey: "ANTHROPIC_API_KEY",
|
||||||
DisplayName: "Anthropic",
|
APIConfigs: []APIConfig{anthropicAPIConfig("https://api.anthropic.com", AuthModeXAPIKey)},
|
||||||
Sort: 60,
|
Models: []Model{{ID: "claude-sonnet-4-6", Name: "Claude Sonnet 4.6"}, {ID: "claude-opus-4-6", Name: "Claude Opus 4.6"}, {ID: "claude-opus-4-5", Name: "Claude Opus 4.5"}, {ID: "claude-sonnet-4-5", Name: "Claude Sonnet 4.5"}, {ID: "claude-haiku-4-5", Name: "Claude Haiku 4.5"}},
|
||||||
DefaultBaseURL: "https://api.anthropic.com",
|
|
||||||
EnvKey: "ANTHROPIC_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "anthropic-messages",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text", "image"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "anthropic/claude-sonnet-4-6", Name: "Claude Sonnet 4.6", Reasoning: true},
|
|
||||||
{ID: "anthropic/claude-opus-4-6", Name: "Claude Opus 4.6", Reasoning: true},
|
|
||||||
{ID: "anthropic/claude-opus-4-5", Name: "Claude Opus 4.5"},
|
|
||||||
{ID: "anthropic/claude-sonnet-4-5", Name: "Claude Sonnet 4.5"},
|
|
||||||
{ID: "anthropic/claude-haiku-4-5", Name: "Claude Haiku 4.5"},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"gemini": {
|
"gemini": {
|
||||||
Key: "gemini",
|
Key: "gemini", DisplayName: "Gemini", Sort: 65, DefaultAPIType: "gemini-generate-content", EnvKey: "GEMINI_API_KEY",
|
||||||
DisplayName: "Gemini",
|
APIConfigs: []APIConfig{{APIType: "gemini-generate-content", BaseURL: "https://generativelanguage.googleapis.com"}},
|
||||||
Sort: 65,
|
Models: []Model{{ID: "gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, {ID: "gemini-flash-latest", Name: "Gemini Flash Latest"}, {ID: "gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}},
|
||||||
DefaultBaseURL: "https://generativelanguage.googleapis.com",
|
|
||||||
EnvKey: "GEMINI_API_KEY",
|
|
||||||
Default: RuntimeDefault{
|
|
||||||
APIType: "openai-completions",
|
|
||||||
ContextWindow: 256000,
|
|
||||||
MaxTokens: 8192,
|
|
||||||
Input: []string{"text", "image"},
|
|
||||||
},
|
|
||||||
Models: []Model{
|
|
||||||
{ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true},
|
|
||||||
{ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"},
|
|
||||||
{ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
"moonshot": {
|
"moonshot": {
|
||||||
Key: "moonshot",
|
Key: "moonshot", DisplayName: "Moonshot (Global)", Sort: 70, DefaultAPIType: "openai-completions", EnvKey: "MOONSHOT_API_KEY",
|
||||||
DisplayName: "Moonshot (Global)",
|
APIConfigs: []APIConfig{{APIType: "openai-completions", BaseURL: "https://api.moonshot.ai/v1"}},
|
||||||
Sort: 70,
|
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"}},
|
||||||
DefaultBaseURL: "https://api.moonshot.ai/v1",
|
},
|
||||||
EnvKey: "MOONSHOT_API_KEY",
|
"bailian": {
|
||||||
Default: RuntimeDefault{
|
Key: "bailian", DisplayNameKey: "AIProviderBailian", Sort: 31, DefaultAPIType: "openai-completions", EnvKey: "DASHSCOPE_API_KEY",
|
||||||
APIType: "openai-completions",
|
APIConfigs: []APIConfig{
|
||||||
ContextWindow: 256000,
|
{
|
||||||
MaxTokens: 8192,
|
APIType: "openai-completions", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
Input: []string{"text"},
|
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"}},
|
||||||
Models: []Model{
|
},
|
||||||
{ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true},
|
{
|
||||||
{ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"},
|
APIType: "openai-responses", BaseURL: "https://dashscope.aliyuncs.com/compatible-mode/v1",
|
||||||
{ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true},
|
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(discoverModels bool, apiTypes ...string) []APIConfig {
|
||||||
|
configs := make([]APIConfig, 0, len(apiTypes))
|
||||||
|
for _, apiType := range apiTypes {
|
||||||
|
if apiType == "anthropic-messages" {
|
||||||
|
config := anthropicAPIConfig("", AuthModeXAPIKey, AuthModeBearer)
|
||||||
|
config.EditableBaseURL = true
|
||||||
|
configs = append(configs, config)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configs = append(configs, APIConfig{
|
||||||
|
APIType: apiType,
|
||||||
|
EditableBaseURL: true,
|
||||||
|
DiscoverModels: discoverModels && (apiType == "openai-completions" || apiType == "openai-responses"),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return configs
|
||||||
|
}
|
||||||
|
|
||||||
|
func anthropicAPIConfig(baseURL, defaultAuthMode string, additionalAuthModes ...string) APIConfig {
|
||||||
|
authModes := []string{defaultAuthMode}
|
||||||
|
for _, authMode := range additionalAuthModes {
|
||||||
|
if authMode != defaultAuthMode {
|
||||||
|
authModes = append(authModes, authMode)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APIConfig{
|
||||||
|
APIType: "anthropic-messages",
|
||||||
|
BaseURL: baseURL,
|
||||||
|
DefaultAuthMode: defaultAuthMode,
|
||||||
|
AuthModes: authModes,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func Get(key string) (Meta, bool) {
|
func Get(key string) (Meta, bool) {
|
||||||
@@ -332,12 +275,148 @@ func All() map[string]Meta {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func DefaultBaseURL(key string) (string, bool) {
|
func FindAPIConfig(key, apiType string) (APIConfig, bool) {
|
||||||
meta, ok := catalog[key]
|
meta, ok := catalog[key]
|
||||||
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
|
if !ok {
|
||||||
|
return APIConfig{}, false
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(apiType)
|
||||||
|
if target == "" {
|
||||||
|
target = meta.DefaultAPIType
|
||||||
|
}
|
||||||
|
for _, config := range meta.APIConfigs {
|
||||||
|
if config.APIType == target {
|
||||||
|
config.AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
|
config.Models = append([]Model(nil), config.Models...)
|
||||||
|
return config, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return APIConfig{}, false
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultModels(key, apiType string) []Model {
|
||||||
|
meta, ok := catalog[key]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
target := strings.TrimSpace(apiType)
|
||||||
|
if target == "" {
|
||||||
|
target = meta.DefaultAPIType
|
||||||
|
}
|
||||||
|
for _, config := range meta.APIConfigs {
|
||||||
|
if config.APIType != target {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(config.Models) > 0 {
|
||||||
|
return append([]Model(nil), config.Models...)
|
||||||
|
}
|
||||||
|
if IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
return append([]Model(nil), meta.Models...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveAuthMode(provider, apiType, requested string) (string, error) {
|
||||||
|
config, ok := FindAPIConfig(provider, apiType)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("provider %s does not support api type %s", provider, apiType)
|
||||||
|
}
|
||||||
|
if config.APIType != "anthropic-messages" {
|
||||||
|
return "", nil
|
||||||
|
}
|
||||||
|
authMode := strings.TrimSpace(requested)
|
||||||
|
if authMode == "" {
|
||||||
|
authMode = config.DefaultAuthMode
|
||||||
|
}
|
||||||
|
for _, allowed := range config.AuthModes {
|
||||||
|
if authMode == allowed {
|
||||||
|
return authMode, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", fmt.Errorf("provider %s does not support auth mode %s", provider, authMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultBaseURL(key string) (string, bool) {
|
||||||
|
config, ok := FindAPIConfig(key, "")
|
||||||
|
if !ok || strings.TrimSpace(config.BaseURL) == "" {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
return meta.DefaultBaseURL, true
|
return config.BaseURL, true
|
||||||
|
}
|
||||||
|
|
||||||
|
func DefaultAPIType(key string) string {
|
||||||
|
meta, ok := catalog[key]
|
||||||
|
if !ok || strings.TrimSpace(meta.DefaultAPIType) == "" {
|
||||||
|
return "openai-completions"
|
||||||
|
}
|
||||||
|
return meta.DefaultAPIType
|
||||||
|
}
|
||||||
|
|
||||||
|
func ResolveBaseURL(key, apiType, requested string) (string, error) {
|
||||||
|
config, ok := FindAPIConfig(key, apiType)
|
||||||
|
if !ok {
|
||||||
|
return "", fmt.Errorf("provider %s does not support api type %s", key, apiType)
|
||||||
|
}
|
||||||
|
if !config.EditableBaseURL {
|
||||||
|
return strings.TrimRight(config.BaseURL, "/"), nil
|
||||||
|
}
|
||||||
|
baseURL := strings.TrimSpace(requested)
|
||||||
|
if baseURL == "" {
|
||||||
|
baseURL = config.BaseURL
|
||||||
|
}
|
||||||
|
if baseURL == "" {
|
||||||
|
return "", fmt.Errorf("base url is required")
|
||||||
|
}
|
||||||
|
parsed, err := url.Parse(baseURL)
|
||||||
|
if err != nil || parsed.Scheme == "" || parsed.Host == "" {
|
||||||
|
return "", fmt.Errorf("invalid base url")
|
||||||
|
}
|
||||||
|
if key == "custom" && (IsImageAPIType(config.APIType) || IsEmbeddingAPIType(config.APIType)) {
|
||||||
|
return baseURL, nil
|
||||||
|
}
|
||||||
|
parsed.Path = normalizeEndpointPath(config.APIType, parsed.Path)
|
||||||
|
parsed.RawQuery = ""
|
||||||
|
parsed.Fragment = ""
|
||||||
|
return strings.TrimRight(parsed.String(), "/"), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeEndpointPath(apiType, value string) string {
|
||||||
|
if IsImageAPIType(apiType) {
|
||||||
|
return strings.TrimRight(value, "/")
|
||||||
|
}
|
||||||
|
path := strings.TrimRight(value, "/")
|
||||||
|
suffixes := []string{}
|
||||||
|
switch apiType {
|
||||||
|
case "openai-completions":
|
||||||
|
suffixes = []string{"/chat/completions"}
|
||||||
|
case "openai-responses":
|
||||||
|
suffixes = []string{"/responses"}
|
||||||
|
case "anthropic-messages":
|
||||||
|
suffixes = []string{"/v1/messages", "/messages"}
|
||||||
|
case "openai-embeddings":
|
||||||
|
suffixes = []string{"/v1/embeddings", "/embeddings"}
|
||||||
|
}
|
||||||
|
for _, suffix := range suffixes {
|
||||||
|
if strings.HasSuffix(strings.ToLower(path), suffix) {
|
||||||
|
return path[:len(path)-len(suffix)]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return path
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsEmbeddingAPIType(apiType string) bool {
|
||||||
|
return apiType == "openai-embeddings"
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsImageAPIType(apiType string) bool {
|
||||||
|
switch apiType {
|
||||||
|
case "openai-images", "dashscope-images", "minimax-images", "openrouter-images":
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func EnvKey(key string) string {
|
func EnvKey(key string) string {
|
||||||
@@ -350,89 +429,62 @@ func EnvKey(key string) string {
|
|||||||
|
|
||||||
func DisplayName(key string) string {
|
func DisplayName(key string) string {
|
||||||
meta, ok := catalog[key]
|
meta, ok := catalog[key]
|
||||||
if !ok {
|
if !ok || strings.TrimSpace(meta.DisplayName) == "" {
|
||||||
return key
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(meta.DisplayName) == "" {
|
|
||||||
return key
|
return key
|
||||||
}
|
}
|
||||||
return meta.DisplayName
|
return meta.DisplayName
|
||||||
}
|
}
|
||||||
|
|
||||||
func FindModel(key, modelID string) (Model, bool) {
|
func DisplayNameKey(key string) string {
|
||||||
meta, ok := Get(key)
|
meta, ok := catalog[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return Model{}, false
|
return ""
|
||||||
}
|
}
|
||||||
for _, item := range meta.Models {
|
return meta.DisplayNameKey
|
||||||
if item.ID == modelID {
|
}
|
||||||
return item, true
|
|
||||||
|
func NormalizeModelID(provider, modelID string) string {
|
||||||
|
target := strings.TrimLeft(strings.TrimSpace(modelID), "/")
|
||||||
|
for _, prefix := range legacyModelPrefixes[provider] {
|
||||||
|
legacyPrefix := prefix + "/"
|
||||||
|
if !strings.HasPrefix(target, legacyPrefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
candidate := strings.TrimSpace(strings.TrimPrefix(target, legacyPrefix))
|
||||||
|
if candidate != "" {
|
||||||
|
return candidate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return Model{}, false
|
return target
|
||||||
|
}
|
||||||
|
|
||||||
|
var legacyModelPrefixes = map[string][]string{
|
||||||
|
"custom": {"custom"},
|
||||||
|
"vllm": {"custom"},
|
||||||
|
"ollama": {"ollama"},
|
||||||
|
"llmman": {"llmman"},
|
||||||
|
"deepseek": {"deepseek"},
|
||||||
|
"bailian-coding-plan": {"bailian-coding-plan"},
|
||||||
|
"ark-coding-plan": {"ark-coding-plan"},
|
||||||
|
"zai": {"zai"},
|
||||||
|
"minimax": {"minimax"},
|
||||||
|
"xiaomi": {"xiaomi"},
|
||||||
|
"kimi": {"kimi", "moonshot"},
|
||||||
|
"kimi-coding": {"kimi-coding"},
|
||||||
|
"openai": {"openai"},
|
||||||
|
"anthropic": {"anthropic"},
|
||||||
|
"gemini": {"google", "gemini"},
|
||||||
|
"moonshot": {"moonshot"},
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneMeta(meta Meta) Meta {
|
func cloneMeta(meta Meta) Meta {
|
||||||
clone := meta
|
clone := meta
|
||||||
if len(meta.Default.Input) > 0 {
|
clone.APIConfigs = make([]APIConfig, len(meta.APIConfigs))
|
||||||
clone.Default.Input = make([]string, len(meta.Default.Input))
|
for index, config := range meta.APIConfigs {
|
||||||
copy(clone.Default.Input, meta.Default.Input)
|
clone.APIConfigs[index] = config
|
||||||
}
|
clone.APIConfigs[index].AuthModes = append([]string(nil), config.AuthModes...)
|
||||||
if len(meta.Models) > 0 {
|
clone.APIConfigs[index].Models = append([]Model(nil), config.Models...)
|
||||||
clone.Models = make([]Model, len(meta.Models))
|
|
||||||
for i, item := range meta.Models {
|
|
||||||
clone.Models[i] = normalizeModel(meta, item)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
clone.Models = append([]Model(nil), meta.Models...)
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeModel(meta Meta, model Model) Model {
|
|
||||||
clone := model
|
|
||||||
clone.ID = strings.TrimSpace(clone.ID)
|
|
||||||
clone.Name = strings.TrimSpace(clone.Name)
|
|
||||||
if clone.Name == "" {
|
|
||||||
clone.Name = clone.ID
|
|
||||||
}
|
|
||||||
if clone.MaxTokens <= 0 {
|
|
||||||
clone.MaxTokens = meta.Default.MaxTokens
|
|
||||||
}
|
|
||||||
if clone.ContextWindow <= 0 {
|
|
||||||
clone.ContextWindow = meta.Default.ContextWindow
|
|
||||||
}
|
|
||||||
if len(clone.Input) == 0 && len(meta.Default.Input) > 0 {
|
|
||||||
clone.Input = make([]string, len(meta.Default.Input))
|
|
||||||
copy(clone.Input, meta.Default.Input)
|
|
||||||
}
|
|
||||||
return clone
|
|
||||||
}
|
|
||||||
|
|
||||||
func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) {
|
|
||||||
defaultAPIType := "openai-completions"
|
|
||||||
defaultMaxTokens := 8192
|
|
||||||
defaultContextWindow := 256000
|
|
||||||
if meta, ok := Get(provider); ok {
|
|
||||||
if meta.Default.APIType != "" {
|
|
||||||
defaultAPIType = meta.Default.APIType
|
|
||||||
}
|
|
||||||
if meta.Default.MaxTokens > 0 {
|
|
||||||
defaultMaxTokens = meta.Default.MaxTokens
|
|
||||||
}
|
|
||||||
if meta.Default.ContextWindow > 0 {
|
|
||||||
defaultContextWindow = meta.Default.ContextWindow
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resolvedAPI := apiType
|
|
||||||
if strings.TrimSpace(apiType) == "" {
|
|
||||||
resolvedAPI = defaultAPIType
|
|
||||||
}
|
|
||||||
resolvedMaxTokens := defaultMaxTokens
|
|
||||||
resolvedContextWindow := defaultContextWindow
|
|
||||||
if maxTokens > 0 {
|
|
||||||
resolvedMaxTokens = maxTokens
|
|
||||||
}
|
|
||||||
if contextWindow > 0 {
|
|
||||||
resolvedContextWindow = contextWindow
|
|
||||||
}
|
|
||||||
return resolvedAPI, resolvedMaxTokens, resolvedContextWindow
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
package provider
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DiscoverModels(baseURL, apiKey string) ([]string, error) {
|
||||||
|
req, err := http.NewRequest(http.MethodGet, buildModelDiscoveryURL(baseURL), nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+apiKey)
|
||||||
|
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if resp.StatusCode >= http.StatusBadRequest {
|
||||||
|
return nil, fmt.Errorf("request failed: %s", resp.Status)
|
||||||
|
}
|
||||||
|
var payload struct {
|
||||||
|
Data []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
models := make([]string, 0, len(payload.Data))
|
||||||
|
seen := make(map[string]struct{}, len(payload.Data))
|
||||||
|
for _, item := range payload.Data {
|
||||||
|
id := strings.TrimSpace(item.ID)
|
||||||
|
if id == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := seen[id]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[id] = struct{}{}
|
||||||
|
models = append(models, id)
|
||||||
|
}
|
||||||
|
if len(models) == 0 {
|
||||||
|
return nil, fmt.Errorf("no models found")
|
||||||
|
}
|
||||||
|
return models, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildModelDiscoveryURL(baseURL string) string {
|
||||||
|
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||||
|
for _, apiType := range []string{"openai-completions", "openai-responses", "anthropic-messages"} {
|
||||||
|
base = normalizeEndpointPath(apiType, base)
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case strings.HasSuffix(base, "/models"):
|
||||||
|
return base
|
||||||
|
case hasAPIVersionSuffix(base):
|
||||||
|
return base + "/models"
|
||||||
|
default:
|
||||||
|
return base + "/v1/models"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAPIVersionSuffix(value string) bool {
|
||||||
|
segment := value[strings.LastIndex(value, "/")+1:]
|
||||||
|
if len(segment) < 2 || segment[0] != 'v' {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
_, err := strconv.Atoi(segment[1:])
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
@@ -15,72 +15,52 @@ type OpenClawProviderPatch struct {
|
|||||||
AuthHeader bool
|
AuthHeader bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
|
func BuildOpenClawProviderPatch(provider, modelName, apiType, authMode, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
|
||||||
if modelName == "" {
|
if modelName == "" {
|
||||||
return nil, fmt.Errorf("model is required")
|
return nil, fmt.Errorf("model is required")
|
||||||
}
|
}
|
||||||
resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0)
|
resolvedAPIType := apiType
|
||||||
modelID := resolveOpenClawModelID(provider, modelName)
|
if _, ok := FindAPIConfig(provider, resolvedAPIType); !ok {
|
||||||
switch provider {
|
resolvedAPIType = DefaultAPIType(provider)
|
||||||
case "deepseek":
|
|
||||||
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
|
|
||||||
case "gemini":
|
|
||||||
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
|
||||||
case "moonshot", "kimi":
|
|
||||||
return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
|
|
||||||
case "bailian-coding-plan":
|
|
||||||
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
|
|
||||||
case "ark-coding-plan":
|
|
||||||
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
|
|
||||||
case "minimax":
|
|
||||||
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
|
|
||||||
case "xiaomi":
|
|
||||||
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
|
|
||||||
case "custom", "vllm":
|
|
||||||
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
|
||||||
case "ollama":
|
|
||||||
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
|
|
||||||
case "kimi-coding":
|
|
||||||
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
|
|
||||||
case "zai":
|
|
||||||
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
|
|
||||||
default:
|
|
||||||
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
|
||||||
}
|
}
|
||||||
}
|
if IsImageAPIType(resolvedAPIType) || IsEmbeddingAPIType(resolvedAPIType) {
|
||||||
|
return nil, fmt.Errorf("api type %s does not support text generation", resolvedAPIType)
|
||||||
func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch {
|
}
|
||||||
|
resolvedAuthMode, err := ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
usesBearer := resolvedAuthMode == AuthModeBearer
|
||||||
|
modelID := NormalizeModelID(provider, modelName)
|
||||||
providerKey := provider
|
providerKey := provider
|
||||||
primaryModel := modelName
|
preserveQualifiedModel := false
|
||||||
if provider == "kimi" {
|
switch provider {
|
||||||
|
case "gemini":
|
||||||
|
providerKey = "google"
|
||||||
|
resolvedAPIType = "google-generative-ai"
|
||||||
|
usesBearer = false
|
||||||
|
case "moonshot", "kimi":
|
||||||
providerKey = "moonshot"
|
providerKey = "moonshot"
|
||||||
primaryModel = "moonshot/" + modelID
|
resolvedAPIType = "openai-completions"
|
||||||
|
usesBearer = false
|
||||||
|
case "ollama", "llmman":
|
||||||
|
apiKey = provider
|
||||||
|
usesBearer = false
|
||||||
|
case "openai", "openrouter", "anthropic":
|
||||||
|
preserveQualifiedModel = strings.Contains(modelName, "/")
|
||||||
}
|
}
|
||||||
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
|
primaryModel := providerKey + "/" + modelID
|
||||||
|
if preserveQualifiedModel {
|
||||||
|
primaryModel = modelName
|
||||||
|
}
|
||||||
return &OpenClawProviderPatch{
|
return &OpenClawProviderPatch{
|
||||||
PrimaryModel: primaryModel,
|
PrimaryModel: primaryModel,
|
||||||
ProviderKey: providerKey,
|
ProviderKey: providerKey,
|
||||||
ModelID: modelID,
|
ModelID: modelID,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
BaseURL: baseURL,
|
BaseURL: baseURL,
|
||||||
APIType: apiType,
|
APIType: resolvedAPIType,
|
||||||
AuthHeader: authHeader,
|
AuthHeader: usesBearer,
|
||||||
}
|
}, nil
|
||||||
}
|
|
||||||
|
|
||||||
func resolveOpenClawModelID(provider, modelName string) string {
|
|
||||||
if provider == "custom" || provider == "vllm" {
|
|
||||||
target := strings.TrimLeft(modelName, "/")
|
|
||||||
if parts := strings.SplitN(target, "/", 2); len(parts) == 2 && parts[0] == "custom" {
|
|
||||||
return strings.TrimLeft(parts[1], "/")
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
|
||||||
return parts[1]
|
|
||||||
}
|
|
||||||
return modelName
|
|
||||||
}
|
}
|
||||||
|
|||||||
+116
-116
@@ -3,7 +3,9 @@ package provider
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -18,160 +20,158 @@ type VerifyRequest struct {
|
|||||||
Body []byte
|
Body []byte
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type verifyErrorResponse struct {
|
||||||
|
Error struct {
|
||||||
|
Message string `json:"message"`
|
||||||
|
} `json:"error"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
defaultVerifyTimeout = 30 * time.Second
|
defaultVerifyTimeout = 30 * time.Second
|
||||||
|
defaultVerifyMaxTokens = 16
|
||||||
)
|
)
|
||||||
|
|
||||||
func SkipVerification(key string) bool {
|
func SkipVerification(provider string) bool {
|
||||||
switch key {
|
switch provider {
|
||||||
case "custom", "vllm", "ollama", "kimi-coding":
|
case "vllm", "ollama", "llmman", "kimi-coding":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func VerifyAccount(provider, baseURL, apiKey string) error {
|
func VerifyAccount(provider, apiType, authMode, baseURL, apiKey, model string) error {
|
||||||
req := BuildVerifyRequest(provider, baseURL, apiKey)
|
req := BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model)
|
||||||
var body *bytes.Buffer
|
httpReq, err := http.NewRequest(req.Method, req.URL, bytes.NewReader(req.Body))
|
||||||
if len(req.Body) > 0 {
|
|
||||||
body = bytes.NewBuffer(req.Body)
|
|
||||||
} else {
|
|
||||||
body = bytes.NewBuffer(nil)
|
|
||||||
}
|
|
||||||
httpReq, err := http.NewRequest(req.Method, req.URL, body)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for key, value := range req.Headers {
|
for key, value := range req.Headers {
|
||||||
httpReq.Header.Set(key, value)
|
httpReq.Header.Set(key, value)
|
||||||
}
|
}
|
||||||
resp, err := (&http.Client{Timeout: verifyTimeout()}).Do(httpReq)
|
httpReq.Header.Set("Accept", "application/json")
|
||||||
|
resp, err := (&http.Client{Timeout: defaultVerifyTimeout}).Do(httpReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 400 {
|
if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusMultipleChoices {
|
||||||
return buserr.WithErr("ErrAgentAccountUnavailable", fmt.Errorf("verify failed: %s", resp.Status))
|
body, readErr := io.ReadAll(io.LimitReader(resp.Body, 1024*1024))
|
||||||
|
if readErr != nil {
|
||||||
|
return buserr.WithErr("ErrAgentAccountUnavailable", readErr)
|
||||||
|
}
|
||||||
|
return buserr.WithErr("ErrAgentAccountUnavailable", errors.New(verifyHTTPError(resp.StatusCode, body)))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func verifyTimeout() time.Duration {
|
func BuildVerifyRequest(provider, apiType, authMode, baseURL, apiKey, model string) VerifyRequest {
|
||||||
return defaultVerifyTimeout
|
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}
|
||||||
|
|
||||||
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
|
if provider == "gemini" {
|
||||||
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
request.URL = baseURL + "/v1beta/models/" + strings.TrimSpace(model) + ":generateContent"
|
||||||
headers := map[string]string{}
|
|
||||||
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
|
|
||||||
|
|
||||||
switch provider {
|
|
||||||
case "anthropic", "kimi-coding":
|
|
||||||
headers["x-api-key"] = apiKey
|
|
||||||
headers["anthropic-version"] = "2023-06-01"
|
|
||||||
if strings.Contains(base, "/v1") {
|
|
||||||
request.URL = base + "/models"
|
|
||||||
} else {
|
|
||||||
request.URL = base + "/v1/models"
|
|
||||||
}
|
|
||||||
case "gemini":
|
|
||||||
request.Method = http.MethodPost
|
|
||||||
if strings.Contains(base, "/v1beta") {
|
|
||||||
request.URL = base + "/models/gemini-3-flash-preview:generateContent"
|
|
||||||
} else {
|
|
||||||
request.URL = base + "/v1beta/models/gemini-3-flash-preview:generateContent"
|
|
||||||
}
|
|
||||||
headers["x-goog-api-key"] = apiKey
|
headers["x-goog-api-key"] = apiKey
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
"contents": []map[string]interface{}{{
|
"contents": []map[string]interface{}{{"parts": []map[string]string{{"text": "test"}}}},
|
||||||
"parts": []map[string]string{{
|
})
|
||||||
"text": "Explain how AI works in a few words",
|
return request
|
||||||
}},
|
}
|
||||||
|
|
||||||
|
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 "zai":
|
case "minimax-images":
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
request.URL = imageVerifyURL(provider, baseURL, "/v1/image_generation")
|
||||||
request.URL = base + "/models"
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
case "bailian-coding-plan":
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
request.Method = http.MethodPost
|
case "openrouter-images":
|
||||||
if !strings.Contains(base, "/v1") {
|
request.URL = imageVerifyURL(provider, baseURL, "/api/v1/images")
|
||||||
base = base + "/v1"
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
request.Body = mustJSON(map[string]interface{}{"model": model, "prompt": "test", "n": 1, "response_format": "url"})
|
||||||
|
case "anthropic-messages":
|
||||||
|
request.URL = baseURL + "/v1/messages"
|
||||||
|
if authMode == AuthModeBearer {
|
||||||
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
|
} else {
|
||||||
|
headers["x-api-key"] = apiKey
|
||||||
}
|
}
|
||||||
request.URL = base + "/chat/completions"
|
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
|
||||||
"model": "qwen3.5-plus",
|
|
||||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
|
||||||
"max_tokens": 1,
|
|
||||||
})
|
|
||||||
case "ark-coding-plan":
|
|
||||||
request.Method = http.MethodPost
|
|
||||||
if !strings.Contains(base, "/api/coding/v3") {
|
|
||||||
base = "https://ark.cn-beijing.volces.com/api/coding/v3"
|
|
||||||
}
|
|
||||||
request.URL = base + "/chat/completions"
|
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
|
||||||
"model": "ark-code-latest",
|
|
||||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
|
||||||
"max_tokens": 1,
|
|
||||||
})
|
|
||||||
case "minimax":
|
|
||||||
request.Method = http.MethodPost
|
|
||||||
headers["x-api-key"] = apiKey
|
|
||||||
headers["anthropic-version"] = "2023-06-01"
|
headers["anthropic-version"] = "2023-06-01"
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
if strings.Contains(base, "/v1") {
|
|
||||||
request.URL = base + "/messages"
|
|
||||||
} else {
|
|
||||||
request.URL = base + "/v1/messages"
|
|
||||||
}
|
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
"model": "MiniMax-M2.5",
|
"model": model, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||||
"max_tokens": 1,
|
"messages": []map[string]interface{}{{"role": "user", "content": []map[string]string{{"type": "text", "text": "test"}}}},
|
||||||
"messages": []map[string]interface{}{{
|
|
||||||
"role": "user",
|
|
||||||
"content": []map[string]string{{
|
|
||||||
"type": "text",
|
|
||||||
"text": "test",
|
|
||||||
}},
|
|
||||||
}},
|
|
||||||
})
|
})
|
||||||
case "xiaomi":
|
case "openai-responses":
|
||||||
request.Method = http.MethodPost
|
request.URL = baseURL + "/responses"
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
headers["Content-Type"] = "application/json"
|
request.Body = mustJSON(map[string]interface{}{"model": model, "input": "test", "max_output_tokens": defaultVerifyMaxTokens, "stream": false})
|
||||||
if !strings.Contains(base, "/v1") {
|
|
||||||
base = base + "/v1"
|
|
||||||
}
|
|
||||||
request.URL = base + "/chat/completions"
|
|
||||||
request.Body = mustJSON(map[string]interface{}{
|
|
||||||
"model": "mimo-v2-flash",
|
|
||||||
"max_tokens": 1,
|
|
||||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
|
||||||
})
|
|
||||||
case "openrouter":
|
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
|
||||||
if strings.Contains(base, "/v1") {
|
|
||||||
request.URL = base + "/key"
|
|
||||||
} else {
|
|
||||||
request.URL = base + "/v1/key"
|
|
||||||
}
|
|
||||||
default:
|
default:
|
||||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
request.URL = baseURL + "/chat/completions"
|
||||||
if strings.Contains(base, "/v1") {
|
if (provider != "ollama" && provider != "llmman") || strings.TrimSpace(apiKey) != "" {
|
||||||
request.URL = base + "/models"
|
headers["Authorization"] = "Bearer " + apiKey
|
||||||
} else {
|
|
||||||
request.URL = base + "/v1/models"
|
|
||||||
}
|
}
|
||||||
|
request.Body = mustJSON(map[string]interface{}{
|
||||||
|
"model": model, "messages": []map[string]string{{"role": "user", "content": "test"}}, "max_tokens": defaultVerifyMaxTokens, "stream": false,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
return request
|
return request
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func embeddingVerifyURL(baseURL string) string {
|
||||||
|
lowerBaseURL := strings.ToLower(baseURL)
|
||||||
|
if strings.HasSuffix(lowerBaseURL, "/embeddings") {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
if strings.HasSuffix(lowerBaseURL, "/v1") {
|
||||||
|
return baseURL + "/embeddings"
|
||||||
|
}
|
||||||
|
return baseURL + "/v1/embeddings"
|
||||||
|
}
|
||||||
|
|
||||||
|
func imageVerifyURL(provider, baseURL, endpoint string) string {
|
||||||
|
if provider == "custom" || strings.HasSuffix(strings.ToLower(baseURL), endpoint) {
|
||||||
|
return baseURL
|
||||||
|
}
|
||||||
|
return baseURL + endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyHTTPError(statusCode int, body []byte) string {
|
||||||
|
message := strings.TrimSpace(string(body))
|
||||||
|
var payload verifyErrorResponse
|
||||||
|
if err := json.Unmarshal(body, &payload); err == nil {
|
||||||
|
if value := strings.TrimSpace(payload.Error.Message); value != "" {
|
||||||
|
message = value
|
||||||
|
} else if value := strings.TrimSpace(payload.Message); value != "" {
|
||||||
|
message = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if message == "" {
|
||||||
|
return fmt.Sprintf("validation request returned status %d", statusCode)
|
||||||
|
}
|
||||||
|
return message
|
||||||
|
}
|
||||||
|
|
||||||
func mustJSON(value interface{}) []byte {
|
func mustJSON(value interface{}) []byte {
|
||||||
payload, err := json.Marshal(value)
|
payload, err := json.Marshal(value)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ type IAgentAccountRepo interface {
|
|||||||
Save(account *model.AgentAccount) error
|
Save(account *model.AgentAccount) error
|
||||||
DeleteByID(id uint) error
|
DeleteByID(id uint) error
|
||||||
List(opts ...DBOption) ([]model.AgentAccount, error)
|
List(opts ...DBOption) ([]model.AgentAccount, error)
|
||||||
CountByProviders(providers []string) (map[string]int64, error)
|
CountTextByProviders(providers []string) (map[string]int64, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAgentAccountRepo() IAgentAccountRepo {
|
func NewIAgentAccountRepo() IAgentAccountRepo {
|
||||||
@@ -67,7 +67,7 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
|
|||||||
return accounts, nil
|
return accounts, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
|
func (a AgentAccountRepo) CountTextByProviders(providers []string) (map[string]int64, error) {
|
||||||
normalizedProviders := normalizeProviders(providers)
|
normalizedProviders := normalizeProviders(providers)
|
||||||
counts := make(map[string]int64, len(normalizedProviders))
|
counts := make(map[string]int64, len(normalizedProviders))
|
||||||
for _, provider := range normalizedProviders {
|
for _, provider := range normalizedProviders {
|
||||||
@@ -86,6 +86,7 @@ func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64
|
|||||||
Model(&model.AgentAccount{}).
|
Model(&model.AgentAccount{}).
|
||||||
Select("provider, COUNT(*) as count").
|
Select("provider, COUNT(*) as count").
|
||||||
Where("provider IN ?", normalizedProviders).
|
Where("provider IN ?", normalizedProviders).
|
||||||
|
Scopes(WithTextAPIType()).
|
||||||
Group("provider").
|
Group("provider").
|
||||||
Scan(&rows).Error; err != nil {
|
Scan(&rows).Error; err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
+217
-9
@@ -1,20 +1,30 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/google/uuid"
|
||||||
"google.golang.org/genproto/googleapis/type/date"
|
"google.golang.org/genproto/googleapis/type/date"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
"strconv"
|
"gorm.io/gorm/clause"
|
||||||
"time"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertRepo struct{}
|
type AlertRepo struct{}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrAlertConfigRevisionConflict = errors.New("alert config revision conflict")
|
||||||
|
ErrAlertConfigRevisionRequired = errors.New("alert config revision is required")
|
||||||
|
)
|
||||||
|
|
||||||
type IAlertRepo interface {
|
type IAlertRepo interface {
|
||||||
WithByType(alertType string) DBOption
|
WithByType(alertType string) DBOption
|
||||||
WithByStatusIn(status []string) DBOption
|
WithByStatusIn(status []string) DBOption
|
||||||
@@ -24,6 +34,7 @@ type IAlertRepo interface {
|
|||||||
WithByCreateAt(date *date.Date) DBOption
|
WithByCreateAt(date *date.Date) DBOption
|
||||||
WithByLicenseId(licenseId string) DBOption
|
WithByLicenseId(licenseId string) DBOption
|
||||||
WithByRecordId(recordId uint) DBOption
|
WithByRecordId(recordId uint) DBOption
|
||||||
|
WithByDeliveryLogID(logID uint) DBOption
|
||||||
WithByAlertMethodContainsConfigID(id uint) DBOption
|
WithByAlertMethodContainsConfigID(id uint) DBOption
|
||||||
WithByMethodConfigIDs(ids []uint) DBOption
|
WithByMethodConfigIDs(ids []uint) DBOption
|
||||||
|
|
||||||
@@ -45,6 +56,8 @@ type IAlertRepo interface {
|
|||||||
CleanAlertLogs() error
|
CleanAlertLogs() error
|
||||||
|
|
||||||
CreateAlertTask(alertTaskBase *model.AlertTask) error
|
CreateAlertTask(alertTaskBase *model.AlertTask) error
|
||||||
|
CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error)
|
||||||
|
FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error)
|
||||||
DeleteAlertTask(opts ...DBOption) error
|
DeleteAlertTask(opts ...DBOption) error
|
||||||
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
GetAlertTask(opts ...DBOption) (model.AlertTask, error)
|
||||||
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
LoadTaskCount(alertType string, project string, method string) (uint, uint, error)
|
||||||
@@ -55,6 +68,7 @@ type IAlertRepo interface {
|
|||||||
GetConfigById(id uint) (model.AlertConfig, error)
|
GetConfigById(id uint) (model.AlertConfig, error)
|
||||||
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
|
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
|
||||||
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
|
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
|
||||||
|
UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error
|
||||||
CreateAlertConfig(config *model.AlertConfig) error
|
CreateAlertConfig(config *model.AlertConfig) error
|
||||||
DeleteAlertConfig(opts ...DBOption) error
|
DeleteAlertConfig(opts ...DBOption) error
|
||||||
|
|
||||||
@@ -223,13 +237,78 @@ func (a *AlertRepo) DeleteLog(opts ...DBOption) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *AlertRepo) CleanAlertLogs() 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 {
|
func (a *AlertRepo) CreateAlertTask(alertTaskBase *model.AlertTask) error {
|
||||||
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
|
return global.AlertDB.Model(&model.AlertTask{}).Create(&alertTaskBase).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AlertRepo) CreatePendingAlertTask(logID, alertID uint, alertTask *model.AlertTask) (bool, error) {
|
||||||
|
if alertTask == nil {
|
||||||
|
return false, fmt.Errorf("pending alert task is required")
|
||||||
|
}
|
||||||
|
created := false
|
||||||
|
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
var log model.AlertLog
|
||||||
|
if err := tx.Where("id = ? AND status = ?", logID, constant.AlertPushing).First(&log).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if log.AlertId != alertID || log.Type != alertTask.Type || log.Method != alertTask.Method {
|
||||||
|
return fmt.Errorf("pending alert task does not match delivery log %d", logID)
|
||||||
|
}
|
||||||
|
alertTask.DeliveryLogID = &logID
|
||||||
|
result := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "delivery_log_id"}},
|
||||||
|
DoNothing: true,
|
||||||
|
}).Create(alertTask)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
created = result.RowsAffected > 0
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return created, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *AlertRepo) FinalizePendingAlertTask(logID uint, succeeded bool, message string, fallback *model.AlertTask) (bool, error) {
|
||||||
|
finalized := false
|
||||||
|
err := global.AlertDB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
status := constant.AlertError
|
||||||
|
if succeeded {
|
||||||
|
status = constant.AlertSuccess
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
result := tx.Model(&model.AlertLog{}).
|
||||||
|
Where("id = ? AND status = ?", logID, constant.AlertPushing).
|
||||||
|
Updates(map[string]interface{}{"status": status, "message": message})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
finalized = true
|
||||||
|
if !succeeded {
|
||||||
|
return tx.Where("delivery_log_id = ?", logID).Delete(&model.AlertTask{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
var count int64
|
||||||
|
if err := tx.Model(&model.AlertTask{}).Where("delivery_log_id = ?", logID).Count(&count).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count > 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if fallback == nil {
|
||||||
|
return fmt.Errorf("pending alert task metadata is unavailable for delivery log %d", logID)
|
||||||
|
}
|
||||||
|
fallback.DeliveryLogID = &logID
|
||||||
|
return tx.Create(fallback).Error
|
||||||
|
})
|
||||||
|
return finalized, err
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
|
func (a *AlertRepo) DeleteAlertTask(opts ...DBOption) error {
|
||||||
db, _ := getAlertDB(opts...)
|
db, _ := getAlertDB(opts...)
|
||||||
return db.Delete(&model.AlertTask{}).Error
|
return db.Delete(&model.AlertTask{}).Error
|
||||||
@@ -310,7 +389,23 @@ func (a *AlertRepo) UpdateAlertConfig(maps map[string]interface{}, opts ...DBOpt
|
|||||||
return db.Model(&model.AlertConfig{}).Updates(maps).Error
|
return db.Model(&model.AlertConfig{}).Updates(maps).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *AlertRepo) UpdateAlertConfigWithRevision(maps map[string]interface{}, revision *time.Time, opts ...DBOption) error {
|
||||||
|
if revision == nil {
|
||||||
|
return a.UpdateAlertConfig(maps, opts...)
|
||||||
|
}
|
||||||
|
db, _ := getAlertDB(opts...)
|
||||||
|
result := db.Model(&model.AlertConfig{}).Where("updated_at = ?", *revision).Updates(maps)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return ErrAlertConfigRevisionConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
|
func (a *AlertRepo) CreateAlertConfig(config *model.AlertConfig) error {
|
||||||
|
ensureAlertConfigUID(config)
|
||||||
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
|
return global.AlertDB.Model(&model.AlertConfig{}).Create(config).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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) {
|
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
|
||||||
var configs []model.AlertConfig
|
var configs []model.AlertConfig
|
||||||
db := global.AlertDB.Model(&model.AlertConfig{})
|
db := global.AlertDB.Model(&model.AlertConfig{})
|
||||||
@@ -378,26 +479,44 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
return err
|
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)
|
oldConfigByType := make(map[string][]model.AlertConfig)
|
||||||
oldConfigByKey := make(map[string][]model.AlertConfig)
|
oldConfigByKey := make(map[string][]model.AlertConfig)
|
||||||
consumedConfigIDs := make(map[uint]struct{})
|
consumedConfigIDs := make(map[uint]struct{})
|
||||||
for _, item := range oldConfigs {
|
for _, item := range oldConfigs {
|
||||||
|
if strings.TrimSpace(item.UID) != "" {
|
||||||
|
oldConfigByUID[item.UID] = item
|
||||||
|
}
|
||||||
if singletonTypes[item.Type] {
|
if singletonTypes[item.Type] {
|
||||||
oldConfigMap[item.Type] = item.ID
|
oldConfigMap[item.Type] = item
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
|
||||||
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
|
||||||
}
|
}
|
||||||
for _, item := range data {
|
for _, item := range data {
|
||||||
|
if uid := strings.TrimSpace(item.UID); uid != "" {
|
||||||
|
if matched, ok := oldConfigByUID[uid]; ok && matched.Type != item.Type {
|
||||||
|
tx.Rollback()
|
||||||
|
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", uid, matched.Type, item.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
if singletonTypes[item.Type] {
|
if singletonTypes[item.Type] {
|
||||||
if val, ok := oldConfigMap[item.Type]; ok {
|
if matched, ok := oldConfigMap[item.Type]; ok {
|
||||||
item.ID = val
|
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
delete(oldConfigMap, item.Type)
|
delete(oldConfigMap, item.Type)
|
||||||
consumedConfigIDs[item.ID] = struct{}{}
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
} else {
|
} else {
|
||||||
item.ID = 0
|
item.ID = 0
|
||||||
|
ensureAlertConfigUID(&item)
|
||||||
|
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if item.ID == 0 {
|
if item.ID == 0 {
|
||||||
if err := tx.Create(&item).Error; err != nil {
|
if err := tx.Create(&item).Error; err != nil {
|
||||||
@@ -411,9 +530,31 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.TrimSpace(item.UID) != "" {
|
||||||
|
if matched, ok := oldConfigByUID[item.UID]; ok {
|
||||||
|
delete(oldConfigByUID, item.UID)
|
||||||
|
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
|
if err := tx.Save(&item).Error; err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
deleteAlertConfigByID(oldConfigByType, matched.ID)
|
||||||
|
deleteAlertConfigByID(oldConfigByKey, matched.ID)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
key := alertConfigSyncKey(item)
|
key := alertConfigSyncKey(item)
|
||||||
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
|
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
|
||||||
item.ID = matched.ID
|
delete(oldConfigByUID, matched.UID)
|
||||||
|
if err := inheritAlertConfigSyncState(&item, matched); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
consumedConfigIDs[item.ID] = struct{}{}
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
if err := tx.Save(&item).Error; err != nil {
|
if err := tx.Save(&item).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -424,7 +565,12 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
|
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
|
||||||
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{}{}
|
consumedConfigIDs[item.ID] = struct{}{}
|
||||||
if err := tx.Save(&item).Error; err != nil {
|
if err := tx.Save(&item).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
@@ -434,6 +580,11 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
item.ID = 0
|
item.ID = 0
|
||||||
|
ensureAlertConfigUID(&item)
|
||||||
|
if err := validateAlertConfigSyncSecret(&item); err != nil {
|
||||||
|
tx.Rollback()
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := tx.Create(&item).Error; err != nil {
|
if err := tx.Create(&item).Error; err != nil {
|
||||||
tx.Rollback()
|
tx.Rollback()
|
||||||
return err
|
return err
|
||||||
@@ -458,6 +609,63 @@ func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func ensureAlertConfigUID(config *model.AlertConfig) {
|
||||||
|
if config != nil && strings.TrimSpace(config.UID) == "" {
|
||||||
|
config.UID = uuid.NewString()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func inheritAlertConfigSyncState(incoming *model.AlertConfig, existing model.AlertConfig) error {
|
||||||
|
if incoming.Type != existing.Type {
|
||||||
|
return fmt.Errorf("alert config UID %q belongs to type %q, not %q", incoming.UID, existing.Type, incoming.Type)
|
||||||
|
}
|
||||||
|
preserveExistingCustom := incoming.Type == constant.Custom &&
|
||||||
|
existing.Status == constant.AlertDisable &&
|
||||||
|
incoming.Title == existing.Title &&
|
||||||
|
incoming.Status == existing.Status &&
|
||||||
|
incoming.Config == existing.Config &&
|
||||||
|
(incoming.SecretConfig == "" || incoming.SecretConfig == existing.SecretConfig)
|
||||||
|
incoming.ID = existing.ID
|
||||||
|
if strings.TrimSpace(incoming.UID) == "" {
|
||||||
|
incoming.UID = existing.UID
|
||||||
|
}
|
||||||
|
if incoming.Type == constant.Custom && incoming.SecretConfig == "" {
|
||||||
|
incoming.SecretConfig = existing.SecretConfig
|
||||||
|
}
|
||||||
|
if preserveExistingCustom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return validateAlertConfigSyncSecret(incoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAlertConfigSyncSecret(incoming *model.AlertConfig) error {
|
||||||
|
if incoming.Type != constant.Custom {
|
||||||
|
incoming.SecretConfig = ""
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(incoming.SecretConfig) == "" {
|
||||||
|
return fmt.Errorf("custom webhook sync secret is missing")
|
||||||
|
}
|
||||||
|
var version struct {
|
||||||
|
SchemaVersion int `json:"schemaVersion"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(incoming.Config), &version); err != nil || version.SchemaVersion != 1 {
|
||||||
|
return fmt.Errorf("custom webhook sync config must use schemaVersion 1")
|
||||||
|
}
|
||||||
|
secret := incoming.SecretConfig
|
||||||
|
for _, prefix := range []string{"core:v1:", "agent:v1:"} {
|
||||||
|
if !strings.HasPrefix(secret, prefix) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
ciphertext, err := base64.StdEncoding.DecodeString(strings.TrimPrefix(secret, prefix))
|
||||||
|
if err != nil || len(ciphertext) < 32 || len(ciphertext)%16 != 0 {
|
||||||
|
return fmt.Errorf("custom webhook sync secret envelope is invalid")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return fmt.Errorf("custom webhook sync secret must use a versioned envelope")
|
||||||
|
}
|
||||||
|
|
||||||
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
|
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
|
||||||
var alerts []model.Alert
|
var alerts []model.Alert
|
||||||
if err := tx.Select("method").Find(&alerts).Error; err != nil {
|
if err := tx.Select("method").Find(&alerts).Error; err != nil {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ type IAppInstallResourceRpo interface {
|
|||||||
WithAppInstallId(appInstallId uint) DBOption
|
WithAppInstallId(appInstallId uint) DBOption
|
||||||
WithLinkId(linkId uint) DBOption
|
WithLinkId(linkId uint) DBOption
|
||||||
WithResourceId(resourceId uint) DBOption
|
WithResourceId(resourceId uint) DBOption
|
||||||
|
WithResourceIds(resourceIds []uint) DBOption
|
||||||
GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
|
GetBy(opts ...DBOption) ([]model.AppInstallResource, error)
|
||||||
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
|
GetFirst(opts ...DBOption) (model.AppInstallResource, error)
|
||||||
Create(ctx context.Context, resource *model.AppInstallResource) error
|
Create(ctx context.Context, resource *model.AppInstallResource) error
|
||||||
@@ -44,6 +45,12 @@ func (a AppInstallResourceRpo) WithResourceId(resourceId uint) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AppInstallResourceRpo) WithResourceIds(resourceIds []uint) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("resource_id IN ?", resourceIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
|
func (a AppInstallResourceRpo) GetBy(opts ...DBOption) ([]model.AppInstallResource, error) {
|
||||||
db := global.DB.Model(&model.AppInstallResource{})
|
db := global.DB.Model(&model.AppInstallResource{})
|
||||||
var resources []model.AppInstallResource
|
var resources []model.AppInstallResource
|
||||||
|
|||||||
@@ -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 {
|
func WithByAddr(addr string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
return g.Where("addr = ?", addr)
|
return g.Where("addr = ?", addr)
|
||||||
@@ -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 {
|
func WithByModel(model string) DBOption {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
if len(model) == 0 {
|
if len(model) == 0 {
|
||||||
|
|||||||
@@ -0,0 +1,121 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DatabaseUserRepo struct{}
|
||||||
|
|
||||||
|
type IDatabaseUserRepo interface {
|
||||||
|
Get(opts ...DBOption) (model.DatabaseUser, error)
|
||||||
|
List(opts ...DBOption) ([]model.DatabaseUser, error)
|
||||||
|
Save(user *model.DatabaseUser) error
|
||||||
|
Delete(opts ...DBOption) error
|
||||||
|
DeleteBy(ctx context.Context, opts ...DBOption) error
|
||||||
|
Update(vars map[string]interface{}, opts ...DBOption) error
|
||||||
|
WithByDatabase(database string) DBOption
|
||||||
|
WithByUser(username, host string) DBOption
|
||||||
|
WithByUserList(users [][2]string) DBOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDatabaseUserRepo() IDatabaseUserRepo {
|
||||||
|
return &DatabaseUserRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) Get(opts ...DBOption) (model.DatabaseUser, error) {
|
||||||
|
var user model.DatabaseUser
|
||||||
|
db := global.DB.Model(&model.DatabaseUser{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
if err := db.First(&user).Error; err != nil {
|
||||||
|
return user, err
|
||||||
|
}
|
||||||
|
password, err := encrypt.StringDecrypt(user.Password)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("decrypt database user %s password failed, err: %v", user.Username, err)
|
||||||
|
}
|
||||||
|
user.Password = password
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) List(opts ...DBOption) ([]model.DatabaseUser, error) {
|
||||||
|
var users []model.DatabaseUser
|
||||||
|
db := global.DB.Model(&model.DatabaseUser{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
if err := db.Find(&users).Error; err != nil {
|
||||||
|
return users, err
|
||||||
|
}
|
||||||
|
for i := 0; i < len(users); i++ {
|
||||||
|
password, err := encrypt.StringDecrypt(users[i].Password)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("decrypt database user %s password failed, err: %v", users[i].Username, err)
|
||||||
|
}
|
||||||
|
users[i].Password = password
|
||||||
|
}
|
||||||
|
return users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) Save(user *model.DatabaseUser) error {
|
||||||
|
if len(user.Password) != 0 {
|
||||||
|
password, err := encrypt.StringEncrypt(user.Password)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("encrypt database user %s password failed, err: %v", user.Username, err)
|
||||||
|
}
|
||||||
|
user.Password = password
|
||||||
|
}
|
||||||
|
return global.DB.Save(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) Delete(opts ...DBOption) error {
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Delete(&model.DatabaseUser{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
|
||||||
|
return getTx(ctx, opts...).Delete(&model.DatabaseUser{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
|
||||||
|
db := global.DB.Model(&model.DatabaseUser{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Updates(vars).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) WithByDatabase(database string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("database = ?", database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) WithByUser(username, host string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
return g.Where("username = ? AND host = ?", username, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserRepo) WithByUserList(users [][2]string) DBOption {
|
||||||
|
return func(g *gorm.DB) *gorm.DB {
|
||||||
|
if len(users) == 0 {
|
||||||
|
return g.Where("1 = 0")
|
||||||
|
}
|
||||||
|
values := make([][]interface{}, 0, len(users))
|
||||||
|
for _, user := range users {
|
||||||
|
values = append(values, []interface{}{user[0], user[1]})
|
||||||
|
}
|
||||||
|
return g.Where("(username, host) IN ?", values)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DatabaseUserGrantRepo struct{}
|
||||||
|
|
||||||
|
type IDatabaseUserGrantRepo interface {
|
||||||
|
Get(opts ...DBOption) (model.DatabaseUserGrant, error)
|
||||||
|
List(opts ...DBOption) ([]model.DatabaseUserGrant, error)
|
||||||
|
Save(grant *model.DatabaseUserGrant) error
|
||||||
|
Replace(dbType, database string, grants []model.DatabaseUserGrant) error
|
||||||
|
Delete(opts ...DBOption) error
|
||||||
|
DeleteBy(ctx context.Context, opts ...DBOption) error
|
||||||
|
Update(vars map[string]interface{}, opts ...DBOption) error
|
||||||
|
WithByDatabase(database string) DBOption
|
||||||
|
WithByDBName(dbName string) DBOption
|
||||||
|
WithByDBNames(dbNames []string) DBOption
|
||||||
|
WithByUser(username, host string) DBOption
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) Get(opts ...DBOption) (model.DatabaseUserGrant, error) {
|
||||||
|
var grant model.DatabaseUserGrant
|
||||||
|
db := global.DB.Model(&model.DatabaseUserGrant{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.First(&grant).Error
|
||||||
|
return grant, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIDatabaseUserGrantRepo() IDatabaseUserGrantRepo {
|
||||||
|
return &DatabaseUserGrantRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) List(opts ...DBOption) ([]model.DatabaseUserGrant, error) {
|
||||||
|
var grants []model.DatabaseUserGrant
|
||||||
|
db := global.DB.Model(&model.DatabaseUserGrant{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
err := db.Find(&grants).Error
|
||||||
|
return grants, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) Save(grant *model.DatabaseUserGrant) error {
|
||||||
|
return global.DB.Save(grant).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) Replace(dbType, database string, grants []model.DatabaseUserGrant) error {
|
||||||
|
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
if err := tx.Where("`type` = ? AND database = ?", dbType, database).Delete(&model.DatabaseUserGrant{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(grants) != 0 {
|
||||||
|
return tx.Create(&grants).Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) Delete(opts ...DBOption) error {
|
||||||
|
db := global.DB
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Delete(&model.DatabaseUserGrant{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) DeleteBy(ctx context.Context, opts ...DBOption) error {
|
||||||
|
return getTx(ctx, opts...).Delete(&model.DatabaseUserGrant{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) Update(vars map[string]interface{}, opts ...DBOption) error {
|
||||||
|
db := global.DB.Model(&model.DatabaseUserGrant{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return db.Updates(vars).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) WithByDatabase(database string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("database = ?", database)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) WithByDBName(dbName string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("db_name = ?", dbName)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) WithByDBNames(dbNames []string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("db_name IN ?", dbNames)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *DatabaseUserGrantRepo) WithByUser(username, host string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("username = ? AND host = ?", username, host)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IDockerPortGuardRepo interface {
|
||||||
|
ListManaged(context.Context) ([]model.DockerPortGuardPolicy, error)
|
||||||
|
DeleteBatch(context.Context, []string) error
|
||||||
|
UpsertBatch(context.Context, []model.DockerPortGuardPolicy) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DockerPortGuardRepo struct{}
|
||||||
|
|
||||||
|
func NewIDockerPortGuardRepo() IDockerPortGuardRepo { return &DockerPortGuardRepo{} }
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) ListManaged(ctx context.Context) ([]model.DockerPortGuardPolicy, error) {
|
||||||
|
var policies []model.DockerPortGuardPolicy
|
||||||
|
err := global.DB.WithContext(ctx).
|
||||||
|
Where("read_only = ?", false).
|
||||||
|
Order("family, host_ip, host_port, protocol").
|
||||||
|
Find(&policies).Error
|
||||||
|
return policies, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) DeleteBatch(ctx context.Context, uuids []string) error {
|
||||||
|
return global.DB.WithContext(ctx).
|
||||||
|
Where("read_only = ? AND uuid IN ?", false, uuids).
|
||||||
|
Delete(&model.DockerPortGuardPolicy{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *DockerPortGuardRepo) UpsertBatch(ctx context.Context, policies []model.DockerPortGuardPolicy) error {
|
||||||
|
return global.DB.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
for i := range policies {
|
||||||
|
policies[i].ReadOnly = false
|
||||||
|
if err := tx.Clauses(clause.OnConflict{
|
||||||
|
Columns: []clause.Column{{Name: "read_only"}, {Name: "family"}, {Name: "host_ip"}, {Name: "host_port"}, {Name: "protocol"}},
|
||||||
|
DoUpdates: clause.AssignmentColumns([]string{"mode", "sources", "description", "updated_at"}),
|
||||||
|
}).Create(&policies[i]).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,187 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/clause"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrFirewallRuleRevisionConflict = errors.New("firewall rule revision conflict")
|
||||||
|
ErrFirewallPersistenceInvalid = errors.New("invalid firewall persistence record")
|
||||||
|
)
|
||||||
|
|
||||||
|
type IFirewallRuleRepo interface {
|
||||||
|
Create(context.Context, *model.FirewallRule) error
|
||||||
|
GetByUUID(context.Context, string) (model.FirewallRule, error)
|
||||||
|
List(context.Context, ...DBOption) ([]model.FirewallRule, error)
|
||||||
|
UpdateWithRevision(context.Context, string, uint, map[string]interface{}) error
|
||||||
|
DeleteWithRevision(context.Context, string, uint) error
|
||||||
|
DeleteBatchWithRevision(context.Context, []model.FirewallRule) map[string]error
|
||||||
|
SaveResetOrder(context.Context, []model.FirewallRule) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type FirewallRuleRepo struct {
|
||||||
|
db *gorm.DB
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewIFirewallRuleRepo() IFirewallRuleRepo {
|
||||||
|
return &FirewallRuleRepo{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFirewallRuleRepo(db *gorm.DB) *FirewallRuleRepo {
|
||||||
|
return &FirewallRuleRepo{db: db}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) Create(ctx context.Context, rule *model.FirewallRule) error {
|
||||||
|
if err := prepareFirewallRule(rule); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return r.dbFor(ctx).Create(rule).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) GetByUUID(ctx context.Context, ruleUUID string) (model.FirewallRule, error) {
|
||||||
|
var rule model.FirewallRule
|
||||||
|
err := r.dbFor(ctx).Where("uuid = ?", ruleUUID).First(&rule).Error
|
||||||
|
return rule, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) List(ctx context.Context, opts ...DBOption) ([]model.FirewallRule, error) {
|
||||||
|
var rules []model.FirewallRule
|
||||||
|
db := r.dbFor(ctx).Model(&model.FirewallRule{})
|
||||||
|
for _, opt := range opts {
|
||||||
|
db = opt(db)
|
||||||
|
}
|
||||||
|
return rules, db.Find(&rules).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) UpdateWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint, updates map[string]interface{}) error {
|
||||||
|
updates = sanitizeRuleUpdates(updates)
|
||||||
|
updates["revision"] = gorm.Expr("revision + 1")
|
||||||
|
result := r.dbFor(ctx).Model(&model.FirewallRule{}).
|
||||||
|
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||||
|
Updates(updates)
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return ErrFirewallRuleRevisionConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) DeleteWithRevision(ctx context.Context, ruleUUID string, expectedRevision uint) error {
|
||||||
|
result := r.dbFor(ctx).
|
||||||
|
Where("uuid = ? AND revision = ?", ruleUUID, expectedRevision).
|
||||||
|
Delete(&model.FirewallRule{})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return ErrFirewallRuleRevisionConflict
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) DeleteBatchWithRevision(ctx context.Context, rules []model.FirewallRule) map[string]error {
|
||||||
|
failures := make(map[string]error)
|
||||||
|
for start := 0; start < len(rules); start += 500 {
|
||||||
|
batch := rules[start:min(start+500, len(rules))]
|
||||||
|
ids := make([][]interface{}, 0, len(batch))
|
||||||
|
for _, rule := range batch {
|
||||||
|
ids = append(ids, []interface{}{rule.UUID, rule.Revision})
|
||||||
|
failures[rule.UUID] = ErrFirewallRuleRevisionConflict
|
||||||
|
}
|
||||||
|
var deleted []model.FirewallRule
|
||||||
|
err := r.dbFor(ctx).Clauses(clause.Returning{Columns: []clause.Column{{Name: "uuid"}}}).
|
||||||
|
Where("(uuid, revision) IN ?", ids).Delete(&deleted).Error
|
||||||
|
if err != nil {
|
||||||
|
for _, rule := range batch {
|
||||||
|
failures[rule.UUID] = err
|
||||||
|
}
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for _, rule := range deleted {
|
||||||
|
delete(failures, rule.UUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failures
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) SaveResetOrder(ctx context.Context, rules []model.FirewallRule) error {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return r.dbFor(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
for _, rule := range rules {
|
||||||
|
result := tx.Model(&model.FirewallRule{}).
|
||||||
|
Where("uuid = ? AND revision = ?", rule.UUID, rule.Revision).
|
||||||
|
Updates(map[string]interface{}{"sequence": rule.Sequence, "priority": rule.Priority, "revision": gorm.Expr("revision + 1")})
|
||||||
|
if result.Error != nil {
|
||||||
|
return result.Error
|
||||||
|
}
|
||||||
|
if result.RowsAffected == 0 {
|
||||||
|
return ErrFirewallRuleRevisionConflict
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *FirewallRuleRepo) dbFor(ctx context.Context) *gorm.DB {
|
||||||
|
return firewallDB(ctx, r.db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func firewallDB(ctx context.Context, fallback *gorm.DB) *gorm.DB {
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
if tx, ok := ctx.Value(constant.DB).(*gorm.DB); ok && tx != nil {
|
||||||
|
return tx.WithContext(ctx)
|
||||||
|
}
|
||||||
|
if fallback == nil {
|
||||||
|
fallback = global.DB
|
||||||
|
}
|
||||||
|
return fallback.WithContext(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareFirewallRule(rule *model.FirewallRule) error {
|
||||||
|
if rule == nil {
|
||||||
|
return fmt.Errorf("%w: rule is nil", ErrFirewallPersistenceInvalid)
|
||||||
|
}
|
||||||
|
if rule.Family == "" || rule.Protocol == "" || rule.Action == "" {
|
||||||
|
return fmt.Errorf("%w: atomic rule identity fields are required", ErrFirewallPersistenceInvalid)
|
||||||
|
}
|
||||||
|
if rule.UUID == "" {
|
||||||
|
rule.UUID = uuid.NewString()
|
||||||
|
}
|
||||||
|
if rule.Revision == 0 {
|
||||||
|
rule.Revision = 1
|
||||||
|
}
|
||||||
|
if rule.Origin == "" {
|
||||||
|
rule.Origin = constant.FirewallRuleOriginCreated
|
||||||
|
}
|
||||||
|
if rule.Owner == "" {
|
||||||
|
rule.Owner = constant.FirewallRuleSourceUser
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sanitizeRuleUpdates(updates map[string]interface{}) map[string]interface{} {
|
||||||
|
result := make(map[string]interface{}, len(updates)+1)
|
||||||
|
for key, value := range updates {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
delete(result, "id")
|
||||||
|
delete(result, "uuid")
|
||||||
|
delete(result, "revision")
|
||||||
|
delete(result, "created_at")
|
||||||
|
return result
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
)
|
||||||
|
|
||||||
|
type IForwardingRuleRepo interface {
|
||||||
|
List(context.Context) ([]model.ForwardingRule, error)
|
||||||
|
CreateBatch(context.Context, []model.ForwardingRule) error
|
||||||
|
DeleteBatch(context.Context, []uint) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingRuleRepo struct{}
|
||||||
|
|
||||||
|
func NewIForwardingRuleRepo() IForwardingRuleRepo { return &ForwardingRuleRepo{} }
|
||||||
|
|
||||||
|
func (r *ForwardingRuleRepo) List(ctx context.Context) ([]model.ForwardingRule, error) {
|
||||||
|
var rules []model.ForwardingRule
|
||||||
|
err := global.DB.WithContext(ctx).Order("id ASC").Find(&rules).Error
|
||||||
|
return rules, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ForwardingRuleRepo) CreateBatch(ctx context.Context, rules []model.ForwardingRule) error {
|
||||||
|
if len(rules) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return global.DB.WithContext(ctx).CreateInBatches(&rules, 500).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *ForwardingRuleRepo) DeleteBatch(ctx context.Context, ids []uint) error {
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return global.DB.WithContext(ctx).Where("id IN ?", ids).Delete(&model.ForwardingRule{}).Error
|
||||||
|
}
|
||||||
@@ -22,11 +22,6 @@ type IHostRepo interface {
|
|||||||
WithByPort(port uint) DBOption
|
WithByPort(port uint) DBOption
|
||||||
WithByUser(user string) DBOption
|
WithByUser(user string) DBOption
|
||||||
|
|
||||||
GetFirewallRecord(opts ...DBOption) (model.Firewall, error)
|
|
||||||
ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error)
|
|
||||||
SaveFirewallRecord(firewall *model.Firewall) error
|
|
||||||
DeleteFirewallRecordByID(id uint) error
|
|
||||||
|
|
||||||
SyncCert(data []model.RootCert) error
|
SyncCert(data []model.RootCert) error
|
||||||
GetCert(opts ...DBOption) (model.RootCert, error)
|
GetCert(opts ...DBOption) (model.RootCert, error)
|
||||||
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
|
PageCert(limit, offset int, opts ...DBOption) (int64, []model.RootCert, error)
|
||||||
@@ -34,8 +29,6 @@ type IHostRepo interface {
|
|||||||
SaveCert(cert *model.RootCert) error
|
SaveCert(cert *model.RootCert) error
|
||||||
UpdateCert(id uint, vars map[string]interface{}) error
|
UpdateCert(id uint, vars map[string]interface{}) error
|
||||||
DeleteCert(opts ...DBOption) error
|
DeleteCert(opts ...DBOption) error
|
||||||
|
|
||||||
WithByChain(chain string) DBOption
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIHostRepo() IHostRepo {
|
func NewIHostRepo() IHostRepo {
|
||||||
@@ -116,65 +109,6 @@ func (h *HostRepo) Delete(opts ...DBOption) error {
|
|||||||
return db.Delete(&model.Host{}).Error
|
return db.Delete(&model.Host{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *HostRepo) GetFirewallRecord(opts ...DBOption) (model.Firewall, error) {
|
|
||||||
var firewall model.Firewall
|
|
||||||
db := global.DB
|
|
||||||
for _, opt := range opts {
|
|
||||||
db = opt(db)
|
|
||||||
}
|
|
||||||
err := db.First(&firewall).Error
|
|
||||||
return firewall, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HostRepo) ListFirewallRecord(opts ...DBOption) ([]model.Firewall, error) {
|
|
||||||
var firewalls []model.Firewall
|
|
||||||
db := global.DB
|
|
||||||
for _, opt := range opts {
|
|
||||||
db = opt(db)
|
|
||||||
}
|
|
||||||
if err := global.DB.Find(&firewalls).Error; err != nil {
|
|
||||||
return firewalls, nil
|
|
||||||
}
|
|
||||||
return firewalls, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
|
|
||||||
if firewall.ID != 0 {
|
|
||||||
return global.DB.Save(firewall).Error
|
|
||||||
}
|
|
||||||
var data model.Firewall
|
|
||||||
switch firewall.Type {
|
|
||||||
case "port":
|
|
||||||
_ = global.DB.Where("type = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND strategy = ?", "port",
|
|
||||||
firewall.DstPort,
|
|
||||||
firewall.Protocol,
|
|
||||||
firewall.SrcIP,
|
|
||||||
firewall.Strategy,
|
|
||||||
).First(&data).Error
|
|
||||||
case "ip":
|
|
||||||
_ = global.DB.Where("type = ? AND src_ip = ? AND strategy = ?", "address", firewall.SrcIP, firewall.Strategy).First(&data)
|
|
||||||
default:
|
|
||||||
_ = global.DB.Where("type = ? AND chain = ? AND src_port = ? AND dst_port = ? AND protocol = ? AND src_ip = ? AND dst_ip = ? AND strategy = ?",
|
|
||||||
firewall.Type,
|
|
||||||
firewall.Chain,
|
|
||||||
firewall.SrcPort,
|
|
||||||
firewall.DstPort,
|
|
||||||
firewall.Protocol,
|
|
||||||
firewall.SrcIP,
|
|
||||||
firewall.DstIP,
|
|
||||||
firewall.Strategy,
|
|
||||||
).First(&data).Error
|
|
||||||
}
|
|
||||||
if data.ID != 0 {
|
|
||||||
firewall.ID = data.ID
|
|
||||||
}
|
|
||||||
return global.DB.Save(firewall).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *HostRepo) DeleteFirewallRecordByID(id uint) error {
|
|
||||||
return global.DB.Where("id = ?", id).Delete(&model.Firewall{}).Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
|
func (u *HostRepo) GetCert(opts ...DBOption) (model.RootCert, error) {
|
||||||
var cert model.RootCert
|
var cert model.RootCert
|
||||||
db := global.DB
|
db := global.DB
|
||||||
@@ -253,9 +187,3 @@ func (u *HostRepo) SyncCert(data []model.RootCert) error {
|
|||||||
tx.Commit()
|
tx.Commit()
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *HostRepo) WithByChain(chain string) DBOption {
|
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
|
||||||
return g.Where("chain = ?", chain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+106
-13
@@ -1,6 +1,8 @@
|
|||||||
package repo
|
package repo
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
@@ -10,11 +12,25 @@ import (
|
|||||||
|
|
||||||
type MonitorRepo struct{}
|
type MonitorRepo struct{}
|
||||||
|
|
||||||
|
type GPUHistoryPoint struct {
|
||||||
|
model.MonitorGPU
|
||||||
|
Bucket int64
|
||||||
|
PowerPercent *float64
|
||||||
|
MemoryPercent *float64
|
||||||
|
ProcessCount *float64
|
||||||
|
}
|
||||||
|
|
||||||
type IMonitorRepo interface {
|
type IMonitorRepo interface {
|
||||||
|
CleanHost() error
|
||||||
|
CleanGPU() error
|
||||||
GetBase(opts ...DBOption) ([]model.MonitorBase, 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)
|
GetIO(opts ...DBOption) ([]model.MonitorIO, error)
|
||||||
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, error)
|
||||||
|
GetIONames() ([]string, error)
|
||||||
|
GetNetworkNames() ([]string, error)
|
||||||
|
|
||||||
CreateMonitorBase(model model.MonitorBase) error
|
CreateMonitorBase(model model.MonitorBase) error
|
||||||
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
BatchCreateMonitorGPU(list []model.MonitorGPU) error
|
||||||
@@ -25,13 +41,26 @@ type IMonitorRepo interface {
|
|||||||
DelMonitorIO(timeForDelete time.Time) error
|
DelMonitorIO(timeForDelete time.Time) error
|
||||||
DelMonitorNet(timeForDelete time.Time) error
|
DelMonitorNet(timeForDelete time.Time) error
|
||||||
|
|
||||||
WithByProductName(name string) DBOption
|
WithByGPUDevice(deviceID, name string, legacy bool) DBOption
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIMonitorRepo() IMonitorRepo {
|
func NewIMonitorRepo() IMonitorRepo {
|
||||||
return &MonitorRepo{}
|
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) {
|
func (u *MonitorRepo) GetBase(opts ...DBOption) ([]model.MonitorBase, error) {
|
||||||
var data []model.MonitorBase
|
var data []model.MonitorBase
|
||||||
db := global.MonitorDB
|
db := global.MonitorDB
|
||||||
@@ -59,20 +88,25 @@ func (u *MonitorRepo) GetNetwork(opts ...DBOption) ([]model.MonitorNetwork, erro
|
|||||||
err := db.Find(&data).Error
|
err := db.Find(&data).Error
|
||||||
return data, err
|
return data, err
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) GetGPU(opts ...DBOption) ([]model.MonitorGPU, error) {
|
func (u *MonitorRepo) GetIONames() ([]string, error) {
|
||||||
var data []model.MonitorGPU
|
var names []string
|
||||||
db := global.GPUMonitorDB
|
err := global.MonitorDB.Model(&model.MonitorIO{}).Distinct().Pluck("name", &names).Error
|
||||||
for _, opt := range opts {
|
return names, err
|
||||||
db = opt(db)
|
}
|
||||||
}
|
|
||||||
err := db.Find(&data).Error
|
func (u *MonitorRepo) GetNetworkNames() ([]string, error) {
|
||||||
return data, err
|
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 {
|
func (u *MonitorRepo) CreateMonitorBase(model model.MonitorBase) error {
|
||||||
return global.MonitorDB.Create(&model).Error
|
return global.MonitorDB.Create(&model).Error
|
||||||
}
|
}
|
||||||
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
func (s *MonitorRepo) BatchCreateMonitorGPU(list []model.MonitorGPU) error {
|
||||||
|
if len(list) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
return global.GPUMonitorDB.CreateInBatches(&list, len(list)).Error
|
||||||
}
|
}
|
||||||
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
func (u *MonitorRepo) BatchCreateMonitorIO(ioList []model.MonitorIO) error {
|
||||||
@@ -94,8 +128,67 @@ func (s *MonitorRepo) DelMonitorGPU(timeForDelete time.Time) error {
|
|||||||
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
return global.GPUMonitorDB.Where("created_at < ?", timeForDelete).Delete(&model.MonitorGPU{}).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *MonitorRepo) WithByProductName(name string) DBOption {
|
func (u *MonitorRepo) GetGPUDevices() ([]model.MonitorGPU, error) {
|
||||||
return func(g *gorm.DB) *gorm.DB {
|
var data []model.MonitorGPU
|
||||||
return g.Where("product_name = ?", name)
|
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,78 @@
|
|||||||
|
package repo
|
||||||
|
|
||||||
|
import (
|
||||||
|
"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
|
||||||
|
HistogramSamples string
|
||||||
|
}
|
||||||
|
|
||||||
|
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(id uint, start, end time.Time) (int64, error) {
|
||||||
|
var count int64
|
||||||
|
db := global.VLLMMonitorDB.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(id uint, start, end time.Time, seconds int64, aggregation string) ([]VLLMHistoryPoint, error) {
|
||||||
|
db := global.VLLMMonitorDB.Model(&model.MonitorVLLM{}).Where("app_install_id = ? AND created_at >= ? AND created_at <= ?", id, start, end)
|
||||||
|
metrics := []string{"running", "waiting", "cache_usage", "prompt_throughput", "generation_throughput", "request_throughput", "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"}
|
||||||
|
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 {
|
||||||
|
if aggregation != "max" && (strings.HasPrefix(column, "time_to_first_token_p") || strings.HasPrefix(column, "time_per_output_token_p") || strings.HasPrefix(column, "request_latency_p")) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
columns = append(columns, operation+"("+column+") AS "+column)
|
||||||
|
}
|
||||||
|
if aggregation != "max" {
|
||||||
|
columns = append(columns, "json_group_array(json(NULLIF(histogram_deltas, ''))) AS histogram_samples")
|
||||||
|
}
|
||||||
|
db = db.Group("bucket").Order("bucket ASC")
|
||||||
|
} else {
|
||||||
|
columns = append([]string{"id", "created_at", "app_install_id", "status"}, metrics...)
|
||||||
|
db = db.Order("created_at ASC, id ASC")
|
||||||
|
}
|
||||||
|
var points []VLLMHistoryPoint
|
||||||
|
err := db.Select(strings.Join(columns, ", ")).Scan(&points).Error
|
||||||
|
return points, err
|
||||||
|
}
|
||||||
@@ -17,10 +17,12 @@ type IWebsiteRepo interface {
|
|||||||
WithGroupID(groupId uint) DBOption
|
WithGroupID(groupId uint) DBOption
|
||||||
WithDefaultServer() DBOption
|
WithDefaultServer() DBOption
|
||||||
WithDomainLike(domain string) DBOption
|
WithDomainLike(domain string) DBOption
|
||||||
|
WithSearchKeyword(keyword string, ids []uint) DBOption
|
||||||
WithRuntimeID(runtimeID uint) DBOption
|
WithRuntimeID(runtimeID uint) DBOption
|
||||||
WithParentID(websiteID uint) DBOption
|
WithParentID(websiteID uint) DBOption
|
||||||
WithType(websiteType string) DBOption
|
WithType(websiteType string) DBOption
|
||||||
WithDBType(dbType string) DBOption
|
WithDBType(dbType string) DBOption
|
||||||
|
WithDBTypes(dbTypes []string) DBOption
|
||||||
WithDBID(dbID uint) DBOption
|
WithDBID(dbID uint) DBOption
|
||||||
|
|
||||||
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
|
Page(page, size int, opts ...DBOption) (int64, []model.Website, error)
|
||||||
@@ -76,6 +78,19 @@ func (w *WebsiteRepo) WithDomainLike(domain string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteRepo) WithSearchKeyword(keyword string, ids []uint) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
if keyword == "" {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
keyword = "%" + keyword + "%"
|
||||||
|
if len(ids) == 0 {
|
||||||
|
return db.Where("(primary_domain like ? OR alias like ?)", keyword, keyword)
|
||||||
|
}
|
||||||
|
return db.Where("(primary_domain like ? OR alias like ? OR id in (?))", keyword, keyword, ids)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (w *WebsiteRepo) WithAlias(alias string) DBOption {
|
func (w *WebsiteRepo) WithAlias(alias string) DBOption {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Where("alias = ?", alias)
|
return db.Where("alias = ?", alias)
|
||||||
@@ -118,6 +133,12 @@ func (w *WebsiteRepo) WithDBType(dbType string) DBOption {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (w *WebsiteRepo) WithDBTypes(dbTypes []string) DBOption {
|
||||||
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
|
return db.Where("db_type IN ?", dbTypes)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
|
func (w *WebsiteRepo) WithDBID(dbID uint) DBOption {
|
||||||
return func(db *gorm.DB) *gorm.DB {
|
return func(db *gorm.DB) *gorm.DB {
|
||||||
return db.Where("db_id = ?", dbID)
|
return db.Where("db_id = ?", dbID)
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
+281
-141
@@ -8,8 +8,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
@@ -30,6 +30,8 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var agentAccountMutationMu sync.Mutex
|
||||||
|
|
||||||
type IAgentService interface {
|
type IAgentService interface {
|
||||||
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
||||||
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
|
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
|
||||||
@@ -77,6 +79,7 @@ type IAgentService interface {
|
|||||||
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
|
||||||
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
|
||||||
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
|
||||||
|
DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error)
|
||||||
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
|
CreateAccountModel(req dto.AgentAccountModelCreateReq) error
|
||||||
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
|
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
|
||||||
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
|
DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error
|
||||||
@@ -102,6 +105,10 @@ type IAgentService interface {
|
|||||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||||
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
||||||
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
||||||
|
ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error)
|
||||||
|
SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error)
|
||||||
|
InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error
|
||||||
|
OperatePlugin(req dto.AgentPluginOperateReq) error
|
||||||
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -144,7 +151,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
|
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
|
||||||
return nil, buserr.New("ErrNameIsExist")
|
return nil, buserr.New("ErrNameIsExist")
|
||||||
}
|
}
|
||||||
if !xpack.MultiNodeProvider.IsXpack() {
|
if !global.CONF.Base.IsEnterprise && !xpack.MultiNodeProvider.IsXpack() {
|
||||||
count, _, err := agentRepo.Page(1, 1)
|
count, _, err := agentRepo.Page(1, 1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -165,8 +172,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
provider := ""
|
provider := ""
|
||||||
baseURL := ""
|
baseURL := ""
|
||||||
apiType := ""
|
apiType := ""
|
||||||
maxTokens := 0
|
|
||||||
contextWindow := 0
|
|
||||||
apiKey := ""
|
apiKey := ""
|
||||||
runtimeModel := ""
|
runtimeModel := ""
|
||||||
accountID := uint(0)
|
accountID := uint(0)
|
||||||
@@ -176,6 +181,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
var allowedOrigins []string
|
var allowedOrigins []string
|
||||||
var account *model.AgentAccount
|
var account *model.AgentAccount
|
||||||
var installHooks *appInstallHooks
|
var installHooks *appInstallHooks
|
||||||
|
var dashboardAuth agentDashboardAuth
|
||||||
|
|
||||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||||
if req.AccountID == 0 {
|
if req.AccountID == 0 {
|
||||||
@@ -196,8 +202,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
}
|
}
|
||||||
storedModel = resolvedRuntime.StoredModel
|
storedModel = resolvedRuntime.StoredModel
|
||||||
apiType = resolvedRuntime.APIType
|
apiType = resolvedRuntime.APIType
|
||||||
maxTokens = resolvedRuntime.MaxTokens
|
|
||||||
contextWindow = resolvedRuntime.ContextWindow
|
|
||||||
runtimeModel = resolvedRuntime.PrimaryModel
|
runtimeModel = resolvedRuntime.PrimaryModel
|
||||||
apiKey = account.APIKey
|
apiKey = account.APIKey
|
||||||
accountID = account.ID
|
accountID = account.ID
|
||||||
@@ -222,11 +226,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if agentType == constant.AppHermesAgent {
|
} else if agentType == constant.AppHermesAgent {
|
||||||
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
installHooks = &appInstallHooks{
|
installHooks = &appInstallHooks{
|
||||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||||
return prepareHermesInstallFiles(appInstall, account, storedModel)
|
if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return writeAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType, dashboardAuth, false)
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
} else if agentType == constant.AppCopaw {
|
||||||
|
dashboardAuth = normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
params := map[string]interface{}{
|
params := map[string]interface{}{
|
||||||
@@ -242,12 +252,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
params["PROVIDER"] = provider
|
params["PROVIDER"] = provider
|
||||||
params["MODEL"] = runtimeModel
|
params["MODEL"] = runtimeModel
|
||||||
params["API_TYPE"] = apiType
|
params["API_TYPE"] = apiType
|
||||||
params["MAX_TOKENS"] = maxTokens
|
|
||||||
params["CONTEXT_WINDOW"] = contextWindow
|
|
||||||
params["BASE_URL"] = baseURL
|
params["BASE_URL"] = baseURL
|
||||||
params["API_KEY"] = apiKey
|
params["API_KEY"] = apiKey
|
||||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||||
}
|
}
|
||||||
|
if usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
|
params[usernameKey] = dashboardAuth.Username
|
||||||
|
params[passwordKey] = dashboardAuth.Password
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
params[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||||
return nil, buserr.New("ErrAgentComposeRequired")
|
return nil, buserr.New("ErrAgentComposeRequired")
|
||||||
@@ -282,22 +297,20 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
|||||||
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
|
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
|
||||||
}
|
}
|
||||||
agent := &model.Agent{
|
agent := &model.Agent{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
AgentType: agentType,
|
AgentType: agentType,
|
||||||
Provider: provider,
|
Provider: provider,
|
||||||
Model: storedModel,
|
Model: storedModel,
|
||||||
APIType: apiType,
|
APIType: apiType,
|
||||||
MaxTokens: maxTokens,
|
BaseURL: baseURL,
|
||||||
ContextWindow: contextWindow,
|
APIKey: apiKey,
|
||||||
BaseURL: baseURL,
|
Token: token,
|
||||||
APIKey: apiKey,
|
Status: appInstall.Status,
|
||||||
Token: token,
|
Message: appInstall.Message,
|
||||||
Status: appInstall.Status,
|
AppInstallID: appInstall.ID,
|
||||||
Message: appInstall.Message,
|
AccountID: accountID,
|
||||||
AppInstallID: appInstall.ID,
|
ConfigPath: configPath,
|
||||||
AccountID: accountID,
|
|
||||||
ConfigPath: configPath,
|
|
||||||
}
|
}
|
||||||
if err := agentRepo.Create(agent); err != nil {
|
if err := agentRepo.Create(agent); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -395,7 +408,7 @@ func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dt
|
|||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID)
|
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchTaskID(req.TaskID, "batch-skill-install", agent.ID), agent.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Message = err.Error()
|
result.Message = err.Error()
|
||||||
results = append(results, result)
|
results = append(results, result)
|
||||||
@@ -437,7 +450,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
|
|||||||
if operate == constant.Delete {
|
if operate == constant.Delete {
|
||||||
if err := a.Delete(dto.AgentDeleteReq{
|
if err := a.Delete(dto.AgentDeleteReq{
|
||||||
ID: agent.ID,
|
ID: agent.ID,
|
||||||
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
|
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
|
||||||
ForceDelete: req.ForceDelete,
|
ForceDelete: req.ForceDelete,
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
result.Message = err.Error()
|
result.Message = err.Error()
|
||||||
@@ -465,7 +478,7 @@ func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBat
|
|||||||
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||||
InstallId: install.ID,
|
InstallId: install.ID,
|
||||||
Operate: operate,
|
Operate: operate,
|
||||||
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
|
TaskID: buildBatchTaskID(req.TaskID, "batch-operate", agent.ID),
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
result.Message = err.Error()
|
result.Message = err.Error()
|
||||||
} else {
|
} else {
|
||||||
@@ -549,35 +562,19 @@ func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, [
|
|||||||
DetailID: detail.ID,
|
DetailID: detail.ID,
|
||||||
Backup: req.Backup,
|
Backup: req.Backup,
|
||||||
PullImage: req.PullImage,
|
PullImage: req.PullImage,
|
||||||
TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID),
|
TaskID: buildBatchTaskID(req.TaskID, "batch-upgrade", install.ID),
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return plans, results, nil
|
return plans, results, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string {
|
func buildBatchTaskID(taskID, prefix string, id uint) string {
|
||||||
taskID = strings.TrimSpace(taskID)
|
taskID = strings.TrimSpace(taskID)
|
||||||
if taskID == "" {
|
if taskID == "" {
|
||||||
taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano())
|
taskID = fmt.Sprintf("%s-%d-%d", prefix, id, time.Now().UnixNano())
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("%s-%d", taskID, appInstallID)
|
return fmt.Sprintf("%s-%d", taskID, id)
|
||||||
}
|
|
||||||
|
|
||||||
func buildBatchSkillInstallTaskID(taskID string, agentID uint) string {
|
|
||||||
taskID = strings.TrimSpace(taskID)
|
|
||||||
if taskID == "" {
|
|
||||||
taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s-%d", taskID, agentID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildBatchOperateTaskID(taskID string, agentID uint) string {
|
|
||||||
taskID = strings.TrimSpace(taskID)
|
|
||||||
if taskID == "" {
|
|
||||||
taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano())
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s-%d", taskID, agentID)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
|
func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
|
||||||
@@ -604,28 +601,30 @@ func batchOperateSkipMessage(operate constant.AppOperate, status string) string
|
|||||||
|
|
||||||
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
|
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
|
||||||
return dto.AgentCreateReq{
|
return dto.AgentCreateReq{
|
||||||
Name: req.Name,
|
Name: req.Name,
|
||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
AppVersion: req.AppVersion,
|
AppVersion: req.AppVersion,
|
||||||
WebUIPort: req.WebUIPort,
|
WebUIPort: req.WebUIPort,
|
||||||
BridgePort: req.BridgePort,
|
BridgePort: req.BridgePort,
|
||||||
AllowedOrigins: req.AllowedOrigins,
|
AllowedOrigins: req.AllowedOrigins,
|
||||||
AgentType: req.AgentType,
|
AgentType: req.AgentType,
|
||||||
Model: req.Model,
|
Model: req.Model,
|
||||||
AccountID: req.AccountID,
|
AccountID: req.AccountID,
|
||||||
Token: req.Token,
|
Token: req.Token,
|
||||||
TaskID: req.TaskID,
|
DashboardUsername: req.DashboardUsername,
|
||||||
Advanced: req.Advanced,
|
DashboardPassword: req.DashboardPassword,
|
||||||
ContainerName: req.ContainerName,
|
TaskID: req.TaskID,
|
||||||
AllowPort: req.AllowPort,
|
Advanced: req.Advanced,
|
||||||
SpecifyIP: req.SpecifyIP,
|
ContainerName: req.ContainerName,
|
||||||
RestartPolicy: req.RestartPolicy,
|
AllowPort: req.AllowPort,
|
||||||
CpuQuota: req.CpuQuota,
|
SpecifyIP: req.SpecifyIP,
|
||||||
MemoryLimit: req.MemoryLimit,
|
RestartPolicy: req.RestartPolicy,
|
||||||
MemoryUnit: req.MemoryUnit,
|
CpuQuota: req.CpuQuota,
|
||||||
PullImage: req.PullImage,
|
MemoryLimit: req.MemoryLimit,
|
||||||
EditCompose: req.EditCompose,
|
MemoryUnit: req.MemoryUnit,
|
||||||
DockerCompose: req.DockerCompose,
|
PullImage: req.PullImage,
|
||||||
|
EditCompose: req.EditCompose,
|
||||||
|
DockerCompose: req.DockerCompose,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -704,6 +703,7 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
|
|||||||
account.RememberAPIKey = snapshot.RememberAPIKey
|
account.RememberAPIKey = snapshot.RememberAPIKey
|
||||||
account.BaseURL = snapshot.BaseURL
|
account.BaseURL = snapshot.BaseURL
|
||||||
account.APIType = snapshot.APIType
|
account.APIType = snapshot.APIType
|
||||||
|
account.AuthMode = snapshot.AuthMode
|
||||||
account.Remark = snapshot.Remark
|
account.Remark = snapshot.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
@@ -711,6 +711,11 @@ func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (u
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
}
|
}
|
||||||
|
verifyModel, err := resolveAgentAccountVerifyModel(account.Provider, snapshot.VerifyModel, initialModels)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
account.VerifyModel = verifyModel
|
||||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if account.ID == 0 {
|
if account.ID == 0 {
|
||||||
if err := tx.Create(account).Error; err != nil {
|
if err := tx.Create(account).Error; err != nil {
|
||||||
@@ -931,6 +936,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
|
|||||||
AccountID: agent.AccountID,
|
AccountID: agent.AccountID,
|
||||||
Model: model,
|
Model: model,
|
||||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||||
|
Metadata: extractOpenclawModelMetadata(conf, account, models),
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -948,7 +954,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
modelName := resolvedRuntime.StoredModel
|
modelName := resolvedRuntime.StoredModel
|
||||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
apiType := resolvedRuntime.APIType
|
||||||
confDir := path.Dir(agent.ConfigPath)
|
confDir := path.Dir(agent.ConfigPath)
|
||||||
if agent.AgentType == constant.AppHermesAgent {
|
if agent.AgentType == constant.AppHermesAgent {
|
||||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||||
@@ -962,15 +968,13 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
|||||||
if agent.AgentType != constant.AppOpenclaw {
|
if agent.AgentType != constant.AppOpenclaw {
|
||||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
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
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
agent.Provider = account.Provider
|
agent.Provider = account.Provider
|
||||||
agent.Model = modelName
|
agent.Model = modelName
|
||||||
agent.APIType = apiType
|
agent.APIType = apiType
|
||||||
agent.MaxTokens = maxTokens
|
|
||||||
agent.ContextWindow = contextWindow
|
|
||||||
agent.BaseURL = account.BaseURL
|
agent.BaseURL = account.BaseURL
|
||||||
agent.APIKey = account.APIKey
|
agent.APIKey = account.APIKey
|
||||||
agent.AccountID = account.ID
|
agent.AccountID = account.ID
|
||||||
@@ -984,20 +988,38 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
models := make([]dto.ProviderModelInfo, 0, len(def.Models))
|
models := make([]dto.ProviderModelInfo, 0, len(def.Models))
|
||||||
for _, item := range def.Models {
|
for _, item := range def.Models {
|
||||||
models = append(models, dto.ProviderModelInfo{
|
models = append(models, dto.ProviderModelInfo{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
ContextWindow: item.ContextWindow,
|
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: append([]string(nil), item.Input...),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
apiTypes := make([]dto.ProviderAPIInfo, 0, len(def.APIConfigs))
|
||||||
|
for _, item := range def.APIConfigs {
|
||||||
|
apiModels := make([]dto.ProviderModelInfo, 0, len(item.Models))
|
||||||
|
for _, model := range item.Models {
|
||||||
|
apiModels = append(apiModels, dto.ProviderModelInfo{
|
||||||
|
ID: model.ID,
|
||||||
|
Name: model.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
apiTypes = append(apiTypes, dto.ProviderAPIInfo{
|
||||||
|
APIType: item.APIType,
|
||||||
|
BaseURL: item.BaseURL,
|
||||||
|
EditableBaseURL: item.EditableBaseURL,
|
||||||
|
SupportsModelDiscovery: item.DiscoverModels,
|
||||||
|
DefaultAuthMode: item.DefaultAuthMode,
|
||||||
|
AuthModes: item.AuthModes,
|
||||||
|
Models: apiModels,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
baseURL, _ := providercatalog.DefaultBaseURL(key)
|
||||||
providers = append(providers, dto.ProviderInfo{
|
providers = append(providers, dto.ProviderInfo{
|
||||||
Sort: def.Sort,
|
Sort: def.Sort,
|
||||||
Provider: key,
|
Provider: key,
|
||||||
DisplayName: def.DisplayName,
|
DisplayName: localizedAgentProviderName(key),
|
||||||
BaseURL: def.DefaultBaseURL,
|
BaseURL: baseURL,
|
||||||
Models: models,
|
DefaultAPIType: def.DefaultAPIType,
|
||||||
|
APITypes: apiTypes,
|
||||||
|
Models: models,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
sort.Slice(providers, func(i, j int) bool {
|
sort.Slice(providers, func(i, j int) bool {
|
||||||
@@ -1007,11 +1029,22 @@ func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
provider := req.Provider
|
provider := req.Provider
|
||||||
if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 {
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, 0); err != nil {
|
||||||
return buserr.New("ErrRecordExist")
|
return err
|
||||||
}
|
}
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
|
initialModels, err := buildInitialAgentAccountModels(&model.AgentAccount{Provider: provider, APIType: req.APIType}, req.Models)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
verifyModel, err := resolveAgentAccountVerifyModel(provider, req.VerifyModel, initialModels)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1021,14 +1054,12 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
APIKey: resolvedInput.APIKey,
|
APIKey: resolvedInput.APIKey,
|
||||||
RememberAPIKey: req.RememberAPIKey,
|
RememberAPIKey: req.RememberAPIKey,
|
||||||
BaseURL: resolvedInput.BaseURL,
|
BaseURL: resolvedInput.BaseURL,
|
||||||
APIType: req.APIType,
|
APIType: resolvedInput.APIType,
|
||||||
|
AuthMode: resolvedInput.AuthMode,
|
||||||
|
VerifyModel: verifyModel,
|
||||||
Verified: true,
|
Verified: true,
|
||||||
Remark: req.Remark,
|
Remark: req.Remark,
|
||||||
}
|
}
|
||||||
initialModels, err := buildInitialAgentAccountModels(account, req.Models)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if err := tx.Create(account).Error; err != nil {
|
if err := tx.Create(account).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1045,12 +1076,33 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||||
|
agentAccountMutationMu.Lock()
|
||||||
|
defer agentAccountMutationMu.Unlock()
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.APIType != account.APIType {
|
||||||
|
return buserr.WithDetail("ErrInvalidParams", "API type cannot be changed", nil)
|
||||||
|
}
|
||||||
provider := account.Provider
|
provider := account.Provider
|
||||||
resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL)
|
if err := ensureAgentAccountNameAvailable(provider, req.Name, account.ID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
models, err := loadAgentAccountModels(account)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
requestedVerifyModel := req.VerifyModel
|
||||||
|
if strings.TrimSpace(requestedVerifyModel) == "" {
|
||||||
|
requestedVerifyModel = account.VerifyModel
|
||||||
|
}
|
||||||
|
verifyModel, err := resolveAgentAccountVerifyModel(provider, requestedVerifyModel, models)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
validateAvailability := req.ValidateAvailability == nil || *req.ValidateAvailability
|
||||||
|
resolvedInput, err := resolveAgentAccountInput(provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, verifyModel, validateAvailability)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1058,7 +1110,9 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
|||||||
account.APIKey = resolvedInput.APIKey
|
account.APIKey = resolvedInput.APIKey
|
||||||
account.RememberAPIKey = req.RememberAPIKey
|
account.RememberAPIKey = req.RememberAPIKey
|
||||||
account.BaseURL = resolvedInput.BaseURL
|
account.BaseURL = resolvedInput.BaseURL
|
||||||
account.APIType = req.APIType
|
account.APIType = resolvedInput.APIType
|
||||||
|
account.AuthMode = resolvedInput.AuthMode
|
||||||
|
account.VerifyModel = verifyModel
|
||||||
account.Remark = req.Remark
|
account.Remark = req.Remark
|
||||||
account.Verified = true
|
account.Verified = true
|
||||||
|
|
||||||
@@ -1080,6 +1134,12 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
if strings.TrimSpace(req.Provider) != "" {
|
if strings.TrimSpace(req.Provider) != "" {
|
||||||
opts = append(opts, repo.WithByProvider(req.Provider))
|
opts = append(opts, repo.WithByProvider(req.Provider))
|
||||||
}
|
}
|
||||||
|
if apiType := strings.TrimSpace(req.APIType); apiType != "" {
|
||||||
|
opts = append(opts, repo.WithByAPIType(apiType))
|
||||||
|
}
|
||||||
|
if req.TextOnly {
|
||||||
|
opts = append(opts, repo.WithTextAPIType())
|
||||||
|
}
|
||||||
if strings.TrimSpace(req.Name) != "" {
|
if strings.TrimSpace(req.Name) != "" {
|
||||||
opts = append(opts, repo.WithByLikeName(req.Name))
|
opts = append(opts, repo.WithByLikeName(req.Name))
|
||||||
}
|
}
|
||||||
@@ -1097,30 +1157,46 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
|
|||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
MasterAccountID: item.MasterAccountID,
|
MasterAccountID: item.MasterAccountID,
|
||||||
Provider: item.Provider,
|
Provider: item.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(item.Provider),
|
ProviderName: localizedAgentProviderName(item.Provider),
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
RememberAPIKey: item.RememberAPIKey,
|
RememberAPIKey: item.RememberAPIKey,
|
||||||
BaseURL: item.BaseURL,
|
BaseURL: item.BaseURL,
|
||||||
Models: nil,
|
Models: nil,
|
||||||
APIType: item.APIType,
|
APIType: item.APIType,
|
||||||
|
AuthMode: item.AuthMode,
|
||||||
|
VerifyModel: item.VerifyModel,
|
||||||
Verified: item.Verified,
|
Verified: item.Verified,
|
||||||
Remark: item.Remark,
|
Remark: item.Remark,
|
||||||
CreatedAt: item.CreatedAt,
|
CreatedAt: item.CreatedAt,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
for i := range items {
|
if len(list) > 0 {
|
||||||
models, err := loadAgentAccountModels(&list[i])
|
accountIDs := make([]uint, 0, len(list))
|
||||||
if err != nil {
|
for _, account := range list {
|
||||||
|
accountIDs = append(accountIDs, account.ID)
|
||||||
|
}
|
||||||
|
var rows []model.AgentAccountModel
|
||||||
|
if err := global.DB.Where("account_id IN ?", accountIDs).Order("account_id ASC, sort_order ASC, id ASC").Find(&rows).Error; err != nil {
|
||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
items[i].Models = models
|
modelsByAccount := make(map[uint][]dto.AgentAccountModel, len(list))
|
||||||
|
for _, row := range rows {
|
||||||
|
modelsByAccount[row.AccountID] = append(modelsByAccount[row.AccountID], dto.AgentAccountModel{
|
||||||
|
RecordID: row.ID,
|
||||||
|
ID: strings.TrimSpace(row.Model),
|
||||||
|
Name: strings.TrimSpace(row.Name),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for index, account := range list {
|
||||||
|
items[index].Models = modelsByAccount[account.ID]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return count, items, nil
|
return count, items, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
|
||||||
return agentAccountRepo.CountByProviders(req.Providers)
|
return agentAccountRepo.CountTextByProviders(req.Providers)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
|
||||||
@@ -1131,6 +1207,22 @@ func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.Agen
|
|||||||
return loadAgentAccountModels(account)
|
return loadAgentAccountModels(account)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AgentService) DiscoverAccountModels(req dto.AgentAccountModelDiscoverReq) ([]dto.AgentAccountModel, error) {
|
||||||
|
config, ok := providercatalog.FindAPIConfig(req.Provider, req.APIType)
|
||||||
|
if !ok || !config.DiscoverModels {
|
||||||
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
|
}
|
||||||
|
baseURL, err := providercatalog.ResolveBaseURL(req.Provider, req.APIType, req.BaseURL)
|
||||||
|
if err != nil {
|
||||||
|
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||||
|
}
|
||||||
|
models, err := providercatalog.DiscoverModels(baseURL, req.APIKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||||
|
}
|
||||||
|
return buildDiscoveredAgentAccountModels(models), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
|
func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) error {
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -1140,24 +1232,19 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
nextModel := cloneAgentAccountModel(req.Model)
|
nextModel, err := normalizeAgentAccountModel(account, req.Model)
|
||||||
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
|
||||||
return buserr.New("ErrRecordExist")
|
|
||||||
}
|
|
||||||
inputPayload, err := json.Marshal(nextModel.Input)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
sortOrder := len(models) + 1
|
sortOrder := len(models) + 1
|
||||||
record := &model.AgentAccountModel{
|
record := &model.AgentAccountModel{
|
||||||
AccountID: account.ID,
|
AccountID: account.ID,
|
||||||
Model: nextModel.ID,
|
Model: nextModel.ID,
|
||||||
Name: nextModel.Name,
|
Name: nextModel.Name,
|
||||||
ContextWindow: nextModel.ContextWindow,
|
SortOrder: sortOrder,
|
||||||
MaxTokens: nextModel.MaxTokens,
|
|
||||||
Reasoning: nextModel.Reasoning,
|
|
||||||
Input: string(inputPayload),
|
|
||||||
SortOrder: sortOrder,
|
|
||||||
}
|
}
|
||||||
if err := agentAccountModelRepo.Create(record); err != nil {
|
if err := agentAccountModelRepo.Create(record); err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1178,7 +1265,11 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
nextModel := cloneAgentAccountModel(req.Model)
|
nextModel, err := normalizeAgentAccountModel(account, req.Model)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
nextModel.RecordID = req.Model.RecordID
|
||||||
for _, item := range models {
|
for _, item := range models {
|
||||||
if item.RecordID == req.Model.RecordID {
|
if item.RecordID == req.Model.RecordID {
|
||||||
continue
|
continue
|
||||||
@@ -1198,19 +1289,18 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
|||||||
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
if err := ensureAccountModelsNotBound(account, nextModels); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
inputPayload, err := json.Marshal(nextModel.Input)
|
previousModelID := record.Model
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
record.Model = nextModel.ID
|
record.Model = nextModel.ID
|
||||||
record.Name = nextModel.Name
|
record.Name = nextModel.Name
|
||||||
record.ContextWindow = nextModel.ContextWindow
|
|
||||||
record.MaxTokens = nextModel.MaxTokens
|
|
||||||
record.Reasoning = nextModel.Reasoning
|
|
||||||
record.Input = string(inputPayload)
|
|
||||||
if err := agentAccountModelRepo.Save(record); err != nil {
|
if err := agentAccountModelRepo.Save(record); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if sameProviderModelID(account.Provider, account.VerifyModel, previousModelID) {
|
||||||
|
account.VerifyModel = nextModel.ID
|
||||||
|
if err := agentAccountRepo.Save(account); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
terminalai.InvalidateFileAIRuntimeCache()
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
return a.syncAgentsByAccount(account)
|
return a.syncAgentsByAccount(account)
|
||||||
@@ -1221,9 +1311,13 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if _, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID)); err != nil {
|
record, err := agentAccountModelRepo.GetFirst(repo.WithByID(req.RecordID), repo.WithByAccountID(req.AccountID))
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if sameProviderModelID(account.Provider, account.VerifyModel, record.Model) {
|
||||||
|
return buserr.New("ErrAgentVerifyModelInUse")
|
||||||
|
}
|
||||||
models, err := loadAgentAccountModels(account)
|
models, err := loadAgentAccountModels(account)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1257,25 +1351,45 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error {
|
||||||
_, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL)
|
_, err := resolveAgentAccountInput(req.Provider, req.APIType, req.AuthMode, req.APIKey, req.BaseURL, req.Model, true)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||||
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
|
agentAccountMutationMu.Lock()
|
||||||
return buserr.New("ErrAgentAccountBound")
|
defer agentAccountMutationMu.Unlock()
|
||||||
}
|
if err := global.DB.Transaction(func(tx *gorm.DB) error {
|
||||||
if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) {
|
var agentCount int64
|
||||||
if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) {
|
if err := tx.Model(&model.Agent{}).Where("account_id = ?", req.ID).Count(&agentCount).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if agentCount > 0 {
|
||||||
|
return buserr.New("ErrAgentAccountBound")
|
||||||
|
}
|
||||||
|
used, err := agentAccountUsedBySetting(tx, req.ID, "AIStatus", "AIAccountID")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if used {
|
||||||
return buserr.New("ErrTerminalAIAccountInUse")
|
return buserr.New("ErrTerminalAIAccountInUse")
|
||||||
}
|
}
|
||||||
}
|
used, err = agentAccountUsedBySetting(tx, req.ID, "FileAIStatus", "FileAIAccountID")
|
||||||
if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if used {
|
||||||
|
return buserr.New("ErrFileAIAccountInUse")
|
||||||
|
}
|
||||||
|
if err := tx.Where("account_id = ?", req.ID).Delete(&model.AgentAccountModel{}).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Delete(&model.AgentAccount{}, req.ID).Error
|
||||||
|
}); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
terminalai.InvalidateTerminalRuntimeCache()
|
terminalai.InvalidateTerminalRuntimeCache()
|
||||||
terminalai.InvalidateFileAIRuntimeCache()
|
terminalai.InvalidateFileAIRuntimeCache()
|
||||||
return agentAccountRepo.DeleteByID(req.ID)
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
|
func (a AgentService) GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error) {
|
||||||
@@ -1327,10 +1441,20 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
return &dto.AgentOtherConfig{
|
return &dto.AgentOtherConfig{
|
||||||
UserTimezone: cfg.Timezone,
|
UserTimezone: cfg.Timezone,
|
||||||
BrowserEnabled: true,
|
BrowserEnabled: true,
|
||||||
NPMRegistry: "https://registry.npmjs.org/",
|
NPMRegistry: "https://registry.npmjs.org/",
|
||||||
|
DashboardUsername: auth.Username,
|
||||||
|
DashboardPassword: auth.Password,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
if agent.AgentType == constant.AppCopaw {
|
||||||
|
auth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
|
return &dto.AgentOtherConfig{
|
||||||
|
DashboardUsername: auth.Username,
|
||||||
|
DashboardPassword: auth.Password,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
@@ -1351,18 +1475,36 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if agent.AgentType == constant.AppHermesAgent {
|
if agent.AgentType == constant.AppHermesAgent {
|
||||||
|
if strings.TrimSpace(req.UserTimezone) == "" {
|
||||||
|
return buserr.New("ErrInvalidParams")
|
||||||
|
}
|
||||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
previousAuth := readAgentDashboardAuthFromInstall(install, agent.AgentType)
|
||||||
|
nextAuth := normalizeAgentDashboardAuth(req.DashboardUsername, req.DashboardPassword)
|
||||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := writeAgentDashboardAuthEnv(install.GetEnvPath(), agent.AgentType, nextAuth, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
operate := constant.Restart
|
||||||
|
if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
|
||||||
|
operate = constant.Rebuild
|
||||||
|
}
|
||||||
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||||
InstallId: install.ID,
|
InstallId: install.ID,
|
||||||
Operate: constant.Restart,
|
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 {
|
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -1534,7 +1676,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
modelName := resolvedRuntime.StoredModel
|
modelName := resolvedRuntime.StoredModel
|
||||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
apiType := resolvedRuntime.APIType
|
||||||
confDir := path.Dir(agent.ConfigPath)
|
confDir := path.Dir(agent.ConfigPath)
|
||||||
switch agent.AgentType {
|
switch agent.AgentType {
|
||||||
case constant.AppOpenclaw:
|
case constant.AppOpenclaw:
|
||||||
@@ -1543,7 +1685,7 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
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
|
return err
|
||||||
}
|
}
|
||||||
case constant.AppHermesAgent:
|
case constant.AppHermesAgent:
|
||||||
@@ -1562,8 +1704,6 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
|||||||
agent.Provider = account.Provider
|
agent.Provider = account.Provider
|
||||||
agent.Model = modelName
|
agent.Model = modelName
|
||||||
agent.APIType = apiType
|
agent.APIType = apiType
|
||||||
agent.MaxTokens = maxTokens
|
|
||||||
agent.ContextWindow = contextWindow
|
|
||||||
_ = agentRepo.Save(&agent)
|
_ = agentRepo.Save(&agent)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -811,9 +812,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
|
|||||||
result.RequireMention = result.GroupPolicy == "allowlist"
|
result.RequireMention = result.GroupPolicy == "allowlist"
|
||||||
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
|
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
|
||||||
result.Proxy = extractStringValue(telegram["proxy"])
|
result.Proxy = extractStringValue(telegram["proxy"])
|
||||||
if streaming := extractStringValue(telegram["streaming"]); streaming != "" {
|
result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming)
|
||||||
result.Streaming = streaming
|
|
||||||
}
|
|
||||||
accounts := childMap(telegram, "accounts")
|
accounts := childMap(telegram, "accounts")
|
||||||
if len(accounts) == 0 {
|
if len(accounts) == 0 {
|
||||||
botToken := extractStringValue(telegram["botToken"])
|
botToken := extractStringValue(telegram["botToken"])
|
||||||
@@ -839,7 +838,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
|
|||||||
BotToken: extractStringValue(account["botToken"]),
|
BotToken: extractStringValue(account["botToken"]),
|
||||||
DmPolicy: extractStringValue(account["dmPolicy"]),
|
DmPolicy: extractStringValue(account["dmPolicy"]),
|
||||||
GroupPolicy: extractStringValue(account["groupPolicy"]),
|
GroupPolicy: extractStringValue(account["groupPolicy"]),
|
||||||
Streaming: extractStringValue(account["streaming"]),
|
Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
|
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
|
||||||
@@ -879,7 +878,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
|
|||||||
} else {
|
} else {
|
||||||
delete(telegram, "proxy")
|
delete(telegram, "proxy")
|
||||||
}
|
}
|
||||||
telegram["streaming"] = config.Streaming
|
telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming)
|
||||||
accounts := make(map[string]interface{}, len(config.Bots))
|
accounts := make(map[string]interface{}, len(config.Bots))
|
||||||
for _, bot := range config.Bots {
|
for _, bot := range config.Bots {
|
||||||
account := map[string]interface{}{
|
account := map[string]interface{}{
|
||||||
@@ -888,7 +887,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
|
|||||||
"botToken": bot.BotToken,
|
"botToken": bot.BotToken,
|
||||||
"dmPolicy": bot.DmPolicy,
|
"dmPolicy": bot.DmPolicy,
|
||||||
"groupPolicy": bot.GroupPolicy,
|
"groupPolicy": bot.GroupPolicy,
|
||||||
"streaming": bot.Streaming,
|
"streaming": buildTelegramStreamingConfig(bot.Streaming),
|
||||||
}
|
}
|
||||||
if bot.DmPolicy == "open" {
|
if bot.DmPolicy == "open" {
|
||||||
account["allowFrom"] = []string{"*"}
|
account["allowFrom"] = []string{"*"}
|
||||||
@@ -899,6 +898,29 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
|
|||||||
delete(telegram, "botToken")
|
delete(telegram, "botToken")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
|
||||||
|
mode := defaultMode
|
||||||
|
switch typed := value.(type) {
|
||||||
|
case string:
|
||||||
|
mode = typed
|
||||||
|
case map[string]interface{}:
|
||||||
|
mode = extractStringValue(typed["mode"])
|
||||||
|
}
|
||||||
|
mode = strings.ToLower(strings.TrimSpace(mode))
|
||||||
|
switch mode {
|
||||||
|
case "off", "partial", "block", "progress":
|
||||||
|
return mode
|
||||||
|
default:
|
||||||
|
return "partial"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"mode": normalizeTelegramStreamingMode(mode, "partial"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
|
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
|
||||||
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
|
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
|
||||||
discord := getChannelConfig(conf, "discord")
|
discord := getChannelConfig(conf, "discord")
|
||||||
@@ -1299,6 +1321,10 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
|
func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
|
||||||
|
help, err := cmd.RunDockerExecWithStdout(time.Minute, containerName, "openclaw", "plugins", "install", "--help")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
|
workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
|
||||||
defer func() {
|
defer func() {
|
||||||
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
_ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
|
||||||
@@ -1320,7 +1346,19 @@ func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID
|
|||||||
if pkgPath == "" {
|
if pkgPath == "" {
|
||||||
return fmt.Errorf("openclaw plugin package not found")
|
return fmt.Errorf("openclaw plugin package not found")
|
||||||
}
|
}
|
||||||
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 {
|
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
|
||||||
|
}
|
||||||
@@ -13,9 +13,7 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
"github.com/joho/godotenv"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -71,7 +69,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
provider := resolveHermesProvider(account.Provider)
|
provider := resolveHermesProvider(account.Provider, account.APIType)
|
||||||
configPath := path.Join(confDir, "config.yaml")
|
configPath := path.Join(confDir, "config.yaml")
|
||||||
cfg, err := readHermesConfigMap(configPath)
|
cfg, err := readHermesConfigMap(configPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -152,7 +150,7 @@ func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
|
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -209,7 +207,7 @@ func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig,
|
|||||||
|
|
||||||
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
|
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -229,7 +227,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
|
|||||||
envMap["TELEGRAM_ALLOWED_USERS"] = allow
|
envMap["TELEGRAM_ALLOWED_USERS"] = allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
if err := writeAgentEnvMap(envPath, envMap, []string{
|
||||||
"TELEGRAM_BOT_TOKEN",
|
"TELEGRAM_BOT_TOKEN",
|
||||||
"TELEGRAM_ALLOWED_USERS",
|
"TELEGRAM_ALLOWED_USERS",
|
||||||
"TELEGRAM_ALLOW_ALL_USERS",
|
"TELEGRAM_ALLOW_ALL_USERS",
|
||||||
@@ -248,7 +246,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
|
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -300,7 +298,7 @@ func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, er
|
|||||||
|
|
||||||
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
|
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -320,7 +318,7 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
|
|||||||
envMap["DISCORD_ALLOWED_USERS"] = allow
|
envMap["DISCORD_ALLOWED_USERS"] = allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
if err := writeAgentEnvMap(envPath, envMap, []string{
|
||||||
"DISCORD_BOT_TOKEN",
|
"DISCORD_BOT_TOKEN",
|
||||||
"DISCORD_ALLOWED_USERS",
|
"DISCORD_ALLOWED_USERS",
|
||||||
"DISCORD_ALLOW_ALL_USERS",
|
"DISCORD_ALLOW_ALL_USERS",
|
||||||
@@ -340,14 +338,14 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
|
|||||||
|
|
||||||
func deleteHermesEnvKeys(confDir string, keys ...string) error {
|
func deleteHermesEnvKeys(confDir string, keys ...string) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for _, key := range keys {
|
for _, key := range keys {
|
||||||
delete(envMap, key)
|
delete(envMap, key)
|
||||||
}
|
}
|
||||||
return writeHermesEnvMap(envPath, envMap, keys)
|
return writeAgentEnvMap(envPath, envMap, keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
|
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
|
||||||
@@ -407,11 +405,18 @@ func normalizeHermesTimezone(timezone string) string {
|
|||||||
return timezone
|
return timezone
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveHermesProvider(provider string) string {
|
func resolveHermesProvider(provider, apiType string) string {
|
||||||
|
if apiType == "anthropic-messages" && (provider == "deepseek" || provider == "bailian-coding-plan" || provider == "ark-coding-plan" || provider == "xiaomi") {
|
||||||
|
return "anthropic"
|
||||||
|
}
|
||||||
switch provider {
|
switch provider {
|
||||||
case "":
|
case "":
|
||||||
return "custom"
|
return "custom"
|
||||||
case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
|
case "moonshot":
|
||||||
|
return "kimi-coding"
|
||||||
|
case "kimi":
|
||||||
|
return "kimi-coding-cn"
|
||||||
|
case "openrouter", "anthropic", "deepseek", "gemini", "zai", "kimi-coding", "xiaomi":
|
||||||
return provider
|
return provider
|
||||||
case "minimax":
|
case "minimax":
|
||||||
return "minimax-cn"
|
return "minimax-cn"
|
||||||
@@ -428,17 +433,7 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
|
|||||||
if targetProvider != "custom" {
|
if targetProvider != "custom" {
|
||||||
return target
|
return target
|
||||||
}
|
}
|
||||||
if sourceProvider == "custom" || sourceProvider == "vllm" {
|
return providercatalog.NormalizeModelID(sourceProvider, target)
|
||||||
return normalizeCustomModel(target)
|
|
||||||
}
|
|
||||||
if strings.Contains(target, "/") {
|
|
||||||
parts := strings.SplitN(target, "/", 2)
|
|
||||||
model := strings.TrimSpace(parts[1])
|
|
||||||
if model != "" {
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
|
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
|
||||||
@@ -449,7 +444,7 @@ func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels [
|
|||||||
if configuredModel == "" {
|
if configuredModel == "" {
|
||||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||||
}
|
}
|
||||||
provider := resolveHermesProvider(account.Provider)
|
provider := resolveHermesProvider(account.Provider, account.APIType)
|
||||||
for _, item := range accountModels {
|
for _, item := range accountModels {
|
||||||
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
|
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
|
||||||
return item.ID, nil
|
return item.ID, nil
|
||||||
@@ -462,7 +457,8 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
if resolveHermesProvider(account.Provider) == "custom" {
|
resolvedProvider := resolveHermesProvider(account.Provider, account.APIType)
|
||||||
|
if resolvedProvider == "custom" {
|
||||||
if account.APIKey == "" {
|
if account.APIKey == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -480,6 +476,11 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
}
|
}
|
||||||
entries = append(entries, hermesEnvEntry{Key: key, Value: value})
|
entries = append(entries, hermesEnvEntry{Key: key, Value: value})
|
||||||
}
|
}
|
||||||
|
if resolvedProvider == "anthropic" {
|
||||||
|
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
||||||
|
appendEntry("ANTHROPIC_BASE_URL", baseURL)
|
||||||
|
return entries
|
||||||
|
}
|
||||||
|
|
||||||
switch account.Provider {
|
switch account.Provider {
|
||||||
case "openrouter":
|
case "openrouter":
|
||||||
@@ -487,6 +488,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
appendEntry("OPENROUTER_BASE_URL", baseURL)
|
appendEntry("OPENROUTER_BASE_URL", baseURL)
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
||||||
|
appendEntry("ANTHROPIC_BASE_URL", baseURL)
|
||||||
case "gemini":
|
case "gemini":
|
||||||
appendEntry("GOOGLE_API_KEY", apiKey)
|
appendEntry("GOOGLE_API_KEY", apiKey)
|
||||||
appendEntry("GEMINI_API_KEY", apiKey)
|
appendEntry("GEMINI_API_KEY", apiKey)
|
||||||
@@ -532,7 +534,7 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
|
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -548,7 +550,7 @@ func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
|
|||||||
envMap[entry.Key] = entry.Value
|
envMap[entry.Key] = entry.Value
|
||||||
order = append(order, entry.Key)
|
order = append(order, entry.Key)
|
||||||
}
|
}
|
||||||
return writeHermesEnvMap(envPath, envMap, order)
|
return writeAgentEnvMap(envPath, envMap, order)
|
||||||
}
|
}
|
||||||
|
|
||||||
func hermesManagedModelEnvKeys() []string {
|
func hermesManagedModelEnvKeys() []string {
|
||||||
@@ -556,6 +558,7 @@ func hermesManagedModelEnvKeys() []string {
|
|||||||
"OPENROUTER_API_KEY",
|
"OPENROUTER_API_KEY",
|
||||||
"OPENROUTER_BASE_URL",
|
"OPENROUTER_BASE_URL",
|
||||||
"ANTHROPIC_API_KEY",
|
"ANTHROPIC_API_KEY",
|
||||||
|
"ANTHROPIC_BASE_URL",
|
||||||
"GOOGLE_API_KEY",
|
"GOOGLE_API_KEY",
|
||||||
"GEMINI_API_KEY",
|
"GEMINI_API_KEY",
|
||||||
"GEMINI_BASE_URL",
|
"GEMINI_BASE_URL",
|
||||||
@@ -598,46 +601,6 @@ func hermesManagedModelEnvKeys() []string {
|
|||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func writeHermesEnv(envPath string, entries []hermesEnvEntry) error {
|
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.Key == "" || entry.Value == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
envMap[entry.Key] = entry.Value
|
|
||||||
}
|
|
||||||
order := make([]string, 0, len(entries))
|
|
||||||
for _, entry := range entries {
|
|
||||||
if entry.Key == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
order = append(order, entry.Key)
|
|
||||||
}
|
|
||||||
return writeHermesEnvMap(envPath, envMap, order)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readHermesEnvMap(envPath string) (map[string]string, error) {
|
|
||||||
fileOp := files.NewFileOp()
|
|
||||||
if !fileOp.Stat(envPath) {
|
|
||||||
return map[string]string{}, nil
|
|
||||||
}
|
|
||||||
envMap, err := godotenv.Read(envPath)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return envMap, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func writeHermesEnvMap(envPath string, envMap map[string]string, order []string) error {
|
|
||||||
if len(envMap) == 0 {
|
|
||||||
return files.NewFileOp().SaveFile(envPath, "", 0600)
|
|
||||||
}
|
|
||||||
return agentenv.WriteWithOrder(envMap, envPath, order)
|
|
||||||
}
|
|
||||||
|
|
||||||
func splitHermesEnvList(value string) []string {
|
func splitHermesEnvList(value string) []string {
|
||||||
if value == "" {
|
if value == "" {
|
||||||
return []string{}
|
return []string{}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ print('Restart the Hermes-Agent container to apply the new Weixin settings.')
|
|||||||
`
|
`
|
||||||
|
|
||||||
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
|
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -78,7 +78,7 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
|
|||||||
|
|
||||||
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
|
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -102,7 +102,7 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
|
|||||||
envMap["QQ_GROUP_ALLOWED_USERS"] = allow
|
envMap["QQ_GROUP_ALLOWED_USERS"] = allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
if err := writeAgentEnvMap(envPath, envMap, []string{
|
||||||
"QQ_APP_ID",
|
"QQ_APP_ID",
|
||||||
"QQ_CLIENT_SECRET",
|
"QQ_CLIENT_SECRET",
|
||||||
"QQ_ALLOW_ALL_USERS",
|
"QQ_ALLOW_ALL_USERS",
|
||||||
@@ -163,7 +163,7 @@ func deleteHermesQQBotChannelConfig(confDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
|
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -194,7 +194,7 @@ func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error)
|
|||||||
|
|
||||||
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
|
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -229,7 +229,7 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
|
|||||||
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
|
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
if err := writeAgentEnvMap(envPath, envMap, []string{
|
||||||
"WECOM_BOT_ID",
|
"WECOM_BOT_ID",
|
||||||
"WECOM_SECRET",
|
"WECOM_SECRET",
|
||||||
"WECOM_DM_POLICY",
|
"WECOM_DM_POLICY",
|
||||||
@@ -282,7 +282,7 @@ func deleteHermesWecomChannelConfig(confDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
|
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -332,7 +332,7 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
|
|||||||
|
|
||||||
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
|
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -356,7 +356,7 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
|
|||||||
} else if config.DmPolicy == "open" {
|
} else if config.DmPolicy == "open" {
|
||||||
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
|
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
|
||||||
}
|
}
|
||||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
if err := writeAgentEnvMap(envPath, envMap, []string{
|
||||||
"DINGTALK_CLIENT_ID",
|
"DINGTALK_CLIENT_ID",
|
||||||
"DINGTALK_CLIENT_SECRET",
|
"DINGTALK_CLIENT_SECRET",
|
||||||
"DINGTALK_ALLOW_ALL_USERS",
|
"DINGTALK_ALLOW_ALL_USERS",
|
||||||
@@ -398,7 +398,7 @@ func deleteHermesDingTalkChannelConfig(confDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
|
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -452,7 +452,7 @@ func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, erro
|
|||||||
|
|
||||||
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
|
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
|
||||||
envPath := path.Join(confDir, ".env")
|
envPath := path.Join(confDir, ".env")
|
||||||
envMap, err := readHermesEnvMap(envPath)
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -488,7 +488,7 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
|
|||||||
envMap["FEISHU_ALLOWED_USERS"] = allow
|
envMap["FEISHU_ALLOWED_USERS"] = allow
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
if err := writeAgentEnvMap(envPath, envMap, []string{
|
||||||
"FEISHU_APP_ID",
|
"FEISHU_APP_ID",
|
||||||
"FEISHU_APP_SECRET",
|
"FEISHU_APP_SECRET",
|
||||||
"FEISHU_DOMAIN",
|
"FEISHU_DOMAIN",
|
||||||
@@ -529,7 +529,7 @@ func deleteHermesFeishuChannelConfig(confDir string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
|
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
|
||||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,315 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/compose"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
openclawPluginPackagePattern = regexp.MustCompile(`^(@[a-z0-9][a-z0-9._-]*/)?[a-z0-9][a-z0-9._-]*$`)
|
||||||
|
openclawPluginVersionPattern = regexp.MustCompile(`^[0-9A-Za-z][0-9A-Za-z._-]*$`)
|
||||||
|
openclawPluginIDPattern = regexp.MustCompile(`^(@[A-Za-z0-9][A-Za-z0-9._-]*/)?[A-Za-z0-9][A-Za-z0-9._-]*$`)
|
||||||
|
)
|
||||||
|
|
||||||
|
type openclawPluginListOutput struct {
|
||||||
|
Plugins []struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
} `json:"plugins"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openclawPluginIndexItem struct {
|
||||||
|
PluginID string `json:"pluginId"`
|
||||||
|
PackageName string `json:"packageName"`
|
||||||
|
PackageVersion string `json:"packageVersion"`
|
||||||
|
Origin string `json:"origin"`
|
||||||
|
Enabled bool `json:"enabled"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type openclawPluginSearchOutput struct {
|
||||||
|
Results []struct {
|
||||||
|
Score float64 `json:"score"`
|
||||||
|
Package struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
RuntimeID string `json:"runtimeId"`
|
||||||
|
DisplayName string `json:"displayName"`
|
||||||
|
Summary string `json:"summary"`
|
||||||
|
LatestVersion string `json:"latestVersion"`
|
||||||
|
Categories []string `json:"categories"`
|
||||||
|
Channel string `json:"channel"`
|
||||||
|
IsOfficial bool `json:"isOfficial"`
|
||||||
|
VerificationTier string `json:"verificationTier"`
|
||||||
|
Stats struct {
|
||||||
|
Downloads int64 `json:"downloads"`
|
||||||
|
} `json:"stats"`
|
||||||
|
} `json:"package"`
|
||||||
|
} `json:"results"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) ListPlugins(req dto.AgentPluginsReq) ([]dto.AgentPluginItem, error) {
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if plugins, err := readOpenclawPluginIndex(filepath.Join(filepath.Dir(agent.ConfigPath), "state", "openclaw.sqlite")); err == nil {
|
||||||
|
return plugins, nil
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(2*time.Minute, install.ContainerName, "openclaw", "plugins", "list", "--json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseOpenclawPluginList([]byte(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) SearchPlugins(req dto.AgentPluginSearchReq) ([]dto.AgentPluginSearchItem, error) {
|
||||||
|
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
limit := req.Limit
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 20
|
||||||
|
}
|
||||||
|
output, err := cmd.RunDockerExecWithStdout(
|
||||||
|
2*time.Minute,
|
||||||
|
install.ContainerName,
|
||||||
|
"openclaw", "plugins", "search", strings.TrimSpace(req.Keyword), "--limit", fmt.Sprint(limit), "--json",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return parseOpenclawPluginSearch([]byte(output))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) InstallMarketPlugin(req dto.AgentPluginMarketInstallReq) error {
|
||||||
|
spec, err := buildOpenclawPluginInstallSpec(req.Package, req.Version)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey("AgentPluginInstall"), req.Package)
|
||||||
|
installTask, err := task.NewTask(taskName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
installTask.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||||
|
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", "install", spec)
|
||||||
|
}, nil)
|
||||||
|
addOpenclawPluginRestartTask(installTask, install)
|
||||||
|
go executeAgentPluginTask(installTask)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AgentService) OperatePlugin(req dto.AgentPluginOperateReq) error {
|
||||||
|
if !openclawPluginIDPattern.MatchString(req.PluginID) {
|
||||||
|
return buserr.New("ErrInvalidChar")
|
||||||
|
}
|
||||||
|
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := task.CheckScopeTaskIsExecuting(task.TaskScopeAI, req.AgentID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.Operate == "update" || req.Operate == "uninstall" {
|
||||||
|
plugins, err := a.ListPlugins(dto.AgentPluginsReq{AgentID: req.AgentID})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
if plugin.ID == req.PluginID && plugin.Origin == "bundled" {
|
||||||
|
return buserr.WithName("ErrNotSupportType", req.Operate)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
taskType := map[string]string{
|
||||||
|
"enable": task.TaskUpdate,
|
||||||
|
"disable": task.TaskUpdate,
|
||||||
|
"update": task.TaskUpgrade,
|
||||||
|
"uninstall": task.TaskUninstall,
|
||||||
|
}[req.Operate]
|
||||||
|
taskName := fmt.Sprintf("%s [%s]", i18n.GetMsgByKey(map[string]string{
|
||||||
|
"enable": "AgentPluginEnable",
|
||||||
|
"disable": "AgentPluginDisable",
|
||||||
|
"update": "AgentPluginUpdate",
|
||||||
|
"uninstall": "AgentPluginUninstall",
|
||||||
|
}[req.Operate]), req.PluginID)
|
||||||
|
operateTask, err := task.NewTask(taskName, taskType, task.TaskScopeAI, req.TaskID, req.AgentID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
operateTask.AddSubTask(taskName, func(t *task.Task) error {
|
||||||
|
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||||
|
if req.Operate == "uninstall" {
|
||||||
|
if err := uninstallOpenclawPlugin(mgr, install.ContainerName, req.PluginID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return cleanupManagedOpenclawPlugin(agent, req.PluginID)
|
||||||
|
}
|
||||||
|
return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "plugins", req.Operate, req.PluginID)
|
||||||
|
}, nil)
|
||||||
|
addOpenclawPluginRestartTask(operateTask, install)
|
||||||
|
go executeAgentPluginTask(operateTask)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenclawPluginList(raw []byte) ([]dto.AgentPluginItem, error) {
|
||||||
|
payload, err := extractEmbeddedJSON(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return []dto.AgentPluginItem{}, nil
|
||||||
|
}
|
||||||
|
var output openclawPluginListOutput
|
||||||
|
if err := json.Unmarshal(payload, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginItem, 0, len(output.Plugins))
|
||||||
|
for _, plugin := range output.Plugins {
|
||||||
|
items = append(items, dto.AgentPluginItem{
|
||||||
|
ID: plugin.ID,
|
||||||
|
Name: plugin.Name,
|
||||||
|
Version: plugin.Version,
|
||||||
|
Origin: plugin.Origin,
|
||||||
|
Enabled: plugin.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readOpenclawPluginIndex(dbPath string) ([]dto.AgentPluginItem, error) {
|
||||||
|
db, err := sql.Open("sqlite", "file:"+filepath.ToSlash(dbPath)+"?mode=ro")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
var raw []byte
|
||||||
|
if err := db.QueryRow(
|
||||||
|
"SELECT plugins_json FROM installed_plugin_index WHERE index_key = ?",
|
||||||
|
"installed-plugin-index",
|
||||||
|
).Scan(&raw); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var plugins []openclawPluginIndexItem
|
||||||
|
if err := json.Unmarshal(raw, &plugins); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginItem, 0, len(plugins))
|
||||||
|
for _, plugin := range plugins {
|
||||||
|
name := plugin.PackageName
|
||||||
|
if name == "" {
|
||||||
|
name = plugin.PluginID
|
||||||
|
}
|
||||||
|
items = append(items, dto.AgentPluginItem{
|
||||||
|
ID: plugin.PluginID,
|
||||||
|
Name: name,
|
||||||
|
Version: plugin.PackageVersion,
|
||||||
|
Origin: plugin.Origin,
|
||||||
|
Enabled: plugin.Enabled,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseOpenclawPluginSearch(raw []byte) ([]dto.AgentPluginSearchItem, error) {
|
||||||
|
payload, err := extractEmbeddedJSON(string(raw))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(payload) == 0 {
|
||||||
|
return []dto.AgentPluginSearchItem{}, nil
|
||||||
|
}
|
||||||
|
var output openclawPluginSearchOutput
|
||||||
|
if err := json.Unmarshal(payload, &output); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]dto.AgentPluginSearchItem, 0, len(output.Results))
|
||||||
|
for _, result := range output.Results {
|
||||||
|
items = append(items, dto.AgentPluginSearchItem{
|
||||||
|
Package: result.Package.Name,
|
||||||
|
PluginID: result.Package.RuntimeID,
|
||||||
|
Name: result.Package.DisplayName,
|
||||||
|
Description: result.Package.Summary,
|
||||||
|
Version: result.Package.LatestVersion,
|
||||||
|
Channel: result.Package.Channel,
|
||||||
|
VerificationTier: result.Package.VerificationTier,
|
||||||
|
Categories: append([]string{}, result.Package.Categories...),
|
||||||
|
Official: result.Package.IsOfficial,
|
||||||
|
Downloads: result.Package.Stats.Downloads,
|
||||||
|
Score: result.Score,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return items, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildOpenclawPluginInstallSpec(packageName, version string) (string, error) {
|
||||||
|
packageName = strings.TrimSpace(packageName)
|
||||||
|
version = strings.TrimSpace(version)
|
||||||
|
if !openclawPluginPackagePattern.MatchString(packageName) || !openclawPluginVersionPattern.MatchString(version) {
|
||||||
|
return "", buserr.New("ErrInvalidChar")
|
||||||
|
}
|
||||||
|
return "clawhub:" + packageName + "@" + version, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func cleanupManagedOpenclawPlugin(agent *model.Agent, pluginID string) error {
|
||||||
|
pluginType := map[string]string{
|
||||||
|
"openclaw-lark": "feishu",
|
||||||
|
"openclaw-qqbot": "qqbot",
|
||||||
|
"wecom-openclaw-plugin": "wecom",
|
||||||
|
"dingtalk-connector": "dingtalk",
|
||||||
|
"openclaw-weixin": "weixin",
|
||||||
|
}[pluginID]
|
||||||
|
if pluginType == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
cleanupOpenclawPluginConfig(conf, pluginType)
|
||||||
|
return writeOpenclawConfigRaw(agent.ConfigPath, conf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addOpenclawPluginRestartTask(t *task.Task, install *model.AppInstall) {
|
||||||
|
t.AddSubTask(task.GetTaskName("OpenClaw", task.TaskRestart, task.TaskScopeAI), func(t *task.Task) error {
|
||||||
|
output, err := compose.Restart(install.GetComposePath())
|
||||||
|
if output != "" {
|
||||||
|
t.Log(output)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func executeAgentPluginTask(t *task.Task) {
|
||||||
|
if err := t.Execute(); err != nil {
|
||||||
|
global.LOG.Errorf("operate openclaw plugin failed: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
+425
-276
@@ -4,10 +4,13 @@ import (
|
|||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"path"
|
"path"
|
||||||
|
"regexp"
|
||||||
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -19,9 +22,12 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -35,6 +41,39 @@ type resolvedAgentAccountInput struct {
|
|||||||
Provider string
|
Provider string
|
||||||
APIKey string
|
APIKey string
|
||||||
BaseURL string
|
BaseURL string
|
||||||
|
APIType string
|
||||||
|
AuthMode string
|
||||||
|
}
|
||||||
|
|
||||||
|
func ensureAgentAccountNameAvailable(provider, name string, excludeID uint) error {
|
||||||
|
opts := []repo.DBOption{repo.WithByProvider(provider), repo.WithByName(name)}
|
||||||
|
if excludeID > 0 {
|
||||||
|
opts = append(opts, repo.WithByNOTID(excludeID))
|
||||||
|
}
|
||||||
|
account, err := agentAccountRepo.GetFirst(opts...)
|
||||||
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if account != nil && account.ID > 0 {
|
||||||
|
return buserr.New("ErrRecordExist")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentAccountUsedBySetting(tx *gorm.DB, accountID uint, statusKey, accountIDKey string) (bool, error) {
|
||||||
|
var settings []model.Setting
|
||||||
|
if err := tx.Where("key IN ?", []string{statusKey, accountIDKey}).Find(&settings).Error; err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
values := make(map[string]string, len(settings))
|
||||||
|
for _, setting := range settings {
|
||||||
|
values[setting.Key] = setting.Value
|
||||||
|
}
|
||||||
|
return strings.EqualFold(strings.TrimSpace(values[statusKey]), constant.StatusEnable) &&
|
||||||
|
strings.TrimSpace(values[accountIDKey]) == strconv.FormatUint(uint64(accountID), 10), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
|
func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
|
||||||
@@ -59,19 +98,27 @@ func ensureContainerRunning(containerName string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
|
func resolveAgentAccountInput(provider, apiType, authMode, apiKey, baseURL, modelID string, validateAvailability bool) (resolvedAgentAccountInput, error) {
|
||||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||||
resolvedBaseURL := strings.TrimSpace(baseURL)
|
resolvedAPIType := strings.TrimSpace(apiType)
|
||||||
if resolvedBaseURL == "" {
|
resolvedAuthMode, err := providercatalog.ResolveAuthMode(provider, resolvedAPIType, authMode)
|
||||||
if requiresInitialAgentAccountModels(provider) {
|
if err != nil {
|
||||||
|
return resolvedAgentAccountInput{}, err
|
||||||
|
}
|
||||||
|
resolvedBaseURL, err := providercatalog.ResolveBaseURL(provider, resolvedAPIType, baseURL)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "base url is required") {
|
||||||
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired")
|
||||||
}
|
}
|
||||||
if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok {
|
return resolvedAgentAccountInput{}, err
|
||||||
resolvedBaseURL = defaultURL
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if !providercatalog.SkipVerification(provider) {
|
modelID = strings.TrimSpace(modelID)
|
||||||
if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil {
|
if modelID == "" {
|
||||||
|
return resolvedAgentAccountInput{}, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
|
}
|
||||||
|
imageAPI := providercatalog.IsImageAPIType(resolvedAPIType)
|
||||||
|
if validateAvailability && (imageAPI || providercatalog.IsEmbeddingAPIType(resolvedAPIType) || !providercatalog.SkipVerification(provider)) {
|
||||||
|
if err := providercatalog.VerifyAccount(provider, resolvedAPIType, resolvedAuthMode, resolvedBaseURL, resolvedAPIKey, modelID); err != nil {
|
||||||
return resolvedAgentAccountInput{}, err
|
return resolvedAgentAccountInput{}, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -79,6 +126,8 @@ func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAc
|
|||||||
Provider: provider,
|
Provider: provider,
|
||||||
APIKey: resolvedAPIKey,
|
APIKey: resolvedAPIKey,
|
||||||
BaseURL: resolvedBaseURL,
|
BaseURL: resolvedBaseURL,
|
||||||
|
APIType: resolvedAPIType,
|
||||||
|
AuthMode: resolvedAuthMode,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -335,26 +384,24 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
|
|||||||
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
|
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
|
||||||
agentType := agent.AgentType
|
agentType := agent.AgentType
|
||||||
item := dto.AgentItem{
|
item := dto.AgentItem{
|
||||||
ID: agent.ID,
|
ID: agent.ID,
|
||||||
Name: agent.Name,
|
Name: agent.Name,
|
||||||
Remark: agent.Remark,
|
Remark: agent.Remark,
|
||||||
AgentType: agentType,
|
AgentType: agentType,
|
||||||
Provider: agent.Provider,
|
Provider: agent.Provider,
|
||||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
ProviderName: localizedAgentProviderName(agent.Provider),
|
||||||
Model: agent.Model,
|
Model: agent.Model,
|
||||||
APIType: agent.APIType,
|
APIType: agent.APIType,
|
||||||
MaxTokens: agent.MaxTokens,
|
BaseURL: agent.BaseURL,
|
||||||
ContextWindow: agent.ContextWindow,
|
APIKey: maskKey(agent.APIKey),
|
||||||
BaseURL: agent.BaseURL,
|
Token: agent.Token,
|
||||||
APIKey: maskKey(agent.APIKey),
|
Status: agent.Status,
|
||||||
Token: agent.Token,
|
Message: agent.Message,
|
||||||
Status: agent.Status,
|
AppInstallID: agent.AppInstallID,
|
||||||
Message: agent.Message,
|
WebsiteID: agent.WebsiteID,
|
||||||
AppInstallID: agent.AppInstallID,
|
AccountID: agent.AccountID,
|
||||||
WebsiteID: agent.WebsiteID,
|
ConfigPath: agent.ConfigPath,
|
||||||
AccountID: agent.AccountID,
|
CreatedAt: agent.CreatedAt,
|
||||||
ConfigPath: agent.ConfigPath,
|
|
||||||
CreatedAt: agent.CreatedAt,
|
|
||||||
}
|
}
|
||||||
if appInstall != nil && appInstall.ID > 0 {
|
if appInstall != nil && appInstall.ID > 0 {
|
||||||
item.Container = appInstall.ContainerName
|
item.Container = appInstall.ContainerName
|
||||||
@@ -376,10 +423,24 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
|||||||
item.BridgePort = toInt(bridge)
|
item.BridgePort = toInt(bridge)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if _, _, ok := agentDashboardAuthEnvKeys(agentType); ok {
|
||||||
|
auth := readAgentDashboardAuthFromInstall(appInstall, agentType)
|
||||||
|
item.DashboardUsername = auth.Username
|
||||||
|
item.DashboardPassword = auth.Password
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return item
|
return item
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func localizedAgentProviderName(provider string) string {
|
||||||
|
if key := providercatalog.DisplayNameKey(provider); key != "" {
|
||||||
|
if name := strings.TrimSpace(i18n.GetMsgByKey(key)); name != "" {
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return providercatalog.DisplayName(provider)
|
||||||
|
}
|
||||||
|
|
||||||
func isAgentAppKey(appKey string) bool {
|
func isAgentAppKey(appKey string) bool {
|
||||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||||
}
|
}
|
||||||
@@ -677,13 +738,11 @@ type modelProvider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type modelEntry struct {
|
type modelEntry struct {
|
||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Reasoning bool `json:"reasoning"`
|
Input []string `json:"input,omitempty"`
|
||||||
Input []string `json:"input"`
|
ContextWindow int `json:"contextWindow,omitempty"`
|
||||||
ContextWindow int `json:"contextWindow"`
|
MaxTokens int `json:"maxTokens,omitempty"`
|
||||||
MaxTokens int `json:"maxTokens"`
|
|
||||||
Cost modelCost `json:"cost"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func requiresOpenclawProviderModels(provider string) bool {
|
func requiresOpenclawProviderModels(provider string) bool {
|
||||||
@@ -703,13 +762,6 @@ func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type modelCost struct {
|
|
||||||
Input float64 `json:"input"`
|
|
||||||
Output float64 `json:"output"`
|
|
||||||
CacheRead float64 `json:"cacheRead"`
|
|
||||||
CacheWrite float64 `json:"cacheWrite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type browserConfig struct {
|
type browserConfig struct {
|
||||||
Enabled bool `json:"enabled"`
|
Enabled bool `json:"enabled"`
|
||||||
ExecutablePath string `json:"executablePath"`
|
ExecutablePath string `json:"executablePath"`
|
||||||
@@ -718,7 +770,7 @@ type browserConfig struct {
|
|||||||
DefaultProfile string `json:"defaultProfile"`
|
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) == "" {
|
if strings.TrimSpace(confDir) == "" {
|
||||||
return fmt.Errorf("config dir is required")
|
return fmt.Errorf("config dir is required")
|
||||||
}
|
}
|
||||||
@@ -803,6 +855,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
}
|
}
|
||||||
conf = initial
|
conf = initial
|
||||||
} else {
|
} else {
|
||||||
|
preserveOpenclawModelMetadata(conf, cfg.Models)
|
||||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -857,16 +910,159 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
|||||||
if allowedOrigins != nil {
|
if allowedOrigins != nil {
|
||||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||||
}
|
}
|
||||||
|
if err := applyOpenclawModelMetadata(conf, account, metadata); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
envPath := path.Join(confDir, ".env")
|
envMap := map[string]string{
|
||||||
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
|
"OPENCLAW_GATEWAY_TOKEN": token,
|
||||||
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
|
|
||||||
lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
|
|
||||||
}
|
}
|
||||||
content := strings.Join(lines, "\n") + "\n"
|
order := []string{"OPENCLAW_GATEWAY_TOKEN"}
|
||||||
return fileOp.SaveFile(envPath, content, 0600)
|
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
|
||||||
|
envMap[envKey] = account.APIKey
|
||||||
|
order = append(order, envKey)
|
||||||
|
}
|
||||||
|
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) {
|
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||||
@@ -990,7 +1186,7 @@ func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.Ag
|
|||||||
return fmt.Errorf("app install is required")
|
return fmt.Errorf("app install is required")
|
||||||
}
|
}
|
||||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
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
|
return err
|
||||||
}
|
}
|
||||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||||
@@ -1053,7 +1249,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
|
func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
|
||||||
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.BaseURL, account.APIKey)
|
providerPatch, err := providercatalog.BuildOpenClawProviderPatch(account.Provider, model.ID, account.APIType, account.AuthMode, account.BaseURL, account.APIKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", modelEntry{}, "", modelProvider{}, err
|
return "", modelEntry{}, "", modelProvider{}, err
|
||||||
}
|
}
|
||||||
@@ -1065,28 +1261,24 @@ func buildOpenclawAccountModelConfig(account *model.AgentAccount, model dto.Agen
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var openclawVisionModelPattern = regexp.MustCompile(`(?i)(\b(gpt-4o|gpt-4\.1|gpt-[5-9]|o[134])\b|\bclaude-(3|4|sonnet|opus|haiku)\b|\bgemini\b|\b(qwen[\w.-]*-?vl|qwen-vl|qwen3\.[5-9]-plus)\b|\b(kimi-k2\.(5|6)|kimi-k2\.7-code|minimax-m3)\b|\b(vision|llava|pixtral|internvl|mllama|minicpm-v|glm-4v|omni)\b|(^|[-_/])vl([-_/]|$))`)
|
||||||
|
|
||||||
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
|
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
|
||||||
name := strings.TrimSpace(model.Name)
|
name := strings.TrimSpace(model.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = strings.TrimSpace(modelID)
|
name = strings.TrimSpace(modelID)
|
||||||
}
|
}
|
||||||
return modelEntry{
|
entry := modelEntry{ID: strings.TrimSpace(modelID), Name: name}
|
||||||
ID: strings.TrimSpace(modelID),
|
if openclawVisionModelPattern.MatchString(modelID) {
|
||||||
Name: name,
|
entry.Input = []string{"text", "image"}
|
||||||
Reasoning: model.Reasoning,
|
|
||||||
Input: sanitizeAgentAccountModelInputs(model.Input),
|
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
Cost: modelCost{},
|
|
||||||
}
|
}
|
||||||
|
return entry
|
||||||
}
|
}
|
||||||
|
|
||||||
type openclawAccountModelRuntime struct {
|
type openclawAccountModelRuntime struct {
|
||||||
StoredModel string
|
StoredModel string
|
||||||
PrimaryModel string
|
PrimaryModel string
|
||||||
APIType string
|
APIType string
|
||||||
MaxTokens int
|
|
||||||
ContextWindow int
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
|
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
|
||||||
@@ -1095,11 +1287,9 @@ func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.Age
|
|||||||
return openclawAccountModelRuntime{}, err
|
return openclawAccountModelRuntime{}, err
|
||||||
}
|
}
|
||||||
return openclawAccountModelRuntime{
|
return openclawAccountModelRuntime{
|
||||||
StoredModel: model.ID,
|
StoredModel: model.ID,
|
||||||
PrimaryModel: primaryModel,
|
PrimaryModel: primaryModel,
|
||||||
APIType: account.APIType,
|
APIType: account.APIType,
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1119,35 +1309,38 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto
|
|||||||
if account == nil {
|
if account == nil {
|
||||||
return nil, fmt.Errorf("account is required")
|
return nil, fmt.Errorf("account is required")
|
||||||
}
|
}
|
||||||
if requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
|
if account.Provider != "custom" && requiresInitialAgentAccountModels(account.Provider) && len(requested) > 1 {
|
||||||
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
|
return nil, buserr.New("ErrAgentAccountSingleInitialModel")
|
||||||
}
|
}
|
||||||
if len(requested) > 0 {
|
if len(requested) > 0 {
|
||||||
models := make([]dto.AgentAccountModel, 0, len(requested))
|
return normalizeAgentAccountModels(account, requested)
|
||||||
for _, item := range requested {
|
|
||||||
models = append(models, cloneAgentAccountModel(item))
|
|
||||||
}
|
|
||||||
return models, nil
|
|
||||||
}
|
}
|
||||||
meta, ok := providercatalog.Get(account.Provider)
|
defaultModels := providercatalog.DefaultModels(account.Provider, account.APIType)
|
||||||
if !ok || len(meta.Models) == 0 {
|
if len(defaultModels) == 0 {
|
||||||
if requiresInitialAgentAccountModels(account.Provider) {
|
if requiresInitialAgentAccountModels(account.Provider) {
|
||||||
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
return nil, buserr.New("ErrAgentAccountModelsRequired")
|
||||||
}
|
}
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
requested = make([]dto.AgentAccountModel, 0, len(meta.Models))
|
requested = make([]dto.AgentAccountModel, 0, len(defaultModels))
|
||||||
for _, item := range meta.Models {
|
for _, item := range defaultModels {
|
||||||
requested = append(requested, dto.AgentAccountModel{
|
requested = append(requested, dto.AgentAccountModel{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
ContextWindow: item.ContextWindow,
|
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: append([]string(nil), item.Input...),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return requested, nil
|
return normalizeAgentAccountModels(account, requested)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDiscoveredAgentAccountModels(modelIDs []string) []dto.AgentAccountModel {
|
||||||
|
models := make([]dto.AgentAccountModel, 0, len(modelIDs))
|
||||||
|
for _, modelID := range modelIDs {
|
||||||
|
models = append(models, dto.AgentAccountModel{
|
||||||
|
ID: modelID,
|
||||||
|
Name: modelID,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return models
|
||||||
}
|
}
|
||||||
|
|
||||||
func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
|
func compactPersistedAgentAccountModelSortOrder(accountID uint) error {
|
||||||
@@ -1175,18 +1368,6 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode
|
|||||||
return listPersistedAgentAccountModels(account.ID)
|
return listPersistedAgentAccountModels(account.ID)
|
||||||
}
|
}
|
||||||
|
|
||||||
func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel {
|
|
||||||
return dto.AgentAccountModel{
|
|
||||||
RecordID: model.RecordID,
|
|
||||||
ID: model.ID,
|
|
||||||
Name: model.Name,
|
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
Reasoning: model.Reasoning,
|
|
||||||
Input: append([]string(nil), model.Input...),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
|
func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
|
||||||
if account == nil {
|
if account == nil {
|
||||||
return nil, fmt.Errorf("account is required")
|
return nil, fmt.Errorf("account is required")
|
||||||
@@ -1210,12 +1391,8 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
requested = append(requested, dto.AgentAccountModel{
|
requested = append(requested, dto.AgentAccountModel{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
Name: item.Name,
|
Name: item.Name,
|
||||||
ContextWindow: item.ContextWindow,
|
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: append([]string(nil), item.Input...),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if len(requested) == len(existing) {
|
if len(requested) == len(existing) {
|
||||||
@@ -1234,18 +1411,10 @@ func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, e
|
|||||||
}
|
}
|
||||||
result := make([]dto.AgentAccountModel, 0, len(rows))
|
result := make([]dto.AgentAccountModel, 0, len(rows))
|
||||||
for _, row := range rows {
|
for _, row := range rows {
|
||||||
inputs := []string{}
|
|
||||||
if strings.TrimSpace(row.Input) != "" {
|
|
||||||
_ = json.Unmarshal([]byte(row.Input), &inputs)
|
|
||||||
}
|
|
||||||
result = append(result, dto.AgentAccountModel{
|
result = append(result, dto.AgentAccountModel{
|
||||||
RecordID: row.ID,
|
RecordID: row.ID,
|
||||||
ID: strings.TrimSpace(row.Model),
|
ID: strings.TrimSpace(row.Model),
|
||||||
Name: strings.TrimSpace(row.Name),
|
Name: strings.TrimSpace(row.Name),
|
||||||
ContextWindow: row.ContextWindow,
|
|
||||||
MaxTokens: row.MaxTokens,
|
|
||||||
Reasoning: row.Reasoning,
|
|
||||||
Input: sanitizeAgentAccountModelInputs(inputs),
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return result, nil
|
return result, nil
|
||||||
@@ -1256,19 +1425,11 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
for index, item := range models {
|
for index, item := range models {
|
||||||
inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(item.Input))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
record := &model.AgentAccountModel{
|
record := &model.AgentAccountModel{
|
||||||
AccountID: accountID,
|
AccountID: accountID,
|
||||||
Model: strings.TrimSpace(item.ID),
|
Model: strings.TrimSpace(item.ID),
|
||||||
Name: strings.TrimSpace(item.Name),
|
Name: strings.TrimSpace(item.Name),
|
||||||
ContextWindow: item.ContextWindow,
|
SortOrder: index + 1,
|
||||||
MaxTokens: item.MaxTokens,
|
|
||||||
Reasoning: item.Reasoning,
|
|
||||||
Input: string(inputPayload),
|
|
||||||
SortOrder: index + 1,
|
|
||||||
}
|
}
|
||||||
if err := tx.Create(record).Error; err != nil {
|
if err := tx.Create(record).Error; err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -1309,125 +1470,34 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
|||||||
if modelID == "" {
|
if modelID == "" {
|
||||||
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
|
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
|
||||||
}
|
}
|
||||||
|
modelID = providercatalog.NormalizeModelID(account.Provider, modelID)
|
||||||
name := strings.TrimSpace(model.Name)
|
name := strings.TrimSpace(model.Name)
|
||||||
if name == "" {
|
if name == "" {
|
||||||
name = modelID
|
name = modelID
|
||||||
}
|
}
|
||||||
inputs := sanitizeAgentAccountModelInputs(model.Input)
|
|
||||||
return dto.AgentAccountModel{
|
return dto.AgentAccountModel{
|
||||||
ID: normalizeAgentAccountModelID(account.Provider, modelID),
|
ID: modelID,
|
||||||
Name: name,
|
Name: name,
|
||||||
ContextWindow: model.ContextWindow,
|
|
||||||
MaxTokens: model.MaxTokens,
|
|
||||||
Reasoning: model.Reasoning,
|
|
||||||
Input: inputs,
|
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeAgentAccountModelID(provider, requestedID string) string {
|
|
||||||
switch provider {
|
|
||||||
case "custom", "vllm":
|
|
||||||
return normalizeCustomModel(requestedID)
|
|
||||||
case "ollama":
|
|
||||||
target := strings.TrimSpace(requestedID)
|
|
||||||
if strings.HasPrefix(target, "ollama/") {
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
target = strings.TrimLeft(strings.TrimSpace(target), "/")
|
|
||||||
if target == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return "ollama/" + target
|
|
||||||
default:
|
|
||||||
target := strings.TrimSpace(requestedID)
|
|
||||||
if target == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
prefix := poolModelPrefix(provider)
|
|
||||||
if strings.Contains(target, "/") {
|
|
||||||
parts := strings.SplitN(target, "/", 2)
|
|
||||||
targetPrefix := parts[0]
|
|
||||||
targetModel := strings.TrimSpace(parts[1])
|
|
||||||
if targetModel == "" {
|
|
||||||
return strings.TrimSpace(target)
|
|
||||||
}
|
|
||||||
for _, item := range supportedProviderModelPrefixes(provider) {
|
|
||||||
if item == targetPrefix {
|
|
||||||
if prefix != "" {
|
|
||||||
return prefix + "/" + targetModel
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return strings.TrimSpace(target)
|
|
||||||
}
|
|
||||||
target = strings.TrimLeft(strings.TrimSpace(target), "/")
|
|
||||||
if prefix == "" {
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
return prefix + "/" + target
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func sanitizeAgentAccountModelInputs(values []string) []string {
|
|
||||||
result := make([]string, 0, len(values))
|
|
||||||
seen := make(map[string]struct{}, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
normalized := value
|
|
||||||
if normalized != "text" && normalized != "image" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[normalized]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[normalized] = struct{}{}
|
|
||||||
result = append(result, normalized)
|
|
||||||
}
|
|
||||||
if len(result) == 0 {
|
|
||||||
return []string{"text"}
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func requiresInitialAgentAccountModels(provider string) bool {
|
func requiresInitialAgentAccountModels(provider string) bool {
|
||||||
switch provider {
|
switch provider {
|
||||||
case "custom", "vllm", "ollama":
|
case "custom", "vllm", "ollama", "llmman":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeComparableProviderModelID(provider, modelID string) string {
|
|
||||||
target := strings.TrimSpace(modelID)
|
|
||||||
if target == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if !strings.Contains(target, "/") {
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
parts := strings.SplitN(target, "/", 2)
|
|
||||||
prefix := parts[0]
|
|
||||||
model := strings.TrimSpace(parts[1])
|
|
||||||
if model == "" {
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
for _, item := range supportedProviderModelPrefixes(provider) {
|
|
||||||
if item == prefix {
|
|
||||||
return model
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return target
|
|
||||||
}
|
|
||||||
|
|
||||||
func sameProviderModelID(provider, left, right string) bool {
|
func sameProviderModelID(provider, left, right string) bool {
|
||||||
leftTrimmed := strings.TrimSpace(left)
|
leftTrimmed := strings.TrimSpace(left)
|
||||||
rightTrimmed := strings.TrimSpace(right)
|
rightTrimmed := strings.TrimSpace(right)
|
||||||
if leftTrimmed == rightTrimmed {
|
if leftTrimmed == rightTrimmed {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
leftComparable := normalizeComparableProviderModelID(provider, leftTrimmed)
|
leftComparable := providercatalog.NormalizeModelID(provider, leftTrimmed)
|
||||||
rightComparable := normalizeComparableProviderModelID(provider, rightTrimmed)
|
rightComparable := providercatalog.NormalizeModelID(provider, rightTrimmed)
|
||||||
return leftComparable != "" && leftComparable == rightComparable
|
return leftComparable != "" && leftComparable == rightComparable
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1448,6 +1518,20 @@ func requireAgentAccountModelForProvider(provider string, models []dto.AgentAcco
|
|||||||
return selectedAccountModel, nil
|
return selectedAccountModel, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveAgentAccountVerifyModel(provider, requested string, models []dto.AgentAccountModel) (string, error) {
|
||||||
|
if len(models) == 0 {
|
||||||
|
return "", buserr.New("ErrAgentAccountModelsRequired")
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(requested) == "" {
|
||||||
|
return models[0].ID, nil
|
||||||
|
}
|
||||||
|
selected, ok := findAgentAccountModelForProvider(provider, models, requested)
|
||||||
|
if !ok {
|
||||||
|
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||||
|
}
|
||||||
|
return selected.ID, nil
|
||||||
|
}
|
||||||
|
|
||||||
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
|
func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.AgentAccountModel) error {
|
||||||
if account == nil || account.ID == 0 {
|
if account == nil || account.ID == 0 {
|
||||||
return nil
|
return nil
|
||||||
@@ -1524,6 +1608,122 @@ func readInstallEnv(envStr string) map[string]interface{} {
|
|||||||
return data
|
return data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
|
||||||
|
hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
|
||||||
|
qwenPawAuthEnabledEnvKey = "QWENPAW_AUTH_ENABLED"
|
||||||
|
qwenPawAuthUsernameEnvKey = "QWENPAW_AUTH_USERNAME"
|
||||||
|
qwenPawAuthPasswordEnvKey = "QWENPAW_AUTH_PASSWORD"
|
||||||
|
)
|
||||||
|
|
||||||
|
type agentDashboardAuth struct {
|
||||||
|
Username string
|
||||||
|
Password string
|
||||||
|
}
|
||||||
|
|
||||||
|
func normalizeAgentDashboardAuth(username, password string) agentDashboardAuth {
|
||||||
|
auth := agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(username),
|
||||||
|
Password: strings.TrimSpace(password),
|
||||||
|
}
|
||||||
|
if auth.Username == "" {
|
||||||
|
auth.Username = "admin"
|
||||||
|
}
|
||||||
|
if auth.Password == "" {
|
||||||
|
auth.Password = common.RandStr(8)
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
func agentDashboardAuthEnvKeys(agentType string) (string, string, bool) {
|
||||||
|
switch agentType {
|
||||||
|
case constant.AppHermesAgent:
|
||||||
|
return hermesDashboardUsernameEnvKey, hermesDashboardPasswordEnvKey, true
|
||||||
|
case constant.AppCopaw:
|
||||||
|
return qwenPawAuthUsernameEnvKey, qwenPawAuthPasswordEnvKey, true
|
||||||
|
default:
|
||||||
|
return "", "", false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAgentDashboardAuthEnv(envPath, agentType string, auth agentDashboardAuth, overwrite bool) error {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
values := map[string]string{
|
||||||
|
usernameKey: auth.Username,
|
||||||
|
passwordKey: auth.Password,
|
||||||
|
}
|
||||||
|
order := []string{usernameKey, passwordKey}
|
||||||
|
if agentType == constant.AppCopaw {
|
||||||
|
values[qwenPawAuthEnabledEnvKey] = "true"
|
||||||
|
order = append([]string{qwenPawAuthEnabledEnvKey}, order...)
|
||||||
|
}
|
||||||
|
return upsertAgentEnv(envPath, values, order, overwrite)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthEnv(envPath, agentType string) (agentDashboardAuth, error) {
|
||||||
|
usernameKey, passwordKey, ok := agentDashboardAuthEnvKeys(agentType)
|
||||||
|
if !ok {
|
||||||
|
return agentDashboardAuth{}, fmt.Errorf("dashboard auth is not supported for %s", agentType)
|
||||||
|
}
|
||||||
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}, err
|
||||||
|
}
|
||||||
|
return agentDashboardAuth{
|
||||||
|
Username: strings.TrimSpace(envMap[usernameKey]),
|
||||||
|
Password: strings.TrimSpace(envMap[passwordKey]),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentDashboardAuthFromInstall(appInstall *model.AppInstall, agentType string) agentDashboardAuth {
|
||||||
|
if appInstall == nil || appInstall.ID == 0 {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
auth, err := readAgentDashboardAuthEnv(appInstall.GetEnvPath(), agentType)
|
||||||
|
if err != nil {
|
||||||
|
return agentDashboardAuth{}
|
||||||
|
}
|
||||||
|
return auth
|
||||||
|
}
|
||||||
|
|
||||||
|
func readAgentEnvMap(envPath string) (map[string]string, error) {
|
||||||
|
fileOp := files.NewFileOp()
|
||||||
|
if !fileOp.Stat(envPath) {
|
||||||
|
return map[string]string{}, nil
|
||||||
|
}
|
||||||
|
envMap, err := godotenv.Read(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return envMap, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
|
||||||
|
if len(envMap) == 0 {
|
||||||
|
return files.NewFileOp().SaveFile(envPath, "", 0600)
|
||||||
|
}
|
||||||
|
return agentenv.WriteWithOrder(envMap, envPath, order)
|
||||||
|
}
|
||||||
|
|
||||||
|
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
|
||||||
|
envMap, err := readAgentEnvMap(envPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for key, value := range values {
|
||||||
|
if key == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if overwrite || strings.TrimSpace(envMap[key]) == "" {
|
||||||
|
envMap[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return writeAgentEnvMap(envPath, envMap, order)
|
||||||
|
}
|
||||||
|
|
||||||
func maskKey(value string) string {
|
func maskKey(value string) string {
|
||||||
trim := strings.TrimSpace(value)
|
trim := strings.TrimSpace(value)
|
||||||
if len(trim) <= 6 {
|
if len(trim) <= 6 {
|
||||||
@@ -1551,57 +1751,6 @@ func toInt(value interface{}) int {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeCustomModel(modelName string) string {
|
|
||||||
trim := strings.TrimSpace(modelName)
|
|
||||||
trim = strings.TrimLeft(trim, "/")
|
|
||||||
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 {
|
|
||||||
if strings.EqualFold(parts[0], "custom") {
|
|
||||||
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return trim
|
|
||||||
}
|
|
||||||
|
|
||||||
func runtimeProviderModelPrefix(provider string) string {
|
|
||||||
switch provider {
|
|
||||||
case "gemini":
|
|
||||||
return "google"
|
|
||||||
case "kimi":
|
|
||||||
return "moonshot"
|
|
||||||
default:
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func poolModelPrefix(provider string) string {
|
|
||||||
meta, ok := providercatalog.Get(provider)
|
|
||||||
if ok && len(meta.Models) > 0 {
|
|
||||||
parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2)
|
|
||||||
if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" {
|
|
||||||
return parts[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return provider
|
|
||||||
}
|
|
||||||
|
|
||||||
func supportedProviderModelPrefixes(provider string) []string {
|
|
||||||
values := []string{poolModelPrefix(provider), runtimeProviderModelPrefix(provider)}
|
|
||||||
result := make([]string, 0, len(values))
|
|
||||||
seen := make(map[string]struct{}, len(values))
|
|
||||||
for _, value := range values {
|
|
||||||
target := value
|
|
||||||
if target == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, ok := seen[target]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[target] = struct{}{}
|
|
||||||
result = append(result, target)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateToken() string {
|
func generateToken() string {
|
||||||
bytes := make([]byte, 24)
|
bytes := make([]byte, 24)
|
||||||
if _, err := rand.Read(bytes); err != nil {
|
if _, err := rand.Read(bytes); err != nil {
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]mo
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
|
func UniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
|
||||||
websiteMap := make(map[uint]model.Website)
|
websiteMap := make(map[uint]model.Website)
|
||||||
duplicateAppInstallIDs := make(map[uint]struct{})
|
duplicateAppInstallIDs := make(map[uint]struct{})
|
||||||
for _, website := range websites {
|
for _, website := range websites {
|
||||||
@@ -182,10 +182,6 @@ func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]m
|
|||||||
return websiteMap
|
return websiteMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
|
|
||||||
return uniqueDeploymentWebsiteMapByAppInstall(websites)
|
|
||||||
}
|
|
||||||
|
|
||||||
func uniqueUintList(items []uint) []uint {
|
func uniqueUintList(items []uint) []uint {
|
||||||
itemMap := make(map[uint]struct{}, len(items))
|
itemMap := make(map[uint]struct{}, len(items))
|
||||||
uniq := make([]uint, 0, len(items))
|
uniq := make([]uint, 0, len(items))
|
||||||
|
|||||||
@@ -0,0 +1,139 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
vllmAppKeyForUpgrade = "vllm"
|
||||||
|
vllmImageEnvKey = "IMAGE"
|
||||||
|
vllmImageTypeNvidia = "nvidia"
|
||||||
|
vllmImageTypeIntel = "intel"
|
||||||
|
vllmImageTypeAscend = "ascend"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resolveVllmVersionFamily(version, image string) string {
|
||||||
|
normalizedVersion := strings.ToLower(strings.TrimSpace(version))
|
||||||
|
if strings.HasPrefix(normalizedVersion, vllmImageTypeIntel+"-") {
|
||||||
|
return vllmImageTypeIntel
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalizedVersion, vllmImageTypeAscend+"-") {
|
||||||
|
return vllmImageTypeAscend
|
||||||
|
}
|
||||||
|
if strings.HasPrefix(normalizedVersion, vllmImageTypeNvidia+"-") {
|
||||||
|
return vllmImageTypeNvidia
|
||||||
|
}
|
||||||
|
normalizedImage := strings.ToLower(strings.TrimSpace(image))
|
||||||
|
if strings.Contains(normalizedImage, "intel/") || strings.Contains(normalizedImage, "llm-scaler-vllm") {
|
||||||
|
return vllmImageTypeIntel
|
||||||
|
}
|
||||||
|
if strings.Contains(normalizedImage, "ascend/") || strings.Contains(normalizedImage, "vllm-ascend") {
|
||||||
|
return vllmImageTypeAscend
|
||||||
|
}
|
||||||
|
return vllmImageTypeNvidia
|
||||||
|
}
|
||||||
|
|
||||||
|
func trimVllmVersionFamily(version string) string {
|
||||||
|
trimmed := strings.TrimSpace(version)
|
||||||
|
normalized := strings.ToLower(trimmed)
|
||||||
|
for _, family := range []string{vllmImageTypeNvidia, vllmImageTypeIntel, vllmImageTypeAscend} {
|
||||||
|
prefix := family + "-"
|
||||||
|
if strings.HasPrefix(normalized, prefix) {
|
||||||
|
return strings.TrimSpace(trimmed[len(prefix):])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return trimmed
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildDefaultVllmImageByVersion(version string) string {
|
||||||
|
tag := trimVllmVersionFamily(version)
|
||||||
|
family := resolveVllmVersionFamily(version, "")
|
||||||
|
if family == vllmImageTypeIntel {
|
||||||
|
return "intel/llm-scaler-vllm:" + tag
|
||||||
|
}
|
||||||
|
if tag != "" && !strings.HasPrefix(strings.ToLower(tag), "v") {
|
||||||
|
tag = "v" + tag
|
||||||
|
}
|
||||||
|
if family == vllmImageTypeAscend {
|
||||||
|
return "quay.io/ascend/vllm-ascend:" + tag
|
||||||
|
}
|
||||||
|
return "vllm/vllm-openai:" + tag
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage string) bool {
|
||||||
|
currentFamily := resolveVllmVersionFamily(currentVersion, currentImage)
|
||||||
|
targetFamily := resolveVllmVersionFamily(targetVersion, "")
|
||||||
|
return currentFamily == targetFamily
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasVllmVersionFamilyPrefix(version string) bool {
|
||||||
|
normalized := strings.ToLower(strings.TrimSpace(version))
|
||||||
|
return strings.HasPrefix(normalized, vllmImageTypeNvidia+"-") ||
|
||||||
|
strings.HasPrefix(normalized, vllmImageTypeIntel+"-") ||
|
||||||
|
strings.HasPrefix(normalized, vllmImageTypeAscend+"-")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isVllmUpgradeCandidate(currentVersion, targetVersion, currentImage string) bool {
|
||||||
|
if strings.TrimSpace(currentVersion) == strings.TrimSpace(targetVersion) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if !isVllmUpgradeVersionAllowed(currentVersion, targetVersion, currentImage) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if common.CompareVersion(targetVersion, currentVersion) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return !hasVllmVersionFamilyPrefix(currentVersion) &&
|
||||||
|
resolveVllmVersionFamily(targetVersion, "") == vllmImageTypeNvidia &&
|
||||||
|
trimVllmVersionFamily(currentVersion) == trimVllmVersionFamily(targetVersion)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildVllmUpgradeImage(currentImage, currentVersion, targetVersion string) string {
|
||||||
|
trimmedImage := strings.TrimSpace(currentImage)
|
||||||
|
if trimmedImage == "" || trimmedImage == buildDefaultVllmImageByVersion(currentVersion) {
|
||||||
|
return buildDefaultVllmImageByVersion(targetVersion)
|
||||||
|
}
|
||||||
|
return trimmedImage
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadVllmImageFromEnv(raw string) string {
|
||||||
|
envs := make(map[string]interface{})
|
||||||
|
if strings.TrimSpace(raw) == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(raw), &envs); err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
if image, ok := envs[vllmImageEnvKey].(string); ok {
|
||||||
|
return strings.TrimSpace(image)
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func setVllmImageInEnvContent(content []byte, image string) []byte {
|
||||||
|
normalizedImage := strings.TrimSpace(image)
|
||||||
|
if normalizedImage == "" {
|
||||||
|
return content
|
||||||
|
}
|
||||||
|
lines := strings.Split(string(content), "\n")
|
||||||
|
replaced := false
|
||||||
|
for index, line := range lines {
|
||||||
|
if strings.HasPrefix(line, vllmImageEnvKey+"=") {
|
||||||
|
lines[index] = vllmImageEnvKey + "=" + normalizedImage
|
||||||
|
replaced = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !replaced {
|
||||||
|
if len(lines) > 0 && lines[len(lines)-1] == "" {
|
||||||
|
lines[len(lines)-1] = vllmImageEnvKey + "=" + normalizedImage
|
||||||
|
lines = append(lines, "")
|
||||||
|
} else {
|
||||||
|
lines = append(lines, vllmImageEnvKey+"="+normalizedImage)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(strings.Join(lines, "\n"))
|
||||||
|
}
|
||||||
+430
-29
@@ -17,10 +17,14 @@ import (
|
|||||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||||
"github.com/1Panel-dev/1Panel/agent/global"
|
"github.com/1Panel-dev/1Panel/agent/global"
|
||||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||||
|
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
|
||||||
|
alertconfig "github.com/1Panel-dev/1Panel/agent/utils/alert_config"
|
||||||
|
alertwebhook "github.com/1Panel-dev/1Panel/agent/utils/alert_webhook"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
"github.com/1Panel-dev/1Panel/agent/utils/copier"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
"github.com/1Panel-dev/1Panel/agent/utils/email"
|
||||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||||
|
"github.com/1Panel-dev/1Panel/agent/utils/xpack/providers"
|
||||||
"github.com/shirou/gopsutil/v4/disk"
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -34,6 +38,28 @@ var communityAlertMethodTypeNames = map[string]string{
|
|||||||
constant.SMS: "SMS",
|
constant.SMS: "SMS",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var legacyAlertMethodTypeMap = map[string]string{
|
||||||
|
"mail": constant.Email,
|
||||||
|
constant.Email: constant.Email,
|
||||||
|
constant.SMS: constant.SMS,
|
||||||
|
constant.Bark: constant.Bark,
|
||||||
|
constant.WeChat: constant.WeCom,
|
||||||
|
constant.WeCom: constant.WeCom,
|
||||||
|
constant.DingTalk: constant.DingTalk,
|
||||||
|
constant.FeiShu: constant.FeiShu,
|
||||||
|
constant.Custom: constant.Custom,
|
||||||
|
}
|
||||||
|
|
||||||
|
var supportedAlertMethodTypes = map[string]struct{}{
|
||||||
|
constant.Email: {},
|
||||||
|
constant.SMS: {},
|
||||||
|
constant.Bark: {},
|
||||||
|
constant.WeCom: {},
|
||||||
|
constant.DingTalk: {},
|
||||||
|
constant.FeiShu: {},
|
||||||
|
constant.Custom: {},
|
||||||
|
}
|
||||||
|
|
||||||
type IAlertService interface {
|
type IAlertService interface {
|
||||||
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
|
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
|
||||||
GetAlerts() ([]dto.AlertDTO, error)
|
GetAlerts() ([]dto.AlertDTO, error)
|
||||||
@@ -53,8 +79,10 @@ type IAlertService interface {
|
|||||||
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
|
||||||
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
|
||||||
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
|
||||||
|
UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error
|
||||||
DeleteAlertConfig(id uint) error
|
DeleteAlertConfig(id uint) error
|
||||||
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
|
||||||
|
TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewIAlertService() IAlertService {
|
func NewIAlertService() IAlertService {
|
||||||
@@ -82,7 +110,38 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
return 0, nil, err
|
return 0, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cronjobProjects := make(map[string]uint)
|
||||||
|
var cronjobIDs []uint
|
||||||
for _, item := range alerts {
|
for _, item := range alerts {
|
||||||
|
if alertUtil.GetCronJobType(item.Type) != "cronJob" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := cronjobProjects[item.Project]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id, parseErr := strconv.ParseUint(item.Project, 10, strconv.IntSize)
|
||||||
|
if parseErr != nil || id == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
cronjobProjects[item.Project] = uint(id)
|
||||||
|
cronjobIDs = append(cronjobIDs, uint(id))
|
||||||
|
}
|
||||||
|
cronjobsByID := make(map[uint]model.Cronjob)
|
||||||
|
if len(cronjobIDs) > 0 {
|
||||||
|
cronjobs, err := cronjobRepo.List(repo.WithByIDs(cronjobIDs))
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
for _, cronjob := range cronjobs {
|
||||||
|
cronjobsByID[cronjob.ID] = cronjob
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, item := range alerts {
|
||||||
|
var taskName string
|
||||||
|
if cronjob, exists := cronjobsByID[cronjobProjects[item.Project]]; exists && cronjob.Type == item.Type {
|
||||||
|
taskName = cronjob.Name
|
||||||
|
}
|
||||||
|
|
||||||
result = append(result, dto.AlertDTO{
|
result = append(result, dto.AlertDTO{
|
||||||
ID: item.ID,
|
ID: item.ID,
|
||||||
@@ -92,6 +151,7 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
|
|||||||
Method: item.Method,
|
Method: item.Method,
|
||||||
Title: item.Title,
|
Title: item.Title,
|
||||||
Project: item.Project,
|
Project: item.Project,
|
||||||
|
TaskName: taskName,
|
||||||
Status: item.Status,
|
Status: item.Status,
|
||||||
SendCount: item.SendCount,
|
SendCount: item.SendCount,
|
||||||
AdvancedParams: item.AdvancedParams,
|
AdvancedParams: item.AdvancedParams,
|
||||||
@@ -163,6 +223,16 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
advanced, err := prepareCronJobAlertParams(create.Type, "", create.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
create.AdvancedParams = advanced
|
||||||
|
if create.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(create.Type, advanced, create.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
alertInfo.Status = constant.AlertEnable
|
alertInfo.Status = constant.AlertEnable
|
||||||
if err := copier.Copy(&alertInfo, &create); err != nil {
|
if err := copier.Copy(&alertInfo, &create); err != nil {
|
||||||
return buserr.WithErr("ErrStructTransform", err)
|
return buserr.WithErr("ErrStructTransform", err)
|
||||||
@@ -180,9 +250,28 @@ func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
|
||||||
if err := a.validateCommunityAlertMethod(req.Method); err != nil {
|
if alertUtil.GetCronJobType(req.Type) == "cronJob" {
|
||||||
|
previous, err := alertRepo.Get(repo.WithByID(req.ID))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.AdvancedParams, err = prepareCronJobAlertParams(req.Type, previous.AdvancedParams, req.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
methodTypes, err := a.validateAlertMethodReferences(req.Method)
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if req.Status != constant.AlertDisable {
|
||||||
|
if err := a.validateCronJobAlertChannels(req.Type, req.AdvancedParams, req.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
upMap := make(map[string]interface{})
|
upMap := make(map[string]interface{})
|
||||||
upMap["id"] = req.ID
|
upMap["id"] = req.ID
|
||||||
@@ -240,7 +329,19 @@ func (a AlertService) UpdateStatus(id uint, status string) error {
|
|||||||
if alertInfo.ID == 0 {
|
if alertInfo.ID == 0 {
|
||||||
return buserr.New("ErrRecordNotFound")
|
return buserr.New("ErrRecordNotFound")
|
||||||
}
|
}
|
||||||
err := alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
methodTypes, err := a.validateAlertMethodReferences(alertInfo.Method)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if status == constant.AlertEnable {
|
||||||
|
if err := a.validateCronJobAlertChannels(alertInfo.Type, alertInfo.AdvancedParams, alertInfo.Method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = alertRepo.Update(map[string]interface{}{"status": status}, repo.WithByID(alertInfo.ID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -384,6 +485,9 @@ func (a AlertService) PageAlertLogs(search dto.AlertLogSearch) (int64, []dto.Ale
|
|||||||
if search.Count != 0 {
|
if search.Count != 0 {
|
||||||
opts = append(opts, alertRepo.WithByCount(search.Count))
|
opts = append(opts, alertRepo.WithByCount(search.Count))
|
||||||
}
|
}
|
||||||
|
if !search.StartTime.IsZero() && !search.EndTime.IsZero() {
|
||||||
|
opts = append(opts, repo.WithByCreatedAt(search.StartTime, search.EndTime))
|
||||||
|
}
|
||||||
opts = append(opts, repo.WithOrderDesc("created_at"))
|
opts = append(opts, repo.WithOrderDesc("created_at"))
|
||||||
|
|
||||||
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
total, alerts, err := alertRepo.PageLog(search.Page, search.PageSize, opts...)
|
||||||
@@ -409,6 +513,7 @@ func (a AlertService) parseAlertLog(item model.AlertLog) (dto.AlertLogDTO, error
|
|||||||
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
|
if err := unmarshalAlertInfo(item.AlertDetail, &alertDetail); err != nil {
|
||||||
return dto.AlertLogDTO{}, err
|
return dto.AlertLogDTO{}, err
|
||||||
}
|
}
|
||||||
|
alertDetail.Task = nil
|
||||||
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
|
if err := unmarshalAlertInfo(item.AlertRule, &alertRule); err != nil {
|
||||||
return dto.AlertLogDTO{}, err
|
return dto.AlertLogDTO{}, err
|
||||||
}
|
}
|
||||||
@@ -491,7 +596,13 @@ func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertCon
|
|||||||
}
|
}
|
||||||
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
|
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
|
||||||
configs, err := alertRepo.AlertConfigList(opts...)
|
configs, err := alertRepo.AlertConfigList(opts...)
|
||||||
return configs, err
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
|
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
|
||||||
@@ -502,13 +613,49 @@ func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []mode
|
|||||||
if len(req.ExcludeTypes) > 0 {
|
if len(req.ExcludeTypes) > 0 {
|
||||||
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
|
||||||
}
|
}
|
||||||
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
total, configs, err := alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
if err := exposeCustomAlertConfigSecrets(configs); err != nil {
|
||||||
|
return 0, nil, err
|
||||||
|
}
|
||||||
|
return total, configs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||||
|
if req.Type == constant.Custom {
|
||||||
|
if req.ID != 0 && req.Revision == nil {
|
||||||
|
return repo.ErrAlertConfigRevisionRequired
|
||||||
|
}
|
||||||
|
return a.updateCustomAlertConfig(req, operator)
|
||||||
|
}
|
||||||
|
usesMutation, err := alertconfig.UsesMutation(req.Type, req.Config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.ID != 0 && usesMutation && req.Revision == nil {
|
||||||
|
return repo.ErrAlertConfigRevisionRequired
|
||||||
|
}
|
||||||
|
var existing *model.AlertConfig
|
||||||
|
if req.ID != 0 {
|
||||||
|
stored, err := alertRepo.GetConfigById(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if stored.Type != req.Type {
|
||||||
|
return fmt.Errorf("alert config %d has type %s, not %s", req.ID, stored.Type, req.Type)
|
||||||
|
}
|
||||||
|
existing = &stored
|
||||||
|
}
|
||||||
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
|
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
prepared, err := alertconfig.Prepare(req.Type, req.Config, req.Status, existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Config = prepared
|
||||||
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
|
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -523,7 +670,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
|||||||
upMap["status"] = req.Status
|
upMap["status"] = req.Status
|
||||||
upMap["config"] = req.Config
|
upMap["config"] = req.Config
|
||||||
upMap["update_user"] = operator
|
upMap["update_user"] = operator
|
||||||
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
|
if err := alertRepo.UpdateAlertConfigWithRevision(upMap, req.Revision, repo.WithByID(req.ID)); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -541,6 +688,99 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator stri
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a AlertService) updateCustomAlertConfig(req dto.AlertConfigUpdate, operator string) error {
|
||||||
|
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var existing *model.AlertConfig
|
||||||
|
if req.ID != 0 {
|
||||||
|
config, err := alertRepo.GetConfigById(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if config.Type != constant.Custom {
|
||||||
|
return fmt.Errorf("alert config %d is not a custom webhook", req.ID)
|
||||||
|
}
|
||||||
|
existing = &config
|
||||||
|
}
|
||||||
|
prepared, err := alertwebhook.Prepare(req.Config, req.Status, existing)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
validatedReq := req
|
||||||
|
validatedReq.Config = prepared.Config
|
||||||
|
if err := a.checkAlertConfigDisplayNameUnique(validatedReq); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existing != nil {
|
||||||
|
return alertRepo.UpdateAlertConfigWithRevision(map[string]interface{}{
|
||||||
|
"type": constant.Custom,
|
||||||
|
"title": req.Title,
|
||||||
|
"status": req.Status,
|
||||||
|
"config": prepared.Config,
|
||||||
|
"secret_config": prepared.SecretConfig,
|
||||||
|
"update_user": operator,
|
||||||
|
}, req.Revision, repo.WithByID(req.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
return alertRepo.CreateAlertConfig(&model.AlertConfig{
|
||||||
|
Type: constant.Custom,
|
||||||
|
Title: req.Title,
|
||||||
|
Status: req.Status,
|
||||||
|
Config: prepared.Config,
|
||||||
|
SecretConfig: prepared.SecretConfig,
|
||||||
|
CreateUser: operator,
|
||||||
|
UpdateUser: operator,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) UpdateAlertConfigStatus(req dto.AlertConfigStatusUpdate, operator string) error {
|
||||||
|
if err := validateAlertConfigStatus(req.Status); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
config, err := alertRepo.GetConfigById(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if req.Status == constant.AlertEnable {
|
||||||
|
if err := a.validateCommunityAlertConfigType(config.Type); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if config.Type == constant.Custom {
|
||||||
|
if _, err := alertwebhook.Resolve(config); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alertRepo.UpdateAlertConfig(map[string]interface{}{
|
||||||
|
"status": req.Status,
|
||||||
|
"update_user": operator,
|
||||||
|
}, repo.WithByID(req.ID))
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateAlertConfigStatus(status string) error {
|
||||||
|
if status != constant.AlertEnable && status != constant.AlertDisable {
|
||||||
|
return fmt.Errorf("alert config status must be Enable or Disable")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func exposeCustomAlertConfigSecrets(configs []model.AlertConfig) error {
|
||||||
|
for index := range configs {
|
||||||
|
if configs[index].Type != constant.Custom {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
view, err := alertwebhook.PlainView(configs[index])
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("build editable custom alert config %d: %w", configs[index].ID, err)
|
||||||
|
}
|
||||||
|
configs[index].Config = view
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
|
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
|
||||||
if req.Type != constant.SMSConfig {
|
if req.Type != constant.SMSConfig {
|
||||||
return nil
|
return nil
|
||||||
@@ -565,6 +805,9 @@ func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
|
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
|
||||||
|
if req.Type != constant.Custom && (global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
displayName := alertConfigDisplayName(req.Type, req.Config)
|
displayName := alertConfigDisplayName(req.Type, req.Config)
|
||||||
if displayName == "" {
|
if displayName == "" {
|
||||||
return nil
|
return nil
|
||||||
@@ -588,37 +831,67 @@ func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdat
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) validateCommunityAlertMethod(method string) error {
|
func (a AlertService) validateCommunityAlertMethod(method string) error {
|
||||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
methodTypes, err := a.validateAlertMethodReferences(method)
|
||||||
return nil
|
if err != nil {
|
||||||
}
|
return err
|
||||||
if strings.TrimSpace(method) == "" {
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
return a.validateAlertMethodEntitlement(methodTypes)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) validateAlertMethodReferences(method string) ([]string, error) {
|
||||||
|
if strings.TrimSpace(method) == "" {
|
||||||
|
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||||
|
}
|
||||||
|
methodTypes := make([]string, 0)
|
||||||
for _, item := range strings.Split(method, ",") {
|
for _, item := range strings.Split(method, ",") {
|
||||||
item = strings.TrimSpace(item)
|
item = strings.TrimSpace(item)
|
||||||
if item == "" {
|
if item == "" {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
configType := ""
|
||||||
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
|
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
|
||||||
config, err := alertRepo.GetConfigById(uint(configID))
|
config, err := alertRepo.GetConfigById(uint(configID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, err
|
||||||
}
|
}
|
||||||
if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
|
configType = config.Type
|
||||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
} else {
|
||||||
|
var ok bool
|
||||||
|
configType, ok = legacyAlertMethodTypeMap[item]
|
||||||
|
if !ok {
|
||||||
|
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
if _, ok := supportedAlertMethodTypes[configType]; !ok {
|
||||||
|
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||||
|
}
|
||||||
|
methodTypes = append(methodTypes, configType)
|
||||||
|
}
|
||||||
|
if len(methodTypes) == 0 {
|
||||||
|
return nil, buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||||
|
}
|
||||||
|
return methodTypes, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) validateAlertMethodEntitlement(methodTypes []string) error {
|
||||||
|
for _, configType := range methodTypes {
|
||||||
|
if configType == constant.Custom {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if _, ok := communityAlertMethodTypeNames[item]; ok {
|
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, ok := communityAlertMethodTypeNames[configType]; ok {
|
||||||
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
|
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
|
||||||
|
if configType == constant.Custom {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -630,7 +903,7 @@ func (a AlertService) validateCommunityAlertConfigType(configType string) error
|
|||||||
|
|
||||||
func alertConfigDisplayName(configType, configData string) string {
|
func alertConfigDisplayName(configType, configData string) string {
|
||||||
switch configType {
|
switch configType {
|
||||||
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
|
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS, constant.Custom:
|
||||||
var cfg struct {
|
var cfg struct {
|
||||||
DisplayName string `json:"displayName"`
|
DisplayName string `json:"displayName"`
|
||||||
}
|
}
|
||||||
@@ -669,20 +942,24 @@ func (a AlertService) DeleteAlertConfig(id uint) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
||||||
username := req.UserName
|
emailConfig, err := resolveEmailTestConfig(req)
|
||||||
if username == "" {
|
if err != nil {
|
||||||
username = req.Sender
|
return false, err
|
||||||
}
|
}
|
||||||
encodedDisplayName := mime.BEncoding.Encode("UTF-8", req.DisplayName)
|
username := emailConfig.UserName
|
||||||
|
if username == "" {
|
||||||
|
username = emailConfig.Sender
|
||||||
|
}
|
||||||
|
encodedDisplayName := mime.BEncoding.Encode("UTF-8", emailConfig.DisplayName)
|
||||||
cfg := email.SMTPConfig{
|
cfg := email.SMTPConfig{
|
||||||
Host: req.Host,
|
Host: emailConfig.Host,
|
||||||
Port: req.Port,
|
Port: emailConfig.Port,
|
||||||
Sender: req.Sender,
|
Sender: emailConfig.Sender,
|
||||||
Username: username,
|
Username: username,
|
||||||
Password: req.Password,
|
Password: emailConfig.Password,
|
||||||
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, req.Sender),
|
From: fmt.Sprintf(`"%s" <%s>`, encodedDisplayName, emailConfig.Sender),
|
||||||
Encryption: req.Encryption,
|
Encryption: emailConfig.Encryption,
|
||||||
Recipient: req.Recipient,
|
Recipient: emailConfig.Recipient,
|
||||||
}
|
}
|
||||||
|
|
||||||
msg := email.EmailMessage{
|
msg := email.EmailMessage{
|
||||||
@@ -697,9 +974,94 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
|
|||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func resolveEmailTestConfig(req dto.AlertConfigTest) (dto.AlertEmailConfig, error) {
|
||||||
|
emailConfig := dto.AlertEmailConfig{
|
||||||
|
Host: req.Host,
|
||||||
|
Port: req.Port,
|
||||||
|
Sender: req.Sender,
|
||||||
|
UserName: req.UserName,
|
||||||
|
Password: req.Password,
|
||||||
|
DisplayName: req.DisplayName,
|
||||||
|
Encryption: req.Encryption,
|
||||||
|
Recipient: req.Recipient,
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(req.Config) != "" {
|
||||||
|
configType := req.Type
|
||||||
|
if configType == "" {
|
||||||
|
configType = constant.EmailConfig
|
||||||
|
}
|
||||||
|
if configType != constant.EmailConfig {
|
||||||
|
return dto.AlertEmailConfig{}, fmt.Errorf("alert config test type must be email")
|
||||||
|
}
|
||||||
|
var existing *model.AlertConfig
|
||||||
|
if req.ID != 0 {
|
||||||
|
stored, err := alertRepo.GetConfigById(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return dto.AlertEmailConfig{}, err
|
||||||
|
}
|
||||||
|
existing = &stored
|
||||||
|
}
|
||||||
|
prepared, err := alertconfig.Prepare(configType, req.Config, constant.AlertEnable, existing)
|
||||||
|
if err != nil {
|
||||||
|
return dto.AlertEmailConfig{}, err
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal([]byte(prepared), &emailConfig); err != nil {
|
||||||
|
return dto.AlertEmailConfig{}, fmt.Errorf("decode email alert config: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return emailConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) TestCustomAlertConfig(req dto.AlertConfigTest) (dto.AlertConfigTestResult, error) {
|
||||||
|
if req.Type != constant.Custom {
|
||||||
|
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config test type must be custom")
|
||||||
|
}
|
||||||
|
var existing *model.AlertConfig
|
||||||
|
if req.ID != 0 {
|
||||||
|
config, err := alertRepo.GetConfigById(req.ID)
|
||||||
|
if err != nil {
|
||||||
|
return dto.AlertConfigTestResult{}, err
|
||||||
|
}
|
||||||
|
if config.Type != constant.Custom {
|
||||||
|
return dto.AlertConfigTestResult{}, fmt.Errorf("alert config %d is not a custom webhook", req.ID)
|
||||||
|
}
|
||||||
|
existing = &config
|
||||||
|
}
|
||||||
|
prepared, err := alertwebhook.Prepare(req.Config, constant.AlertEnable, existing)
|
||||||
|
if err != nil {
|
||||||
|
return dto.AlertConfigTestResult{}, err
|
||||||
|
}
|
||||||
|
resolved, err := alertwebhook.Resolve(model.AlertConfig{
|
||||||
|
Type: constant.Custom,
|
||||||
|
Config: prepared.Config,
|
||||||
|
SecretConfig: prepared.SecretConfig,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return dto.AlertConfigTestResult{}, err
|
||||||
|
}
|
||||||
|
tester, ok := xpack.AlertProvider.(providers.CustomWebhookTester)
|
||||||
|
if !ok {
|
||||||
|
return dto.AlertConfigTestResult{
|
||||||
|
Success: false,
|
||||||
|
Message: providers.ErrCustomWebhookUnsupported.Error(),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
return tester.TestCustomWebhook(resolved)
|
||||||
|
}
|
||||||
|
|
||||||
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
|
||||||
if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
|
var methodTypes []string
|
||||||
return err
|
if updateAlert.SendCount != 0 || strings.TrimSpace(updateAlert.Method) != "" {
|
||||||
|
var err error
|
||||||
|
methodTypes, err = a.validateAlertMethodReferences(updateAlert.Method)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if updateAlert.SendCount != 0 {
|
||||||
|
if err := a.validateAlertMethodEntitlement(methodTypes); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
upMap := make(map[string]interface{})
|
upMap := make(map[string]interface{})
|
||||||
var newStatus string
|
var newStatus string
|
||||||
@@ -718,6 +1080,23 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
alertRepo.WithByType(updateAlert.Type),
|
alertRepo.WithByType(updateAlert.Type),
|
||||||
alertRepo.WithByProject(updateAlert.Project),
|
alertRepo.WithByProject(updateAlert.Project),
|
||||||
)
|
)
|
||||||
|
advanced, err := prepareCronJobAlertParams(updateAlert.Type, alertInfo.AdvancedParams, updateAlert.AdvancedParams)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
updateAlert.AdvancedParams = advanced
|
||||||
|
if alertUtil.GetCronJobType(updateAlert.Type) == "cronJob" {
|
||||||
|
upMap["advanced_params"] = advanced
|
||||||
|
}
|
||||||
|
if newStatus == constant.AlertEnable {
|
||||||
|
method := updateAlert.Method
|
||||||
|
if method == "" {
|
||||||
|
method = alertInfo.Method
|
||||||
|
}
|
||||||
|
if err := a.validateCronJobAlertChannels(updateAlert.Type, advanced, method); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if alertInfo.ID > 0 {
|
if alertInfo.ID > 0 {
|
||||||
shouldUpdate := false
|
shouldUpdate := false
|
||||||
@@ -731,6 +1110,9 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
if val, ok := upMap["method"]; ok && val != "" && val != alertInfo.Method {
|
||||||
shouldUpdate = true
|
shouldUpdate = true
|
||||||
}
|
}
|
||||||
|
if val, ok := upMap["advanced_params"]; ok && val != alertInfo.AdvancedParams {
|
||||||
|
shouldUpdate = true
|
||||||
|
}
|
||||||
|
|
||||||
if shouldUpdate {
|
if shouldUpdate {
|
||||||
if err := alertRepo.Update(
|
if err := alertRepo.Update(
|
||||||
@@ -752,3 +1134,22 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator
|
|||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func prepareCronJobAlertParams(alertType, previous, incoming string) (string, error) {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return incoming, nil
|
||||||
|
}
|
||||||
|
return alertUtil.MergeCronJobAlertParams(previous, incoming)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a AlertService) validateCronJobAlertChannels(alertType, advanced, method string) error {
|
||||||
|
if alertUtil.GetCronJobType(alertType) != "cronJob" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
mode, err := alertUtil.CronJobAlertTriggerMode(advanced)
|
||||||
|
if err != nil || mode != alertUtil.CronJobAlertSuccess {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
_, err = a.validateAlertMethodReferences(method)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math"
|
"math"
|
||||||
"net"
|
"net"
|
||||||
@@ -29,9 +30,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ResourceAlertInterval = 30
|
ResourceAlertInterval = 30
|
||||||
CheckIntervalSec = 3
|
CheckIntervalSec = 3
|
||||||
LoadCheckIntervalMin = 5
|
LoadCheckIntervalMin = 5
|
||||||
|
sshIPLoginWindow = 30 * time.Minute
|
||||||
|
sslAutoRenewAlertSkipDays = 31
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertTaskHelper struct {
|
type AlertTaskHelper struct {
|
||||||
@@ -512,10 +515,32 @@ func loadPanelLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadSSHLogin(alert dto.AlertDTO) {
|
func loadSSHLogin(alert dto.AlertDTO) {
|
||||||
count, isAlert, err := alertUtil.CountRecentFailedSSHLog(alert.Cycle, alert.Count)
|
now := time.Now()
|
||||||
if err != nil {
|
failedWindow := time.Duration(alert.Cycle) * time.Minute
|
||||||
global.LOG.Errorf("Failed to count recent failed ssh login logs: %v", err)
|
loadWindow := failedWindow
|
||||||
|
if loadWindow < sshIPLoginWindow {
|
||||||
|
loadWindow = sshIPLoginWindow
|
||||||
}
|
}
|
||||||
|
location, err := time.LoadLocation(common.LoadTimeZoneByCmd())
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Failed to load timezone for ssh login logs: %v", err)
|
||||||
|
location = time.Local
|
||||||
|
}
|
||||||
|
histories, err := loadSSHAlertHistories(defaultSSHLogDir, now.Add(-loadWindow), now, location)
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Errorf("Failed to load ssh login logs: %v", err)
|
||||||
|
}
|
||||||
|
interfaceAddrs, err := net.InterfaceAddrs()
|
||||||
|
if err != nil {
|
||||||
|
global.LOG.Warnf("Failed to load local IP addresses for ssh login alert: %v", err)
|
||||||
|
}
|
||||||
|
count, records := summarizeSSHLoginHistories(
|
||||||
|
histories,
|
||||||
|
now,
|
||||||
|
failedWindow,
|
||||||
|
sshSuccessLoginWhitelist(alert.AdvancedParams, interfaceAddrs),
|
||||||
|
)
|
||||||
|
isAlert := count >= int(alert.Count)
|
||||||
if isAlert {
|
if isAlert {
|
||||||
params := []dto.Param{
|
params := []dto.Param{
|
||||||
{
|
{
|
||||||
@@ -531,12 +556,6 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
|
sendAlerts(alert, "sshLogin", strconv.Itoa(count), "sshLogin", params)
|
||||||
}
|
}
|
||||||
whitelist := strings.Split(strings.TrimSpace(alert.AdvancedParams), "\n")
|
|
||||||
records, err := alertUtil.FindRecentSuccessLoginNotInWhitelist(30, whitelist)
|
|
||||||
if err != nil {
|
|
||||||
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
|
|
||||||
}
|
|
||||||
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
|
|
||||||
if len(records) > 0 {
|
if len(records) > 0 {
|
||||||
quota := strings.Join(records, "\n")
|
quota := strings.Join(records, "\n")
|
||||||
params := []dto.Param{
|
params := []dto.Param{
|
||||||
@@ -555,6 +574,19 @@ func loadSSHLogin(alert dto.AlertDTO) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func sshSuccessLoginWhitelist(configured string, interfaceAddrs []net.Addr) []string {
|
||||||
|
whitelist := strings.Split(strings.TrimSpace(configured), "\n")
|
||||||
|
whitelist = append(whitelist, "127.0.0.0/8", "::1")
|
||||||
|
for _, addr := range interfaceAddrs {
|
||||||
|
ipNet, ok := addr.(*net.IPNet)
|
||||||
|
if !ok || ipNet.IP == nil || ipNet.IP.IsUnspecified() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
whitelist = append(whitelist, ipNet.IP.String())
|
||||||
|
}
|
||||||
|
return whitelist
|
||||||
|
}
|
||||||
|
|
||||||
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
|
||||||
filtered := make([]model.LoginLog, 0, len(records))
|
filtered := make([]model.LoginLog, 0, len(records))
|
||||||
for _, record := range records {
|
for _, record := range records {
|
||||||
@@ -565,20 +597,6 @@ func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string)
|
|||||||
return filtered
|
return filtered
|
||||||
}
|
}
|
||||||
|
|
||||||
func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string {
|
|
||||||
filtered := make([]string, 0, len(records))
|
|
||||||
for _, record := range records {
|
|
||||||
ip := record
|
|
||||||
if idx := strings.Index(record, "-"); idx >= 0 {
|
|
||||||
ip = record[:idx]
|
|
||||||
}
|
|
||||||
if !isIPInWhitelist(ip, whitelist) {
|
|
||||||
filtered = append(filtered, record)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
func isIPInWhitelist(ip string, whitelist []string) bool {
|
func isIPInWhitelist(ip string, whitelist []string) bool {
|
||||||
targetIP := net.ParseIP(strings.TrimSpace(ip))
|
targetIP := net.ParseIP(strings.TrimSpace(ip))
|
||||||
if targetIP == nil {
|
if targetIP == nil {
|
||||||
@@ -695,9 +713,10 @@ func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string
|
|||||||
|
|
||||||
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
|
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
|
||||||
typeMap := map[string]string{
|
typeMap := map[string]string{
|
||||||
"mail": constant.Email,
|
"mail": constant.Email,
|
||||||
constant.Bark: constant.Bark,
|
constant.Bark: constant.Bark,
|
||||||
constant.SMS: constant.SMS,
|
constant.SMS: constant.SMS,
|
||||||
|
constant.Custom: constant.Custom,
|
||||||
}
|
}
|
||||||
configType, ok := typeMap[method]
|
configType, ok := typeMap[method]
|
||||||
if !ok {
|
if !ok {
|
||||||
@@ -705,7 +724,6 @@ func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType st
|
|||||||
}
|
}
|
||||||
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
|
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
doSendAlert(alert, alertType, quota, quotaType, params, config)
|
doSendAlert(alert, alertType, quota, quotaType, params, config)
|
||||||
@@ -786,7 +804,7 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
|||||||
}
|
}
|
||||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||||
|
|
||||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||||
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
|
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
|
||||||
if !isValid {
|
if !isValid {
|
||||||
return
|
return
|
||||||
@@ -799,12 +817,31 @@ func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params
|
|||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||||
alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
|
queued := false
|
||||||
|
var alertErr error
|
||||||
|
if config.Type == constant.Custom {
|
||||||
|
task := dto.AlertTaskMetadata{
|
||||||
|
AlertID: alert.ID,
|
||||||
|
Type: alertType,
|
||||||
|
Quota: quota,
|
||||||
|
QuotaType: quotaType,
|
||||||
|
Method: methodStr,
|
||||||
|
}
|
||||||
|
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo, task)
|
||||||
|
queued, alertErr = result.Queued, deliveryErr
|
||||||
|
if alertErr == nil && result.Queued {
|
||||||
|
_, alertErr = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
alertErr = xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
|
||||||
|
}
|
||||||
if alertErr != nil {
|
if alertErr != nil {
|
||||||
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
|
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
if !queued {
|
||||||
|
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -845,7 +882,7 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
|||||||
daysDiff := int(math.Ceil(
|
daysDiff := int(math.Ceil(
|
||||||
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
ssl.ExpireDate.Sub(currentDate).Hours() / 24,
|
||||||
))
|
))
|
||||||
if daysDiff > 0 && int(cycle) >= daysDiff {
|
if daysDiff > 0 && int(cycle) >= daysDiff && !shouldSuppressSSLExpiryAlert(ssl, daysDiff) {
|
||||||
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
daysDiffMap[daysDiff] = append(daysDiffMap[daysDiff], ssl.PrimaryDomain)
|
||||||
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
projectMap[ssl.ID] = append(projectMap[ssl.ID], ssl.ExpireDate)
|
||||||
}
|
}
|
||||||
@@ -853,6 +890,10 @@ func calculateSSLExpiryDays(sslList []model.WebsiteSSL, cycle uint) (map[int][]s
|
|||||||
return daysDiffMap, projectMap
|
return daysDiffMap, projectMap
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func shouldSuppressSSLExpiryAlert(ssl model.WebsiteSSL, remainingDays int) bool {
|
||||||
|
return ssl.AutoRenew && remainingDays < sslAutoRenewAlertSkipDays
|
||||||
|
}
|
||||||
|
|
||||||
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
func calculateWebsiteExpiryDays(websites []model.Website, cycle uint) (map[int][]string, map[uint][]time.Time) {
|
||||||
currentDate := time.Now()
|
currentDate := time.Now()
|
||||||
daysDiffMap := make(map[int][]string)
|
daysDiffMap := make(map[int][]string)
|
||||||
@@ -1098,3 +1139,55 @@ func calculateMinutesDifference(newDate time.Time) int {
|
|||||||
minutesDifference := int(now.Sub(newDate).Minutes())
|
minutesDifference := int(now.Sub(newDate).Minutes())
|
||||||
return minutesDifference
|
return minutesDifference
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func loadSSHAlertHistories(
|
||||||
|
baseDir string,
|
||||||
|
startTime, endTime time.Time,
|
||||||
|
location *time.Location,
|
||||||
|
) ([]dto.SSHHistory, error) {
|
||||||
|
fileList, err := listSSHLogFiles(baseDir)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
histories []dto.SSHHistory
|
||||||
|
loadErr error
|
||||||
|
)
|
||||||
|
for _, file := range fileList {
|
||||||
|
items, err := loadSSHHistoriesFromFile(file.Name, "", "", startTime, endTime, file.Year, location)
|
||||||
|
if err != nil {
|
||||||
|
loadErr = errors.Join(loadErr, fmt.Errorf("load SSH log file %s: %w", file.Name, err))
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
histories = append(histories, items...)
|
||||||
|
}
|
||||||
|
return histories, loadErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func summarizeSSHLoginHistories(
|
||||||
|
histories []dto.SSHHistory,
|
||||||
|
now time.Time,
|
||||||
|
failedWindow time.Duration,
|
||||||
|
whitelist []string,
|
||||||
|
) (int, []string) {
|
||||||
|
failedStartTime := now.Add(-failedWindow)
|
||||||
|
successStartTime := now.Add(-sshIPLoginWindow)
|
||||||
|
failedCount := 0
|
||||||
|
var abnormalLogins []string
|
||||||
|
|
||||||
|
for _, item := range histories {
|
||||||
|
switch item.Status {
|
||||||
|
case constant.StatusFailed:
|
||||||
|
if isSSHLogWithinTimeRange(item.Date, failedStartTime, now) {
|
||||||
|
failedCount++
|
||||||
|
}
|
||||||
|
case constant.StatusSuccess:
|
||||||
|
if !isSSHLogWithinTimeRange(item.Date, successStartTime, now) || isIPInWhitelist(item.Address, whitelist) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
abnormalLogins = append(abnormalLogins, fmt.Sprintf("%s-%s", item.Address, item.Date.Format(constant.DateTimeLayout)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return failedCount, abnormalLogins
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
|
|||||||
} else {
|
} else {
|
||||||
s.sendBarkWithConfig(config, quota, params)
|
s.sendBarkWithConfig(config, quota, params)
|
||||||
}
|
}
|
||||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
case constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Custom:
|
||||||
if isResource {
|
if isResource {
|
||||||
s.sendResourceWebhookWithConfig(config, quota, params)
|
s.sendResourceWebhookWithConfig(config, quota, params)
|
||||||
} else {
|
} else {
|
||||||
@@ -86,14 +86,13 @@ func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, param
|
|||||||
|
|
||||||
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
|
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
|
||||||
alertRepo := repo.NewIAlertRepo()
|
alertRepo := repo.NewIAlertRepo()
|
||||||
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
|
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS, constant.Custom: constant.Custom}
|
||||||
configType := method
|
configType := method
|
||||||
if mapped, ok := typeMap[method]; ok {
|
if mapped, ok := typeMap[method]; ok {
|
||||||
configType = mapped
|
configType = mapped
|
||||||
}
|
}
|
||||||
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
|
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if !alertUtil.IsAlertConfigEnabled(config) {
|
if !alertUtil.IsAlertConfigEnabled(config) {
|
||||||
@@ -309,12 +308,31 @@ func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota stri
|
|||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||||
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
queued := false
|
||||||
|
var err error
|
||||||
|
if config.Type == constant.Custom {
|
||||||
|
task := dto.AlertTaskMetadata{
|
||||||
|
AlertID: s.alert.ID,
|
||||||
|
Type: s.alert.Type,
|
||||||
|
Quota: quota,
|
||||||
|
QuotaType: s.quotaType,
|
||||||
|
Method: strconv.Itoa(int(config.ID)),
|
||||||
|
}
|
||||||
|
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
|
||||||
|
queued, err = result.Queued, deliveryErr
|
||||||
|
if err == nil && result.Queued {
|
||||||
|
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
if !queued {
|
||||||
|
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
|
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
|
||||||
@@ -335,11 +353,31 @@ func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, qu
|
|||||||
}
|
}
|
||||||
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
transport := xpack.MultiNodeProvider.LoadRequestTransport()
|
||||||
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
|
||||||
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
|
queued := false
|
||||||
|
var err error
|
||||||
|
if config.Type == constant.Custom {
|
||||||
|
task := dto.AlertTaskMetadata{
|
||||||
|
AlertID: s.alert.ID,
|
||||||
|
Type: s.alert.Type,
|
||||||
|
Quota: quota,
|
||||||
|
QuotaType: s.quotaType,
|
||||||
|
Method: strconv.Itoa(int(config.ID)),
|
||||||
|
}
|
||||||
|
result, deliveryErr := xpack.DeliverCustomWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo, task)
|
||||||
|
queued, err = result.Queued, deliveryErr
|
||||||
|
if err == nil && result.Queued {
|
||||||
|
_, err = alertUtil.RecordQueuedAlertTask(result.LogID, task)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
if !queued {
|
||||||
|
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
|
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user