mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-10-09 16:00:29 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7159aca226 | ||
|
|
2c5728e27e | ||
|
|
defb40702c | ||
|
|
b476df3b61 | ||
|
|
4bbd9b6f06 | ||
|
|
325b9c4d88 | ||
|
|
76d965b3b7 | ||
|
|
4b1b27abd8 | ||
|
|
f26b4f290d | ||
|
|
5d2221203a | ||
|
|
0aff0d529e | ||
|
|
20f09b3a0c | ||
|
|
c392b72470 | ||
|
|
50a70ef1e5 | ||
|
|
75266ef659 | ||
|
|
b3c593b852 | ||
|
|
73530bb8b9 | ||
|
|
1b6e70964b | ||
|
|
61d42b05e9 | ||
|
|
79703e3c71 | ||
|
|
aee4e4e282 | ||
|
|
744c7559f3 | ||
|
|
d94c3ec9cc | ||
|
|
f2625eab16 | ||
|
|
c6d498a9be | ||
|
|
b06bc0a455 | ||
|
|
68411bddd9 | ||
|
|
a2ac2e0b22 | ||
|
|
a5a5d9f1ac | ||
|
|
82876eac8d | ||
|
|
19c9c0f4e5 | ||
|
|
70f044dc50 | ||
|
|
1c8418026c | ||
|
|
17f8471c14 | ||
|
|
4deb2ea484 | ||
|
|
d05317dbf5 | ||
|
|
1db6ccd72b | ||
|
|
bc00760404 | ||
|
|
e57dc1240b | ||
|
|
4c396d68ca | ||
|
|
ab0fe4286a | ||
|
|
5ab758d626 | ||
|
|
3a0a6537f0 | ||
|
|
4eda92f25c | ||
|
|
94f8c5d2f1 | ||
|
|
92598effc1 | ||
|
|
748cecaee8 | ||
|
|
b783034d0a | ||
|
|
94a4e39793 | ||
|
|
1864dee591 | ||
|
|
8c4ea85d02 | ||
|
|
a714d5a84e | ||
|
|
4f97a7890f | ||
|
|
f15f91e5fa | ||
|
|
02bd500917 | ||
|
|
810c6fd232 | ||
|
|
d50a8983bc | ||
|
|
89647304bb | ||
|
|
9ebad13bf8 | ||
|
|
cc0ac8cc43 | ||
|
|
9f5a1d842a | ||
|
|
beb33957be | ||
|
|
ffcbbb705b | ||
|
|
058bc71630 | ||
|
|
0a38979bd9 | ||
|
|
5fa4a15914 | ||
|
|
81c551a11b | ||
|
|
5681449353 | ||
|
|
88849e6355 | ||
|
|
d6bbcdc32f | ||
|
|
abd1581c1d | ||
|
|
cd30ec7632 | ||
|
|
646826769a | ||
|
|
074c0abb3b | ||
|
|
3a00e893bc | ||
|
|
2f37677650 | ||
|
|
12ae89923b | ||
|
|
031a203a74 | ||
|
|
646b99974a | ||
|
|
254ed954e9 | ||
|
|
a0050dca09 | ||
|
|
cd80b4e6c3 | ||
|
|
df5d3d782c | ||
|
|
3c5b3c3467 | ||
|
|
2bdba42d87 | ||
|
|
0b146f7ff3 | ||
|
|
c28a4f6d69 | ||
|
|
f93a34acc4 | ||
|
|
4e78841bf6 | ||
|
|
7f5bb58c72 | ||
|
|
6a28359ac2 | ||
|
|
59655695b5 | ||
|
|
786c5c1d7c | ||
|
|
08388124b3 | ||
|
|
34ccc68b3a | ||
|
|
97ef386f17 | ||
|
|
394339d7dc | ||
|
|
8362578355 | ||
|
|
6dc218cf80 | ||
|
|
89888cef73 | ||
|
|
4d06f444ac | ||
|
|
6ea07421ac | ||
|
|
326d52e311 | ||
|
|
6e3f751dba | ||
|
|
6982fd22d4 | ||
|
|
04fd456b63 | ||
|
|
bea4bfeee8 | ||
|
|
fa6157b610 | ||
|
|
30af26779c | ||
|
|
16d5c00b9a | ||
|
|
3f14f8f30e | ||
|
|
218f92a960 | ||
|
|
75be8d3a23 | ||
|
|
e2d8125b3d | ||
|
|
7ec3c517c2 | ||
|
|
e186e30d8c | ||
|
|
67a8ba2421 | ||
|
|
24fc15beaf | ||
|
|
c1fe70b53e | ||
|
|
30bf4a8d46 | ||
|
|
9e501ce02b | ||
|
|
d74692a68b | ||
|
|
57fd80fb3d | ||
|
|
39d2c34682 | ||
|
|
3e4a48c892 | ||
|
|
2c8f8f32fd | ||
|
|
3cf6d9ac51 | ||
|
|
64d8fe8bd0 | ||
|
|
8129260508 | ||
|
|
a40786a7fa | ||
|
|
368193a777 | ||
|
|
61ef725892 | ||
|
|
0c49c41085 | ||
|
|
2a498de96e | ||
|
|
d055cbb053 | ||
|
|
4b3f8a9fd0 | ||
|
|
4c26945910 | ||
|
|
55b6104b56 | ||
|
|
12dc8aae81 | ||
|
|
37e2b93693 | ||
|
|
62daf1e019 | ||
|
|
f5e75d4bfa | ||
|
|
32fed45c1e | ||
|
|
8b7597abef | ||
|
|
01fb9a461b | ||
|
|
bc022a4d21 | ||
|
|
973cab2837 | ||
|
|
5ce155eee6 | ||
|
|
d83b49cc70 | ||
|
|
ec981f8153 | ||
|
|
b2853277e7 | ||
|
|
f573a0fa0e | ||
|
|
f787734feb | ||
|
|
52778f520a | ||
|
|
b5aba18248 | ||
|
|
bc9882da50 | ||
|
|
0ba7e98e8d | ||
|
|
e6dc44d135 | ||
|
|
a9f30e9f5f | ||
|
|
f3bf9d0905 | ||
|
|
05ea384a00 | ||
|
|
62c50aafe1 | ||
|
|
6e052b8ef2 | ||
|
|
1d40bdc11b | ||
|
|
3b62197f29 |
@@ -0,0 +1,14 @@
|
||||
version: 2
|
||||
|
||||
updates:
|
||||
- package-ecosystem: "gomod"
|
||||
directories:
|
||||
- "/agent"
|
||||
- "/core"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
|
||||
- package-ecosystem: "npm"
|
||||
directory: "/frontend"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
@@ -6,7 +6,7 @@ permissions:
|
||||
jobs:
|
||||
generic_handler:
|
||||
name: Add Labels to PR
|
||||
if: github.repository == '1Panel-dev/1Panel'
|
||||
if: github.repository == '1Panel-dev/1Panel' && github.event.pull_request.user.login != 'dependabot[bot]'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
|
||||
@@ -15,7 +15,7 @@ jobs:
|
||||
- name: Setup Node
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '22.19.0'
|
||||
node-version: '24.11.1'
|
||||
|
||||
- name: Build Web
|
||||
run: |
|
||||
@@ -26,7 +26,7 @@ jobs:
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.24'
|
||||
go-version: '1.25'
|
||||
cache-dependency-path: |
|
||||
core/go.sum
|
||||
agent/go.sum
|
||||
|
||||
+2
-12
@@ -9,7 +9,6 @@ build/1panel-core
|
||||
|
||||
# Mac
|
||||
.DS_Store
|
||||
*/.DS_Store
|
||||
|
||||
# VS Code
|
||||
.vscode
|
||||
@@ -41,20 +40,11 @@ core/cmd/server/web/index.html
|
||||
frontend/auto-imports.d.ts
|
||||
frontend/components.d.ts
|
||||
frontend/src/xpack
|
||||
frontend/src/xpack-ee
|
||||
agent/xpack
|
||||
agent/router/entry_xpack.go
|
||||
agent/server/init_xpack.go
|
||||
agent/utils/xpack/xpack.go
|
||||
agent/utils/xpack/xpack_xpack.go
|
||||
agent/xpack-ee
|
||||
core/xpack
|
||||
core/router/entry_xpack.go
|
||||
core/server/init_xpack.go
|
||||
core/utils/xpack/xpack.go
|
||||
core/utils/xpack/xpack_xpack.go
|
||||
core/xpack-ee
|
||||
core/router/entry_xpackee.go
|
||||
core/server/init_xpackee.go
|
||||
core/utils/xpack/xpack_xpackee.go
|
||||
|
||||
.history/
|
||||
dist/
|
||||
|
||||
+4
-2
@@ -10,10 +10,12 @@ PR are always welcome, even if they only contain small fixes like typos or a few
|
||||
|
||||
Please submit a PR broken down into small changes bit by bit. A PR consisting of a lot of features and code changes may be hard to review. It is recommended to submit PRs in an incremental fashion.
|
||||
|
||||
This [development guideline](https://docs.1panel.pro/dev_manual/dev_manual/) contains information about repository structure, how to set up development environment, how to run it, and more.
|
||||
|
||||
Note: If you split your pull request to small changes, please make sure any of the changes goes to master will not break anything. Otherwise, it can not be merged until this feature complete.
|
||||
|
||||
## Add a new translation
|
||||
|
||||
If you'd like to help translate 1Panel into a new language, please read the [Translation Contribution Guide](docs/TRANSLATION.md). It lists every file you need to create or modify, along with a reference PR you can use as a concrete example.
|
||||
|
||||
## Report issues
|
||||
It is a great way to contribute by reporting an issue. Well-written and complete bug reports are always welcome! Please open an issue and follow the template to fill in required information.
|
||||
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
<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">Take control of your VPS with 1Panel</h3>
|
||||
|
||||
<h3 align="center">The open-source VPS control panel with native AI agent support</h3>
|
||||
|
||||
<p align="center">
|
||||
Trusted by <strong>2,000,000+</strong> self-hosters worldwide
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://trendshift.io/repositories/2462" target="_blank"><img src="https://trendshift.io/api/badge/repositories/2462" alt="1Panel-dev%2F1Panel | Trendshift" style="width: 240px; height: auto;" /></a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://www.gnu.org/licenses/gpl-3.0.html"><img src="https://shields.io/github/license/1Panel-dev/1Panel?color=%231890FF" alt="License: GPL v3"></a>
|
||||
<a href="https://app.codacy.com/gh/1Panel-dev/1Panel?utm_source=github.com&utm_medium=referral&utm_content=1Panel-dev/1Panel&utm_campaign=Badge_Grade_Dashboard"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
|
||||
<a href="https://discord.gg/bUpUqWqdRr" target="_blank">
|
||||
<img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb"
|
||||
alt="chat on Discord"></a>
|
||||
<a href="https://app.codacy.com/gh/1Panel-dev/1Panel"><img src="https://app.codacy.com/project/badge/Grade/da67574fd82b473992781d1386b937ef" alt="Codacy"></a>
|
||||
<a href="https://discord.gg/bUpUqWqdRr"><img src="https://img.shields.io/discord/1318846410149335080?logo=discord&labelColor=%20%235462eb&logoColor=%20%23f5f5f5&color=%20%235462eb" alt="Discord"></a>
|
||||
<a href="https://github.com/1Panel-dev/1Panel/releases"><img src="https://img.shields.io/github/v/release/1Panel-dev/1Panel" alt="GitHub release"></a>
|
||||
<a href="https://github.com/1Panel-dev/1Panel"><img src="https://img.shields.io/github/stars/1Panel-dev/1Panel?color=%231890FF&style=flat-square" alt="Stars"></a><br>
|
||||
<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>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="/README.md"><img alt="English" src="https://img.shields.io/badge/English-d9d9d9"></a>
|
||||
<a href="/docs/README.zh-Hans.md"><img alt="中文(简体)" src="https://img.shields.io/badge/中文(简体)-d9d9d9"></a>
|
||||
@@ -19,58 +25,91 @@
|
||||
<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.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><br>
|
||||
<a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
|
||||
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
|
||||
<a href="/docs/README.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.zh-Hant.md"><img alt="中文(繁體)" src="https://img.shields.io/badge/中文(繁體)-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/%D0%A0%D1%83%D1%81%D1%81%D0%BA%D0%B8%D0%B9-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>
|
||||
</p>
|
||||
|
||||
------------------------------
|
||||
## What is 1Panel?
|
||||
---
|
||||
|
||||
1Panel is a modern, open-source VPS control panel that streamlines administration through an intuitive, clean web interface.
|
||||
## What is 1Panel?
|
||||
|
||||
## Key Features
|
||||
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.
|
||||
|
||||
- **One-Click OpenClaw Deployment**: Seamlessly deploy OpenClaw agents, host local LLMs using Ollama, and monitor GPU utilization—all within a unified workspace.
|
||||
- **One-Click Website Deployment**: Launch production-ready websites instantly with automatic domain binding, SSL certificate provisioning, and configuration—zero manual setup required.
|
||||
- **Curated App Marketplace**: Discover, install, and update trusted open-source applications (Nextcloud, Bitwarden, Umami, and more) with a single click.
|
||||
- **Centralized Server Management**: Monitor system resources and manage files, databases, containers, and services effortlessly through a unified, intuitive dashboard.
|
||||
- **Enterprise-Grade Security**: Strengthen your defenses with container isolation, firewall controls, audit logging, and secure deployment practices—ensuring compliance and peace of mind.
|
||||
- **Intelligent Backup & Restore**: Schedule automated backups or perform instant restores to local storage or major cloud platforms (AWS S3, Cloudflare R2, and more), with flexible retention and destination options.
|
||||
👉 Watch the [2-minute introduction](https://www.youtube.com/watch?v=Jl_wqp-XA08)
|
||||
|
||||
## Why 1Panel?
|
||||
|
||||
| | 1Panel | cPanel / Plesk | aaPanel | Webmin |
|
||||
|--|--------|----------------|---------|--------|
|
||||
| Free & open source | ✅ | ❌ | Partial | ✅ |
|
||||
| Native AI agent runtime | ✅ | ❌ | ❌ | ❌ |
|
||||
| One-click app marketplace | ✅ 165+ apps | ❌ | ✅ | ❌ |
|
||||
| Modern UI (post-2020) | ✅ | ❌ | Partial | ❌ |
|
||||
| Docker / container management | ✅ | ❌ | ❌ | ❌ |
|
||||
| Active development | ✅ | ✅ | ✅ | Slow |
|
||||
|
||||
## Key Features
|
||||
|
||||
- **AI Agent Runtime**: Deploy Ollama LLMs, spin up OpenClaw personal agents, and monitor GPU utilization — all from the dashboard. No separate AI stack to manage.
|
||||
- **One-Click Website Deployment**: Launch production-ready websites with automatic domain binding, SSL provisioning, and Nginx config — zero manual setup.
|
||||
- **App Marketplace**: 165+ trusted open-source apps (Nextcloud, Bitwarden, Umami, NocoBase, and more) installed and updated with a single click.
|
||||
- **Docker & Container Management**: Create, start, stop, and inspect containers, images, networks, and volumes through a visual UI — no CLI juggling.
|
||||
- **Security Out of the Box**: Firewall rules, fail2ban, container isolation, WAF, and audit logs — configured and running from day one.
|
||||
- **Backup & Restore**: Schedule automated backups to AWS S3, Cloudflare R2, or local storage. Restore any snapshot in one click.
|
||||
|
||||
## Quick Start
|
||||
|
||||
Execute the script below and follow the prompts to install 1Panel:
|
||||
> **Requirements:** Linux VPS (Debian / Ubuntu / CentOS / Rocky), 1 GB RAM, internet access.
|
||||
> Takes ~60 seconds.
|
||||
|
||||
```bash
|
||||
bash -c "$(curl -sSL https://resource.1panel.pro/v2/quick_start.sh)"
|
||||
```
|
||||
|
||||
After installation, open `http://<your-server-ip>:<port>/<security-path>` in your browser.
|
||||
Run `1pctl user-info` via SSH if you need to retrieve your access credentials.
|
||||
|
||||
## Screenshot
|
||||
|
||||

|
||||

|
||||
|
||||
## Pro Edition
|
||||
|
||||
1Panel OSS is free forever. Pro adds features built for teams and production workloads:
|
||||
|
||||
| Feature | OSS | Pro |
|
||||
|---------|:---:|:---:|
|
||||
| One-click app installs | ✅ | ✅ |
|
||||
| AI agents (OpenClaw) | 1 agent | Unlimited |
|
||||
| WAF & advanced security | Basic | ✅ |
|
||||
| Website tamper protection | ❌ | ✅ |
|
||||
| Website uptime monitoring | ❌ | ✅ |
|
||||
| Multi-node management | ❌ | ✅ |
|
||||
| Custom logo & theme | ❌ | ✅ |
|
||||
| Priority support | ❌ | ✅ |
|
||||
|
||||
**From $80/year.** [Compare plans & start 30-day free trial →](https://1panel.pro/pricing)
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#1Panel-dev/1Panel&Date)
|
||||
|
||||
## Pro Edition
|
||||
## Community & Support
|
||||
|
||||
Compared to the OSS Edition, 1Panel Pro Edition provides users with a wealth of enhanced features and technical support services. Enhanced features include WAF enhancement, website tamper protection, website monitoring, GPU monitoring, custom logo and theme color, etc. [Click to view the detailed introduction of the Pro Edition](https://1panel.pro/pricing).
|
||||
- **Discord** — [Join the community](https://discord.gg/bUpUqWqdRr) for help, feature requests, and show-and-tell
|
||||
- **Docs** — [1panel.pro/docs](https://1panel.pro/docs)
|
||||
- **Issues** — [GitHub Issues](https://github.com/1Panel-dev/1Panel/issues) for bug reports
|
||||
|
||||
## Security Information
|
||||
## Security
|
||||
|
||||
If you discover any security issues, please refer to [SECURITY.md](/SECURITY.md).
|
||||
Found a vulnerability? Please read [SECURITY.md](/SECURITY.md) before disclosing.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under The GNU General Public License version 3 (GPLv3) (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at
|
||||
|
||||
<https://www.gnu.org/licenses/gpl-3.0.html>
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License.
|
||||
Licensed under the [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.html).
|
||||
|
||||
@@ -51,6 +51,27 @@ func (b *BaseApi) PageAgents(c *gin.Context) {
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete check Agent
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {array} dto.AppResource
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/delete/check [post]
|
||||
func (b *BaseApi) DeleteCheckAgent(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
checkData, err := agentService.DeleteCheck(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, checkData)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent
|
||||
// @Accept json
|
||||
@@ -91,6 +112,87 @@ func (b *BaseApi) ResetAgentToken(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent remark
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRemarkUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/remark [post]
|
||||
func (b *BaseApi) UpdateAgentRemark(c *gin.Context) {
|
||||
var req dto.AgentRemarkUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateRemark(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Bind Agent website
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentWebsiteBindReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/website/bind [post]
|
||||
func (b *BaseApi) BindAgentWebsite(c *gin.Context) {
|
||||
var req dto.AgentWebsiteBindReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.BindWebsite(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Unbind Agent website
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/website/unbind [post]
|
||||
func (b *BaseApi) UnbindAgentWebsite(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UnbindWebsite(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent model config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {object} dto.AgentModelConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/model/get [post]
|
||||
func (b *BaseApi) GetAgentModelConfig(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := agentService.GetModelConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent model config
|
||||
// @Accept json
|
||||
@@ -132,6 +234,67 @@ func (b *BaseApi) GetAgentOverview(c *gin.Context) {
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Hermes chat sessions
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {array} dto.AgentHermesChatSessionItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/hermes/chat/sessions [post]
|
||||
func (b *BaseApi) GetHermesChatSessions(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := agentService.GetHermesChatSessions(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Rename Hermes chat session
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentHermesChatSessionRenameReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/hermes/chat/sessions/rename [post]
|
||||
func (b *BaseApi) RenameHermesChatSession(c *gin.Context) {
|
||||
var req dto.AgentHermesChatSessionRenameReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.RenameHermesChatSession(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Hermes chat session
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentHermesChatSessionDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/hermes/chat/sessions/delete [post]
|
||||
func (b *BaseApi) DeleteHermesChatSession(c *gin.Context) {
|
||||
var req dto.AgentHermesChatSessionDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteHermesChatSession(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Providers
|
||||
// @Success 200 {array} dto.ProviderInfo
|
||||
@@ -332,6 +495,170 @@ func (b *BaseApi) DeleteAgentAccount(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Create Agent role
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleCreateReq true "request"
|
||||
// @Success 200 {object} dto.AgentRoleCreateResp
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/create [post]
|
||||
func (b *BaseApi) CreateAgentRole(c *gin.Context) {
|
||||
var req dto.AgentRoleCreateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.CreateRole(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent role
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/delete [post]
|
||||
func (b *BaseApi) DeleteAgentRole(c *gin.Context) {
|
||||
var req dto.AgentRoleDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteRole(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Bind Agent role channel
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleBindReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/bind [post]
|
||||
func (b *BaseApi) BindAgentRole(c *gin.Context) {
|
||||
var req dto.AgentRoleBindReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.BindRole(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Unbind Agent role channel
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleBindReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/unbind [post]
|
||||
func (b *BaseApi) UnbindAgentRole(c *gin.Context) {
|
||||
var req dto.AgentRoleBindReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UnbindRole(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get configured Agent roles from config file
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentConfiguredAgentsReq true "request"
|
||||
// @Success 200 {array} dto.AgentConfiguredAgentItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/list [post]
|
||||
func (b *BaseApi) GetConfiguredAgentRoles(c *gin.Context) {
|
||||
var req dto.AgentConfiguredAgentsReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetConfiguredAgents(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent role channels from config file
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleChannelsReq true "request"
|
||||
// @Success 200 {array} dto.AgentRoleChannelItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/channels [post]
|
||||
func (b *BaseApi) GetAgentRoleChannels(c *gin.Context) {
|
||||
var req dto.AgentRoleChannelsReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetRoleChannels(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent role markdown files
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleMarkdownFilesReq true "request"
|
||||
// @Success 200 {array} dto.AgentRoleMarkdownFileItem
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/md/list [post]
|
||||
func (b *BaseApi) GetAgentRoleMarkdownFiles(c *gin.Context) {
|
||||
var req dto.AgentRoleMarkdownFilesReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetRoleMarkdownFiles(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Update Agent role markdown file
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentRoleMarkdownFilesUpdateReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/agent/md/update [post]
|
||||
func (b *BaseApi) UpdateAgentRoleMarkdownFile(c *gin.Context) {
|
||||
var req dto.AgentRoleMarkdownFilesUpdateReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpdateRoleMarkdownFiles(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Feishu channel config
|
||||
// @Accept json
|
||||
@@ -537,6 +864,27 @@ func (b *BaseApi) UpdateAgentDingTalkConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent Weixin channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentIDReq true "request"
|
||||
// @Success 200 {object} dto.AgentWeixinConfig
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/weixin/get [post]
|
||||
func (b *BaseApi) GetAgentWeixinConfig(c *gin.Context) {
|
||||
var req dto.AgentIDReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
data, err := agentService.GetWeixinConfig(req)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, data)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Get Agent QQ Bot channel config
|
||||
// @Accept json
|
||||
@@ -578,6 +926,26 @@ func (b *BaseApi) UpdateAgentQQBotConfig(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Delete Agent channel config
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentChannelDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/channel/delete [post]
|
||||
func (b *BaseApi) DeleteAgentChannelConfig(c *gin.Context) {
|
||||
var req dto.AgentChannelDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.DeleteChannelConfig(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Install Agent plugin
|
||||
// @Accept json
|
||||
@@ -598,6 +966,46 @@ func (b *BaseApi) InstallAgentPlugin(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Upgrade Agent plugin
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentPluginUpgradeReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/plugin/upgrade [post]
|
||||
func (b *BaseApi) UpgradeAgentPlugin(c *gin.Context) {
|
||||
var req dto.AgentPluginUpgradeReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UpgradePlugin(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Uninstall Agent plugin
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentPluginUninstallReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/plugin/uninstall [post]
|
||||
func (b *BaseApi) UninstallAgentPlugin(c *gin.Context) {
|
||||
var req dto.AgentPluginUninstallReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UninstallPlugin(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Check Agent plugin installation status
|
||||
// @Accept json
|
||||
@@ -824,6 +1232,26 @@ func (b *BaseApi) InstallAgentSkill(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Uninstall Agent skill
|
||||
// @Accept json
|
||||
// @Param request body dto.AgentSkillUninstallReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /ai/agents/skills/uninstall [post]
|
||||
func (b *BaseApi) UninstallAgentSkill(c *gin.Context) {
|
||||
var req dto.AgentSkillUninstallReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := agentService.UninstallSkill(req); err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags AI
|
||||
// @Summary Login Agent Weixin channel
|
||||
// @Accept json
|
||||
|
||||
@@ -415,6 +415,11 @@ func (b *BaseApi) Backup(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlBackup(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -475,6 +480,11 @@ func (b *BaseApi) Recover(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecover(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
@@ -530,6 +540,11 @@ func (b *BaseApi) RecoverByUpload(c *gin.Context) {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case "mongodb":
|
||||
if err := backupService.MongodbRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
if err := backupService.PostgresqlRecoverByUpload(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Create mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBCreate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb [post]
|
||||
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 mongodb 数据库 [name]","formatEN":"create mongodb database [name]"}
|
||||
func (b *BaseApi) CreateMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBCreate
|
||||
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 := mongodbService.Create(context.Background(), req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Page mongodb databases
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBSearch true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/search [post]
|
||||
func (b *BaseApi) SearchMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBSearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
total, list, err := mongodbService.SearchWithPage(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Items: list,
|
||||
Total: total,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Update mongodb database description
|
||||
// @Accept json
|
||||
// @Param request body dto.UpdateDescription true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/description [post]
|
||||
// @x-panel-log {"bodyKeys":["id","description"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mongodbs","output_column":"name","output_value":"name"}],"formatZH":"mongodb 数据库 [name] 描述信息修改 [description]","formatEN":"The description of the mongodb database [name] is modified => [description]"}
|
||||
func (b *BaseApi) UpdateMongodbDescription(c *gin.Context) {
|
||||
var req dto.UpdateDescription
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.UpdateDescription(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Load mongodb database from remote
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbLoadDB true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/load [post]
|
||||
func (b *BaseApi) LoadMongodbFromRemote(c *gin.Context) {
|
||||
var req dto.MongodbLoadDB
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.LoadFromRemote(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Bind mongodb database user info
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbBind true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/bind [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "name", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"绑定 mongodb 数据库 [database] [name] 用户 [username]","formatEN":"bind mongodb database [database] [name] user [username]"}
|
||||
func (b *BaseApi) BindMongodbUser(c *gin.Context) {
|
||||
var req dto.MongodbBind
|
||||
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 := mongodbService.BindUser(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb database password
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPassword true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/password [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] [name] 密码","formatEN":"update mongodb database [database] [name] password"}
|
||||
func (b *BaseApi) ChangeMongodbPassword(c *gin.Context) {
|
||||
var req dto.MongodbPassword
|
||||
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 := mongodbService.ChangePassword(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb root password
|
||||
// @Accept json
|
||||
// @Param request body dto.ChangeDBInfo true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/root/password [post]
|
||||
// @x-panel-log {"bodyKeys":["database"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] root 密码","formatEN":"update mongodb database [database] root password"}
|
||||
func (b *BaseApi) ChangeMongodbRootPassword(c *gin.Context) {
|
||||
var req dto.ChangeDBInfo
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.Value) != 0 {
|
||||
value, err := base64.StdEncoding.DecodeString(req.Value)
|
||||
if err != nil {
|
||||
helper.BadRequest(c, err)
|
||||
return
|
||||
}
|
||||
req.Value = string(value)
|
||||
}
|
||||
|
||||
if err := mongodbService.ChangeRootPassword(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Load mongodb privileges
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPrivilegesLoad true "request"
|
||||
// @Success 200 {string} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/privileges [post]
|
||||
func (b *BaseApi) LoadMongodbPrivileges(c *gin.Context) {
|
||||
var req dto.MongodbPrivilegesLoad
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
permission, err := mongodbService.LoadPrivileges(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, permission)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Change mongodb privileges
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbPrivileges true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/privileges/change [post]
|
||||
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 mongodb 数据库 [database] 用户 [username] 权限","formatEN":"update mongodb database [database] user [username] privileges"}
|
||||
func (b *BaseApi) ChangeMongodbPrivileges(c *gin.Context) {
|
||||
var req dto.MongodbPrivileges
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := mongodbService.ChangePrivileges(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Check before delete mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBDeleteCheck true "request"
|
||||
// @Success 200 {array} string
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/del/check [post]
|
||||
func (b *BaseApi) DeleteCheckMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBDeleteCheck
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
apps, err := mongodbService.DeleteCheck(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, apps)
|
||||
}
|
||||
|
||||
// @Tags Database Mongodb
|
||||
// @Summary Delete mongodb database
|
||||
// @Accept json
|
||||
// @Param request body dto.MongodbDBDelete true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /databases/mongodb/del [post]
|
||||
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"database_mongodbs","output_column":"name","output_value":"name"}],"formatZH":"删除 mongodb 数据库 [name]","formatEN":"delete mongodb database [name]"}
|
||||
func (b *BaseApi) DeleteMongodb(c *gin.Context) {
|
||||
var req dto.MongodbDBDelete
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tx, ctx := helper.GetTxAndContext()
|
||||
if err := mongodbService.Delete(ctx, req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
tx.Rollback()
|
||||
return
|
||||
}
|
||||
tx.Commit()
|
||||
helper.Success(c)
|
||||
}
|
||||
@@ -31,17 +31,20 @@ var (
|
||||
dbCommonService = service.NewIDBCommonService()
|
||||
mysqlService = service.NewIMysqlService()
|
||||
postgresqlService = service.NewIPostgresqlService()
|
||||
mongodbService = service.NewIMongodbService()
|
||||
databaseService = service.NewIDatabaseService()
|
||||
redisService = service.NewIRedisService()
|
||||
|
||||
cronjobService = service.NewICronjobService()
|
||||
|
||||
fileService = service.NewIFileService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
fileService = service.NewIFileService()
|
||||
fileHistoryService = service.NewIFileHistoryService()
|
||||
fileShareService = service.NewIFileShareService()
|
||||
sshService = service.NewISSHService()
|
||||
firewallService = service.NewIFirewallService()
|
||||
iptablesService = service.NewIIptablesService()
|
||||
monitorService = service.NewIMonitorService()
|
||||
systemService = service.NewISystemService()
|
||||
|
||||
deviceService = service.NewIDeviceService()
|
||||
fail2banService = service.NewIFail2BanService()
|
||||
|
||||
+320
-5
@@ -25,6 +25,7 @@ import (
|
||||
websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
qrcode "github.com/skip2/go-qrcode"
|
||||
)
|
||||
|
||||
// @Tags File
|
||||
@@ -48,6 +49,31 @@ func (b *BaseApi) ListFiles(c *gin.Context) {
|
||||
helper.SuccessWithData(c, fileList)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary File search: content grep + optional AI summary
|
||||
// @Description When file-management AI is enabled, returns mode=ai with summary and hits. When disabled, returns mode=grep with hits only. Scans file contents only. Supports match options, extension/size/time filters, and scan limits.
|
||||
// @Accept json
|
||||
// @Param request body request.FileAISearch true "request"
|
||||
// @Success 200 {object} response.FileAISearchResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/ai-search [post]
|
||||
func (b *BaseApi) FileAISearch(c *gin.Context) {
|
||||
var req request.FileAISearch
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.ResponseLanguage) == "" {
|
||||
req.ResponseLanguage = strings.TrimSpace(c.GetHeader("Accept-Language"))
|
||||
}
|
||||
res, err := fileService.AISearch(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Page file
|
||||
// @Accept json
|
||||
@@ -224,6 +250,26 @@ func (b *BaseApi) CompressFile(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Stop compress task
|
||||
// @Accept json
|
||||
// @Param request body request.FileCompressStopReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/compress/stop [post]
|
||||
func (b *BaseApi) StopCompressFile(c *gin.Context) {
|
||||
var req request.FileCompressStopReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileService.StopCompress(req.TaskID); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Decompress file
|
||||
// @Accept json
|
||||
@@ -246,6 +292,26 @@ func (b *BaseApi) DeCompressFile(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Stop decompress task
|
||||
// @Accept json
|
||||
// @Param request body request.FileDeCompressStopReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/decompress/stop [post]
|
||||
func (b *BaseApi) StopDeCompressFile(c *gin.Context) {
|
||||
var req request.FileDeCompressStopReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileService.StopDeCompress(req.TaskID); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Load file content
|
||||
// @Accept json
|
||||
@@ -345,7 +411,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
||||
helper.BadRequest(c, errors.New("error paths in request"))
|
||||
return
|
||||
}
|
||||
dir := path.Dir(paths[0])
|
||||
dir := path.Clean(paths[0])
|
||||
|
||||
_, err = os.Stat(dir)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
@@ -386,8 +452,14 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
||||
}
|
||||
_ = os.Chown(dstDir, uid, gid)
|
||||
}
|
||||
dstDirMode := mode
|
||||
dstFileMode := mode.Perm()
|
||||
if dstDirInfo, err := os.Stat(dstDir); err == nil {
|
||||
dstDirMode = dstDirInfo.Mode()
|
||||
dstFileMode = dstDirInfo.Mode().Perm()
|
||||
}
|
||||
tmpFilename := dstFilename + ".tmp"
|
||||
if err := c.SaveUploadedFile(file, tmpFilename); err != nil {
|
||||
if err := c.SaveUploadedFile(file, tmpFilename, dstDirMode); err != nil {
|
||||
_ = os.Remove(tmpFilename)
|
||||
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
|
||||
failures[file.Filename] = e
|
||||
@@ -410,7 +482,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
|
||||
if statErr == nil {
|
||||
_ = os.Chmod(dstFilename, dstInfo.Mode())
|
||||
} else {
|
||||
_ = os.Chmod(dstFilename, mode)
|
||||
_ = os.Chmod(dstFilename, dstFileMode)
|
||||
}
|
||||
if uid != -1 && gid != -1 {
|
||||
_ = os.Chown(dstFilename, uid, gid)
|
||||
@@ -721,12 +793,13 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dstDirInfo, err := os.Stat(dstDir); err == nil {
|
||||
mode = dstDirInfo.Mode().Perm()
|
||||
}
|
||||
dstFileName := filepath.Join(dstDir, fileName)
|
||||
dstInfo, statErr := os.Stat(dstFileName)
|
||||
if statErr == nil {
|
||||
mode = dstInfo.Mode()
|
||||
} else {
|
||||
mode = 0644
|
||||
}
|
||||
if overwrite {
|
||||
_ = os.Remove(dstFileName)
|
||||
@@ -748,6 +821,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
|
||||
}
|
||||
_ = os.Remove(chunkPath)
|
||||
}
|
||||
_ = os.Chmod(dstFileName, mode)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -1038,3 +1112,244 @@ func (b *BaseApi) SetFileRemark(c *gin.Context) {
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary List file shares
|
||||
// @Accept json
|
||||
// @Param request body dto.PageInfo true "request"
|
||||
// @Success 200 {object} dto.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/share/search [post]
|
||||
func (b *BaseApi) SearchFileShare(c *gin.Context) {
|
||||
var req dto.PageInfo
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, list, err := fileShareService.Page(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{
|
||||
Total: total,
|
||||
Items: list,
|
||||
})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Get file share detail by path
|
||||
// @Accept json
|
||||
// @Param request body dto.FilePath true "request"
|
||||
// @Success 200 {object} response.FileShareInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/share/detail [post]
|
||||
func (b *BaseApi) GetFileShareDetail(c *gin.Context) {
|
||||
var req dto.FilePath
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
info, err := fileShareService.GetByPath(req.Path)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Get file share detail by code (no login)
|
||||
// @Param code query string true "share code"
|
||||
// @Success 200 {object} response.FileSharePublicInfo
|
||||
// @Router /files/share/info [get]
|
||||
func (b *BaseApi) GetPublicFileShareInfo(c *gin.Context) {
|
||||
code := strings.TrimSpace(c.Query("code"))
|
||||
if code == "" {
|
||||
helper.BadRequest(c, errors.New("code is required"))
|
||||
return
|
||||
}
|
||||
info, err := fileShareService.GetPublicByCode(code)
|
||||
if err != nil {
|
||||
if be, ok := err.(buserr.BusinessError); ok {
|
||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
|
||||
func buildSharePublicURL(c *gin.Context, code, operateNode string) string {
|
||||
scheme := strings.TrimSpace(c.GetHeader("X-Forwarded-Proto"))
|
||||
if scheme == "" {
|
||||
if c.Request.TLS != nil {
|
||||
scheme = "https"
|
||||
} else {
|
||||
scheme = "http"
|
||||
}
|
||||
}
|
||||
host := strings.TrimSpace(c.GetHeader("X-Forwarded-Host"))
|
||||
if host == "" {
|
||||
host = c.Request.Host
|
||||
}
|
||||
shareURL := url.URL{
|
||||
Scheme: scheme,
|
||||
Host: host,
|
||||
Path: "/s/" + url.PathEscape(code),
|
||||
}
|
||||
query := shareURL.Query()
|
||||
if strings.TrimSpace(operateNode) != "" {
|
||||
query.Set("operateNode", operateNode)
|
||||
}
|
||||
shareURL.RawQuery = query.Encode()
|
||||
return shareURL.String()
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Get file share QR code image
|
||||
// @Produce png
|
||||
// @Param code query string true "share code"
|
||||
// @Param operateNode query string false "operate node"
|
||||
// @Success 200 {file} file
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/share/qrcode [get]
|
||||
func (b *BaseApi) GetFileShareQRCode(c *gin.Context) {
|
||||
code := strings.TrimSpace(c.Query("code"))
|
||||
if code == "" {
|
||||
helper.BadRequest(c, errors.New("code is required"))
|
||||
return
|
||||
}
|
||||
if _, err := fileShareService.GetByCode(code); err != nil {
|
||||
if be, ok := err.(buserr.BusinessError); ok {
|
||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
png, err := qrcode.Encode(buildSharePublicURL(c, code, c.Query("operateNode")), qrcode.Medium, 256)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("Cache-Control", "private, max-age=300")
|
||||
c.Data(http.StatusOK, "image/png", png)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Create temporary file share link
|
||||
// @Accept json
|
||||
// @Param request body request.FileShareCreate true "request"
|
||||
// @Success 200 {object} response.FileShareInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/share/create [post]
|
||||
// @x-panel-log {"bodyKeys":["path","expireMinutes"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建文件分享 [path]","formatEN":"Create file share [path]"}
|
||||
func (b *BaseApi) CreateFileShare(c *gin.Context) {
|
||||
var req request.FileShareCreate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
res, err := fileShareService.Create(req)
|
||||
if err != nil {
|
||||
if be, ok := err.(buserr.BusinessError); ok {
|
||||
helper.ErrorWithDetail(c, http.StatusInternalServerError, be.Msg, be.Err)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, res)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Delete file share by path
|
||||
// @Accept json
|
||||
// @Param request body dto.FilePath true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/share/del [post]
|
||||
// @x-panel-log {"bodyKeys":["path"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"关闭文件分享 [path]","formatEN":"Close file share [path]"}
|
||||
func (b *BaseApi) DeleteFileShare(c *gin.Context) {
|
||||
var req dto.FilePath
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileShareService.DeleteByPath(req.Path); err != nil {
|
||||
if be, ok := err.(buserr.BusinessError); ok {
|
||||
helper.ErrorWithDetail(c, http.StatusInternalServerError, be.Msg, be.Err)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Check file share code (no login)
|
||||
// @Param code query string true "share code"
|
||||
// @Param password query string false "optional password"
|
||||
// @Success 200 {object} dto.Response
|
||||
// @Router /files/share/check [get]
|
||||
func (b *BaseApi) CheckFileShare(c *gin.Context) {
|
||||
code := strings.TrimSpace(c.Query("code"))
|
||||
password := c.Query("password")
|
||||
if code == "" {
|
||||
helper.BadRequest(c, errors.New("code is required"))
|
||||
return
|
||||
}
|
||||
if err := fileShareService.Check(code, password); err != nil {
|
||||
if be, ok := err.(buserr.BusinessError); ok {
|
||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Download file by share code (no login)
|
||||
// @Produce octet-stream
|
||||
// @Param code query string true "share code"
|
||||
// @Param password query string false "optional password"
|
||||
// @Success 200 {file} file
|
||||
// @Router /files/share/download [get]
|
||||
func (b *BaseApi) DownloadFileShare(c *gin.Context) {
|
||||
code := strings.TrimSpace(c.Query("code"))
|
||||
password := c.Query("password")
|
||||
if code == "" {
|
||||
helper.BadRequest(c, errors.New("code is required"))
|
||||
return
|
||||
}
|
||||
filePath, displayName, err := fileShareService.PrepareDownload(code, password)
|
||||
if err != nil {
|
||||
if be, ok := err.(buserr.BusinessError); ok {
|
||||
helper.ErrorWithDetail(c, http.StatusBadRequest, be.Msg, be)
|
||||
return
|
||||
}
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
info, err := file.Stat()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
c.Header("Content-Length", strconv.FormatInt(info.Size(), 10))
|
||||
c.Header("Content-Disposition", "attachment; filename*=utf-8''"+url.PathEscape(displayName))
|
||||
http.ServeContent(c.Writer, c.Request, displayName, info.ModTime(), file)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
package v2
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// @Tags File
|
||||
// @Summary Load file history list
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistorySearchReq true "request"
|
||||
// @Success 200 {object} response.PageResult
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/search [post]
|
||||
func (b *BaseApi) SearchFileHistory(c *gin.Context) {
|
||||
var req request.FileHistorySearchReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
total, items, err := fileHistoryService.Search(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, dto.PageResult{Items: items, Total: total})
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Load file history content
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistoryContentReq true "request"
|
||||
// @Success 200 {object} response.FileHistoryInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/content [post]
|
||||
func (b *BaseApi) GetFileHistoryContent(c *gin.Context) {
|
||||
var req request.FileHistoryContentReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
item, err := fileHistoryService.GetContent(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, item)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Delete file history record
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistoryDeleteReq true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/del [post]
|
||||
func (b *BaseApi) DeleteFileHistory(c *gin.Context) {
|
||||
var req request.FileHistoryDeleteReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := fileHistoryService.Delete(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags File
|
||||
// @Summary Restore file history record
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistoryRestoreReq true "request"
|
||||
// @Success 200 {object} response.FileInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /files/history/restore [post]
|
||||
func (b *BaseApi) RestoreFileHistory(c *gin.Context) {
|
||||
var req request.FileHistoryRestoreReq
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
info, err := fileHistoryService.Restore(req)
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, info)
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
|
||||
@@ -81,6 +82,62 @@ func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
func (b *BaseApi) GetFileManageAISettingInfo(c *gin.Context) {
|
||||
setting, err := settingService.GetFileManageAIInfo()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, setting)
|
||||
}
|
||||
|
||||
func (b *BaseApi) UpdateFileManageAISetting(c *gin.Context) {
|
||||
var req dto.FileManageAIInfo
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := settingService.UpdateFileManageAI(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Load file history setting info
|
||||
// @Success 200 {object} response.FileHistorySettingInfo
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /settings/file-history/search [post]
|
||||
func (b *BaseApi) GetFileHistorySettingInfo(c *gin.Context) {
|
||||
setting, err := settingService.GetFileHistorySettingInfo()
|
||||
if err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.SuccessWithData(c, setting)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Update file history setting
|
||||
// @Accept json
|
||||
// @Param request body request.FileHistorySettingUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /settings/file-history/update [post]
|
||||
func (b *BaseApi) UpdateFileHistorySetting(c *gin.Context) {
|
||||
var req request.FileHistorySettingUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
if err := settingService.UpdateFileHistorySetting(req); err != nil {
|
||||
helper.InternalServer(c, err)
|
||||
return
|
||||
}
|
||||
helper.Success(c)
|
||||
}
|
||||
|
||||
// @Tags System Setting
|
||||
// @Summary Load local backup dir
|
||||
// @Success 200 {string} path
|
||||
|
||||
@@ -249,14 +249,14 @@ func (b *BaseApi) LoadSSHFile(c *gin.Context) {
|
||||
// @Tags SSH
|
||||
// @Summary Update host SSH setting by file
|
||||
// @Accept json
|
||||
// @Param request body dto.SSHConf true "request"
|
||||
// @Param request body dto.SSHConfUpdate true "request"
|
||||
// @Success 200
|
||||
// @Security ApiKeyAuth
|
||||
// @Security Timestamp
|
||||
// @Router /hosts/ssh/file/update [post]
|
||||
// @x-panel-log {"bodyKeys":["key"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改 SSH 配置文件 [key]","formatEN":"update SSH conf [key]"}
|
||||
func (b *BaseApi) UpdateSSHByFile(c *gin.Context) {
|
||||
var req dto.SettingUpdate
|
||||
var req dto.SSHConfUpdate
|
||||
if err := helper.CheckBindAndValidate(&req, c); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -213,19 +213,29 @@ func loadContainerInitCmd(c *gin.Context) ([]string, error) {
|
||||
func loadDatabaseInitCmd(c *gin.Context) ([]string, error) {
|
||||
database := c.Query("database")
|
||||
databaseType := c.Query("databaseType")
|
||||
if len(database) == 0 || len(databaseType) == 0 {
|
||||
if len(databaseType) == 0 {
|
||||
return nil, fmt.Errorf("error param of database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
databaseConn, err := appInstallService.LoadConnInfo(dto.OperationWithNameAndType{Type: databaseType, Name: database})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("no such database in db, err: %v", err)
|
||||
}
|
||||
if len(databaseConn.ContainerName) == 0 {
|
||||
return nil, fmt.Errorf("no such database container for database: %s or database type: %s", database, databaseType)
|
||||
}
|
||||
commands := []string{"exec", "-it", databaseConn.ContainerName}
|
||||
switch databaseType {
|
||||
case "mysql", "mysql-cluster":
|
||||
commands = append(commands, []string{"mysql", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mariadb":
|
||||
commands = append(commands, []string{"mariadb", "-uroot", "-p" + databaseConn.Password}...)
|
||||
case "mongodb":
|
||||
commands = append(commands, []string{
|
||||
"mongosh",
|
||||
"--username", databaseConn.Username,
|
||||
"--password", databaseConn.Password,
|
||||
"--authenticationDatabase", "admin",
|
||||
}...)
|
||||
case "postgresql", "postgresql-cluster":
|
||||
commands = []string{"exec", "-e", fmt.Sprintf("PGPASSWORD=%s", databaseConn.Password), "-it", databaseConn.ContainerName, "psql", "-t", "-U", databaseConn.Username}
|
||||
}
|
||||
|
||||
+349
-100
@@ -4,11 +4,12 @@ import "time"
|
||||
|
||||
type AgentCreateReq struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Remark string `json:"remark"`
|
||||
AppVersion string `json:"appVersion" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required"`
|
||||
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
AllowedOrigins []string `json:"allowedOrigins"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw"`
|
||||
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
|
||||
Model string `json:"model"`
|
||||
AccountID uint `json:"accountId"`
|
||||
Token string `json:"token"`
|
||||
@@ -27,30 +28,35 @@ type AgentCreateReq struct {
|
||||
}
|
||||
|
||||
type AgentItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
AgentType string `json:"agentType"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"providerName"`
|
||||
Model string `json:"model"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Token string `json:"token"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
AccountID uint `json:"accountId"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
Container string `json:"containerName"`
|
||||
WebUIPort int `json:"webUIPort"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
Path string `json:"path"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
Upgradable bool `json:"upgradable"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Remark string `json:"remark"`
|
||||
AgentType string `json:"agentType"`
|
||||
Provider string `json:"provider"`
|
||||
ProviderName string `json:"providerName"`
|
||||
Model string `json:"model"`
|
||||
APIType string `json:"apiType"`
|
||||
MaxTokens int `json:"maxTokens"`
|
||||
ContextWindow int `json:"contextWindow"`
|
||||
BaseURL string `json:"baseUrl"`
|
||||
APIKey string `json:"apiKey"`
|
||||
Token string `json:"token"`
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
WebsitePrimaryDomain string `json:"websitePrimaryDomain"`
|
||||
WebsiteType string `json:"websiteType"`
|
||||
WebsiteProtocol string `json:"websiteProtocol"`
|
||||
AccountID uint `json:"accountId"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
Container string `json:"containerName"`
|
||||
WebUIPort int `json:"webUIPort"`
|
||||
BridgePort int `json:"bridgePort"`
|
||||
Path string `json:"path"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
Upgradable bool `json:"upgradable"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
}
|
||||
|
||||
type AgentDeleteReq struct {
|
||||
@@ -63,10 +69,36 @@ type AgentTokenResetReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentRemarkUpdateReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type AgentWebsiteBindReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
WebsiteID uint `json:"websiteId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentModelConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
AccountID uint `json:"accountId" validate:"required"`
|
||||
Model string `json:"model" validate:"required"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
}
|
||||
|
||||
type AgentModelConfig struct {
|
||||
AccountID uint `json:"accountId"`
|
||||
Model string `json:"model"`
|
||||
Fallbacks []string `json:"fallbacks"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionItem struct {
|
||||
ID string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Model string `json:"model"`
|
||||
MessageCount int64 `json:"messageCount"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
LastActive string `json:"lastActive"`
|
||||
}
|
||||
|
||||
type AgentOverviewReq struct {
|
||||
@@ -77,10 +109,94 @@ type AgentIDReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionRenameReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
Title string `json:"title" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentHermesChatSessionDeleteReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentOverview struct {
|
||||
Snapshot AgentOverviewSnapshot `json:"snapshot"`
|
||||
}
|
||||
|
||||
type AgentRoleBinding struct {
|
||||
Channel string `json:"channel" validate:"required"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type AgentRoleCreateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Model string `json:"model"`
|
||||
Bindings []AgentRoleBinding `json:"bindings"`
|
||||
}
|
||||
|
||||
type AgentRoleCreateResp struct {
|
||||
Output string `json:"output"`
|
||||
}
|
||||
|
||||
type AgentRoleDeleteReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentRoleBindReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
ID string `json:"id" validate:"required"`
|
||||
Channel string `json:"channel" validate:"required"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type AgentConfiguredAgentsReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentRoleChannelsReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentRoleChannelItem struct {
|
||||
Name string `json:"name"`
|
||||
Bound bool `json:"bound"`
|
||||
AccountIDs []string `json:"accountIds"`
|
||||
}
|
||||
|
||||
type AgentRoleMarkdownFilesReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Workspace string `json:"workspace" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentConfiguredAgentItem struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
Workspace string `json:"workspace"`
|
||||
Model string `json:"model"`
|
||||
AgentDir string `json:"agentDir"`
|
||||
Bindings []AgentRoleBinding `json:"bindings"`
|
||||
}
|
||||
|
||||
type AgentRoleMarkdownFileItem struct {
|
||||
Name string `json:"name"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type AgentRoleMarkdownFileUpdateItem struct {
|
||||
Name string `json:"name" validate:"required,oneof=AGENTS.md SOUL.md USER.md IDENTITY.md TOOLS.md HEARTBEAT.md BOOT.md BOOTSTRAP.md"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type AgentRoleMarkdownFilesUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Workspace string `json:"workspace" validate:"required"`
|
||||
Restart bool `json:"restart"`
|
||||
Files []AgentRoleMarkdownFileUpdateItem `json:"files" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentOverviewSnapshot struct {
|
||||
ContainerStatus string `json:"containerStatus"`
|
||||
AppVersion string `json:"appVersion"`
|
||||
@@ -195,12 +311,17 @@ type AgentFeishuConfigReq struct {
|
||||
}
|
||||
|
||||
type AgentFeishuConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
BotName string `json:"botName" validate:"required"`
|
||||
AppID string `json:"appId" validate:"required"`
|
||||
AppSecret string `json:"appSecret" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode" validate:"required"`
|
||||
Streaming bool `json:"streaming"`
|
||||
RequireMention string `json:"requireMention" validate:"required,oneof=true false open"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentFeishuPairingApproveReq struct {
|
||||
@@ -209,11 +330,17 @@ type AgentFeishuPairingApproveReq struct {
|
||||
}
|
||||
|
||||
type AgentFeishuConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
BotName string `json:"botName"`
|
||||
AppID string `json:"appId"`
|
||||
AppSecret string `json:"appSecret"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ThreadSession bool `json:"threadSession"`
|
||||
ReplyMode string `json:"replyMode"`
|
||||
Streaming bool `json:"streaming"`
|
||||
RequireMention string `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Domain string `json:"domain"`
|
||||
ConnectionMode string `json:"connectionMode"`
|
||||
Bots []AgentFeishuBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentTelegramConfigReq struct {
|
||||
@@ -221,113 +348,223 @@ type AgentTelegramConfigReq struct {
|
||||
}
|
||||
|
||||
type AgentTelegramConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required"`
|
||||
BotToken string `json:"botToken" validate:"required"`
|
||||
Proxy string `json:"proxy"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open allowlist disabled"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Proxy string `json:"proxy"`
|
||||
Streaming string `json:"streaming" validate:"required,oneof=off partial block progress"`
|
||||
DefaultAccount string `json:"defaultAccount" validate:"required"`
|
||||
Bots []AgentTelegramBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentTelegramConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
BotToken string `json:"botToken"`
|
||||
Proxy string `json:"proxy"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Proxy string `json:"proxy"`
|
||||
Streaming string `json:"streaming"`
|
||||
DefaultAccount string `json:"defaultAccount"`
|
||||
Bots []AgentTelegramBot `json:"bots"`
|
||||
}
|
||||
|
||||
type AgentChannelPairingApproveReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom dingtalk-connector"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot dingtalk"`
|
||||
PairingCode string `json:"pairingCode" validate:"required"`
|
||||
AccountID string `json:"accountId"`
|
||||
}
|
||||
|
||||
type AgentChannelDeleteReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu telegram discord wecom qqbot dingtalk weixin"`
|
||||
}
|
||||
|
||||
type AgentWecomConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open"`
|
||||
BotID string `json:"botId" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentWecomConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
BotID string `json:"botId"`
|
||||
Secret string `json:"secret"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentDingTalkConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
ClientID string `json:"clientId" validate:"required"`
|
||||
ClientSecret string `json:"clientSecret" validate:"required"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing allowlist open disabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing open allowlist disabled"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
BotID string `json:"botId" validate:"required"`
|
||||
Secret string `json:"secret" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentDingTalkConfig struct {
|
||||
type AgentWecomConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
BotID string `json:"botId"`
|
||||
Secret string `json:"secret"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentDingTalkConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required,oneof=pairing allowlist open disabled"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
SeparateSessionByConversation bool `json:"separateSessionByConversation"`
|
||||
GroupSessionScope string `json:"groupSessionScope" validate:"required,oneof=group group_sender"`
|
||||
SharedMemoryAcrossConversations bool `json:"sharedMemoryAcrossConversations"`
|
||||
AsyncMode bool `json:"asyncMode"`
|
||||
AckText string `json:"ackText"`
|
||||
Bots []AgentDingTalkBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentDingTalkConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
SeparateSessionByConversation bool `json:"separateSessionByConversation"`
|
||||
GroupSessionScope string `json:"groupSessionScope"`
|
||||
SharedMemoryAcrossConversations bool `json:"sharedMemoryAcrossConversations"`
|
||||
AsyncMode bool `json:"asyncMode"`
|
||||
AckText string `json:"ackText"`
|
||||
Bots []AgentDingTalkBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentWeixinLoginReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentWeixinConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"appId" validate:"required"`
|
||||
ClientSecret string `json:"clientSecret" validate:"required"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Bots []AgentQQBotBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentQQBotConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
AppID string `json:"appId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
Installed bool `json:"installed"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
GroupAllowFrom []string `json:"groupAllowFrom"`
|
||||
Bots []AgentQQBotBot `json:"bots"`
|
||||
Installed bool `json:"installed"`
|
||||
}
|
||||
|
||||
type AgentPluginInstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=qqbot wecom dingtalk weixin"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu qqbot wecom dingtalk weixin"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentPluginUpgradeReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu qqbot wecom dingtalk weixin"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentPluginUninstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu qqbot wecom dingtalk weixin"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentPluginCheckReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=qqbot wecom dingtalk weixin"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=feishu qqbot wecom dingtalk weixin"`
|
||||
CheckLatest bool `json:"checkLatest"`
|
||||
}
|
||||
|
||||
type AgentPluginStatus struct {
|
||||
Installed bool `json:"installed"`
|
||||
Installed bool `json:"installed"`
|
||||
CurrentVersion string `json:"currentVersion"`
|
||||
LatestVersion string `json:"latestVersion"`
|
||||
Upgradable bool `json:"upgradable"`
|
||||
}
|
||||
|
||||
type AgentDiscordConfigUpdateReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
Token string `json:"token" validate:"required"`
|
||||
Proxy string `json:"proxy"`
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy" validate:"required"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy" validate:"required,oneof=open allowlist disabled"`
|
||||
Proxy string `json:"proxy"`
|
||||
DefaultAccount string `json:"defaultAccount" validate:"required"`
|
||||
Bots []AgentDiscordBot `json:"bots" validate:"required,min=1"`
|
||||
}
|
||||
|
||||
type AgentDiscordConfig struct {
|
||||
Enabled bool `json:"enabled"`
|
||||
Enabled bool `json:"enabled"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
RequireMention bool `json:"requireMention"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
Proxy string `json:"proxy"`
|
||||
DefaultAccount string `json:"defaultAccount"`
|
||||
Bots []AgentDiscordBot `json:"bots"`
|
||||
}
|
||||
|
||||
type AgentChannelBotBase struct {
|
||||
AccountID string `json:"accountId"`
|
||||
Name string `json:"name"`
|
||||
Enabled bool `json:"enabled"`
|
||||
IsDefault bool `json:"isDefault"`
|
||||
}
|
||||
|
||||
func (b AgentChannelBotBase) IsEnabled() bool {
|
||||
return b.Enabled
|
||||
}
|
||||
|
||||
type AgentFeishuBot struct {
|
||||
AgentChannelBotBase
|
||||
AppID string `json:"appId"`
|
||||
AppSecret string `json:"appSecret"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
}
|
||||
|
||||
type AgentTelegramBot struct {
|
||||
AgentChannelBotBase
|
||||
BotToken string `json:"botToken"`
|
||||
DmPolicy string `json:"dmPolicy"`
|
||||
GroupPolicy string `json:"groupPolicy"`
|
||||
Token string `json:"token"`
|
||||
Proxy string `json:"proxy"`
|
||||
Streaming string `json:"streaming"`
|
||||
}
|
||||
|
||||
type AgentDiscordBot struct {
|
||||
AgentChannelBotBase
|
||||
Token string `json:"token"`
|
||||
}
|
||||
|
||||
type AgentQQBotBot struct {
|
||||
AgentChannelBotBase
|
||||
AppID string `json:"appId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
AllowFrom []string `json:"allowFrom"`
|
||||
SystemPrompt string `json:"systemPrompt"`
|
||||
}
|
||||
|
||||
type AgentDingTalkBot struct {
|
||||
AgentChannelBotBase
|
||||
ClientID string `json:"clientId"`
|
||||
ClientSecret string `json:"clientSecret"`
|
||||
}
|
||||
|
||||
type AgentSecurityConfigUpdateReq struct {
|
||||
@@ -367,25 +604,32 @@ type AgentConfigFile struct {
|
||||
|
||||
type AgentSkillSearchReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub skillhub"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
|
||||
Keyword string `json:"keyword" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentSkillItem struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Source string `json:"source"`
|
||||
Bundled bool `json:"bundled"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
Tags []string `json:"tags"`
|
||||
Source string `json:"source"`
|
||||
Trust string `json:"trust"`
|
||||
Identifier string `json:"identifier"`
|
||||
Bundled bool `json:"bundled"`
|
||||
Disabled bool `json:"disabled"`
|
||||
Uninstallable bool `json:"uninstallable"`
|
||||
}
|
||||
|
||||
type AgentSkillSearchItem struct {
|
||||
Slug string `json:"slug"`
|
||||
Identifier string `json:"identifier"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Summary string `json:"summary"`
|
||||
Version string `json:"version"`
|
||||
Source string `json:"source"`
|
||||
Trust string `json:"trust"`
|
||||
Score string `json:"score"`
|
||||
}
|
||||
|
||||
@@ -397,7 +641,12 @@ type AgentSkillUpdateReq struct {
|
||||
|
||||
type AgentSkillInstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub skillhub"`
|
||||
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"`
|
||||
Slug string `json:"slug" validate:"required"`
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type AgentSkillUninstallReq struct {
|
||||
AgentID uint `json:"agentId" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
}
|
||||
|
||||
+11
-10
@@ -65,7 +65,7 @@ type UploadForRecover struct {
|
||||
}
|
||||
|
||||
type CommonBackup struct {
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql 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"`
|
||||
DetailName string `json:"detailName"`
|
||||
Secret string `json:"secret"`
|
||||
@@ -77,15 +77,16 @@ type CommonBackup struct {
|
||||
Description string `json:"description"`
|
||||
}
|
||||
type CommonRecover struct {
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mysql-cluster postgresql-cluster redis-cluster container compose"`
|
||||
Name string `json:"name"`
|
||||
DetailName string `json:"detailName"`
|
||||
File string `json:"file"`
|
||||
Secret string `json:"secret"`
|
||||
TaskID string `json:"taskID"`
|
||||
BackupRecordID uint `json:"backupRecordID"`
|
||||
Timeout int `json:"timeout"`
|
||||
DownloadAccountID uint `json:"downloadAccountID" validate:"required"`
|
||||
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"`
|
||||
DetailName string `json:"detailName"`
|
||||
File string `json:"file"`
|
||||
Secret string `json:"secret"`
|
||||
DropAllCollections bool `json:"dropAllCollections"`
|
||||
TaskID string `json:"taskID"`
|
||||
BackupRecordID uint `json:"backupRecordID"`
|
||||
Timeout int `json:"timeout"`
|
||||
}
|
||||
|
||||
type RecordSearch struct {
|
||||
|
||||
@@ -11,7 +11,7 @@ type DBConfUpdateByFile struct {
|
||||
type ChangeDBInfo struct {
|
||||
ID uint `json:"id"`
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Type string `json:"type" validate:"required,oneof=mysql mariadb postgresql redis mongodb mysql-cluster postgresql-cluster redis-cluster"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
Value string `json:"value" validate:"required"`
|
||||
}
|
||||
@@ -137,6 +137,83 @@ type MysqlStatus struct {
|
||||
Position string `json:"Position"`
|
||||
}
|
||||
|
||||
// mongodb
|
||||
type MongodbDBSearch struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
OrderBy string `json:"orderBy" validate:"required,oneof=name createdAt"`
|
||||
Order string `json:"order" validate:"required,oneof=null ascending descending"`
|
||||
}
|
||||
|
||||
type MongodbDBInfo struct {
|
||||
ID uint `json:"id"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
Name string `json:"name"`
|
||||
From string `json:"from"`
|
||||
MongodbName string `json:"mongodbName"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MongodbDBCreate struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
Permission string `json:"permission" validate:"required,oneof=dbOwner read readWrite userAdmin"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
type MongodbLoadDB struct {
|
||||
From string `json:"from" validate:"required,oneof=local remote"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbDBDeleteCheck struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbDBDelete struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required,oneof=mongodb"`
|
||||
Database string `json:"database" validate:"required"`
|
||||
ForceDelete bool `json:"forceDelete"`
|
||||
DeleteBackup bool `json:"deleteBackup"`
|
||||
}
|
||||
|
||||
type MongodbBind struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbPassword struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Password string `json:"password" validate:"required"`
|
||||
}
|
||||
|
||||
type MongodbPrivileges struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
Permission string `json:"permission" validate:"required,oneof=dbOwner read readWrite userAdmin"`
|
||||
}
|
||||
|
||||
type MongodbPrivilegesLoad struct {
|
||||
Database string `json:"database" validate:"required"`
|
||||
Name string `json:"name" validate:"required"`
|
||||
Username string `json:"username" validate:"required"`
|
||||
}
|
||||
|
||||
type MysqlVariables struct {
|
||||
BinlogCacheSize string `json:"binlog_cache_size"`
|
||||
InnodbBufferPoolSize string `json:"innodb_buffer_pool_size"`
|
||||
|
||||
@@ -7,6 +7,7 @@ import (
|
||||
type SearchTaskLogReq struct {
|
||||
Status string `json:"status"`
|
||||
Type string `json:"type"`
|
||||
TaskID string `json:"taskID"`
|
||||
PageInfo
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,29 @@ type FileOption struct {
|
||||
files.FileOption
|
||||
}
|
||||
|
||||
type FileAISearch struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Query string `json:"query" validate:"required"`
|
||||
ResponseLanguage string `json:"responseLanguage,omitempty"`
|
||||
ContainSub *bool `json:"containSub,omitempty"`
|
||||
MaxItems int `json:"maxItems" validate:"omitempty,min=1,max=2000"`
|
||||
MatchCase bool `json:"matchCase"`
|
||||
WholeWord bool `json:"wholeWord"`
|
||||
UseRegex bool `json:"useRegex"`
|
||||
Extensions []string `json:"extensions,omitempty"`
|
||||
MinSize int64 `json:"minSize"`
|
||||
MaxSize int64 `json:"maxSize"`
|
||||
ModifiedAfter string `json:"modifiedAfter,omitempty"`
|
||||
ModifiedBefore string `json:"modifiedBefore,omitempty"`
|
||||
|
||||
MaxScanFiles int `json:"maxScanFiles"`
|
||||
MaxFileBytes int64 `json:"maxFileBytes"`
|
||||
MaxHitsPerFile int `json:"maxHitsPerFile"`
|
||||
MaxTotalHits int `json:"maxTotalHits"`
|
||||
ContentHitsPromptMaxBytes int `json:"contentHitsPromptMaxBytes"`
|
||||
LlmMaxOutputTokens int `json:"llmMaxOutputTokens"`
|
||||
}
|
||||
|
||||
type FileContentReq struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
IsDetail bool `json:"isDetail"`
|
||||
@@ -55,6 +78,11 @@ type FileCompress struct {
|
||||
Name string `json:"name" validate:"required"`
|
||||
Replace bool `json:"replace"`
|
||||
Secret string `json:"secret"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type FileCompressStopReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type FileDeCompress struct {
|
||||
@@ -62,6 +90,11 @@ type FileDeCompress struct {
|
||||
Type string `json:"type" validate:"required"`
|
||||
Path string `json:"path" validate:"required"`
|
||||
Secret string `json:"secret"`
|
||||
TaskID string `json:"taskID"`
|
||||
}
|
||||
|
||||
type FileDeCompressStopReq struct {
|
||||
TaskID string `json:"taskID" validate:"required"`
|
||||
}
|
||||
|
||||
type FileEdit struct {
|
||||
@@ -172,3 +205,9 @@ type FileRemarkUpdate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
Remark string `json:"remark"`
|
||||
}
|
||||
|
||||
type FileShareCreate struct {
|
||||
Path string `json:"path" validate:"required"`
|
||||
ExpireMinutes int `json:"expireMinutes" validate:"min=0,max=10080"`
|
||||
Password *string `json:"password"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
package request
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
)
|
||||
|
||||
type FileHistorySearchReq struct {
|
||||
dto.PageInfo
|
||||
Path string `json:"path"`
|
||||
Scope string `json:"scope" validate:"required,oneof=current all"`
|
||||
Operation string `json:"operation"`
|
||||
}
|
||||
|
||||
type FileHistoryContentReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type FileHistoryIDs []uint
|
||||
|
||||
func (ids *FileHistoryIDs) UnmarshalJSON(data []byte) error {
|
||||
type alias []uint
|
||||
var direct alias
|
||||
if err := json.Unmarshal(data, &direct); err == nil {
|
||||
*ids = FileHistoryIDs(direct)
|
||||
return nil
|
||||
}
|
||||
|
||||
var wrapped struct {
|
||||
IDs []uint `json:"ids"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &wrapped); err != nil {
|
||||
return err
|
||||
}
|
||||
*ids = FileHistoryIDs(wrapped.IDs)
|
||||
return nil
|
||||
}
|
||||
|
||||
type FileHistoryDeleteReq struct {
|
||||
IDs FileHistoryIDs `json:"ids" validate:"required"`
|
||||
}
|
||||
|
||||
type FileHistoryRestoreReq struct {
|
||||
ID uint `json:"id" validate:"required"`
|
||||
}
|
||||
|
||||
type FileHistorySettingUpdate struct {
|
||||
Enable string `json:"enable" validate:"required,oneof=Enable Disable"`
|
||||
MaxPerPath int `json:"maxPerPath" validate:"min=0,max=1000"`
|
||||
DiskQuotaMB int `json:"diskQuotaMB" validate:"min=0,max=1048576"`
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
package request
|
||||
@@ -51,7 +51,6 @@ type WebsiteSSLApply struct {
|
||||
ID uint `json:"ID" validate:"required"`
|
||||
SkipDNSCheck bool `json:"skipDNSCheck"`
|
||||
Nameservers []string `json:"nameservers"`
|
||||
DisableLog bool `json:"disableLog"`
|
||||
}
|
||||
|
||||
type WebsiteSSLObtain struct {
|
||||
|
||||
@@ -85,3 +85,41 @@ type FileConvertLog struct {
|
||||
type FileRemarksRes struct {
|
||||
Remarks map[string]string `json:"remarks"`
|
||||
}
|
||||
|
||||
type FileShareInfo struct {
|
||||
Code string `json:"code"`
|
||||
Path string `json:"path"`
|
||||
FileName string `json:"fileName"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
Permanent bool `json:"permanent"`
|
||||
HasPassword bool `json:"hasPassword"`
|
||||
Password string `json:"password,omitempty"`
|
||||
}
|
||||
|
||||
type FileSharePublicInfo struct {
|
||||
FileName string `json:"fileName"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
Permanent bool `json:"permanent"`
|
||||
HasPassword bool `json:"hasPassword"`
|
||||
}
|
||||
|
||||
type FileAIContentHit struct {
|
||||
Path string `json:"path"`
|
||||
Line int `json:"line"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type FileAISearchResult struct {
|
||||
Mode string `json:"mode"`
|
||||
Summary string `json:"summary"`
|
||||
Hits []FileAIContentHit `json:"hits"`
|
||||
ContentScannedFiles int `json:"contentScannedFiles"`
|
||||
ContentHitsTruncated bool `json:"contentHitsTruncated"`
|
||||
Truncated bool `json:"truncated"`
|
||||
PreFiltered bool `json:"preFiltered"`
|
||||
ItemCount int `json:"itemCount"`
|
||||
PromptTokens int `json:"promptTokens"`
|
||||
CompletionTokens int `json:"completionTokens"`
|
||||
TotalTokens int `json:"totalTokens"`
|
||||
Duration string `json:"duration"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
package response
|
||||
|
||||
import "time"
|
||||
|
||||
type FileHistoryInfo struct {
|
||||
ID uint `json:"id"`
|
||||
FileID string `json:"fileId"`
|
||||
Path string `json:"path"`
|
||||
CurrentPath string `json:"currentPath"`
|
||||
PreviousID uint `json:"previousId"`
|
||||
SourcePath string `json:"sourcePath"`
|
||||
TargetPath string `json:"targetPath"`
|
||||
FileName string `json:"fileName"`
|
||||
Extension string `json:"extension"`
|
||||
FileMode string `json:"fileMode"`
|
||||
Operation string `json:"operation"`
|
||||
Deleted bool `json:"deleted"`
|
||||
ContentSize int64 `json:"contentSize"`
|
||||
ContentSHA string `json:"contentSHA"`
|
||||
StoragePath string `json:"storagePath,omitempty"`
|
||||
Content string `json:"content,omitempty"`
|
||||
CurrentContent string `json:"currentContent,omitempty"`
|
||||
CreatedAt time.Time `json:"createdAt"`
|
||||
UpdatedAt time.Time `json:"updatedAt"`
|
||||
}
|
||||
|
||||
type FileHistorySettingInfo struct {
|
||||
Enable string `json:"enable"`
|
||||
MaxPerPath int `json:"maxPerPath"`
|
||||
DiskQuotaMB int `json:"diskQuotaMB"`
|
||||
}
|
||||
@@ -55,6 +55,7 @@ type CleanTree struct {
|
||||
Size uint64 `json:"size"`
|
||||
IsCheck bool `json:"isCheck"`
|
||||
IsRecommend bool `json:"isRecommend"`
|
||||
CanDelete bool `json:"canDelete"`
|
||||
}
|
||||
|
||||
type Clean struct {
|
||||
@@ -95,6 +96,11 @@ type TerminalAIInfo struct {
|
||||
AIRiskCommandsDefault string `json:"aiRiskCommandsDefault"`
|
||||
}
|
||||
|
||||
type FileManageAIInfo struct {
|
||||
AIStatus string `json:"aiStatus"`
|
||||
AIAccountID string `json:"aiAccountId"`
|
||||
}
|
||||
|
||||
type CommonDescription struct {
|
||||
ID string `json:"id" validate:"required"`
|
||||
Type string `json:"type" validate:"required"`
|
||||
|
||||
@@ -4,7 +4,6 @@ import "time"
|
||||
|
||||
type SSHUpdate struct {
|
||||
Key string `json:"key" validate:"required"`
|
||||
OldValue string `json:"oldValue"`
|
||||
NewValue string `json:"newValue"`
|
||||
}
|
||||
|
||||
@@ -50,6 +49,12 @@ type GenerateLoad struct {
|
||||
type SSHConf struct {
|
||||
File string `json:"file"`
|
||||
}
|
||||
|
||||
type SSHConfUpdate struct {
|
||||
Key string `json:"key" validate:"required"`
|
||||
Path string `json:"path"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
type SearchSSHLog struct {
|
||||
PageInfo
|
||||
Info string `json:"info"`
|
||||
|
||||
@@ -3,6 +3,7 @@ package model
|
||||
type Agent struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null;unique"`
|
||||
Remark string `json:"remark"`
|
||||
AgentType string `json:"agentType" gorm:"default:openclaw"`
|
||||
Provider string `json:"provider"`
|
||||
Model string `json:"model"`
|
||||
@@ -15,6 +16,7 @@ type Agent struct {
|
||||
Status string `json:"status"`
|
||||
Message string `json:"message"`
|
||||
AppInstallID uint `json:"appInstallId"`
|
||||
WebsiteID uint `json:"websiteId"`
|
||||
AccountID uint `json:"accountId"`
|
||||
ConfigPath string `json:"configPath"`
|
||||
}
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
package model
|
||||
|
||||
type DatabaseMongodb struct {
|
||||
BaseModel
|
||||
Name string `json:"name" gorm:"not null"`
|
||||
From string `json:"from" gorm:"not null;default:local"`
|
||||
MongodbName string `json:"mongodbName" gorm:"not null"`
|
||||
Username string `json:"username" gorm:"not null"`
|
||||
Password string `json:"password" gorm:"not null"`
|
||||
IsDelete bool `json:"isDelete"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package model
|
||||
|
||||
type FileHistory struct {
|
||||
BaseModel
|
||||
FileID string `json:"fileId" gorm:"not null;index:idx_file_history_file_id_created_at,priority:1;index:idx_file_history_file_id_deleted_created_at,priority:1"`
|
||||
Path string `json:"path" gorm:"not null;index:idx_file_history_path_created_at,priority:1;index:idx_file_history_path_hash_created_at,priority:1;index:idx_file_history_related_path,priority:1"`
|
||||
PathHash string `json:"pathHash" gorm:"not null;index:idx_file_history_path_created_at,priority:2;index:idx_file_history_path_hash_created_at,priority:2"`
|
||||
PreviousID uint `json:"previousId" gorm:"index"`
|
||||
SourcePath string `json:"sourcePath" gorm:"index:idx_file_history_related_path,priority:2"`
|
||||
TargetPath string `json:"targetPath" gorm:"index:idx_file_history_related_path,priority:3"`
|
||||
FileName string `json:"fileName" gorm:"not null"`
|
||||
Extension string `json:"extension"`
|
||||
FileMode string `json:"fileMode"`
|
||||
Operation string `json:"operation" gorm:"not null;index:idx_file_history_operation_created_at,priority:1"`
|
||||
Deleted bool `json:"deleted" gorm:"not null;default:false;index:idx_file_history_deleted_created_at,priority:1;index:idx_file_history_file_id_deleted_created_at,priority:2"`
|
||||
ContentSize int64 `json:"contentSize" gorm:"not null"`
|
||||
ContentSHA string `json:"contentSHA" gorm:"not null;index"`
|
||||
StoragePath string `json:"storagePath" gorm:"not null;uniqueIndex"`
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package model
|
||||
|
||||
type FileShare struct {
|
||||
BaseModel
|
||||
Path string `gorm:"not null;uniqueIndex" json:"path"`
|
||||
Token string `gorm:"not null;uniqueIndex" json:"token"`
|
||||
FileName string `gorm:"not null" json:"fileName"`
|
||||
ExpiresUnix int64 `json:"expiresUnix"`
|
||||
PasswordEnc string `json:"-"`
|
||||
PasswordSalt string `json:"passwordSalt"`
|
||||
PasswordHash string `json:"passwordHash"`
|
||||
MaxDownloads int `json:"maxDownloads"`
|
||||
DownloadCount int `json:"downloadCount"`
|
||||
}
|
||||
@@ -78,14 +78,13 @@ var catalog = map[string]Meta{
|
||||
EnvKey: "DEEPSEEK_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 128000,
|
||||
ContextWindow: 131072,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"},
|
||||
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner", Reasoning: true},
|
||||
{ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B", Reasoning: true},
|
||||
{ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
|
||||
{ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
|
||||
},
|
||||
},
|
||||
"bailian-coding-plan": {
|
||||
@@ -120,16 +119,16 @@ var catalog = map[string]Meta{
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
MaxTokens: 4096,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "ark-coding-plan/doubao-seed-2.0-code", Name: "Doubao-Seed-2.0-Code"},
|
||||
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao-Seed-Code"},
|
||||
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5", Reasoning: true},
|
||||
{ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true},
|
||||
{ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2", Reasoning: true},
|
||||
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking", Reasoning: true},
|
||||
{ID: "ark-coding-plan/ark-code-latest", Name: "Ark Coding Plan", ContextWindow: 256000, MaxTokens: 4096},
|
||||
{ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao Seed Code", ContextWindow: 256000, MaxTokens: 4096},
|
||||
{ID: "ark-coding-plan/glm-4.7", Name: "GLM 4.7 Coding", ContextWindow: 200000, MaxTokens: 4096},
|
||||
{ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi K2 Thinking", ContextWindow: 256000, MaxTokens: 4096},
|
||||
{ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi K2.5 Coding", ContextWindow: 256000, MaxTokens: 4096},
|
||||
{ID: "ark-coding-plan/doubao-seed-code-preview-251028", Name: "Doubao Seed Code Preview", ContextWindow: 256000, MaxTokens: 4096},
|
||||
},
|
||||
},
|
||||
"zai": {
|
||||
@@ -174,19 +173,18 @@ var catalog = map[string]Meta{
|
||||
Key: "xiaomi",
|
||||
DisplayName: "Xiaomi",
|
||||
Sort: 46,
|
||||
DefaultBaseURL: "https://api.xiaomimimo.com/anthropic",
|
||||
DefaultBaseURL: "https://api.xiaomimimo.com/v1",
|
||||
EnvKey: "XIAOMI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "anthropic-messages",
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 262144,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"},
|
||||
{ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"},
|
||||
{ID: "xiaomi/mimo-v2-tts", Name: "Xiaomi MiMo V2 TTS"},
|
||||
{ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"},
|
||||
{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"}},
|
||||
},
|
||||
},
|
||||
"kimi": {
|
||||
@@ -214,13 +212,14 @@ var catalog = map[string]Meta{
|
||||
DefaultBaseURL: "https://api.kimi.com/coding/",
|
||||
EnvKey: "KIMI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
APIType: "anthropic-messages",
|
||||
ContextWindow: 262144,
|
||||
MaxTokens: 32768,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "kimi-coding/k2p5", Name: "Kimi K2.5", Reasoning: true},
|
||||
{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": {
|
||||
@@ -230,17 +229,16 @@ var catalog = map[string]Meta{
|
||||
DefaultBaseURL: "https://api.openai.com/v1",
|
||||
EnvKey: "OPENAI_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
APIType: "openai-responses",
|
||||
ContextWindow: 272000,
|
||||
MaxTokens: 128000,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "openai/codex-mini-latest", Name: "Codex Mini", Reasoning: true},
|
||||
{ID: "openai/gpt-5", Name: "GPT-5", Reasoning: true},
|
||||
{ID: "openai/gpt-5-mini", Name: "GPT-5 Mini", Reasoning: true},
|
||||
{ID: "openai/gpt-5.4", Name: "GPT-5.4", Reasoning: true},
|
||||
{ID: "openai/gpt-5.3-codex", Name: "GPT-5.3-Codex", Reasoning: true},
|
||||
{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"}},
|
||||
},
|
||||
},
|
||||
"openrouter": {
|
||||
@@ -267,17 +265,17 @@ var catalog = map[string]Meta{
|
||||
DefaultBaseURL: "https://api.anthropic.com",
|
||||
EnvKey: "ANTHROPIC_API_KEY",
|
||||
Default: RuntimeDefault{
|
||||
APIType: "openai-completions",
|
||||
APIType: "anthropic-messages",
|
||||
ContextWindow: 256000,
|
||||
MaxTokens: 8192,
|
||||
Input: []string{"text", "image"},
|
||||
},
|
||||
Models: []Model{
|
||||
{ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"},
|
||||
{ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"},
|
||||
{ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet", Reasoning: true},
|
||||
{ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1", Reasoning: true},
|
||||
{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": {
|
||||
@@ -319,7 +317,7 @@ var catalog = map[string]Meta{
|
||||
}
|
||||
|
||||
func Get(key string) (Meta, bool) {
|
||||
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
return Meta{}, false
|
||||
}
|
||||
@@ -335,7 +333,7 @@ func All() map[string]Meta {
|
||||
}
|
||||
|
||||
func DefaultBaseURL(key string) (string, bool) {
|
||||
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
|
||||
meta, ok := catalog[key]
|
||||
if !ok || strings.TrimSpace(meta.DefaultBaseURL) == "" {
|
||||
return "", false
|
||||
}
|
||||
@@ -343,7 +341,7 @@ func DefaultBaseURL(key string) (string, bool) {
|
||||
}
|
||||
|
||||
func EnvKey(key string) string {
|
||||
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
@@ -351,7 +349,7 @@ func EnvKey(key string) string {
|
||||
}
|
||||
|
||||
func DisplayName(key string) string {
|
||||
meta, ok := catalog[strings.ToLower(strings.TrimSpace(key))]
|
||||
meta, ok := catalog[key]
|
||||
if !ok {
|
||||
return key
|
||||
}
|
||||
|
||||
+40
-292
@@ -5,334 +5,82 @@ import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
type OpenClawPatch struct {
|
||||
type OpenClawProviderPatch struct {
|
||||
PrimaryModel string
|
||||
Models map[string]interface{}
|
||||
}
|
||||
|
||||
type openClawModelSpec struct {
|
||||
ID string
|
||||
Name string
|
||||
Reasoning bool
|
||||
Input []string
|
||||
ContextWindow int
|
||||
MaxTokens int
|
||||
}
|
||||
|
||||
type openClawPatchSpec struct {
|
||||
PrimaryModel string
|
||||
Provider string
|
||||
ProviderKey string
|
||||
ModelID string
|
||||
APIKey string
|
||||
BaseURL string
|
||||
APIType string
|
||||
AuthHeader bool
|
||||
Model openClawModelSpec
|
||||
}
|
||||
|
||||
func BuildOpenClawPatch(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
modelName = strings.TrimSpace(modelName)
|
||||
func BuildOpenClawProviderPatch(provider, modelName, apiType, baseURL, apiKey string) (*OpenClawProviderPatch, error) {
|
||||
if modelName == "" {
|
||||
return nil, fmt.Errorf("model is required")
|
||||
}
|
||||
apiType, maxTokens, contextWindow = ResolveRuntimeParams(provider, apiType, maxTokens, contextWindow)
|
||||
modelID := modelName
|
||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
||||
modelID = parts[1]
|
||||
}
|
||||
|
||||
var spec openClawPatchSpec
|
||||
resolvedAPIType, _, _ := ResolveRuntimeParams(provider, apiType, 0, 0)
|
||||
modelID := resolveOpenClawModelID(provider, modelName)
|
||||
switch provider {
|
||||
case "deepseek":
|
||||
spec = buildDeepseekPatchSpec(modelName, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch(modelName, "deepseek", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "gemini":
|
||||
spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch("google/"+modelID, "google", modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
||||
case "moonshot", "kimi":
|
||||
spec = buildMoonshotPatchSpec(provider, modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey), nil
|
||||
case "bailian-coding-plan":
|
||||
spec = buildBailianPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch("bailian-coding-plan/"+modelID, "bailian-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "ark-coding-plan":
|
||||
spec = buildArkPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch("ark-coding-plan/"+modelID, "ark-coding-plan", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "minimax":
|
||||
spec = buildMiniMaxPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch("minimax/"+modelID, "minimax", modelID, apiKey, baseURL, "anthropic-messages", true), nil
|
||||
case "xiaomi":
|
||||
spec = buildXiaomiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch("xiaomi/"+modelID, "xiaomi", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
case "custom", "vllm":
|
||||
spec = buildCustomPatchSpec(provider, modelName, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch(provider+"/"+modelID, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
||||
case "ollama":
|
||||
spec = buildOllamaPatchSpec(modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL)
|
||||
return newOpenClawProviderPatch(modelName, "ollama", modelID, "ollama", baseURL, resolvedAPIType, false), nil
|
||||
case "kimi-coding":
|
||||
spec = buildKimiCodingPatchSpec(modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch(modelName, "kimi-coding", modelID, apiKey, baseURL, "anthropic-messages", false), nil
|
||||
case "zai":
|
||||
spec = buildZaiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
return newOpenClawProviderPatch("zai/"+modelID, "zai", modelID, apiKey, baseURL, "openai-completions", false), nil
|
||||
default:
|
||||
spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey)
|
||||
}
|
||||
return buildOpenClawPatch(spec), nil
|
||||
}
|
||||
|
||||
func buildOpenClawPatch(spec openClawPatchSpec) *OpenClawPatch {
|
||||
return &OpenClawPatch{
|
||||
PrimaryModel: spec.PrimaryModel,
|
||||
Models: providerModels(
|
||||
spec.Provider,
|
||||
spec.APIKey,
|
||||
spec.BaseURL,
|
||||
spec.APIType,
|
||||
spec.AuthHeader,
|
||||
buildOpenClawModel(spec.Model),
|
||||
),
|
||||
return newOpenClawProviderPatch(modelName, provider, modelID, apiKey, baseURL, resolvedAPIType, false), nil
|
||||
}
|
||||
}
|
||||
|
||||
func buildOpenClawModel(spec openClawModelSpec) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"id": spec.ID,
|
||||
"name": spec.Name,
|
||||
"reasoning": spec.Reasoning,
|
||||
"input": spec.Input,
|
||||
"contextWindow": spec.ContextWindow,
|
||||
"maxTokens": spec.MaxTokens,
|
||||
"cost": map[string]interface{}{},
|
||||
}
|
||||
}
|
||||
|
||||
func buildDeepseekPatchSpec(modelName string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: modelName,
|
||||
Provider: "deepseek",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "openai-completions",
|
||||
Model: openClawModelSpec{
|
||||
ID: "deepseek-chat",
|
||||
Name: "DeepSeek Chat",
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildMoonshotPatchSpec(provider, modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
configProvider := provider
|
||||
func buildMoonshotProviderPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawProviderPatch {
|
||||
providerKey := provider
|
||||
primaryModel := modelName
|
||||
if provider == "kimi" {
|
||||
configProvider = "moonshot"
|
||||
providerKey = "moonshot"
|
||||
primaryModel = "moonshot/" + modelID
|
||||
}
|
||||
return openClawPatchSpec{
|
||||
return newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, "openai-completions", false)
|
||||
}
|
||||
|
||||
func newOpenClawProviderPatch(primaryModel, providerKey, modelID, apiKey, baseURL, apiType string, authHeader bool) *OpenClawProviderPatch {
|
||||
return &OpenClawProviderPatch{
|
||||
PrimaryModel: primaryModel,
|
||||
Provider: configProvider,
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "openai-completions",
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildBailianPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: "bailian-coding-plan/" + modelID,
|
||||
Provider: "bailian-coding-plan",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "openai-completions",
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildArkPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: "ark-coding-plan/" + modelID,
|
||||
Provider: "ark-coding-plan",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "openai-completions",
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildMiniMaxPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: "minimax/" + modelID,
|
||||
Provider: "minimax",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "anthropic-messages",
|
||||
AuthHeader: true,
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildXiaomiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
normalizedID := strings.TrimSpace(modelID)
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: "xiaomi/" + normalizedID,
|
||||
Provider: "xiaomi",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "anthropic-messages",
|
||||
Model: openClawModelSpec{
|
||||
ID: normalizedID,
|
||||
Name: normalizedID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildCustomPatchSpec(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
customModelID := normalizeCustomModel(modelName)
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: provider + "/" + customModelID,
|
||||
Provider: provider,
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: strings.TrimSpace(baseURL),
|
||||
APIType: apiType,
|
||||
Model: openClawModelSpec{
|
||||
ID: customModelID,
|
||||
Name: customModelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildOllamaPatchSpec(modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: modelName,
|
||||
Provider: "ollama",
|
||||
APIKey: "ollama",
|
||||
BaseURL: strings.TrimSpace(baseURL),
|
||||
APIType: apiType,
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildKimiCodingPatchSpec(modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: modelName,
|
||||
Provider: "kimi-coding",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "anthropic-messages",
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: "Kimi for Coding",
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text", "image"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildZaiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: "zai/" + modelID,
|
||||
Provider: "zai",
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
BaseURL: baseURL,
|
||||
APIType: "openai-completions",
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func buildGenericPatchSpec(provider, modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec {
|
||||
providerName := provider
|
||||
primaryModel := modelName
|
||||
if provider == "gemini" {
|
||||
providerName = "google"
|
||||
primaryModel = "google/" + modelID
|
||||
}
|
||||
return openClawPatchSpec{
|
||||
PrimaryModel: primaryModel,
|
||||
Provider: providerName,
|
||||
APIKey: strings.TrimSpace(apiKey),
|
||||
ProviderKey: providerKey,
|
||||
ModelID: modelID,
|
||||
APIKey: apiKey,
|
||||
BaseURL: baseURL,
|
||||
APIType: apiType,
|
||||
Model: openClawModelSpec{
|
||||
ID: modelID,
|
||||
Name: modelID,
|
||||
Reasoning: reasoning,
|
||||
Input: []string{"text"},
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
},
|
||||
AuthHeader: authHeader,
|
||||
}
|
||||
}
|
||||
|
||||
func providerModels(provider, apiKey, baseURL, api string, authHeader bool, model map[string]interface{}) map[string]interface{} {
|
||||
providerConfig := map[string]interface{}{
|
||||
"apiKey": apiKey,
|
||||
"baseUrl": baseURL,
|
||||
"api": api,
|
||||
"models": []map[string]interface{}{model},
|
||||
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 authHeader {
|
||||
providerConfig["authHeader"] = true
|
||||
}
|
||||
return map[string]interface{}{
|
||||
"mode": "merge",
|
||||
"providers": map[string]interface{}{
|
||||
provider: providerConfig,
|
||||
},
|
||||
if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 {
|
||||
return parts[1]
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeCustomModel(modelName string) string {
|
||||
trim := strings.TrimSpace(modelName)
|
||||
trim = strings.TrimLeft(trim, "/")
|
||||
if parts := strings.SplitN(trim, "/", 2); len(parts) == 2 && strings.EqualFold(parts[0], "custom") {
|
||||
return strings.TrimLeft(strings.TrimSpace(parts[1]), "/")
|
||||
}
|
||||
return trim
|
||||
return modelName
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@ const (
|
||||
)
|
||||
|
||||
func SkipVerification(key string) bool {
|
||||
switch strings.ToLower(strings.TrimSpace(key)) {
|
||||
switch key {
|
||||
case "custom", "vllm", "ollama", "kimi-coding":
|
||||
return true
|
||||
default:
|
||||
@@ -62,7 +62,6 @@ func verifyTimeout() time.Duration {
|
||||
}
|
||||
|
||||
func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
|
||||
provider = strings.ToLower(strings.TrimSpace(provider))
|
||||
base := strings.TrimRight(strings.TrimSpace(baseURL), "/")
|
||||
headers := map[string]string{}
|
||||
request := VerifyRequest{Method: http.MethodGet, Headers: headers}
|
||||
@@ -117,7 +116,7 @@ func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
headers["Content-Type"] = "application/json"
|
||||
request.Body = mustJSON(map[string]interface{}{
|
||||
"model": "doubao-seed-2.0-code",
|
||||
"model": "ark-code-latest",
|
||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
||||
"max_tokens": 1,
|
||||
})
|
||||
@@ -144,24 +143,16 @@ func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
|
||||
})
|
||||
case "xiaomi":
|
||||
request.Method = http.MethodPost
|
||||
headers["x-api-key"] = apiKey
|
||||
headers["anthropic-version"] = "2023-06-01"
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
headers["Content-Type"] = "application/json"
|
||||
if strings.Contains(base, "/v1") {
|
||||
request.URL = base + "/messages"
|
||||
} else {
|
||||
request.URL = base + "/v1/messages"
|
||||
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]interface{}{{
|
||||
"role": "user",
|
||||
"content": []map[string]string{{
|
||||
"type": "text",
|
||||
"text": "test",
|
||||
}},
|
||||
}},
|
||||
"messages": []map[string]string{{"role": "user", "content": "test"}},
|
||||
})
|
||||
case "openrouter":
|
||||
headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey)
|
||||
|
||||
@@ -16,6 +16,7 @@ type IAgentRepo interface {
|
||||
DeleteByID(id uint) error
|
||||
DeleteByAppInstallID(appInstallID uint) error
|
||||
DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID uint) error
|
||||
ClearWebsiteIDByWebsiteIDWithCtx(ctx context.Context, websiteID uint) error
|
||||
List(opts ...DBOption) ([]model.Agent, error)
|
||||
}
|
||||
|
||||
@@ -66,6 +67,13 @@ func (a AgentRepo) DeleteByAppInstallIDWithCtx(ctx context.Context, appInstallID
|
||||
return getTx(ctx).Where("app_install_id = ?", appInstallID).Delete(&model.Agent{}).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) ClearWebsiteIDByWebsiteIDWithCtx(ctx context.Context, websiteID uint) error {
|
||||
if websiteID == 0 {
|
||||
return nil
|
||||
}
|
||||
return getTx(ctx).Model(&model.Agent{}).Where("website_id = ?", websiteID).Update("website_id", 0).Error
|
||||
}
|
||||
|
||||
func (a AgentRepo) List(opts ...DBOption) ([]model.Agent, error) {
|
||||
var agents []model.Agent
|
||||
if err := getDb(opts...).Find(&agents).Error; err != nil {
|
||||
|
||||
@@ -112,6 +112,24 @@ func WithByAccountID(accountID uint) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func WithByWebsiteID(websiteID uint) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if websiteID == 0 {
|
||||
return g
|
||||
}
|
||||
return g.Where("website_id = ?", websiteID)
|
||||
}
|
||||
}
|
||||
|
||||
func WithByAppInstallID(appInstallID uint) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
if appInstallID == 0 {
|
||||
return g
|
||||
}
|
||||
return g.Where("app_install_id = ?", appInstallID)
|
||||
}
|
||||
}
|
||||
|
||||
func WithByType(tp string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("`type` = ?", tp)
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
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 MongodbRepo struct{}
|
||||
|
||||
type IMongodbRepo interface {
|
||||
Get(opts ...DBOption) (model.DatabaseMongodb, error)
|
||||
WithByMongodbName(mongodbName string) DBOption
|
||||
List(opts ...DBOption) ([]model.DatabaseMongodb, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.DatabaseMongodb, error)
|
||||
Create(ctx context.Context, mongodb *model.DatabaseMongodb) error
|
||||
Delete(ctx context.Context, opts ...DBOption) error
|
||||
Update(id uint, vars map[string]interface{}) error
|
||||
DeleteLocal(ctx context.Context) error
|
||||
}
|
||||
|
||||
func NewIMongodbRepo() IMongodbRepo {
|
||||
return &MongodbRepo{}
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Get(opts ...DBOption) (model.DatabaseMongodb, error) {
|
||||
var mongodb model.DatabaseMongodb
|
||||
db := global.DB
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.First(&mongodb).Error; err != nil {
|
||||
return mongodb, err
|
||||
}
|
||||
|
||||
pass, err := encrypt.StringDecrypt(mongodb.Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodb.Name, err)
|
||||
}
|
||||
mongodb.Password = pass
|
||||
return mongodb, err
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) List(opts ...DBOption) ([]model.DatabaseMongodb, error) {
|
||||
var mongodbs []model.DatabaseMongodb
|
||||
db := global.DB.Model(&model.DatabaseMongodb{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.Find(&mongodbs).Error; err != nil {
|
||||
return mongodbs, err
|
||||
}
|
||||
for i := 0; i < len(mongodbs); i++ {
|
||||
pass, err := encrypt.StringDecrypt(mongodbs[i].Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodbs[i].Name, err)
|
||||
}
|
||||
mongodbs[i].Password = pass
|
||||
}
|
||||
return mongodbs, nil
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Page(page, size int, opts ...DBOption) (int64, []model.DatabaseMongodb, error) {
|
||||
var mongodbs []model.DatabaseMongodb
|
||||
db := global.DB.Model(&model.DatabaseMongodb{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
count := int64(0)
|
||||
db = db.Count(&count)
|
||||
if err := db.Limit(size).Offset(size * (page - 1)).Find(&mongodbs).Error; err != nil {
|
||||
return count, mongodbs, err
|
||||
}
|
||||
for i := 0; i < len(mongodbs); i++ {
|
||||
pass, err := encrypt.StringDecrypt(mongodbs[i].Password)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("decrypt mongodb db %s password failed, err: %v", mongodbs[i].Name, err)
|
||||
}
|
||||
mongodbs[i].Password = pass
|
||||
}
|
||||
return count, mongodbs, nil
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Create(ctx context.Context, mongodb *model.DatabaseMongodb) error {
|
||||
pass, err := encrypt.StringEncrypt(mongodb.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", mongodb.Name, err)
|
||||
}
|
||||
mongodb.Password = pass
|
||||
return getTx(ctx).Create(mongodb).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Delete(ctx context.Context, opts ...DBOption) error {
|
||||
return getTx(ctx, opts...).Delete(&model.DatabaseMongodb{}).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) DeleteLocal(ctx context.Context) error {
|
||||
return getTx(ctx).Where("`from` = ?", "local").Delete(&model.DatabaseMongodb{}).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) Update(id uint, vars map[string]interface{}) error {
|
||||
return global.DB.Model(&model.DatabaseMongodb{}).Where("id = ?", id).Updates(vars).Error
|
||||
}
|
||||
|
||||
func (u *MongodbRepo) WithByMongodbName(mongodbName string) DBOption {
|
||||
return func(g *gorm.DB) *gorm.DB {
|
||||
return g.Where("mongodb_name = ?", mongodbName)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileHistoryRepo struct{}
|
||||
|
||||
type IFileHistoryRepo interface {
|
||||
Get(opts ...DBOption) (model.FileHistory, error)
|
||||
Page(limit, offset int, opts ...DBOption) (int64, []model.FileHistory, error)
|
||||
Create(history *model.FileHistory) error
|
||||
Delete(opts ...DBOption) error
|
||||
DeleteByIDs(ids []uint) error
|
||||
WithByID(id uint) DBOption
|
||||
WithByFileID(fileID string) DBOption
|
||||
WithByPath(path string) DBOption
|
||||
WithByPathHash(pathHash string) DBOption
|
||||
WithByPathLike(path string) DBOption
|
||||
WithByOperation(operation string) DBOption
|
||||
WithNotOperation(operation string) DBOption
|
||||
WithByRelatedPath(path string) DBOption
|
||||
}
|
||||
|
||||
func NewIFileHistoryRepo() IFileHistoryRepo {
|
||||
return &FileHistoryRepo{}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Get(opts ...DBOption) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return item, db.First(&item).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Page(limit, offset int, opts ...DBOption) (int64, []model.FileHistory, error) {
|
||||
var total int64
|
||||
var items []model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := db.Order("created_at desc").Limit(limit).Offset(offset).Find(&items).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, items, nil
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Create(history *model.FileHistory) error {
|
||||
return global.DB.Create(history).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) Delete(opts ...DBOption) error {
|
||||
db := global.DB.Model(&model.FileHistory{})
|
||||
for _, opt := range opts {
|
||||
db = opt(db)
|
||||
}
|
||||
return db.Delete(&model.FileHistory{}).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) DeleteByIDs(ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
return global.DB.Where("id in ?", ids).Delete(&model.FileHistory{}).Error
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByID(id uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("id = ?", id)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByFileID(fileID string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("file_id = ?", fileID)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByPath(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path = ?", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByPathHash(pathHash string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path_hash = ?", pathHash)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByPathLike(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path like ?", "%"+path+"%")
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByOperation(operation string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("operation = ?", operation)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithNotOperation(operation string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("operation <> ?", operation)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileHistoryRepo) WithByRelatedPath(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path = ? OR source_path = ? OR target_path = ?", path, path, path)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,77 @@
|
||||
package repo
|
||||
|
||||
import (
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileShareRepo struct{}
|
||||
|
||||
type IFileShareRepo interface {
|
||||
Page(page, size int, opts ...DBOption) (int64, []model.FileShare, error)
|
||||
Create(fileShare *model.FileShare) error
|
||||
Save(fileShare *model.FileShare) error
|
||||
Delete(opts ...DBOption) error
|
||||
GetFirst(opts ...DBOption) (model.FileShare, error)
|
||||
All() ([]model.FileShare, error)
|
||||
WithByPath(path string) DBOption
|
||||
WithByCode(code string) DBOption
|
||||
}
|
||||
|
||||
func NewIFileShareRepo() IFileShareRepo {
|
||||
return &FileShareRepo{}
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) WithByPath(path string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("path = ?", path)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) WithByCode(code string) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("token = ?", code)
|
||||
}
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) Page(page, size int, opts ...DBOption) (int64, []model.FileShare, error) {
|
||||
var (
|
||||
items []model.FileShare
|
||||
count int64
|
||||
)
|
||||
db := getDb(opts...).Model(&model.FileShare{})
|
||||
db = db.Count(&count)
|
||||
err := db.Order("path asc").Limit(size).Offset(size * (page - 1)).Find(&items).Error
|
||||
return count, items, err
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) Create(fileShare *model.FileShare) error {
|
||||
return global.DB.Create(fileShare).Error
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) Save(fileShare *model.FileShare) error {
|
||||
return global.DB.Save(fileShare).Error
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) GetFirst(opts ...DBOption) (model.FileShare, error) {
|
||||
var item model.FileShare
|
||||
db := getDb(opts...).Model(&model.FileShare{})
|
||||
if err := db.First(&item).Error; err != nil {
|
||||
return item, err
|
||||
}
|
||||
return item, nil
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) Delete(opts ...DBOption) error {
|
||||
db := getDb(opts...).Model(&model.FileShare{})
|
||||
return db.Delete(&model.FileShare{}).Error
|
||||
}
|
||||
|
||||
func (r *FileShareRepo) All() ([]model.FileShare, error) {
|
||||
var items []model.FileShare
|
||||
if err := getDb().Order("path asc").Find(&items).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -165,6 +165,9 @@ func (h *HostRepo) SaveFirewallRecord(firewall *model.Firewall) error {
|
||||
firewall.Strategy,
|
||||
).First(&data).Error
|
||||
}
|
||||
if data.ID != 0 {
|
||||
firewall.ID = data.ID
|
||||
}
|
||||
return global.DB.Save(firewall).Error
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
type IWebsiteRepo interface {
|
||||
WithAppInstallId(appInstallId uint) DBOption
|
||||
WithAppInstallIds(appInstallIds []uint) DBOption
|
||||
WithDomain(domain string) DBOption
|
||||
WithAlias(alias string) DBOption
|
||||
WithWebsiteSSLID(sslId uint) DBOption
|
||||
@@ -48,6 +49,15 @@ func (w *WebsiteRepo) WithAppInstallId(appInstallID uint) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebsiteRepo) WithAppInstallIds(appInstallIDs []uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if len(appInstallIDs) == 0 {
|
||||
return db
|
||||
}
|
||||
return db.Where("app_install_id in (?)", appInstallIDs)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebsiteRepo) WithRuntimeID(runtimeID uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("runtime_id = ?", runtimeID)
|
||||
|
||||
@@ -13,6 +13,7 @@ type WebsiteDomainRepo struct {
|
||||
|
||||
type IWebsiteDomainRepo interface {
|
||||
WithWebsiteId(websiteId uint) DBOption
|
||||
WithWebsiteIds(websiteIds []uint) DBOption
|
||||
WithPort(port int) DBOption
|
||||
WithDomain(domain string) DBOption
|
||||
WithDomainLike(domain string) DBOption
|
||||
@@ -36,6 +37,15 @@ func (w WebsiteDomainRepo) WithWebsiteId(websiteId uint) DBOption {
|
||||
}
|
||||
}
|
||||
|
||||
func (w WebsiteDomainRepo) WithWebsiteIds(websiteIds []uint) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
if len(websiteIds) == 0 {
|
||||
return db
|
||||
}
|
||||
return db.Where("website_id in (?)", websiteIds)
|
||||
}
|
||||
}
|
||||
|
||||
func (w WebsiteDomainRepo) WithPort(port int) DBOption {
|
||||
return func(db *gorm.DB) *gorm.DB {
|
||||
return db.Where("port = ?", port)
|
||||
|
||||
+294
-46
@@ -7,6 +7,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -19,17 +20,28 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/docker"
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/docker/docker/api/types/container"
|
||||
"gopkg.in/yaml.v3"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type IAgentService interface {
|
||||
Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
|
||||
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
|
||||
DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error)
|
||||
Delete(req dto.AgentDeleteReq) error
|
||||
ResetToken(req dto.AgentTokenResetReq) error
|
||||
UpdateRemark(req dto.AgentRemarkUpdateReq) error
|
||||
BindWebsite(req dto.AgentWebsiteBindReq) error
|
||||
UnbindWebsite(req dto.AgentIDReq) error
|
||||
GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error)
|
||||
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)
|
||||
RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error
|
||||
DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error
|
||||
GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error)
|
||||
GetProviders() ([]dto.ProviderInfo, error)
|
||||
GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error)
|
||||
@@ -42,6 +54,16 @@ type IAgentService interface {
|
||||
SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error)
|
||||
UpdateSkill(req dto.AgentSkillUpdateReq) error
|
||||
InstallSkill(req dto.AgentSkillInstallReq) error
|
||||
UninstallSkill(req dto.AgentSkillUninstallReq) error
|
||||
|
||||
CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error)
|
||||
DeleteRole(req dto.AgentRoleDeleteReq) error
|
||||
BindRole(req dto.AgentRoleBindReq) error
|
||||
UnbindRole(req dto.AgentRoleBindReq) error
|
||||
GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error)
|
||||
GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error)
|
||||
GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error)
|
||||
UpdateRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesUpdateReq) error
|
||||
|
||||
CreateAccount(req dto.AgentAccountCreateReq) error
|
||||
UpdateAccount(req dto.AgentAccountUpdateReq) error
|
||||
@@ -64,16 +86,20 @@ type IAgentService interface {
|
||||
UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error
|
||||
GetDingTalkConfig(req dto.AgentIDReq) (*dto.AgentDingTalkConfig, error)
|
||||
UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error
|
||||
GetWeixinConfig(req dto.AgentIDReq) (*dto.AgentWeixinConfig, error)
|
||||
LoginWeixinChannel(req dto.AgentWeixinLoginReq) error
|
||||
GetQQBotConfig(req dto.AgentIDReq) (*dto.AgentQQBotConfig, error)
|
||||
UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error
|
||||
DeleteChannelConfig(req dto.AgentChannelDeleteReq) error
|
||||
InstallPlugin(req dto.AgentPluginInstallReq) error
|
||||
UpgradePlugin(req dto.AgentPluginUpgradeReq) error
|
||||
UninstallPlugin(req dto.AgentPluginUninstallReq) error
|
||||
CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error)
|
||||
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
|
||||
}
|
||||
|
||||
const (
|
||||
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome"
|
||||
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser"
|
||||
defaultBrowserProfile = "openclaw"
|
||||
defaultUserTimezone = "Asia/Shanghai"
|
||||
defaultToolsProfile = "full"
|
||||
@@ -134,15 +160,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
var account *model.AgentAccount
|
||||
var installHooks *appInstallHooks
|
||||
|
||||
if agentType == constant.AppOpenclaw {
|
||||
var err error
|
||||
allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
return nil, fmt.Errorf("allowed origins is required")
|
||||
}
|
||||
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
|
||||
if req.AccountID == 0 {
|
||||
return nil, buserr.New("ErrAgentAccountRequired")
|
||||
}
|
||||
@@ -154,7 +172,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
return nil, buserr.New("ErrAgentAccountNotVerified")
|
||||
}
|
||||
provider = account.Provider
|
||||
baseURL = strings.TrimSpace(account.BaseURL)
|
||||
baseURL = account.BaseURL
|
||||
resolvedRuntime, err := resolveOpenclawAccountModelRuntimeByID(account, req.Model)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -166,6 +184,17 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
runtimeModel = resolvedRuntime.PrimaryModel
|
||||
apiKey = account.APIKey
|
||||
accountID = account.ID
|
||||
}
|
||||
|
||||
if agentType == constant.AppOpenclaw {
|
||||
var err error
|
||||
allowedOrigins, err = normalizeAllowedOrigins(req.AllowedOrigins)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(allowedOrigins) == 0 {
|
||||
return nil, fmt.Errorf("allowed origins is required")
|
||||
}
|
||||
token = strings.TrimSpace(req.Token)
|
||||
if token == "" {
|
||||
token = generateToken()
|
||||
@@ -175,6 +204,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
return prepareOpenclawInstallFiles(appInstall, account, storedModel, token, allowedOrigins)
|
||||
},
|
||||
}
|
||||
} else if agentType == constant.AppHermesAgent {
|
||||
installHooks = &appInstallHooks{
|
||||
AfterCopyData: func(appInstall *model.AppInstall) error {
|
||||
return prepareHermesInstallFiles(appInstall, account, storedModel)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
params := map[string]interface{}{
|
||||
@@ -182,12 +217,8 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
constant.MemoryLimit: "0",
|
||||
constant.HostIP: "",
|
||||
}
|
||||
setAgentWebUIParams(params, agentType, detail.Version, req.WebUIPort)
|
||||
if agentType == constant.AppOpenclaw {
|
||||
if isOpenclawHTTPSWindowVersion(detail.Version) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = req.WebUIPort
|
||||
} else {
|
||||
params["PANEL_APP_PORT_HTTP"] = req.WebUIPort
|
||||
}
|
||||
if allowedOrigin := firstAllowedOrigin(allowedOrigins); allowedOrigin != "" {
|
||||
params["ALLOWED_ORIGIN"] = allowedOrigin
|
||||
}
|
||||
@@ -199,8 +230,6 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
params["BASE_URL"] = baseURL
|
||||
params["API_KEY"] = apiKey
|
||||
params["OPENCLAW_GATEWAY_TOKEN"] = token
|
||||
} else {
|
||||
params["PANEL_APP_PORT_HTTP"] = req.WebUIPort
|
||||
}
|
||||
|
||||
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
|
||||
@@ -232,8 +261,12 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
if agentType == constant.AppOpenclaw {
|
||||
configPath = path.Join(appInstall.GetPath(), "data", "conf", "openclaw.json")
|
||||
}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
configPath = path.Join(appInstall.GetPath(), "data", "config.yaml")
|
||||
}
|
||||
agent := &model.Agent{
|
||||
Name: req.Name,
|
||||
Remark: req.Remark,
|
||||
AgentType: agentType,
|
||||
Provider: provider,
|
||||
Model: storedModel,
|
||||
@@ -257,6 +290,14 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
|
||||
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
|
||||
params["PANEL_APP_PORT_HTTPS"] = webUIPort
|
||||
return
|
||||
}
|
||||
params["PANEL_APP_PORT_HTTP"] = webUIPort
|
||||
}
|
||||
|
||||
func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) {
|
||||
var opts []repo.DBOption
|
||||
if strings.TrimSpace(req.Info) != "" {
|
||||
@@ -267,13 +308,22 @@ func (a AgentService) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, erro
|
||||
return 0, nil, err
|
||||
}
|
||||
items := make([]dto.AgentItem, 0, len(list))
|
||||
appInstalls := make([]model.AppInstall, 0, len(list))
|
||||
for _, item := range list {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(item.AppInstallID))
|
||||
appInstalls = append(appInstalls, appInstall)
|
||||
}
|
||||
syncAgentAppInstalls(appInstalls)
|
||||
for index, item := range list {
|
||||
appInstall := appInstalls[index]
|
||||
envMap := readInstallEnv(appInstall.Env)
|
||||
agentItem := buildAgentItem(&item, &appInstall, envMap)
|
||||
agentItem.Upgradable = checkAgentUpgradable(appInstall)
|
||||
items = append(items, agentItem)
|
||||
}
|
||||
if err := hydrateAgentWebsiteItems(items); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return count, items, nil
|
||||
}
|
||||
|
||||
@@ -282,6 +332,13 @@ func (a AgentService) Delete(req dto.AgentDeleteReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resources, err := a.deleteCheckByAgent(agent)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(resources) > 0 {
|
||||
return buserr.New("ErrAgentWebsiteBound")
|
||||
}
|
||||
if agent.AppInstallID == 0 {
|
||||
return agentRepo.DeleteByID(agent.ID)
|
||||
}
|
||||
@@ -297,6 +354,58 @@ func (a AgentService) Delete(req dto.AgentDeleteReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return a.deleteCheckByAgent(agent)
|
||||
}
|
||||
|
||||
func (a AgentService) deleteCheckByAgent(agent *model.Agent) ([]dto.AppResource, error) {
|
||||
if agent == nil || agent.WebsiteID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID))
|
||||
if err != nil {
|
||||
if err == gorm.ErrRecordNotFound {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
websiteName, err := loadAgentWebsiteResourceName(website)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return []dto.AppResource{{Type: "website", Name: websiteName}}, nil
|
||||
}
|
||||
|
||||
func syncAgentAppInstalls(appInstalls []model.AppInstall) {
|
||||
if len(appInstalls) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
var containersMap map[string]container.Summary
|
||||
cli, err := docker.NewClient()
|
||||
if err == nil {
|
||||
defer cli.Close()
|
||||
containers, err := cli.ListAllContainers()
|
||||
if err == nil {
|
||||
containersMap = make(map[string]container.Summary, len(containers))
|
||||
for _, contain := range containers {
|
||||
containersMap[contain.Names[0]] = contain
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for index := range appInstalls {
|
||||
if appInstalls[index].ID == 0 || doNotNeedSync(appInstalls[index]) {
|
||||
continue
|
||||
}
|
||||
synAppInstall(containersMap, &appInstalls[index], false)
|
||||
}
|
||||
}
|
||||
|
||||
func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error {
|
||||
agent, err := loadOpenclawAgentByID(req.ID)
|
||||
if err != nil {
|
||||
@@ -323,8 +432,68 @@ func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error {
|
||||
return agentRepo.Save(agent)
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateRemark(req dto.AgentRemarkUpdateReq) error {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
agent.Remark = req.Remark
|
||||
return agentRepo.Save(agent)
|
||||
}
|
||||
|
||||
func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model, err := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentModelConfig{
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: []string{},
|
||||
}, nil
|
||||
}
|
||||
agent, _, conf, err := a.loadOpenclawAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
models, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
model := extractOpenclawPrimaryModelID(conf, account, models)
|
||||
if model == "" {
|
||||
model = agent.Model
|
||||
}
|
||||
return &dto.AgentModelConfig{
|
||||
AccountID: agent.AccountID,
|
||||
Model: model,
|
||||
Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error {
|
||||
agent, err := loadOpenclawAgentByID(req.AgentID)
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -339,8 +508,21 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
|
||||
modelName := resolvedRuntime.StoredModel
|
||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
||||
confDir := path.Dir(agent.ConfigPath)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil {
|
||||
return err
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
agent.Provider = account.Provider
|
||||
agent.Model = modelName
|
||||
@@ -442,6 +624,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
if req.SyncAgents {
|
||||
if err := a.syncAgentsByAccount(account); err != nil {
|
||||
return err
|
||||
@@ -582,6 +765,7 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return a.syncAgentsByAccount(account)
|
||||
}
|
||||
|
||||
@@ -614,6 +798,7 @@ func (a AgentService) DeleteAccountModel(req dto.AgentAccountModelDeleteReq) err
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return a.syncAgentsByAccount(account)
|
||||
}
|
||||
|
||||
@@ -633,10 +818,16 @@ func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error {
|
||||
if exists, _ := agentRepo.GetFirst(repo.WithByAccountID(req.ID)); exists != nil && exists.ID > 0 {
|
||||
return buserr.New("ErrAgentAccountBound")
|
||||
}
|
||||
if aiStatus, _ := settingRepo.GetValueByKey("AIStatus"); strings.EqualFold(strings.TrimSpace(aiStatus), constant.StatusEnable) {
|
||||
if aiAccountID, _ := settingRepo.GetValueByKey("AIAccountID"); strings.TrimSpace(aiAccountID) == strconv.FormatUint(uint64(req.ID), 10) {
|
||||
return buserr.New("ErrTerminalAIAccountInUse")
|
||||
}
|
||||
}
|
||||
if err := agentAccountModelRepo.Delete(repo.WithByAccountID(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateTerminalRuntimeCache()
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return agentAccountRepo.DeleteByID(req.ID)
|
||||
}
|
||||
|
||||
@@ -684,6 +875,17 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentOtherConfig{
|
||||
UserTimezone: cfg.Timezone,
|
||||
BrowserEnabled: true,
|
||||
NPMRegistry: "https://registry.npmjs.org/",
|
||||
}, nil
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -701,6 +903,19 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
|
||||
return err
|
||||
}
|
||||
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||
InstallId: install.ID,
|
||||
Operate: constant.Restart,
|
||||
})
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -715,11 +930,14 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
|
||||
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
return setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry)
|
||||
if err := setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) {
|
||||
agent, _, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, _, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -731,12 +949,11 @@ func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfi
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
||||
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var payload interface{}
|
||||
if err := json.Unmarshal([]byte(req.Content), &payload); err != nil {
|
||||
if err := validateAgentConfigFileContent(agent.AgentType, req.Content); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(agent.ConfigPath)
|
||||
@@ -746,14 +963,34 @@ func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
|
||||
if err := os.WriteFile(agent.ConfigPath, []byte(req.Content), info.Mode()); err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if cfg.Model.Default != "" {
|
||||
agent.Model = cfg.Model.Default
|
||||
if err := agentRepo.Save(agent); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||
InstallId: install.ID,
|
||||
Operate: constant.Restart,
|
||||
})
|
||||
}
|
||||
|
||||
func validateAgentConfigFileContent(agentType, content string) error {
|
||||
var payload interface{}
|
||||
if agentType == constant.AppHermesAgent {
|
||||
return yaml.Unmarshal([]byte(content), &payload)
|
||||
}
|
||||
return json.Unmarshal([]byte(content), &payload)
|
||||
}
|
||||
|
||||
func getOpenclawNPMRegistry(containerName string) (string, error) {
|
||||
registry, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut("docker exec %s npm get registry", 20*time.Second, containerName)
|
||||
registry, err := runDockerExecWithStdout(20*time.Second, containerName, "npm", "get", "registry")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -788,8 +1025,8 @@ func (a AgentService) loadOpenclawAgentAndInstall(agentID uint) (*model.Agent, *
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
return nil, nil, fmt.Errorf("copaw does not support")
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return agent, install, nil
|
||||
}
|
||||
@@ -811,8 +1048,8 @@ func (a AgentService) loadOpenclawAgentConfig(agentID uint) (*model.Agent, *mode
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
return nil, nil, nil, fmt.Errorf("copaw does not support")
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, nil, nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return agent, install, conf, nil
|
||||
}
|
||||
@@ -840,29 +1077,40 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error {
|
||||
if len(accountModels) == 0 {
|
||||
return nil
|
||||
}
|
||||
baseURL := resolveAccountBaseURL(account)
|
||||
for _, agent := range agents {
|
||||
confDir := path.Dir(agent.ConfigPath)
|
||||
modelName := strings.TrimSpace(agent.Model)
|
||||
var selectedAccountModel dto.AgentAccountModel
|
||||
if modelName != "" {
|
||||
selectedAccountModel, err = requireAgentAccountModelForProvider(account.Provider, accountModels, modelName)
|
||||
if err != nil {
|
||||
return buserr.WithName("ErrAgentModelInUse", agent.Name)
|
||||
}
|
||||
} else {
|
||||
selectedAccountModel = accountModels[0]
|
||||
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, agent.Model)
|
||||
if err != nil {
|
||||
return buserr.WithName("ErrAgentModelInUse", agent.Name)
|
||||
}
|
||||
resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
modelName = resolvedRuntime.StoredModel
|
||||
modelName := resolvedRuntime.StoredModel
|
||||
apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil {
|
||||
return err
|
||||
confDir := path.Dir(agent.ConfigPath)
|
||||
switch agent.AgentType {
|
||||
case constant.AppOpenclaw:
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID)
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppHermesAgent:
|
||||
cfg, err := readHermesConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := writeHermesConfig(confDir, account, modelName, cfg.Timezone); err != nil {
|
||||
return err
|
||||
}
|
||||
default:
|
||||
continue
|
||||
}
|
||||
agent.BaseURL = baseURL
|
||||
agent.BaseURL = account.BaseURL
|
||||
agent.APIKey = account.APIKey
|
||||
agent.Provider = account.Provider
|
||||
agent.Model = modelName
|
||||
|
||||
@@ -0,0 +1,533 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
var agentMarkdownFileNames = []string{"AGENTS.md", "SOUL.md", "USER.md", "IDENTITY.md", "TOOLS.md", "HEARTBEAT.md", "BOOT.md", "BOOTSTRAP.md"}
|
||||
|
||||
func (a AgentService) CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCreateResp, error) {
|
||||
_, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
args := []string{"exec", install.ContainerName, "openclaw", "agents", "add", req.Name}
|
||||
workspace := "/home/node/.openclaw/workspace-agent_" + req.Name
|
||||
agentDir := "/home/node/.openclaw/agents/" + req.Name
|
||||
args = append(args, "--workspace", workspace)
|
||||
if model := strings.TrimSpace(req.Model); model != "" {
|
||||
args = append(args, "--model", model)
|
||||
}
|
||||
for _, binding := range req.Bindings {
|
||||
channel := binding.Channel
|
||||
if channel == "" {
|
||||
continue
|
||||
}
|
||||
if accountID := binding.AccountID; accountID != "" {
|
||||
channel = channel + ":" + accountID
|
||||
}
|
||||
args = append(args, "--bind", channel)
|
||||
}
|
||||
args = append(args, "--agent-dir", agentDir)
|
||||
args = append(args, "--non-interactive", "--json")
|
||||
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute))
|
||||
output, err := mgr.RunWithStdout("docker", args...)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentRoleCreateResp{Output: strings.TrimSpace(output)}, nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetConfiguredAgents(req dto.AgentConfiguredAgentsReq) ([]dto.AgentConfiguredAgentItem, error) {
|
||||
agent, _, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
agents, ok := conf["agents"].(map[string]interface{})
|
||||
if !ok {
|
||||
return []dto.AgentConfiguredAgentItem{}, nil
|
||||
}
|
||||
rawList, ok := agents["list"].([]interface{})
|
||||
if !ok {
|
||||
return []dto.AgentConfiguredAgentItem{}, nil
|
||||
}
|
||||
result := make([]dto.AgentConfiguredAgentItem, 0, len(rawList))
|
||||
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
|
||||
for _, item := range rawList {
|
||||
record, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
configured := extractConfiguredAgentItem(baseDir, record)
|
||||
if strings.EqualFold(configured.ID, "main") {
|
||||
continue
|
||||
}
|
||||
result = append(result, configured)
|
||||
}
|
||||
applyConfiguredAgentBindings(result, conf["bindings"])
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a AgentService) GetRoleChannels(req dto.AgentRoleChannelsReq) ([]dto.AgentRoleChannelItem, error) {
|
||||
_, _, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
channels, ok := conf["channels"].(map[string]interface{})
|
||||
if !ok || len(channels) == 0 {
|
||||
return []dto.AgentRoleChannelItem{}, nil
|
||||
}
|
||||
boundBindings := loadBoundChannelBindings(conf["bindings"])
|
||||
result := make([]dto.AgentRoleChannelItem, 0, len(channels))
|
||||
for key := range channels {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
accountIDs := extractRoleChannelAccountIDs(conf, key)
|
||||
availableAccountIDs := filterAvailableChannelAccountIDs(boundBindings, key, accountIDs)
|
||||
result = append(result, dto.AgentRoleChannelItem{
|
||||
Name: key,
|
||||
Bound: isRoleChannelFullyBound(boundBindings, key, accountIDs, availableAccountIDs),
|
||||
AccountIDs: availableAccountIDs,
|
||||
})
|
||||
}
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Name < result[j].Name
|
||||
})
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteRole(req dto.AgentRoleDeleteReq) error {
|
||||
agent, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
|
||||
if req.ID == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
target, ok := findConfiguredAgentByID(baseDir, conf, req.ID)
|
||||
if !ok {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
args := []string{"exec", install.ContainerName, "openclaw", "agents", "delete", req.ID, "--force"}
|
||||
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute))
|
||||
if _, err = mgr.RunWithStdout("docker", args...); err != nil {
|
||||
return err
|
||||
}
|
||||
if target.Workspace != "" {
|
||||
if err := os.RemoveAll(target.Workspace); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if target.AgentDir != "" {
|
||||
if err := os.RemoveAll(target.AgentDir); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) BindRole(req dto.AgentRoleBindReq) error {
|
||||
return a.operateRoleBinding(req, "bind")
|
||||
}
|
||||
|
||||
func (a AgentService) UnbindRole(req dto.AgentRoleBindReq) error {
|
||||
return a.operateRoleBinding(req, "unbind")
|
||||
}
|
||||
|
||||
func (a AgentService) operateRoleBinding(req dto.AgentRoleBindReq, action string) error {
|
||||
agent, install, conf, err := a.loadAgentConfig(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
|
||||
if req.ID == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if _, ok := findConfiguredAgentByID(baseDir, conf, req.ID); !ok {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
binding := formatRoleBinding(req.Channel, req.AccountID)
|
||||
if binding == "" {
|
||||
return buserr.New("ErrInvalidParams")
|
||||
}
|
||||
args := []string{
|
||||
"exec",
|
||||
install.ContainerName,
|
||||
"openclaw",
|
||||
"agents",
|
||||
action,
|
||||
"--agent",
|
||||
req.ID,
|
||||
"--bind",
|
||||
binding,
|
||||
}
|
||||
args = append(args, "--json")
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute))
|
||||
_, err = mgr.RunWithStdout("docker", args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) GetRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesReq) ([]dto.AgentRoleMarkdownFileItem, error) {
|
||||
agent, err := loadOpenclawAgentByID(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
|
||||
workspaceDir, err := resolveMarkdownWorkspaceDir(baseDir, req.Workspace)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]dto.AgentRoleMarkdownFileItem, 0, len(agentMarkdownFileNames))
|
||||
for _, name := range agentMarkdownFileNames {
|
||||
item := dto.AgentRoleMarkdownFileItem{
|
||||
Name: name,
|
||||
}
|
||||
content, readErr := os.ReadFile(path.Join(workspaceDir, name))
|
||||
if readErr == nil {
|
||||
item.Content = string(content)
|
||||
} else if !os.IsNotExist(readErr) {
|
||||
return nil, readErr
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (a AgentService) UpdateRoleMarkdownFiles(req dto.AgentRoleMarkdownFilesUpdateReq) error {
|
||||
agent, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
baseDir := path.Join(global.Dir.AppInstallDir, agent.AgentType, agent.Name, "data")
|
||||
dirPath, err := resolveMarkdownWorkspaceDir(baseDir, req.Workspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = os.MkdirAll(dirPath, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range req.Files {
|
||||
file := path.Join(dirPath, item.Name)
|
||||
if err = os.WriteFile(file, []byte(item.Content), 0644); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if req.Restart {
|
||||
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
|
||||
InstallId: install.ID,
|
||||
Operate: constant.Restart,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractConfiguredAgentItem(installDir string, record map[string]interface{}) dto.AgentConfiguredAgentItem {
|
||||
item := dto.AgentConfiguredAgentItem{Bindings: []dto.AgentRoleBinding{}}
|
||||
if id, ok := record["id"].(string); ok {
|
||||
item.ID = id
|
||||
} else if id, ok := record["agentId"].(string); ok {
|
||||
item.ID = id
|
||||
}
|
||||
if name, ok := record["name"].(string); ok {
|
||||
item.Name = name
|
||||
}
|
||||
if workspace, ok := record["workspace"].(string); ok {
|
||||
item.Workspace = resolveRoleDir(installDir, workspace)
|
||||
}
|
||||
if model, ok := record["model"].(string); ok {
|
||||
item.Model = model
|
||||
}
|
||||
if agentDir, ok := record["agentDir"].(string); ok {
|
||||
item.AgentDir = agentDir
|
||||
} else if agentDir, ok := record["agent_dir"].(string); ok {
|
||||
item.AgentDir = agentDir
|
||||
}
|
||||
item.AgentDir = resolveRoleDir(installDir, item.AgentDir)
|
||||
return item
|
||||
}
|
||||
|
||||
func findConfiguredAgentByID(baseDir string, conf map[string]interface{}, id string) (dto.AgentConfiguredAgentItem, bool) {
|
||||
agents, ok := conf["agents"].(map[string]interface{})
|
||||
if !ok {
|
||||
return dto.AgentConfiguredAgentItem{}, false
|
||||
}
|
||||
rawList, ok := agents["list"].([]interface{})
|
||||
if !ok {
|
||||
return dto.AgentConfiguredAgentItem{}, false
|
||||
}
|
||||
for _, item := range rawList {
|
||||
record, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
configured := extractConfiguredAgentItem(baseDir, record)
|
||||
if strings.EqualFold(configured.ID, id) {
|
||||
return configured, true
|
||||
}
|
||||
}
|
||||
return dto.AgentConfiguredAgentItem{}, false
|
||||
}
|
||||
|
||||
func formatRoleBinding(channel, accountID string) string {
|
||||
if channel == "" {
|
||||
return ""
|
||||
}
|
||||
if accountID == "" {
|
||||
return channel
|
||||
}
|
||||
return channel + ":" + accountID
|
||||
}
|
||||
|
||||
func applyConfiguredAgentBindings(agents []dto.AgentConfiguredAgentItem, value interface{}) {
|
||||
bindings, ok := value.([]interface{})
|
||||
if !ok || len(agents) == 0 {
|
||||
return
|
||||
}
|
||||
indexByID := make(map[string]int, len(agents))
|
||||
indexByName := make(map[string]int, len(agents))
|
||||
for i, agent := range agents {
|
||||
if agent.ID != "" {
|
||||
indexByID[agent.ID] = i
|
||||
}
|
||||
if agent.Name != "" {
|
||||
indexByName[agent.Name] = i
|
||||
}
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
record, ok := binding.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if bindingType, _ := record["type"].(string); !strings.EqualFold(bindingType, "route") {
|
||||
continue
|
||||
}
|
||||
targetID, _ := record["agentId"].(string)
|
||||
if targetID == "" {
|
||||
continue
|
||||
}
|
||||
match, ok := record["match"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
channel, _ := match["channel"].(string)
|
||||
if channel == "" {
|
||||
continue
|
||||
}
|
||||
index, ok := indexByID[targetID]
|
||||
if !ok {
|
||||
index, ok = indexByName[targetID]
|
||||
}
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
accountID, _ := match["accountId"].(string)
|
||||
if accountID == "" {
|
||||
accountID, _ = record["accountId"].(string)
|
||||
}
|
||||
agents[index].Bindings = append(agents[index].Bindings, dto.AgentRoleBinding{
|
||||
Channel: channel,
|
||||
AccountID: accountID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func loadBoundChannelBindings(value interface{}) map[string]map[string]struct{} {
|
||||
result := make(map[string]map[string]struct{})
|
||||
bindings, ok := value.([]interface{})
|
||||
if !ok {
|
||||
return result
|
||||
}
|
||||
for _, binding := range bindings {
|
||||
record, ok := binding.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if bindingType, _ := record["type"].(string); !strings.EqualFold(bindingType, "route") {
|
||||
continue
|
||||
}
|
||||
match, ok := record["match"].(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
channel, _ := match["channel"].(string)
|
||||
if channel == "" {
|
||||
continue
|
||||
}
|
||||
accountID, _ := match["accountId"].(string)
|
||||
if accountID == "" {
|
||||
accountID, _ = record["accountId"].(string)
|
||||
}
|
||||
if _, ok := result[channel]; !ok {
|
||||
result[channel] = make(map[string]struct{})
|
||||
}
|
||||
result[channel][accountID] = struct{}{}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func extractRoleChannelAccountIDs(conf map[string]interface{}, channel string) []string {
|
||||
switch channel {
|
||||
case "feishu":
|
||||
config := extractFeishuConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "telegram":
|
||||
config := extractTelegramConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "discord":
|
||||
config := extractDiscordConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "qqbot":
|
||||
config := extractQQBotConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "dingtalk-connector":
|
||||
config := extractDingTalkConfig(conf)
|
||||
accountIDs := make([]string, 0, len(config.Bots))
|
||||
for _, item := range config.Bots {
|
||||
if accountID := item.AccountID; accountID != "" {
|
||||
accountIDs = append(accountIDs, accountID)
|
||||
}
|
||||
}
|
||||
sort.Strings(accountIDs)
|
||||
return accountIDs
|
||||
case "wecom":
|
||||
return []string{}
|
||||
default:
|
||||
return extractRawChannelAccountIDs(getChannelConfig(conf, channel))
|
||||
}
|
||||
}
|
||||
|
||||
func filterAvailableChannelAccountIDs(bindings map[string]map[string]struct{}, channel string, accountIDs []string) []string {
|
||||
channelBindings, ok := bindings[channel]
|
||||
if !ok || len(channelBindings) == 0 {
|
||||
return append([]string(nil), accountIDs...)
|
||||
}
|
||||
if _, ok := channelBindings[""]; ok {
|
||||
return []string{}
|
||||
}
|
||||
result := make([]string, 0, len(accountIDs))
|
||||
for _, accountID := range accountIDs {
|
||||
if _, ok := channelBindings[accountID]; ok {
|
||||
continue
|
||||
}
|
||||
result = append(result, accountID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func isRoleChannelFullyBound(bindings map[string]map[string]struct{}, channel string, allAccountIDs, availableAccountIDs []string) bool {
|
||||
channelBindings, ok := bindings[channel]
|
||||
if !ok || len(channelBindings) == 0 {
|
||||
return false
|
||||
}
|
||||
if _, ok := channelBindings[""]; ok {
|
||||
return true
|
||||
}
|
||||
if len(allAccountIDs) == 0 {
|
||||
return true
|
||||
}
|
||||
return len(availableAccountIDs) == 0
|
||||
}
|
||||
|
||||
func extractRawChannelAccountIDs(channel map[string]interface{}) []string {
|
||||
if len(channel) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
accounts, ok := channel["accounts"].(map[string]interface{})
|
||||
if !ok || len(accounts) == 0 {
|
||||
return []string{}
|
||||
}
|
||||
result := make([]string, 0, len(accounts))
|
||||
for key := range accounts {
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
result = append(result, key)
|
||||
}
|
||||
sort.Strings(result)
|
||||
return result
|
||||
}
|
||||
|
||||
func resolveRoleDir(installDir, workspace string) string {
|
||||
if workspace == "" {
|
||||
return ""
|
||||
}
|
||||
if strings.HasPrefix(workspace, "/home/node/.openclaw/workspace/") {
|
||||
return strings.ReplaceAll(workspace, "/home/node/.openclaw", installDir)
|
||||
}
|
||||
return strings.ReplaceAll(workspace, "/home/node/.openclaw", path.Join(installDir, "conf"))
|
||||
}
|
||||
|
||||
func resolveMarkdownWorkspaceDir(installDir, workspace string) (string, error) {
|
||||
if workspace == "" {
|
||||
return "", buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
confDir := path.Join(installDir, "conf")
|
||||
allowedOpenclawPrefixes := []string{
|
||||
"/home/node/.openclaw/workspace/",
|
||||
"/home/node/.openclaw/workspace-agent_",
|
||||
}
|
||||
for _, prefix := range allowedOpenclawPrefixes {
|
||||
if strings.HasPrefix(workspace, prefix) {
|
||||
return resolveRoleDir(installDir, workspace), nil
|
||||
}
|
||||
}
|
||||
|
||||
cleanWorkspace := path.Clean(workspace)
|
||||
cleanConfDir := path.Clean(confDir)
|
||||
if cleanWorkspace == cleanConfDir || strings.HasPrefix(cleanWorkspace, cleanConfDir+"/") {
|
||||
return cleanWorkspace, nil
|
||||
}
|
||||
return "", buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
+1184
-200
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,731 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"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/joho/godotenv"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const hermesWorkspaceDir = "/opt/data/workspace"
|
||||
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
|
||||
|
||||
type hermesConfig struct {
|
||||
Model hermesModelConfig `yaml:"model"`
|
||||
Terminal hermesTerminalConfig `yaml:"terminal"`
|
||||
Timezone string `yaml:"timezone,omitempty"`
|
||||
}
|
||||
|
||||
type hermesModelConfig struct {
|
||||
Default string `yaml:"default"`
|
||||
Provider string `yaml:"provider"`
|
||||
BaseURL string `yaml:"base_url,omitempty"`
|
||||
APIKey string `yaml:"api_key,omitempty"`
|
||||
}
|
||||
|
||||
type hermesTerminalConfig struct {
|
||||
Backend string `yaml:"backend"`
|
||||
Cwd string `yaml:"cwd"`
|
||||
}
|
||||
|
||||
type hermesEnvEntry struct {
|
||||
Key string
|
||||
Value string
|
||||
}
|
||||
|
||||
func buildHermesDockerExecCommandArgs(containerName, command string, commandArgs ...string) []string {
|
||||
args := []string{"exec", "-u", "hermes", containerName, command}
|
||||
return append(args, commandArgs...)
|
||||
}
|
||||
|
||||
func buildHermesDockerExecArgs(containerName string, hermesArgs ...string) []string {
|
||||
return buildHermesDockerExecCommandArgs(containerName, "hermes", hermesArgs...)
|
||||
}
|
||||
|
||||
func writeHermesConfig(confDir string, account *model.AgentAccount, modelName string, timezone string) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
if account == nil {
|
||||
return fmt.Errorf("account is required")
|
||||
}
|
||||
if strings.TrimSpace(modelName) == "" {
|
||||
return fmt.Errorf("model is required")
|
||||
}
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(confDir) {
|
||||
if err := fileOp.CreateDir(confDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
provider := resolveHermesProvider(account.Provider)
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
model := map[string]interface{}{
|
||||
"default": resolveHermesModel(account.Provider, provider, modelName),
|
||||
"provider": provider,
|
||||
"base_url": account.BaseURL,
|
||||
}
|
||||
if provider == "custom" && account.APIKey != "" {
|
||||
model["api_key"] = "${CUSTOM_API_KEY}"
|
||||
}
|
||||
cfg["model"] = model
|
||||
cfg["terminal"] = map[string]interface{}{
|
||||
"backend": "local",
|
||||
"cwd": hermesWorkspaceDir,
|
||||
}
|
||||
if timezone != "" {
|
||||
cfg["timezone"] = timezone
|
||||
} else {
|
||||
delete(cfg, "timezone")
|
||||
}
|
||||
if err := writeHermesConfigMap(configPath, cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
return writeHermesModelEnv(path.Join(confDir, ".env"), account)
|
||||
}
|
||||
|
||||
func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName string) error {
|
||||
if appInstall == nil {
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
if err := writeHermesConfig(dataDir, account, modelName, normalizeHermesTimezone(common.LoadTimeZoneByCmd())); err != nil {
|
||||
return err
|
||||
}
|
||||
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
|
||||
}
|
||||
|
||||
func readHermesConfig(configPath string) (*hermesConfig, error) {
|
||||
content, err := files.NewFileOp().GetContent(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var cfg hermesConfig
|
||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &cfg, nil
|
||||
}
|
||||
|
||||
func readHermesConfigMap(configPath string) (map[string]interface{}, error) {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(configPath) {
|
||||
return map[string]interface{}{}, nil
|
||||
}
|
||||
content, err := fileOp.GetContent(configPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg := map[string]interface{}{}
|
||||
if strings.TrimSpace(string(content)) == "" {
|
||||
return cfg, nil
|
||||
}
|
||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
|
||||
payload, err := yaml.Marshal(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return files.NewFileOp().SaveFile(configPath, string(payload), 0600)
|
||||
}
|
||||
|
||||
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowFrom := splitHermesEnvList(envMap["TELEGRAM_ALLOWED_USERS"])
|
||||
requireMention := extractBoolValue(childMap(cfg, "telegram")["require_mention"], false)
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "TELEGRAM_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if envMap["TELEGRAM_BOT_TOKEN"] != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
groupPolicy := ""
|
||||
if requireMention {
|
||||
groupPolicy = "allowlist"
|
||||
} else if envMap["TELEGRAM_BOT_TOKEN"] != "" {
|
||||
groupPolicy = "open"
|
||||
}
|
||||
token := envMap["TELEGRAM_BOT_TOKEN"]
|
||||
result := &dto.AgentTelegramConfig{
|
||||
Enabled: token != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
RequireMention: requireMention,
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: []string{},
|
||||
Streaming: "",
|
||||
DefaultAccount: "",
|
||||
}
|
||||
if token != "" {
|
||||
result.Bots = []dto.AgentTelegramBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
BotToken: token,
|
||||
DmPolicy: dmPolicy,
|
||||
GroupPolicy: groupPolicy,
|
||||
Streaming: "",
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token := firstHermesTelegramBotToken(config.Bots, config.DefaultAccount)
|
||||
if token != "" {
|
||||
envMap["TELEGRAM_BOT_TOKEN"] = token
|
||||
} else {
|
||||
delete(envMap, "TELEGRAM_BOT_TOKEN")
|
||||
}
|
||||
delete(envMap, "TELEGRAM_ALLOWED_USERS")
|
||||
delete(envMap, "TELEGRAM_ALLOW_ALL_USERS")
|
||||
switch config.DmPolicy {
|
||||
case "open":
|
||||
envMap["TELEGRAM_ALLOW_ALL_USERS"] = "true"
|
||||
case "allowlist":
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["TELEGRAM_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"TELEGRAM_ALLOWED_USERS",
|
||||
"TELEGRAM_ALLOW_ALL_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
telegram := ensureChildMap(cfg, "telegram")
|
||||
telegram["require_mention"] = config.RequireMention
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
allowFrom := splitHermesEnvList(envMap["DISCORD_ALLOWED_USERS"])
|
||||
requireMention := extractBoolValue(childMap(cfg, "discord")["require_mention"], false)
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "DISCORD_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if envMap["DISCORD_BOT_TOKEN"] != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
groupPolicy := ""
|
||||
if requireMention {
|
||||
groupPolicy = "allowlist"
|
||||
} else if envMap["DISCORD_BOT_TOKEN"] != "" {
|
||||
groupPolicy = "open"
|
||||
}
|
||||
token := envMap["DISCORD_BOT_TOKEN"]
|
||||
result := &dto.AgentDiscordConfig{
|
||||
Enabled: token != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
RequireMention: requireMention,
|
||||
GroupPolicy: groupPolicy,
|
||||
DefaultAccount: "",
|
||||
}
|
||||
if token != "" {
|
||||
result.Bots = []dto.AgentDiscordBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
Token: token,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
token := firstHermesDiscordBotToken(config.Bots, config.DefaultAccount)
|
||||
if token != "" {
|
||||
envMap["DISCORD_BOT_TOKEN"] = token
|
||||
} else {
|
||||
delete(envMap, "DISCORD_BOT_TOKEN")
|
||||
}
|
||||
delete(envMap, "DISCORD_ALLOWED_USERS")
|
||||
delete(envMap, "DISCORD_ALLOW_ALL_USERS")
|
||||
switch config.DmPolicy {
|
||||
case "open":
|
||||
envMap["DISCORD_ALLOW_ALL_USERS"] = "true"
|
||||
case "allowlist":
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["DISCORD_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"DISCORD_ALLOWED_USERS",
|
||||
"DISCORD_ALLOW_ALL_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
discord := ensureChildMap(cfg, "discord")
|
||||
discord["require_mention"] = config.RequireMention
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesEnvKeys(confDir string, keys ...string) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range keys {
|
||||
delete(envMap, key)
|
||||
}
|
||||
return writeHermesEnvMap(envPath, envMap, keys)
|
||||
}
|
||||
|
||||
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range topLevelKeys {
|
||||
delete(cfg, key)
|
||||
}
|
||||
if len(platformKeys) > 0 {
|
||||
if platforms, ok := cfg["platforms"].(map[string]interface{}); ok {
|
||||
for _, key := range platformKeys {
|
||||
delete(platforms, key)
|
||||
}
|
||||
if len(platforms) == 0 {
|
||||
delete(cfg, "platforms")
|
||||
}
|
||||
}
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesTelegramChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"TELEGRAM_BOT_TOKEN",
|
||||
"TELEGRAM_ALLOWED_USERS",
|
||||
"TELEGRAM_ALLOW_ALL_USERS",
|
||||
"TELEGRAM_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, []string{"telegram"}, []string{"telegram"})
|
||||
}
|
||||
|
||||
func deleteHermesDiscordChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"DISCORD_BOT_TOKEN",
|
||||
"DISCORD_ALLOWED_USERS",
|
||||
"DISCORD_ALLOW_ALL_USERS",
|
||||
"DISCORD_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, []string{"discord"}, []string{"discord"})
|
||||
}
|
||||
|
||||
func normalizeHermesTimezone(timezone string) string {
|
||||
timezone = strings.TrimSpace(timezone)
|
||||
if timezone == "" {
|
||||
return ""
|
||||
}
|
||||
if _, err := time.LoadLocation(timezone); err != nil {
|
||||
return ""
|
||||
}
|
||||
return timezone
|
||||
}
|
||||
|
||||
func resolveHermesProvider(provider string) string {
|
||||
switch provider {
|
||||
case "":
|
||||
return "custom"
|
||||
case "openrouter", "anthropic", "gemini", "zai", "kimi-coding", "xiaomi":
|
||||
return provider
|
||||
case "minimax":
|
||||
return "minimax-cn"
|
||||
default:
|
||||
return "custom"
|
||||
}
|
||||
}
|
||||
|
||||
func resolveHermesModel(sourceProvider, targetProvider, modelName string) string {
|
||||
target := strings.TrimSpace(modelName)
|
||||
if target == "" {
|
||||
return ""
|
||||
}
|
||||
if targetProvider != "custom" {
|
||||
return target
|
||||
}
|
||||
if sourceProvider == "custom" || sourceProvider == "vllm" {
|
||||
return normalizeCustomModel(target)
|
||||
}
|
||||
if strings.Contains(target, "/") {
|
||||
parts := strings.SplitN(target, "/", 2)
|
||||
model := strings.TrimSpace(parts[1])
|
||||
if model != "" {
|
||||
return model
|
||||
}
|
||||
}
|
||||
return target
|
||||
}
|
||||
|
||||
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
|
||||
if account == nil {
|
||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
configuredModel = strings.TrimSpace(configuredModel)
|
||||
if configuredModel == "" {
|
||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
provider := resolveHermesProvider(account.Provider)
|
||||
for _, item := range accountModels {
|
||||
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
|
||||
return item.ID, nil
|
||||
}
|
||||
}
|
||||
return "", buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
|
||||
func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
|
||||
if account == nil {
|
||||
return nil
|
||||
}
|
||||
if resolveHermesProvider(account.Provider) == "custom" {
|
||||
if account.APIKey == "" {
|
||||
return nil
|
||||
}
|
||||
return []hermesEnvEntry{
|
||||
{Key: "CUSTOM_API_KEY", Value: account.APIKey},
|
||||
}
|
||||
}
|
||||
apiKey := account.APIKey
|
||||
baseURL := account.BaseURL
|
||||
entries := make([]hermesEnvEntry, 0, 4)
|
||||
|
||||
appendEntry := func(key, value string) {
|
||||
if key == "" || value == "" {
|
||||
return
|
||||
}
|
||||
entries = append(entries, hermesEnvEntry{Key: key, Value: value})
|
||||
}
|
||||
|
||||
switch account.Provider {
|
||||
case "openrouter":
|
||||
appendEntry("OPENROUTER_API_KEY", apiKey)
|
||||
appendEntry("OPENROUTER_BASE_URL", baseURL)
|
||||
case "anthropic":
|
||||
appendEntry("ANTHROPIC_API_KEY", apiKey)
|
||||
case "gemini":
|
||||
appendEntry("GOOGLE_API_KEY", apiKey)
|
||||
appendEntry("GEMINI_API_KEY", apiKey)
|
||||
appendEntry("GEMINI_BASE_URL", baseURL)
|
||||
case "zai":
|
||||
appendEntry("GLM_API_KEY", apiKey)
|
||||
appendEntry("ZAI_API_KEY", apiKey)
|
||||
appendEntry("Z_AI_API_KEY", apiKey)
|
||||
appendEntry("GLM_BASE_URL", baseURL)
|
||||
case "kimi-coding", "moonshot":
|
||||
appendEntry("KIMI_API_KEY", apiKey)
|
||||
appendEntry("KIMI_BASE_URL", baseURL)
|
||||
case "kimi":
|
||||
appendEntry("KIMI_CN_API_KEY", apiKey)
|
||||
appendEntry("KIMI_BASE_URL", baseURL)
|
||||
case "minimax":
|
||||
appendEntry("MINIMAX_CN_API_KEY", apiKey)
|
||||
appendEntry("MINIMAX_CN_BASE_URL", baseURL)
|
||||
case "xiaomi":
|
||||
appendEntry("XIAOMI_API_KEY", apiKey)
|
||||
appendEntry("XIAOMI_BASE_URL", baseURL)
|
||||
case "deepseek":
|
||||
appendEntry("DEEPSEEK_API_KEY", apiKey)
|
||||
appendEntry("DEEPSEEK_BASE_URL", baseURL)
|
||||
case "bailian-coding-plan":
|
||||
appendEntry("DASHSCOPE_API_KEY", apiKey)
|
||||
appendEntry("DASHSCOPE_BASE_URL", baseURL)
|
||||
case "openai":
|
||||
appendEntry("OPENAI_API_KEY", apiKey)
|
||||
appendEntry("OPENAI_BASE_URL", baseURL)
|
||||
case "custom", "vllm":
|
||||
if account.APIType == "openai-completions" || account.APIType == "openai-responses" {
|
||||
appendEntry("OPENAI_API_KEY", apiKey)
|
||||
appendEntry("OPENAI_BASE_URL", baseURL)
|
||||
}
|
||||
default:
|
||||
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" {
|
||||
appendEntry(envKey, apiKey)
|
||||
}
|
||||
}
|
||||
|
||||
return entries
|
||||
}
|
||||
|
||||
func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, key := range hermesManagedModelEnvKeys() {
|
||||
delete(envMap, key)
|
||||
}
|
||||
entries := resolveHermesEnvEntries(account)
|
||||
order := make([]string, 0, len(entries))
|
||||
for _, entry := range entries {
|
||||
if entry.Key == "" || entry.Value == "" {
|
||||
continue
|
||||
}
|
||||
envMap[entry.Key] = entry.Value
|
||||
order = append(order, entry.Key)
|
||||
}
|
||||
return writeHermesEnvMap(envPath, envMap, order)
|
||||
}
|
||||
|
||||
func hermesManagedModelEnvKeys() []string {
|
||||
keys := []string{
|
||||
"OPENROUTER_API_KEY",
|
||||
"OPENROUTER_BASE_URL",
|
||||
"ANTHROPIC_API_KEY",
|
||||
"GOOGLE_API_KEY",
|
||||
"GEMINI_API_KEY",
|
||||
"GEMINI_BASE_URL",
|
||||
"GLM_API_KEY",
|
||||
"ZAI_API_KEY",
|
||||
"Z_AI_API_KEY",
|
||||
"GLM_BASE_URL",
|
||||
"KIMI_API_KEY",
|
||||
"KIMI_CN_API_KEY",
|
||||
"KIMI_BASE_URL",
|
||||
"MINIMAX_CN_API_KEY",
|
||||
"MINIMAX_CN_BASE_URL",
|
||||
"XIAOMI_API_KEY",
|
||||
"XIAOMI_BASE_URL",
|
||||
"DEEPSEEK_API_KEY",
|
||||
"DEEPSEEK_BASE_URL",
|
||||
"DASHSCOPE_API_KEY",
|
||||
"DASHSCOPE_BASE_URL",
|
||||
"OPENAI_API_KEY",
|
||||
"OPENAI_BASE_URL",
|
||||
}
|
||||
seen := make(map[string]struct{}, len(keys))
|
||||
result := make([]string, 0, len(keys))
|
||||
appendKey := func(key string) {
|
||||
if key == "" {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[key]; ok {
|
||||
return
|
||||
}
|
||||
seen[key] = struct{}{}
|
||||
result = append(result, key)
|
||||
}
|
||||
for _, key := range keys {
|
||||
appendKey(key)
|
||||
}
|
||||
for _, meta := range providercatalog.All() {
|
||||
appendKey(meta.EnvKey)
|
||||
}
|
||||
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 {
|
||||
if value == "" {
|
||||
return []string{}
|
||||
}
|
||||
parts := strings.Split(value, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
seen := make(map[string]struct{}, len(parts))
|
||||
for _, part := range parts {
|
||||
item := strings.TrimSpace(part)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func joinHermesEnvList(values []string) string {
|
||||
if len(values) == 0 {
|
||||
return ""
|
||||
}
|
||||
result := make([]string, 0, len(values))
|
||||
seen := make(map[string]struct{}, len(values))
|
||||
for _, value := range values {
|
||||
item := strings.TrimSpace(value)
|
||||
if item == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[item]; ok {
|
||||
continue
|
||||
}
|
||||
seen[item] = struct{}{}
|
||||
result = append(result, item)
|
||||
}
|
||||
return strings.Join(result, ",")
|
||||
}
|
||||
|
||||
func firstHermesTelegramBotToken(bots []dto.AgentTelegramBot, defaultAccount string) string {
|
||||
for _, bot := range bots {
|
||||
if bot.AccountID == defaultAccount || bot.IsDefault {
|
||||
return bot.BotToken
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return ""
|
||||
}
|
||||
return bots[0].BotToken
|
||||
}
|
||||
|
||||
func firstHermesDiscordBotToken(bots []dto.AgentDiscordBot, defaultAccount string) string {
|
||||
for _, bot := range bots {
|
||||
if bot.AccountID == defaultAccount || bot.IsDefault {
|
||||
return bot.Token
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return ""
|
||||
}
|
||||
return bots[0].Token
|
||||
}
|
||||
|
||||
func extractHermesEnvBool(envMap map[string]string, key string, defaultValue bool) bool {
|
||||
value, ok := envMap[key]
|
||||
if !ok || value == "" {
|
||||
return defaultValue
|
||||
}
|
||||
return strings.EqualFold(value, "true")
|
||||
}
|
||||
|
||||
func validateHermesPairingApproveResult(output string, err error) error {
|
||||
if strings.Contains(output, "Approved!") {
|
||||
return nil
|
||||
}
|
||||
if strings.Contains(output, "not found or expired for platform") {
|
||||
return buserr.New("ErrHermesPairingCodeUnavailable")
|
||||
}
|
||||
if err == nil {
|
||||
if strings.TrimSpace(output) == "" {
|
||||
return fmt.Errorf("unexpected hermes pairing approve result")
|
||||
}
|
||||
return errors.New(strings.TrimSpace(output))
|
||||
}
|
||||
if strings.Contains(err.Error(), "not found or expired for platform") {
|
||||
return buserr.New("ErrHermesPairingCodeUnavailable")
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"path"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
)
|
||||
|
||||
const hermesWeixinLoginScript = `import asyncio
|
||||
from gateway.platforms.weixin import qr_login
|
||||
from hermes_cli.config import save_env_value
|
||||
|
||||
creds = asyncio.run(qr_login('/opt/data'))
|
||||
print('RESULT =', creds)
|
||||
|
||||
if not creds:
|
||||
raise SystemExit(1)
|
||||
|
||||
save_env_value('WEIXIN_ACCOUNT_ID', creds.get('account_id', ''))
|
||||
save_env_value('WEIXIN_TOKEN', creds.get('token', ''))
|
||||
if creds.get('base_url'):
|
||||
save_env_value('WEIXIN_BASE_URL', creds['base_url'])
|
||||
save_env_value('WEIXIN_CDN_BASE_URL', 'https://novac2c.cdn.weixin.qq.com/c2c')
|
||||
save_env_value('WEIXIN_DM_POLICY', 'open')
|
||||
save_env_value('WEIXIN_ALLOW_ALL_USERS', 'true')
|
||||
save_env_value('WEIXIN_ALLOWED_USERS', '')
|
||||
save_env_value('WEIXIN_GROUP_POLICY', 'disabled')
|
||||
save_env_value('WEIXIN_GROUP_ALLOWED_USERS', '')
|
||||
save_env_value('WEIXIN_HOME_CHANNEL', creds.get('user_id', ''))
|
||||
|
||||
print('WEIXIN ENV WRITTEN')
|
||||
print('Restart the Hermes-Agent container to apply the new Weixin settings.')
|
||||
`
|
||||
|
||||
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "qq")
|
||||
extra := childMap(platform, "extra")
|
||||
appID := envMap["QQ_APP_ID"]
|
||||
clientSecret := envMap["QQ_CLIENT_SECRET"]
|
||||
dmPolicy := extractStringValue(extra["dm_policy"])
|
||||
allowFrom := splitHermesEnvList(envMap["QQ_ALLOWED_USERS"])
|
||||
groupPolicy := extractStringValue(extra["group_policy"])
|
||||
groupAllowFrom := splitHermesEnvList(envMap["QQ_GROUP_ALLOWED_USERS"])
|
||||
|
||||
result := &dto.AgentQQBotConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], false) && appID != "" && clientSecret != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: groupAllowFrom,
|
||||
}
|
||||
if appID != "" || clientSecret != "" {
|
||||
result.Bots = []dto.AgentQQBotBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
AppID: appID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defaultBot := getDefaultQQBot(config.Bots)
|
||||
envMap["QQ_APP_ID"] = defaultBot.AppID
|
||||
envMap["QQ_CLIENT_SECRET"] = defaultBot.ClientSecret
|
||||
delete(envMap, "QQ_ALLOWED_USERS")
|
||||
delete(envMap, "QQ_GROUP_ALLOWED_USERS")
|
||||
delete(envMap, "QQ_ALLOW_ALL_USERS")
|
||||
delete(envMap, "QQ_MARKDOWN_SUPPORT")
|
||||
if config.DmPolicy == "open" {
|
||||
envMap["QQ_ALLOW_ALL_USERS"] = "true"
|
||||
} else if config.DmPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["QQ_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.GroupAllowFrom); allow != "" {
|
||||
envMap["QQ_GROUP_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"QQ_APP_ID",
|
||||
"QQ_CLIENT_SECRET",
|
||||
"QQ_ALLOW_ALL_USERS",
|
||||
"QQ_ALLOWED_USERS",
|
||||
"QQ_GROUP_ALLOWED_USERS",
|
||||
"QQ_HOME_CHANNEL",
|
||||
"QQ_HOME_CHANNEL_NAME",
|
||||
"QQ_STT_API_KEY",
|
||||
"QQ_STT_BASE_URL",
|
||||
"QQ_STT_MODEL",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "qq")
|
||||
platform["enabled"] = config.Enabled && defaultBot.AppID != "" && defaultBot.ClientSecret != ""
|
||||
extra := ensureChildMap(platform, "extra")
|
||||
extra["markdown_support"] = true
|
||||
extra["dm_policy"] = config.DmPolicy
|
||||
extra["group_policy"] = config.GroupPolicy
|
||||
delete(extra, "app_id")
|
||||
delete(extra, "client_secret")
|
||||
if config.DmPolicy == "allowlist" {
|
||||
extra["allow_from"] = append([]string(nil), config.AllowFrom...)
|
||||
} else {
|
||||
delete(extra, "allow_from")
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
extra["group_allow_from"] = append([]string(nil), config.GroupAllowFrom...)
|
||||
} else {
|
||||
delete(extra, "group_allow_from")
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesQQBotChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"QQ_APP_ID",
|
||||
"QQ_CLIENT_SECRET",
|
||||
"QQ_ALLOW_ALL_USERS",
|
||||
"QQ_ALLOWED_USERS",
|
||||
"QQ_GROUP_ALLOWED_USERS",
|
||||
"QQ_HOME_CHANNEL",
|
||||
"QQ_HOME_CHANNEL_NAME",
|
||||
"QQ_STT_API_KEY",
|
||||
"QQ_STT_BASE_URL",
|
||||
"QQ_STT_MODEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"qq"})
|
||||
}
|
||||
|
||||
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "wecom")
|
||||
allowFrom := splitHermesEnvList(envMap["WECOM_ALLOWED_USERS"])
|
||||
groupAllowFrom := splitHermesEnvList(envMap["WECOM_GROUP_ALLOWED_USERS"])
|
||||
dmPolicy := envMap["WECOM_DM_POLICY"]
|
||||
groupPolicy := envMap["WECOM_GROUP_POLICY"]
|
||||
|
||||
botID := envMap["WECOM_BOT_ID"]
|
||||
secret := envMap["WECOM_SECRET"]
|
||||
|
||||
return &dto.AgentWecomConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], false) && botID != "" && secret != "",
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: groupAllowFrom,
|
||||
BotID: botID,
|
||||
Secret: secret,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if config.BotID != "" {
|
||||
envMap["WECOM_BOT_ID"] = config.BotID
|
||||
} else {
|
||||
delete(envMap, "WECOM_BOT_ID")
|
||||
}
|
||||
if config.Secret != "" {
|
||||
envMap["WECOM_SECRET"] = config.Secret
|
||||
} else {
|
||||
delete(envMap, "WECOM_SECRET")
|
||||
}
|
||||
delete(envMap, "WECOM_ALLOWED_USERS")
|
||||
delete(envMap, "WECOM_GROUP_ALLOWED_USERS")
|
||||
delete(envMap, "WECOM_ALLOW_ALL_USERS")
|
||||
delete(envMap, "WECOM_DM_POLICY")
|
||||
delete(envMap, "WECOM_GROUP_POLICY")
|
||||
if config.DmPolicy != "" {
|
||||
envMap["WECOM_DM_POLICY"] = config.DmPolicy
|
||||
}
|
||||
if config.GroupPolicy != "" {
|
||||
envMap["WECOM_GROUP_POLICY"] = config.GroupPolicy
|
||||
}
|
||||
if config.DmPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["WECOM_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.GroupAllowFrom); allow != "" {
|
||||
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"WECOM_BOT_ID",
|
||||
"WECOM_SECRET",
|
||||
"WECOM_DM_POLICY",
|
||||
"WECOM_ALLOWED_USERS",
|
||||
"WECOM_GROUP_POLICY",
|
||||
"WECOM_GROUP_ALLOWED_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "wecom")
|
||||
platform["enabled"] = config.Enabled && config.BotID != "" && config.Secret != ""
|
||||
extra := ensureChildMap(platform, "extra")
|
||||
extra["dm_policy"] = config.DmPolicy
|
||||
extra["group_policy"] = config.GroupPolicy
|
||||
delete(extra, "bot_id")
|
||||
delete(extra, "secret")
|
||||
if config.DmPolicy == "allowlist" {
|
||||
extra["allow_from"] = append([]string(nil), config.AllowFrom...)
|
||||
} else {
|
||||
delete(extra, "allow_from")
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
extra["group_allow_from"] = append([]string(nil), config.GroupAllowFrom...)
|
||||
} else {
|
||||
delete(extra, "group_allow_from")
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesWecomChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"WECOM_BOT_ID",
|
||||
"WECOM_SECRET",
|
||||
"WECOM_ALLOW_ALL_USERS",
|
||||
"WECOM_DM_POLICY",
|
||||
"WECOM_ALLOWED_USERS",
|
||||
"WECOM_GROUP_POLICY",
|
||||
"WECOM_GROUP_ALLOWED_USERS",
|
||||
"WECOM_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"wecom"})
|
||||
}
|
||||
|
||||
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "dingtalk")
|
||||
extra := childMap(platform, "extra")
|
||||
clientID := envMap["DINGTALK_CLIENT_ID"]
|
||||
clientSecret := envMap["DINGTALK_CLIENT_SECRET"]
|
||||
allowFrom := splitHermesEnvList(envMap["DINGTALK_ALLOWED_USERS"])
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "DINGTALK_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if extractStringValue(extra["unauthorized_dm_behavior"]) == "ignore" {
|
||||
dmPolicy = "disabled"
|
||||
} else if clientID != "" || clientSecret != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
result := &dto.AgentDingTalkConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], clientID != "" && clientSecret != ""),
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
GroupPolicy: "",
|
||||
GroupAllowFrom: []string{},
|
||||
}
|
||||
if clientID != "" || clientSecret != "" {
|
||||
result.Bots = []dto.AgentDingTalkBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
ClientID: clientID,
|
||||
ClientSecret: clientSecret,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
clientID, clientSecret := firstHermesDingTalkBotCredentials(config.Bots)
|
||||
if config.Enabled && clientID != "" {
|
||||
envMap["DINGTALK_CLIENT_ID"] = clientID
|
||||
} else {
|
||||
delete(envMap, "DINGTALK_CLIENT_ID")
|
||||
}
|
||||
if config.Enabled && clientSecret != "" {
|
||||
envMap["DINGTALK_CLIENT_SECRET"] = clientSecret
|
||||
} else {
|
||||
delete(envMap, "DINGTALK_CLIENT_SECRET")
|
||||
}
|
||||
delete(envMap, "DINGTALK_ALLOWED_USERS")
|
||||
delete(envMap, "DINGTALK_ALLOW_ALL_USERS")
|
||||
if config.DmPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(config.AllowFrom); allow != "" {
|
||||
envMap["DINGTALK_ALLOWED_USERS"] = allow
|
||||
}
|
||||
} else if config.DmPolicy == "open" {
|
||||
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"DINGTALK_CLIENT_ID",
|
||||
"DINGTALK_CLIENT_SECRET",
|
||||
"DINGTALK_ALLOW_ALL_USERS",
|
||||
"DINGTALK_ALLOWED_USERS",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "dingtalk")
|
||||
platform["enabled"] = config.Enabled && clientID != "" && clientSecret != ""
|
||||
extra := ensureChildMap(platform, "extra")
|
||||
switch config.DmPolicy {
|
||||
case "pairing":
|
||||
extra["unauthorized_dm_behavior"] = "pair"
|
||||
case "disabled":
|
||||
extra["unauthorized_dm_behavior"] = "ignore"
|
||||
default:
|
||||
delete(extra, "unauthorized_dm_behavior")
|
||||
}
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesDingTalkChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"DINGTALK_CLIENT_ID",
|
||||
"DINGTALK_CLIENT_SECRET",
|
||||
"DINGTALK_ALLOW_ALL_USERS",
|
||||
"DINGTALK_ALLOWED_USERS",
|
||||
"DINGTALK_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"dingtalk"})
|
||||
}
|
||||
|
||||
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cfg, err := readHermesConfigMap(path.Join(confDir, "config.yaml"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
platform := childMap(childMap(cfg, "platforms"), "feishu")
|
||||
appID := envMap["FEISHU_APP_ID"]
|
||||
appSecret := envMap["FEISHU_APP_SECRET"]
|
||||
allowFrom := splitHermesEnvList(envMap["FEISHU_ALLOWED_USERS"])
|
||||
dmPolicy := ""
|
||||
if extractHermesEnvBool(envMap, "FEISHU_ALLOW_ALL_USERS", false) {
|
||||
dmPolicy = "open"
|
||||
} else if len(allowFrom) > 0 {
|
||||
dmPolicy = "allowlist"
|
||||
} else if appID != "" || appSecret != "" {
|
||||
dmPolicy = "pairing"
|
||||
}
|
||||
groupPolicy := envMap["FEISHU_GROUP_POLICY"]
|
||||
result := &dto.AgentFeishuConfig{
|
||||
Enabled: extractBoolValue(platform["enabled"], appID != "" && appSecret != ""),
|
||||
ThreadSession: false,
|
||||
ReplyMode: "",
|
||||
Streaming: false,
|
||||
RequireMention: "",
|
||||
GroupPolicy: groupPolicy,
|
||||
GroupAllowFrom: []string{},
|
||||
Domain: envMap["FEISHU_DOMAIN"],
|
||||
ConnectionMode: envMap["FEISHU_CONNECTION_MODE"],
|
||||
}
|
||||
if appID != "" || appSecret != "" {
|
||||
result.Bots = []dto.AgentFeishuBot{
|
||||
{
|
||||
AgentChannelBotBase: dto.AgentChannelBotBase{
|
||||
AccountID: "default",
|
||||
Name: "Default",
|
||||
Enabled: true,
|
||||
IsDefault: true,
|
||||
},
|
||||
AppID: appID,
|
||||
AppSecret: appSecret,
|
||||
DmPolicy: dmPolicy,
|
||||
AllowFrom: allowFrom,
|
||||
},
|
||||
}
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
|
||||
envPath := path.Join(confDir, ".env")
|
||||
envMap, err := readHermesEnvMap(envPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
bot := firstHermesFeishuBot(config.Bots)
|
||||
if config.GroupPolicy == "allowlist" && bot.DmPolicy == "pairing" {
|
||||
return buserr.New("ErrHermesFeishuGroupAllowlistRequiresAllowlist")
|
||||
}
|
||||
if bot.AppID != "" {
|
||||
envMap["FEISHU_APP_ID"] = bot.AppID
|
||||
} else {
|
||||
delete(envMap, "FEISHU_APP_ID")
|
||||
}
|
||||
if bot.AppSecret != "" {
|
||||
envMap["FEISHU_APP_SECRET"] = bot.AppSecret
|
||||
} else {
|
||||
delete(envMap, "FEISHU_APP_SECRET")
|
||||
}
|
||||
envMap["FEISHU_DOMAIN"] = "feishu"
|
||||
envMap["FEISHU_CONNECTION_MODE"] = "websocket"
|
||||
envMap["FEISHU_GROUP_POLICY"] = config.GroupPolicy
|
||||
delete(envMap, "FEISHU_ALLOW_ALL_USERS")
|
||||
delete(envMap, "FEISHU_ALLOWED_USERS")
|
||||
switch bot.DmPolicy {
|
||||
case "open":
|
||||
envMap["FEISHU_ALLOW_ALL_USERS"] = "true"
|
||||
case "allowlist":
|
||||
if allow := joinHermesEnvList(bot.AllowFrom); allow != "" {
|
||||
envMap["FEISHU_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if config.GroupPolicy == "allowlist" {
|
||||
if allow := joinHermesEnvList(bot.AllowFrom); allow != "" {
|
||||
envMap["FEISHU_ALLOWED_USERS"] = allow
|
||||
}
|
||||
}
|
||||
if err := writeHermesEnvMap(envPath, envMap, []string{
|
||||
"FEISHU_APP_ID",
|
||||
"FEISHU_APP_SECRET",
|
||||
"FEISHU_DOMAIN",
|
||||
"FEISHU_CONNECTION_MODE",
|
||||
"FEISHU_ALLOW_ALL_USERS",
|
||||
"FEISHU_ALLOWED_USERS",
|
||||
"FEISHU_GROUP_POLICY",
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configPath := path.Join(confDir, "config.yaml")
|
||||
cfg, err := readHermesConfigMap(configPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
platform := ensureChildMap(ensureChildMap(cfg, "platforms"), "feishu")
|
||||
platform["enabled"] = config.Enabled && bot.AppID != "" && bot.AppSecret != ""
|
||||
return writeHermesConfigMap(configPath, cfg)
|
||||
}
|
||||
|
||||
func deleteHermesFeishuChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"FEISHU_APP_ID",
|
||||
"FEISHU_APP_SECRET",
|
||||
"FEISHU_DOMAIN",
|
||||
"FEISHU_CONNECTION_MODE",
|
||||
"FEISHU_ALLOW_ALL_USERS",
|
||||
"FEISHU_ALLOWED_USERS",
|
||||
"FEISHU_GROUP_POLICY",
|
||||
"FEISHU_HOME_CHANNEL",
|
||||
"FEISHU_VERIFICATION_TOKEN",
|
||||
"FEISHU_ENCRYPT_KEY",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"feishu"})
|
||||
}
|
||||
|
||||
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
|
||||
envMap, err := readHermesEnvMap(path.Join(confDir, ".env"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &dto.AgentWeixinConfig{
|
||||
Enabled: envMap["WEIXIN_ACCOUNT_ID"] != "" || envMap["WEIXIN_TOKEN"] != "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
func deleteHermesWeixinChannelConfig(confDir string) error {
|
||||
if err := deleteHermesEnvKeys(confDir,
|
||||
"WEIXIN_ACCOUNT_ID",
|
||||
"WEIXIN_TOKEN",
|
||||
"WEIXIN_BASE_URL",
|
||||
"WEIXIN_CDN_BASE_URL",
|
||||
"WEIXIN_DM_POLICY",
|
||||
"WEIXIN_ALLOW_ALL_USERS",
|
||||
"WEIXIN_ALLOWED_USERS",
|
||||
"WEIXIN_GROUP_POLICY",
|
||||
"WEIXIN_GROUP_ALLOWED_USERS",
|
||||
"WEIXIN_HOME_CHANNEL",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return deleteHermesConfigSections(confDir, nil, []string{"weixin"})
|
||||
}
|
||||
|
||||
func firstHermesDingTalkBotCredentials(bots []dto.AgentDingTalkBot) (string, string) {
|
||||
for _, bot := range bots {
|
||||
if bot.IsDefault || bot.AccountID == "default" {
|
||||
return bot.ClientID, bot.ClientSecret
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return "", ""
|
||||
}
|
||||
return bots[0].ClientID, bots[0].ClientSecret
|
||||
}
|
||||
|
||||
func firstHermesFeishuBot(bots []dto.AgentFeishuBot) dto.AgentFeishuBot {
|
||||
for _, bot := range bots {
|
||||
if bot.IsDefault || bot.AccountID == "default" {
|
||||
return bot
|
||||
}
|
||||
}
|
||||
if len(bots) == 0 {
|
||||
return dto.AgentFeishuBot{}
|
||||
}
|
||||
return bots[0]
|
||||
}
|
||||
|
||||
func buildHermesWeixinLoginArgs(containerName string) []string {
|
||||
return buildHermesDockerExecCommandArgs(
|
||||
containerName,
|
||||
"/opt/hermes/.venv/bin/python",
|
||||
"-u",
|
||||
"-c",
|
||||
hermesWeixinLoginScript,
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,126 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"math"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
)
|
||||
|
||||
const hermesSessionListLimit = 100
|
||||
|
||||
func (a AgentService) GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error) {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return listHermesChatSessionsFromStateDB(filepath.Join(install.GetPath(), "data", "state.db"))
|
||||
}
|
||||
|
||||
func (a AgentService) RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
|
||||
_, err = cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
|
||||
"docker",
|
||||
buildHermesDockerExecArgs(install.ContainerName, "sessions", "rename", req.ID, req.Title)...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func (a AgentService) DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
|
||||
_, err = cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
|
||||
"docker",
|
||||
buildHermesDockerExecArgs(install.ContainerName, "sessions", "delete", req.ID, "--yes")...,
|
||||
)
|
||||
return err
|
||||
}
|
||||
|
||||
func listHermesChatSessionsFromStateDB(stateDBPath string) ([]dto.AgentHermesChatSessionItem, error) {
|
||||
if !files.NewFileOp().Stat(stateDBPath) {
|
||||
return []dto.AgentHermesChatSessionItem{}, nil
|
||||
}
|
||||
|
||||
db, err := sql.Open("sqlite", stateDBPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
rows, err := db.Query(`
|
||||
SELECT
|
||||
s.id,
|
||||
COALESCE(NULLIF(TRIM(s.title), ''), s.id) AS title,
|
||||
COALESCE(s.model, '') AS model,
|
||||
COALESCE(s.message_count, 0) AS message_count,
|
||||
s.started_at,
|
||||
COALESCE(MAX(m.timestamp), s.started_at) AS last_active
|
||||
FROM sessions s
|
||||
LEFT JOIN messages m ON m.session_id = s.id
|
||||
WHERE s.source = 'cli'
|
||||
GROUP BY s.id, title, model, s.message_count, s.started_at
|
||||
ORDER BY last_active DESC
|
||||
LIMIT ?
|
||||
`, hermesSessionListLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
items := make([]dto.AgentHermesChatSessionItem, 0, 8)
|
||||
for rows.Next() {
|
||||
var item dto.AgentHermesChatSessionItem
|
||||
var title sql.NullString
|
||||
var model sql.NullString
|
||||
var startedAt sql.NullFloat64
|
||||
var lastActive sql.NullFloat64
|
||||
if err := rows.Scan(&item.ID, &title, &model, &item.MessageCount, &startedAt, &lastActive); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Title = strings.TrimSpace(title.String)
|
||||
if item.Title == "" {
|
||||
item.Title = item.ID
|
||||
}
|
||||
item.Model = strings.TrimSpace(model.String)
|
||||
item.StartedAt = formatHermesSessionTimestamp(startedAt)
|
||||
item.LastActive = formatHermesSessionTimestamp(lastActive)
|
||||
items = append(items, item)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func formatHermesSessionTimestamp(value sql.NullFloat64) string {
|
||||
if !value.Valid || value.Float64 <= 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
seconds, fraction := math.Modf(value.Float64)
|
||||
return time.Unix(int64(seconds), int64(fraction*float64(time.Second))).UTC().Format(time.RFC3339)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
type hermesSkillsListEntry struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Category string `json:"category"`
|
||||
}
|
||||
|
||||
type hermesSkillsListPayload struct {
|
||||
Skills []hermesSkillsListEntry `json:"skills"`
|
||||
}
|
||||
|
||||
func listHermesSkills(containerName string) ([]dto.AgentSkillItem, error) {
|
||||
output, err := runHermesSkillsCommandWithStdout(2*time.Minute, containerName, "list", "--source", "all")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items, err := parseHermesSkillsListOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata, err := readHermesSkillsListMetadata(containerName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return mergeHermesSkillsWithMetadata(items, metadata), nil
|
||||
}
|
||||
|
||||
func searchHermesSkills(containerName, source, keyword string) ([]dto.AgentSkillSearchItem, error) {
|
||||
if source != "official" && source != "skills-sh" {
|
||||
return nil, fmt.Errorf("unsupported hermes skill source: %s", source)
|
||||
}
|
||||
output, err := runHermesSkillsCommandWithStdout(
|
||||
2*time.Minute,
|
||||
containerName,
|
||||
"search",
|
||||
keyword,
|
||||
"--source",
|
||||
source,
|
||||
"--limit",
|
||||
"20",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseHermesSkillSearchOutput(output)
|
||||
}
|
||||
|
||||
func runHermesSkillsCommandWithStdout(timeout time.Duration, containerName string, hermesArgs ...string) (string, error) {
|
||||
args := []string{"exec", "-e", "COLUMNS=240", "-u", "hermes", containerName, "hermes", "skills"}
|
||||
args = append(args, hermesArgs...)
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(timeout)).RunWithStdout("docker", args...)
|
||||
}
|
||||
|
||||
func readHermesSkillsListMetadata(containerName string) (map[string]hermesSkillsListEntry, error) {
|
||||
output, err := cmd.NewCommandMgr(cmd.WithTimeout(2*time.Minute)).RunWithStdout(
|
||||
"docker",
|
||||
"exec",
|
||||
"-u",
|
||||
"hermes",
|
||||
containerName,
|
||||
"python",
|
||||
"-c",
|
||||
"import sys; sys.path.insert(0, '/opt/hermes'); from tools.skills_tool import skills_list; print(skills_list())",
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return parseHermesSkillsListMetadataOutput(output)
|
||||
}
|
||||
|
||||
func parseHermesSkillsListOutput(output string) ([]dto.AgentSkillItem, error) {
|
||||
headers, rows, err := parseHermesTableOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.AgentSkillItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
item := dto.AgentSkillItem{
|
||||
Name: row[headers["Name"]],
|
||||
Category: row[headers["Category"]],
|
||||
Source: row[headers["Source"]],
|
||||
Trust: row[headers["Trust"]],
|
||||
Uninstallable: row[headers["Source"]] != "builtin" && row[headers["Source"]] != "local",
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseHermesSkillsListMetadataOutput(output string) (map[string]hermesSkillsListEntry, error) {
|
||||
if strings.TrimSpace(output) == "" {
|
||||
return map[string]hermesSkillsListEntry{}, nil
|
||||
}
|
||||
var payload hermesSkillsListPayload
|
||||
if err := json.Unmarshal([]byte(output), &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metadata := make(map[string]hermesSkillsListEntry, len(payload.Skills))
|
||||
for _, skill := range payload.Skills {
|
||||
if skill.Name == "" {
|
||||
continue
|
||||
}
|
||||
metadata[skill.Name] = skill
|
||||
}
|
||||
return metadata, nil
|
||||
}
|
||||
|
||||
func mergeHermesSkillsWithMetadata(items []dto.AgentSkillItem, metadata map[string]hermesSkillsListEntry) []dto.AgentSkillItem {
|
||||
for i := range items {
|
||||
entry, ok := metadata[items[i].Name]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
items[i].Description = entry.Description
|
||||
if items[i].Category == "" && entry.Category != "" {
|
||||
items[i].Category = entry.Category
|
||||
}
|
||||
}
|
||||
return items
|
||||
}
|
||||
|
||||
func parseHermesSkillSearchOutput(output string) ([]dto.AgentSkillSearchItem, error) {
|
||||
if strings.Contains(output, "No skills found matching your query.") {
|
||||
return []dto.AgentSkillSearchItem{}, nil
|
||||
}
|
||||
headers, rows, err := parseHermesTableOutput(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.AgentSkillSearchItem, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
identifier := row[headers["Identifier"]]
|
||||
items = append(items, dto.AgentSkillSearchItem{
|
||||
Slug: identifier,
|
||||
Identifier: identifier,
|
||||
Name: row[headers["Name"]],
|
||||
Description: row[headers["Description"]],
|
||||
Source: row[headers["Source"]],
|
||||
Trust: row[headers["Trust"]],
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseHermesTableOutput(output string) (map[string]int, [][]string, error) {
|
||||
lines := strings.Split(strings.TrimSpace(ansiEscapePattern.ReplaceAllString(output, "")), "\n")
|
||||
var headers []string
|
||||
rows := make([][]string, 0)
|
||||
var current []string
|
||||
|
||||
for _, rawLine := range lines {
|
||||
line := strings.TrimSpace(rawLine)
|
||||
if (!strings.HasPrefix(line, "│") || !strings.HasSuffix(line, "│")) &&
|
||||
(!strings.HasPrefix(line, "┃") || !strings.HasSuffix(line, "┃")) {
|
||||
continue
|
||||
}
|
||||
line = strings.ReplaceAll(line, "┃", "│")
|
||||
parts := strings.Split(line, "│")
|
||||
if len(parts) < 3 {
|
||||
continue
|
||||
}
|
||||
cols := make([]string, 0, len(parts)-2)
|
||||
for _, part := range parts[1 : len(parts)-1] {
|
||||
cols = append(cols, strings.TrimSpace(part))
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
headers = cols
|
||||
continue
|
||||
}
|
||||
if cols[0] != "" {
|
||||
if current != nil {
|
||||
rows = append(rows, current)
|
||||
}
|
||||
current = cols
|
||||
continue
|
||||
}
|
||||
if current == nil {
|
||||
continue
|
||||
}
|
||||
for i := range cols {
|
||||
if cols[i] == "" {
|
||||
continue
|
||||
}
|
||||
if current[i] == "" {
|
||||
current[i] = cols[i]
|
||||
continue
|
||||
}
|
||||
current[i] = current[i] + " " + cols[i]
|
||||
}
|
||||
}
|
||||
|
||||
if current != nil {
|
||||
rows = append(rows, current)
|
||||
}
|
||||
if len(headers) == 0 {
|
||||
return nil, nil, fmt.Errorf("hermes skills table not found")
|
||||
}
|
||||
|
||||
headerIndex := make(map[string]int, len(headers))
|
||||
for i, header := range headers {
|
||||
headerIndex[header] = i
|
||||
}
|
||||
return headerIndex, rows, nil
|
||||
}
|
||||
@@ -10,7 +10,6 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -102,11 +101,7 @@ func countOpenclawConfiguredChannels(conf map[string]interface{}) int {
|
||||
}
|
||||
|
||||
func loadOpenclawOverviewSkillStats(containerName string) (int, error) {
|
||||
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
|
||||
"docker exec %s openclaw skills list --json 2>&1",
|
||||
30*time.Second,
|
||||
containerName,
|
||||
)
|
||||
output, err := runDockerExecWithStdout(5*time.Minute, containerName, "sh", "-c", "openclaw skills list --json 2>&1")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
@@ -9,10 +9,14 @@ import (
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
)
|
||||
|
||||
const clawhubGlobalRegistry = "https://clawhub.com"
|
||||
const clawhubChinaRegistry = "https://mirror-cn.clawhub.com"
|
||||
|
||||
type openclawSkillsList struct {
|
||||
Skills []openclawSkillListItem `json:"skills"`
|
||||
}
|
||||
@@ -35,20 +39,23 @@ type skillhubSearchPayload struct {
|
||||
}
|
||||
|
||||
var clawhubSearchLinePattern = regexp.MustCompile(`^(\S+)\s+(.+?)\s+\(([\d.]+)\)$`)
|
||||
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
|
||||
|
||||
func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, error) {
|
||||
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
|
||||
"docker exec %s openclaw skills list --json 2>&1",
|
||||
30*time.Second,
|
||||
install.ContainerName,
|
||||
)
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return listHermesSkills(install.ContainerName)
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
output, err := runDockerExecWithStdout(5*time.Minute, install.ContainerName, "sh", "-c", "openclaw skills list --json 2>&1")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -59,13 +66,19 @@ func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, erro
|
||||
}
|
||||
|
||||
func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) {
|
||||
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
return searchHermesSkills(install.ContainerName, req.Source, req.Keyword)
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
output, err := loadOpenclawSkillSearchOutput(install.ContainerName, req.Source, req.Keyword)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -77,7 +90,7 @@ func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkil
|
||||
case "skillhub":
|
||||
return parseSkillhubSearchResult(output)
|
||||
default:
|
||||
return parseClawhubSearchResult(output), nil
|
||||
return parseClawhubSearchResult(output, req.Source), nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -102,7 +115,7 @@ func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error {
|
||||
}
|
||||
|
||||
func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
|
||||
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -113,8 +126,23 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType == constant.AppHermesAgent {
|
||||
installTask.AddSubTask("Install Hermes skill", func(t *task.Task) error {
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
|
||||
return mgr.Run("docker", buildHermesDockerExecArgs(install.ContainerName, "skills", "install", req.Slug, "--yes")...)
|
||||
}, nil)
|
||||
go func() {
|
||||
if err := installTask.Execute(); err != nil {
|
||||
global.LOG.Errorf("install hermes skill failed: %v", err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
installTask.AddSubTask("Install OpenClaw skill", func(t *task.Task) error {
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
|
||||
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
|
||||
return mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawSkillInstallCommand(req.Source, req.Slug))
|
||||
}, nil)
|
||||
go func() {
|
||||
@@ -125,9 +153,40 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a AgentService) UninstallSkill(req dto.AgentSkillUninstallReq) error {
|
||||
agent, install, err := a.loadAgentAndInstall(req.AgentID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.AgentType != constant.AppHermesAgent {
|
||||
return fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
if err := ensureContainerRunning(install.ContainerName); err != nil {
|
||||
return err
|
||||
}
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).Run(
|
||||
"docker",
|
||||
buildHermesDockerExecCommandArgs(
|
||||
install.ContainerName,
|
||||
"sh",
|
||||
"-lc",
|
||||
fmt.Sprintf(`printf 'y\n' | %s skills uninstall "$1"`, hermesExecutablePath),
|
||||
"sh",
|
||||
req.Name,
|
||||
)...,
|
||||
)
|
||||
}
|
||||
|
||||
func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) {
|
||||
payloadBytes, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(payloadBytes) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
var payload openclawSkillsList
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
|
||||
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]dto.AgentSkillItem, 0, len(payload.Skills))
|
||||
@@ -146,18 +205,14 @@ func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) {
|
||||
func loadOpenclawSkillSearchOutput(containerName, source, keyword string) (string, error) {
|
||||
switch source {
|
||||
case "skillhub":
|
||||
return cmd.RunDefaultWithStdoutBashCfAndTimeOut(
|
||||
"docker exec %s skillhub search %q --json 2>&1",
|
||||
30*time.Second,
|
||||
containerName,
|
||||
keyword,
|
||||
)
|
||||
return runDockerExecWithStdout(2*time.Minute, containerName, "skillhub", "search", keyword, "--json")
|
||||
default:
|
||||
return cmd.RunDefaultWithStdoutBashCfAndTimeOut(
|
||||
"docker exec %s clawhub search %q 2>&1",
|
||||
30*time.Second,
|
||||
return runDockerExecWithStdout(
|
||||
2*time.Minute,
|
||||
containerName,
|
||||
keyword,
|
||||
"sh",
|
||||
"-c",
|
||||
fmt.Sprintf("CLAWHUB_REGISTRY=%q clawhub search %q", resolveClawhubRegistry(source), keyword),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -188,7 +243,7 @@ func parseSkillhubSearchResult(output string) ([]dto.AgentSkillSearchItem, error
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func parseClawhubSearchResult(output string) []dto.AgentSkillSearchItem {
|
||||
func parseClawhubSearchResult(output, source string) []dto.AgentSkillSearchItem {
|
||||
lines := strings.Split(strings.TrimSpace(output), "\n")
|
||||
items := make([]dto.AgentSkillSearchItem, 0, len(lines))
|
||||
for _, line := range lines {
|
||||
@@ -200,7 +255,7 @@ func parseClawhubSearchResult(output string) []dto.AgentSkillSearchItem {
|
||||
Slug: matches[1],
|
||||
Name: matches[2],
|
||||
Score: matches[3],
|
||||
Source: "clawhub",
|
||||
Source: source,
|
||||
})
|
||||
}
|
||||
return items
|
||||
@@ -208,10 +263,11 @@ func parseClawhubSearchResult(output string) []dto.AgentSkillSearchItem {
|
||||
|
||||
func buildOpenclawSkillInstallCommand(source, slug string) string {
|
||||
switch source {
|
||||
case "clawhub":
|
||||
case "clawhub-global", "clawhub-cn":
|
||||
return fmt.Sprintf(
|
||||
"mkdir -p %s && clawhub --workdir /home/node/.openclaw --dir skills install %q",
|
||||
"mkdir -p %s && CLAWHUB_REGISTRY=%q clawhub --workdir /home/node/.openclaw --dir skills install %q",
|
||||
openclawManagedSkillsDir,
|
||||
resolveClawhubRegistry(source),
|
||||
slug,
|
||||
)
|
||||
default:
|
||||
@@ -224,13 +280,17 @@ func buildOpenclawSkillInstallCommand(source, slug string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func resolveClawhubRegistry(source string) string {
|
||||
switch source {
|
||||
case "clawhub-cn":
|
||||
return clawhubChinaRegistry
|
||||
default:
|
||||
return clawhubGlobalRegistry
|
||||
}
|
||||
}
|
||||
|
||||
func getOpenclawSkillKey(containerName, name string) (string, error) {
|
||||
output, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut(
|
||||
"docker exec %s openclaw skills info %q --json 2>&1",
|
||||
30*time.Second,
|
||||
containerName,
|
||||
name,
|
||||
)
|
||||
output, err := runDockerExecWithStdout(2*time.Minute, containerName, "sh", "-c", fmt.Sprintf("openclaw skills info %q --json 2>&1", name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
@@ -238,8 +298,12 @@ func getOpenclawSkillKey(containerName, name string) (string, error) {
|
||||
}
|
||||
|
||||
func parseOpenclawSkillKey(name, output string) (string, error) {
|
||||
payloadBytes, err := extractEmbeddedJSON(output)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var payload openclawSkillInfo
|
||||
if err := json.Unmarshal([]byte(strings.TrimSpace(output)), &payload); err != nil {
|
||||
if err := json.Unmarshal(payloadBytes, &payload); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if payload.SkillKey == "" {
|
||||
@@ -248,6 +312,24 @@ func parseOpenclawSkillKey(name, output string) (string, error) {
|
||||
return payload.SkillKey, nil
|
||||
}
|
||||
|
||||
func extractEmbeddedJSON(output string) ([]byte, error) {
|
||||
trimmed := strings.TrimSpace(ansiEscapePattern.ReplaceAllString(output, ""))
|
||||
if trimmed == "" {
|
||||
return nil, nil
|
||||
}
|
||||
for i := 0; i < len(trimmed); i++ {
|
||||
if trimmed[i] != '{' && trimmed[i] != '[' {
|
||||
continue
|
||||
}
|
||||
decoder := json.NewDecoder(strings.NewReader(trimmed[i:]))
|
||||
var raw json.RawMessage
|
||||
if err := decoder.Decode(&raw); err == nil {
|
||||
return raw, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("json payload not found")
|
||||
}
|
||||
|
||||
func setOpenclawSkillEnabled(conf map[string]interface{}, skillKey string, enabled bool) {
|
||||
skills := ensureChildMap(conf, "skills")
|
||||
entries := ensureChildMap(skills, "entries")
|
||||
|
||||
+206
-190
@@ -19,6 +19,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||
@@ -42,8 +43,8 @@ func loadOpenclawAgentByID(agentID uint) (*model.Agent, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if agent.AgentType == constant.AppCopaw {
|
||||
return nil, fmt.Errorf("copaw does not support")
|
||||
if agent.AgentType != constant.AppOpenclaw {
|
||||
return nil, fmt.Errorf("%s does not support", agent.AgentType)
|
||||
}
|
||||
return agent, nil
|
||||
}
|
||||
@@ -59,6 +60,11 @@ func ensureContainerRunning(containerName string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func runDockerExecWithStdout(timeout time.Duration, containerName string, args ...string) (string, error) {
|
||||
commandArgs := append([]string{"exec", containerName}, args...)
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(timeout)).RunWithStdout("docker", commandArgs...)
|
||||
}
|
||||
|
||||
func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
|
||||
resolvedAPIKey := strings.TrimSpace(apiKey)
|
||||
resolvedBaseURL := strings.TrimSpace(baseURL)
|
||||
@@ -333,13 +339,11 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) {
|
||||
}
|
||||
|
||||
func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem {
|
||||
agentType := normalizeAgentType(agent.AgentType)
|
||||
if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppCopaw {
|
||||
agentType = constant.AppCopaw
|
||||
}
|
||||
agentType := agent.AgentType
|
||||
item := dto.AgentItem{
|
||||
ID: agent.ID,
|
||||
Name: agent.Name,
|
||||
Remark: agent.Remark,
|
||||
AgentType: agentType,
|
||||
Provider: agent.Provider,
|
||||
ProviderName: providercatalog.DisplayName(agent.Provider),
|
||||
@@ -353,6 +357,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
Status: agent.Status,
|
||||
Message: agent.Message,
|
||||
AppInstallID: agent.AppInstallID,
|
||||
WebsiteID: agent.WebsiteID,
|
||||
AccountID: agent.AccountID,
|
||||
ConfigPath: agent.ConfigPath,
|
||||
CreatedAt: agent.CreatedAt,
|
||||
@@ -381,6 +386,10 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
|
||||
return item
|
||||
}
|
||||
|
||||
func isAgentAppKey(appKey string) bool {
|
||||
return appKey == constant.AppOpenclaw || appKey == constant.AppCopaw || appKey == constant.AppHermesAgent
|
||||
}
|
||||
|
||||
func isOpenclawLegacyHTTPVersion(version string) bool {
|
||||
return !common.CompareAppVersion(version, openclawHTTPSVersion)
|
||||
}
|
||||
@@ -653,7 +662,8 @@ type agentDefaults struct {
|
||||
}
|
||||
|
||||
type modelRef struct {
|
||||
Primary string `json:"primary"`
|
||||
Primary string `json:"primary"`
|
||||
Fallbacks []string `json:"fallbacks,omitempty"`
|
||||
}
|
||||
|
||||
type modelsConfig struct {
|
||||
@@ -679,6 +689,23 @@ type modelEntry struct {
|
||||
Cost modelCost `json:"cost"`
|
||||
}
|
||||
|
||||
func requiresOpenclawProviderModels(provider string) bool {
|
||||
return provider != "gemini"
|
||||
}
|
||||
|
||||
func applyOpenclawModelsConfig(conf map[string]interface{}, models *modelsConfig) error {
|
||||
if models == nil {
|
||||
delete(conf, "models")
|
||||
return nil
|
||||
}
|
||||
modelsMap, err := structToMap(models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf["models"] = modelsMap
|
||||
return nil
|
||||
}
|
||||
|
||||
type modelCost struct {
|
||||
Input float64 `json:"input"`
|
||||
Output float64 `json:"output"`
|
||||
@@ -694,7 +721,7 @@ type browserConfig struct {
|
||||
DefaultProfile string `json:"defaultProfile"`
|
||||
}
|
||||
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string) error {
|
||||
func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error {
|
||||
if strings.TrimSpace(confDir) == "" {
|
||||
return fmt.Errorf("config dir is required")
|
||||
}
|
||||
@@ -717,6 +744,10 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
resolvedFallbacks, err := resolveOpenclawFallbackModels(account, modelName, fallbacks)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfg := openclawConfig{
|
||||
Gateway: gatewayConfig{
|
||||
@@ -736,7 +767,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
Agents: agentsConfig{
|
||||
Defaults: agentDefaults{
|
||||
UserTimezone: resolveServerTimezone(),
|
||||
Model: modelRef{Primary: primaryModel},
|
||||
Model: modelRef{Primary: primaryModel, Fallbacks: resolvedFallbacks},
|
||||
Models: defaultsModels,
|
||||
},
|
||||
},
|
||||
@@ -775,12 +806,8 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
conf = initial
|
||||
} else {
|
||||
if cfg.Models != nil {
|
||||
modelsMap, err := structToMap(cfg.Models)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf["models"] = modelsMap
|
||||
if err := applyOpenclawModelsConfig(conf, cfg.Models); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, ok := conf["browser"]; !ok {
|
||||
browserMap, err := structToMap(cfg.Browser)
|
||||
@@ -804,6 +831,11 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
modelMap := ensureChildMap(defaultsMap, "model")
|
||||
modelMap["primary"] = cfg.Agents.Defaults.Model.Primary
|
||||
if len(cfg.Agents.Defaults.Model.Fallbacks) > 0 {
|
||||
modelMap["fallbacks"] = cfg.Agents.Defaults.Model.Fallbacks
|
||||
} else {
|
||||
delete(modelMap, "fallbacks")
|
||||
}
|
||||
if cfg.Agents.Defaults.Models != nil {
|
||||
defaultsMap["models"] = cfg.Agents.Defaults.Models
|
||||
}
|
||||
@@ -833,19 +865,135 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
|
||||
}
|
||||
envPath := path.Join(confDir, ".env")
|
||||
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)}
|
||||
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && strings.TrimSpace(account.APIKey) != "" {
|
||||
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"
|
||||
return fileOp.SaveFile(envPath, content, 0600)
|
||||
}
|
||||
|
||||
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
|
||||
accountModels, err := loadAgentAccountModels(account)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return resolveOpenclawFallbackModelsFromPool(account, accountModels, primaryModel, fallbackIDs, true)
|
||||
}
|
||||
|
||||
func resolveOpenclawFallbackModelsFromPool(account *model.AgentAccount, accountModels []dto.AgentAccountModel, primaryModel string, fallbackIDs []string, strict bool) ([]string, error) {
|
||||
if len(fallbackIDs) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
result := make([]string, 0, len(fallbackIDs))
|
||||
seen := make(map[string]struct{}, len(fallbackIDs))
|
||||
for _, item := range fallbackIDs {
|
||||
target := strings.TrimSpace(item)
|
||||
if target == "" {
|
||||
continue
|
||||
}
|
||||
accountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, target)
|
||||
if err != nil {
|
||||
if strict {
|
||||
return nil, err
|
||||
}
|
||||
continue
|
||||
}
|
||||
if sameProviderModelID(account.Provider, accountModel.ID, primaryModel) {
|
||||
if strict {
|
||||
return nil, buserr.New("ErrAgentFallbackModelPrimary")
|
||||
}
|
||||
continue
|
||||
}
|
||||
resolvedPrimary, _, _, _, err := buildOpenclawAccountModelConfig(account, accountModel)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if _, ok := seen[resolvedPrimary]; ok {
|
||||
if strict {
|
||||
return nil, buserr.New("ErrAgentFallbackModelDuplicate")
|
||||
}
|
||||
continue
|
||||
}
|
||||
seen[resolvedPrimary] = struct{}{}
|
||||
result = append(result, resolvedPrimary)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func extractOpenclawPrimaryModelID(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) string {
|
||||
modelMap := openclawModelRefMap(conf)
|
||||
primary := strings.TrimSpace(fmt.Sprintf("%v", modelMap["primary"]))
|
||||
if primary == "" {
|
||||
return ""
|
||||
}
|
||||
resolved, err := buildOpenclawResolvedModelIDMap(account, accountModels)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return resolved[primary]
|
||||
}
|
||||
|
||||
func extractOpenclawFallbackModelIDs(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel, primaryModel string) []string {
|
||||
modelMap := openclawModelRefMap(conf)
|
||||
raw, ok := modelMap["fallbacks"].([]interface{})
|
||||
if !ok || len(raw) == 0 {
|
||||
return nil
|
||||
}
|
||||
resolved, err := buildOpenclawResolvedModelIDMap(account, accountModels)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
result := make([]string, 0, len(raw))
|
||||
seen := make(map[string]struct{}, len(raw))
|
||||
for _, item := range raw {
|
||||
target := strings.TrimSpace(fmt.Sprintf("%v", item))
|
||||
modelID, ok := resolved[target]
|
||||
if !ok || sameProviderModelID(account.Provider, modelID, primaryModel) {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[modelID]; ok {
|
||||
continue
|
||||
}
|
||||
seen[modelID] = struct{}{}
|
||||
result = append(result, modelID)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func openclawModelRefMap(conf map[string]interface{}) map[string]interface{} {
|
||||
agentsMap, ok := conf["agents"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
defaultsMap, ok := agentsMap["defaults"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
modelMap, ok := defaultsMap["model"].(map[string]interface{})
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return modelMap
|
||||
}
|
||||
|
||||
func buildOpenclawResolvedModelIDMap(account *model.AgentAccount, accountModels []dto.AgentAccountModel) (map[string]string, error) {
|
||||
result := make(map[string]string, len(accountModels))
|
||||
for _, item := range accountModels {
|
||||
resolvedPrimary, _, _, _, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result[resolvedPrimary] = item.ID
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName, token string, allowedOrigins []string) error {
|
||||
if appInstall == nil {
|
||||
return fmt.Errorf("app install is required")
|
||||
}
|
||||
confDir := path.Join(appInstall.GetPath(), "data", "conf")
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins); err != nil {
|
||||
if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
dataDir := path.Join(appInstall.GetPath(), "data")
|
||||
@@ -858,14 +1006,10 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
||||
return "", nil, nil, err
|
||||
}
|
||||
if len(accountModels) == 0 {
|
||||
return "", nil, nil, fmt.Errorf("model is required")
|
||||
}
|
||||
selectedModel = strings.TrimSpace(selectedModel)
|
||||
if selectedModel == "" {
|
||||
selectedModel = strings.TrimSpace(accountModels[0].ID)
|
||||
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if selectedModel == "" {
|
||||
return "", nil, nil, fmt.Errorf("model is required")
|
||||
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
selectedAccountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, selectedModel)
|
||||
if err != nil {
|
||||
@@ -879,7 +1023,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
||||
primaryModel := ""
|
||||
defaultsModels := make(map[string]map[string]interface{}, len(accountModels))
|
||||
for _, item := range accountModels {
|
||||
resolvedPrimary, entry, key, baseCfg, err := buildOpenclawCatalogModel(account, item)
|
||||
resolvedPrimary, entry, key, baseCfg, err := buildOpenclawAccountModelConfig(account, item)
|
||||
if err != nil {
|
||||
return "", nil, nil, err
|
||||
}
|
||||
@@ -899,6 +1043,9 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
||||
if primaryModel == "" {
|
||||
return "", nil, nil, buserr.New("ErrAgentModelNotInAccount")
|
||||
}
|
||||
if !requiresOpenclawProviderModels(account.Provider) {
|
||||
return primaryModel, defaultsModels, nil, nil
|
||||
}
|
||||
providerCfg.Models = entries
|
||||
return primaryModel, defaultsModels, &modelsConfig{
|
||||
Mode: "merge",
|
||||
@@ -908,25 +1055,33 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildOpenclawCatalogModel(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) {
|
||||
primaryModel, inferredEntry, providerKey, providerCfg, err := inferOpenclawCatalogModel(account, model.ID, model.Reasoning, model.MaxTokens, model.ContextWindow)
|
||||
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)
|
||||
if err != nil {
|
||||
return "", modelEntry{}, "", modelProvider{}, err
|
||||
}
|
||||
if strings.TrimSpace(model.Name) != "" {
|
||||
inferredEntry.Name = strings.TrimSpace(model.Name)
|
||||
return providerPatch.PrimaryModel, buildOpenclawModelEntry(providerPatch.ModelID, model), providerPatch.ProviderKey, modelProvider{
|
||||
ApiKey: providerPatch.APIKey,
|
||||
BaseUrl: providerPatch.BaseURL,
|
||||
Api: providerPatch.APIType,
|
||||
AuthHeader: providerPatch.AuthHeader,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func buildOpenclawModelEntry(modelID string, model dto.AgentAccountModel) modelEntry {
|
||||
name := strings.TrimSpace(model.Name)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(modelID)
|
||||
}
|
||||
if len(model.Input) > 0 {
|
||||
inferredEntry.Input = sanitizeAgentAccountModelInputs(model.Input)
|
||||
return modelEntry{
|
||||
ID: strings.TrimSpace(modelID),
|
||||
Name: name,
|
||||
Reasoning: model.Reasoning,
|
||||
Input: sanitizeAgentAccountModelInputs(model.Input),
|
||||
ContextWindow: model.ContextWindow,
|
||||
MaxTokens: model.MaxTokens,
|
||||
Cost: modelCost{},
|
||||
}
|
||||
inferredEntry.Reasoning = model.Reasoning
|
||||
if model.ContextWindow > 0 {
|
||||
inferredEntry.ContextWindow = model.ContextWindow
|
||||
}
|
||||
if model.MaxTokens > 0 {
|
||||
inferredEntry.MaxTokens = model.MaxTokens
|
||||
}
|
||||
return primaryModel, inferredEntry, providerKey, providerCfg, nil
|
||||
}
|
||||
|
||||
type openclawAccountModelRuntime struct {
|
||||
@@ -938,22 +1093,16 @@ type openclawAccountModelRuntime struct {
|
||||
}
|
||||
|
||||
func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) {
|
||||
apiType, maxTokens, contextWindow := providercatalog.ResolveRuntimeParams(
|
||||
account.Provider,
|
||||
account.APIType,
|
||||
model.MaxTokens,
|
||||
model.ContextWindow,
|
||||
)
|
||||
primaryModel, _, _, _, err := buildOpenclawCatalogModel(account, model)
|
||||
primaryModel, _, _, _, err := buildOpenclawAccountModelConfig(account, model)
|
||||
if err != nil {
|
||||
return openclawAccountModelRuntime{}, err
|
||||
}
|
||||
return openclawAccountModelRuntime{
|
||||
StoredModel: model.ID,
|
||||
PrimaryModel: primaryModel,
|
||||
APIType: apiType,
|
||||
MaxTokens: maxTokens,
|
||||
ContextWindow: contextWindow,
|
||||
APIType: account.APIType,
|
||||
MaxTokens: model.MaxTokens,
|
||||
ContextWindow: model.ContextWindow,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -969,44 +1118,6 @@ func resolveOpenclawAccountModelRuntimeByID(account *model.AgentAccount, modelID
|
||||
return buildOpenclawAccountModelRuntime(account, selectedAccountModel)
|
||||
}
|
||||
|
||||
func inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, reasoning bool, maxTokens, contextWindow int) (string, modelEntry, string, modelProvider, error) {
|
||||
baseURL := resolveAccountBaseURL(account)
|
||||
resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := providercatalog.ResolveRuntimeParams(account.Provider, account.APIType, maxTokens, contextWindow)
|
||||
patch, err := providercatalog.BuildOpenClawPatch(account.Provider, modelID, resolvedAPIType, reasoning, resolvedMaxTokens, resolvedContextWindow, baseURL, account.APIKey)
|
||||
if err != nil {
|
||||
return "", modelEntry{}, "", modelProvider{}, err
|
||||
}
|
||||
if patch.Models == nil {
|
||||
return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is required")
|
||||
}
|
||||
modelsCfg, err := mapToModelsConfig(patch.Models)
|
||||
if err != nil {
|
||||
return "", modelEntry{}, "", modelProvider{}, err
|
||||
}
|
||||
for key, providerCfg := range modelsCfg.Providers {
|
||||
if len(providerCfg.Models) == 0 {
|
||||
continue
|
||||
}
|
||||
return patch.PrimaryModel, providerCfg.Models[0], key, modelProvider{
|
||||
ApiKey: providerCfg.ApiKey,
|
||||
BaseUrl: providerCfg.BaseUrl,
|
||||
Api: providerCfg.Api,
|
||||
AuthHeader: providerCfg.AuthHeader,
|
||||
}, nil
|
||||
}
|
||||
return "", modelEntry{}, "", modelProvider{}, fmt.Errorf("models patch is invalid")
|
||||
}
|
||||
|
||||
func resolveAccountBaseURL(account *model.AgentAccount) string {
|
||||
baseURL := strings.TrimSpace(account.BaseURL)
|
||||
if baseURL == "" {
|
||||
if defaultURL, ok := providercatalog.DefaultBaseURL(account.Provider); ok {
|
||||
baseURL = defaultURL
|
||||
}
|
||||
}
|
||||
return baseURL
|
||||
}
|
||||
|
||||
func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
|
||||
if account == nil {
|
||||
return nil, fmt.Errorf("account is required")
|
||||
@@ -1169,12 +1280,6 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model
|
||||
return nil
|
||||
}
|
||||
|
||||
func replacePersistedAgentAccountModels(accountID uint, models []dto.AgentAccountModel) error {
|
||||
return global.DB.Transaction(func(tx *gorm.DB) error {
|
||||
return replacePersistedAgentAccountModelsWithTx(tx, accountID, models)
|
||||
})
|
||||
}
|
||||
|
||||
func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) {
|
||||
requested := append([]dto.AgentAccountModel(nil), models...)
|
||||
if len(requested) == 0 {
|
||||
@@ -1207,76 +1312,37 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco
|
||||
if modelID == "" {
|
||||
return dto.AgentAccountModel{}, fmt.Errorf("model is required")
|
||||
}
|
||||
inferredReasoning := model.Reasoning
|
||||
if !model.Reasoning && model.Name == "" && model.MaxTokens == 0 && model.ContextWindow == 0 && len(model.Input) == 0 {
|
||||
if catalogModel, ok := providercatalog.FindModel(account.Provider, modelID); ok {
|
||||
inferredReasoning = catalogModel.Reasoning
|
||||
}
|
||||
}
|
||||
primaryModel, inferredEntry, _, _, err := inferOpenclawCatalogModel(account, modelID, inferredReasoning, model.MaxTokens, model.ContextWindow)
|
||||
if err != nil {
|
||||
return dto.AgentAccountModel{}, err
|
||||
}
|
||||
name := strings.TrimSpace(model.Name)
|
||||
if name == "" {
|
||||
name = strings.TrimSpace(inferredEntry.Name)
|
||||
}
|
||||
reasoning := model.Reasoning
|
||||
if !model.Reasoning && model.Name == "" && model.MaxTokens == 0 && model.ContextWindow == 0 && len(model.Input) == 0 {
|
||||
reasoning = inferredEntry.Reasoning
|
||||
name = modelID
|
||||
}
|
||||
inputs := sanitizeAgentAccountModelInputs(model.Input)
|
||||
if len(inputs) == 0 {
|
||||
inputs = sanitizeAgentAccountModelInputs(inferredEntry.Input)
|
||||
}
|
||||
contextWindow := model.ContextWindow
|
||||
if contextWindow <= 0 {
|
||||
contextWindow = inferredEntry.ContextWindow
|
||||
}
|
||||
maxTokens := model.MaxTokens
|
||||
if maxTokens <= 0 {
|
||||
maxTokens = inferredEntry.MaxTokens
|
||||
}
|
||||
return dto.AgentAccountModel{
|
||||
ID: normalizeAgentAccountModelID(account.Provider, primaryModel, modelID),
|
||||
ID: normalizeAgentAccountModelID(account.Provider, modelID),
|
||||
Name: name,
|
||||
ContextWindow: contextWindow,
|
||||
MaxTokens: maxTokens,
|
||||
Reasoning: reasoning,
|
||||
ContextWindow: model.ContextWindow,
|
||||
MaxTokens: model.MaxTokens,
|
||||
Reasoning: model.Reasoning,
|
||||
Input: inputs,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func normalizeAgentAccountModelID(provider, primaryModel, requestedID string) string {
|
||||
func normalizeAgentAccountModelID(provider, requestedID string) string {
|
||||
switch provider {
|
||||
case "custom", "vllm":
|
||||
target := requestedID
|
||||
if strings.TrimSpace(target) == "" {
|
||||
target = primaryModel
|
||||
}
|
||||
return normalizeCustomModel(target)
|
||||
return normalizeCustomModel(requestedID)
|
||||
case "ollama":
|
||||
target := strings.TrimSpace(primaryModel)
|
||||
if strings.HasPrefix(target, "ollama/") {
|
||||
return target
|
||||
}
|
||||
target = strings.TrimSpace(requestedID)
|
||||
target := strings.TrimSpace(requestedID)
|
||||
if strings.HasPrefix(target, "ollama/") {
|
||||
return target
|
||||
}
|
||||
target = strings.TrimLeft(strings.TrimSpace(target), "/")
|
||||
if target == "" {
|
||||
target = strings.TrimLeft(strings.TrimSpace(primaryModel), "/")
|
||||
}
|
||||
if target == "" {
|
||||
return ""
|
||||
}
|
||||
return "ollama/" + target
|
||||
default:
|
||||
target := strings.TrimSpace(requestedID)
|
||||
if target == "" {
|
||||
target = strings.TrimSpace(primaryModel)
|
||||
}
|
||||
if target == "" {
|
||||
return ""
|
||||
}
|
||||
@@ -1394,9 +1460,6 @@ func ensureAccountModelsNotBound(account *model.AgentAccount, models []dto.Agent
|
||||
return err
|
||||
}
|
||||
for _, agent := range agents {
|
||||
if strings.TrimSpace(agent.Model) == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := findAgentAccountModelForProvider(account.Provider, models, agent.Model); !ok {
|
||||
return buserr.WithName("ErrAgentModelInUse", agent.Name)
|
||||
}
|
||||
@@ -1432,19 +1495,6 @@ func extractStringList(value interface{}) []string {
|
||||
}
|
||||
}
|
||||
|
||||
func extractGatewayToken(conf map[string]interface{}) string {
|
||||
gateway, ok := conf["gateway"].(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
auth, ok := gateway["auth"].(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
token, _ := auth["token"].(string)
|
||||
return token
|
||||
}
|
||||
|
||||
func ensureChildMap(parent map[string]interface{}, key string) map[string]interface{} {
|
||||
if child, ok := parent[key].(map[string]interface{}); ok {
|
||||
return child
|
||||
@@ -1466,18 +1516,6 @@ func structToMap(value interface{}) (map[string]interface{}, error) {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func mapToModelsConfig(value map[string]interface{}) (*modelsConfig, error) {
|
||||
payload, err := json.Marshal(value)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := &modelsConfig{}
|
||||
if err := json.Unmarshal(payload, result); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func readInstallEnv(envStr string) map[string]interface{} {
|
||||
if strings.TrimSpace(envStr) == "" {
|
||||
return nil
|
||||
@@ -1527,24 +1565,6 @@ func normalizeCustomModel(modelName string) string {
|
||||
return trim
|
||||
}
|
||||
|
||||
func normalizeAgentType(agentType string) string {
|
||||
trim := agentType
|
||||
if trim == "" {
|
||||
return constant.AppOpenclaw
|
||||
}
|
||||
return trim
|
||||
}
|
||||
|
||||
func modelMatchesProvider(provider, modelName string) bool {
|
||||
target := strings.TrimSpace(modelName)
|
||||
for _, prefix := range supportedProviderModelPrefixes(provider) {
|
||||
if prefix != "" && strings.HasPrefix(target, prefix+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func runtimeProviderModelPrefix(provider string) string {
|
||||
switch provider {
|
||||
case "gemini":
|
||||
@@ -1594,11 +1614,7 @@ func generateToken() string {
|
||||
}
|
||||
|
||||
func asyncReportAIProviderInstall(provider string) {
|
||||
if global.CONF.Base.Mode != "stable" {
|
||||
return
|
||||
}
|
||||
provider = provider
|
||||
if provider == "" {
|
||||
if global.CONF.Base.Mode != "stable" || provider == "" {
|
||||
return
|
||||
}
|
||||
go func(provider string) {
|
||||
|
||||
@@ -0,0 +1,338 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
func (a AgentService) BindWebsite(req dto.AgentWebsiteBindReq) error {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.WebsiteID != 0 {
|
||||
return buserr.New("ErrAgentWebsiteBound")
|
||||
}
|
||||
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(req.WebsiteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !isBindableAgentWebsiteType(website.Type) {
|
||||
return buserr.New("ErrAgentWebsiteTypeUnsupported")
|
||||
}
|
||||
|
||||
boundAgent, err := agentRepo.GetFirst(repo.WithByWebsiteID(req.WebsiteID))
|
||||
if err == nil && boundAgent.ID > 0 {
|
||||
return buserr.New("ErrAgentWebsiteInUse")
|
||||
}
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
|
||||
agent.WebsiteID = req.WebsiteID
|
||||
if err := agentRepo.Save(agent); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureOpenclawWebsiteAllowedOrigin(agent, &website)
|
||||
}
|
||||
|
||||
func (a AgentService) UnbindWebsite(req dto.AgentIDReq) error {
|
||||
agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.WebsiteID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
website, err := websiteRepo.GetFirst(repo.WithByID(agent.WebsiteID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if website.Type == constant.Deployment {
|
||||
return buserr.New("ErrAgentWebsiteUnbindUnsupported")
|
||||
}
|
||||
|
||||
agent.WebsiteID = 0
|
||||
return agentRepo.Save(agent)
|
||||
}
|
||||
|
||||
func hydrateAgentWebsiteItems(items []dto.AgentItem) error {
|
||||
explicitWebsiteMap, err := loadAgentWebsiteMapByID(items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
websiteDomainMap, err := loadAgentWebsiteDomainMapByWebsiteID(items)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fillAgentWebsiteItems(items, explicitWebsiteMap, websiteDomainMap)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadAgentWebsiteMapByID(items []dto.AgentItem) (map[uint]model.Website, error) {
|
||||
websiteIDs := make([]uint, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.WebsiteID > 0 {
|
||||
websiteIDs = append(websiteIDs, item.WebsiteID)
|
||||
}
|
||||
}
|
||||
if len(websiteIDs) == 0 {
|
||||
return map[uint]model.Website{}, nil
|
||||
}
|
||||
websites, err := websiteRepo.GetBy(repo.WithByIDs(uniqueUintList(websiteIDs)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websiteMap := make(map[uint]model.Website, len(websites))
|
||||
for _, website := range websites {
|
||||
websiteMap[website.ID] = website
|
||||
}
|
||||
return websiteMap, nil
|
||||
}
|
||||
|
||||
func loadAgentWebsiteDomainMapByWebsiteID(items []dto.AgentItem) (map[uint][]model.WebsiteDomain, error) {
|
||||
websiteIDs := make([]uint, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item.WebsiteID > 0 {
|
||||
websiteIDs = append(websiteIDs, item.WebsiteID)
|
||||
}
|
||||
}
|
||||
if len(websiteIDs) == 0 {
|
||||
return map[uint][]model.WebsiteDomain{}, nil
|
||||
}
|
||||
websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteIds(uniqueUintList(websiteIDs)), repo.WithOrderAsc("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
websiteDomainMap := make(map[uint][]model.WebsiteDomain, len(websiteDomains))
|
||||
for _, websiteDomain := range websiteDomains {
|
||||
websiteDomainMap[websiteDomain.WebsiteID] = append(websiteDomainMap[websiteDomain.WebsiteID], websiteDomain)
|
||||
}
|
||||
return websiteDomainMap, nil
|
||||
}
|
||||
|
||||
func loadAgentWebsiteResourceName(website model.Website) (string, error) {
|
||||
websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID), repo.WithOrderAsc("id"))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if len(websiteDomains) > 0 {
|
||||
return websiteDomains[0].Domain, nil
|
||||
}
|
||||
if website.PrimaryDomain != "" {
|
||||
return website.PrimaryDomain, nil
|
||||
}
|
||||
return fmt.Sprintf("%d", website.ID), nil
|
||||
}
|
||||
|
||||
func fillAgentWebsiteItems(items []dto.AgentItem, explicitWebsiteMap map[uint]model.Website, websiteDomainMap map[uint][]model.WebsiteDomain) {
|
||||
for index := range items {
|
||||
if items[index].WebsiteID == 0 {
|
||||
continue
|
||||
}
|
||||
website, ok := explicitWebsiteMap[items[index].WebsiteID]
|
||||
if !ok {
|
||||
items[index].WebsiteID = 0
|
||||
items[index].WebsitePrimaryDomain = ""
|
||||
items[index].WebsiteType = ""
|
||||
items[index].WebsiteProtocol = ""
|
||||
continue
|
||||
}
|
||||
items[index].WebsiteType = website.Type
|
||||
items[index].WebsiteProtocol = website.Protocol
|
||||
websiteDomains := websiteDomainMap[items[index].WebsiteID]
|
||||
if len(websiteDomains) == 0 {
|
||||
items[index].WebsitePrimaryDomain = ""
|
||||
continue
|
||||
}
|
||||
items[index].WebsitePrimaryDomain = websiteDomains[0].Domain
|
||||
}
|
||||
}
|
||||
|
||||
func uniqueDeploymentWebsiteMapByAppInstall(websites []model.Website) map[uint]model.Website {
|
||||
websiteMap := make(map[uint]model.Website)
|
||||
duplicateAppInstallIDs := make(map[uint]struct{})
|
||||
for _, website := range websites {
|
||||
if website.AppInstallID == 0 {
|
||||
continue
|
||||
}
|
||||
if _, duplicated := duplicateAppInstallIDs[website.AppInstallID]; duplicated {
|
||||
continue
|
||||
}
|
||||
if _, exists := websiteMap[website.AppInstallID]; exists {
|
||||
delete(websiteMap, website.AppInstallID)
|
||||
duplicateAppInstallIDs[website.AppInstallID] = struct{}{}
|
||||
continue
|
||||
}
|
||||
websiteMap[website.AppInstallID] = website
|
||||
}
|
||||
return websiteMap
|
||||
}
|
||||
|
||||
func UniqueDeploymentWebsiteMapForMigration(websites []model.Website) map[uint]model.Website {
|
||||
return uniqueDeploymentWebsiteMapByAppInstall(websites)
|
||||
}
|
||||
|
||||
func uniqueUintList(items []uint) []uint {
|
||||
itemMap := make(map[uint]struct{}, len(items))
|
||||
uniq := make([]uint, 0, len(items))
|
||||
for _, item := range items {
|
||||
if item == 0 {
|
||||
continue
|
||||
}
|
||||
if _, exists := itemMap[item]; exists {
|
||||
continue
|
||||
}
|
||||
itemMap[item] = struct{}{}
|
||||
uniq = append(uniq, item)
|
||||
}
|
||||
return uniq
|
||||
}
|
||||
|
||||
func isBindableAgentWebsiteType(websiteType string) bool {
|
||||
return websiteType == constant.Proxy || websiteType == constant.Static
|
||||
}
|
||||
|
||||
func bindDeploymentWebsiteToAgentByAppInstall(website *model.Website) error {
|
||||
if website.ID == 0 || website.Type != constant.Deployment || website.AppInstallID == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
appInstall, err := appInstallRepo.GetFirst(repo.WithByID(website.AppInstallID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInstall.App.Key != constant.AppOpenclaw && appInstall.App.Key != constant.AppCopaw && appInstall.App.Key != constant.AppHermesAgent {
|
||||
return nil
|
||||
}
|
||||
|
||||
agent, err := agentRepo.GetFirst(repo.WithByAppInstallID(website.AppInstallID))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if agent.WebsiteID != 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
agent.WebsiteID = website.ID
|
||||
if err := agentRepo.Save(agent); err != nil {
|
||||
return err
|
||||
}
|
||||
return ensureOpenclawWebsiteAllowedOrigin(agent, website)
|
||||
}
|
||||
|
||||
func ensureOpenclawWebsiteAllowedOrigin(agent *model.Agent, website *model.Website) error {
|
||||
if agent == nil || website == nil || agent.AgentType != constant.AppOpenclaw {
|
||||
return nil
|
||||
}
|
||||
|
||||
origins, err := buildWebsiteAllowedOrigins(website)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if len(origins) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
conf, err := readOpenclawConfig(agent.ConfigPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
allowedOrigins := extractSecurityConfig(conf).AllowedOrigins
|
||||
allowedOrigins, err = normalizeAllowedOrigins(append(allowedOrigins, origins...))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
setSecurityConfig(conf, dto.AgentSecurityConfig{AllowedOrigins: allowedOrigins})
|
||||
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := syncOpenclawAllowedOriginEnv(&install, allowedOrigins); err != nil {
|
||||
return err
|
||||
}
|
||||
return appInstallRepo.Save(context.Background(), &install)
|
||||
}
|
||||
|
||||
func buildWebsiteAllowedOrigins(website *model.Website) ([]string, error) {
|
||||
websiteDomains, err := websiteDomainRepo.GetBy(websiteDomainRepo.WithWebsiteId(website.ID), repo.WithOrderAsc("id"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(websiteDomains) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
defaultHTTPSPort := 443
|
||||
if strings.EqualFold(website.Protocol, "https") {
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
if err == nil && nginxInstall.HttpsPort > 0 {
|
||||
defaultHTTPSPort = nginxInstall.HttpsPort
|
||||
}
|
||||
}
|
||||
return buildWebsiteAllowedOriginsFromDomains(website, websiteDomains, defaultHTTPSPort)
|
||||
}
|
||||
|
||||
func buildWebsiteAllowedOriginsFromDomains(website *model.Website, websiteDomains []model.WebsiteDomain, defaultHTTPSPort int) ([]string, error) {
|
||||
if len(websiteDomains) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
sort.Slice(websiteDomains, func(i, j int) bool {
|
||||
return websiteDomains[i].ID < websiteDomains[j].ID
|
||||
})
|
||||
origins := make([]string, 0, len(websiteDomains))
|
||||
for _, websiteDomain := range websiteDomains {
|
||||
origin, err := buildWebsiteDomainOrigin(website.Protocol, websiteDomain, defaultHTTPSPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
origins = append(origins, origin)
|
||||
}
|
||||
return origins, nil
|
||||
}
|
||||
|
||||
func buildWebsiteDomainOrigin(protocol string, websiteDomain model.WebsiteDomain, defaultHTTPSPort int) (string, error) {
|
||||
origin := strings.ToLower(strings.TrimSpace(protocol)) + "://" + formatWebsiteDomainHost(websiteDomain.Domain)
|
||||
switch strings.ToLower(strings.TrimSpace(protocol)) {
|
||||
case "http":
|
||||
if websiteDomain.Port > 0 && websiteDomain.Port != 80 {
|
||||
origin = fmt.Sprintf("%s:%d", origin, websiteDomain.Port)
|
||||
}
|
||||
case "https":
|
||||
port := websiteDomain.Port
|
||||
if !websiteDomain.SSL {
|
||||
port = defaultHTTPSPort
|
||||
}
|
||||
if port > 0 && port != 443 {
|
||||
origin = fmt.Sprintf("%s:%d", origin, port)
|
||||
}
|
||||
}
|
||||
return normalizeAllowedOrigin(origin)
|
||||
}
|
||||
|
||||
func formatWebsiteDomainHost(domain string) string {
|
||||
host := strings.Trim(strings.TrimSpace(domain), "[]")
|
||||
if ip := net.ParseIP(host); ip != nil && strings.Contains(host, ":") {
|
||||
return "[" + host + "]"
|
||||
}
|
||||
return host
|
||||
}
|
||||
+130
-4
@@ -1,11 +1,14 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
@@ -19,6 +22,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/jinzhu/copier"
|
||||
)
|
||||
|
||||
@@ -107,8 +111,7 @@ func (u *AIToolService) Create(req dto.OllamaModelName) error {
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelPull", req.Name), func(t *task.Task) error {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem), cmd.WithTimeout(time.Hour))
|
||||
return cmdMgr.Run("docker", "exec", containerName, "ollama", "pull", info.Name)
|
||||
return runOllamaPullWithProcess(t, containerName, info.Name)
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
||||
itemSize, err := loadModelSize(info.Name, containerName)
|
||||
@@ -162,8 +165,7 @@ func (u *AIToolService) Recreate(req dto.OllamaModelName) error {
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelPull", req.Name), func(t *task.Task) error {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*taskItem), cmd.WithTimeout(time.Hour))
|
||||
return cmdMgr.Run("docker", "exec", containerName, "ollama", "pull", req.Name)
|
||||
return runOllamaPullWithProcess(t, containerName, req.Name)
|
||||
}, nil)
|
||||
taskItem.AddSubTask(i18n.GetWithName("OllamaModelSize", req.Name), func(t *task.Task) error {
|
||||
itemSize, err := loadModelSize(modelInfo.Name, containerName)
|
||||
@@ -394,3 +396,127 @@ func loadModelSize(name string, containerName string) (string, error) {
|
||||
}
|
||||
return "", fmt.Errorf("no such model %s in ollama list, std: %s", name, stdout)
|
||||
}
|
||||
|
||||
func runOllamaPullWithProcess(taskItem *task.Task, containerName, modelName string) error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Hour)
|
||||
defer cancel()
|
||||
|
||||
cmdItem := exec.CommandContext(ctx, "docker", "exec", containerName, "ollama", "pull", modelName)
|
||||
cmdItem.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||
writer := &ollamaPullLogWriter{taskItem: taskItem}
|
||||
cmdItem.Stdout = writer
|
||||
cmdItem.Stderr = writer
|
||||
|
||||
if err := cmdItem.Start(); err != nil {
|
||||
return fmt.Errorf("failed to start ollama pull %s: %w", modelName, err)
|
||||
}
|
||||
waitErr := cmdItem.Wait()
|
||||
writer.Flush()
|
||||
if ctx.Err() == context.DeadlineExceeded {
|
||||
if cmdItem.Process != nil && cmdItem.Process.Pid > 0 {
|
||||
_ = syscall.Kill(-cmdItem.Process.Pid, syscall.SIGKILL)
|
||||
}
|
||||
return buserr.New("ErrCmdTimeout")
|
||||
}
|
||||
if waitErr == nil {
|
||||
return nil
|
||||
}
|
||||
if len(strings.TrimSpace(writer.errBuf.String())) > 0 {
|
||||
return fmt.Errorf("%s", strings.TrimSpace(writer.errBuf.String()))
|
||||
}
|
||||
return waitErr
|
||||
}
|
||||
|
||||
type ollamaPullLogWriter struct {
|
||||
taskItem *task.Task
|
||||
errBuf bytes.Buffer
|
||||
}
|
||||
|
||||
func (w *ollamaPullLogWriter) Write(p []byte) (n int, err error) {
|
||||
w.errBuf.Write(p)
|
||||
for _, segment := range splitOllamaPullSegments(string(p)) {
|
||||
w.logLine(segment)
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (w *ollamaPullLogWriter) Flush() {}
|
||||
|
||||
func (w *ollamaPullLogWriter) logLine(line string) {
|
||||
if w == nil || w.taskItem == nil {
|
||||
return
|
||||
}
|
||||
line = sanitizeOllamaPullLine(line)
|
||||
if line == "" {
|
||||
return
|
||||
}
|
||||
if prefix := resolveOllamaPullLogPrefix(line); prefix != "" {
|
||||
_ = replaceOllamaPullLogLine(prefix, line, w.taskItem)
|
||||
return
|
||||
}
|
||||
w.taskItem.Log(line)
|
||||
}
|
||||
|
||||
func sanitizeOllamaPullLine(line string) string {
|
||||
line = re.StripAnsiControlSeq(line)
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "pulling manifes") {
|
||||
return ""
|
||||
}
|
||||
return line
|
||||
}
|
||||
|
||||
func splitOllamaPullSegments(chunk string) []string {
|
||||
if chunk == "" {
|
||||
return nil
|
||||
}
|
||||
chunk = strings.ReplaceAll(chunk, "\r", "\n")
|
||||
parts := strings.Split(chunk, "\x1b[1G")
|
||||
segments := make([]string, 0, len(parts))
|
||||
for _, part := range parts {
|
||||
for _, line := range strings.Split(part, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
segments = append(segments, line)
|
||||
}
|
||||
}
|
||||
return segments
|
||||
}
|
||||
|
||||
func resolveOllamaPullLogPrefix(line string) string {
|
||||
if strings.HasPrefix(line, "pulling ") {
|
||||
if idx := strings.Index(line, ":"); idx > 0 {
|
||||
return strings.TrimSpace(line[:idx])
|
||||
}
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) >= 2 {
|
||||
return strings.TrimSpace(fields[0] + " " + fields[1])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func replaceOllamaPullLogLine(prefix, newLine string, taskItem *task.Task) error {
|
||||
if taskItem == nil || taskItem.Task == nil {
|
||||
return nil
|
||||
}
|
||||
data, err := os.ReadFile(taskItem.Task.LogFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for idx, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if trimmed == "" {
|
||||
continue
|
||||
}
|
||||
if strings.Contains(trimmed, prefix) {
|
||||
lines[idx] = time.Now().Format("2006/01/02 15:04:05") + " " + newLine
|
||||
return os.WriteFile(taskItem.Task.LogFile, []byte(strings.Join(lines, "\n")), os.ModePerm)
|
||||
}
|
||||
}
|
||||
taskItem.Log(newLine)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -650,6 +650,28 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [
|
||||
continue
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m)
|
||||
case constant.Bark:
|
||||
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, m)
|
||||
if !isValid {
|
||||
continue
|
||||
}
|
||||
var create = dto.AlertLogCreate{
|
||||
Type: alertUtil.GetCronJobType(alert.Type),
|
||||
AlertId: alert.ID,
|
||||
Count: todayCount + 1,
|
||||
}
|
||||
alertInfo := alert
|
||||
alertInfo.Type = alertType
|
||||
create.AlertRule = alertUtil.ProcessAlertRule(alert)
|
||||
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, m)
|
||||
transport := xpack.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.GetAgentInfo()
|
||||
alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo)
|
||||
if alertErr != nil {
|
||||
global.LOG.Infof("%s alert %s push failed, err: %v", alertType, m, alertErr.Error())
|
||||
continue
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m)
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,8 @@ func (s *AlertSender) Send(quota string, params []dto.Param) {
|
||||
s.sendSMS(quota, params)
|
||||
case constant.Email:
|
||||
s.sendEmail(quota, params)
|
||||
case constant.Bark:
|
||||
s.sendBark(quota, params)
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
||||
s.sendWebhook(quota, params, method)
|
||||
}
|
||||
@@ -45,6 +47,8 @@ func (s *AlertSender) ResourceSend(quota string, params []dto.Param) {
|
||||
s.sendResourceSMS(quota, params)
|
||||
case constant.Email:
|
||||
s.sendResourceEmail(quota, params)
|
||||
case constant.Bark:
|
||||
s.sendResourceBark(quota, params)
|
||||
case constant.WeCom, constant.DingTalk, constant.FeiShu:
|
||||
s.sendResourceWebhook(quota, params, method)
|
||||
}
|
||||
@@ -101,6 +105,31 @@ func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendBark(quota string, params []dto.Param) {
|
||||
totalCount, isValid := s.canSendAlert(constant.Bark)
|
||||
if !isValid {
|
||||
return
|
||||
}
|
||||
|
||||
create := dto.AlertLogCreate{
|
||||
Status: constant.AlertSuccess,
|
||||
Count: totalCount + 1,
|
||||
AlertId: s.alert.ID,
|
||||
Type: s.alert.Type,
|
||||
AlertRule: alertUtil.ProcessAlertRule(s.alert),
|
||||
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
|
||||
}
|
||||
|
||||
transport := xpack.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.GetAgentInfo()
|
||||
err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark)
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
|
||||
totalCount, isValid := s.canSendAlert(method)
|
||||
if !isValid {
|
||||
@@ -171,6 +200,30 @@ func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) {
|
||||
todayCount, isValid := s.canResourceSendAlert(constant.Bark)
|
||||
if !isValid {
|
||||
return
|
||||
}
|
||||
|
||||
create := dto.AlertLogCreate{
|
||||
Status: constant.AlertSuccess,
|
||||
Count: todayCount + 1,
|
||||
AlertId: s.alert.ID,
|
||||
Type: s.alert.Type,
|
||||
AlertRule: alertUtil.ProcessAlertRule(s.alert),
|
||||
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
|
||||
}
|
||||
|
||||
transport := xpack.LoadRequestTransport()
|
||||
agentInfo, _ := xpack.GetAgentInfo()
|
||||
if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo); err != nil {
|
||||
global.LOG.Errorf("failed to send Bark alert: %v", err)
|
||||
return
|
||||
}
|
||||
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark)
|
||||
}
|
||||
|
||||
func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, method string) {
|
||||
todayCount, isValid := s.canResourceSendAlert(method)
|
||||
if !isValid {
|
||||
|
||||
@@ -13,6 +13,7 @@ import (
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
@@ -576,7 +577,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
|
||||
_ = appInstallRepo.Save(context.Background(), appInstall)
|
||||
}
|
||||
|
||||
installTask.AddSubTask(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus)
|
||||
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, time.Hour)
|
||||
|
||||
go func() {
|
||||
if taskErr := installTask.Execute(); taskErr != nil {
|
||||
@@ -895,7 +896,7 @@ func getAppFromRepo(downloadPath string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := fileOp.Decompress(packagePath, global.Dir.ResourceDir, files.SdkZip, ""); err != nil {
|
||||
if err := fileOp.Decompress(context.Background(), packagePath, global.Dir.ResourceDir, files.SdkZip, ""); err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
|
||||
@@ -853,6 +853,10 @@ func syncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncAppInstallStatus(appInstall *model.AppInstall, force bool) error {
|
||||
return syncAppInstallStatus(appInstall, force)
|
||||
}
|
||||
|
||||
func updateInstallInfoInDB(appKey, appName, param string, value interface{}) error {
|
||||
if param != "password" && param != "port" && param != "user-password" {
|
||||
return nil
|
||||
@@ -870,7 +874,7 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
|
||||
envKey := ""
|
||||
switch param {
|
||||
case "password":
|
||||
if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" {
|
||||
if appKey == "mysql" || appKey == "mariadb" || appKey == "postgresql" || appKey == "mongodb" {
|
||||
envKey = "PANEL_DB_ROOT_PASSWORD="
|
||||
} else {
|
||||
envKey = "PANEL_REDIS_ROOT_PASSWORD="
|
||||
@@ -911,7 +915,7 @@ func updateInstallInfoInDB(appKey, appName, param string, value interface{}) err
|
||||
"param": strings.ReplaceAll(appInstall.Param, oldVal, newVal),
|
||||
"env": strings.ReplaceAll(appInstall.Env, oldVal, newVal),
|
||||
}, repo.WithByID(appInstall.ID))
|
||||
if appKey == "mysql" || appKey == "postgresql" {
|
||||
if appKey == "mysql" || appKey == "postgresql" || appKey == "mongodb" {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
return err
|
||||
}
|
||||
appKey := install.App.Key
|
||||
if appKey == constant.AppOpenclaw || appKey == constant.AppCopaw {
|
||||
if isAgentAppKey(appKey) {
|
||||
_ = agentRepo.DeleteByAppInstallIDWithCtx(ctx, install.ID)
|
||||
}
|
||||
|
||||
@@ -425,6 +425,8 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
|
||||
switch install.App.Key {
|
||||
case constant.AppMysql, constant.AppMariaDB, constant.AppMysqlCluster:
|
||||
_ = mysqlRepo.Delete(ctx, mysqlRepo.WithByMysqlName(install.Name))
|
||||
case constant.AppMongodb:
|
||||
_ = mongodbRepo.Delete(ctx, mongodbRepo.WithByMongodbName(install.Name))
|
||||
case constant.AppPostgresql, constant.AppPostgresqlCluster:
|
||||
_ = postgresqlRepo.Delete(ctx, postgresqlRepo.WithByPostgresqlName(install.Name))
|
||||
}
|
||||
@@ -1032,7 +1034,7 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
|
||||
}
|
||||
return
|
||||
}
|
||||
if err = fileOp.Decompress(filePath, appResourceDir, files.SdkTarGz, ""); err != nil {
|
||||
if err = fileOp.Decompress(context.Background(), filePath, appResourceDir, files.SdkTarGz, ""); err != nil {
|
||||
if logger == nil {
|
||||
global.LOG.Errorf("decompress app[%s] error %v", app.Name, err)
|
||||
} else {
|
||||
@@ -2246,10 +2248,39 @@ func isEditCompose(installed model.AppInstall) bool {
|
||||
if rawCompose == "" || err != nil {
|
||||
return false
|
||||
}
|
||||
if rawCompose != installed.DockerCompose {
|
||||
return true
|
||||
equal, err := composeEqualExceptImage(rawCompose, installed.DockerCompose)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return !equal
|
||||
}
|
||||
|
||||
func composeEqualExceptImage(expected, current string) (bool, error) {
|
||||
expectedCompose := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal([]byte(expected), &expectedCompose); err != nil {
|
||||
return false, err
|
||||
}
|
||||
currentCompose := make(map[string]interface{})
|
||||
if err := yaml.Unmarshal([]byte(current), ¤tCompose); err != nil {
|
||||
return false, err
|
||||
}
|
||||
removeComposeServiceImages(expectedCompose)
|
||||
removeComposeServiceImages(currentCompose)
|
||||
return reflect.DeepEqual(expectedCompose, currentCompose), nil
|
||||
}
|
||||
|
||||
func removeComposeServiceImages(composeMap map[string]interface{}) {
|
||||
services, ok := composeMap["services"].(map[string]interface{})
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
for _, service := range services {
|
||||
serviceMap, ok := service.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
delete(serviceMap, "image")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func getAppVersions(key string, details []model.AppDetail) []string {
|
||||
|
||||
@@ -44,10 +44,13 @@ type IBackupService interface {
|
||||
|
||||
MysqlBackup(db dto.CommonBackup) error
|
||||
PostgresqlBackup(db dto.CommonBackup) error
|
||||
MongodbBackup(db dto.CommonBackup) error
|
||||
MysqlRecover(db dto.CommonRecover) error
|
||||
PostgresqlRecover(db dto.CommonRecover) error
|
||||
MongodbRecover(db dto.CommonRecover) error
|
||||
MysqlRecoverByUpload(req dto.CommonRecover) error
|
||||
PostgresqlRecoverByUpload(req dto.CommonRecover) error
|
||||
MongodbRecoverByUpload(req dto.CommonRecover) error
|
||||
|
||||
RedisBackup(db dto.CommonBackup) error
|
||||
RedisRecover(db dto.CommonRecover) error
|
||||
|
||||
@@ -0,0 +1,588 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"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/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/task"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/i18n"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
dockerImage "github.com/docker/docker/api/types/image"
|
||||
dockerClient "github.com/docker/docker/client"
|
||||
)
|
||||
|
||||
const opensslSaltedHeader = "Salted__"
|
||||
|
||||
func (u *BackupService) MongodbBackup(req dto.CommonBackup) error {
|
||||
timeNow := time.Now().Format(constant.DateTimeSlimLayout)
|
||||
itemDir := fmt.Sprintf("database/%s/%s/%s", req.Type, req.Name, req.DetailName)
|
||||
targetDir := path.Join(global.Dir.LocalBackupDir, itemDir)
|
||||
fileName := fmt.Sprintf("%s_%s.gz", req.DetailName, timeNow+common.RandStrAndNum(5))
|
||||
|
||||
record := &model.BackupRecord{
|
||||
Type: req.Type,
|
||||
Name: req.Name,
|
||||
DetailName: req.DetailName,
|
||||
SourceAccountIDs: "1",
|
||||
DownloadAccountID: 1,
|
||||
FileDir: itemDir,
|
||||
FileName: fileName,
|
||||
TaskID: req.TaskID,
|
||||
Status: constant.StatusWaiting,
|
||||
Description: req.Description,
|
||||
}
|
||||
if err := backupRepo.CreateRecord(record); err != nil {
|
||||
global.LOG.Errorf("save backup record failed, err: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := handleMongodbBackup(req, nil, record.ID, targetDir, fileName); err != nil {
|
||||
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *BackupService) MongodbRecover(req dto.CommonRecover) error {
|
||||
return handleMongodbRecover(req, nil, false)
|
||||
}
|
||||
|
||||
func (u *BackupService) MongodbRecoverByUpload(req dto.CommonRecover) error {
|
||||
return handleMongodbRecover(req, nil, false)
|
||||
}
|
||||
|
||||
func handleMongodbBackup(req dto.CommonBackup, parentTask *task.Task, recordID uint, targetDir, fileName string) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(req.DetailName), mongodbRepo.WithByMongodbName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
|
||||
backupTask := parentTask
|
||||
if backupTask == nil {
|
||||
backupTask, err = task.NewTaskWithOps(itemName, task.TaskBackup, task.TaskScopeBackup, req.TaskID, dbItem.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
itemHandler := func(t *task.Task) error {
|
||||
return doMongodbBackup(req.Name, req.Type, req.DetailName, targetDir, fileName, req.Secret, t)
|
||||
}
|
||||
if parentTask != nil {
|
||||
return itemHandler(parentTask)
|
||||
}
|
||||
|
||||
backupTask.AddSubTaskWithOps(
|
||||
task.GetTaskName(itemName, task.TaskBackup, task.TaskScopeBackup),
|
||||
func(t *task.Task) error { return itemHandler(t) },
|
||||
nil,
|
||||
0,
|
||||
3*time.Hour,
|
||||
)
|
||||
go func() {
|
||||
if err := backupTask.Execute(); err != nil {
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
|
||||
return
|
||||
}
|
||||
backupRepo.UpdateRecordByMap(recordID, map[string]interface{}{"status": constant.StatusSuccess})
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func handleMongodbRecover(req dto.CommonRecover, parentTask *task.Task, isRollback bool) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(req.DetailName), mongodbRepo.WithByMongodbName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
itemName := fmt.Sprintf("%s[%s] - %s", req.Name, req.Type, req.DetailName)
|
||||
recoverTask := parentTask
|
||||
if recoverTask == nil {
|
||||
recoverTask, err = task.NewTaskWithOps(itemName, task.TaskRecover, task.TaskScopeBackup, req.TaskID, dbItem.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
recoverDatabase := func(t *task.Task) error {
|
||||
fileOp := files.NewFileOp()
|
||||
if !fileOp.Stat(req.File) {
|
||||
return buserr.WithName("ErrFileNotFound", req.File)
|
||||
}
|
||||
|
||||
restoreFile, cleanup, err := prepareMongodbBackupFileForRestore(req.File, req.Secret, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cleanup()
|
||||
|
||||
isOk := false
|
||||
if !isRollback {
|
||||
rollbackFile := path.Join(
|
||||
global.Dir.TmpDir,
|
||||
fmt.Sprintf("database/%s/%s_%s.gz", req.Type, req.DetailName, time.Now().Format(constant.DateTimeSlimLayout)),
|
||||
)
|
||||
if err := doMongodbBackup(req.Name, req.Type, req.DetailName, path.Dir(rollbackFile), path.Base(rollbackFile), "", t); err != nil {
|
||||
return fmt.Errorf("backup mongodb db %s for rollback before recover failed, err: %v", req.DetailName, err)
|
||||
}
|
||||
defer func() {
|
||||
if !isOk {
|
||||
global.LOG.Info("recover failed, start to rollback now")
|
||||
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, rollbackFile, t); err != nil {
|
||||
global.LOG.Errorf("rollback mongodb db %s from %s failed, err: %v", req.DetailName, rollbackFile, err)
|
||||
} else {
|
||||
global.LOG.Infof("rollback mongodb db %s from %s successful", req.DetailName, rollbackFile)
|
||||
}
|
||||
}
|
||||
_ = os.RemoveAll(rollbackFile)
|
||||
}()
|
||||
}
|
||||
|
||||
if req.DropAllCollections {
|
||||
if err := clearMongodbDatabase(req.Name, req.Type, req.DetailName, t); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := doMongodbRestore(req.Name, req.Type, req.DetailName, restoreFile, t); err != nil {
|
||||
global.LOG.Errorf("recover mongodb db %s from %s failed, err: %v", req.DetailName, restoreFile, err)
|
||||
return err
|
||||
}
|
||||
isOk = true
|
||||
return nil
|
||||
}
|
||||
if parentTask != nil {
|
||||
return recoverDatabase(parentTask)
|
||||
}
|
||||
|
||||
var timeout time.Duration
|
||||
switch req.Timeout {
|
||||
case -1:
|
||||
timeout = 0
|
||||
case 0:
|
||||
timeout = 3 * time.Hour
|
||||
default:
|
||||
timeout = time.Duration(req.Timeout) * time.Second
|
||||
}
|
||||
recoverTask.AddSubTaskWithOps(i18n.GetMsgByKey("TaskRecover"), recoverDatabase, nil, 0, timeout)
|
||||
go func() {
|
||||
_ = recoverTask.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMongodbBackup(database, dbType, dbName, targetDir, fileName, secret string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
if err := doRemoteMongodbBackup(database, dbName, targetDir, fileName, taskItem); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secret) != 0 {
|
||||
return files.OpensslEncrypt(path.Join(targetDir, fileName), secret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
if err := os.MkdirAll(targetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile := path.Join(targetDir, fileName)
|
||||
containerFile := path.Join("/tmp", fileName)
|
||||
defer func() {
|
||||
_ = cmd.NewCommandMgr().Run("docker", "exec", appInfo.ContainerName, "rm", "-f", containerFile)
|
||||
}()
|
||||
|
||||
uri := buildMongodbDumpURI(appInfo.UserName, appInfo.Password, dbName)
|
||||
cmdMgr := mongodbCmdMgr(taskItem)
|
||||
if err := cmdMgr.Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongodump",
|
||||
"--uri="+uri,
|
||||
"--archive="+containerFile,
|
||||
"--gzip",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cmdMgr.Run("docker", "cp", fmt.Sprintf("%s:%s", appInfo.ContainerName, containerFile), targetFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(secret) != 0 {
|
||||
return files.OpensslEncrypt(targetFile, secret)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doMongodbRestore(database, dbType, dbName, sourceFile string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
return doRemoteMongodbRestore(database, dbName, sourceFile, taskItem)
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
|
||||
containerFile := path.Join("/tmp", fmt.Sprintf("1panel-mongodb-restore-%s.gz", common.RandStrAndNum(8)))
|
||||
defer func() {
|
||||
_ = cmd.NewCommandMgr().Run("docker", "exec", appInfo.ContainerName, "rm", "-f", containerFile)
|
||||
}()
|
||||
|
||||
cmdMgr := mongodbCmdMgr(taskItem)
|
||||
if err := cmdMgr.Run("docker", "cp", sourceFile, fmt.Sprintf("%s:%s", appInfo.ContainerName, containerFile)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
uri := buildMongodbRestoreURI(appInfo.UserName, appInfo.Password)
|
||||
if err := cmdMgr.Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongorestore",
|
||||
"--uri="+uri,
|
||||
"--nsInclude="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsFrom="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsTo="+dbName+".*",
|
||||
"--archive="+containerFile,
|
||||
"--gzip",
|
||||
"--drop",
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareMongodbBackupFileForRestore(filePath, secret string, taskItem *task.Task) (string, func(), error) {
|
||||
isEncrypted, err := isOpenSSLEncryptedMongodbBackup(filePath)
|
||||
if err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
if !isEncrypted {
|
||||
return filePath, func() {}, nil
|
||||
}
|
||||
|
||||
if secret == "" {
|
||||
return "", nil, buserr.New("ErrBadDecrypt")
|
||||
}
|
||||
|
||||
if err := files.OpensslDecrypt(filePath, secret); err != nil {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
restoreFile := path.Join(path.Dir(filePath), "tmp_"+path.Base(filePath))
|
||||
taskItem.LogWithStatus(i18n.GetMsgByKey("Decrypt"), nil)
|
||||
return restoreFile, func() { _ = os.Remove(restoreFile) }, nil
|
||||
}
|
||||
|
||||
func isOpenSSLEncryptedMongodbBackup(filePath string) (bool, error) {
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
header := make([]byte, len(opensslSaltedHeader))
|
||||
n, err := io.ReadFull(file, header)
|
||||
if err != nil {
|
||||
if err == io.EOF || err == io.ErrUnexpectedEOF {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return n == len(opensslSaltedHeader) && string(header) == opensslSaltedHeader, nil
|
||||
}
|
||||
|
||||
func clearMongodbDatabase(database, dbType, dbName string, taskItem *task.Task) error {
|
||||
dbItem, err := mongodbRepo.Get(repo.WithByName(dbName), mongodbRepo.WithByMongodbName(database))
|
||||
if err == nil && dbItem.From == constant.AppResourceRemote {
|
||||
return clearRemoteMongodbDatabase(database, dbName, taskItem)
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(dbType, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("clear local mongodb database %s before restore", dbName))
|
||||
uri := buildMongodbRestoreURI(appInfo.UserName, appInfo.Password)
|
||||
return mongodbCmdMgr(taskItem).Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
uri,
|
||||
"--quiet",
|
||||
"--eval",
|
||||
fmt.Sprintf(`db.getSiblingDB(%q).dropDatabase()`, dbName),
|
||||
)
|
||||
}
|
||||
|
||||
func clearRemoteMongodbDatabase(database, dbName string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(context.Background())
|
||||
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("clear remote mongodb database %s before restore", dbName))
|
||||
if err := client.Database(dbName).Drop(ctx); err != nil {
|
||||
return fmt.Errorf("drop mongodb database %s failed, err: %v", dbName, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildMongodbDumpURI(username, password, dbName string) string {
|
||||
return (&url.URL{
|
||||
Scheme: "mongodb",
|
||||
User: url.UserPassword(username, password),
|
||||
Host: "127.0.0.1:27017",
|
||||
Path: "/" + dbName,
|
||||
RawQuery: "authSource=admin",
|
||||
}).String()
|
||||
}
|
||||
|
||||
func buildMongodbRestoreURI(username, password string) string {
|
||||
return (&url.URL{
|
||||
Scheme: "mongodb",
|
||||
User: url.UserPassword(username, password),
|
||||
Host: "127.0.0.1:27017",
|
||||
Path: "/",
|
||||
RawQuery: "authSource=admin",
|
||||
}).String()
|
||||
}
|
||||
|
||||
func mongodbCmdMgr(taskItem *task.Task) *cmd.CommandHelper {
|
||||
if taskItem == nil {
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(3 * time.Hour))
|
||||
}
|
||||
return cmd.NewCommandMgr(cmd.WithTimeout(3*time.Hour), cmd.WithTask(*taskItem))
|
||||
}
|
||||
|
||||
func doRemoteMongodbBackup(database, dbName, targetDir, fileName string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageTag, err := ensureMongodbImage(database, taskItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logRemoteMongodbImage(taskItem, "backup", database, dbName, imageTag, info)
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s is ready, start backup", imageTag))
|
||||
if err := os.MkdirAll(targetDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
targetFile, err := os.OpenFile(path.Join(targetDir, fileName), os.O_RDWR|os.O_CREATE|os.O_TRUNC, constant.DirPerm)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open file %s failed, err: %v", path.Join(targetDir, fileName), err)
|
||||
}
|
||||
defer func() { _ = targetFile.Close() }()
|
||||
|
||||
backupCmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--net=host",
|
||||
"-i",
|
||||
imageTag,
|
||||
"mongodump",
|
||||
"--uri="+buildRemoteMongodbURI(info),
|
||||
"--db="+dbName,
|
||||
"--archive",
|
||||
"--gzip",
|
||||
)
|
||||
backupCmd.Stdout = targetFile
|
||||
var stderr bytes.Buffer
|
||||
backupCmd.Stderr = &stderr
|
||||
if err := backupCmd.Run(); err != nil {
|
||||
return fmt.Errorf("handle backup mongodb database failed, err: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func doRemoteMongodbRestore(database, dbName, sourceFile string, taskItem *task.Task) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
imageTag, err := ensureMongodbImage(database, taskItem)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logRemoteMongodbImage(taskItem, "restore", database, dbName, imageTag, info)
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s is ready, start restore", imageTag))
|
||||
|
||||
fi, err := os.Open(sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() { _ = fi.Close() }()
|
||||
|
||||
restoreCmd := exec.Command(
|
||||
"docker",
|
||||
"run",
|
||||
"--rm",
|
||||
"--net=host",
|
||||
"-i",
|
||||
imageTag,
|
||||
"mongorestore",
|
||||
"--uri="+buildRemoteMongodbURI(info),
|
||||
"--nsInclude="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsFrom="+buildMongodbNamespace(sourceFile, dbName),
|
||||
"--nsTo="+dbName+".*",
|
||||
"--archive",
|
||||
"--gzip",
|
||||
"--drop",
|
||||
)
|
||||
restoreCmd.Stdin = fi
|
||||
var stderr bytes.Buffer
|
||||
restoreCmd.Stderr = &stderr
|
||||
if err := restoreCmd.Run(); err != nil {
|
||||
return fmt.Errorf("handle recover mongodb database failed, err: %s", strings.TrimSpace(stderr.String()))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureMongodbImage(database string, taskItem *task.Task) (string, error) {
|
||||
imageTag, exists, err := loadMongodbImageTag(database)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("check local image %s", imageTag))
|
||||
if exists {
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s exists", imageTag))
|
||||
return imageTag, nil
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("local image %s not found, start docker pull", imageTag))
|
||||
if err := mongodbCmdMgr(taskItem).Run("docker", "pull", imageTag); err != nil {
|
||||
return "", err
|
||||
}
|
||||
logRemoteMongodbStep(taskItem, fmt.Sprintf("docker pull %s finished", imageTag))
|
||||
return imageTag, nil
|
||||
}
|
||||
|
||||
func loadMongodbImageTag(database string) (string, bool, error) {
|
||||
databaseInfo, err := databaseRepo.Get(repo.WithByName(database))
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
|
||||
cli, err := dockerClient.NewClientWithOpts(dockerClient.FromEnv, dockerClient.WithAPIVersionNegotiation())
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
defer cli.Close()
|
||||
|
||||
images, err := cli.ImageList(context.Background(), dockerImage.ListOptions{})
|
||||
if err != nil {
|
||||
return "", false, err
|
||||
}
|
||||
imagePrefix := "mongo:" + loadMongodbImageMajor(databaseInfo.Version)
|
||||
for _, image := range images {
|
||||
for _, tag := range image.RepoTags {
|
||||
if strings.HasPrefix(tag, imagePrefix) {
|
||||
return tag, true, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
return imagePrefix, false, nil
|
||||
}
|
||||
|
||||
func buildMongodbNamespace(sourceFile, targetDB string) string {
|
||||
sourceDB := loadMongodbBackupDBName(sourceFile, targetDB)
|
||||
return sourceDB + ".*"
|
||||
}
|
||||
|
||||
func loadMongodbBackupDBName(sourceFile, defaultDB string) string {
|
||||
baseName := path.Base(sourceFile)
|
||||
if strings.HasSuffix(baseName, ".gz") {
|
||||
baseName = strings.TrimSuffix(baseName, ".gz")
|
||||
}
|
||||
// Encrypted backups are restored from a decrypted temp file like tmp_<original>.
|
||||
// Strip the temp prefix before deriving the original database name.
|
||||
baseName = strings.TrimPrefix(baseName, "tmp_")
|
||||
patterns := []*regexp.Regexp{
|
||||
regexp.MustCompile(`^1panel_mongodb_(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
regexp.MustCompile(`^db_(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
regexp.MustCompile(`^(.+)_\d{14}[A-Za-z0-9]*$`),
|
||||
}
|
||||
for _, pattern := range patterns {
|
||||
matches := pattern.FindStringSubmatch(baseName)
|
||||
if len(matches) == 2 && len(matches[1]) != 0 {
|
||||
return matches[1]
|
||||
}
|
||||
}
|
||||
return defaultDB
|
||||
}
|
||||
|
||||
func loadMongodbImageMajor(version string) string {
|
||||
switch {
|
||||
case strings.HasPrefix(version, "6"):
|
||||
return "6"
|
||||
case strings.HasPrefix(version, "7"):
|
||||
return "7"
|
||||
default:
|
||||
return "8"
|
||||
}
|
||||
}
|
||||
|
||||
func logRemoteMongodbImage(taskItem *task.Task, action, database, dbName, imageTag string, info mongodbConnectionInfo) {
|
||||
message := fmt.Sprintf(
|
||||
"use local docker image %s to %s remote mongodb %s/%s via %s:%d",
|
||||
imageTag,
|
||||
action,
|
||||
database,
|
||||
dbName,
|
||||
info.Address,
|
||||
info.Port,
|
||||
)
|
||||
global.LOG.Info(message)
|
||||
if taskItem != nil {
|
||||
taskItem.Log(message)
|
||||
}
|
||||
}
|
||||
|
||||
func logRemoteMongodbStep(taskItem *task.Task, message string) {
|
||||
global.LOG.Info(message)
|
||||
if taskItem != nil {
|
||||
taskItem.Log(message)
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
@@ -263,7 +264,7 @@ func loadSqlFile(file string) (string, error) {
|
||||
_ = os.RemoveAll(dstDir)
|
||||
return "", err
|
||||
}
|
||||
if err := archiver.Extract(file, dstDir, ""); err != nil {
|
||||
if err := archiver.Extract(context.Background(), file, dstDir, ""); err != nil {
|
||||
_ = os.RemoveAll(dstDir)
|
||||
return "", err
|
||||
}
|
||||
|
||||
@@ -1076,14 +1076,18 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
|
||||
}
|
||||
stdout, err := dockerCmd.StdoutPipe()
|
||||
if err != nil {
|
||||
_ = dockerCmd.Process.Signal(syscall.SIGTERM)
|
||||
return err
|
||||
}
|
||||
dockerCmd.Stderr = dockerCmd.Stdout
|
||||
if err := dockerCmd.Start(); err != nil {
|
||||
_ = dockerCmd.Process.Signal(syscall.SIGTERM)
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if dockerCmd.Process != nil {
|
||||
_ = dockerCmd.Process.Kill()
|
||||
_ = dockerCmd.Wait()
|
||||
}
|
||||
}()
|
||||
|
||||
tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
|
||||
if err != nil {
|
||||
@@ -1095,7 +1099,7 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
|
||||
global.LOG.Errorf("os.Remove() failed: %v", err)
|
||||
}
|
||||
}()
|
||||
errCh := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
scanner := bufio.NewScanner(stdout)
|
||||
var ansiRegex = re.GetRegex(re.AnsiEscapePattern)
|
||||
@@ -1118,8 +1122,8 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
|
||||
if err != nil {
|
||||
global.LOG.Errorf("Error: %v", err)
|
||||
}
|
||||
case <-time.After(3 * time.Second):
|
||||
global.LOG.Errorf("Timeout reached")
|
||||
case <-time.After(40 * time.Second):
|
||||
global.LOG.Errorf("Download container logs timeout reached")
|
||||
}
|
||||
info, _ := tempFile.Stat()
|
||||
|
||||
|
||||
@@ -287,6 +287,15 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error {
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(db.Name), repo.WithByName(db.DetailName))
|
||||
if err != nil {
|
||||
hasNotFound = true
|
||||
continue
|
||||
}
|
||||
dbIDs = append(dbIDs, fmt.Sprintf("%v", dbItem.ID))
|
||||
}
|
||||
} else {
|
||||
for _, db := range item.DBNames {
|
||||
dbItem, err := mysqlRepo.Get(mysqlRepo.WithByMysqlName(db.Name), repo.WithByName(db.DetailName))
|
||||
|
||||
@@ -165,7 +165,20 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
record.DownloadAccountID, record.SourceAccountIDs = cronjob.DownloadAccountID, cronjob.SourceAccountIDs
|
||||
|
||||
backupDir := path.Join(global.Dir.LocalBackupDir, fmt.Sprintf("tmp/database/%s/%s/%s", dbInfo.DBType, record.Name, dbInfo.Name))
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf("db_%s_%s.sql.gz", dbInfo.Name, startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5)))
|
||||
switch dbInfo.DBType {
|
||||
case constant.AppMongodb:
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf(
|
||||
"db_%s_%s.gz",
|
||||
dbInfo.Name,
|
||||
startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5),
|
||||
))
|
||||
default:
|
||||
record.FileName = simplifiedFileName(fmt.Sprintf(
|
||||
"db_%s_%s.sql.gz",
|
||||
dbInfo.Name,
|
||||
startTime.Format(constant.DateTimeSlimLayout)+common.RandStrAndNum(5),
|
||||
))
|
||||
}
|
||||
if cronjob.DBType == "mysql" || cronjob.DBType == "mariadb" || cronjob.DBType == "mysql-cluster" {
|
||||
if err := doMysqlBackup(dbInfo, backupDir, record.FileName, cronjob.Secret); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
@@ -177,6 +190,17 @@ func (u *CronjobService) handleDatabase(cronjob model.Cronjob, startTime time.Ti
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
if err := doMongodbBackup(dbInfo.Database, dbInfo.DBType, dbInfo.Name, backupDir, record.FileName, cronjob.Secret, task); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
retry++
|
||||
return err
|
||||
} else {
|
||||
task.Log(i18n.GetMsgWithDetail("IgnoreBackupErr", err.Error()))
|
||||
cleanAccountMap(accountMap)
|
||||
return nil
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if err := doPostgresqlBackup(dbInfo, backupDir, record.FileName, cronjob.Secret, taskItem); err != nil {
|
||||
if retry < int(cronjob.RetryTimes) || !cronjob.IgnoreErr {
|
||||
@@ -404,6 +428,18 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
}
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
databaseService := NewIDatabaseService()
|
||||
mongodbItems, _ := databaseService.LoadItems(cronjob.DBType)
|
||||
for _, mongodb := range mongodbItems {
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
ID: mongodb.ID,
|
||||
DBType: cronjob.DBType,
|
||||
Database: mongodb.Database,
|
||||
Name: mongodb.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
}
|
||||
} else {
|
||||
pgItems, _ := postgresqlRepo.List()
|
||||
for _, pg := range pgItems {
|
||||
@@ -430,6 +466,15 @@ func loadDbsForJob(cronjob model.Cronjob) []DatabaseHelper {
|
||||
Name: mysqlItem.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
} else if cronjob.DBType == constant.AppMongodb {
|
||||
mongodbItem, _ := mongodbRepo.Get(repo.WithByID(uint(itemID)))
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
ID: mongodbItem.ID,
|
||||
DBType: cronjob.DBType,
|
||||
Database: mongodbItem.MongodbName,
|
||||
Name: mongodbItem.Name,
|
||||
Args: strings.Split(cronjob.Args, ","),
|
||||
})
|
||||
} else {
|
||||
pgItem, _ := postgresqlRepo.Get(repo.WithByID(uint(itemID)))
|
||||
dbs = append(dbs, DatabaseHelper{
|
||||
|
||||
@@ -90,7 +90,7 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
|
||||
dbs, err := databaseRepo.GetList(databaseRepo.WithTypeList(dbType))
|
||||
var datas []dto.DatabaseItem
|
||||
for _, db := range dbs {
|
||||
if dbType == constant.AppPostgresql || dbType == constant.AppPostgresqlCluster {
|
||||
if db.Type == constant.AppPostgresql || db.Type == constant.AppPostgresqlCluster {
|
||||
items, _ := postgresqlRepo.List(postgresqlRepo.WithByPostgresqlName(db.Name))
|
||||
for _, item := range items {
|
||||
var dItem dto.DatabaseItem
|
||||
@@ -100,6 +100,16 @@ func (u *DatabaseService) LoadItems(dbType string) ([]dto.DatabaseItem, error) {
|
||||
dItem.Database = db.Name
|
||||
datas = append(datas, dItem)
|
||||
}
|
||||
} else if db.Type == constant.AppMongodb {
|
||||
items, _ := mongodbRepo.List(mongodbRepo.WithByMongodbName(db.Name))
|
||||
for _, item := range items {
|
||||
var dItem dto.DatabaseItem
|
||||
if err := copier.Copy(&dItem, &item); err != nil {
|
||||
continue
|
||||
}
|
||||
dItem.Database = db.Name
|
||||
datas = append(datas, dItem)
|
||||
}
|
||||
} else {
|
||||
items, _ := mysqlRepo.List(mysqlRepo.WithByMysqlName(db.Name))
|
||||
for _, item := range items {
|
||||
@@ -138,6 +148,13 @@ func (u *DatabaseService) CheckDatabase(req dto.DatabaseCreate) bool {
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
})
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, connectErr := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(req))
|
||||
if connectErr == nil {
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
}
|
||||
err = connectErr
|
||||
case "mysql", "mariadb":
|
||||
_, err = mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
@@ -195,6 +212,13 @@ func (u *DatabaseService) Create(req dto.DatabaseCreate) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(req))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
case "mysql", "mariadb":
|
||||
if _, err := mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
@@ -265,6 +289,10 @@ func (u *DatabaseService) Delete(req dto.DatabaseDelete) error {
|
||||
if err := mysqlRepo.Delete(context.Background(), mysqlRepo.WithByMysqlName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else if db.Type == constant.AppMongodb {
|
||||
if err := mongodbRepo.Delete(context.Background(), mongodbRepo.WithByMongodbName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
if err := postgresqlRepo.Delete(context.Background(), postgresqlRepo.WithByPostgresqlName(db.Name)); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
@@ -297,6 +325,24 @@ func (u *DatabaseService) Update(req dto.DatabaseUpdate) error {
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
case constant.AppMongodb:
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(mongodbConnectionInfoFromCreate(dto.DatabaseCreate{
|
||||
Address: req.Address,
|
||||
Port: req.Port,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
SSL: req.SSL,
|
||||
RootCert: req.RootCert,
|
||||
ClientKey: req.ClientKey,
|
||||
ClientCert: req.ClientCert,
|
||||
SkipVerify: req.SkipVerify,
|
||||
}))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
case "mysql", "mariadb":
|
||||
if _, err := mysql.NewMysqlClient(client.DBInfo{
|
||||
From: "remote",
|
||||
|
||||
@@ -0,0 +1,948 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"github.com/jinzhu/copier"
|
||||
"go.mongodb.org/mongo-driver/bson"
|
||||
)
|
||||
|
||||
type MongodbService struct{}
|
||||
|
||||
type IMongodbService interface {
|
||||
SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error)
|
||||
Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error)
|
||||
LoadFromRemote(req dto.MongodbLoadDB) error
|
||||
UpdateDescription(req dto.UpdateDescription) error
|
||||
BindUser(req dto.MongodbBind) error
|
||||
ChangePassword(req dto.MongodbPassword) error
|
||||
ChangeRootPassword(req dto.ChangeDBInfo) error
|
||||
LoadPrivileges(req dto.MongodbPrivilegesLoad) (string, error)
|
||||
ChangePrivileges(req dto.MongodbPrivileges) error
|
||||
DeleteCheck(req dto.MongodbDBDeleteCheck) ([]dto.DBResource, error)
|
||||
Delete(ctx context.Context, req dto.MongodbDBDelete) error
|
||||
}
|
||||
|
||||
func NewIMongodbService() IMongodbService {
|
||||
return &MongodbService{}
|
||||
}
|
||||
|
||||
func (u *MongodbService) SearchWithPage(search dto.MongodbDBSearch) (int64, interface{}, error) {
|
||||
total, mongodbs, err := mongodbRepo.Page(
|
||||
search.Page,
|
||||
search.PageSize,
|
||||
mongodbRepo.WithByMongodbName(search.Database),
|
||||
repo.WithByLikeName(search.Info),
|
||||
repo.WithOrderRuleBy(search.OrderBy, search.Order),
|
||||
)
|
||||
var dtoMongodbs []dto.MongodbDBInfo
|
||||
for _, mongodb := range mongodbs {
|
||||
var item dto.MongodbDBInfo
|
||||
if err := copier.Copy(&item, &mongodb); err != nil {
|
||||
return 0, nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
dtoMongodbs = append(dtoMongodbs, item)
|
||||
}
|
||||
return total, dtoMongodbs, err
|
||||
}
|
||||
|
||||
func (u *MongodbService) UpdateDescription(req dto.UpdateDescription) error {
|
||||
return mongodbRepo.Update(req.ID, map[string]interface{}{"description": req.Description})
|
||||
}
|
||||
|
||||
func (u *MongodbService) Create(ctx context.Context, req dto.MongodbDBCreate) (*model.DatabaseMongodb, error) {
|
||||
if cmd.CheckIllegal(req.Name, req.Username, req.Password) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
return nil, buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
mongodb, _ := mongodbRepo.Get(repo.WithByName(req.Name), mongodbRepo.WithByMongodbName(req.Database), repo.WithByFrom(req.From))
|
||||
if mongodb.ID != 0 {
|
||||
return nil, buserr.New("ErrRecordExist")
|
||||
}
|
||||
|
||||
var createItem model.DatabaseMongodb
|
||||
if err := copier.Copy(&createItem, &req); err != nil {
|
||||
return nil, buserr.WithDetail("ErrStructTransform", err.Error(), nil)
|
||||
}
|
||||
createItem.MongodbName = req.Database
|
||||
|
||||
if err := runMongodbCreate(req.From, req.Database, req.Name, req.Username, req.Password, req.Permission); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
global.LOG.Infof("create mongodb database %s successful", req.Name)
|
||||
if err := mongodbRepo.Create(ctx, &createItem); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &createItem, nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) LoadFromRemote(req dto.MongodbLoadDB) error {
|
||||
databases, err := mongodbRepo.List(mongodbRepo.WithByMongodbName(req.Database))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
datas, err := loadMongodbDatabases(req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
global.LOG.Infof("sync mongodb databases from %s:%s, found %d items", req.From, req.Database, len(datas))
|
||||
|
||||
deleteList := append([]model.DatabaseMongodb(nil), databases...)
|
||||
for _, data := range datas {
|
||||
hasOld := false
|
||||
for i := 0; i < len(databases); i++ {
|
||||
if strings.EqualFold(databases[i].Name, data.Name) && strings.EqualFold(databases[i].MongodbName, req.Database) {
|
||||
hasOld = true
|
||||
updateMap := map[string]interface{}{"is_delete": false}
|
||||
if len(data.Username) != 0 {
|
||||
updateMap["username"] = data.Username
|
||||
}
|
||||
_ = mongodbRepo.Update(databases[i].ID, updateMap)
|
||||
for j := 0; j < len(deleteList); j++ {
|
||||
if deleteList[j].ID == databases[i].ID {
|
||||
deleteList = append(deleteList[:j], deleteList[j+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if !hasOld {
|
||||
createItem := model.DatabaseMongodb{
|
||||
Name: data.Name,
|
||||
From: req.From,
|
||||
MongodbName: req.Database,
|
||||
Username: data.Username,
|
||||
Password: "",
|
||||
Description: "",
|
||||
}
|
||||
if err := mongodbRepo.Create(context.Background(), &createItem); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
for _, delItem := range deleteList {
|
||||
_ = mongodbRepo.Update(delItem.ID, map[string]interface{}{"is_delete": true})
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) BindUser(req dto.MongodbBind) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Username, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindMongodbUser(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", req.Name, err)
|
||||
}
|
||||
return mongodbRepo.Update(dbItem.ID, map[string]interface{}{
|
||||
"username": req.Username,
|
||||
"password": pass,
|
||||
})
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePassword(req dto.MongodbPassword) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Name, req.Password) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if dbItem.Username == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := updateMongodbPassword(dbItem.From, dbItem.MongodbName, dbItem.Name, dbItem.Username, req.Password); err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Password)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb db %s password failed, err: %v", req.Name, err)
|
||||
}
|
||||
return mongodbRepo.Update(dbItem.ID, map[string]interface{}{"password": pass})
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangeRootPassword(req dto.ChangeDBInfo) error {
|
||||
if cmd.CheckIllegal(req.Value) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if req.From != constant.AppResourceLocal {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.UserName == "" {
|
||||
return buserr.New("ErrRecordNotFound")
|
||||
}
|
||||
if err := updateMongodbPassword(req.From, req.Database, "admin", appInfo.UserName, req.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := updateInstallInfoInDB(req.Type, req.Database, "password", req.Value); err != nil {
|
||||
return err
|
||||
}
|
||||
remote, err := databaseRepo.Get(repo.WithByName(req.Database))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
pass, err := encrypt.StringEncrypt(req.Value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("encrypt mongodb root password failed, err: %v", err)
|
||||
}
|
||||
_ = databaseRepo.Update(remote.ID, map[string]interface{}{"password": pass})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) DeleteCheck(req dto.MongodbDBDeleteCheck) ([]dto.DBResource, error) {
|
||||
var res []dto.DBResource
|
||||
db, err := mongodbRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
|
||||
if db.From == "local" {
|
||||
app, err := appInstallRepo.LoadBaseInfo(req.Type, req.Database)
|
||||
if err != nil {
|
||||
return res, err
|
||||
}
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithLinkId(app.ID), appInstallResourceRepo.WithResourceId(db.ID))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
Name: appInstall.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
} else {
|
||||
apps, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithResourceId(db.ID), appRepo.WithKey(req.Type))
|
||||
for _, app := range apps {
|
||||
appInstall, _ := appInstallRepo.GetFirst(repo.WithByID(app.AppInstallId))
|
||||
if appInstall.ID != 0 {
|
||||
res = append(res, dto.DBResource{
|
||||
Type: constant.TypeApp,
|
||||
Name: appInstall.Name,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return res, nil
|
||||
}
|
||||
|
||||
func (u *MongodbService) LoadPrivileges(req dto.MongodbPrivilegesLoad) (string, error) {
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return loadMongodbPrivilege(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username)
|
||||
}
|
||||
|
||||
func (u *MongodbService) ChangePrivileges(req dto.MongodbPrivileges) error {
|
||||
if cmd.CheckIllegal(req.Database, req.Username) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
if !isSupportedMongodbPrivilege(req.Permission) {
|
||||
return buserr.New("ErrCmdIllegal")
|
||||
}
|
||||
dbItem, err := mongodbRepo.Get(mongodbRepo.WithByMongodbName(req.Database), repo.WithByName(req.Name))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return updateMongodbPrivilege(dbItem.From, dbItem.MongodbName, dbItem.Name, req.Username, req.Permission)
|
||||
}
|
||||
|
||||
func (u *MongodbService) Delete(ctx context.Context, req dto.MongodbDBDelete) error {
|
||||
db, err := mongodbRepo.Get(repo.WithByID(req.ID))
|
||||
if err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := runMongodbDelete(db.From, req.Database, db.Name); err != nil && !req.ForceDelete {
|
||||
return err
|
||||
}
|
||||
|
||||
if req.DeleteBackup {
|
||||
uploadDir := filepath.Join(global.Dir.DataDir, fmt.Sprintf("uploads/database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
if _, err := os.Stat(uploadDir); err == nil {
|
||||
_ = os.RemoveAll(uploadDir)
|
||||
}
|
||||
backupDir := filepath.Join(global.Dir.LocalBackupDir, fmt.Sprintf("database/%s/%s/%s", req.Type, req.Database, db.Name))
|
||||
if _, err := os.Stat(backupDir); err == nil {
|
||||
_ = os.RemoveAll(backupDir)
|
||||
}
|
||||
_ = backupRepo.DeleteRecord(ctx, repo.WithByType(req.Type), repo.WithByName(req.Database), repo.WithByDetailName(db.Name))
|
||||
global.LOG.Infof("delete mongodb database %s-%s backups successful", req.Database, db.Name)
|
||||
}
|
||||
|
||||
_ = mongodbRepo.Delete(ctx, repo.WithByID(db.ID))
|
||||
return nil
|
||||
}
|
||||
|
||||
func runMongodbCreate(from, database, dbName, username, password, permission string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return runRemoteMongodbCreate(database, dbName, username, password, permission)
|
||||
}
|
||||
script, err := buildMongodbCreateScript(dbName, username, password, permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(database, script)
|
||||
}
|
||||
|
||||
func runMongodbDelete(from, database, dbName string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return runRemoteMongodbDelete(database, dbName)
|
||||
}
|
||||
script, err := buildMongodbDeleteScript(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(database, script)
|
||||
}
|
||||
|
||||
func bindMongodbUser(from, connectionName, dbName, username, password string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return bindRemoteMongodbUser(connectionName, dbName, username, password)
|
||||
}
|
||||
script, err := buildMongodbBindUserScript(dbName, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func updateMongodbPassword(from, connectionName, dbName, username, password string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return updateRemoteMongodbPasswordOnly(connectionName, dbName, username, password)
|
||||
}
|
||||
script, err := buildMongodbPasswordScript(dbName, username, password)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func runMongodbAdminScript(database, script string) error {
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMongodb, database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
return cmd.NewCommandMgr().Run(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
buildMongodbRestoreURI(appInfo.UserName, appInfo.Password),
|
||||
"--quiet",
|
||||
"--eval",
|
||||
script,
|
||||
)
|
||||
}
|
||||
|
||||
func runMongodbAdminScriptWithStdout(database, script string) (string, error) {
|
||||
appInfo, err := appInstallRepo.LoadBaseInfo(constant.AppMongodb, database)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if appInfo.ContainerName == "" {
|
||||
return "", fmt.Errorf("mongodb container not found for database %s", database)
|
||||
}
|
||||
return cmd.NewCommandMgr().RunWithStdout(
|
||||
"docker",
|
||||
"exec",
|
||||
appInfo.ContainerName,
|
||||
"mongosh",
|
||||
buildMongodbRestoreURI(appInfo.UserName, appInfo.Password),
|
||||
"--quiet",
|
||||
"--eval",
|
||||
script,
|
||||
)
|
||||
}
|
||||
|
||||
func buildMongodbCreateScript(dbName, username, password, permission string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
permissionJSON, err := json.Marshal(permission)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const permission = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
targetDb.createCollection("_init");
|
||||
targetDb.createUser({
|
||||
user: userName,
|
||||
pwd: password,
|
||||
roles: [{ role: permission, db: dbName }]
|
||||
});
|
||||
`, dbNameJSON, usernameJSON, passwordJSON, permissionJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbDeleteScript(dbName string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const dropUsersResult = targetDb.runCommand({ dropAllUsersFromDatabase: 1 });
|
||||
if (!dropUsersResult || dropUsersResult.ok !== 1) {
|
||||
throw new Error("failed to drop users from " + dbName);
|
||||
}
|
||||
const dropDatabaseResult = targetDb.runCommand({ dropDatabase: 1 });
|
||||
if (!dropDatabaseResult || dropDatabaseResult.ok !== 1) {
|
||||
throw new Error("failed to drop database " + dbName);
|
||||
}
|
||||
`, dbNameJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbBindUserScript(dbName, username, password string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const userInfo = targetDb.runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!userInfo || userInfo.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user " + userName);
|
||||
}
|
||||
const roles = [{ role: "readWrite", db: dbName }];
|
||||
if (Array.isArray(userInfo.users) && userInfo.users.length > 0) {
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
pwd: password,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user " + userName);
|
||||
}
|
||||
} else {
|
||||
const result = targetDb.runCommand({
|
||||
createUser: userName,
|
||||
pwd: password,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to create mongodb user " + userName);
|
||||
}
|
||||
}
|
||||
`, dbNameJSON, usernameJSON, passwordJSON)), nil
|
||||
}
|
||||
|
||||
func buildMongodbPasswordScript(dbName, username, password string) (string, error) {
|
||||
dbNameJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
passwordJSON, err := json.Marshal(password)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const password = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
pwd: password
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user password " + userName);
|
||||
}
|
||||
`, dbNameJSON, usernameJSON, passwordJSON)), nil
|
||||
}
|
||||
|
||||
type mongodbSyncItem struct {
|
||||
Name string `json:"name"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
||||
func loadMongodbDatabases(req dto.MongodbLoadDB) ([]mongodbSyncItem, error) {
|
||||
if req.From == constant.AppResourceRemote {
|
||||
return loadRemoteMongodbDatabases(req.Database)
|
||||
}
|
||||
return loadLocalMongodbDatabases(req.Database)
|
||||
}
|
||||
|
||||
func loadLocalMongodbDatabases(database string) ([]mongodbSyncItem, error) {
|
||||
script := strings.TrimSpace(`
|
||||
const systemDbs = new Set(["admin", "config", "local"]);
|
||||
const result = db.adminCommand({ listDatabases: 1, nameOnly: false });
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to list mongodb databases");
|
||||
}
|
||||
const items = result.databases
|
||||
.filter(item => !systemDbs.has(item.name))
|
||||
.map(item => ({ name: item.name, username: "" }));
|
||||
print("__1panel_json_begin__");
|
||||
print(JSON.stringify(items));
|
||||
print("__1panel_json_end__");
|
||||
`)
|
||||
stdout, err := runMongodbAdminScriptWithStdout(database, script)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items := make([]mongodbSyncItem, 0)
|
||||
jsonResult, err := extractMongodbJSONOutput(stdout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := json.Unmarshal(jsonResult, &items); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func runRemoteMongodbCreate(database, dbName, username, password, permission string) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
if err := targetDB.CreateCollection(ctx, "_init"); err != nil {
|
||||
return err
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: permission}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func runRemoteMongodbDelete(database, dbName string) error {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
if err := targetDB.RunCommand(ctx, bson.D{{Key: "dropAllUsersFromDatabase", Value: 1}}).Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{{Key: "dropDatabase", Value: 1}}).Err()
|
||||
}
|
||||
|
||||
func bindRemoteMongodbUser(connectionName, dbName, username, password string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var userInfo struct {
|
||||
Users []struct{} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(userInfo.Users) > 0 {
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: "readWrite"}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "createUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
{Key: "roles", Value: bson.A{
|
||||
bson.D{{Key: "role", Value: "readWrite"}, {Key: "db", Value: dbName}},
|
||||
}},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func updateRemoteMongodbPasswordOnly(connectionName, dbName, username, password string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "pwd", Value: password},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func loadRemoteMongodbDatabases(database string) ([]mongodbSyncItem, error) {
|
||||
info, err := loadRemoteMongodbConnection(database)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
adminDB := client.Database("admin")
|
||||
var listResult struct {
|
||||
Databases []struct {
|
||||
Name string `bson:"name"`
|
||||
} `bson:"databases"`
|
||||
}
|
||||
if err := adminDB.RunCommand(ctx, bson.D{
|
||||
{Key: "listDatabases", Value: 1},
|
||||
{Key: "nameOnly", Value: false},
|
||||
}).Decode(&listResult); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
systemDbs := map[string]struct{}{
|
||||
"admin": {},
|
||||
"config": {},
|
||||
"local": {},
|
||||
}
|
||||
items := make([]mongodbSyncItem, 0, len(listResult.Databases))
|
||||
for _, databaseItem := range listResult.Databases {
|
||||
if _, ok := systemDbs[databaseItem.Name]; ok {
|
||||
continue
|
||||
}
|
||||
items = append(items, mongodbSyncItem{
|
||||
Name: databaseItem.Name,
|
||||
Username: "",
|
||||
})
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func loadMongodbPrivilege(from, connectionName, dbName, username string) (string, error) {
|
||||
if from == constant.AppResourceRemote {
|
||||
return loadRemoteMongodbPrivilege(connectionName, dbName, username)
|
||||
}
|
||||
return loadLocalMongodbPrivilege(connectionName, dbName, username)
|
||||
}
|
||||
|
||||
func updateMongodbPrivilege(from, connectionName, dbName, username, permission string) error {
|
||||
if from == constant.AppResourceRemote {
|
||||
return updateRemoteMongodbPrivilege(connectionName, dbName, username, permission)
|
||||
}
|
||||
return updateLocalMongodbPrivilege(connectionName, dbName, username, permission)
|
||||
}
|
||||
|
||||
func loadLocalMongodbPrivilege(connectionName, dbName, username string) (string, error) {
|
||||
databaseJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
script := strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const result = db.getSiblingDB(dbName).runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user privileges");
|
||||
}
|
||||
const roles = Array.isArray(result.users) && result.users.length > 0 ? result.users[0].roles || [] : [];
|
||||
const permissions = roles.filter(role => role.db === dbName).map(role => role.role);
|
||||
print("__1panel_json_begin__");
|
||||
print(JSON.stringify(permissions));
|
||||
print("__1panel_json_end__");
|
||||
`, databaseJSON, usernameJSON))
|
||||
stdout, err := runMongodbAdminScriptWithStdout(connectionName, script)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
var permissions []string
|
||||
jsonResult, err := extractMongodbJSONOutput(stdout)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if err := json.Unmarshal(jsonResult, &permissions); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return normalizeMongodbPrivilege(permissions), nil
|
||||
}
|
||||
|
||||
func updateLocalMongodbPrivilege(connectionName, dbName, username, permission string) error {
|
||||
databaseJSON, err := json.Marshal(dbName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
usernameJSON, err := json.Marshal(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
permissionJSON, err := json.Marshal(permission)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
script := strings.TrimSpace(fmt.Sprintf(`
|
||||
const dbName = %s;
|
||||
const userName = %s;
|
||||
const permission = %s;
|
||||
const targetDb = db.getSiblingDB(dbName);
|
||||
const userInfo = targetDb.runCommand({
|
||||
usersInfo: userName,
|
||||
showCredentials: false,
|
||||
showCustomData: false
|
||||
});
|
||||
if (!userInfo || userInfo.ok !== 1) {
|
||||
throw new Error("failed to load mongodb user privileges");
|
||||
}
|
||||
if (!Array.isArray(userInfo.users) || userInfo.users.length === 0) {
|
||||
throw new Error("mongodb user not found: " + userName);
|
||||
}
|
||||
const roles = (userInfo.users[0].roles || []).filter(role => role.db !== dbName);
|
||||
roles.push({ role: permission, db: dbName });
|
||||
const result = targetDb.runCommand({
|
||||
updateUser: userName,
|
||||
roles: roles
|
||||
});
|
||||
if (!result || result.ok !== 1) {
|
||||
throw new Error("failed to update mongodb user privileges");
|
||||
}
|
||||
`, databaseJSON, usernameJSON, permissionJSON))
|
||||
return runMongodbAdminScript(connectionName, script)
|
||||
}
|
||||
|
||||
func loadRemoteMongodbPrivilege(connectionName, dbName, username string) (string, error) {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var result struct {
|
||||
Users []struct {
|
||||
Roles []struct {
|
||||
Role string `bson:"role"`
|
||||
DB string `bson:"db"`
|
||||
} `bson:"roles"`
|
||||
} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&result); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
permissions := make([]string, 0)
|
||||
if len(result.Users) > 0 {
|
||||
for _, role := range result.Users[0].Roles {
|
||||
if role.DB == dbName {
|
||||
permissions = append(permissions, role.Role)
|
||||
}
|
||||
}
|
||||
}
|
||||
return normalizeMongodbPrivilege(permissions), nil
|
||||
}
|
||||
|
||||
func updateRemoteMongodbPrivilege(connectionName, dbName, username, permission string) error {
|
||||
info, err := loadRemoteMongodbConnection(connectionName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
client, ctx, cancel, err := newRemoteMongodbClient(info)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer cancel()
|
||||
defer client.Disconnect(ctx)
|
||||
|
||||
targetDB := client.Database(dbName)
|
||||
var userInfo struct {
|
||||
Users []struct {
|
||||
Roles []struct {
|
||||
Role string `bson:"role"`
|
||||
DB string `bson:"db"`
|
||||
} `bson:"roles"`
|
||||
} `bson:"users"`
|
||||
}
|
||||
if err := targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "usersInfo", Value: username},
|
||||
{Key: "showCredentials", Value: false},
|
||||
{Key: "showCustomData", Value: false},
|
||||
}).Decode(&userInfo); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(userInfo.Users) == 0 {
|
||||
return fmt.Errorf("mongodb user %s not found in database %s", username, dbName)
|
||||
}
|
||||
roles := make(bson.A, 0, len(userInfo.Users[0].Roles)+1)
|
||||
for _, role := range userInfo.Users[0].Roles {
|
||||
if role.DB == dbName {
|
||||
continue
|
||||
}
|
||||
roles = append(roles, bson.D{{Key: "role", Value: role.Role}, {Key: "db", Value: role.DB}})
|
||||
}
|
||||
roles = append(roles, bson.D{{Key: "role", Value: permission}, {Key: "db", Value: dbName}})
|
||||
return targetDB.RunCommand(ctx, bson.D{
|
||||
{Key: "updateUser", Value: username},
|
||||
{Key: "roles", Value: roles},
|
||||
}).Err()
|
||||
}
|
||||
|
||||
func normalizeMongodbPrivilege(permissions []string) string {
|
||||
roleMap := map[string]struct{}{
|
||||
"dbOwner": {},
|
||||
"read": {},
|
||||
"readWrite": {},
|
||||
"userAdmin": {},
|
||||
}
|
||||
for _, permission := range permissions {
|
||||
if _, ok := roleMap[permission]; ok {
|
||||
return permission
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func isSupportedMongodbPrivilege(permission string) bool {
|
||||
switch permission {
|
||||
case "dbOwner", "read", "readWrite", "userAdmin":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func extractMongodbJSONOutput(stdout string) ([]byte, error) {
|
||||
const (
|
||||
beginMark = "__1panel_json_begin__"
|
||||
endMark = "__1panel_json_end__"
|
||||
)
|
||||
|
||||
output := strings.TrimSpace(stdout)
|
||||
if output == "" {
|
||||
return nil, fmt.Errorf("empty mongodb command output")
|
||||
}
|
||||
|
||||
beginIndex := strings.Index(output, beginMark)
|
||||
endIndex := strings.LastIndex(output, endMark)
|
||||
if beginIndex >= 0 && endIndex > beginIndex {
|
||||
content := strings.TrimSpace(output[beginIndex+len(beginMark) : endIndex])
|
||||
if content != "" {
|
||||
return []byte(content), nil
|
||||
}
|
||||
}
|
||||
|
||||
lines := strings.Split(output, "\n")
|
||||
for i := len(lines) - 1; i >= 0; i-- {
|
||||
line := strings.TrimSpace(lines[i])
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
return []byte(line), nil
|
||||
}
|
||||
return nil, fmt.Errorf("mongodb command output does not contain json result")
|
||||
}
|
||||
@@ -49,6 +49,7 @@ func (u *DeviceService) Scan() dto.CleanData {
|
||||
Size: uint64(originalSize),
|
||||
IsCheck: originalSize > 0,
|
||||
IsRecommend: true,
|
||||
CanDelete: false,
|
||||
Type: "1panel_original",
|
||||
Children: loadTreeWithDir(true, "1panel_original", originalPath, fileOp),
|
||||
})
|
||||
@@ -68,6 +69,7 @@ func (u *DeviceService) Scan() dto.CleanData {
|
||||
Size: rollbackSize,
|
||||
IsCheck: rollbackSize > 0,
|
||||
IsRecommend: true,
|
||||
CanDelete: false,
|
||||
Type: "rollback",
|
||||
Children: rollBackTree,
|
||||
})
|
||||
@@ -105,11 +107,7 @@ func (u *DeviceService) Clean(req []dto.Clean) {
|
||||
case "tmp_backup":
|
||||
dropFileOrDir(path.Join(global.Dir.LocalBackupDir, "tmp"))
|
||||
case "unknown_backup":
|
||||
if strings.HasPrefix(item.Name, path.Join(global.Dir.LocalBackupDir, "log/website")) {
|
||||
dropFileOrDir(item.Name)
|
||||
} else {
|
||||
dropFile(item.Name)
|
||||
}
|
||||
dropFileOrDir(item.Name)
|
||||
|
||||
case "rollback":
|
||||
dropFileOrDir(path.Join(global.Dir.BaseDir, rollbackPath, "app"))
|
||||
@@ -269,6 +267,7 @@ func loadUpgradeTree(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Size: uint64(upgradeSize),
|
||||
IsCheck: false,
|
||||
IsRecommend: true,
|
||||
CanDelete: false,
|
||||
Type: "upgrade",
|
||||
Children: loadTreeWithDir(true, "upgrade", upgradePath, fileOp),
|
||||
}
|
||||
@@ -302,6 +301,7 @@ func loadAgentPackage(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Label: "agent_packages",
|
||||
IsCheck: false,
|
||||
IsRecommend: true,
|
||||
CanDelete: false,
|
||||
Type: "agent",
|
||||
}
|
||||
files, _ := os.ReadDir(pathItem)
|
||||
@@ -316,6 +316,7 @@ func loadAgentPackage(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Size: uint64(itemSize),
|
||||
IsCheck: true,
|
||||
IsRecommend: true,
|
||||
CanDelete: true,
|
||||
Type: "agent",
|
||||
})
|
||||
} else {
|
||||
@@ -333,6 +334,7 @@ func loadAgentPackage(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Size: uint64(itemSize.Size()),
|
||||
IsCheck: !isCurrentVersion,
|
||||
IsRecommend: true,
|
||||
CanDelete: true,
|
||||
Type: "agent",
|
||||
})
|
||||
}
|
||||
@@ -353,6 +355,7 @@ func loadBackupTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
Size: uint64(tmpSize),
|
||||
IsCheck: tmpSize != 0,
|
||||
IsRecommend: true,
|
||||
CanDelete: true,
|
||||
Type: "tmp_backup",
|
||||
})
|
||||
backupRecords, _ := backupRepo.ListRecord()
|
||||
@@ -391,6 +394,7 @@ func loadUnknownApps(fileOp fileUtils.FileOp, recordMap map[string][]string) dto
|
||||
Label: "unknown_app",
|
||||
IsCheck: false,
|
||||
IsRecommend: false,
|
||||
CanDelete: false,
|
||||
Name: backupPath,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
@@ -419,6 +423,7 @@ func loadUnknownDbs(fileOp fileUtils.FileOp, recordMap map[string][]string) dto.
|
||||
Name: backupPath,
|
||||
IsCheck: false,
|
||||
IsRecommend: false,
|
||||
CanDelete: false,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
|
||||
@@ -442,6 +447,7 @@ func loadUnknownWebsites(fileOp fileUtils.FileOp, recordMap map[string][]string)
|
||||
Name: backupPath,
|
||||
IsCheck: false,
|
||||
IsRecommend: false,
|
||||
CanDelete: false,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
_ = loadFileOrDirWithExclude(fileOp, 0, backupPath, &treeData, excludePaths)
|
||||
@@ -460,6 +466,7 @@ func loadUnknownSnapshot(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Name: backupPath,
|
||||
IsCheck: false,
|
||||
IsRecommend: false,
|
||||
CanDelete: false,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
entries, _ := os.ReadDir(backupPath)
|
||||
@@ -473,6 +480,7 @@ func loadUnknownSnapshot(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Label: entry.Name(),
|
||||
IsCheck: false,
|
||||
IsRecommend: false,
|
||||
CanDelete: true,
|
||||
Name: childPath,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
@@ -499,6 +507,7 @@ func loadUnknownWebsiteLog(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Label: "unknown_website_log",
|
||||
IsCheck: false,
|
||||
IsRecommend: true,
|
||||
CanDelete: false,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
dir := path.Join(global.Dir.LocalBackupDir, "log/website")
|
||||
@@ -522,6 +531,7 @@ func loadUnknownWebsiteLog(fileOp fileUtils.FileOp) dto.CleanTree {
|
||||
Label: dirName,
|
||||
IsCheck: true,
|
||||
IsRecommend: true,
|
||||
CanDelete: true,
|
||||
Name: dirPath,
|
||||
Type: "unknown_backup",
|
||||
Size: uint64(itemSize),
|
||||
@@ -552,6 +562,7 @@ func loadFileOrDirWithExclude(fileOp fileUtils.FileOp, index uint, dir string, r
|
||||
Label: entry.Name(),
|
||||
IsCheck: false,
|
||||
IsRecommend: false,
|
||||
CanDelete: true,
|
||||
Name: childPath,
|
||||
Type: "unknown_backup",
|
||||
}
|
||||
@@ -564,18 +575,19 @@ func loadFileOrDirWithExclude(fileOp fileUtils.FileOp, index uint, dir string, r
|
||||
for _, child := range childNode.Children {
|
||||
childNode.Size += child.Size
|
||||
}
|
||||
rootTree.Size += childNode.Size
|
||||
} else {
|
||||
itemSize, _ := fileOp.GetDirSize(childPath)
|
||||
childNode.Size = uint64(itemSize)
|
||||
rootTree.Size += childNode.Size
|
||||
}
|
||||
if childNode.Size == 0 {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
info, _ := entry.Info()
|
||||
childNode.Size = uint64(info.Size())
|
||||
rootTree.Size += childNode.Size
|
||||
}
|
||||
|
||||
rootTree.Size += childNode.Size
|
||||
rootTree.Children = append(rootTree.Children, childNode)
|
||||
}
|
||||
return nil
|
||||
@@ -633,6 +645,7 @@ func loadDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
Label: "app_tmp_download",
|
||||
IsCheck: true,
|
||||
IsRecommend: true,
|
||||
CanDelete: false,
|
||||
Type: "app_tmp_download",
|
||||
Name: "apps",
|
||||
}
|
||||
@@ -653,19 +666,19 @@ func loadLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
for _, file := range list1 {
|
||||
size += file.Size
|
||||
}
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "system_log", Size: size, Children: list1, Type: "system_log", IsRecommend: true})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "system_log", Size: size, Children: list1, Type: "system_log", IsRecommend: true, CanDelete: false})
|
||||
|
||||
path2 := path.Join(global.Dir.TaskDir)
|
||||
list2 := loadTreeWithDir(false, "task_log", path2, fileOp)
|
||||
size2, _ := fileOp.GetDirSize(path2)
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "task_log", Size: uint64(size2), Children: list2, Type: "task_log"})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "task_log", Size: uint64(size2), Children: list2, Type: "task_log", CanDelete: false})
|
||||
|
||||
websiteLogList := loadWebsiteLogTree(fileOp)
|
||||
logTotalSize := uint64(0)
|
||||
for _, websiteLog := range websiteLogList {
|
||||
logTotalSize += websiteLog.Size
|
||||
}
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "website_log", Size: logTotalSize, Children: websiteLogList, Type: "website_log", IsRecommend: false})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "website_log", Size: logTotalSize, Children: websiteLogList, Type: "website_log", IsRecommend: false, CanDelete: false})
|
||||
|
||||
return treeData
|
||||
}
|
||||
@@ -679,11 +692,12 @@ func loadWebsiteLogTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
for _, website := range websites {
|
||||
size3, _ := fileOp.GetDirSize(path.Join(GetSiteDir(website.Alias), "log"))
|
||||
res = append(res, dto.CleanTree{
|
||||
ID: uuid.NewString(),
|
||||
Label: website.PrimaryDomain,
|
||||
Size: uint64(size3),
|
||||
Type: "website_log",
|
||||
Name: website.Alias,
|
||||
ID: uuid.NewString(),
|
||||
Label: website.PrimaryDomain,
|
||||
Size: uint64(size3),
|
||||
Type: "website_log",
|
||||
Name: website.Alias,
|
||||
CanDelete: true,
|
||||
})
|
||||
}
|
||||
return res
|
||||
@@ -733,6 +747,7 @@ func loadAppTmpDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
appTree.Name = appKey
|
||||
appTree.IsRecommend = true
|
||||
appTree.IsCheck = true
|
||||
appTree.CanDelete = false
|
||||
for _, version := range missingVersions {
|
||||
versionPath := filepath.Join(appPath, version)
|
||||
size, _ := fileOp.GetDirSize(versionPath)
|
||||
@@ -743,6 +758,7 @@ func loadAppTmpDownloadTree(fileOp fileUtils.FileOp) []dto.CleanTree {
|
||||
Size: uint64(size),
|
||||
IsCheck: true,
|
||||
IsRecommend: true,
|
||||
CanDelete: true,
|
||||
Type: "app_tmp_download_version",
|
||||
Name: path.Join(appKey, version),
|
||||
})
|
||||
@@ -770,7 +786,7 @@ func loadContainerTree() []dto.CleanTree {
|
||||
imageSize += uint64(file.Size)
|
||||
}
|
||||
}
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: nil, Type: "images", IsRecommend: true})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_images", Size: imageSize, Children: nil, Type: "images", IsRecommend: true, CanDelete: true})
|
||||
|
||||
containerSize := uint64(0)
|
||||
for _, file := range diskUsage.Containers {
|
||||
@@ -778,7 +794,7 @@ func loadContainerTree() []dto.CleanTree {
|
||||
containerSize += uint64(file.SizeRw)
|
||||
}
|
||||
}
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: nil, Type: "containers", IsRecommend: true})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_containers", Size: containerSize, Children: nil, Type: "containers", IsRecommend: true, CanDelete: true})
|
||||
|
||||
volumeSize := uint64(0)
|
||||
for _, file := range diskUsage.Volumes {
|
||||
@@ -786,7 +802,7 @@ func loadContainerTree() []dto.CleanTree {
|
||||
volumeSize += uint64(file.UsageData.Size)
|
||||
}
|
||||
}
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, IsCheck: volumeSize > 0, Children: nil, Type: "volumes", IsRecommend: true})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "container_volumes", Size: volumeSize, IsCheck: volumeSize > 0, Children: nil, Type: "volumes", IsRecommend: true, CanDelete: true})
|
||||
|
||||
var buildCacheTotalSize int64
|
||||
for _, cache := range diskUsage.BuildCache {
|
||||
@@ -794,7 +810,7 @@ func loadContainerTree() []dto.CleanTree {
|
||||
buildCacheTotalSize += cache.Size
|
||||
}
|
||||
}
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), IsCheck: buildCacheTotalSize > 0, Type: "build_cache", IsRecommend: true})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: "build_cache", Size: uint64(buildCacheTotalSize), IsCheck: buildCacheTotalSize > 0, Type: "build_cache", IsRecommend: true, CanDelete: true})
|
||||
return treeData
|
||||
}
|
||||
|
||||
@@ -804,7 +820,7 @@ func loadTreeWithCheck(treeData []dto.CleanTree, pathItem, treeType string, file
|
||||
return treeData
|
||||
}
|
||||
list := loadTreeWithAllFile(true, pathItem, treeType, pathItem, fileOp)
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true})
|
||||
treeData = append(treeData, dto.CleanTree{ID: uuid.NewString(), Label: treeType, Size: uint64(size), IsCheck: size > 0, Children: list, Type: treeType, IsRecommend: true, CanDelete: false})
|
||||
return treeData
|
||||
}
|
||||
|
||||
@@ -831,6 +847,7 @@ func loadTreeWithDir(isCheck bool, treeType, pathItem string, fileOp fileUtils.F
|
||||
Name: strings.TrimPrefix(file.Name(), "/"),
|
||||
IsCheck: isCheck,
|
||||
IsRecommend: isCheck,
|
||||
CanDelete: true,
|
||||
}
|
||||
lists = append(lists, item)
|
||||
}
|
||||
@@ -878,6 +895,7 @@ func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string,
|
||||
Name: name,
|
||||
IsCheck: isCheck,
|
||||
IsRecommend: isCheck,
|
||||
CanDelete: true,
|
||||
}
|
||||
if file.IsDir() {
|
||||
item.Children = loadTreeWithAllFile(isCheck, originalPath, treeType, path.Join(pathItem, file.Name()), fileOp)
|
||||
|
||||
@@ -21,6 +21,7 @@ var (
|
||||
|
||||
mysqlRepo = repo.NewIMysqlRepo()
|
||||
postgresqlRepo = repo.NewIPostgresqlRepo()
|
||||
mongodbRepo = repo.NewIMongodbRepo()
|
||||
databaseRepo = repo.NewIDatabaseRepo()
|
||||
|
||||
imageRepoRepo = repo.NewIImageRepoRepo()
|
||||
@@ -49,7 +50,8 @@ var (
|
||||
runtimeRepo = repo.NewIRunTimeRepo()
|
||||
phpExtensionsRepo = repo.NewIPHPExtensionsRepo()
|
||||
|
||||
favoriteRepo = repo.NewIFavoriteRepo()
|
||||
favoriteRepo = repo.NewIFavoriteRepo()
|
||||
fileShareRepo = repo.NewIFileShareRepo()
|
||||
|
||||
taskRepo = repo.NewITaskRepo()
|
||||
|
||||
|
||||
+589
-27
@@ -2,6 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
@@ -41,12 +42,15 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
terminalai "github.com/1Panel-dev/1Panel/agent/utils/terminal/ai"
|
||||
"github.com/pkg/errors"
|
||||
)
|
||||
|
||||
type FileService struct {
|
||||
}
|
||||
|
||||
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
|
||||
|
||||
type IFileService interface {
|
||||
GetFileList(op request.FileOption) (response.FileInfo, error)
|
||||
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
|
||||
@@ -55,7 +59,9 @@ type IFileService interface {
|
||||
Delete(op request.FileDelete) error
|
||||
BatchDelete(op request.FileBatchDelete) error
|
||||
Compress(c request.FileCompress) error
|
||||
StopCompress(taskID string) error
|
||||
DeCompress(c request.FileDeCompress) error
|
||||
StopDeCompress(taskID string) error
|
||||
GetContent(op request.FileContentReq) (response.FileInfo, error)
|
||||
GetPreviewContent(op request.FileContentReq) (response.FileInfo, error)
|
||||
SaveContent(edit request.FileEdit) error
|
||||
@@ -78,10 +84,7 @@ type IFileService interface {
|
||||
ConvertLog(req dto.PageInfo) (int64, []response.FileConvertLog, error)
|
||||
BatchGetRemarks(req request.FileRemarkBatch) map[string]string
|
||||
SetRemark(req request.FileRemarkUpdate) error
|
||||
}
|
||||
|
||||
var filteredPaths = []string{
|
||||
"/.1panel_clash",
|
||||
AISearch(req request.FileAISearch) (*response.FileAISearchResult, error)
|
||||
}
|
||||
|
||||
const (
|
||||
@@ -106,10 +109,29 @@ func (f *FileService) GetFileList(op request.FileOption) (response.FileInfo, err
|
||||
if err != nil {
|
||||
return fileInfo, err
|
||||
}
|
||||
shareMap, err := NewIFileShareService().SharePathCodeMap()
|
||||
if err != nil {
|
||||
return fileInfo, err
|
||||
}
|
||||
applyFileShares(info, shareMap)
|
||||
fileInfo.FileInfo = *info
|
||||
return fileInfo, nil
|
||||
}
|
||||
|
||||
func applyFileShares(info *files.FileInfo, shareMap map[string]string) {
|
||||
if info == nil {
|
||||
return
|
||||
}
|
||||
if code, ok := shareMap[info.Path]; ok {
|
||||
info.ShareCode = code
|
||||
} else {
|
||||
info.ShareCode = ""
|
||||
}
|
||||
for _, item := range info.Items {
|
||||
applyFileShares(item, shareMap)
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) {
|
||||
var (
|
||||
files []response.UploadInfo
|
||||
@@ -170,14 +192,7 @@ func (f *FileService) GetFileTree(op request.FileOption) ([]response.FileTree, e
|
||||
}
|
||||
|
||||
func shouldFilterPath(path string) bool {
|
||||
cleanedPath := filepath.Clean(path)
|
||||
for _, filteredPath := range filteredPaths {
|
||||
cleanedFilteredPath := filepath.Clean(filteredPath)
|
||||
if cleanedFilteredPath == cleanedPath || strings.HasPrefix(cleanedPath, cleanedFilteredPath+"/") {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
return files.ShouldFilterSensitivePath(path)
|
||||
}
|
||||
|
||||
func (f *FileService) buildFileTree(node *response.FileTree, items []*files.FileInfo, op request.FileOption, level int) error {
|
||||
@@ -219,8 +234,12 @@ func (f *FileService) buildChildNode(childNode *response.FileTree, fileInfo *fil
|
||||
return f.buildFileTree(childNode, subInfo.Items, op, level-1)
|
||||
}
|
||||
|
||||
func hasInvalidFileName(fullPath string) bool {
|
||||
return files.IsInvalidChar(filepath.Base(fullPath))
|
||||
}
|
||||
|
||||
func (f *FileService) Create(op request.FileCreate) error {
|
||||
if files.IsInvalidChar(op.Path) {
|
||||
if hasInvalidFileName(op.Path) {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
@@ -272,16 +291,37 @@ func (f *FileService) Delete(op request.FileDelete) error {
|
||||
if recycleBinStatus.Value == "Disable" {
|
||||
op.ForceDelete = true
|
||||
}
|
||||
var historyTargets []string
|
||||
if op.ForceDelete {
|
||||
if op.IsDir {
|
||||
return fo.DeleteDir(op.Path)
|
||||
} else {
|
||||
return fo.DeleteFile(op.Path)
|
||||
var err error
|
||||
historyTargets, err = f.collectPermanentDeleteTargets(op.Path, op.IsDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if op.ForceDelete {
|
||||
var err error
|
||||
if op.IsDir {
|
||||
err = fo.DeleteDir(op.Path)
|
||||
} else {
|
||||
err = fo.DeleteFile(op.Path)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(op.Path); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", op.Path, err)
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory(historyTargets)
|
||||
return nil
|
||||
}
|
||||
info, _ := fo.Fs.Stat(op.Path)
|
||||
if info == nil || files.IsSymlink(info.Mode()) {
|
||||
return os.Remove(op.Path)
|
||||
if err := os.Remove(op.Path); err != nil {
|
||||
return err
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory([]string{op.Path})
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := NewIRecycleBinService().Create(request.RecycleBinCreate{SourcePath: op.Path}); err != nil {
|
||||
@@ -294,15 +334,27 @@ func (f *FileService) BatchDelete(op request.FileBatchDelete) error {
|
||||
fo := files.NewFileOp()
|
||||
if op.IsDir {
|
||||
for _, file := range op.Paths {
|
||||
targets, err := f.collectPermanentDeleteTargets(file, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fo.DeleteDir(file); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(file); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", file, err)
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory(targets)
|
||||
}
|
||||
} else {
|
||||
for _, file := range op.Paths {
|
||||
if err := fo.DeleteFile(file); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(file); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", file, err)
|
||||
}
|
||||
f.cleanupPermanentDeleteHistory([]string{file})
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -310,7 +362,10 @@ func (f *FileService) BatchDelete(op request.FileBatchDelete) error {
|
||||
|
||||
func (f *FileService) ChangeMode(op request.FileCreate) error {
|
||||
fo := files.NewFileOp()
|
||||
return fo.ChmodR(op.Path, op.Mode, op.Sub)
|
||||
if err := fo.ChmodR(op.Path, op.Mode, op.Sub); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error {
|
||||
@@ -329,9 +384,44 @@ func (f *FileService) BatchChangeModeAndOwner(op request.FileRoleReq) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) collectPermanentDeleteTargets(targetPath string, isDir bool) ([]string, error) {
|
||||
if !isDir {
|
||||
return []string{targetPath}, nil
|
||||
}
|
||||
|
||||
var targets []string
|
||||
if err := filepath.WalkDir(targetPath, func(currentPath string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d == nil || d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
targets = append(targets, currentPath)
|
||||
return nil
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return targets, nil
|
||||
}
|
||||
|
||||
func (f *FileService) cleanupPermanentDeleteHistory(targets []string) {
|
||||
if len(targets) == 0 {
|
||||
return
|
||||
}
|
||||
for _, target := range targets {
|
||||
if err := historyService.DeleteRelatedHistory(target); err != nil {
|
||||
global.LOG.Warnf("cleanup file history failed for %s: %v", target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) ChangeOwner(req request.FileRoleUpdate) error {
|
||||
fo := files.NewFileOp()
|
||||
return fo.ChownR(req.Path, req.User, req.Group, req.Sub)
|
||||
if err := fo.ChownR(req.Path, req.User, req.Group, req.Sub); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) Compress(c request.FileCompress) error {
|
||||
@@ -339,7 +429,76 @@ func (f *FileService) Compress(c request.FileCompress) error {
|
||||
if !c.Replace && fo.Stat(filepath.Join(c.Dst, c.Name)) {
|
||||
return buserr.New("ErrFileIsExist")
|
||||
}
|
||||
return fo.Compress(c.Files, c.Dst, c.Name, files.CompressType(c.Type), c.Secret)
|
||||
if err := preflightCompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Name, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Name, func(t *task.Task) error {
|
||||
t.LogStart(c.Name)
|
||||
compressType := files.CompressType(c.Type)
|
||||
dstFile := filepath.Join(c.Dst, c.Name)
|
||||
success := false
|
||||
defer func() {
|
||||
if !success {
|
||||
_ = os.Remove(dstFile)
|
||||
}
|
||||
}()
|
||||
if err := fo.Compress(t.TaskCtx, c.Files, c.Dst, c.Name, compressType, c.Secret, nil); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Stat(dstFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Size() == 0 {
|
||||
return fmt.Errorf("compressed file not generated: %s", dstFile)
|
||||
}
|
||||
success = true
|
||||
return nil
|
||||
}, nil)
|
||||
_ = taskItem.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func preflightCompressTool(compressType files.CompressType) error {
|
||||
switch compressType {
|
||||
case files.TarGz, files.Rar, files.X7z:
|
||||
_, err := files.NewShellArchiver(compressType)
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func preflightDecompressTool(decompressType files.CompressType) error {
|
||||
switch decompressType {
|
||||
case files.Rar, files.X7z:
|
||||
_, err := files.NewExtractShellArchiver(decompressType)
|
||||
return err
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func (f *FileService) StopCompress(taskID string) error {
|
||||
if cancel, ok := global.TaskCtxMap[taskID]; ok {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
return buserr.New("TaskNotFound")
|
||||
}
|
||||
|
||||
func (f *FileService) StopDeCompress(taskID string) error {
|
||||
if cancel, ok := global.TaskCtxMap[taskID]; ok {
|
||||
cancel()
|
||||
return nil
|
||||
}
|
||||
return buserr.New("TaskNotFound")
|
||||
}
|
||||
|
||||
func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
@@ -347,7 +506,167 @@ func (f *FileService) DeCompress(c request.FileDeCompress) error {
|
||||
if c.Type == "tar" && len(c.Secret) != 0 {
|
||||
c.Type = "tar.gz"
|
||||
}
|
||||
return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type), c.Secret)
|
||||
if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
|
||||
return err
|
||||
}
|
||||
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
|
||||
t.LogStart(c.Path)
|
||||
dstExisted := fo.Stat(c.Dst)
|
||||
parentDir := filepath.Dir(c.Dst)
|
||||
if !fo.Stat(parentDir) {
|
||||
if err := fo.CreateDir(parentDir, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
tempDst, err := os.MkdirTemp(parentDir, ".decompress-*")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
success := false
|
||||
defer func() {
|
||||
_ = os.RemoveAll(tempDst)
|
||||
if !success && !dstExisted {
|
||||
_ = os.RemoveAll(c.Dst)
|
||||
}
|
||||
}()
|
||||
if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := copyDecompressTree(t.TaskCtx, tempDst, c.Dst); err != nil {
|
||||
return err
|
||||
}
|
||||
success = true
|
||||
return nil
|
||||
}, nil)
|
||||
_ = taskItem.Execute()
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
|
||||
entries, err := os.ReadDir(srcDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
info, err := os.Lstat(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
if err := os.RemoveAll(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
target, err := os.Readlink(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.Symlink(target, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return applyDecompressOwnership(srcPath, dstPath)
|
||||
}
|
||||
|
||||
if info.IsDir() {
|
||||
dstInfo, err := os.Lstat(dstPath)
|
||||
keepExistingDir := err == nil && dstInfo.IsDir() && dstInfo.Mode()&os.ModeSymlink == 0
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
if !keepExistingDir {
|
||||
if err := os.RemoveAll(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(dstPath, info.Mode().Perm()); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
entries, err := os.ReadDir(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, entry := range entries {
|
||||
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if keepExistingDir {
|
||||
return nil
|
||||
}
|
||||
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||
}
|
||||
|
||||
if err := os.RemoveAll(dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
srcFile, err := os.Open(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer srcFile.Close()
|
||||
|
||||
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode().Perm())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
if cerr := dstFile.Close(); cerr != nil && retErr == nil {
|
||||
retErr = cerr
|
||||
}
|
||||
}()
|
||||
|
||||
if _, err := io.Copy(dstFile, srcFile); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
|
||||
}
|
||||
|
||||
func applyDecompressOwnership(srcPath, dstPath string) error {
|
||||
info, err := os.Lstat(srcPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
stat, ok := info.Sys().(*unix.Stat_t)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return os.Lchown(dstPath, int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
return os.Chown(dstPath, int(stat.Uid), int(stat.Gid))
|
||||
}
|
||||
|
||||
func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) {
|
||||
@@ -472,15 +791,35 @@ func (f *FileService) SaveContent(edit request.FileEdit) error {
|
||||
}
|
||||
|
||||
fo := files.NewFileOp()
|
||||
return fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode)
|
||||
oldContent, _ := os.ReadFile(edit.Path)
|
||||
if bytes.Equal(oldContent, []byte(edit.Content)) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := fo.WriteFile(edit.Path, strings.NewReader(edit.Content), info.FileMode); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := historyService.RecordSave(edit.Path, oldContent, info.FileMode); err != nil {
|
||||
global.LOG.Warnf("record file save history failed for %s: %v", edit.Path, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) ChangeName(req request.FileRename) error {
|
||||
if files.IsInvalidChar(req.NewName) {
|
||||
if hasInvalidFileName(req.NewName) {
|
||||
return buserr.New("ErrInvalidChar")
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
return fo.Rename(req.OldName, req.NewName)
|
||||
content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
|
||||
if err := fo.Rename(req.OldName, req.NewName); err != nil {
|
||||
return err
|
||||
}
|
||||
if shouldRecordHistory {
|
||||
if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, mode, req.OldName, req.NewName); histErr != nil {
|
||||
global.LOG.Warnf("record file rename history failed for %s: %v", req.OldName, histErr)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *FileService) Wget(w request.FileWget) (string, error) {
|
||||
@@ -502,8 +841,19 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
return buserr.New("ErrMovePathFailed")
|
||||
}
|
||||
}
|
||||
type moveSnapshot struct {
|
||||
path string
|
||||
content []byte
|
||||
mode os.FileMode
|
||||
record bool
|
||||
}
|
||||
var errs []error
|
||||
if m.Type == "cut" {
|
||||
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
|
||||
for _, oldPath := range m.OldPaths {
|
||||
content, mode, record := readEditableFileHistoryContent(oldPath)
|
||||
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, record: record})
|
||||
}
|
||||
if len(m.CoverPaths) > 0 {
|
||||
for _, src := range m.CoverPaths {
|
||||
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
|
||||
@@ -512,7 +862,18 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
return fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover)
|
||||
if err := fo.Cut(m.OldPaths, m.NewPath, m.Name, m.Cover); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, snapshot := range snapshots {
|
||||
if snapshot.record {
|
||||
targetPath := buildHistoryMoveTargetPath(m.NewPath, m.Name, snapshot.path, len(m.OldPaths))
|
||||
if histErr := historyService.RecordOperation(fileHistoryOpMove, snapshot.path, snapshot.content, snapshot.mode, snapshot.path, targetPath); histErr != nil {
|
||||
global.LOG.Warnf("record file move history failed for %s: %v", snapshot.path, histErr)
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
if m.Type == "copy" {
|
||||
for _, src := range m.OldPaths {
|
||||
@@ -541,6 +902,49 @@ func (f *FileService) MvFile(m request.FileMove) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
|
||||
info, err := os.Lstat(filePath)
|
||||
if err != nil {
|
||||
return nil, 0640, false
|
||||
}
|
||||
mode := info.Mode()
|
||||
if mode.IsDir() || mode&os.ModeSymlink != 0 || !mode.IsRegular() || files.IsBlockDevice(mode) || info.Size() > fileHistorySnapshotMaxSize {
|
||||
return nil, mode, false
|
||||
}
|
||||
file, err := os.Open(filePath)
|
||||
if err != nil {
|
||||
return nil, mode, false
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
headBuf := make([]byte, 1024)
|
||||
n, err := file.Read(headBuf)
|
||||
if err != nil && err != io.EOF {
|
||||
return nil, mode, false
|
||||
}
|
||||
if n > 0 && files.DetectBinary(headBuf[:n]) {
|
||||
return nil, mode, false
|
||||
}
|
||||
if _, err := file.Seek(0, 0); err != nil {
|
||||
return nil, mode, false
|
||||
}
|
||||
content, err := io.ReadAll(io.LimitReader(file, fileHistorySnapshotMaxSize+1))
|
||||
if err != nil || int64(len(content)) > fileHistorySnapshotMaxSize {
|
||||
return nil, mode, false
|
||||
}
|
||||
return content, mode, true
|
||||
}
|
||||
|
||||
func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) string {
|
||||
if strings.TrimSpace(dst) == "" {
|
||||
return sourcePath
|
||||
}
|
||||
if strings.TrimSpace(name) != "" && sourceCount == 1 {
|
||||
return filepath.Join(dst, name)
|
||||
}
|
||||
return filepath.Join(dst, filepath.Base(sourcePath))
|
||||
}
|
||||
|
||||
func (f *FileService) FileDownload(d request.FileDownload) (string, error) {
|
||||
filePath := d.Paths[0]
|
||||
if d.Compress {
|
||||
@@ -549,7 +953,7 @@ func (f *FileService) FileDownload(d request.FileDownload) (string, error) {
|
||||
return "", err
|
||||
}
|
||||
fo := files.NewFileOp()
|
||||
if err := fo.Compress(d.Paths, tempPath, d.Name, files.CompressType(d.Type), ""); err != nil {
|
||||
if err := fo.Compress(context.Background(), d.Paths, tempPath, d.Name, files.CompressType(d.Type), "", nil); err != nil {
|
||||
return "", err
|
||||
}
|
||||
filePath = filepath.Join(tempPath, d.Name)
|
||||
@@ -1019,3 +1423,161 @@ func (f *FileService) ConvertLog(req dto.PageInfo) (total int64, data []response
|
||||
|
||||
return total, data, nil
|
||||
}
|
||||
|
||||
func (f *FileService) AISearch(req request.FileAISearch) (*response.FileAISearchResult, error) {
|
||||
root := filepath.Clean(strings.TrimSpace(req.Path))
|
||||
if root == "" {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "path is required", nil)
|
||||
}
|
||||
query := strings.TrimSpace(req.Query)
|
||||
if query == "" {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", "query is required", nil)
|
||||
}
|
||||
st, err := os.Stat(root)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, buserr.New("ErrPathNotFound")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if !st.IsDir() {
|
||||
return nil, buserr.New("ErrPathNotFound")
|
||||
}
|
||||
|
||||
maxItems := req.MaxItems
|
||||
if maxItems <= 0 {
|
||||
maxItems = files.DefaultFileAIMaxItems
|
||||
}
|
||||
if maxItems > 2000 {
|
||||
maxItems = 2000
|
||||
}
|
||||
|
||||
containSub := true
|
||||
if req.ContainSub != nil {
|
||||
containSub = *req.ContainSub
|
||||
}
|
||||
|
||||
searchOpts, err := files.MergeContentSearchOptions(
|
||||
req.MatchCase, req.WholeWord, req.UseRegex,
|
||||
req.Extensions,
|
||||
req.MinSize, req.MaxSize,
|
||||
req.ModifiedAfter, req.ModifiedBefore,
|
||||
req.MaxScanFiles,
|
||||
req.MaxFileBytes,
|
||||
req.MaxHitsPerFile, req.MaxTotalHits,
|
||||
req.ContentHitsPromptMaxBytes,
|
||||
req.LlmMaxOutputTokens,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, buserr.WithDetail("ErrInvalidParams", err.Error(), nil)
|
||||
}
|
||||
|
||||
matchFn, err := files.NewContentLineMatcher(query, searchOpts)
|
||||
if err != nil {
|
||||
return nil, buserr.WithDetail("ErrFileAISearchBadPattern", err.Error(), nil)
|
||||
}
|
||||
|
||||
cfg, timeout, err := terminalai.LoadFileAIRuntimeConfig()
|
||||
aiEnabled := err == nil
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items, truncated, err := files.CollectDirInventory(root, containSub, maxItems)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
preFiltered := false
|
||||
llmItems := items
|
||||
qLower := strings.ToLower(query)
|
||||
if len(llmItems) > 0 && query != "" {
|
||||
filtered := make([]files.AISearchInventoryItem, 0, len(llmItems))
|
||||
for _, it := range llmItems {
|
||||
rel := strings.TrimSpace(it.RelPath)
|
||||
if rel == "" {
|
||||
continue
|
||||
}
|
||||
if !req.UseRegex && !req.MatchCase && !req.WholeWord && strings.Contains(strings.ToLower(rel), qLower) {
|
||||
filtered = append(filtered, it)
|
||||
}
|
||||
}
|
||||
if len(filtered) >= 8 {
|
||||
llmItems = filtered
|
||||
preFiltered = true
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
contentHits, scannedFiles, hitsTrunc := files.SearchFileAIContentHits(root, llmItems, searchOpts, matchFn)
|
||||
hitsDTO := make([]response.FileAIContentHit, 0, len(contentHits))
|
||||
for _, h := range contentHits {
|
||||
hitsDTO = append(hitsDTO, response.FileAIContentHit{Path: h.Path, Line: h.Line, Text: h.Text})
|
||||
}
|
||||
|
||||
matchDesc := searchOpts.ContentMatchDescription()
|
||||
result := &response.FileAISearchResult{
|
||||
Hits: hitsDTO,
|
||||
ContentScannedFiles: scannedFiles,
|
||||
ContentHitsTruncated: hitsTrunc,
|
||||
Truncated: truncated,
|
||||
PreFiltered: preFiltered,
|
||||
ItemCount: len(llmItems),
|
||||
}
|
||||
|
||||
if len(llmItems) == 0 {
|
||||
if aiEnabled {
|
||||
result.Mode = "ai"
|
||||
result.Summary = i18n.GetMsgByKey("FileAISearchEmptyDir")
|
||||
if result.Summary == "" || result.Summary == "FileAISearchEmptyDir" {
|
||||
result.Summary = "No files or directories found under this path (or all entries were filtered)."
|
||||
}
|
||||
} else {
|
||||
result.Mode = "grep"
|
||||
result.Summary = ""
|
||||
}
|
||||
result.Duration = time.Since(start).Round(time.Millisecond).String()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
if !aiEnabled {
|
||||
result.Mode = "grep"
|
||||
result.Summary = ""
|
||||
result.Duration = time.Since(start).Round(time.Millisecond).String()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
result.Mode = "ai"
|
||||
|
||||
clientTimeout := timeout
|
||||
if clientTimeout < 30*time.Second {
|
||||
clientTimeout = 90 * time.Second
|
||||
}
|
||||
if clientTimeout > 5*time.Minute {
|
||||
clientTimeout = 5 * time.Minute
|
||||
}
|
||||
|
||||
runCtx, cancel := context.WithTimeout(context.Background(), timeout+time.Minute)
|
||||
defer cancel()
|
||||
|
||||
llmMaxOut := searchOpts.LlmMaxOutputTokens
|
||||
summary, usage, err := files.RunFileAISearchLLM(runCtx, cfg, clientTimeout, root, query, req.ResponseLanguage, llmItems, truncated, preFiltered, contentHits, scannedFiles, hitsTrunc, matchDesc, searchOpts.ContentHitsPromptMaxBytes, llmMaxOut)
|
||||
if err != nil {
|
||||
result.Mode = "grep"
|
||||
result.Summary = ""
|
||||
result.Duration = time.Since(start).Round(time.Millisecond).String()
|
||||
if errors.Is(err, context.DeadlineExceeded) || strings.Contains(strings.ToLower(err.Error()), "timeout") {
|
||||
return result, nil
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
result.Summary = summary
|
||||
result.PromptTokens = usage.PromptTokens
|
||||
result.CompletionTokens = usage.CompletionTokens
|
||||
result.TotalTokens = usage.TotalTokens
|
||||
if result.TotalTokens == 0 {
|
||||
result.TotalTokens = usage.PromptTokens + usage.CompletionTokens
|
||||
}
|
||||
result.Duration = time.Since(start).Round(time.Millisecond).String()
|
||||
return result, nil
|
||||
}
|
||||
|
||||
@@ -0,0 +1,750 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"gorm.io/gorm"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const (
|
||||
fileHistorySettingEnable = "FileHistoryStatus"
|
||||
fileHistorySettingMaxPerPath = "FileHistoryMaxPerPath"
|
||||
fileHistorySettingDiskQuotaMB = "FileHistoryDiskQuotaMB"
|
||||
fileHistoryOpSave = "save"
|
||||
fileHistoryOpRestore = "restore"
|
||||
fileHistoryOpRename = "rename"
|
||||
fileHistoryOpMove = "move"
|
||||
fileHistoryRootDirName = "file-history"
|
||||
defaultFileHistoryMaxPerPath = 20
|
||||
defaultFileHistoryDiskQuotaMB = 1024
|
||||
)
|
||||
|
||||
var historyService = NewIFileHistoryService()
|
||||
var fileHistoryPathLocks sync.Map
|
||||
|
||||
type FileHistoryService struct {
|
||||
repo repo.IFileHistoryRepo
|
||||
}
|
||||
|
||||
type IFileHistoryService interface {
|
||||
GetSettingInfo() (*response.FileHistorySettingInfo, error)
|
||||
UpdateSetting(req request.FileHistorySettingUpdate) error
|
||||
RecordSave(path string, content []byte, fileMode os.FileMode) error
|
||||
HasRelatedHistory(path string) (bool, error)
|
||||
RecordOperation(operation string, filePath string, content []byte, fileMode os.FileMode, sourcePath string, targetPath string) error
|
||||
Restore(req request.FileHistoryRestoreReq) (response.FileInfo, error)
|
||||
Search(req request.FileHistorySearchReq) (int64, []response.FileHistoryInfo, error)
|
||||
GetContent(req request.FileHistoryContentReq) (response.FileHistoryInfo, error)
|
||||
Delete(req request.FileHistoryDeleteReq) error
|
||||
DeleteRelatedHistory(path string) error
|
||||
}
|
||||
|
||||
func NewIFileHistoryService() IFileHistoryService {
|
||||
return &FileHistoryService{repo: repo.NewIFileHistoryRepo()}
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) GetSettingInfo() (*response.FileHistorySettingInfo, error) {
|
||||
info := &response.FileHistorySettingInfo{
|
||||
Enable: constant.StatusEnable,
|
||||
MaxPerPath: defaultFileHistoryMaxPerPath,
|
||||
DiskQuotaMB: defaultFileHistoryDiskQuotaMB,
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey(fileHistorySettingEnable); err == nil && value != "" {
|
||||
info.Enable = value
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey(fileHistorySettingMaxPerPath); err == nil && value != "" {
|
||||
if parsed, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && parsed > 0 {
|
||||
info.MaxPerPath = parsed
|
||||
}
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey(fileHistorySettingDiskQuotaMB); err == nil && value != "" {
|
||||
if parsed, err := strconv.Atoi(strings.TrimSpace(value)); err == nil && parsed > 0 {
|
||||
info.DiskQuotaMB = parsed
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) UpdateSetting(req request.FileHistorySettingUpdate) error {
|
||||
if req.MaxPerPath <= 0 {
|
||||
req.MaxPerPath = defaultFileHistoryMaxPerPath
|
||||
}
|
||||
if req.DiskQuotaMB <= 0 {
|
||||
req.DiskQuotaMB = defaultFileHistoryDiskQuotaMB
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(fileHistorySettingEnable, req.Enable); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(fileHistorySettingMaxPerPath, fmt.Sprintf("%d", req.MaxPerPath)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate(fileHistorySettingDiskQuotaMB, fmt.Sprintf("%d", req.DiskQuotaMB)); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) RecordSave(filePath string, content []byte, fileMode os.FileMode) error {
|
||||
return s.RecordOperation(fileHistoryOpSave, filePath, content, fileMode, "", "")
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) HasRelatedHistory(filePath string) (bool, error) {
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
absPath = filePath
|
||||
}
|
||||
if _, err = s.getLatestActiveRelatedByPath(absPath); err == nil {
|
||||
return true, nil
|
||||
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, err
|
||||
}
|
||||
if _, err = s.getLatestRelatedByPath(absPath); err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return false, nil
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) RecordOperation(operation string, filePath string, content []byte, fileMode os.FileMode, sourcePath string, targetPath string) error {
|
||||
info, err := s.GetSettingInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Enable != constant.StatusEnable {
|
||||
return nil
|
||||
}
|
||||
if fileMode == 0 {
|
||||
fileMode = 0640
|
||||
}
|
||||
if content == nil {
|
||||
content = []byte{}
|
||||
}
|
||||
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
absPath = filePath
|
||||
}
|
||||
absSourcePath := normalizeAbsPath(sourcePath)
|
||||
absTargetPath := normalizeAbsPath(targetPath)
|
||||
resolvePaths := []string{absPath}
|
||||
recordPath := absPath
|
||||
switch operation {
|
||||
case fileHistoryOpRename, fileHistoryOpMove:
|
||||
resolvePaths = []string{absSourcePath, absTargetPath, absPath}
|
||||
if absTargetPath != "" {
|
||||
recordPath = absTargetPath
|
||||
}
|
||||
default:
|
||||
if absSourcePath != "" {
|
||||
resolvePaths = append(resolvePaths, absSourcePath)
|
||||
}
|
||||
if absTargetPath != "" {
|
||||
resolvePaths = append(resolvePaths, absTargetPath)
|
||||
}
|
||||
}
|
||||
|
||||
fileID, latestChainRecord, chainErr := s.resolveFileChain(resolvePaths...)
|
||||
if chainErr != nil && !errors.Is(chainErr, gorm.ErrRecordNotFound) {
|
||||
return chainErr
|
||||
}
|
||||
if fileID == "" {
|
||||
fileID = common.GetUuid()
|
||||
}
|
||||
if operation == fileHistoryOpRename || operation == fileHistoryOpMove {
|
||||
if !s.isEditableHistorySnapshot(fileMode, content) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
previousID := uint(0)
|
||||
if latestVersion, err := s.getLatestVersionByFileID(fileID); err == nil {
|
||||
switch operation {
|
||||
case fileHistoryOpSave:
|
||||
contentHash := sha256HexBytes(content)
|
||||
if latestVersion.ContentSHA == contentHash {
|
||||
return nil
|
||||
}
|
||||
previousID = latestVersion.ID
|
||||
case fileHistoryOpRestore:
|
||||
previousID = latestVersion.ID
|
||||
default:
|
||||
if latestChainRecord.ID != 0 {
|
||||
previousID = latestChainRecord.ID
|
||||
}
|
||||
}
|
||||
} else if latestChainRecord.ID != 0 {
|
||||
previousID = latestChainRecord.ID
|
||||
}
|
||||
|
||||
recordContent := content
|
||||
recordDeleted := false
|
||||
|
||||
recordID, storagePath, err := s.recordSnapshot(fileID, recordDeleted, operation, recordPath, recordContent, fileMode, absSourcePath, absTargetPath, previousID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := s.enforceRetention(fileID, recordID); err != nil {
|
||||
_ = s.repo.DeleteByIDs([]uint{recordID})
|
||||
_ = os.Remove(s.absStoragePath(storagePath))
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) recordSnapshot(fileID string, deleted bool, operation string, absPath string, content []byte, fileMode os.FileMode, sourcePath string, targetPath string, previousID uint) (uint, string, error) {
|
||||
if fileMode == 0 {
|
||||
fileMode = 0640
|
||||
}
|
||||
now := time.Now()
|
||||
pathHash := sha256Hex(absPath)
|
||||
contentHash := sha256HexBytes(content)
|
||||
fileName := filepath.Base(absPath)
|
||||
extension := filepath.Ext(absPath)
|
||||
storagePath := s.buildStoragePath(absPath, now)
|
||||
|
||||
if err := os.MkdirAll(path.Dir(s.absStoragePath(storagePath)), os.ModePerm); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
if err := os.WriteFile(s.absStoragePath(storagePath), content, 0640); err != nil {
|
||||
return 0, "", err
|
||||
}
|
||||
|
||||
record := &model.FileHistory{
|
||||
FileID: fileID,
|
||||
Path: absPath,
|
||||
PathHash: pathHash,
|
||||
SourcePath: sourcePath,
|
||||
TargetPath: targetPath,
|
||||
FileName: fileName,
|
||||
Extension: extension,
|
||||
FileMode: fmt.Sprintf("%04o", fileMode.Perm()),
|
||||
Operation: operation,
|
||||
Deleted: deleted,
|
||||
ContentSize: int64(len(content)),
|
||||
ContentSHA: contentHash,
|
||||
StoragePath: storagePath,
|
||||
PreviousID: previousID,
|
||||
}
|
||||
|
||||
if err := s.repo.Create(record); err != nil {
|
||||
_ = os.Remove(s.absStoragePath(storagePath))
|
||||
return 0, "", err
|
||||
}
|
||||
return record.ID, storagePath, nil
|
||||
}
|
||||
|
||||
func normalizeAbsPath(filePath string) string {
|
||||
filePath = strings.TrimSpace(filePath)
|
||||
if filePath == "" {
|
||||
return ""
|
||||
}
|
||||
absPath, err := filepath.Abs(filePath)
|
||||
if err != nil {
|
||||
return filePath
|
||||
}
|
||||
return absPath
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) Search(req request.FileHistorySearchReq) (int64, []response.FileHistoryInfo, error) {
|
||||
page := req.Page
|
||||
if page <= 0 {
|
||||
page = 1
|
||||
}
|
||||
size := req.PageSize
|
||||
if size <= 0 {
|
||||
size = 20
|
||||
}
|
||||
offset := (page - 1) * size
|
||||
opts := []repo.DBOption{}
|
||||
opts = append(opts, s.repo.WithNotOperation("init"))
|
||||
switch strings.ToLower(strings.TrimSpace(req.Scope)) {
|
||||
case "current":
|
||||
if strings.TrimSpace(req.Path) == "" {
|
||||
return 0, nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
absPath, err := filepath.Abs(req.Path)
|
||||
if err != nil {
|
||||
absPath = req.Path
|
||||
}
|
||||
chain, err := s.getLatestActiveRelatedByPath(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
chain, err = s.getLatestRelatedByPath(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return 0, []response.FileHistoryInfo{}, nil
|
||||
}
|
||||
return 0, nil, err
|
||||
}
|
||||
} else {
|
||||
return 0, nil, err
|
||||
}
|
||||
}
|
||||
opts = append(opts, s.repo.WithByFileID(chain.FileID))
|
||||
case "all":
|
||||
if strings.TrimSpace(req.Path) != "" {
|
||||
opts = append(opts, s.repo.WithByRelatedPath(normalizeAbsPath(req.Path)))
|
||||
}
|
||||
default:
|
||||
return 0, nil, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
if strings.TrimSpace(req.Operation) != "" {
|
||||
opts = append(opts, s.repo.WithByOperation(strings.TrimSpace(req.Operation)))
|
||||
}
|
||||
|
||||
total, items, err := s.repo.Page(size, offset, opts...)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
res := make([]response.FileHistoryInfo, 0, len(items))
|
||||
for _, item := range items {
|
||||
currentPath := item.Path
|
||||
if resolved, err := s.getCurrentPathByFileID(item.FileID); err == nil && strings.TrimSpace(resolved) != "" {
|
||||
currentPath = resolved
|
||||
}
|
||||
res = append(res, response.FileHistoryInfo{
|
||||
ID: item.ID,
|
||||
FileID: item.FileID,
|
||||
Path: item.Path,
|
||||
CurrentPath: currentPath,
|
||||
PreviousID: item.PreviousID,
|
||||
SourcePath: item.SourcePath,
|
||||
TargetPath: item.TargetPath,
|
||||
FileName: item.FileName,
|
||||
Extension: item.Extension,
|
||||
FileMode: item.FileMode,
|
||||
Operation: item.Operation,
|
||||
Deleted: item.Deleted,
|
||||
ContentSize: item.ContentSize,
|
||||
ContentSHA: item.ContentSHA,
|
||||
StoragePath: item.StoragePath,
|
||||
CreatedAt: item.CreatedAt,
|
||||
UpdatedAt: item.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return total, res, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) GetContent(req request.FileHistoryContentReq) (response.FileHistoryInfo, error) {
|
||||
record, err := s.repo.Get(s.repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return response.FileHistoryInfo{}, err
|
||||
}
|
||||
content, err := os.ReadFile(s.absStoragePath(record.StoragePath))
|
||||
if err != nil {
|
||||
return response.FileHistoryInfo{}, err
|
||||
}
|
||||
currentContent := s.getCurrentContent(record)
|
||||
return response.FileHistoryInfo{
|
||||
ID: record.ID,
|
||||
FileID: record.FileID,
|
||||
Path: record.Path,
|
||||
CurrentPath: func() string {
|
||||
if currentPath, err := s.getCurrentPathByFileID(record.FileID); err == nil && strings.TrimSpace(currentPath) != "" {
|
||||
return currentPath
|
||||
}
|
||||
return record.Path
|
||||
}(),
|
||||
PreviousID: record.PreviousID,
|
||||
SourcePath: record.SourcePath,
|
||||
TargetPath: record.TargetPath,
|
||||
FileName: record.FileName,
|
||||
Extension: record.Extension,
|
||||
FileMode: record.FileMode,
|
||||
Operation: record.Operation,
|
||||
Deleted: record.Deleted,
|
||||
ContentSize: record.ContentSize,
|
||||
ContentSHA: record.ContentSHA,
|
||||
StoragePath: record.StoragePath,
|
||||
Content: string(content),
|
||||
CurrentContent: currentContent,
|
||||
CreatedAt: record.CreatedAt,
|
||||
UpdatedAt: record.UpdatedAt,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) Restore(req request.FileHistoryRestoreReq) (response.FileInfo, error) {
|
||||
record, err := s.repo.Get(s.repo.WithByID(req.ID))
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
if !s.isVersionOperation(record.Operation) {
|
||||
return response.FileInfo{}, buserr.New("ErrInvalidParams")
|
||||
}
|
||||
content, err := os.ReadFile(s.absStoragePath(record.StoragePath))
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
|
||||
currentPath := record.Path
|
||||
if chainPath, err := s.getCurrentPathByFileID(record.FileID); err == nil && strings.TrimSpace(chainPath) != "" {
|
||||
currentPath = chainPath
|
||||
}
|
||||
|
||||
lock := s.getPathLock(currentPath)
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
|
||||
var rollbackContent []byte
|
||||
var rollbackMode os.FileMode
|
||||
var existedBefore bool
|
||||
currentInfo, currentErr := files.NewFileInfo(files.FileOption{
|
||||
Path: currentPath,
|
||||
Expand: false,
|
||||
})
|
||||
if currentErr == nil {
|
||||
existedBefore = true
|
||||
rollbackContent, err = os.ReadFile(currentPath)
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
rollbackMode = currentInfo.FileMode
|
||||
} else {
|
||||
rollbackMode = parseFileMode(record.FileMode)
|
||||
if rollbackMode == 0 {
|
||||
rollbackMode = 0640
|
||||
}
|
||||
}
|
||||
|
||||
targetMode := rollbackMode
|
||||
if targetMode == 0 {
|
||||
targetMode = parseFileMode(record.FileMode)
|
||||
}
|
||||
if targetMode == 0 {
|
||||
targetMode = 0640
|
||||
}
|
||||
|
||||
fo := files.NewFileOp()
|
||||
if err := fo.WriteFile(currentPath, strings.NewReader(string(content)), targetMode); err != nil {
|
||||
_ = s.rollbackRestore(currentPath, rollbackContent, rollbackMode, existedBefore)
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
if err := historyService.RecordOperation(fileHistoryOpRestore, currentPath, rollbackContent, rollbackMode, "", ""); err != nil {
|
||||
global.LOG.Warnf("record file restore history failed for %s: %v", currentPath, err)
|
||||
}
|
||||
|
||||
info, err := files.NewFileInfo(files.FileOption{
|
||||
Path: currentPath,
|
||||
Expand: true,
|
||||
})
|
||||
if err != nil {
|
||||
return response.FileInfo{}, err
|
||||
}
|
||||
return response.FileInfo{FileInfo: *info}, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) Delete(req request.FileHistoryDeleteReq) error {
|
||||
if len(req.IDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
for _, id := range req.IDs {
|
||||
record, err := s.repo.Get(s.repo.WithByID(id))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
continue
|
||||
}
|
||||
return err
|
||||
}
|
||||
if err := s.repo.DeleteByIDs([]uint{id}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(s.absStoragePath(record.StoragePath))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) DeleteRelatedHistory(filePath string) error {
|
||||
absPath := normalizeAbsPath(filePath)
|
||||
if strings.TrimSpace(absPath) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
fileID, latestRecord, err := s.resolveFileChain(absPath)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
if strings.TrimSpace(fileID) == "" {
|
||||
fileID = latestRecord.FileID
|
||||
}
|
||||
if strings.TrimSpace(fileID) == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
var records []model.FileHistory
|
||||
if err := global.DB.Model(&model.FileHistory{}).Where("file_id = ?", fileID).Find(&records).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
ids := make([]uint, 0, len(records))
|
||||
for _, record := range records {
|
||||
ids = append(ids, record.ID)
|
||||
_ = os.Remove(s.absStoragePath(record.StoragePath))
|
||||
}
|
||||
return s.repo.DeleteByIDs(ids)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) rollbackRestore(filePath string, rollbackContent []byte, rollbackMode os.FileMode, existedBefore bool) error {
|
||||
fo := files.NewFileOp()
|
||||
if !existedBefore {
|
||||
return os.Remove(filePath)
|
||||
}
|
||||
if rollbackMode == 0 {
|
||||
rollbackMode = 0640
|
||||
}
|
||||
return fo.WriteFile(filePath, strings.NewReader(string(rollbackContent)), rollbackMode)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) enforceRetention(fileID string, keepID uint) error {
|
||||
info, err := s.GetSettingInfo()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Enable != constant.StatusEnable {
|
||||
return nil
|
||||
}
|
||||
|
||||
maxPerPath := info.MaxPerPath
|
||||
if maxPerPath <= 0 {
|
||||
maxPerPath = 20
|
||||
}
|
||||
quotaBytes := int64(info.DiskQuotaMB) * 1024 * 1024
|
||||
|
||||
if maxPerPath > 0 {
|
||||
total, records, err := s.listFileOldest(fileID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if int(total) > maxPerPath {
|
||||
removeCount := int(total) - maxPerPath
|
||||
for i := 0; i < removeCount && i < len(records); i++ {
|
||||
if err := s.repo.DeleteByIDs([]uint{records[i].ID}); err != nil {
|
||||
return err
|
||||
}
|
||||
_ = os.Remove(s.absStoragePath(records[i].StoragePath))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if quotaBytes <= 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
totalSize, err := s.totalSize()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if totalSize <= quotaBytes {
|
||||
return nil
|
||||
}
|
||||
return buserr.New("ErrHistoryQuotaExceeded")
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getPathLock(absPath string) *sync.Mutex {
|
||||
actual, _ := fileHistoryPathLocks.LoadOrStore(absPath, &sync.Mutex{})
|
||||
return actual.(*sync.Mutex)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) listFileOldest(fileID string) (int64, []model.FileHistory, error) {
|
||||
var total int64
|
||||
var items []model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("file_id = ?", fileID)
|
||||
if err := db.Count(&total).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if err := db.Order("created_at asc").Find(&items).Error; err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
return total, items, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestRelatedByPath(absPath string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("path = ? OR source_path = ? OR target_path = ?", absPath, absPath, absPath).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestActiveRelatedByPath(absPath string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("deleted = ? AND (path = ? OR source_path = ? OR target_path = ?)", false, absPath, absPath, absPath).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestVersionByFileID(fileID string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("file_id = ? AND operation IN ?", fileID, []string{fileHistoryOpSave, fileHistoryOpRestore}).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getCurrentPathByFileID(fileID string) (string, error) {
|
||||
record, err := s.getLatestRelatedByFileID(fileID)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return record.Path, nil
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getCurrentContent(record model.FileHistory) string {
|
||||
currentPath := ""
|
||||
if resolved, err := s.getCurrentPathByFileID(record.FileID); err == nil && strings.TrimSpace(resolved) != "" {
|
||||
currentPath = resolved
|
||||
}
|
||||
candidates := []string{}
|
||||
if strings.TrimSpace(currentPath) != "" {
|
||||
candidates = append(candidates, currentPath)
|
||||
}
|
||||
if strings.TrimSpace(record.Path) != "" && record.Path != currentPath {
|
||||
candidates = append(candidates, record.Path)
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
content, err := os.ReadFile(candidate)
|
||||
if err == nil {
|
||||
return string(content)
|
||||
}
|
||||
}
|
||||
content, err := os.ReadFile(s.absStoragePath(record.StoragePath))
|
||||
if err == nil {
|
||||
return string(content)
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) isEditableHistorySnapshot(fileMode os.FileMode, content []byte) bool {
|
||||
if fileMode.IsDir() || files.IsBlockDevice(fileMode) {
|
||||
return false
|
||||
}
|
||||
if len(content) == 0 {
|
||||
return true
|
||||
}
|
||||
return !files.DetectBinary(content)
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) getLatestRelatedByFileID(fileID string) (model.FileHistory, error) {
|
||||
var item model.FileHistory
|
||||
db := global.DB.Model(&model.FileHistory{}).Where("file_id = ?", fileID).Order("created_at desc")
|
||||
err := db.First(&item).Error
|
||||
return item, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) resolveFileChain(paths ...string) (string, model.FileHistory, error) {
|
||||
for _, itemPath := range paths {
|
||||
itemPath = strings.TrimSpace(itemPath)
|
||||
if itemPath == "" {
|
||||
continue
|
||||
}
|
||||
record, err := s.getLatestActiveRelatedByPath(itemPath)
|
||||
if err == nil {
|
||||
return record.FileID, record, nil
|
||||
}
|
||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return "", model.FileHistory{}, err
|
||||
}
|
||||
}
|
||||
return "", model.FileHistory{}, gorm.ErrRecordNotFound
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) isVersionOperation(operation string) bool {
|
||||
return operation == fileHistoryOpSave || operation == fileHistoryOpRestore
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) totalSize() (int64, error) {
|
||||
var total int64
|
||||
err := global.DB.Model(&model.FileHistory{}).Select("coalesce(sum(content_size),0)").Scan(&total).Error
|
||||
return total, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) listOldest() ([]model.FileHistory, error) {
|
||||
var items []model.FileHistory
|
||||
err := global.DB.Model(&model.FileHistory{}).Order("created_at asc").Find(&items).Error
|
||||
return items, err
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) buildStoragePath(absPath string, now time.Time) string {
|
||||
fileName := filepath.Base(absPath)
|
||||
extension := filepath.Ext(fileName)
|
||||
baseName := strings.TrimSuffix(fileName, extension)
|
||||
if baseName == "" {
|
||||
baseName = fileName
|
||||
extension = ""
|
||||
}
|
||||
baseName = sanitizeHistoryFileName(baseName)
|
||||
if baseName == "" {
|
||||
baseName = "file"
|
||||
}
|
||||
dayDir := now.Format("2006-01-02")
|
||||
shortCode := strings.ReplaceAll(common.GetUuid(), "-", "")
|
||||
if len(shortCode) > 8 {
|
||||
shortCode = shortCode[:8]
|
||||
}
|
||||
return path.Join(fileHistoryRootDirName, dayDir, fmt.Sprintf("%s__%s%s", baseName, shortCode, extension))
|
||||
}
|
||||
|
||||
func (s *FileHistoryService) absStoragePath(rel string) string {
|
||||
return path.Join(global.Dir.LocalBackupDir, rel)
|
||||
}
|
||||
|
||||
func sanitizeHistoryFileName(name string) string {
|
||||
name = strings.TrimSpace(name)
|
||||
replacer := strings.NewReplacer(
|
||||
"/", "_",
|
||||
"\\", "_",
|
||||
":", "_",
|
||||
"*", "_",
|
||||
"?", "_",
|
||||
"\"", "_",
|
||||
"<", "_",
|
||||
">", "_",
|
||||
"|", "_",
|
||||
)
|
||||
return replacer.Replace(name)
|
||||
}
|
||||
|
||||
func parseFileMode(mode string) os.FileMode {
|
||||
if strings.TrimSpace(mode) == "" {
|
||||
return 0
|
||||
}
|
||||
parsed, err := strconv.ParseUint(strings.TrimSpace(mode), 8, 32)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
return os.FileMode(parsed)
|
||||
}
|
||||
|
||||
func sha256Hex(value string) string {
|
||||
sum := sha256.Sum256([]byte(value))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func sha256HexBytes(value []byte) string {
|
||||
sum := sha256.Sum256(value)
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
@@ -0,0 +1,381 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"crypto/subtle"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/encrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type FileShareService struct{}
|
||||
|
||||
const (
|
||||
fileShareCodeMinLength = 10
|
||||
fileShareCodeMaxLength = 16
|
||||
fileShareCodeDefaultLength = 13
|
||||
fileShareCharset = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
||||
)
|
||||
|
||||
var fileShareCodeRegexp = regexp.MustCompile(`^[A-Za-z0-9]{10,16}$`)
|
||||
|
||||
type IFileShareService interface {
|
||||
Create(req request.FileShareCreate) (*response.FileShareInfo, error)
|
||||
Page(req dto.PageInfo) (int64, []response.FileShareInfo, error)
|
||||
GetByPath(path string) (*response.FileShareInfo, error)
|
||||
GetByCode(code string) (*response.FileShareInfo, error)
|
||||
GetPublicByCode(code string) (*response.FileSharePublicInfo, error)
|
||||
DeleteByPath(path string) error
|
||||
SharePathCodeMap() (map[string]string, error)
|
||||
Check(code, password string) error
|
||||
PrepareDownload(code, password string) (filePath, fileName string, err error)
|
||||
}
|
||||
|
||||
func NewIFileShareService() IFileShareService {
|
||||
return &FileShareService{}
|
||||
}
|
||||
|
||||
func randomShareCode(length int) (string, error) {
|
||||
if length < fileShareCodeMinLength || length > fileShareCodeMaxLength {
|
||||
length = fileShareCodeDefaultLength
|
||||
}
|
||||
b := make([]byte, length)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
buf := make([]byte, length)
|
||||
for i := range b {
|
||||
buf[i] = fileShareCharset[int(b[i])%len(fileShareCharset)]
|
||||
}
|
||||
return string(buf), nil
|
||||
}
|
||||
|
||||
func randomSalt() (string, error) {
|
||||
b := make([]byte, 16)
|
||||
if _, err := rand.Read(b); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return hex.EncodeToString(b), nil
|
||||
}
|
||||
|
||||
func hashPassword(salt, password string) string {
|
||||
sum := sha256.Sum256([]byte(salt + ":" + password))
|
||||
return hex.EncodeToString(sum[:])
|
||||
}
|
||||
|
||||
func shareModelToInfo(item model.FileShare) response.FileShareInfo {
|
||||
return response.FileShareInfo{
|
||||
Code: item.Token,
|
||||
Path: item.Path,
|
||||
FileName: item.FileName,
|
||||
ExpiresAt: item.ExpiresUnix,
|
||||
Permanent: item.ExpiresUnix == 0,
|
||||
HasPassword: item.PasswordHash != "",
|
||||
}
|
||||
}
|
||||
|
||||
func fillSharePassword(info *response.FileShareInfo, item model.FileShare) {
|
||||
if info == nil || item.PasswordEnc == "" {
|
||||
return
|
||||
}
|
||||
password, err := encrypt.StringDecrypt(item.PasswordEnc)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
info.Password = password
|
||||
}
|
||||
|
||||
func shareModelToPublicInfo(item model.FileShare) response.FileSharePublicInfo {
|
||||
return response.FileSharePublicInfo{
|
||||
FileName: item.FileName,
|
||||
ExpiresAt: item.ExpiresUnix,
|
||||
Permanent: item.ExpiresUnix == 0,
|
||||
HasPassword: item.PasswordHash != "",
|
||||
}
|
||||
}
|
||||
|
||||
func (s *FileShareService) generateUniqueCode() (string, error) {
|
||||
for i := 0; i < 8; i++ {
|
||||
code, err := randomShareCode(fileShareCodeDefaultLength)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
_, err = fileShareRepo.GetFirst(fileShareRepo.WithByCode(code))
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return code, nil
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
return "", errors.New("failed to generate unique file share code")
|
||||
}
|
||||
|
||||
func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileShareInfo, error) {
|
||||
path := strings.TrimSpace(req.Path)
|
||||
if path == "" || strings.Contains(path, "..") {
|
||||
return nil, buserr.New("ErrFileSharePath")
|
||||
}
|
||||
info, err := os.Stat(path)
|
||||
if err != nil || info.IsDir() {
|
||||
return nil, buserr.New("ErrFileSharePath")
|
||||
}
|
||||
|
||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(path))
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
isNew := errors.Is(err, gorm.ErrRecordNotFound)
|
||||
if isNew {
|
||||
code, err := s.generateUniqueCode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item = model.FileShare{
|
||||
Path: path,
|
||||
Token: code,
|
||||
FileName: filepath.Base(path),
|
||||
}
|
||||
} else if !fileShareCodeRegexp.MatchString(item.Token) {
|
||||
code, err := s.generateUniqueCode()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.Token = code
|
||||
}
|
||||
|
||||
item.FileName = filepath.Base(path)
|
||||
item.MaxDownloads = 0
|
||||
item.DownloadCount = 0
|
||||
item.ExpiresUnix = 0
|
||||
if req.ExpireMinutes > 0 {
|
||||
item.ExpiresUnix = time.Now().Add(time.Duration(req.ExpireMinutes) * time.Minute).Unix()
|
||||
}
|
||||
|
||||
if req.Password != nil {
|
||||
pw := strings.TrimSpace(*req.Password)
|
||||
if pw == "" {
|
||||
item.PasswordEnc = ""
|
||||
item.PasswordSalt = ""
|
||||
item.PasswordHash = ""
|
||||
} else {
|
||||
pwLen := utf8.RuneCountInString(pw)
|
||||
if pwLen < 4 || pwLen > 256 {
|
||||
return nil, buserr.New("ErrFileSharePasswordPolicy")
|
||||
}
|
||||
enc, err := encrypt.StringEncrypt(pw)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.PasswordEnc = enc
|
||||
salt, err := randomSalt()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
item.PasswordSalt = salt
|
||||
item.PasswordHash = hashPassword(salt, pw)
|
||||
}
|
||||
}
|
||||
|
||||
if isNew {
|
||||
if err := fileShareRepo.Create(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
if err := fileShareRepo.Save(&item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
res := shareModelToInfo(item)
|
||||
fillSharePassword(&res, item)
|
||||
return &res, nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) Page(req dto.PageInfo) (int64, []response.FileShareInfo, error) {
|
||||
items, err := fileShareRepo.All()
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
result := make([]response.FileShareInfo, 0, len(items))
|
||||
for _, item := range items {
|
||||
if err := s.pruneInvalidShare(item); err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||
continue
|
||||
}
|
||||
result = append(result, shareModelToInfo(item))
|
||||
}
|
||||
total := len(result)
|
||||
start := (req.Page - 1) * req.PageSize
|
||||
if start >= total {
|
||||
return int64(total), []response.FileShareInfo{}, nil
|
||||
}
|
||||
end := start + req.PageSize
|
||||
if end > total {
|
||||
end = total
|
||||
}
|
||||
return int64(total), result[start:end], nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) GetByPath(path string) (*response.FileShareInfo, error) {
|
||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := s.pruneInvalidShare(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||
return nil, nil
|
||||
}
|
||||
info := shareModelToInfo(item)
|
||||
fillSharePassword(&info, item)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) GetByCode(code string) (*response.FileShareInfo, error) {
|
||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, buserr.New("ErrFileShareInvalid")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := s.pruneInvalidShare(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||
return nil, buserr.New("ErrFileShareExpired")
|
||||
}
|
||||
info := shareModelToInfo(item)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) GetPublicByCode(code string) (*response.FileSharePublicInfo, error) {
|
||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(strings.TrimSpace(code)))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, buserr.New("ErrFileShareInvalid")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err := s.pruneInvalidShare(item); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if item.ExpiresUnix > 0 && time.Now().Unix() > item.ExpiresUnix {
|
||||
return nil, buserr.New("ErrFileShareExpired")
|
||||
}
|
||||
info := shareModelToPublicInfo(item)
|
||||
return &info, nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) DeleteByPath(path string) error {
|
||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByPath(strings.TrimSpace(path)))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return buserr.New("ErrFileShareInvalid")
|
||||
}
|
||||
return err
|
||||
}
|
||||
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||
}
|
||||
|
||||
func (s *FileShareService) SharePathCodeMap() (map[string]string, error) {
|
||||
items, err := fileShareRepo.All()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]string, len(items))
|
||||
now := time.Now().Unix()
|
||||
for _, item := range items {
|
||||
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
||||
continue
|
||||
}
|
||||
if _, err := os.Stat(item.Path); err != nil {
|
||||
continue
|
||||
}
|
||||
result[item.Path] = item.Token
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) Check(code, password string) error {
|
||||
_, err := s.check(code, password)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *FileShareService) PrepareDownload(code, password string) (string, string, error) {
|
||||
item, err := s.check(code, password)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
return item.Path, item.FileName, nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) pruneInvalidShare(item model.FileShare) error {
|
||||
now := time.Now().Unix()
|
||||
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
||||
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||
}
|
||||
info, err := os.Stat(item.Path)
|
||||
if err != nil || info.IsDir() {
|
||||
return fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *FileShareService) check(code, password string) (*model.FileShare, error) {
|
||||
code = strings.TrimSpace(code)
|
||||
password = strings.TrimSpace(password)
|
||||
if code == "" {
|
||||
return nil, buserr.New("ErrFileShareInvalid")
|
||||
}
|
||||
|
||||
item, err := fileShareRepo.GetFirst(fileShareRepo.WithByCode(code))
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, buserr.New("ErrFileShareInvalid")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
now := time.Now().Unix()
|
||||
if item.ExpiresUnix > 0 && now > item.ExpiresUnix {
|
||||
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||
return nil, buserr.New("ErrFileShareExpired")
|
||||
}
|
||||
if item.PasswordHash != "" {
|
||||
if subtle.ConstantTimeCompare([]byte(hashPassword(item.PasswordSalt, password)), []byte(item.PasswordHash)) != 1 {
|
||||
return nil, buserr.New("ErrFileSharePassword")
|
||||
}
|
||||
}
|
||||
|
||||
info, err := os.Stat(item.Path)
|
||||
if err != nil || info.IsDir() {
|
||||
_ = fileShareRepo.Delete(repo.WithByID(item.ID))
|
||||
return nil, buserr.New("ErrFileSharePath")
|
||||
}
|
||||
|
||||
return &item, nil
|
||||
}
|
||||
@@ -0,0 +1,147 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"go.mongodb.org/mongo-driver/mongo"
|
||||
"go.mongodb.org/mongo-driver/mongo/options"
|
||||
"go.mongodb.org/mongo-driver/mongo/readpref"
|
||||
)
|
||||
|
||||
type mongodbConnectionInfo struct {
|
||||
Address string
|
||||
Port uint
|
||||
InitialDB string
|
||||
Username string
|
||||
Password string
|
||||
Timeout uint
|
||||
SSL bool
|
||||
RootCert string
|
||||
ClientKey string
|
||||
ClientCert string
|
||||
SkipVerify bool
|
||||
}
|
||||
|
||||
func mongodbConnectionInfoFromCreate(req dto.DatabaseCreate) mongodbConnectionInfo {
|
||||
return mongodbConnectionInfo{
|
||||
Address: req.Address,
|
||||
Port: req.Port,
|
||||
InitialDB: req.InitialDB,
|
||||
Username: req.Username,
|
||||
Password: req.Password,
|
||||
Timeout: req.Timeout,
|
||||
SSL: req.SSL,
|
||||
RootCert: req.RootCert,
|
||||
ClientKey: req.ClientKey,
|
||||
ClientCert: req.ClientCert,
|
||||
SkipVerify: req.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func mongodbConnectionInfoFromModel(db model.Database) mongodbConnectionInfo {
|
||||
return mongodbConnectionInfo{
|
||||
Address: db.Address,
|
||||
Port: db.Port,
|
||||
InitialDB: db.InitialDB,
|
||||
Username: db.Username,
|
||||
Password: db.Password,
|
||||
Timeout: db.Timeout,
|
||||
SSL: db.SSL,
|
||||
RootCert: db.RootCert,
|
||||
ClientKey: db.ClientKey,
|
||||
ClientCert: db.ClientCert,
|
||||
SkipVerify: db.SkipVerify,
|
||||
}
|
||||
}
|
||||
|
||||
func loadRemoteMongodbConnection(database string) (mongodbConnectionInfo, error) {
|
||||
db, err := databaseRepo.Get(repo.WithByName(database))
|
||||
if err != nil {
|
||||
return mongodbConnectionInfo{}, err
|
||||
}
|
||||
return mongodbConnectionInfoFromModel(db), nil
|
||||
}
|
||||
|
||||
func newRemoteMongodbClient(info mongodbConnectionInfo) (*mongo.Client, context.Context, context.CancelFunc, error) {
|
||||
timeout := time.Duration(info.Timeout) * time.Second
|
||||
if timeout == 0 {
|
||||
timeout = 30 * time.Second
|
||||
}
|
||||
|
||||
clientOptions := options.Client().ApplyURI(buildRemoteMongodbURI(info)).
|
||||
SetServerSelectionTimeout(timeout).
|
||||
SetConnectTimeout(timeout)
|
||||
if info.SSL {
|
||||
tlsConfig, err := buildRemoteMongodbTLSConfig(info)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
clientOptions.SetTLSConfig(tlsConfig)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), timeout)
|
||||
client, err := mongo.Connect(ctx, clientOptions)
|
||||
if err != nil {
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
if err := client.Ping(ctx, readpref.Primary()); err != nil {
|
||||
_ = client.Disconnect(context.Background())
|
||||
cancel()
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
return client, ctx, cancel, nil
|
||||
}
|
||||
|
||||
func buildRemoteMongodbURI(info mongodbConnectionInfo) string {
|
||||
uri := url.URL{
|
||||
Scheme: "mongodb",
|
||||
Host: net.JoinHostPort(info.Address, strconv.Itoa(int(info.Port))),
|
||||
Path: "/",
|
||||
}
|
||||
uri.User = url.UserPassword(info.Username, info.Password)
|
||||
query := url.Values{}
|
||||
authSource := info.InitialDB
|
||||
if authSource == "" {
|
||||
authSource = "admin"
|
||||
}
|
||||
query.Set("authSource", authSource)
|
||||
query.Set("directConnection", "true")
|
||||
uri.RawQuery = query.Encode()
|
||||
return uri.String()
|
||||
}
|
||||
|
||||
func buildRemoteMongodbTLSConfig(info mongodbConnectionInfo) (*tls.Config, error) {
|
||||
tlsConfig := &tls.Config{
|
||||
MinVersion: tls.VersionTLS12,
|
||||
InsecureSkipVerify: info.SkipVerify,
|
||||
}
|
||||
|
||||
if info.RootCert != "" {
|
||||
pool := x509.NewCertPool()
|
||||
if !pool.AppendCertsFromPEM([]byte(info.RootCert)) {
|
||||
return nil, fmt.Errorf("load mongodb ca cert failed")
|
||||
}
|
||||
tlsConfig.RootCAs = pool
|
||||
}
|
||||
|
||||
if info.ClientCert != "" && info.ClientKey != "" {
|
||||
cert, err := tls.X509KeyPair([]byte(info.ClientCert), []byte(info.ClientKey))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlsConfig.Certificates = []tls.Certificate{cert}
|
||||
}
|
||||
|
||||
return tlsConfig, nil
|
||||
}
|
||||
@@ -52,7 +52,12 @@ func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningPro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
procCache := make(map[int32]ListeningProcess, 64)
|
||||
// One cache entry per (PID, socket type) so TCP and UDP sockets are not merged under one Protocol.
|
||||
type procKey struct {
|
||||
pid int32
|
||||
protocol uint32
|
||||
}
|
||||
procCache := make(map[procKey]ListeningProcess, 64)
|
||||
|
||||
for _, conn := range conn {
|
||||
if conn.Pid == 0 {
|
||||
@@ -60,7 +65,8 @@ func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningPro
|
||||
}
|
||||
|
||||
if (conn.Status == "LISTEN" && conn.Type == syscall.SOCK_STREAM) || (conn.Type == syscall.SOCK_DGRAM && conn.Raddr.Port == 0) {
|
||||
if _, exists := procCache[conn.Pid]; !exists {
|
||||
key := procKey{pid: conn.Pid, protocol: conn.Type}
|
||||
if _, exists := procCache[key]; !exists {
|
||||
proc, err := process.NewProcess(conn.Pid)
|
||||
if err != nil {
|
||||
continue
|
||||
@@ -72,11 +78,11 @@ func (ps *ProcessService) GetListeningProcess(c context.Context) ([]ListeningPro
|
||||
procData.Port = make(map[uint32]struct{})
|
||||
procData.Port[conn.Laddr.Port] = struct{}{}
|
||||
procData.Protocol = conn.Type
|
||||
procCache[conn.Pid] = procData
|
||||
procCache[key] = procData
|
||||
} else {
|
||||
p := procCache[conn.Pid]
|
||||
p := procCache[key]
|
||||
p.Port[conn.Laddr.Port] = struct{}{}
|
||||
procCache[conn.Pid] = p
|
||||
procCache[key] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"math"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"strconv"
|
||||
@@ -15,9 +17,20 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
"github.com/1Panel-dev/1Panel/agent/constant"
|
||||
"github.com/1Panel-dev/1Panel/agent/global"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/common"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/re"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"gopkg.in/ini.v1"
|
||||
)
|
||||
|
||||
const (
|
||||
recycleBinClashDir = ".1panel_clash"
|
||||
recycleBinFilesSubdir = "files"
|
||||
recycleBinInfoSubdir = "info"
|
||||
trashInfoSuffix = ".trashinfo"
|
||||
trashInfoSection = "Trash Info"
|
||||
trashInfoTimeLayout = "2006-01-02T15:04:05"
|
||||
)
|
||||
|
||||
type RecycleBinService struct {
|
||||
@@ -35,39 +48,32 @@ func NewIRecycleBinService() IRecycleBinService {
|
||||
}
|
||||
|
||||
func (r RecycleBinService) Page(search dto.PageInfo) (int64, []response.RecycleBinDTO, error) {
|
||||
var (
|
||||
result []response.RecycleBinDTO
|
||||
)
|
||||
var result []response.RecycleBinDTO
|
||||
partitions, err := disk.Partitions(false)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
op := files.NewFileOp()
|
||||
for _, p := range partitions {
|
||||
dir := path.Join(p.Mountpoint, ".1panel_clash")
|
||||
if !op.Stat(dir) {
|
||||
clashRoot := path.Join(p.Mountpoint, recycleBinClashDir)
|
||||
if !op.Stat(clashRoot) {
|
||||
continue
|
||||
}
|
||||
clashFiles, err := os.ReadDir(dir)
|
||||
if err != nil {
|
||||
return 0, nil, err
|
||||
}
|
||||
for _, file := range clashFiles {
|
||||
if strings.HasPrefix(file.Name(), "_1p_") {
|
||||
recycleDTO, err := getRecycleBinDTOFromName(file.Name())
|
||||
recycleDTO.IsDir = file.IsDir()
|
||||
recycleDTO.From = dir
|
||||
if err == nil {
|
||||
result = append(result, *recycleDTO)
|
||||
}
|
||||
}
|
||||
}
|
||||
result = append(result, collectTrashEntries(clashRoot)...)
|
||||
result = append(result, collectLegacyEntries(clashRoot)...)
|
||||
}
|
||||
startIndex := (search.Page - 1) * search.PageSize
|
||||
if search.PageSize <= 0 {
|
||||
return int64(len(result)), []response.RecycleBinDTO{}, nil
|
||||
}
|
||||
page := search.Page
|
||||
if page < 1 {
|
||||
page = 1
|
||||
}
|
||||
startIndex := (page - 1) * search.PageSize
|
||||
endIndex := startIndex + search.PageSize
|
||||
|
||||
if startIndex > len(result) {
|
||||
return int64(len(result)), result, nil
|
||||
if startIndex >= len(result) {
|
||||
return int64(len(result)), []response.RecycleBinDTO{}, nil
|
||||
}
|
||||
if endIndex > len(result) {
|
||||
endIndex = len(result)
|
||||
@@ -83,34 +89,55 @@ func (r RecycleBinService) Create(create request.RecycleBinCreate) error {
|
||||
if !op.Stat(create.SourcePath) {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
clashDir, err := getClashDir(create.SourcePath)
|
||||
clashRoot, err := getClashDir(create.SourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
paths := strings.Split(create.SourcePath, "/")
|
||||
rNamePre := strings.Join(paths, "_1p_")
|
||||
filesDir, infoDir, err := ensureTrashDirs(clashRoot)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
deleteTime := time.Now()
|
||||
openFile, err := op.OpenFile(create.SourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fileInfo, err := openFile.Stat()
|
||||
_ = openFile.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size := 0
|
||||
size := int64(0)
|
||||
if fileInfo.IsDir() {
|
||||
sizeF, err := op.GetDirSize(create.SourcePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
size = int(sizeF)
|
||||
size = int64(sizeF)
|
||||
} else {
|
||||
size = int(fileInfo.Size())
|
||||
size = fileInfo.Size()
|
||||
}
|
||||
|
||||
rName := fmt.Sprintf("_1p_%s%s_p_%d_%d", "file", rNamePre, size, deleteTime.Unix())
|
||||
return op.Mv(create.SourcePath, path.Join(clashDir, rName))
|
||||
rName := allocateTrashEntryName(filesDir, infoDir, create.SourcePath)
|
||||
info := trashInfo{
|
||||
Path: create.SourcePath,
|
||||
DeletionDate: deleteTime,
|
||||
Size: size,
|
||||
IsDir: fileInfo.IsDir(),
|
||||
}
|
||||
infoPath := path.Join(infoDir, rName+trashInfoSuffix)
|
||||
if err := writeTrashInfo(infoPath, info); err != nil {
|
||||
return err
|
||||
}
|
||||
targetPath := path.Join(filesDir, rName)
|
||||
if err := op.Mv(create.SourcePath, targetPath); err != nil {
|
||||
if rmErr := os.Remove(infoPath); rmErr != nil && !os.IsNotExist(rmErr) {
|
||||
global.LOG.Warnf("rollback trashinfo failed for %s: %v", infoPath, rmErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
|
||||
@@ -119,10 +146,13 @@ func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
|
||||
if !op.Stat(filePath) {
|
||||
return buserr.New("ErrLinkPathNotFound")
|
||||
}
|
||||
recycleBinDTO, err := getRecycleBinDTOFromName(reduce.RName)
|
||||
recycleBinDTO, err := loadRecycleBinDTO(reduce.From, reduce.RName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if recycleBinDTO.SourcePath == "" {
|
||||
return buserr.New("ErrSourcePathNotFound")
|
||||
}
|
||||
if !op.Stat(path.Dir(recycleBinDTO.SourcePath)) {
|
||||
return buserr.New("ErrSourcePathNotFound")
|
||||
}
|
||||
@@ -131,7 +161,13 @@ func (r RecycleBinService) Reduce(reduce request.RecycleBinReduce) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return op.Mv(filePath, recycleBinDTO.SourcePath)
|
||||
if err := op.Mv(filePath, recycleBinDTO.SourcePath); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := cleanupTrashInfoByEntryPath(filePath); err != nil {
|
||||
global.LOG.Warnf("cleanup trashinfo failed for %s: %v", filePath, err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r RecycleBinService) Clear() error {
|
||||
@@ -141,17 +177,22 @@ func (r RecycleBinService) Clear() error {
|
||||
}
|
||||
op := files.NewFileOp()
|
||||
for _, p := range partitions {
|
||||
dir := path.Join(p.Mountpoint, ".1panel_clash")
|
||||
dir := path.Join(p.Mountpoint, recycleBinClashDir)
|
||||
if !op.Stat(dir) {
|
||||
continue
|
||||
}
|
||||
newDir := path.Join(p.Mountpoint, "1panel_clash")
|
||||
newDir := path.Join(p.Mountpoint, recycleBinClashDir+"-"+strings.ReplaceAll(common.GetUuid(), "-", ""))
|
||||
if err := op.Mv(dir, newDir); err != nil {
|
||||
return err
|
||||
}
|
||||
go func() {
|
||||
_ = op.DeleteDir(newDir)
|
||||
}()
|
||||
go func(target string) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
global.LOG.Warnf("clear recycle bin panic on %s: %v", target, r)
|
||||
}
|
||||
}()
|
||||
_ = op.DeleteDir(target)
|
||||
}(newDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -166,8 +207,8 @@ func getClashDir(realPath string) (string, error) {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(realPath, p.Mountpoint) {
|
||||
clashDir := path.Join(p.Mountpoint, ".1panel_clash")
|
||||
if err = createClashDir(path.Join(p.Mountpoint, ".1panel_clash")); err != nil {
|
||||
clashDir := path.Join(p.Mountpoint, recycleBinClashDir)
|
||||
if err = createClashDir(clashDir); err != nil {
|
||||
return "", err
|
||||
}
|
||||
return clashDir, nil
|
||||
@@ -186,6 +227,264 @@ func createClashDir(clashDir string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ensureTrashDirs(clashRoot string) (string, string, error) {
|
||||
filesDir := path.Join(clashRoot, recycleBinFilesSubdir)
|
||||
infoDir := path.Join(clashRoot, recycleBinInfoSubdir)
|
||||
op := files.NewFileOp()
|
||||
if !op.Stat(filesDir) {
|
||||
if err := op.CreateDir(filesDir, constant.DirPerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
if !op.Stat(infoDir) {
|
||||
if err := op.CreateDir(infoDir, constant.DirPerm); err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
}
|
||||
return filesDir, infoDir, nil
|
||||
}
|
||||
|
||||
func trashFilesDir(clashRoot string) string {
|
||||
return path.Join(clashRoot, recycleBinFilesSubdir)
|
||||
}
|
||||
|
||||
func trashInfoDir(clashRoot string) string {
|
||||
return path.Join(clashRoot, recycleBinInfoSubdir)
|
||||
}
|
||||
|
||||
func trashInfoPath(infoDir, entryName string) string {
|
||||
return path.Join(infoDir, entryName+trashInfoSuffix)
|
||||
}
|
||||
|
||||
type trashInfo struct {
|
||||
Path string
|
||||
DeletionDate time.Time
|
||||
Size int64
|
||||
IsDir bool
|
||||
}
|
||||
|
||||
func writeTrashInfo(dstPath string, info trashInfo) error {
|
||||
cfg := ini.Empty()
|
||||
section, err := cfg.NewSection(trashInfoSection)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("Path", url.PathEscape(info.Path)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("DeletionDate", info.DeletionDate.Format(trashInfoTimeLayout)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("Size", strconv.FormatInt(info.Size, 10)); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := section.NewKey("IsDir", strconv.FormatBool(info.IsDir)); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if _, err := cfg.WriteTo(&buf); err != nil {
|
||||
return err
|
||||
}
|
||||
tmp := dstPath + ".tmp"
|
||||
if err := os.WriteFile(tmp, buf.Bytes(), constant.FilePerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(tmp, dstPath)
|
||||
}
|
||||
|
||||
func readTrashInfo(srcPath string) (*trashInfo, error) {
|
||||
cfg, err := ini.Load(srcPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
section, err := cfg.GetSection(trashInfoSection)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
rawPath := section.Key("Path").Value()
|
||||
decodedPath, err := url.PathUnescape(rawPath)
|
||||
if err != nil {
|
||||
decodedPath = rawPath
|
||||
}
|
||||
info := &trashInfo{
|
||||
Path: decodedPath,
|
||||
IsDir: section.Key("IsDir").MustBool(false),
|
||||
}
|
||||
if deletionStr := section.Key("DeletionDate").Value(); deletionStr != "" {
|
||||
if t, parseErr := time.ParseInLocation(trashInfoTimeLayout, deletionStr, time.Local); parseErr == nil {
|
||||
info.DeletionDate = t
|
||||
}
|
||||
}
|
||||
if sizeStr := section.Key("Size").Value(); sizeStr != "" {
|
||||
if sz, sizeErr := strconv.ParseInt(sizeStr, 10, 64); sizeErr == nil {
|
||||
info.Size = sz
|
||||
}
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func collectTrashEntries(clashRoot string) []response.RecycleBinDTO {
|
||||
filesDir := trashFilesDir(clashRoot)
|
||||
infoDir := trashInfoDir(clashRoot)
|
||||
op := files.NewFileOp()
|
||||
if !op.Stat(filesDir) {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(filesDir)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("read recycle files dir %s failed: %v", filesDir, err)
|
||||
return nil
|
||||
}
|
||||
var result []response.RecycleBinDTO
|
||||
for _, entry := range entries {
|
||||
dto, err := buildTrashDTO(filesDir, infoDir, entry)
|
||||
if err != nil {
|
||||
global.LOG.Warnf("build recycle dto for %s failed: %v", entry.Name(), err)
|
||||
continue
|
||||
}
|
||||
result = append(result, *dto)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func collectLegacyEntries(clashRoot string) []response.RecycleBinDTO {
|
||||
op := files.NewFileOp()
|
||||
if !op.Stat(clashRoot) {
|
||||
return nil
|
||||
}
|
||||
entries, err := os.ReadDir(clashRoot)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
var result []response.RecycleBinDTO
|
||||
for _, entry := range entries {
|
||||
name := entry.Name()
|
||||
if name == recycleBinFilesSubdir || name == recycleBinInfoSubdir {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(name, "_1p_") {
|
||||
continue
|
||||
}
|
||||
dto, err := getRecycleBinDTOFromName(name)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
dto.IsDir = entry.IsDir()
|
||||
dto.From = clashRoot
|
||||
result = append(result, *dto)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func buildTrashDTO(filesDir, infoDir string, entry os.DirEntry) (*response.RecycleBinDTO, error) {
|
||||
entryName := entry.Name()
|
||||
infoPath := trashInfoPath(infoDir, entryName)
|
||||
if info, err := readTrashInfo(infoPath); err == nil {
|
||||
return trashInfoToDTO(entryName, filesDir, info), nil
|
||||
} else if !os.IsNotExist(err) {
|
||||
global.LOG.Warnf("read trashinfo %s failed: %v", infoPath, err)
|
||||
}
|
||||
|
||||
entryPath := path.Join(filesDir, entryName)
|
||||
fi, err := os.Stat(entryPath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
size := fi.Size()
|
||||
if fi.IsDir() {
|
||||
if sz, sizeErr := files.NewFileOp().GetDirSize(entryPath); sizeErr == nil {
|
||||
size = int64(sz)
|
||||
}
|
||||
}
|
||||
return &response.RecycleBinDTO{
|
||||
Name: entryName,
|
||||
Size: clampToInt(size),
|
||||
Type: "file",
|
||||
DeleteTime: fi.ModTime(),
|
||||
RName: entryName,
|
||||
SourcePath: "",
|
||||
IsDir: fi.IsDir(),
|
||||
From: filesDir,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func trashInfoToDTO(entryName, filesDir string, info *trashInfo) *response.RecycleBinDTO {
|
||||
return &response.RecycleBinDTO{
|
||||
Name: path.Base(info.Path),
|
||||
Size: clampToInt(info.Size),
|
||||
Type: "file",
|
||||
DeleteTime: info.DeletionDate,
|
||||
RName: entryName,
|
||||
SourcePath: info.Path,
|
||||
IsDir: info.IsDir,
|
||||
From: filesDir,
|
||||
}
|
||||
}
|
||||
|
||||
func loadRecycleBinDTO(from, name string) (*response.RecycleBinDTO, error) {
|
||||
if path.Base(from) == recycleBinFilesSubdir {
|
||||
infoDir := trashInfoDir(path.Dir(from))
|
||||
info, err := readTrashInfo(trashInfoPath(infoDir, name))
|
||||
if err == nil {
|
||||
return trashInfoToDTO(name, from, info), nil
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
global.LOG.Warnf("read trashinfo for %s failed: %v", name, err)
|
||||
}
|
||||
}
|
||||
|
||||
dto, err := getRecycleBinDTOFromName(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto.From = from
|
||||
return dto, nil
|
||||
}
|
||||
|
||||
func cleanupTrashInfoByEntryPath(filePath string) error {
|
||||
cleaned := path.Clean(filePath)
|
||||
parent := path.Dir(cleaned)
|
||||
if path.Base(parent) != recycleBinFilesSubdir {
|
||||
return nil
|
||||
}
|
||||
clashRoot := path.Dir(parent)
|
||||
if path.Base(clashRoot) != recycleBinClashDir {
|
||||
return nil
|
||||
}
|
||||
infoPath := trashInfoPath(trashInfoDir(clashRoot), path.Base(cleaned))
|
||||
if err := os.Remove(infoPath); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func allocateTrashEntryName(filesDir, infoDir, sourcePath string) string {
|
||||
ext := path.Ext(path.Base(sourcePath))
|
||||
op := files.NewFileOp()
|
||||
for i := 0; i < 5; i++ {
|
||||
name := strings.ReplaceAll(common.GetUuid(), "-", "") + ext
|
||||
if op.Stat(path.Join(filesDir, name)) {
|
||||
continue
|
||||
}
|
||||
if op.Stat(trashInfoPath(infoDir, name)) {
|
||||
continue
|
||||
}
|
||||
return name
|
||||
}
|
||||
return fmt.Sprintf("%s-%d%s", strings.ReplaceAll(common.GetUuid(), "-", ""), time.Now().UnixNano(), ext)
|
||||
}
|
||||
|
||||
func clampToInt(v int64) int {
|
||||
if v > math.MaxInt {
|
||||
return math.MaxInt
|
||||
}
|
||||
if v < math.MinInt {
|
||||
return math.MinInt
|
||||
}
|
||||
return int(v)
|
||||
}
|
||||
|
||||
func getRecycleBinDTOFromName(filename string) (*response.RecycleBinDTO, error) {
|
||||
matches := re.GetRegex(re.RecycleBinFilePattern).FindStringSubmatch(filename)
|
||||
if len(matches) != 4 {
|
||||
|
||||
@@ -41,19 +41,24 @@ import (
|
||||
|
||||
func handleRuntime(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
|
||||
runtimeDir := path.Join(global.Dir.RuntimeDir, create.Type)
|
||||
projectDir := path.Join(runtimeDir, create.Name)
|
||||
if create.CodeDir != "" && isPathInsideOrEqual(projectDir, create.CodeDir) {
|
||||
return buserr.New("ErrRuntimeProjectDirContainsCodeDir")
|
||||
}
|
||||
if err = fileOp.CopyDir(appVersionDir, runtimeDir); err != nil {
|
||||
return
|
||||
}
|
||||
versionDir := path.Join(runtimeDir, filepath.Base(appVersionDir))
|
||||
projectDir := path.Join(runtimeDir, create.Name)
|
||||
cleanupTarget := versionDir
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = fileOp.DeleteDir(projectDir)
|
||||
_ = fileOp.DeleteDir(cleanupTarget)
|
||||
}
|
||||
}()
|
||||
if err = fileOp.Rename(versionDir, projectDir); err != nil {
|
||||
return
|
||||
}
|
||||
cleanupTarget = projectDir
|
||||
composeContent, envContent, _, err := handleParams(create, projectDir)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -76,12 +81,36 @@ func handleRuntime(create request.RuntimeCreate, runtime *model.Runtime, fileOp
|
||||
return
|
||||
}
|
||||
|
||||
func isPathInsideOrEqual(baseDir, targetDir string) bool {
|
||||
baseAbs := resolveRuntimePath(baseDir)
|
||||
targetAbs := resolveRuntimePath(targetDir)
|
||||
rel, err := filepath.Rel(baseAbs, targetAbs)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return rel == "." || (rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator)))
|
||||
}
|
||||
|
||||
func resolveRuntimePath(dir string) string {
|
||||
cleanDir := filepath.Clean(dir)
|
||||
if realDir, err := filepath.EvalSymlinks(cleanDir); err == nil {
|
||||
return realDir
|
||||
}
|
||||
if absDir, err := filepath.Abs(cleanDir); err == nil {
|
||||
return absDir
|
||||
}
|
||||
return cleanDir
|
||||
}
|
||||
|
||||
func handlePHP(create request.RuntimeCreate, runtime *model.Runtime, fileOp files.FileOp, appVersionDir string) (err error) {
|
||||
runtimeDir := path.Join(global.Dir.RuntimeDir, create.Type)
|
||||
projectDir := path.Join(runtimeDir, create.Name)
|
||||
if create.CodeDir != "" && isPathInsideOrEqual(projectDir, create.CodeDir) {
|
||||
return buserr.New("ErrRuntimeProjectDirContainsCodeDir")
|
||||
}
|
||||
if err = fileOp.CopyDirWithNewName(appVersionDir, runtimeDir, create.Name); err != nil {
|
||||
return
|
||||
}
|
||||
projectDir := path.Join(runtimeDir, create.Name)
|
||||
defer func() {
|
||||
if err != nil {
|
||||
_ = fileOp.DeleteDir(projectDir)
|
||||
@@ -640,7 +669,7 @@ func handleCompose(env gotenv.Env, composeContent []byte, create request.Runtime
|
||||
for _, host := range create.ExtraHosts {
|
||||
extraHosts = append(extraHosts, fmt.Sprintf("%s:%s", host.Hostname, host.IP))
|
||||
}
|
||||
delete(serviceValue, "extraHosts")
|
||||
delete(serviceValue, "extra_hosts")
|
||||
if len(extraHosts) > 0 {
|
||||
serviceValue["extra_hosts"] = extraHosts
|
||||
}
|
||||
|
||||
@@ -8,6 +8,8 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/repo"
|
||||
"github.com/1Panel-dev/1Panel/agent/buserr"
|
||||
@@ -23,8 +25,12 @@ type SettingService struct{}
|
||||
type ISettingService interface {
|
||||
GetSettingInfo() (*dto.SettingInfo, error)
|
||||
GetTerminalAIInfo() (*dto.TerminalAIInfo, error)
|
||||
GetFileManageAIInfo() (*dto.FileManageAIInfo, error)
|
||||
GetFileHistorySettingInfo() (*response.FileHistorySettingInfo, error)
|
||||
Update(key, value string) error
|
||||
UpdateTerminalAI(req dto.TerminalAIInfo) error
|
||||
UpdateFileManageAI(req dto.FileManageAIInfo) error
|
||||
UpdateFileHistorySetting(req request.FileHistorySettingUpdate) error
|
||||
|
||||
TestConnByInfo(req dto.SSHConnData) bool
|
||||
SaveConnInfo(req dto.SSHConnData) error
|
||||
@@ -87,6 +93,24 @@ func (u *SettingService) GetTerminalAIInfo() (*dto.TerminalAIInfo, error) {
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (u *SettingService) GetFileManageAIInfo() (*dto.FileManageAIInfo, error) {
|
||||
info := &dto.FileManageAIInfo{
|
||||
AIStatus: constant.StatusDisable,
|
||||
AIAccountID: "",
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey("FileAIStatus"); err == nil && value != "" {
|
||||
info.AIStatus = value
|
||||
}
|
||||
if value, err := settingRepo.GetValueByKey("FileAIAccountID"); err == nil {
|
||||
info.AIAccountID = value
|
||||
}
|
||||
return info, nil
|
||||
}
|
||||
|
||||
func (u *SettingService) GetFileHistorySettingInfo() (*response.FileHistorySettingInfo, error) {
|
||||
return historyService.GetSettingInfo()
|
||||
}
|
||||
|
||||
func (u *SettingService) Update(key, value string) error {
|
||||
return settingRepo.UpdateOrCreate(key, value)
|
||||
}
|
||||
@@ -123,6 +147,40 @@ func (u *SettingService) UpdateTerminalAI(req dto.TerminalAIInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SettingService) UpdateFileManageAI(req dto.FileManageAIInfo) error {
|
||||
if strings.EqualFold(strings.TrimSpace(req.AIStatus), constant.StatusEnable) {
|
||||
accountID, err := strconv.ParseUint(strings.TrimSpace(req.AIAccountID), 10, 64)
|
||||
if err != nil || accountID == 0 {
|
||||
return buserr.New("ErrAgentAccountIDRequired")
|
||||
}
|
||||
currentStatus, _ := settingRepo.GetValueByKey("FileAIStatus")
|
||||
currentAccountID, _ := settingRepo.GetValueByKey("FileAIAccountID")
|
||||
needValidate := !strings.EqualFold(strings.TrimSpace(currentStatus), constant.StatusEnable) ||
|
||||
strings.TrimSpace(currentAccountID) != strings.TrimSpace(req.AIAccountID)
|
||||
if needValidate {
|
||||
if err := terminalai.ValidateTerminalAccount(uint(accountID)); err != nil {
|
||||
return buserr.WithErr("ErrAgentAccountUnavailable", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
accountVal := strings.TrimSpace(req.AIAccountID)
|
||||
if !strings.EqualFold(strings.TrimSpace(req.AIStatus), constant.StatusEnable) {
|
||||
accountVal = ""
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate("FileAIStatus", req.AIStatus); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := settingRepo.UpdateOrCreate("FileAIAccountID", accountVal); err != nil {
|
||||
return err
|
||||
}
|
||||
terminalai.InvalidateFileAIRuntimeCache()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *SettingService) UpdateFileHistorySetting(req request.FileHistorySettingUpdate) error {
|
||||
return historyService.UpdateSetting(req)
|
||||
}
|
||||
|
||||
func (u *SettingService) TestConnByInfo(req dto.SSHConnData) bool {
|
||||
if req.AuthMode == "password" && len(req.Password) != 0 {
|
||||
password, err := base64.StdEncoding.DecodeString(req.Password)
|
||||
|
||||
+570
-123
@@ -9,6 +9,7 @@ import (
|
||||
"os"
|
||||
"os/user"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -33,6 +34,8 @@ import (
|
||||
)
|
||||
|
||||
const sshPath = "/etc/ssh/sshd_config"
|
||||
const defaultSSHPort = "22"
|
||||
const sshManagedMarker = "# config by 1panel"
|
||||
|
||||
type SSHService struct{}
|
||||
|
||||
@@ -41,7 +44,7 @@ type ISSHService interface {
|
||||
OperateSSH(operation string) error
|
||||
Update(req dto.SSHUpdate) error
|
||||
LoadSSHFile(name string) (string, error)
|
||||
UpdateByFile(req dto.SettingUpdate) error
|
||||
UpdateByFile(req dto.SSHConfUpdate) error
|
||||
|
||||
LoadLog(ctx *gin.Context, req dto.SearchSSHLog) (int64, []dto.SSHHistory, error)
|
||||
ExportLog(ctx *gin.Context, req dto.SearchSSHLog) (string, error)
|
||||
@@ -57,6 +60,28 @@ func NewISSHService() ISSHService {
|
||||
return &SSHService{}
|
||||
}
|
||||
|
||||
type sshDirective struct {
|
||||
Key string
|
||||
Value string
|
||||
File string
|
||||
Line int
|
||||
InMatch bool
|
||||
}
|
||||
|
||||
type sshConfigFile struct {
|
||||
Path string `json:"path"`
|
||||
Priority int `json:"priority"`
|
||||
}
|
||||
|
||||
type sshManagedBlock struct {
|
||||
Start int
|
||||
End int
|
||||
}
|
||||
|
||||
func (b sshManagedBlock) contains(line int) bool {
|
||||
return line >= b.Start && line < b.End
|
||||
}
|
||||
|
||||
func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
|
||||
data := dto.SSHInfo{
|
||||
AutoStart: true,
|
||||
@@ -70,64 +95,9 @@ func (u *SSHService) GetSSHInfo() (*dto.SSHInfo, error) {
|
||||
PermitRootLogin: "yes",
|
||||
UseDNS: "yes",
|
||||
}
|
||||
serviceName, err := loadServiceName()
|
||||
if err != nil {
|
||||
data.IsExist = false
|
||||
data.Message = err.Error()
|
||||
} else {
|
||||
active, err := controller.CheckActive(serviceName)
|
||||
data.IsActive = active
|
||||
if !active && err != nil {
|
||||
data.Message = err.Error()
|
||||
}
|
||||
}
|
||||
|
||||
enable, err := controller.CheckEnable(serviceName)
|
||||
if err != nil {
|
||||
data.AutoStart = false
|
||||
} else {
|
||||
data.AutoStart = enable
|
||||
}
|
||||
|
||||
sshConf, err := os.ReadFile(sshPath)
|
||||
if err != nil {
|
||||
data.Message = err.Error()
|
||||
data.IsActive = false
|
||||
}
|
||||
lines := strings.Split(string(sshConf), "\n")
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(line, "Port ") {
|
||||
data.Port = strings.ReplaceAll(line, "Port ", "")
|
||||
}
|
||||
if strings.HasPrefix(line, "ListenAddress ") {
|
||||
itemAddr := strings.ReplaceAll(line, "ListenAddress ", "")
|
||||
if len(data.ListenAddress) != 0 {
|
||||
data.ListenAddress += ("," + itemAddr)
|
||||
} else {
|
||||
data.ListenAddress = itemAddr
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(line, "PasswordAuthentication ") {
|
||||
data.PasswordAuthentication = strings.ReplaceAll(line, "PasswordAuthentication ", "")
|
||||
}
|
||||
if strings.HasPrefix(line, "PubkeyAuthentication ") {
|
||||
data.PubkeyAuthentication = strings.ReplaceAll(line, "PubkeyAuthentication ", "")
|
||||
}
|
||||
if strings.HasPrefix(line, "PermitRootLogin ") {
|
||||
data.PermitRootLogin = strings.ReplaceAll(strings.ReplaceAll(line, "PermitRootLogin ", ""), "prohibit-password", "without-password")
|
||||
}
|
||||
if strings.HasPrefix(line, "UseDNS ") {
|
||||
data.UseDNS = strings.ReplaceAll(line, "UseDNS ", "")
|
||||
}
|
||||
}
|
||||
|
||||
currentUser, err := user.Current()
|
||||
if err != nil || len(currentUser.Name) == 0 {
|
||||
data.CurrentUser = "root"
|
||||
} else {
|
||||
data.CurrentUser = currentUser.Name
|
||||
}
|
||||
|
||||
loadSSHServiceStatus(&data)
|
||||
loadSSHConfigInfo(&data)
|
||||
data.CurrentUser = loadCurrentUserName()
|
||||
return &data, nil
|
||||
}
|
||||
|
||||
@@ -160,58 +130,175 @@ func (u *SSHService) Update(req dto.SSHUpdate) error {
|
||||
return err
|
||||
}
|
||||
|
||||
sshConf, err := os.ReadFile(sshPath)
|
||||
directives, _, err := parseSSHConfigTree(sshPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(string(sshConf), "\n")
|
||||
newFiles := updateSSHConf(lines, req.Key, req.NewValue)
|
||||
file, err := os.OpenFile(sshPath, os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
|
||||
if err != nil {
|
||||
oldPortValue := strings.Join(loadSSHPortValues(directives), ",")
|
||||
if err := updateSSHDirectiveValue(req.Key, req.NewValue, directives); err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
if _, err = file.WriteString(strings.Join(newFiles, "\n")); err != nil {
|
||||
return err
|
||||
}
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
if req.Key == "Port" {
|
||||
stdout, _ := cmd.RunDefaultWithStdoutBashCf("%s getenforce", sudo)
|
||||
if stdout == "Enforcing\n" {
|
||||
_ = cmd.RunDefaultBashCf("%s semanage port -a -t ssh_port_t -p tcp %s", sudo, req.NewValue)
|
||||
}
|
||||
|
||||
ruleItem := dto.PortRuleUpdate{
|
||||
OldRule: dto.PortRuleOperate{
|
||||
Operation: "remove",
|
||||
Port: req.OldValue,
|
||||
Protocol: "tcp",
|
||||
Strategy: "accept",
|
||||
},
|
||||
NewRule: dto.PortRuleOperate{
|
||||
Operation: "add",
|
||||
Port: req.NewValue,
|
||||
Protocol: "tcp",
|
||||
Strategy: "accept",
|
||||
},
|
||||
}
|
||||
if err := NewIFirewallService().UpdatePortRule(ruleItem); err != nil {
|
||||
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", req.OldValue, req.NewValue, err)
|
||||
}
|
||||
newPort, _ := strconv.Atoi(req.NewValue)
|
||||
if err := updateLocalConn(uint(newPort)); err != nil {
|
||||
global.LOG.Errorf("update local conn for terminal failed, err: %v", err)
|
||||
}
|
||||
|
||||
if err := updateSSHSocketFile(req.NewValue); err != nil {
|
||||
global.LOG.Errorf("update port for ssh.socket failed, err: %v", err)
|
||||
}
|
||||
handleSSHPortUpdate(oldPortValue, req.NewValue)
|
||||
}
|
||||
|
||||
_ = controller.HandleRestart(serviceName)
|
||||
return nil
|
||||
}
|
||||
|
||||
func loadSSHServiceStatus(data *dto.SSHInfo) {
|
||||
serviceName, err := loadServiceName()
|
||||
if err != nil {
|
||||
data.IsExist = false
|
||||
data.Message = err.Error()
|
||||
return
|
||||
}
|
||||
|
||||
active, err := controller.CheckActive(serviceName)
|
||||
data.IsActive = active
|
||||
if !active && err != nil {
|
||||
data.Message = err.Error()
|
||||
}
|
||||
|
||||
enable, err := controller.CheckEnable(serviceName)
|
||||
if err != nil {
|
||||
data.AutoStart = false
|
||||
return
|
||||
}
|
||||
data.AutoStart = enable
|
||||
}
|
||||
|
||||
func loadSSHConfigInfo(data *dto.SSHInfo) {
|
||||
directives, _, err := parseSSHConfigTree(sshPath)
|
||||
if err != nil {
|
||||
data.Message = err.Error()
|
||||
data.IsActive = false
|
||||
return
|
||||
}
|
||||
|
||||
if port := strings.Join(loadSSHPortValues(directives), ","); port != "" {
|
||||
data.Port = port
|
||||
}
|
||||
data.ListenAddress = strings.Join(loadSSHDirectiveValues(directives, "ListenAddress"), ",")
|
||||
if value, ok := loadFirstSSHDirectiveValue(directives, "PasswordAuthentication"); ok {
|
||||
data.PasswordAuthentication = value
|
||||
}
|
||||
if value, ok := loadFirstSSHDirectiveValue(directives, "PubkeyAuthentication"); ok {
|
||||
data.PubkeyAuthentication = value
|
||||
}
|
||||
if value, ok := loadFirstSSHDirectiveValue(directives, "PermitRootLogin"); ok {
|
||||
data.PermitRootLogin = strings.ReplaceAll(value, "prohibit-password", "without-password")
|
||||
}
|
||||
if value, ok := loadFirstSSHDirectiveValue(directives, "UseDNS"); ok {
|
||||
data.UseDNS = value
|
||||
}
|
||||
}
|
||||
|
||||
func loadCurrentUserName() string {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil || currentUser.Name == "" {
|
||||
return "root"
|
||||
}
|
||||
return currentUser.Name
|
||||
}
|
||||
|
||||
func handleSSHPortUpdate(oldValue, newValue string) {
|
||||
sudo := cmd.SudoHandleCmd()
|
||||
newPorts := splitSSHPorts(newValue)
|
||||
oldPorts := splitSSHPorts(oldValue)
|
||||
|
||||
stdout, _ := runWithOptionalSudo(sudo, "getenforce")
|
||||
if stdout == "Enforcing\n" {
|
||||
for _, port := range diffSSHPorts(oldPorts, newPorts) {
|
||||
if _, err := runWithOptionalSudo(sudo, "semanage", "port", "-d", "-t", "ssh_port_t", "-p", "tcp", port); err != nil {
|
||||
global.LOG.Warnf("remove selinux ssh port %s failed, err: %v", port, err)
|
||||
}
|
||||
}
|
||||
for _, port := range newPorts {
|
||||
_, _ = runWithOptionalSudo(sudo, "semanage", "port", "-a", "-t", "ssh_port_t", "-p", "tcp", port)
|
||||
}
|
||||
}
|
||||
|
||||
removedPorts, err := parseSSHPortsToInts(diffSSHPorts(oldPorts, newPorts))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("parse removed ssh ports failed, err: %v", err)
|
||||
} else {
|
||||
addedPorts, err := parseSSHPortsToInts(diffSSHPorts(newPorts, oldPorts))
|
||||
if err != nil {
|
||||
global.LOG.Errorf("parse added ssh ports failed, err: %v", err)
|
||||
} else if err := OperateFirewallPort(removedPorts, addedPorts); err != nil {
|
||||
global.LOG.Errorf("reset firewall rules %s -> %s failed, err: %v", oldValue, newValue, err)
|
||||
}
|
||||
}
|
||||
|
||||
primaryPort, err := loadPrimarySSHPort(newValue)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("load primary ssh port from %s failed, err: %v", newValue, err)
|
||||
return
|
||||
}
|
||||
if err := updateLocalConn(uint(primaryPort)); err != nil {
|
||||
global.LOG.Errorf("update local conn for terminal failed, err: %v", err)
|
||||
}
|
||||
if err := updateSSHSocketFile(strconv.Itoa(primaryPort)); err != nil {
|
||||
global.LOG.Errorf("update port for ssh.socket failed, err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func splitSSHPorts(value string) []string {
|
||||
var ports []string
|
||||
for _, item := range strings.Split(value, ",") {
|
||||
port := strings.TrimSpace(item)
|
||||
if port != "" {
|
||||
ports = append(ports, port)
|
||||
}
|
||||
}
|
||||
return ports
|
||||
}
|
||||
|
||||
func diffSSHPorts(left, right []string) []string {
|
||||
rightSet := make(map[string]struct{}, len(right))
|
||||
for _, item := range right {
|
||||
rightSet[item] = struct{}{}
|
||||
}
|
||||
var diff []string
|
||||
for _, item := range left {
|
||||
if _, ok := rightSet[item]; ok {
|
||||
continue
|
||||
}
|
||||
diff = append(diff, item)
|
||||
}
|
||||
return diff
|
||||
}
|
||||
|
||||
func loadPrimarySSHPort(value string) (int, error) {
|
||||
ports := splitSSHPorts(value)
|
||||
if len(ports) == 0 {
|
||||
return 0, fmt.Errorf("ssh port is empty")
|
||||
}
|
||||
return strconv.Atoi(ports[0])
|
||||
}
|
||||
|
||||
func parseSSHPortsToInts(ports []string) ([]int, error) {
|
||||
var values []int
|
||||
for _, port := range ports {
|
||||
value, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
values = append(values, value)
|
||||
}
|
||||
return values, nil
|
||||
}
|
||||
|
||||
func runWithOptionalSudo(sudo, name string, args ...string) (string, error) {
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
|
||||
if sudo != "" {
|
||||
commandArgs := append([]string{name}, args...)
|
||||
return cmdMgr.RunWithStdout("sudo", commandArgs...)
|
||||
}
|
||||
return cmdMgr.RunWithStdout(name, args...)
|
||||
}
|
||||
|
||||
func (u *SSHService) SyncRootCert() error {
|
||||
currentUser, err := user.Current()
|
||||
if err != nil {
|
||||
@@ -582,8 +669,25 @@ func (u *SSHService) LoadSSHFile(name string) (string, error) {
|
||||
fileName = currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
case "sshdConf":
|
||||
fileName = "/etc/ssh/sshd_config"
|
||||
case "sshdConfOptions":
|
||||
_, fileList, err := parseSSHConfigTree(sshPath)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
content, err := json.Marshal(fileList)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(content), nil
|
||||
default:
|
||||
return "", buserr.WithName("ErrNotSupportType", name)
|
||||
if strings.HasPrefix(name, "sshdConfPath:") {
|
||||
fileName = strings.TrimPrefix(name, "sshdConfPath:")
|
||||
if !isSSHConfigPathAllowed(fileName) {
|
||||
return "", buserr.WithName("ErrNotSupportType", name)
|
||||
}
|
||||
} else {
|
||||
return "", buserr.WithName("ErrNotSupportType", name)
|
||||
}
|
||||
}
|
||||
if _, err := os.Stat(fileName); err != nil {
|
||||
return "", buserr.WithErr("ErrHttpReqNotFound", err)
|
||||
@@ -595,7 +699,7 @@ func (u *SSHService) LoadSSHFile(name string) (string, error) {
|
||||
return string(content), nil
|
||||
}
|
||||
|
||||
func (u *SSHService) UpdateByFile(req dto.SettingUpdate) error {
|
||||
func (u *SSHService) UpdateByFile(req dto.SSHConfUpdate) error {
|
||||
var fileName string
|
||||
switch req.Key {
|
||||
case "authKeys":
|
||||
@@ -606,6 +710,11 @@ func (u *SSHService) UpdateByFile(req dto.SettingUpdate) error {
|
||||
fileName = currentUser.HomeDir + "/.ssh/authorized_keys"
|
||||
case "sshdConf":
|
||||
fileName = "/etc/ssh/sshd_config"
|
||||
case "sshdConfPath":
|
||||
fileName = req.Path
|
||||
if !isSSHConfigPathAllowed(fileName) {
|
||||
return buserr.WithName("ErrNotSupportType", req.Key)
|
||||
}
|
||||
default:
|
||||
return buserr.WithName("ErrNotSupportType", req.Key)
|
||||
}
|
||||
@@ -647,35 +756,372 @@ func sortFileList(fileNames []sshFileItem) []sshFileItem {
|
||||
return fileNames
|
||||
}
|
||||
|
||||
func updateSSHConf(oldFiles []string, param string, value string) []string {
|
||||
var valueItems []string
|
||||
if param != "ListenAddress" {
|
||||
valueItems = append(valueItems, value)
|
||||
} else {
|
||||
if value != "" {
|
||||
valueItems = strings.Split(value, ",")
|
||||
}
|
||||
func parseSSHConfigTree(root string) ([]sshDirective, []sshConfigFile, error) {
|
||||
rootPath, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var newFiles []string
|
||||
for _, line := range oldFiles {
|
||||
lineItem := strings.TrimSpace(line)
|
||||
if (strings.HasPrefix(lineItem, param) || strings.HasPrefix(lineItem, fmt.Sprintf("#%s", param))) && len(valueItems) != 0 {
|
||||
newFiles = append(newFiles, fmt.Sprintf("%s %s", param, valueItems[0]))
|
||||
valueItems = valueItems[1:]
|
||||
var (
|
||||
directives []sshDirective
|
||||
fileList []sshConfigFile
|
||||
order int
|
||||
priority int
|
||||
)
|
||||
visited := make(map[string]bool)
|
||||
if err := parseSSHConfigFile(rootPath, false, visited, &directives, &fileList, &order, &priority); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return directives, fileList, nil
|
||||
}
|
||||
|
||||
func parseSSHConfigFile(
|
||||
fileName string,
|
||||
inMatch bool,
|
||||
visited map[string]bool,
|
||||
directives *[]sshDirective,
|
||||
fileList *[]sshConfigFile,
|
||||
order *int,
|
||||
priority *int,
|
||||
) error {
|
||||
absolutePath, err := filepath.Abs(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if visited[absolutePath] {
|
||||
return nil
|
||||
}
|
||||
visited[absolutePath] = true
|
||||
|
||||
content, err := os.ReadFile(absolutePath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
lines := strings.Split(string(content), "\n")
|
||||
currentMatch := inMatch
|
||||
fileRegistered := false
|
||||
for index, rawLine := range lines {
|
||||
line := strings.TrimSpace(rawLine)
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(lineItem, param) && len(valueItems) == 0 {
|
||||
newFiles = append(newFiles, fmt.Sprintf("#%s", line))
|
||||
key, value, ok := parseSSHConfigLine(line)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
newFiles = append(newFiles, line)
|
||||
if strings.EqualFold(key, "Match") {
|
||||
currentMatch = true
|
||||
continue
|
||||
}
|
||||
if strings.EqualFold(key, "Include") {
|
||||
for _, includePath := range expandSSHIncludePaths(absolutePath, value) {
|
||||
if err := parseSSHConfigFile(includePath, currentMatch, visited, directives, fileList, order, priority); err != nil {
|
||||
global.LOG.Warnf("parse ssh include %s failed, err: %v", includePath, err)
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
if !fileRegistered {
|
||||
registerSSHConfigFile(absolutePath, fileList, priority)
|
||||
fileRegistered = true
|
||||
}
|
||||
*order++
|
||||
*directives = append(*directives, sshDirective{
|
||||
Key: key,
|
||||
Value: normalizeSSHDirectiveValue(key, value),
|
||||
File: absolutePath,
|
||||
Line: index,
|
||||
InMatch: currentMatch,
|
||||
})
|
||||
}
|
||||
if len(valueItems) != 0 {
|
||||
for _, item := range valueItems {
|
||||
newFiles = append(newFiles, fmt.Sprintf("%s %s", param, item))
|
||||
if !fileRegistered {
|
||||
registerSSHConfigFile(absolutePath, fileList, priority)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func registerSSHConfigFile(fileName string, fileList *[]sshConfigFile, priority *int) {
|
||||
for _, item := range *fileList {
|
||||
if item.Path == fileName {
|
||||
return
|
||||
}
|
||||
}
|
||||
return newFiles
|
||||
*priority++
|
||||
*fileList = append(*fileList, sshConfigFile{Path: fileName, Priority: *priority})
|
||||
}
|
||||
|
||||
func parseSSHConfigLine(line string) (string, string, bool) {
|
||||
line = trimSSHInlineComment(line)
|
||||
fields := strings.Fields(line)
|
||||
if len(fields) < 2 {
|
||||
return "", "", false
|
||||
}
|
||||
return fields[0], strings.Join(fields[1:], " "), true
|
||||
}
|
||||
|
||||
func trimSSHInlineComment(line string) string {
|
||||
inQuote := false
|
||||
for i := 0; i < len(line); i++ {
|
||||
switch line[i] {
|
||||
case '"':
|
||||
inQuote = !inQuote
|
||||
case '#':
|
||||
if !inQuote && (i == 0 || line[i-1] == ' ' || line[i-1] == '\t') {
|
||||
return strings.TrimSpace(line[:i])
|
||||
}
|
||||
}
|
||||
}
|
||||
return strings.TrimSpace(line)
|
||||
}
|
||||
|
||||
func expandSSHIncludePaths(baseFile, value string) []string {
|
||||
var includeFiles []string
|
||||
for _, item := range strings.Fields(strings.ReplaceAll(value, "\"", "")) {
|
||||
pattern := item
|
||||
if !filepath.IsAbs(pattern) {
|
||||
pattern = filepath.Join(filepath.Dir(baseFile), pattern)
|
||||
}
|
||||
matches, err := filepath.Glob(pattern)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
sort.Strings(matches)
|
||||
includeFiles = append(includeFiles, matches...)
|
||||
}
|
||||
return includeFiles
|
||||
}
|
||||
|
||||
func normalizeSSHDirectiveValue(key, value string) string {
|
||||
if strings.EqualFold(key, "PermitRootLogin") && value == "prohibit-password" {
|
||||
return "without-password"
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func loadFirstSSHDirectiveValue(directives []sshDirective, key string) (string, bool) {
|
||||
for _, item := range directives {
|
||||
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
||||
continue
|
||||
}
|
||||
return item.Value, true
|
||||
}
|
||||
return "", false
|
||||
}
|
||||
|
||||
func loadSSHDirectiveValues(directives []sshDirective, key string) []string {
|
||||
var values []string
|
||||
for _, item := range directives {
|
||||
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
||||
continue
|
||||
}
|
||||
values = append(values, item.Value)
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func loadSSHPortValues(directives []sshDirective) []string {
|
||||
values := loadSSHDirectiveValues(directives, "Port")
|
||||
if len(values) == 0 {
|
||||
return []string{defaultSSHPort}
|
||||
}
|
||||
return values
|
||||
}
|
||||
|
||||
func updateSSHDirectiveValue(key, value string, directives []sshDirective) error {
|
||||
if key == "Port" || key == "ListenAddress" {
|
||||
return rewriteSSHMultiValueDirective(key, value, directives)
|
||||
}
|
||||
return updateSSHSingleDirectiveValue(key, value, directives)
|
||||
}
|
||||
|
||||
func updateSSHSingleDirectiveValue(key, value string, directives []sshDirective) error {
|
||||
block, err := loadSSHManagedBlockFromFile(sshPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, item := range directives {
|
||||
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
||||
continue
|
||||
}
|
||||
if item.File == sshPath && block.contains(item.Line) {
|
||||
continue
|
||||
}
|
||||
if err := commentSSHConfigLines(item.File, []int{item.Line}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return rewriteSSHManagedDirectives(sshPath, key, []string{fmt.Sprintf("%s %s", key, value)})
|
||||
}
|
||||
|
||||
func rewriteSSHMultiValueDirective(key, value string, directives []sshDirective) error {
|
||||
block, err := loadSSHManagedBlockFromFile(sshPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
targetFiles := make(map[string][]int)
|
||||
for _, item := range directives {
|
||||
if item.InMatch || !strings.EqualFold(item.Key, key) {
|
||||
continue
|
||||
}
|
||||
if item.File == sshPath && block.contains(item.Line) {
|
||||
continue
|
||||
}
|
||||
targetFiles[item.File] = append(targetFiles[item.File], item.Line)
|
||||
}
|
||||
for fileName, lines := range targetFiles {
|
||||
if err := commentSSHConfigLines(fileName, lines); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return rewriteSSHManagedDirectives(sshPath, key, buildSSHDirectiveLines(key, value))
|
||||
}
|
||||
|
||||
func buildSSHDirectiveLines(key, value string) []string {
|
||||
var directives []string
|
||||
for _, item := range strings.Split(value, ",") {
|
||||
if item != "" {
|
||||
directives = append(directives, fmt.Sprintf("%s %s", key, item))
|
||||
}
|
||||
}
|
||||
return directives
|
||||
}
|
||||
|
||||
func rewriteSSHManagedDirectives(fileName, key string, newDirectives []string) error {
|
||||
content, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
lines, insertAt := ensureSSHManagedMarker(lines)
|
||||
|
||||
block, ok := loadSSHManagedBlock(lines)
|
||||
if ok {
|
||||
var filtered []string
|
||||
for index := block.Start; index < block.End; index++ {
|
||||
line := strings.TrimSpace(lines[index])
|
||||
if line == "" || strings.HasPrefix(line, "#") {
|
||||
filtered = append(filtered, lines[index])
|
||||
continue
|
||||
}
|
||||
itemKey, _, ok := parseSSHConfigLine(line)
|
||||
if ok && strings.EqualFold(itemKey, key) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, lines[index])
|
||||
}
|
||||
lines = append(append(lines[:block.Start], filtered...), lines[block.End:]...)
|
||||
insertAt = block.Start
|
||||
}
|
||||
|
||||
if len(newDirectives) != 0 {
|
||||
lines = insertSSHDirectivesAt(lines, insertAt, newDirectives)
|
||||
}
|
||||
return os.WriteFile(fileName, []byte(strings.Join(lines, "\n")), constant.FilePerm)
|
||||
}
|
||||
|
||||
func commentSSHConfigLines(fileName string, targetLines []int) error {
|
||||
content, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lines := strings.Split(string(content), "\n")
|
||||
targetSet := make(map[int]struct{}, len(targetLines))
|
||||
for _, line := range targetLines {
|
||||
targetSet[line] = struct{}{}
|
||||
}
|
||||
for index := range lines {
|
||||
if _, ok := targetSet[index]; !ok {
|
||||
continue
|
||||
}
|
||||
trimmed := strings.TrimSpace(lines[index])
|
||||
if trimmed == "" || strings.HasPrefix(trimmed, "#") {
|
||||
continue
|
||||
}
|
||||
lines[index] = "#" + lines[index]
|
||||
}
|
||||
return os.WriteFile(fileName, []byte(strings.Join(lines, "\n")), constant.FilePerm)
|
||||
}
|
||||
|
||||
func loadSSHInsertIndex(lines []string) int {
|
||||
for index, line := range lines {
|
||||
trimmed := strings.TrimSpace(line)
|
||||
if strings.HasPrefix(strings.ToLower(trimmed), "match ") {
|
||||
return index
|
||||
}
|
||||
}
|
||||
return len(lines)
|
||||
}
|
||||
|
||||
func ensureSSHManagedMarker(lines []string) ([]string, int) {
|
||||
for index, line := range lines {
|
||||
if strings.TrimSpace(line) == sshManagedMarker {
|
||||
return lines, index + 1
|
||||
}
|
||||
}
|
||||
insertAt := loadSSHInsertIndex(lines)
|
||||
lines = insertSSHDirectivesAt(lines, insertAt, []string{sshManagedMarker})
|
||||
return lines, insertAt + 1
|
||||
}
|
||||
|
||||
func loadSSHManagedBlock(lines []string) (sshManagedBlock, bool) {
|
||||
for index, line := range lines {
|
||||
if strings.TrimSpace(line) != sshManagedMarker {
|
||||
continue
|
||||
}
|
||||
end := len(lines)
|
||||
for next := index + 1; next < len(lines); next++ {
|
||||
if strings.HasPrefix(strings.ToLower(strings.TrimSpace(lines[next])), "match ") {
|
||||
end = next
|
||||
break
|
||||
}
|
||||
}
|
||||
return sshManagedBlock{Start: index + 1, End: end}, true
|
||||
}
|
||||
return sshManagedBlock{}, false
|
||||
}
|
||||
|
||||
func loadSSHManagedBlockFromFile(fileName string) (sshManagedBlock, error) {
|
||||
content, err := os.ReadFile(fileName)
|
||||
if err != nil {
|
||||
return sshManagedBlock{}, err
|
||||
}
|
||||
block, ok := loadSSHManagedBlock(strings.Split(string(content), "\n"))
|
||||
if !ok {
|
||||
return sshManagedBlock{}, nil
|
||||
}
|
||||
return block, nil
|
||||
}
|
||||
|
||||
func insertSSHDirectivesAt(lines []string, insertAt int, directives []string) []string {
|
||||
newLines := make([]string, 0, len(lines)+len(directives))
|
||||
newLines = append(newLines, lines[:insertAt]...)
|
||||
newLines = append(newLines, directives...)
|
||||
newLines = append(newLines, lines[insertAt:]...)
|
||||
return newLines
|
||||
}
|
||||
|
||||
func isSSHConfigPathAllowed(fileName string) bool {
|
||||
if fileName == "" {
|
||||
return false
|
||||
}
|
||||
absolutePath, err := filepath.Abs(fileName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if absolutePath == sshPath {
|
||||
return true
|
||||
}
|
||||
_, fileList, err := parseSSHConfigTree(sshPath)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
for _, item := range fileList {
|
||||
if item.Path == absolutePath {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func loadSSHData(ctx *gin.Context, command string, showCountFrom, showCountTo, currentYear int, nyc *time.Location) ([]dto.SSHHistory, int, int) {
|
||||
@@ -804,7 +1250,8 @@ func checkIsStandard(item dto.SSHHistory) bool {
|
||||
}
|
||||
|
||||
func handleGunzip(path string) error {
|
||||
if err := cmd.RunDefaultBashCf("gunzip %s", path); err != nil {
|
||||
cmdMgr := cmd.NewCommandMgr()
|
||||
if err := cmdMgr.Run("gunzip", path); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -21,6 +21,9 @@ func (u *TaskLogService) Page(req dto.SearchTaskLogReq) (int64, []dto.TaskDTO, e
|
||||
opts := []repo.DBOption{
|
||||
repo.WithOrderDesc("created_at"),
|
||||
}
|
||||
if req.TaskID != "" {
|
||||
opts = append(opts, taskRepo.WithByID(req.TaskID))
|
||||
}
|
||||
if req.Status != "" {
|
||||
opts = append(opts, repo.WithByStatus(req.Status))
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"os"
|
||||
"path"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -46,6 +47,28 @@ import (
|
||||
type WebsiteService struct {
|
||||
}
|
||||
|
||||
func buildRedirectHostPattern(domain string) string {
|
||||
labels := strings.Split(domain, ".")
|
||||
patternLabels := make([]string, 0, len(labels))
|
||||
for _, label := range labels {
|
||||
if label == "*" {
|
||||
patternLabels = append(patternLabels, `[^.]+`)
|
||||
continue
|
||||
}
|
||||
patternLabels = append(patternLabels, regexp.QuoteMeta(label))
|
||||
}
|
||||
return fmt.Sprintf("^%s$", strings.Join(patternLabels, `\.`))
|
||||
}
|
||||
|
||||
func parseRedirectHostPattern(pattern string) string {
|
||||
pattern = strings.Trim(pattern, "'")
|
||||
pattern = strings.TrimPrefix(pattern, "^")
|
||||
pattern = strings.TrimSuffix(pattern, "$")
|
||||
pattern = strings.ReplaceAll(pattern, `[^.]+`, "*")
|
||||
pattern = strings.ReplaceAll(pattern, `\.`, ".")
|
||||
return pattern
|
||||
}
|
||||
|
||||
type IWebsiteService interface {
|
||||
PageWebsite(req request.WebsiteSearch) (int64, []response.WebsiteRes, error)
|
||||
GetWebsites() ([]response.WebsiteDTO, error)
|
||||
@@ -379,6 +402,17 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
req request.AppInstallCreate
|
||||
install *model.AppInstall
|
||||
)
|
||||
appDetail, err := appDetailRepo.GetFirst(repo.WithByID(create.AppInstall.AppDetailId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
app, err := appRepo.GetFirst(repo.WithByID(appDetail.AppId))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if isAgentAppKey(app.Key) {
|
||||
return fmt.Errorf("%s does not support website deployment", app.Key)
|
||||
}
|
||||
req.Name = create.AppInstall.Name
|
||||
req.AppDetailId = create.AppInstall.AppDetailId
|
||||
req.Params = create.AppInstall.Params
|
||||
@@ -538,7 +572,13 @@ func (w WebsiteService) CreateWebsite(create request.WebsiteCreate) (err error)
|
||||
createTask.AddSubTaskWithIgnoreErr(i18n.GetWithName("ConfigFTP", create.FtpUser), createFtpUser)
|
||||
}
|
||||
|
||||
return createTask.Execute()
|
||||
if err := createTask.Execute(); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := bindDeploymentWebsiteToAgentByAppInstall(website); err != nil {
|
||||
global.LOG.Errorf("bind deployment website to agent failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (w WebsiteService) OpWebsite(req request.WebsiteOp) error {
|
||||
@@ -733,6 +773,9 @@ func (w WebsiteService) DeleteWebsite(req request.WebsiteDelete) error {
|
||||
if err := websiteRepo.DeleteBy(ctx, repo.WithByID(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := agentRepo.ClearWebsiteIDByWebsiteIDWithCtx(ctx, req.ID); err != nil {
|
||||
return err
|
||||
}
|
||||
if err := websiteDomainRepo.DeleteBy(ctx, websiteDomainRepo.WithWebsiteId(req.ID)); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1708,7 +1751,7 @@ func (w WebsiteService) OperateRedirect(req request.NginxRedirectReq) (err error
|
||||
for _, domain := range req.Domains {
|
||||
block.Directives = append(block.Directives, &components.Directive{
|
||||
Name: "if",
|
||||
Parameters: []string{"($host", "~", fmt.Sprintf("'^%s')", domain)},
|
||||
Parameters: []string{"($host", "~", fmt.Sprintf("'%s')", buildRedirectHostPattern(domain))},
|
||||
Block: returnBlock,
|
||||
})
|
||||
}
|
||||
@@ -1814,7 +1857,7 @@ func (w WebsiteService) GetRedirect(id uint) (res []response.NginxRedirectConfig
|
||||
for _, ifDir := range dirs {
|
||||
params := ifDir.GetParameters()
|
||||
if len(params) > 2 && params[0] == "($host" {
|
||||
domain := strings.Trim(strings.Trim(params[2], "'"), "^")
|
||||
domain := parseRedirectHostPattern(params[2])
|
||||
redirectConfig.Domains = append(redirectConfig.Domains, domain)
|
||||
if len(redirectConfig.Domains) > 1 {
|
||||
continue
|
||||
|
||||
@@ -188,7 +188,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
existDomains = append(existDomains, strings.Split(websiteSSL.Domains, ",")...)
|
||||
}
|
||||
for _, domain := range existDomains {
|
||||
if ipAddress := net.ParseIP(domain); ipAddress == nil {
|
||||
if ipAddress := common.ParseIPLoose(domain); ipAddress == nil {
|
||||
domains = append(domains, domain)
|
||||
} else {
|
||||
ips = append(ips, ipAddress)
|
||||
@@ -220,7 +220,7 @@ func (w WebsiteCAService) ObtainSSL(req request.WebsiteCAObtain) (*model.Website
|
||||
if req.Domains != "" {
|
||||
domainArray := strings.Split(req.Domains, "\n")
|
||||
for _, domain := range domainArray {
|
||||
if ipAddress := net.ParseIP(domain); ipAddress == nil {
|
||||
if ipAddress := common.ParseIPLoose(domain); ipAddress == nil {
|
||||
if domain != "localhost" && !common.IsValidDomain(domain) {
|
||||
err = buserr.WithName("ErrDomainFormat", domain)
|
||||
return nil, err
|
||||
@@ -443,7 +443,7 @@ func (w WebsiteCAService) DownloadFile(id uint) (*os.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
fileName := ca.Name + ".zip"
|
||||
if err = fileOp.Compress([]string{path.Join(dir, "ca.crt"), path.Join(dir, "ca.key")}, dir, fileName, files.SdkZip, ""); err != nil {
|
||||
if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "ca.crt"), path.Join(dir, "ca.key")}, dir, fileName, files.SdkZip, "", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(path.Join(dir, fileName))
|
||||
|
||||
@@ -5,8 +5,7 @@ import (
|
||||
"crypto/x509"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
@@ -14,6 +13,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/go-acme/lego/v4/certificate"
|
||||
legoLogger "github.com/go-acme/lego/v4/log"
|
||||
"github.com/jinzhu/gorm"
|
||||
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
|
||||
"github.com/1Panel-dev/1Panel/agent/app/model"
|
||||
@@ -27,8 +30,7 @@ import (
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/files"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/req_helper"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/ssl"
|
||||
legoLogger "github.com/go-acme/lego/v4/log"
|
||||
"github.com/jinzhu/gorm"
|
||||
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
|
||||
)
|
||||
|
||||
type WebsiteSSLService struct {
|
||||
@@ -45,6 +47,7 @@ type IWebsiteSSLService interface {
|
||||
Update(update request.WebsiteSSLUpdate) error
|
||||
Upload(req request.WebsiteSSLUpload) error
|
||||
ObtainSSL(apply request.WebsiteSSLApply) error
|
||||
AutoRenewSSL(id uint) error
|
||||
SyncForRestart() error
|
||||
DownloadFile(id uint) (*os.File, error)
|
||||
ImportMasterSSL(create model.WebsiteSSL) error
|
||||
@@ -210,13 +213,35 @@ func (w WebsiteSSLService) Create(create request.WebsiteSSLCreate) (request.Webs
|
||||
return create, nil
|
||||
}
|
||||
|
||||
func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{}, disableLog bool) {
|
||||
if disableLog {
|
||||
func printSSLLog(logger *log.Logger, msgKey string, params map[string]interface{}) {
|
||||
if logger == nil {
|
||||
return
|
||||
}
|
||||
logger.Println(i18n.GetMsgWithMap(msgKey, params))
|
||||
}
|
||||
|
||||
func newWebsiteSSLLogger(websiteSSL *model.WebsiteSSL, autoRenew bool) (*os.File, *log.Logger) {
|
||||
flags := os.O_CREATE | os.O_WRONLY | os.O_TRUNC
|
||||
if autoRenew {
|
||||
flags = os.O_CREATE | os.O_WRONLY | os.O_APPEND
|
||||
}
|
||||
|
||||
logFile, err := os.OpenFile(websiteSSL.GetLogPath(), flags, constant.FilePerm)
|
||||
if err != nil {
|
||||
global.LOG.Errorf("open ssl log file failed, domain: %s, err: %v", websiteSSL.PrimaryDomain, err)
|
||||
return nil, log.New(io.Discard, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
if autoRenew {
|
||||
if info, statErr := logFile.Stat(); statErr == nil && info.Size() > 0 {
|
||||
_, _ = logFile.WriteString("\n")
|
||||
}
|
||||
_, _ = logFile.WriteString(fmt.Sprintf("========== [%s] auto renew attempt ==========\n", time.Now().Format(constant.DateTimeLayout)))
|
||||
}
|
||||
|
||||
return logFile, log.New(logFile, "", log.LstdFlags)
|
||||
}
|
||||
|
||||
func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
if !global.IsMaster {
|
||||
return
|
||||
@@ -226,7 +251,7 @@ func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
fileOp := files.NewFileOp()
|
||||
certPath := path.Join(global.Dir.DataDir, "secret/server.crt")
|
||||
keyPath := path.Join(global.Dir.DataDir, "secret/server.key")
|
||||
printSSLLog(logger, "StartUpdateSystemSSL", nil, logger == nil)
|
||||
printSSLLog(logger, "StartUpdateSystemSSL", nil)
|
||||
if err := fileOp.WriteFile(certPath, strings.NewReader(websiteSSL.Pem), 0600); err != nil {
|
||||
logger.Printf("Failed to update the SSL certificate File for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
|
||||
return
|
||||
@@ -239,11 +264,19 @@ func reloadSystemSSL(websiteSSL *model.WebsiteSSL, logger *log.Logger) {
|
||||
logger.Printf("Failed to update the SSL certificate for 1Panel System domain [%s] , err:%s", websiteSSL.PrimaryDomain, err.Error())
|
||||
return
|
||||
}
|
||||
printSSLLog(logger, "UpdateSystemSSLSuccess", nil, logger == nil)
|
||||
printSSLLog(logger, "UpdateSystemSSLSuccess", nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
return w.obtainSSL(apply.ID, false)
|
||||
}
|
||||
|
||||
func (w WebsiteSSLService) AutoRenewSSL(id uint) error {
|
||||
return w.obtainSSL(id, true)
|
||||
}
|
||||
|
||||
func (w WebsiteSSLService) obtainSSL(id uint, autoRenew bool) error {
|
||||
var (
|
||||
err error
|
||||
websiteSSL *model.WebsiteSSL
|
||||
@@ -254,7 +287,7 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
resource certificate.Resource
|
||||
)
|
||||
|
||||
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(apply.ID))
|
||||
websiteSSL, err = websiteSSLRepo.GetFirst(repo.WithByID(id))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -306,17 +339,16 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
}
|
||||
|
||||
go func() {
|
||||
logFile, _ := os.OpenFile(path.Join(global.Dir.SSLLogDir, fmt.Sprintf("%s-ssl-%d.log", websiteSSL.PrimaryDomain, websiteSSL.ID)), os.O_CREATE|os.O_WRONLY|os.O_TRUNC, constant.FilePerm)
|
||||
defer logFile.Close()
|
||||
logger := log.New(logFile, "", log.LstdFlags)
|
||||
legoLogger.Logger = logger
|
||||
if !apply.DisableLog {
|
||||
startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)})
|
||||
if websiteSSL.Provider == constant.DNSAccount {
|
||||
startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type})
|
||||
}
|
||||
logger.Println(startMsg)
|
||||
logFile, logger := newWebsiteSSLLogger(websiteSSL, autoRenew)
|
||||
if logFile != nil {
|
||||
defer logFile.Close()
|
||||
}
|
||||
legoLogger.Logger = logger
|
||||
startMsg := i18n.GetMsgWithMap("ApplySSLStart", map[string]interface{}{"domain": strings.Join(domains, ","), "type": i18n.GetMsgByKey(websiteSSL.Provider)})
|
||||
if websiteSSL.Provider == constant.DNSAccount {
|
||||
startMsg = startMsg + i18n.GetMsgWithMap("DNSAccountName", map[string]interface{}{"name": dnsAccount.Name, "type": dnsAccount.Type})
|
||||
}
|
||||
logger.Println(startMsg)
|
||||
if websiteSSL.Provider != constant.DnsManual {
|
||||
privateKey, err := ssl.GetPrivateKeyByType(websiteSSL.KeyType, websiteSSL.PrivateKey)
|
||||
if err != nil {
|
||||
@@ -361,7 +393,7 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
websiteSSL.Organization = cert.Issuer.Organization[0]
|
||||
}
|
||||
websiteSSL.Status = constant.SSLReady
|
||||
printSSLLog(logger, "ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")}, apply.DisableLog)
|
||||
printSSLLog(logger, "ApplySSLSuccess", map[string]interface{}{"domain": strings.Join(domains, ",")})
|
||||
saveCertificateFile(websiteSSL, logger)
|
||||
|
||||
if websiteSSL.ExecShell {
|
||||
@@ -369,12 +401,12 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
if websiteSSL.PushDir {
|
||||
workDir = websiteSSL.Dir
|
||||
}
|
||||
printSSLLog(logger, "ExecShellStart", nil, apply.DisableLog)
|
||||
printSSLLog(logger, "ExecShellStart", nil)
|
||||
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(30*time.Minute), cmd.WithLogger(logger), cmd.WithWorkDir(workDir))
|
||||
if err = cmdMgr.RunBashC(websiteSSL.Shell); err != nil {
|
||||
printSSLLog(logger, "ErrExecShell", map[string]interface{}{"err": err.Error()}, apply.DisableLog)
|
||||
printSSLLog(logger, "ErrExecShell", map[string]interface{}{"err": err.Error()})
|
||||
} else {
|
||||
printSSLLog(logger, "ExecShellSuccess", nil, apply.DisableLog)
|
||||
printSSLLog(logger, "ExecShellSuccess", nil)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,9 +418,9 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
websites, _ := websiteRepo.GetBy(websiteRepo.WithWebsiteSSLID(websiteSSL.ID))
|
||||
if len(websites) > 0 {
|
||||
for _, website := range websites {
|
||||
printSSLLog(logger, "ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain}, apply.DisableLog)
|
||||
printSSLLog(logger, "ApplyWebSiteSSLLog", map[string]interface{}{"name": website.PrimaryDomain})
|
||||
if err := createPemFile(website, *websiteSSL); err != nil {
|
||||
printSSLLog(logger, "ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()}, apply.DisableLog)
|
||||
printSSLLog(logger, "ErrUpdateWebsiteSSL", map[string]interface{}{"name": website.PrimaryDomain, "err": err.Error()})
|
||||
}
|
||||
}
|
||||
nginxInstall, err := getAppInstallByKey(constant.AppOpenresty)
|
||||
@@ -396,19 +428,19 @@ func (w WebsiteSSLService) ObtainSSL(apply request.WebsiteSSLApply) error {
|
||||
return
|
||||
}
|
||||
if err := opNginx(nginxInstall.ContainerName, constant.NginxReload); err != nil {
|
||||
printSSLLog(logger, "ErrSSLApply", nil, apply.DisableLog)
|
||||
printSSLLog(logger, "ErrSSLApply", nil)
|
||||
return
|
||||
}
|
||||
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil, apply.DisableLog)
|
||||
printSSLLog(logger, "ApplyWebSiteSSLSuccess", nil)
|
||||
}
|
||||
reloadSystemSSL(websiteSSL, logger)
|
||||
if websiteSSL.PushNode {
|
||||
printSSLLog(logger, "StartPushSSLToNode", nil, apply.DisableLog)
|
||||
printSSLLog(logger, "StartPushSSLToNode", nil)
|
||||
if err = xpack.PushSSLToNode(websiteSSL); err != nil {
|
||||
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()}, apply.DisableLog)
|
||||
printSSLLog(logger, "PushSSLToNodeFailed", map[string]interface{}{"err": err.Error()})
|
||||
return
|
||||
}
|
||||
printSSLLog(logger, "PushSSLToNodeSuccess", nil, apply.DisableLog)
|
||||
printSSLLog(logger, "PushSSLToNodeSuccess", nil)
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -726,7 +758,7 @@ func (w WebsiteSSLService) DownloadFile(id uint) (*os.File, error) {
|
||||
return nil, err
|
||||
}
|
||||
fileName := websiteSSL.PrimaryDomain + ".zip"
|
||||
if err = fileOp.Compress([]string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, ""); err != nil {
|
||||
if err = fileOp.Compress(context.Background(), []string{path.Join(dir, "fullchain.pem"), path.Join(dir, "privkey.pem")}, dir, fileName, files.SdkZip, "", nil); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return os.Open(path.Join(dir, fileName))
|
||||
|
||||
@@ -127,6 +127,19 @@ func CheckResourceTaskIsExecuting(operate, scope string, resourceID uint) bool {
|
||||
return task.ID != ""
|
||||
}
|
||||
|
||||
func CheckScopeTaskIsExecuting(scope string, resourceID uint) error {
|
||||
taskRepo := repo.NewITaskRepo()
|
||||
task, _ := taskRepo.GetFirst(
|
||||
taskRepo.WithByStatus(constant.StatusExecuting),
|
||||
taskRepo.WithResourceID(resourceID),
|
||||
repo.WithByType(scope),
|
||||
)
|
||||
if task.ID != "" {
|
||||
return buserr.New("TaskIsExecuting")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewTask(name, operate, taskScope, taskID string, resourceID uint) (*Task, error) {
|
||||
if taskID == "" {
|
||||
taskID = uuid.New().String()
|
||||
|
||||
@@ -3,6 +3,7 @@ base:
|
||||
mode: dev
|
||||
is_demo: false
|
||||
is_offline: false
|
||||
is_fxplay: false
|
||||
|
||||
log:
|
||||
level: debug
|
||||
|
||||
@@ -25,4 +25,5 @@ const (
|
||||
DingTalk = "dingTalk"
|
||||
FeiShu = "feiShu"
|
||||
Custom = "custom"
|
||||
Bark = "bark"
|
||||
)
|
||||
|
||||
@@ -9,6 +9,7 @@ const (
|
||||
AppOpenresty = "openresty"
|
||||
AppOpenclaw = "openclaw"
|
||||
AppCopaw = "copaw"
|
||||
AppHermesAgent = "hermes-agent"
|
||||
AppMysql = "mysql"
|
||||
AppMariaDB = "mariadb"
|
||||
AppPostgresql = "postgresql"
|
||||
|
||||
+1
-1
@@ -41,7 +41,7 @@ func Run() {
|
||||
if _, err := global.Cron.AddJob("@daily", job.NewWebsiteJob()); err != nil {
|
||||
global.LOG.Errorf("can not add website corn job: %s", err.Error())
|
||||
}
|
||||
if _, err := global.Cron.AddJob("@daily", job.NewSSLJob()); err != nil {
|
||||
if _, err := global.Cron.AddJob("0 */6 * * *", job.NewSSLJob()); err != nil {
|
||||
global.LOG.Errorf("can not add ssl corn job: %s", err.Error())
|
||||
}
|
||||
minuteRand, err := rand.Int(rand.Reader, big.NewInt(60))
|
||||
|
||||
@@ -46,10 +46,7 @@ func (ssl *ssl) Run() {
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
if err := sslService.ObtainSSL(request.WebsiteSSLApply{
|
||||
ID: s.ID,
|
||||
DisableLog: true,
|
||||
}); err != nil {
|
||||
if err := sslService.AutoRenewSSL(s.ID); err != nil {
|
||||
global.LOG.Errorf("Failed to update the SSL certificate for the [%s] domain , err:%s", s.PrimaryDomain, err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type ServerConfig struct {
|
||||
|
||||
type Base struct {
|
||||
Port string `mapstructure:"port"`
|
||||
IsFxplay bool `mapstructure:"is_fxplay"`
|
||||
Edition string `mapstructure:"edition"` // [ cn / intl]
|
||||
Version string `mapstructure:"version"`
|
||||
EncryptKey string `mapstructure:"encrypt_key"`
|
||||
|
||||
@@ -42,18 +42,27 @@ var (
|
||||
)
|
||||
|
||||
func RepoURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://resource.fit2cloud.com/1panel/package/fusionxplay"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://resource.fit2cloud.com/1panel/package/v2"
|
||||
}
|
||||
return "https://resource.1panel.pro/v2"
|
||||
}
|
||||
func ResourceURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://resource.fit2cloud.com/1panel/resource/fusionxplay"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://resource.fit2cloud.com/1panel/resource/v2"
|
||||
}
|
||||
return "https://resource.1panel.pro/v2/resource"
|
||||
}
|
||||
func AppRepoURL() string {
|
||||
if CONF.Base.IsFxplay {
|
||||
return "https://apps-assets.fit2cloud.com"
|
||||
}
|
||||
if CONF.Base.Edition != "intl" {
|
||||
return "https://apps-assets.fit2cloud.com"
|
||||
}
|
||||
|
||||
+149
-145
@@ -6,184 +6,185 @@ replace github.com/moby/go-archive => github.com/moby/go-archive v0.1.0
|
||||
|
||||
require (
|
||||
github.com/aliyun/aliyun-oss-go-sdk v3.0.2+incompatible
|
||||
github.com/aws/aws-sdk-go v1.55.0
|
||||
github.com/compose-spec/compose-go/v2 v2.9.1
|
||||
github.com/compose-spec/compose-go/v2 v2.10.2
|
||||
github.com/creack/pty v1.1.24
|
||||
github.com/docker/cli v29.2.0+incompatible
|
||||
github.com/docker/cli v29.4.0+incompatible
|
||||
github.com/docker/compose/v2 v2.40.3
|
||||
github.com/docker/docker v28.5.2+incompatible
|
||||
github.com/docker/go-connections v0.6.0
|
||||
github.com/docker/go-connections v0.7.0
|
||||
github.com/fsnotify/fsnotify v1.9.0
|
||||
github.com/gin-gonic/gin v1.10.0
|
||||
github.com/gin-gonic/gin v1.12.0
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-acme/lego/v4 v4.30.1
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.2
|
||||
github.com/go-playground/validator/v10 v10.26.0
|
||||
github.com/go-acme/lego/v4 v4.34.0
|
||||
github.com/go-gormigrate/gormigrate/v2 v2.1.5
|
||||
github.com/go-playground/validator/v10 v10.30.2
|
||||
github.com/go-redis/redis v6.15.9+incompatible
|
||||
github.com/go-resty/resty/v2 v2.17.0
|
||||
github.com/go-resty/resty/v2 v2.17.2
|
||||
github.com/go-sql-driver/mysql v1.9.3
|
||||
github.com/goh-chunlin/go-onedrive v1.1.1
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674
|
||||
github.com/jackc/pgx/v5 v5.7.5
|
||||
github.com/jackc/pgx/v5 v5.9.1
|
||||
github.com/jinzhu/copier v0.4.0
|
||||
github.com/jinzhu/gorm v1.9.16
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/klauspost/compress v1.18.2
|
||||
github.com/klauspost/compress v1.18.5
|
||||
github.com/mholt/archiver/v4 v4.0.0-alpha.8
|
||||
github.com/miekg/dns v1.1.69
|
||||
github.com/minio/minio-go/v7 v7.0.74
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/miekg/dns v1.1.72
|
||||
github.com/minio/minio-go/v7 v7.0.100
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.1
|
||||
github.com/opencontainers/image-spec v1.1.1
|
||||
github.com/oschwald/maxminddb-golang v1.13.1
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pkg/errors v0.9.1
|
||||
github.com/pkg/sftp v1.13.6
|
||||
github.com/pkg/sftp v1.13.10
|
||||
github.com/qiniu/go-sdk/v7 v7.26.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.11
|
||||
github.com/shirou/gopsutil/v4 v4.26.3
|
||||
github.com/sirupsen/logrus v1.9.4
|
||||
github.com/spf13/afero v1.11.0
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/spf13/afero v1.15.0
|
||||
github.com/spf13/cobra v1.10.2
|
||||
github.com/spf13/viper v1.19.0
|
||||
github.com/spf13/viper v1.21.0
|
||||
github.com/subosito/gotenv v1.6.0
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.54
|
||||
github.com/tencentyun/cos-go-sdk-v5 v0.7.73
|
||||
github.com/tomasen/fcgi_client v0.0.0-20180423082037-2bb3d819fd19
|
||||
github.com/upyun/go-sdk v2.1.0+incompatible
|
||||
golang.org/x/crypto v0.46.0
|
||||
golang.org/x/net v0.48.0
|
||||
golang.org/x/oauth2 v0.34.0
|
||||
golang.org/x/sys v0.40.0
|
||||
golang.org/x/text v0.32.0
|
||||
google.golang.org/genproto v0.0.0-20241021214115-324edc3d5d38
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
go.mongodb.org/mongo-driver v1.17.9
|
||||
golang.org/x/crypto v0.50.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/oauth2 v0.36.0
|
||||
golang.org/x/sys v0.43.0
|
||||
golang.org/x/text v0.36.0
|
||||
golang.org/x/time v0.15.0
|
||||
google.golang.org/genproto v0.0.0-20260414002931-afd174a4e478
|
||||
gopkg.in/ini.v1 v1.67.1
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
gorm.io/gorm v1.30.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.1.1 // indirect
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/BurntSushi/toml v1.6.0 // indirect
|
||||
github.com/Microsoft/go-winio v0.6.2 // indirect
|
||||
github.com/STARRY-S/zip v0.2.3 // indirect
|
||||
github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect
|
||||
github.com/alibabacloud-go/alibabacloud-gateway-spi v0.0.5 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.13 // indirect
|
||||
github.com/alibabacloud-go/darabonba-openapi/v2 v2.1.16 // indirect
|
||||
github.com/alibabacloud-go/debug v1.0.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.3.14 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.7 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.7 // indirect
|
||||
github.com/andybalholm/brotli v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.16 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.12 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 // indirect
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.254 // indirect
|
||||
github.com/bodgit/plumbing v1.2.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.3.0 // indirect
|
||||
github.com/bodgit/windows v1.0.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/alibabacloud-go/tea v1.4.0 // indirect
|
||||
github.com/alibabacloud-go/tea-utils/v2 v2.0.9 // indirect
|
||||
github.com/aliyun/credentials-go v1.4.12 // indirect
|
||||
github.com/andybalholm/brotli v1.2.1 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/credentials v1.19.14 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.6 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.7 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.21 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/route53 v1.62.5 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/signin v1.0.9 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sso v1.30.15 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.19 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.10 // indirect
|
||||
github.com/aws/smithy-go v1.25.0 // indirect
|
||||
github.com/baidubce/bce-sdk-go v0.9.264 // indirect
|
||||
github.com/bodgit/plumbing v1.3.0 // indirect
|
||||
github.com/bodgit/sevenzip v1.6.1 // indirect
|
||||
github.com/bodgit/windows v1.0.1 // indirect
|
||||
github.com/bytedance/gopkg v0.1.4 // indirect
|
||||
github.com/bytedance/sonic v1.15.0 // indirect
|
||||
github.com/bytedance/sonic/loader v0.5.1 // indirect
|
||||
github.com/cenkalti/backoff/v4 v4.3.0 // indirect
|
||||
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/clbanning/mxj v1.8.4 // indirect
|
||||
github.com/clbanning/mxj/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/connesc/cipherio v0.2.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.7.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/containerd/console v1.0.5 // indirect
|
||||
github.com/containerd/containerd/api v1.10.0 // indirect
|
||||
github.com/containerd/containerd/v2 v2.2.1 // indirect
|
||||
github.com/containerd/containerd/v2 v2.2.3 // indirect
|
||||
github.com/containerd/continuity v0.4.5 // indirect
|
||||
github.com/containerd/errdefs v1.0.0 // indirect
|
||||
github.com/containerd/errdefs/pkg v0.3.0 // indirect
|
||||
github.com/containerd/log v0.1.0 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.2 // indirect
|
||||
github.com/containerd/ttrpc v1.2.7 // indirect
|
||||
github.com/containerd/platforms v1.0.0-rc.4 // indirect
|
||||
github.com/containerd/ttrpc v1.2.8 // indirect
|
||||
github.com/containerd/typeurl/v2 v2.2.3 // indirect
|
||||
github.com/denisenkom/go-mssqldb v0.0.0-20191128021309-1d7a30a10f73 // indirect
|
||||
github.com/distribution/reference v0.6.0 // indirect
|
||||
github.com/docker/buildx v0.31.1 // indirect
|
||||
github.com/docker/buildx v0.33.0 // indirect
|
||||
github.com/docker/docker-credential-helpers v0.9.5 // indirect
|
||||
github.com/docker/go-units v0.5.0 // indirect
|
||||
github.com/dsnet/compress v0.0.1 // indirect
|
||||
github.com/dsnet/compress v0.0.2-0.20230904184137-39efe44ab707 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/fatih/color v1.18.0 // indirect
|
||||
github.com/ebitengine/purego v0.10.0 // indirect
|
||||
github.com/felixge/httpsnoop v1.0.4 // indirect
|
||||
github.com/fvbommel/sortorder v1.1.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
|
||||
github.com/gammazero/toposort v0.1.1 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-contrib/sse v1.1.1 // indirect
|
||||
github.com/glebarez/go-sqlite v1.22.0 // indirect
|
||||
github.com/go-acme/alidns-20150109/v4 v4.7.0 // indirect
|
||||
github.com/go-acme/esa-20240910/v2 v2.40.3 // indirect
|
||||
github.com/go-acme/tencentclouddnspod v1.1.25 // indirect
|
||||
github.com/go-acme/esa-20240910/v2 v2.48.0 // indirect
|
||||
github.com/go-acme/tencentclouddnspod v1.3.24 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.1.4 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||
github.com/goccy/go-json v0.10.5 // indirect
|
||||
github.com/goccy/go-yaml v1.9.8 // indirect
|
||||
github.com/go-viper/mapstructure/v2 v2.5.0 // indirect
|
||||
github.com/goccy/go-json v0.10.6 // indirect
|
||||
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||
github.com/gofrs/flock v0.13.0 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/go-querystring v1.1.0 // indirect
|
||||
github.com/google/go-querystring v1.2.0 // indirect
|
||||
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
|
||||
github.com/h2non/filetype v1.1.1 // indirect
|
||||
github.com/hashicorp/errwrap v1.1.0 // indirect
|
||||
github.com/hashicorp/go-multierror v1.1.1 // indirect
|
||||
github.com/hashicorp/go-version v1.8.0 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.180 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.9.0 // indirect
|
||||
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
|
||||
github.com/h2non/filetype v1.1.3 // indirect
|
||||
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
|
||||
github.com/hashicorp/go-version v1.9.0 // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/huaweicloud/huaweicloud-sdk-go-v3 v0.1.192 // indirect
|
||||
github.com/in-toto/attestation v1.2.0 // indirect
|
||||
github.com/in-toto/in-toto-golang v0.10.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||
github.com/json-iterator/go v1.1.13-0.20220915233716-71ac16282d12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.8 // indirect
|
||||
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/kr/fs v0.1.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.9 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.12 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
|
||||
github.com/mattn/go-isatty v0.0.21 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.23 // indirect
|
||||
github.com/mattn/go-shellwords v1.0.13 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/moby/buildkit v0.27.0 // indirect
|
||||
github.com/moby/buildkit v0.29.0 // indirect
|
||||
github.com/moby/docker-image-spec v1.3.1 // indirect
|
||||
github.com/moby/go-archive v0.2.0 // indirect
|
||||
github.com/moby/locker v1.0.1 // indirect
|
||||
github.com/moby/moby/api v1.52.0 // indirect
|
||||
github.com/moby/moby/client v0.2.1 // indirect
|
||||
github.com/moby/patternmatcher v0.6.0 // indirect
|
||||
github.com/moby/moby/api v1.54.1 // indirect
|
||||
github.com/moby/moby/client v0.4.0 // indirect
|
||||
github.com/moby/patternmatcher v0.6.1 // indirect
|
||||
github.com/moby/sys/atomicwriter v0.1.0 // indirect
|
||||
github.com/moby/sys/sequential v0.6.0 // indirect
|
||||
github.com/moby/sys/signal v0.7.1 // indirect
|
||||
@@ -192,38 +193,40 @@ require (
|
||||
github.com/moby/term v0.5.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect
|
||||
github.com/montanaflynn/stats v0.9.0 // indirect
|
||||
github.com/morikuni/aec v1.1.0 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.2.1 // indirect
|
||||
github.com/mozillazg/go-httpheader v0.4.0 // indirect
|
||||
github.com/namedotcom/go/v4 v4.0.2 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nrdcg/dnspod-go v0.4.0 // indirect
|
||||
github.com/nrdcg/freemyip v0.3.0 // indirect
|
||||
github.com/nrdcg/goacmedns v0.2.0 // indirect
|
||||
github.com/nrdcg/namesilo v0.5.0 // indirect
|
||||
github.com/nrdcg/porkbun v0.4.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.0 // indirect
|
||||
github.com/nwaples/rardecode/v2 v2.2.2 // indirect
|
||||
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||
github.com/ovh/go-ovh v1.9.0 // indirect
|
||||
github.com/pelletier/go-toml v1.9.5 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.15 // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.3.0 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.26 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rs/xid v1.5.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.9.1 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.12.0 // indirect
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2 // indirect
|
||||
github.com/secure-systems-lab/go-securesystemslib v0.10.0 // indirect
|
||||
github.com/shibumi/go-pathspec v1.3.0 // indirect
|
||||
github.com/sigstore/sigstore v1.10.0 // indirect
|
||||
github.com/sigstore/sigstore-go v1.1.4-0.20251124094504-b5fe07a5a7d7 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/cast v1.7.0 // indirect
|
||||
github.com/sigstore/sigstore v1.10.5 // indirect
|
||||
github.com/sigstore/sigstore-go v1.1.4 // indirect
|
||||
github.com/sorairolake/lzip-go v0.3.8 // indirect
|
||||
github.com/spf13/cast v1.10.0 // indirect
|
||||
github.com/spf13/pflag v1.0.10 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.12 // indirect
|
||||
github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.80 // indirect
|
||||
github.com/therootcompany/xz v1.0.1 // indirect
|
||||
github.com/tinylib/msgp v1.6.4 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
@@ -233,43 +236,44 @@ require (
|
||||
github.com/tonistiigi/units v0.0.0-20180711220420-6950e57a87ea // indirect
|
||||
github.com/tonistiigi/vt100 v0.0.0-20240514184818-90bafcd6abab // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.230 // indirect
|
||||
github.com/volcengine/volc-sdk-golang v1.0.241 // indirect
|
||||
github.com/xdg-go/pbkdf2 v1.0.0 // indirect
|
||||
github.com/xdg-go/scram v1.2.0 // indirect
|
||||
github.com/xdg-go/stringprep v1.0.4 // indirect
|
||||
github.com/xhit/go-str2duration/v2 v2.1.0 // indirect
|
||||
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.mongodb.org/mongo-driver v1.17.6 // indirect
|
||||
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.63.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0 // indirect
|
||||
go.opentelemetry.io/otel v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.38.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.40.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.7.1 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/httptrace/otelhttptrace v0.68.0 // indirect
|
||||
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.68.0 // indirect
|
||||
go.opentelemetry.io/otel v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetricgrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/sdk/metric v1.43.0 // indirect
|
||||
go.opentelemetry.io/otel/trace v1.43.0 // indirect
|
||||
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
|
||||
golang.org/x/mod v0.31.0 // indirect
|
||||
golang.org/x/sync v0.19.0 // indirect
|
||||
golang.org/x/term v0.38.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.0 // indirect
|
||||
golang.org/x/xerrors v0.0.0-20231012003039-104605ab7028 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251103181224-f26f9409b101 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251124214823-79d6a2a48846 // indirect
|
||||
google.golang.org/grpc v1.77.0 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.4 // indirect
|
||||
go4.org v0.0.0-20260112195520-a5071408f32f // indirect
|
||||
golang.org/x/arch v0.26.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sync v0.20.0 // indirect
|
||||
golang.org/x/term v0.42.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect
|
||||
google.golang.org/grpc v1.80.0 // indirect
|
||||
google.golang.org/protobuf v1.36.11 // indirect
|
||||
modernc.org/fileutil v1.3.3 // indirect
|
||||
modernc.org/libc v1.66.1 // indirect
|
||||
modernc.org/fileutil v1.4.0 // indirect
|
||||
modernc.org/libc v1.72.0 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
modernc.org/sqlite v1.38.0 // indirect
|
||||
modernc.org/sqlite v1.48.2 // indirect
|
||||
)
|
||||
|
||||
+437
-423
File diff suppressed because it is too large
Load Diff
+21
-1
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Bad request: {{ .detail }}'
|
||||
ErrTokenParse: 'Token error: {{ .detail }}'
|
||||
ErrInitialPassword: 'Original password is incorrect'
|
||||
ErrInternalServer: 'Internal server error: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'File-management AI summary requires an enabled AI account in File Management. Content-only search still works when AI is off.'
|
||||
ErrFileAISearchBadPattern: 'Invalid file search pattern (regex or whole word): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'Remote download failed or URL is unreachable: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'Remote URL did not return a file (possibly an error page), {{ .detail }}'
|
||||
ErrRecordExist: 'Record already exists'
|
||||
@@ -42,6 +44,7 @@ Decrypt: "Decrypt"
|
||||
|
||||
#agent
|
||||
ErrAgentAccountBound: 'Account is bound to an agent and cannot be deleted'
|
||||
ErrTerminalAIAccountInUse: 'This model account is currently used by Terminal AI. Switch the account in Terminal AI settings or disable Terminal AI and try again.'
|
||||
ErrAgentAccountUnavailable: 'Account connection unavailable: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'Unsupported agent provider'
|
||||
ErrAgentAccountRequired: 'Select an agent account first'
|
||||
@@ -49,13 +52,21 @@ ErrAgentAccountNotVerified: 'Agent account not verified'
|
||||
ErrAgentAccountModelsRequired: 'Please configure at least one model'
|
||||
ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation'
|
||||
ErrAgentModelNotInAccount: 'Model is not configured in the selected account'
|
||||
ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated'
|
||||
ErrAgentFallbackModelPrimary: 'Fallback model cannot match the primary model'
|
||||
ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Base URL is required'
|
||||
ErrAgentApiKeyRequired: 'API key is required'
|
||||
ErrAgentComposeRequired: 'Compose content is required'
|
||||
ErrAgentIDRequired: 'Agent ID is required'
|
||||
ErrAgentAccountIDRequired: 'Account ID is required'
|
||||
ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Upgrade to Professional Edition for unlimited agents'
|
||||
ErrAgentLimitReached: 'Community Edition supports up to {{ .max }} AI agents. Upgrade to Business Edition for unlimited agents'
|
||||
ErrAgentWebsiteBound: 'This agent is already bound to a website'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Only proxy or static websites can be bound'
|
||||
ErrAgentWebsiteInUse: 'This website is already bound to another agent'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Deployment websites cannot be unbound manually'
|
||||
ErrHermesPairingCodeUnavailable: 'The pairing code is temporarily unavailable in Hermes, possibly due to network issues. Please try again later.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'When the Feishu group policy is Allowlist, the DM policy cannot be Pairing Code.'
|
||||
|
||||
#backup
|
||||
Localhost: 'Local'
|
||||
@@ -135,6 +146,13 @@ ErrLinkPathNotFound: 'The target path does not exist!'
|
||||
ErrFileIsExist: 'The file or folder already exists!'
|
||||
ErrFileUpload: 'Upload failed: {{ .name }} {{ .detail }}'
|
||||
ErrFileDownloadDir: 'Directory download is not supported'
|
||||
ErrFileSharePath: 'Invalid file path or target is not a file'
|
||||
ErrFileShareInvalid: 'Invalid or expired share link'
|
||||
ErrFileShareExpired: 'Share link has expired'
|
||||
ErrFileSharePassword: 'Incorrect share password'
|
||||
ErrFileShareExhausted: 'Download limit for this share has been reached'
|
||||
ErrFileSharePasswordPolicy: 'Password must be at least 4 characters'
|
||||
ErrFileShareRateLimit: 'Too many requests, please try again later'
|
||||
ErrCmdNotFound: 'Command not found: {{ .name }}'
|
||||
ErrSourcePathNotFound: 'Source directory does not exist'
|
||||
ErrFavoriteExist: 'Path already in favorites'
|
||||
@@ -163,6 +181,7 @@ ErrSSLValid: 'Certificate file is invalid'
|
||||
ErrWebsiteDir: 'Select a subdirectory under the website directory'
|
||||
ErrComposerFileNotFound: "composer.json file does not exist"
|
||||
ErrRuntimeNoPort: 'Runtime port is not set'
|
||||
ErrRuntimeProjectDirContainsCodeDir: 'The runtime directory contains the selected project directory. Please choose another project directory and create it again.'
|
||||
Status: 'Status'
|
||||
start: 'Start'
|
||||
stop: 'Stop'
|
||||
@@ -573,3 +592,4 @@ TerminalAIBlockedRiskyCommand: 'blocked risky command: {{ .command }}'
|
||||
TerminalAIThinking: 'AI is thinking...'
|
||||
TerminalAIReadyToExecute: 'Thinking complete, press Enter to execute (duration: {{ .duration }}, tokens: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'AI request failed: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'No files or directories found under this path (or all entries were filtered).'
|
||||
|
||||
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Solicitud inválida: {{ .detail }}'
|
||||
ErrTokenParse: 'Token inválido: {{ .detail }}'
|
||||
ErrInitialPassword: 'Contraseña original incorrecta'
|
||||
ErrInternalServer: 'Error interno: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'La búsqueda con IA de archivos está desactivada o sin cuenta. En Gestión de archivos, use Cuenta de IA, active y elija una cuenta.'
|
||||
ErrFileAISearchBadPattern: 'Patrón de búsqueda no válido (regex u otra opción): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'La descarga remota falló o la URL no es accesible: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'La URL remota no devolvió un archivo (posiblemente una página de error), {{ .detail }}'
|
||||
ErrRecordExist: 'Registro ya existe'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: 'Grupo predeterminado, no se puede eliminar'
|
||||
ErrGroupIsInWebsiteUse: 'El grupo está siendo usado por otro sitio web y no se puede eliminar.'
|
||||
Decrypt: 'Descifrar'
|
||||
ErrAgentAccountBound: 'Cuenta vinculada a agente'
|
||||
ErrTerminalAIAccountInUse: 'Esta cuenta de modelo esta siendo usada por Terminal AI. Cambia la cuenta en la configuracion de Terminal AI o desactiva Terminal AI y vuelve a intentarlo.'
|
||||
ErrAgentAccountUnavailable: 'Conexión de cuenta no disponible: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'Proveedor de agente no soportado'
|
||||
ErrAgentAccountRequired: 'Elige una cuenta de agente primero'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: 'Cuenta de agente no verificada'
|
||||
ErrAgentAccountModelsRequired: 'Configure al menos un modelo'
|
||||
ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta'
|
||||
ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada'
|
||||
ErrAgentFallbackModelDuplicate: 'Los modelos de respaldo no pueden duplicarse'
|
||||
ErrAgentFallbackModelPrimary: 'El modelo de respaldo no puede ser igual al modelo principal'
|
||||
ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'URL base requerida'
|
||||
ErrAgentApiKeyRequired: 'Clave API requerida'
|
||||
ErrAgentComposeRequired: 'Contenido compose requerido'
|
||||
ErrAgentIDRequired: 'ID de agente requerido'
|
||||
ErrAgentAccountIDRequired: 'ID de cuenta requerido'
|
||||
ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de IA. Actualice a la edición Professional para eliminar el límite'
|
||||
ErrAgentLimitReached: 'La edición Community admite hasta {{ .max }} agentes de IA. Actualiza a la edición Business para usar agentes ilimitados'
|
||||
ErrAgentWebsiteBound: 'Este agente ya está vinculado a un sitio web'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Solo se pueden vincular sitios proxy o estáticos'
|
||||
ErrAgentWebsiteInUse: 'Este sitio web ya está vinculado a otro agente'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Los sitios web de despliegue no se pueden desvincular manualmente'
|
||||
ErrHermesPairingCodeUnavailable: 'El código de emparejamiento no está disponible temporalmente en Hermes, posiblemente por un problema de red. Inténtalo de nuevo más tarde.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Cuando la política de grupo de Feishu es Lista permitida, la política de MD no puede ser Código de emparejamiento.'
|
||||
Localhost: 'Máquina local'
|
||||
ErrBackupInUsed: 'Cuenta de respaldo en uso por tarea programada'
|
||||
ErrBackupCheck: 'Conexión de respaldo falló: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: 'La ruta de destino no existe'
|
||||
ErrFileIsExist: 'El archivo o carpeta ya existe'
|
||||
ErrFileUpload: '{{ .name }} falló al subir archivo {{ .detail }}'
|
||||
ErrFileDownloadDir: 'No se admite la descarga de carpetas'
|
||||
ErrFileSharePath: 'Ruta no válida o el destino no es un archivo'
|
||||
ErrFileShareInvalid: 'Enlace de uso compartido no válido o caducado'
|
||||
ErrFileShareExpired: 'El enlace de uso compartido ha caducado'
|
||||
ErrFileSharePassword: 'Contraseña de uso compartido incorrecta'
|
||||
ErrFileShareExhausted: 'Se alcanzó el límite de descargas de este recurso compartido'
|
||||
ErrFileSharePasswordPolicy: 'La contraseña debe tener al menos 4 caracteres'
|
||||
ErrFileShareRateLimit: 'Demasiadas solicitudes, inténtelo de nuevo más tarde'
|
||||
ErrCmdNotFound: 'El comando {{ .name }} no existe, instálelo primero en el host'
|
||||
ErrSourcePathNotFound: 'El directorio fuente no existe'
|
||||
ErrFavoriteExist: 'Esta ruta ya ha sido marcada como favorita'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: 'Archivo de certificado anómalo, revise el estado del certificado'
|
||||
ErrWebsiteDir: 'seleccione un directorio dentro del directorio del sitio web'
|
||||
ErrComposerFileNotFound: 'El archivo composer.json no existe'
|
||||
ErrRuntimeNoPort: 'El entorno de ejecución no tiene configurado un puerto, edítelo primero'
|
||||
ErrRuntimeProjectDirContainsCodeDir: 'El directorio de ejecución incluye el directorio de proyecto seleccionado. Elija otro directorio de proyecto y créelo de nuevo.'
|
||||
Status: 'Estado'
|
||||
start: 'Iniciar'
|
||||
stop: 'Detener'
|
||||
@@ -169,7 +188,7 @@ ErrSSLCertificateNotFound: 'El archivo de certificado no existe'
|
||||
ErrSSLKeyFormat: 'Error en la verificación del archivo de clave privada'
|
||||
ErrSSLCertificateFormat: 'Formato de certificado incorrecto, use formato pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid o EabHmacKey no pueden estar en blanco'
|
||||
ErrOpenrestyNotFound: 'El modo Http requiere tener Openresty instalado primero'
|
||||
ErrOpenrestyNotFound: 'El modo HTTP requiere OpenResty'
|
||||
ApplySSLStart: 'Iniciando solicitud de certificado, dominio [{{ .domain }}] método de solicitud [{{ .type }}]'
|
||||
dnsAccount: 'DNS Automático'
|
||||
dnsManual: 'DNS Manual'
|
||||
@@ -343,9 +362,9 @@ sqlInject: 'Inyección SQL'
|
||||
ErrDBNotExist: 'La base de datos no existe'
|
||||
allow: 'permitir'
|
||||
deny: 'denegar'
|
||||
OpenrestyNotFound: 'Openresty no está instalado'
|
||||
OpenrestyNotFound: 'OpenResty no está instalado'
|
||||
remoteIpIsNull: 'La lista de IP está vacía'
|
||||
OpenrestyVersionErr: 'La versión de Openresty es demasiado baja, actualice a 1.27.1.2-2-2-focal'
|
||||
OpenrestyVersionErr: 'La versión de OpenResty es demasiado antigua. Actualízalo a 1.27.1.2-2-2-focal'
|
||||
ErrFileTooLarge: 'El archivo supera 1 MB y no se puede cargar'
|
||||
TaskStart: 'La tarea {{ .name }} inicia [INICIO]'
|
||||
TaskEnd: 'La tarea {{ .name }} completada [COMPLETADA]'
|
||||
@@ -369,7 +388,7 @@ TaskRollback: 'Revertir'
|
||||
TaskPull: 'Descargar'
|
||||
TaskCommit: 'Commit'
|
||||
TaskBuild: 'Construir'
|
||||
TaskPush: 'Subir'
|
||||
TaskPush: 'Enviar'
|
||||
TaskClean: 'Limpieza'
|
||||
TaskHandle: 'Ejecutar'
|
||||
TaskImport: 'Importar'
|
||||
@@ -379,7 +398,7 @@ App: 'Aplicación'
|
||||
Runtime: 'Entorno de ejecución'
|
||||
Database: 'Base de datos'
|
||||
ConfigFTP: 'Crear usuario FTP {{ .name }}'
|
||||
ConfigOpenresty: 'Crear archivo de configuración Openresty'
|
||||
ConfigOpenresty: 'Crear archivo de configuración de OpenResty'
|
||||
InstallAppSuccess: 'Aplicación {{ .name }} instalada correctamente'
|
||||
ConfigRuntime: 'Configurar entorno de ejecución'
|
||||
ConfigApp: 'Configurar aplicación'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: 'Comando de riesgo bloqueado: {{ .command }}'
|
||||
TerminalAIThinking: 'La IA está pensando...'
|
||||
TerminalAIReadyToExecute: 'Pensamiento completado, pulsa Enter para ejecutar (duración: {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'La solicitud de AI ha fallado: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'No hay archivos ni carpetas bajo esta ruta (o todos fueron filtrados).'
|
||||
|
||||
+26
-6
@@ -2,6 +2,8 @@ ErrInvalidParams: '不正なリクエスト: {{ .detail }}'
|
||||
ErrTokenParse: 'トークンエラー: {{ .detail }}'
|
||||
ErrInitialPassword: '元のパスワードが違います'
|
||||
ErrInternalServer: '内部エラー: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'ファイル管理の「AI アカウント」でファイル AI 検索を有効にし、アカウントを選んでください。'
|
||||
ErrFileAISearchBadPattern: '検索パターンが無効です(正規表現など): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'リモートのダウンロード URL が無効か、到達できません:{{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'リモート URL がファイルではなくエラーページ等を返しました、{{ .detail }}'
|
||||
ErrRecordExist: 'レコードが既に存在します'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: 'デフォルト グループ、削除できません'
|
||||
ErrGroupIsInWebsiteUse: 'グループは別の Web サイトで使用されているため、削除できません。'
|
||||
Decrypt: '復号化'
|
||||
ErrAgentAccountBound: 'アカウントはエージェントに紐づいています'
|
||||
ErrTerminalAIAccountInUse: 'このモデルアカウントは Terminal AI で使用中です。Terminal AI の設定で別のアカウントへ切り替えるか、Terminal AI を無効にしてから再試行してください。'
|
||||
ErrAgentAccountUnavailable: 'アカウント接続不可: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'エージェントプロバイダ非対応'
|
||||
ErrAgentAccountRequired: 'まずエージェントアカウントを選択'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: 'アカウント未検証です'
|
||||
ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください'
|
||||
ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです'
|
||||
ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません'
|
||||
ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません'
|
||||
ErrAgentFallbackModelPrimary: 'フォールバックモデルは主モデルと同じにできません'
|
||||
ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'ベースURLが必要です'
|
||||
ErrAgentApiKeyRequired: 'APIキーが必要です'
|
||||
ErrAgentComposeRequired: 'Composeが必要です'
|
||||
ErrAgentIDRequired: 'エージェントIDが必要です'
|
||||
ErrAgentAccountIDRequired: 'アカウントIDが必要です'
|
||||
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Professional Edition にアップグレードすると数の制限はありません'
|
||||
ErrAgentLimitReached: 'Community Edition では AI エージェントを最大 {{ .max }} 件まで作成できます。Business Edition にアップグレードすると無制限で利用できます'
|
||||
ErrAgentWebsiteBound: 'このエージェントはすでにサイトに関連付けられています'
|
||||
ErrAgentWebsiteTypeUnsupported: '関連付けできるのはプロキシサイトまたは静的サイトのみです'
|
||||
ErrAgentWebsiteInUse: 'このサイトはすでに別のエージェントに関連付けられています'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'ワンクリックデプロイのサイトは手動で関連解除できません'
|
||||
ErrHermesPairingCodeUnavailable: 'Hermes でペアリングコードが一時的に見つかりません。ネットワーク要因の可能性があるため、しばらくしてから再試行してください。'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu のグループポリシーが許可リストの場合、DM ポリシーをペアリングコードにはできません。'
|
||||
Localhost: 'ローカルマシン'
|
||||
ErrBackupInUsed: 'バックアップアカウントがスケジュールで使用中'
|
||||
ErrBackupCheck: '接続テストに失敗しました: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: 'ターゲット パスが存在しません'
|
||||
ErrFileIsExist: 'ファイルまたはフォルダーは既に存在します'
|
||||
ErrFileUpload: '{{ .name }} はファイル {{ .detail }} のアップロードに失敗しました'
|
||||
ErrFileDownloadDir: 'ダウンロード フォルダーはサポートされていません'
|
||||
ErrFileSharePath: '無効なパスか、対象がファイルではありません'
|
||||
ErrFileShareInvalid: '共有リンクが無効か期限切れです'
|
||||
ErrFileShareExpired: '共有リンクの有効期限が切れました'
|
||||
ErrFileSharePassword: '共有パスワードが正しくありません'
|
||||
ErrFileShareExhausted: 'この共有のダウンロード上限に達しました'
|
||||
ErrFileSharePasswordPolicy: 'パスワードは4文字以上にしてください'
|
||||
ErrFileShareRateLimit: 'アクセスが多すぎます。しばらくしてから再試行してください'
|
||||
ErrCmdNotFound: '{{ .name }} コマンドが存在しません。まずこのコマンドをホストにインストールしてください'
|
||||
ErrSourcePathNotFound: 'ソース ディレクトリが存在しません'
|
||||
ErrFavoriteExist: 'このパスはすでにお気に入りに登録されています'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: '証明書ファイルが異常です、証明書の状態を確認
|
||||
ErrWebsiteDir: 'ウェブサイトディレクトリ内のディレクトリを選択してください。'
|
||||
ErrComposerFileNotFound: 'composer.json ファイルが存在しません'
|
||||
ErrRuntimeNoPort: 'ランタイム環境にポートが設定されていません。先にランタイム環境を編集してください。'
|
||||
ErrRuntimeProjectDirContainsCodeDir: '実行環境ディレクトリに選択したプロジェクトディレクトリが含まれています。別のプロジェクトディレクトリを選択して、再作成してください。'
|
||||
Status: 'ステータス'
|
||||
start: '開始'
|
||||
stop: '停止'
|
||||
@@ -169,7 +188,7 @@ ErrSSLCertificateNotFound: '証明書ファイルが存在しません'
|
||||
ErrSSLKeyFormat: '秘密鍵ファイルの検証に失敗しました'
|
||||
ErrSSLCertificateFormat: '証明書ファイルの形式が正しくありません。pem 形式を使用してください'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid または EabHmacKey は空白にできません'
|
||||
ErrOpenrestyNotFound: 'Http モードでは、まず Openresty をインストールする必要があります'
|
||||
ErrOpenrestyNotFound: 'HTTP モードでは OpenResty が必要です'
|
||||
ApplySSLStart: '証明書の申請を開始します。ドメイン名 [{{ .domain }}] 申請方法 [{{ .type }}]'
|
||||
dnsAccount: 'DNS 自動'
|
||||
dnsManual: 'DNS マニュアル'
|
||||
@@ -343,9 +362,9 @@ sqlInject: 'SQL インジェクション'
|
||||
ErrDBNotExist: 'データベースが存在しません'
|
||||
allow: '許可'
|
||||
deny: '拒否'
|
||||
OpenrestyNotFound: 'Openresty がインストールされていません'
|
||||
OpenrestyNotFound: 'OpenResty がインストールされていません'
|
||||
remoteIpIsNull: 'IPリストが空です'
|
||||
OpenrestyVersionErr: 'Openrestyのバージョンが低すぎます。Openrestyを1.27.1.2-2-2-focalにアップグレードしてください'
|
||||
OpenrestyVersionErr: 'OpenResty のバージョンが古すぎます。1.27.1.2-2-2-focal 以降にアップグレードしてください'
|
||||
ErrFileTooLarge: 'ファイルが 1MB を超えているためアップロードできません'
|
||||
TaskStart: '{{ .name }} タスクが開始されました [START]'
|
||||
TaskEnd: '{{ .name }} タスクが完了しました [COMPLETED]'
|
||||
@@ -369,7 +388,7 @@ TaskRollback: 'ロールバック'
|
||||
TaskPull: 'プル'
|
||||
TaskCommit: 'コミット'
|
||||
TaskBuild: 'ビルド'
|
||||
TaskPush: 'プッシュ'
|
||||
TaskPush: '送信'
|
||||
TaskClean: 'クリーンアップ'
|
||||
TaskHandle: '実行'
|
||||
TaskImport: 'インポート'
|
||||
@@ -379,7 +398,7 @@ App: 'アプリケーション'
|
||||
Runtime: 'ランタイム環境'
|
||||
Database: 'データベース'
|
||||
ConfigFTP: 'FTP ユーザー {{ .name }} を作成します'
|
||||
ConfigOpenresty: 'Openresty 構成ファイルを作成する'
|
||||
ConfigOpenresty: 'OpenResty の設定ファイルを作成'
|
||||
InstallAppSuccess: 'アプリケーション {{ .name }} が正常にインストールされました'
|
||||
ConfigRuntime: 'ランタイム環境を構成する'
|
||||
ConfigApp: '構成アプリケーション'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: '危険なコマンドをブロックしました
|
||||
TerminalAIThinking: 'AI が考えています...'
|
||||
TerminalAIReadyToExecute: '思考が完了しました。Enter で実行してください(所要時間 {{ .duration }}、token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'AI リクエストに失敗しました: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'このパス以下に一覧できるファイルまたはフォルダーがありません(またはすべて除外されました)。'
|
||||
|
||||
+26
-6
@@ -2,6 +2,8 @@ ErrInvalidParams: '잘못된 요청: {{ .detail }}'
|
||||
ErrTokenParse: '토큰 오류: {{ .detail }}'
|
||||
ErrInitialPassword: '원래 비밀번호가 틀립니다'
|
||||
ErrInternalServer: '서버 오류: {{ .detail }}'
|
||||
ErrFileAINotEnabled: '파일 관리에서 「AI 계정」으로 파일 AI 검색을 켜고 계정을 선택하세요.'
|
||||
ErrFileAISearchBadPattern: '검색 패턴이 잘못되었습니다(정규식 등): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: '원격 다운로드 주소가 잘못되었거나 접근할 수 없습니다: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: '원격 주소가 파일이 아닌 콘텐츠를 반환했습니다 (오류 페이지일 수 있음), {{ .detail }}'
|
||||
ErrRecordExist: '레코드가 이미 있습니다'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: '기본 그룹, 삭제할 수 없습니다'
|
||||
ErrGroupIsInWebsiteUse: '그룹이 다른 웹사이트에서 사용 중이므로 삭제할 수 없습니다.'
|
||||
Decrypt: '복호화'
|
||||
ErrAgentAccountBound: '계정이 에이전트에 묶여 있습니다'
|
||||
ErrTerminalAIAccountInUse: '이 모델 계정은 현재 터미널 AI에서 사용 중입니다. 터미널 AI 설정에서 다른 계정으로 변경하거나 터미널 AI를 비활성화한 뒤 다시 시도하세요.'
|
||||
ErrAgentAccountUnavailable: '계정 연결 불가: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: '지원되지 않는 에이전트 공급자'
|
||||
ErrAgentAccountRequired: '먼저 에이전트 계정을 선택하세요'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: '에이전트 계정 미확인'
|
||||
ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요'
|
||||
ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다'
|
||||
ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다'
|
||||
ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다'
|
||||
ErrAgentFallbackModelPrimary: '폴백 모델은 기본 모델과 같을 수 없습니다'
|
||||
ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: '기본 URL 필요'
|
||||
ErrAgentApiKeyRequired: 'API 키 필요'
|
||||
ErrAgentComposeRequired: 'Compose 내용 필요'
|
||||
ErrAgentIDRequired: '에이전트 ID 필요'
|
||||
ErrAgentAccountIDRequired: '계정 ID 필요'
|
||||
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 생성할 수 있으며, Professional 에디션으로 업그레이드하면 수량 제한이 없습니다'
|
||||
ErrAgentLimitReached: '커뮤니티 에디션에서는 AI 에이전트를 최대 {{ .max }}개까지 만들 수 있습니다. Business Edition으로 업그레이드하면 제한 없이 사용할 수 있습니다'
|
||||
ErrAgentWebsiteBound: '이 에이전트는 이미 웹사이트에 연결되어 있습니다'
|
||||
ErrAgentWebsiteTypeUnsupported: '프록시 또는 정적 웹사이트만 연결할 수 있습니다'
|
||||
ErrAgentWebsiteInUse: '이 웹사이트는 이미 다른 에이전트에 연결되어 있습니다'
|
||||
ErrAgentWebsiteUnbindUnsupported: '원클릭 배포 웹사이트는 수동으로 연결 해제할 수 없습니다'
|
||||
ErrHermesPairingCodeUnavailable: 'Hermes에서 페어링 코드가 일시적으로 존재하지 않습니다. 네트워크 문제일 수 있으니 잠시 후 다시 시도해 주세요.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu 그룹 정책이 허용 목록이면 DM 정책을 페어링 코드로 설정할 수 없습니다.'
|
||||
Localhost: '로컬 머신'
|
||||
ErrBackupInUsed: '백업 계정이 예약에 사용 중'
|
||||
ErrBackupCheck: '연결 테스트 실패: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: '대상 경로가 존재하지 않습니다'
|
||||
ErrFileIsExist: '파일이나 폴더가 이미 존재합니다'
|
||||
ErrFileUpload: '{{ .name }}이 파일 {{ .detail }}을 업로드하지 못했습니다.'
|
||||
ErrFileDownloadDir: '다운로드 폴더가 지원되지 않습니다'
|
||||
ErrFileSharePath: '잘못된 파일 경로이거나 대상이 파일이 아닙니다'
|
||||
ErrFileShareInvalid: '유효하지 않거나 만료된 공유 링크입니다'
|
||||
ErrFileShareExpired: '공유 링크가 만료되었습니다'
|
||||
ErrFileSharePassword: '공유 비밀번호가 올바르지 않습니다'
|
||||
ErrFileShareExhausted: '이 공유의 다운로드 한도에 도달했습니다'
|
||||
ErrFileSharePasswordPolicy: '비밀번호는 4자 이상이어야 합니다'
|
||||
ErrFileShareRateLimit: '요청이 너무 많습니다. 잠시 후 다시 시도해 주세요'
|
||||
ErrCmdNotFound: '{{ .name }} 명령이 존재하지 않습니다. 먼저 호스트에 이 명령을 설치하세요'
|
||||
ErrSourcePathNotFound: '소스 디렉토리가 존재하지 않습니다'
|
||||
ErrFavoriteExist: '이 경로는 이미 즐겨찾기되었습니다'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: '인증서 파일에 문제가 있습니다. 인증서 상태를
|
||||
ErrWebsiteDir: '웹사이트 디렉토리 내의 디렉토리를 선택하세요.'
|
||||
ErrComposerFileNotFound: 'composer.json 파일이 존재하지 않습니다'
|
||||
ErrRuntimeNoPort: '런타임 환경에 포트가 설정되지 않았습니다. 먼저 런타임 환경을 편집하세요.'
|
||||
ErrRuntimeProjectDirContainsCodeDir: '런타임 디렉터리에 선택한 프로젝트 디렉터리가 포함되어 있습니다. 다른 프로젝트 디렉터리를 선택한 후 다시 생성해 주세요.'
|
||||
Status: '상태'
|
||||
start: '시작'
|
||||
stop: '중지'
|
||||
@@ -169,7 +188,7 @@ ErrSSLCertificateNotFound: '인증서 파일이 존재하지 않습니다'
|
||||
ErrSSLKeyFormat: '개인 키 파일 검증에 실패했습니다'
|
||||
ErrSSLCertificateFormat: '인증서 파일 형식이 올바르지 않습니다. pem 형식을 사용하세요.'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid 또는 EabHmacKey는 비워둘 수 없습니다'
|
||||
ErrOpenrestyNotFound: 'Http 모드를 사용하려면 먼저 Openresty를 설치해야 합니다.'
|
||||
ErrOpenrestyNotFound: 'HTTP 모드에서는 OpenResty가 필요합니다'
|
||||
ApplySSLStart: '인증서 신청 시작, 도메인 이름 [{{ .domain }}] 신청 방법 [{{ .type }}]'
|
||||
dnsAccount: 'DNS 자동'
|
||||
dnsManual: 'DNS 매뉴얼'
|
||||
@@ -343,9 +362,9 @@ sqlInject: 'SQL 주입'
|
||||
ErrDBNotExist: '데이터베이스가 존재하지 않습니다'
|
||||
allow: '허용하다'
|
||||
deny: '거부하다'
|
||||
OpenrestyNotFound: 'Openresty가 설치되지 않았습니다'
|
||||
OpenrestyNotFound: 'OpenResty가 설치되어 있지 않습니다'
|
||||
remoteIpIsNull: 'IP 목록이 비어 있습니다'
|
||||
OpenrestyVersionErr: 'Openresty 버전이 너무 낮습니다. Openresty를 1.27.1.2-2-2-focal로 업그레이드하세요'
|
||||
OpenrestyVersionErr: 'OpenResty 버전이 너무 낮습니다. 1.27.1.2-2-2-focal 이상으로 업그레이드하세요'
|
||||
ErrFileTooLarge: '파일이 1MB를 초과하여 업로드할 수 없습니다'
|
||||
TaskStart: '[START] {{ .name }} 작업이 시작됩니다.'
|
||||
TaskEnd: '{{ .name }} 작업이 완료되었습니다 [완료]'
|
||||
@@ -369,7 +388,7 @@ TaskRollback: '롤백'
|
||||
TaskPull: '당기기'
|
||||
TaskCommit: '커밋'
|
||||
TaskBuild: '빌드'
|
||||
TaskPush: '푸시'
|
||||
TaskPush: '전송'
|
||||
TaskClean: '정리'
|
||||
TaskHandle: '실행'
|
||||
TaskImport: '가져오기'
|
||||
@@ -379,7 +398,7 @@ App: '애플리케이션'
|
||||
Runtime: '런타임 환경'
|
||||
Database: '데이터베이스'
|
||||
ConfigFTP: 'FTP 사용자 {{ .name }} 생성'
|
||||
ConfigOpenresty: 'Openresty 구성 파일 생성'
|
||||
ConfigOpenresty: 'OpenResty 구성 파일 생성'
|
||||
InstallAppSuccess: '애플리케이션 {{ .name }}이 성공적으로 설치되었습니다.'
|
||||
ConfigRuntime: '런타임 환경 구성'
|
||||
ConfigApp: '구성 애플리케이션'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: '위험한 명령이 차단되었습니다: {{ .c
|
||||
TerminalAIThinking: 'AI가 생각 중입니다...'
|
||||
TerminalAIReadyToExecute: '생각이 완료되었습니다. Enter를 눌러 실행하세요 (소요 시간 {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'AI 요청 실패: {{ .err }}'
|
||||
FileAISearchEmptyDir: '이 경로에 나열할 파일이나 디렉터리가 없습니다(또는 모두 필터링됨).'
|
||||
|
||||
+30
-10
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Permintaan tidak sah: {{ .detail }}'
|
||||
ErrTokenParse: 'Token tidak sah: {{ .detail }}'
|
||||
ErrInitialPassword: 'Kata laluan asal salah'
|
||||
ErrInternalServer: 'Ralat pelayan: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'Carian AI fail dimatikan atau tiada akaun. Dalam Pengurusan Fail, gunakan Akaun AI, aktifkan dan pilih akaun.'
|
||||
ErrFileAISearchBadPattern: 'Corak carian tidak sah (regex dsb.): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'Muat turun jauh gagal atau URL tidak boleh diakses: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'URL jauh tidak mengembalikan fail (mungkin halaman ralat), {{ .detail }}'
|
||||
ErrRecordExist: 'Rekod sudah wujud'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: 'Kumpulan lalai, tidak boleh dipadamkan'
|
||||
ErrGroupIsInWebsiteUse: 'Kumpulan sedang digunakan oleh tapak web lain dan tidak boleh dipadamkan.'
|
||||
Decrypt: 'Dekripsi'
|
||||
ErrAgentAccountBound: 'Akaun terikat kepada ejen'
|
||||
ErrTerminalAIAccountInUse: 'Akaun model ini sedang digunakan oleh Terminal AI. Tukar akaun dalam tetapan Terminal AI atau nyahaktifkan Terminal AI, kemudian cuba lagi.'
|
||||
ErrAgentAccountUnavailable: 'Sambungan akaun tidak tersedia: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'Penyedia ejen tidak disokong'
|
||||
ErrAgentAccountRequired: 'Pilih akaun ejen terlebih dahulu'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan'
|
||||
ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model'
|
||||
ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun'
|
||||
ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih'
|
||||
ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang'
|
||||
ErrAgentFallbackModelPrimary: 'Model fallback tidak boleh sama dengan model utama'
|
||||
ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'URL asas diperlukan'
|
||||
ErrAgentApiKeyRequired: 'Kunci API diperlukan'
|
||||
ErrAgentComposeRequired: 'Kandungan compose diperlukan'
|
||||
ErrAgentIDRequired: 'ID ejen diperlukan'
|
||||
ErrAgentAccountIDRequired: 'ID akaun diperlukan'
|
||||
ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Naik taraf ke edisi Professional untuk tiada had bilangan'
|
||||
ErrAgentLimitReached: 'Edisi Community menyokong sehingga {{ .max }} ejen AI. Naik taraf ke Edisi Business untuk penggunaan tanpa had'
|
||||
ErrAgentWebsiteBound: 'Ejen ini sudah dipautkan ke laman web'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Hanya laman web proxy atau statik boleh dipautkan'
|
||||
ErrAgentWebsiteInUse: 'Laman web ini sudah dipautkan ke ejen lain'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Laman web one-click deployment tidak menyokong nyahikat manual'
|
||||
ErrHermesPairingCodeUnavailable: 'Kod pasangan buat sementara waktu tidak wujud dalam Hermes, mungkin disebabkan masalah rangkaian. Sila cuba lagi sebentar nanti.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Apabila dasar kumpulan Feishu ialah senarai benarkan, dasar DM tidak boleh menggunakan kod pasangan.'
|
||||
Localhost: 'Mesin Tempatan'
|
||||
ErrBackupInUsed: 'Akaun sandaran sedang digunakan oleh tugas'
|
||||
ErrBackupCheck: 'Ujian sambungan gagal: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: 'Laluan sasaran tidak wujud'
|
||||
ErrFileIsExist: 'Fail atau folder sudah wujud'
|
||||
ErrFileUpload: '{{ .name }} gagal memuat naik fail {{ .detail }}'
|
||||
ErrFileDownloadDir: 'Folder muat turun tidak disokong'
|
||||
ErrFileSharePath: 'Laluan fail tidak sah atau sasaran bukan fail'
|
||||
ErrFileShareInvalid: 'Pautan kongsi tidak sah atau telah tamat tempoh'
|
||||
ErrFileShareExpired: 'Pautan kongsi telah tamat tempoh'
|
||||
ErrFileSharePassword: 'Kata laluan kongsi tidak betul'
|
||||
ErrFileShareExhausted: 'Had muat turun untuk kongsi ini telah dicapai'
|
||||
ErrFileSharePasswordPolicy: 'Kata laluan mestilah sekurang-kurangnya 4 aksara'
|
||||
ErrFileShareRateLimit: 'Terlalu banyak permintaan, sila cuba lagi kemudian'
|
||||
ErrCmdNotFound: 'Arahan {{ .name }} tidak wujud, sila pasang arahan ini pada hos dahulu'
|
||||
ErrSourcePathNotFound: 'Direktori sumber tidak wujud'
|
||||
ErrFavoriteExist: 'Laluan ini telah digemari'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: 'Fail sijil bermasalah, sila periksa status sijil'
|
||||
ErrWebsiteDir: 'Sila pilih direktori dalam direktori laman web.'
|
||||
ErrComposerFileNotFound: 'Fail composer.json tidak wujud'
|
||||
ErrRuntimeNoPort: 'Persekitaran runtime tidak diset dengan port, sila edit persekitaran runtime terlebih dahulu.'
|
||||
ErrRuntimeProjectDirContainsCodeDir: 'Direktori runtime mengandungi direktori projek yang dipilih. Sila pilih direktori projek lain dan cipta semula.'
|
||||
Status: 'Status'
|
||||
start: 'Mulakan'
|
||||
stop: 'Berhenti'
|
||||
@@ -169,7 +188,7 @@ ErrSSLCertificateNotFound: 'Fail sijil tidak wujud'
|
||||
ErrSSLKeyFormat: 'Pengesahan fail kunci peribadi gagal'
|
||||
ErrSSLCertificateFormat: 'Format fail sijil tidak betul, sila gunakan format pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid atau EabHmacKey tidak boleh kosong'
|
||||
ErrOpenrestyNotFound: 'Mod Http memerlukan Openresty dipasang dahulu'
|
||||
ErrOpenrestyNotFound: 'Mod HTTP memerlukan OpenResty dipasang terlebih dahulu'
|
||||
ApplySSLStart: 'Mula memohon sijil, nama domain [{{ .domain }}] kaedah permohonan [{{ .type }}]'
|
||||
dnsAccount: 'DNS Auto'
|
||||
dnsManual: 'Manual DNS'
|
||||
@@ -254,8 +273,8 @@ ErrScriptsNotFound: 'Item konfigurasi skrip tidak ditemui dalam package.json'
|
||||
ErrContainerNameNotFound: 'Tidak dapat mendapatkan nama kontena, sila semak fail.env'
|
||||
ErrNodeModulesNotFound: 'Folder node_modules tidak wujud Sila edit persekitaran masa jalan atau tunggu persekitaran masa jalan berjaya dimulakan'
|
||||
ErrContainerNameIsNull: 'Nama kontena tidak wujud'
|
||||
ErrPHPPortIsDefault: 'Порт 9000 является портом по умолчанию, пожалуйста, измените и попробуйте снова'
|
||||
ErrPHPRuntimePortFailed: 'Порт {{ .name }} уже используется текущей средой выполнения, пожалуйста, измените и попробуйте снова'
|
||||
ErrPHPPortIsDefault: 'Port 9000 ialah port lalai. Sila tukar dan cuba lagi'
|
||||
ErrPHPRuntimePortFailed: 'Port {{ .name }} sudah digunakan oleh runtime semasa. Sila tukar dan cuba lagi'
|
||||
ErrConfigNotFound: 'Fail konfigurasi tidak wujud'
|
||||
ErrConfigParse: 'Format fail konfigurasi tidak betul'
|
||||
ErrConfigIsNull: 'Fail konfigurasi tidak boleh kosong'
|
||||
@@ -343,10 +362,10 @@ sqlInject: 'Suntikan SQL'
|
||||
ErrDBNotExist: 'Pangkalan data tidak wujud'
|
||||
allow: 'membenarkan'
|
||||
deny: 'menafikan'
|
||||
OpenrestyNotFound: 'Openresty tidak dipasang'
|
||||
OpenrestyNotFound: 'OpenResty tidak dipasang'
|
||||
remoteIpIsNull: 'Senarai IP kosong'
|
||||
OpenrestyVersionErr: 'Versi Openresty terlalu rendah, sila naik taraf Openresty ke 1.27.1.2-2-2-focal'
|
||||
ErrFileTooLarge: 'File is larger than 1 MB and cannot be uploaded'
|
||||
OpenrestyVersionErr: 'Versi OpenResty terlalu lama. Naik taraf ke 1.27.1.2-2-2-focal'
|
||||
ErrFileTooLarge: 'Fail melebihi 1 MB dan tidak boleh dimuat naik'
|
||||
TaskStart: '{{ .name }} Task mula [MULA]'
|
||||
TaskEnd: '{{ .name }} Task selesai [SELESAI]'
|
||||
TaskFailed: 'tugas {{ .name }} gagal'
|
||||
@@ -369,7 +388,7 @@ TaskRollback: 'Rollback'
|
||||
TaskPull: 'Tarik'
|
||||
TaskCommit: 'Komit'
|
||||
TaskBuild: 'Bina'
|
||||
TaskPush: 'Tolak'
|
||||
TaskPush: 'Hantar'
|
||||
TaskClean: 'Pembersihan'
|
||||
TaskHandle: 'Laksanakan'
|
||||
TaskImport: 'Import'
|
||||
@@ -379,7 +398,7 @@ App: 'Aplikasi'
|
||||
Runtime: 'Persekitaran masa jalan'
|
||||
Database: 'Pangkalan Data'
|
||||
ConfigFTP: 'Buat pengguna FTP {{ .name }}'
|
||||
ConfigOpenresty: 'Buat fail konfigurasi Openresty'
|
||||
ConfigOpenresty: 'Buat fail konfigurasi OpenResty'
|
||||
InstallAppSuccess: 'Aplikasi {{ .name }} berjaya dipasang'
|
||||
ConfigRuntime: 'Konfigurasikan persekitaran masa jalan'
|
||||
ConfigApp: 'Aplikasi Konfigurasi'
|
||||
@@ -436,7 +455,7 @@ SnapCompressSize: 'Saiz fail syot kilat {{ .name }}'
|
||||
SnapUpload: 'Muat naik fail syot kilat'
|
||||
SnapUploadTo: 'Muat naik fail syot kilat ke {{ .name }}'
|
||||
SnapUploadRes: 'Muat naik fail syot kilat ke {{ .name }}'
|
||||
SnapshotRecover: 'Snapshot Restore'
|
||||
SnapshotRecover: 'Pulihkan syot kilat'
|
||||
RecoverDownload: 'Muat turun fail syot kilat'
|
||||
Download: 'Muat turun'
|
||||
RecoverDownloadAccount: 'Dapatkan syot kilat muat turun akaun sandaran {{ .name }}'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: 'Perintah berisiko telah disekat: {{ .command }}'
|
||||
TerminalAIThinking: 'AI sedang berfikir...'
|
||||
TerminalAIReadyToExecute: 'Pemikiran selesai, tekan Enter untuk jalankan (tempoh: {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'Permintaan AI gagal: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'Tiada fail atau direktori di bawah laluan ini (atau semua ditapis).'
|
||||
|
||||
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Requisição inválida: {{ .detail }}'
|
||||
ErrTokenParse: 'Erro de token: {{ .detail }}'
|
||||
ErrInitialPassword: 'Senha original incorreta'
|
||||
ErrInternalServer: 'Erro interno: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'A pesquisa com IA de arquivos está desativada ou sem conta. Em Gerenciamento de arquivos, use Conta de IA, ative e escolha uma conta.'
|
||||
ErrFileAISearchBadPattern: 'Padrão de busca inválido (regex ou opção): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'Falha no download remoto ou URL inacessível: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'A URL remota não retornou um arquivo (possivelmente uma página de erro), {{ .detail }}'
|
||||
ErrRecordExist: 'Registro já existe'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: 'Grupo padrão, não pode ser excluído'
|
||||
ErrGroupIsInWebsiteUse: 'O grupo está sendo usado por outro site e não pode ser excluído.'
|
||||
Decrypt: 'Descriptografar'
|
||||
ErrAgentAccountBound: 'Conta vinculada a agente'
|
||||
ErrTerminalAIAccountInUse: 'Esta conta de modelo esta sendo usada pelo Terminal AI. Troque a conta nas configuracoes do Terminal AI ou desative o Terminal AI e tente novamente.'
|
||||
ErrAgentAccountUnavailable: 'Conexão da conta indisponível: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'Provedor de agente não suportado'
|
||||
ErrAgentAccountRequired: 'Selecione uma conta de agente primeiro'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: 'Conta de agente não verificada'
|
||||
ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo'
|
||||
ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta'
|
||||
ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada'
|
||||
ErrAgentFallbackModelDuplicate: 'Os modelos de fallback não podem ser duplicados'
|
||||
ErrAgentFallbackModelPrimary: 'O modelo de fallback não pode ser igual ao modelo principal'
|
||||
ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'URL base obrigatória'
|
||||
ErrAgentApiKeyRequired: 'Chave API obrigatória'
|
||||
ErrAgentComposeRequired: 'Conteúdo de compose obrigatório'
|
||||
ErrAgentIDRequired: 'ID do agente obrigatório'
|
||||
ErrAgentAccountIDRequired: 'ID da conta obrigatório'
|
||||
ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de IA. Atualize para a edição Professional para remover o limite'
|
||||
ErrAgentLimitReached: 'A edição Community suporta até {{ .max }} agentes de IA. Faça upgrade para a edição Business para usar agentes ilimitados'
|
||||
ErrAgentWebsiteBound: 'Este agente já está vinculado a um site'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Somente sites proxy ou estáticos podem ser vinculados'
|
||||
ErrAgentWebsiteInUse: 'Este site já está vinculado a outro agente'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Sites implantados em um clique não podem ser desvinculados manualmente'
|
||||
ErrHermesPairingCodeUnavailable: 'O código de pareamento está temporariamente indisponível no Hermes, possivelmente por causa de rede. Tente novamente mais tarde.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Quando a política de grupo do Feishu é Lista de permissões, a política de DM não pode ser Código de pareamento.'
|
||||
Localhost: 'Máquina Local'
|
||||
ErrBackupInUsed: 'Conta de backup em uso por tarefa'
|
||||
ErrBackupCheck: 'Teste de conexão falhou: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: 'O caminho de destino não existe'
|
||||
ErrFileIsExist: 'O arquivo ou pasta já existe'
|
||||
ErrFileUpload: '{{ .name }} falhou ao carregar o arquivo {{ .detail }}'
|
||||
ErrFileDownloadDir: 'A pasta de download não é suportada'
|
||||
ErrFileSharePath: 'Caminho inválido ou o destino não é um arquivo'
|
||||
ErrFileShareInvalid: 'Link de compartilhamento inválido ou expirado'
|
||||
ErrFileShareExpired: 'O link de compartilhamento expirou'
|
||||
ErrFileSharePassword: 'Senha de compartilhamento incorreta'
|
||||
ErrFileShareExhausted: 'O limite de downloads deste compartilhamento foi atingido'
|
||||
ErrFileSharePasswordPolicy: 'A senha deve ter pelo menos 4 caracteres'
|
||||
ErrFileShareRateLimit: 'Muitas solicitações, tente novamente mais tarde'
|
||||
ErrCmdNotFound: 'O comando {{ .name }} não existe, instale este comando no host primeiro'
|
||||
ErrSourcePathNotFound: 'O diretório de origem não existe'
|
||||
ErrFavoriteExist: 'Este caminho já foi favorito'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: 'O arquivo do certificado está anormal, verifique o status do cert
|
||||
ErrWebsiteDir: ', selecione um diretório dentro do diretório do site.'
|
||||
ErrComposerFileNotFound: 'O arquivo composer.json não existe'
|
||||
ErrRuntimeNoPort: 'O ambiente de tempo de execução não está configurado com uma porta, edite o ambiente de tempo de execução primeiro.'
|
||||
ErrRuntimeProjectDirContainsCodeDir: 'O diretório de execução inclui o diretório de projeto selecionado. Escolha outro diretório de projeto e crie novamente.'
|
||||
Status: 'Status'
|
||||
start: 'Iniciar'
|
||||
stop: 'Parar'
|
||||
@@ -162,14 +181,14 @@ SSL: 'Certificado'
|
||||
Set: 'Configurações'
|
||||
ErrSSLCannotDelete: 'O certificado {{ .name }} está sendo usado por um site e não pode ser excluído'
|
||||
ErrAccountCannotDelete: 'A conta está associada a um certificado e não pode ser excluída'
|
||||
ErrSSLApply: 'Renovação do certificado bem-sucedida, falha na recarga do Openresty, verifique a configuração'
|
||||
ErrSSLApply: 'Renovação do certificado concluída, mas a recarga do OpenResty falhou. Verifique a configuração'
|
||||
ErrEmailIsExist: 'A caixa de correio já existe'
|
||||
ErrSSLKeyNotFound: 'O arquivo de chave privada não existe'
|
||||
ErrSSLCertificateNotFound: 'O arquivo de certificado não existe'
|
||||
ErrSSLKeyFormat: 'Falha na verificação do arquivo de chave privada'
|
||||
ErrSSLCertificateFormat: 'O formato do arquivo de certificado está incorreto, use o formato pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid ou EabHmacKey não podem estar em branco'
|
||||
ErrOpenrestyNotFound: 'O modo HTTP requer que o Openresty seja instalado primeiro'
|
||||
ErrOpenrestyNotFound: 'O modo HTTP requer o OpenResty instalado'
|
||||
ApplySSLStart: 'Comece a solicitar um certificado, nome de domínio [{{ .domain }}] método de solicitação [{{ .type }}]'
|
||||
dnsAccount: 'DNS Automático'
|
||||
dnsManual: 'Manual DNS'
|
||||
@@ -254,8 +273,8 @@ ErrScriptsNotFound: 'O item de configuração de scripts não foi encontrado em
|
||||
ErrContainerNameNotFound: 'Não foi possível obter o nome do contêiner, verifique o arquivo.env'
|
||||
ErrNodeModulesNotFound: 'A pasta node_modules não existe Edite o ambiente de execução ou aguarde até que o ambiente de execução inicie com sucesso'
|
||||
ErrContainerNameIsNull: 'O nome do contêiner não existe'
|
||||
ErrPHPPortIsDefault: 'Port 9000 adalah port lalai, sila ubah dan cuba lagi'
|
||||
ErrPHPRuntimePortFailed: 'Port {{ .name }} telah digunakan oleh persekitaran runtime semasa, sila ubah dan cuba lagi'
|
||||
ErrPHPPortIsDefault: 'A porta 9000 é a porta padrão. Altere-a e tente novamente'
|
||||
ErrPHPRuntimePortFailed: 'A porta {{ .name }} já está em uso neste runtime. Altere-a e tente novamente'
|
||||
ErrConfigNotFound: 'O arquivo de configuração não existe'
|
||||
ErrConfigParse: 'O formato do arquivo de configuração está incorreto'
|
||||
ErrConfigIsNull: 'O arquivo de configuração não pode estar vazio'
|
||||
@@ -343,9 +362,9 @@ sqlInject: 'injeção de SQL'
|
||||
ErrDBNotExist: 'O banco de dados não existe'
|
||||
allow: 'permitir'
|
||||
deny: 'negar'
|
||||
OpenrestyNotFound: 'Openresty não está instalado'
|
||||
OpenrestyNotFound: 'OpenResty não está instalado'
|
||||
remoteIpIsNull: 'A lista de IP está vazia'
|
||||
OpenrestyVersionErr: 'A versão do Openresty é muito baixa atualize o Openresty para 1.27.1.2-2-2-focal'
|
||||
OpenrestyVersionErr: 'A versão do OpenResty é muito antiga. Atualize para 1.27.1.2-2-2-focal'
|
||||
ErrFileTooLarge: 'O arquivo excede 1 MB e não pode ser enviado'
|
||||
TaskStart: '{{ .name }} A tarefa inicia [START]'
|
||||
TaskEnd: '{{ .name }} Tarefa concluída [CONCLUÍDA]'
|
||||
@@ -369,7 +388,7 @@ TaskRollback: 'Reverter'
|
||||
TaskPull: 'Puxar'
|
||||
TaskCommit: 'Commit'
|
||||
TaskBuild: 'Construir'
|
||||
TaskPush: 'Empurrar'
|
||||
TaskPush: 'Enviar'
|
||||
TaskClean: 'Limpeza'
|
||||
TaskHandle: 'Executar'
|
||||
TaskImport: 'Importar'
|
||||
@@ -379,7 +398,7 @@ App: 'Aplicativo'
|
||||
Runtime: 'Ambiente de tempo de execução'
|
||||
Database: 'Banco de dados'
|
||||
ConfigFTP: 'Criar usuário FTP {{ .name }}'
|
||||
ConfigOpenresty: 'Criar arquivo de configuração Openresty'
|
||||
ConfigOpenresty: 'Criar arquivo de configuração do OpenResty'
|
||||
InstallAppSuccess: 'Aplicativo {{ .name }} instalado com sucesso'
|
||||
ConfigRuntime: 'Configurar o ambiente de tempo de execução'
|
||||
ConfigApp: 'Aplicativo de configuração'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: 'Comando de risco bloqueado: {{ .command }}'
|
||||
TerminalAIThinking: 'A IA está pensando...'
|
||||
TerminalAIReadyToExecute: 'Pensamento concluído, pressione Enter para executar (duração: {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'Falha na solicitação de AI: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'Nenhum arquivo ou pasta encontrado neste caminho (ou todos foram filtrados).'
|
||||
|
||||
+29
-9
@@ -2,6 +2,8 @@ ErrInvalidParams: 'Неверный запрос: {{ .detail }}'
|
||||
ErrTokenParse: 'Ошибка токена: {{ .detail }}'
|
||||
ErrInitialPassword: 'Исходный пароль неверен'
|
||||
ErrInternalServer: 'Внутренняя ошибка: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'Поиск с AI в файлах выключен или не выбран аккаунт. В управлении файлами откройте «AI-аккаунт», включите и выберите аккаунт.'
|
||||
ErrFileAISearchBadPattern: 'Недопустимый шаблон поиска (regex и т.п.): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'Неверный или недоступный URL удалённой загрузки: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'Удалённый сервер вернул не файл (возможно, страницу ошибки), {{ .detail }}'
|
||||
ErrRecordExist: 'Запись уже существует'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: 'Группа по умолчанию, не может быт
|
||||
ErrGroupIsInWebsiteUse: 'Группа используется другим веб-сайтом и не может быть удалена.'
|
||||
Decrypt: 'Расшифровать'
|
||||
ErrAgentAccountBound: 'Акаунт привязан к агенту'
|
||||
ErrTerminalAIAccountInUse: 'Эта учетная запись модели сейчас используется Terminal AI. Смените учетную запись в настройках Terminal AI или отключите Terminal AI и повторите попытку.'
|
||||
ErrAgentAccountUnavailable: 'Связь с аккаунтом недоступна: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'Провайдер агента не поддерживается'
|
||||
ErrAgentAccountRequired: 'Выберите аккаунт агента'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: 'Аккаунт не подтверждён'
|
||||
ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель'
|
||||
ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель'
|
||||
ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте'
|
||||
ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться'
|
||||
ErrAgentFallbackModelPrimary: 'Резервная модель не может совпадать с основной'
|
||||
ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Нужен базовый URL'
|
||||
ErrAgentApiKeyRequired: 'Нужен API-ключ'
|
||||
ErrAgentComposeRequired: 'Нужен compose'
|
||||
ErrAgentIDRequired: 'Нужен ID агента'
|
||||
ErrAgentAccountIDRequired: 'Нужен ID аккаунта'
|
||||
ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Перейдите на Professional Edition, чтобы снять ограничение'
|
||||
ErrAgentLimitReached: 'Community Edition поддерживает до {{ .max }} AI-агентов. Обновитесь до Business Edition, чтобы использовать их без ограничений'
|
||||
ErrAgentWebsiteBound: 'Этот агент уже связан с сайтом'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Можно связывать только proxy- или static-сайты'
|
||||
ErrAgentWebsiteInUse: 'Этот сайт уже связан с другим агентом'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Сайты one-click deployment нельзя отвязать вручную'
|
||||
ErrHermesPairingCodeUnavailable: 'Код сопряжения временно недоступен в Hermes, возможно из-за проблем с сетью. Повторите попытку позже.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Когда групповая политика Feishu — белый список, политика личных сообщений не может быть кодом сопряжения.'
|
||||
Localhost: 'Локальная машина'
|
||||
ErrBackupInUsed: 'Аккаунт бэкапа занят задачей'
|
||||
ErrBackupCheck: 'Проверка подключения не удалась: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: 'Целевой путь не существует'
|
||||
ErrFileIsExist: 'Файл или папка уже существует'
|
||||
ErrFileUpload: '{{ .name }} не удалось загрузить файл {{ .detail }}'
|
||||
ErrFileDownloadDir: 'Папка загрузки не поддерживается'
|
||||
ErrFileSharePath: 'Неверный путь или цель не является файлом'
|
||||
ErrFileShareInvalid: 'Недействительная или просроченная ссылка'
|
||||
ErrFileShareExpired: 'Срок действия ссылки истёк'
|
||||
ErrFileSharePassword: 'Неверный пароль доступа'
|
||||
ErrFileShareExhausted: 'Лимит скачиваний по этой ссылке исчерпан'
|
||||
ErrFileSharePasswordPolicy: 'Пароль должен содержать не менее 4 символов'
|
||||
ErrFileShareRateLimit: 'Слишком много запросов, попробуйте позже'
|
||||
ErrCmdNotFound: 'Команда {{ .name }} не существует, сначала установите эту команду на хосте'
|
||||
ErrSourcePathNotFound: 'Исходный каталог не существует'
|
||||
ErrFavoriteExist: 'Этот путь уже добавлен в избранное'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: 'Файл сертификата аномален, проверьт
|
||||
ErrWebsiteDir: 'Пожалуйста, выберите директорию внутри директории сайта.'
|
||||
ErrComposerFileNotFound: 'Файл composer.json не существует'
|
||||
ErrRuntimeNoPort: 'Среда выполнения не настроена с портом, сначала отредактируйте среду выполнения.'
|
||||
ErrRuntimeProjectDirContainsCodeDir: 'Каталог среды выполнения содержит выбранный каталог проекта. Выберите другой каталог проекта и создайте его заново.'
|
||||
Status: 'Статус'
|
||||
start: 'Запустить'
|
||||
stop: 'Остановить'
|
||||
@@ -169,7 +188,7 @@ ErrSSLCertificateNotFound: 'Файл сертификата не существ
|
||||
ErrSSLKeyFormat: 'Проверка файла закрытого ключа не удалась'
|
||||
ErrSSLCertificateFormat: 'Неверный формат файла сертификата, используйте формат pem'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid или EabHmacKey не могут быть пустыми'
|
||||
ErrOpenrestyNotFound: 'Для режима HTTP необходимо сначала установить Openresty'
|
||||
ErrOpenrestyNotFound: 'Для режима HTTP требуется OpenResty'
|
||||
ApplySSLStart: 'Начать подачу заявки на сертификат, доменное имя [{{ .domain }}], метод подачи заявки [{{ .type }}]'
|
||||
dnsAccount: 'DNS Авто'
|
||||
dnsManual: 'Руководство по DNS'
|
||||
@@ -254,8 +273,8 @@ ErrScriptsNotFound: 'Элемент конфигурации скриптов н
|
||||
ErrContainerNameNotFound: 'Не удалось получить имя контейнера, проверьте файл.env'
|
||||
ErrNodeModulesNotFound: 'Папка node_modules не существует Измените среду выполнения или дождитесь ее успешного запуска'
|
||||
ErrContainerNameIsNull: 'Имя контейнера не существует'
|
||||
ErrPHPPortIsDefault: 'A porta 9000 é a porta padrão, por favor, modifique e tente novamente'
|
||||
ErrPHPRuntimePortFailed: 'A porta {{ .name }} já está sendo usada pelo ambiente de tempo de execução atual, por favor, modifique e tente novamente'
|
||||
ErrPHPPortIsDefault: 'Порт 9000 зарезервирован как стандартный. Укажите другой порт'
|
||||
ErrPHPRuntimePortFailed: 'Порт {{ .name }} уже используется этой средой выполнения'
|
||||
ErrConfigNotFound: 'Файл конфигурации не существует'
|
||||
ErrConfigParse: 'Неверный формат файла конфигурации'
|
||||
ErrConfigIsNull: 'Файл конфигурации не может быть пустым'
|
||||
@@ -343,10 +362,10 @@ sqlInject: 'SQL-инъекция'
|
||||
ErrDBNotExist: 'База данных не существует'
|
||||
allow: 'разрешить'
|
||||
deny: 'отрицать'
|
||||
OpenrestyNotFound: 'Openresty не установлен'
|
||||
OpenrestyNotFound: 'OpenResty не установлен'
|
||||
remoteIpIsNull: 'Список IP пуст'
|
||||
OpenrestyVersionErr: 'Версия Openresty слишком низкая, пожалуйста, обновите Openresty до 1.27.1.2-2-2-focal'
|
||||
ErrFileTooLarge: 'File is larger than 1 MB and cannot be uploaded'
|
||||
OpenrestyVersionErr: 'Версия OpenResty слишком старая. Обновите до 1.27.1.2-2-2-focal'
|
||||
ErrFileTooLarge: 'Файл больше 1 МБ и не может быть загружен'
|
||||
TaskStart: '{{ .name }} Задача начинается [START]'
|
||||
TaskEnd: '{{ .name }} Задача выполнена [ЗАВЕРШЕНО]'
|
||||
TaskFailed: 'Задача {{ .name }} не выполнена'
|
||||
@@ -369,7 +388,7 @@ TaskRollback: 'Откат'
|
||||
TaskPull: 'Вытянуть'
|
||||
TaskCommit: 'Kоммит'
|
||||
TaskBuild: 'Сборка'
|
||||
TaskPush: 'Push'
|
||||
TaskPush: 'Отправить'
|
||||
TaskClean: 'Очистка'
|
||||
TaskHandle: 'Выполнить'
|
||||
TaskImport: 'Импорт'
|
||||
@@ -379,7 +398,7 @@ App: 'Приложение'
|
||||
Runtime: 'Среда выполнения'
|
||||
Database: 'База данных'
|
||||
ConfigFTP: 'Создать пользователя FTP {{ .name }}'
|
||||
ConfigOpenresty: 'Создать файл конфигурации Openresty'
|
||||
ConfigOpenresty: 'Создать файл конфигурации OpenResty'
|
||||
InstallAppSuccess: 'Приложение {{ .name }} успешно установлено'
|
||||
ConfigRuntime: 'Настройка среды выполнения'
|
||||
ConfigApp: 'Приложение конфигурации'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: 'Опасная команда заблокиро
|
||||
TerminalAIThinking: 'AI думает...'
|
||||
TerminalAIReadyToExecute: 'Обдумывание завершено, нажмите Enter для выполнения (время: {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'Ошибка запроса AI: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'В этом пути нет файлов или каталогов для отображения (или все отфильтрованы).'
|
||||
|
||||
+26
-6
@@ -2,6 +2,8 @@ ErrInvalidParams: 'İstek geçersiz: {{ .detail }}'
|
||||
ErrTokenParse: 'Token hatası: {{ .detail }}'
|
||||
ErrInitialPassword: 'Orijinal şifre yanlış'
|
||||
ErrInternalServer: 'Sunucu hatası: {{ .detail }}'
|
||||
ErrFileAINotEnabled: 'Dosya yönetimi AI araması kapalı veya hesap yok. Dosya Yönetimi’nde AI hesabını açıp etkinleştirin ve bir hesap seçin.'
|
||||
ErrFileAISearchBadPattern: 'Geçersiz arama deseni (regex vb.): {{ .detail }}'
|
||||
ErrWgetRemoteFailed: 'Uzak indirme başarısız veya URL erişilemiyor: {{ .detail }}'
|
||||
ErrWgetInvalidContentType: 'Uzak URL bir dosya değil, muhtemelen bir hata sayfası döndürdü, {{ .detail }}'
|
||||
ErrRecordExist: 'Kayıt zaten var'
|
||||
@@ -37,6 +39,7 @@ ErrGroupIsDefault: 'Varsayılan grup, silinemez'
|
||||
ErrGroupIsInWebsiteUse: 'Grup başka bir web sitesi tarafından kullanılıyor ve silinemez.'
|
||||
Decrypt: 'Şifre Çöz'
|
||||
ErrAgentAccountBound: 'Hesap bir ajana bağlı'
|
||||
ErrTerminalAIAccountInUse: 'Bu model hesabi su anda Terminal AI tarafindan kullaniliyor. Terminal AI ayarlarinda baska bir hesaba gecin veya Terminal AI yi devre disi birakip tekrar deneyin.'
|
||||
ErrAgentAccountUnavailable: 'Hesap bağlantısı yok: {{ .err }}'
|
||||
ErrAgentProviderNotSupported: 'Ajans sağlayıcısı desteklenmiyor'
|
||||
ErrAgentAccountRequired: 'Önce bir ajans hesabı seçin'
|
||||
@@ -44,13 +47,21 @@ ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı'
|
||||
ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın'
|
||||
ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir'
|
||||
ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı'
|
||||
ErrAgentFallbackModelDuplicate: 'Yedek modeller tekrar edemez'
|
||||
ErrAgentFallbackModelPrimary: 'Yedek model birincil modelle aynı olamaz'
|
||||
ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}'
|
||||
ErrAgentBaseURLRequired: 'Temel URL gerekli'
|
||||
ErrAgentApiKeyRequired: 'API anahtarı gerekli'
|
||||
ErrAgentComposeRequired: 'Compose içeriği gerekli'
|
||||
ErrAgentIDRequired: 'Ajans ID gerekli'
|
||||
ErrAgentAccountIDRequired: 'Hesap ID gerekli'
|
||||
ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını destekler. Sınırı kaldırmak için Professional sürüme yükseltin'
|
||||
ErrAgentLimitReached: 'Community sürümü en fazla {{ .max }} AI ajanını destekler. Sınırsız kullanım için Business Sürüme yükseltin'
|
||||
ErrAgentWebsiteBound: 'Bu ajan zaten bir web sitesine bağlı'
|
||||
ErrAgentWebsiteTypeUnsupported: 'Yalnızca proxy veya statik web siteleri bağlanabilir'
|
||||
ErrAgentWebsiteInUse: 'Bu web sitesi zaten başka bir ajana bağlı'
|
||||
ErrAgentWebsiteUnbindUnsupported: 'Tek tıkla dağıtılan web sitelerinin bağlantısı manuel olarak kaldırılamaz'
|
||||
ErrHermesPairingCodeUnavailable: 'Eşleştirme kodu Hermes içinde geçici olarak bulunamıyor; bu durum ağ kaynaklı olabilir. Lütfen daha sonra tekrar deneyin.'
|
||||
ErrHermesFeishuGroupAllowlistRequiresAllowlist: 'Feishu grup ilkesi izin listesi olduğunda, DM ilkesi eşleştirme kodu olamaz.'
|
||||
Localhost: 'Yerel Makine'
|
||||
ErrBackupInUsed: 'Yedek hesabı görevde kullanılıyor'
|
||||
ErrBackupCheck: 'Bağlantı testi başarısız: {{ .err }}'
|
||||
@@ -122,6 +133,13 @@ ErrLinkPathNotFound: 'Hedef yol mevcut değil'
|
||||
ErrFileIsExist: 'Dosya veya klasör zaten mevcut'
|
||||
ErrFileUpload: '{{ .name }} dosya yükleme başarısız {{ .detail }}'
|
||||
ErrFileDownloadDir: 'Klasör indirme desteklenmiyor'
|
||||
ErrFileSharePath: 'Geçersiz dosya yolu veya hedef bir dosya değil'
|
||||
ErrFileShareInvalid: 'Geçersiz veya süresi dolmuş paylaşım bağlantısı'
|
||||
ErrFileShareExpired: 'Paylaşım bağlantısının süresi doldu'
|
||||
ErrFileSharePassword: 'Yanlış paylaşım parolası'
|
||||
ErrFileShareExhausted: 'Bu paylaşım için indirme sınırına ulaşıldı'
|
||||
ErrFileSharePasswordPolicy: 'Parola en az 4 karakter olmalıdır'
|
||||
ErrFileShareRateLimit: 'Çok fazla istek gönderildi, lütfen daha sonra tekrar deneyin'
|
||||
ErrCmdNotFound: '{{ .name }} komutu mevcut değil, lütfen önce bu komutu host a yükleyin'
|
||||
ErrSourcePathNotFound: 'Kaynak dizin mevcut değil'
|
||||
ErrFavoriteExist: 'Bu yol zaten favorilere eklendi'
|
||||
@@ -148,6 +166,7 @@ ErrSSLValid: 'Sertifika dosyası anormal, lütfen sertifika durumunu kontrol edi
|
||||
ErrWebsiteDir: 'Lütfen web sitesi dizini içindeki bir dizin seçin.'
|
||||
ErrComposerFileNotFound: 'composer.json dosyası mevcut değil'
|
||||
ErrRuntimeNoPort: 'Çalışma zamanı ortamı bir porta sahip değil, lütfen önce çalışma zamanı ortamını düzenleyin.'
|
||||
ErrRuntimeProjectDirContainsCodeDir: 'Çalışma zamanı dizini, seçilen proje dizinini içeriyor. Lütfen başka bir proje dizini seçip yeniden oluşturun.'
|
||||
Status: 'Durum'
|
||||
start: 'Başlat'
|
||||
stop: 'Durdur'
|
||||
@@ -169,7 +188,7 @@ ErrSSLCertificateNotFound: 'Sertifika dosyası mevcut değil'
|
||||
ErrSSLKeyFormat: 'Özel anahtar dosyası doğrulama başarısız'
|
||||
ErrSSLCertificateFormat: 'Sertifika dosyası formatı yanlış, lütfen pem formatını kullanın'
|
||||
ErrEabKidOrEabHmacKeyCannotBlank: 'EabKid veya EabHmacKey boş olamaz'
|
||||
ErrOpenrestyNotFound: 'Http modu önce Openresty yüklenmesini gerektiriyor'
|
||||
ErrOpenrestyNotFound: 'HTTP modu için OpenResty gereklidir'
|
||||
ApplySSLStart: 'Sertifika başvurusu başlatılıyor, alan adı [{{ .domain }}] başvuru yöntemi [{{ .type }}]'
|
||||
dnsAccount: 'DNS Otomatik'
|
||||
dnsManual: 'DNS Manuel'
|
||||
@@ -343,10 +362,10 @@ sqlInject: 'SQL enjeksiyonu'
|
||||
ErrDBNotExist: 'Veritabanı mevcut değil'
|
||||
allow: 'izin ver'
|
||||
deny: 'reddet'
|
||||
OpenrestyNotFound: 'Openresty yüklü değil'
|
||||
OpenrestyNotFound: 'OpenResty yüklü değil'
|
||||
remoteIpIsNull: 'IP listesi boş'
|
||||
OpenrestyVersionErr: 'Openresty sürümü çok düşük, lütfen Openresty''i 1.27.1.2-2-2-focal olarak güncelleyin'
|
||||
ErrFileTooLarge: 'File is larger than 1 MB and cannot be uploaded'
|
||||
OpenrestyVersionErr: 'OpenResty sürümü çok eski. 1.27.1.2-2-2-focal sürümüne yükseltin'
|
||||
ErrFileTooLarge: 'Dosya 1 MB''den büyük olduğu için yüklenemiyor'
|
||||
TaskStart: '{{ .name }} görevi başlıyor [BAŞLAT]'
|
||||
TaskEnd: '{{ .name }} Görev tamamlandı [TAMAMLANDI]'
|
||||
TaskFailed: '{{ .name }} görev başarısız'
|
||||
@@ -379,7 +398,7 @@ App: 'Uygulama'
|
||||
Runtime: 'Çalışma ortamı'
|
||||
Database: 'Veritabanı'
|
||||
ConfigFTP: 'FTP kullanıcısı {{ .name }} oluştur'
|
||||
ConfigOpenresty: 'Openresty yapılandırma dosyası oluştur'
|
||||
ConfigOpenresty: 'OpenResty yapılandırma dosyası oluştur'
|
||||
InstallAppSuccess: 'Uygulama {{ .name }} başarıyla yüklendi'
|
||||
ConfigRuntime: 'Çalışma ortamını yapılandır'
|
||||
ConfigApp: 'Uygulama Yapılandırması'
|
||||
@@ -515,3 +534,4 @@ TerminalAIBlockedRiskyCommand: 'Riskli komut engellendi: {{ .command }}'
|
||||
TerminalAIThinking: 'AI dusunuyor...'
|
||||
TerminalAIReadyToExecute: 'Dusunme tamamlandi, calistirmak icin Enter tusuna basin (sure: {{ .duration }}, token: {{ .tokens }})'
|
||||
TerminalAIRequestFailed: 'AI istegi basarisiz oldu: {{ .err }}'
|
||||
FileAISearchEmptyDir: 'Bu yol altında listelenecek dosya veya klasör yok (veya tümü filtrelendi).'
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user