Compare commits

..
Author SHA1 Message Date
wanghe-fit2cloud 3a888d289f fix app image pull check 2026-06-12 14:10:20 +08:00
王贺 dbff9c02e9 fix: improve login page compatibility (#13029) 2026-06-12 11:45:14 +08:00
ssongliu 45c5c5b40f fix: resolve SSH key generation issue (#13027) 2026-06-12 11:17:08 +08:00
CityFun aea71664ee feat: add some translate (#13019) 2026-06-12 11:13:49 +08:00
ssongliu fc65fb9323 fix: fix monitoring tooltip styles (#13017) 2026-06-11 17:26:00 +08:00
ssongliu f476823531 fix: correct node administrator permissions (#13016) 2026-06-11 17:25:47 +08:00
CityFun 8222579d99 feat: update website ssl apply logic (#13015) 2026-06-11 17:25:34 +08:00
蘭 209c126445 ref: update visibility logic alert configuration (#13007) 2026-06-11 14:12:53 +08:00
蘭 c846ca3e71 feat: add methodInvalid translation for alert configuration (#13005) 2026-06-11 12:24:29 +08:00
ssongliu e777fa143e fix: add middleware whitelist (#13004) 2026-06-11 12:24:17 +08:00
蘭 e66000ffd2 feat: improve alert config handling (#13001) 2026-06-10 21:48:54 +08:00
蘭 cfdf448765 feat: add displayName to alert configuration and update related logging (#13000) 2026-06-10 19:08:37 +08:00
CityFun c61139c5cc fix: optimize sync apps button display logic (#12999) 2026-06-10 18:45:32 +08:00
ssongliu 571a4068c4 feat: add missing operation logs (#12998) 2026-06-10 18:30:23 +08:00
CityFun 9ce63adb4f feat: add some translate (#12996)
* feat: add some translate

* feat: add some translate
2026-06-10 18:30:07 +08:00
蘭 ef51a2f9b2 style: adjust width of input help text for better alignment (#12995) 2026-06-10 18:29:54 +08:00
蘭 fa3e137db8 fix: prevent reading of binary preview files and improve file type handling (#12984) 2026-06-10 16:28:16 +08:00
蘭 af6a708049 fix: simplify error messages for unsupported alert methods (#12983) 2026-06-10 16:28:03 +08:00
ssongliu 5d6ccf5717 chore: refine multi-node upgrade text (#12982) 2026-06-10 14:29:37 +08:00
蘭 59c870aca4 fix: refine alert methods (#12980) 2026-06-09 18:53:51 +08:00
CityFun 94cb014598 chore: update dependencies (#12979) 2026-06-09 18:53:37 +08:00
ssongliu ed30567a22 fix: resolve custom login background image issue (#12977) 2026-06-09 17:28:36 +08:00
ssongliu 791350c299 chore: improve API documentation and logs (#12962) 2026-06-09 14:30:43 +08:00
ssongliu 6547de1758 fix: improve firewall port occupancy display (#12961)
* c

* fix: improve firewall port occupancy display
2026-06-09 14:18:54 +08:00
蘭 2151daab1f fix: Fix duplicate alert issues (#12960) 2026-06-09 11:29:28 +08:00
CityFun b791def0df chore: update dependencies (#12959) 2026-06-09 11:29:15 +08:00
ssongliu c036d5859f style: adjust overview memo style (#12958) 2026-06-09 11:28:55 +08:00
ssongliu 6495869664 fix: fix firewall whitelist rules not applied after restart (#12957) 2026-06-09 11:27:38 +08:00
蘭 a2e6e7e879 ref: update alert logging methods and improve alert configuration handling (#12955) 2026-06-08 21:47:47 +08:00
CityFun 338301907a feat: add some translate (#12954) 2026-06-08 19:01:19 +08:00
ssongliu 681141f971 feat: add license import warning message (#12952) 2026-06-08 19:01:04 +08:00
bin64 506ff1d46e fix: preserve custom rewrite template name case (#12714)
* refactor: enhance rewrite configuration handling by introducing safe name validation and custom rewrite existence checks

* test: cover custom rewrite name handling
2026-06-08 17:33:45 +08:00
bin64 c3b7deedb4 fix: parse supervisor uptime with optional days segment (#12713) 2026-06-08 17:33:26 +08:00
ssongliu 51252a15db feat: support port usage check for firewall port range rules (#12950) 2026-06-08 14:57:50 +08:00
ssongliu 375824f77a fix: relocate firewall port whitelist settings (#12949) 2026-06-08 14:24:24 +08:00
Rowan Chen 8918d10253 chore(deps): bump go-acme/lego to v5.2.2 (#12947)
lego v5.2.2 (released 2026-06-02) is a single-fix patch on top of v5.2.1: the Namecheap DNS provider now derives the record key sub-domain correctly. 1Panel exposes the Namecheap provider through agent/utils/ssl/dns_provider.go, so this patch is meaningful for users issuing certificates against Namecheap-hosted zones.

Changes are confined to agent/go.mod and agent/go.sum; the source files under agent/utils/ssl/ already use the import path github.com/go-acme/lego/v5/... and require no code changes. Diff is a 6-line dependency bump (1 line in go.mod plus 4 lines of refreshed go.sum hashes); lego itself did not move any of its own dependency pins between v5.2.1 and v5.2.2.

Built and verified on linux/amd64:

  GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' ./...

agent and core binaries link cleanly against the upgraded lego release; no source-level adaptations needed.
2026-06-08 14:20:21 +08:00
ssongliu fe343a64ed feat: adjust captcha verification method (#12946) 2026-06-05 18:50:11 +08:00
蘭 205f12e14a feat: add display name support for SMS alerts and enhance validation (#12945) 2026-06-05 18:49:58 +08:00
ssongliu aca94e470a style: optimize dark theme styles (#12944) 2026-06-05 18:49:48 +08:00
蘭 a69f7b1144 feat: add proxy support for file wget downloads (#12942) 2026-06-05 18:49:34 +08:00
蘭 e55abfb174 feat: implement range selection for table rows with shift key support (#12941) 2026-06-05 14:49:16 +08:00
CityFun 4a734b5bb6 feat: add some translate (#12940) 2026-06-05 14:48:53 +08:00
ssongliu 758eb9dfda style: optimize container log display (#12939) 2026-06-05 14:48:38 +08:00
ssongliu 04e2770763 style: optimize disabled button styles in tables (#12937) 2026-06-05 14:48:20 +08:00
ssongliu 7391b4bcd0 fix: resolve issues with abnormal operation logs (#12936) 2026-06-05 14:48:07 +08:00
蘭 d9d4d55eca fix: Fix some issues with alarms and reports (#12934) 2026-06-04 19:48:14 +08:00
ssongliu 9a64f8de98 feat: show agents and certificates in node overview (#12933) 2026-06-04 17:32:22 +08:00
ssongliu 9f9c9a2688 chore: adjust partial i18n content (#12931) 2026-06-03 18:18:43 +08:00
CityFun f80f4d1c19 feat: add some translate (#12930) 2026-06-03 18:11:55 +08:00
蘭 145b39c4c0 ref: Remove unnecessary references and improve international content (#12927)
* ref: Remove unnecessary references and improve international content

* ref: Code formatting

* ref: Code formatting
2026-06-03 16:41:26 +08:00
ssongliu 44e264fe65 feat: optimize node switch logic for large node sets (#12928) 2026-06-03 16:36:42 +08:00
蘭 15fef4d84e feat: add alert database initialization and migration steps (#12925) 2026-06-03 10:49:04 +08:00
ssongliu b8bd1490ec fix: remove offline settings from panel settings (#12924) 2026-06-03 10:47:24 +08:00
ssongliu ccd8923fdd feat: support multi-node batch installation in Skills Hub (#12923) 2026-06-03 09:37:50 +08:00
蘭 e49fa620c8 feat: Support adding multiple alert methods (#12922)
* feat: Support adding multiple alert methods

* feat: Support adding multiple alert methods
2026-06-02 22:35:21 +08:00
ssongliu 654691543c feat: support multi-node batch installation in Skills Hub (#12921) 2026-06-02 22:25:05 +08:00
ssongliu 929cd38184 docs: adjust API documentation content (#12920) 2026-06-02 16:31:06 +08:00
ssongliu 9748a0794b refactor: adjust login settings logic (#12916) 2026-06-02 15:13:59 +08:00
王贺 a3f8c30508 fix api annotations (#12915)
* fix api annotations

* complete 1panel api docs

* add operation logs for api docs
2026-06-02 15:11:43 +08:00
CityFun 602c0e8a3b feat: add some translate (#12913) 2026-06-01 18:28:03 +08:00
ssongliu 7b7f840c2e feat: support firewall port whitelist management (#12912) 2026-06-01 18:26:54 +08:00
Rowan Chen ba9a8592ee chore(deps): bump go-acme/lego to v5.2.1 (#12909)
lego v5.2.1 (released 2026-06-01) is a small follow-up patch on top of v5.2.0; the only fixed item is a CLI/migration ergonomics tweak (printing the suggested configuration when the file cannot be created) that does not affect 1Panel, but moving to the latest tag keeps us on a published release rather than the previous one.

Changes are confined to agent/go.mod and agent/go.sum; the source files under agent/utils/ssl/ already use the import path github.com/go-acme/lego/v5/... and require no code changes. Compared with the previous v5.2.0 step, this revision is a 6-line dependency bump (1 line in go.mod plus 4 lines of refreshed go.sum hashes), since lego itself did not move any of its own dependency pins between v5.2.0 and v5.2.1.

Indirect dependency bumps (carried over from the v5.2.0 -> v5.2.1 rebase, produced by 'go mod tidy' on a fresh checkout):

- alibabacloud-go/darabonba-openapi v2.1.16 -> v2.2.1, alibabacloud-go/tea v1.4.0 -> v1.5.0 (alidns provider chain)

- aws-sdk-go-v2 family v1.41.7 -> v1.41.8 plus matching service modules (route53 provider chain)

- baidubce/bce-sdk-go v0.9.266 -> v0.9.267, huaweicloud/huaweicloud-sdk-go-v3 v0.1.197 -> v0.1.198, tencentcloud-sdk-go v1.3.102 -> v1.3.106, volcengine/volc-sdk-golang v1.0.248 -> v1.0.249

- go-acme/alidns-20150109 major bump v4 -> v5, go-acme/esa-20240910 major bump v2 -> v3 (required by lego v5.2.x directly)

- fsnotify/fsnotify v1.9.0 -> v1.10.1 (also targeted by dependabot PRs #12864 / #12865)

Built and verified on linux/amd64:

  GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -trimpath -ldflags='-s -w' ./...

agent and core binaries link cleanly against the upgraded lego and indirect dependencies; no source-level adaptations needed.
2026-06-01 17:42:24 +08:00
ssongliu 389fcbf10c fix: resolve delayed theme update after license import (#12911) 2026-06-01 13:52:54 +08:00
CityFun 9cca14c382 feat: add some translate (#12904) 2026-05-29 22:06:26 +08:00
ssongliu 243a2fe6f3 fix: fix SSH service auto-enable issue (#12898) 2026-05-29 22:06:09 +08:00
ssongliu 3d5069fd40 fix: preserve container IP after upgrade (#12902) 2026-05-29 18:25:02 +08:00
ssongliu 2b78bae451 fix: fix terminal connection failure for overview nodes (#12901) 2026-05-29 16:38:56 +08:00
ssongliu 8693cc17df fix: fix backup retention count not taking effect during script updates (#12896) 2026-05-29 11:41:57 +08:00
蘭 9a2b7a7775 ref: Code formatting (#12894) 2026-05-28 18:19:40 +08:00
蘭 024a4a9ef3 feat: alert better visibility control (#12893)
* feat: alert better visibility control

* feat: alert better visibility control
2026-05-28 18:04:41 +08:00
ssongliu f5b47cf74d fix: fix ClamAV scan failure issue (#12892) 2026-05-28 18:04:29 +08:00
蘭 4956b96193 ref: add some translate (#12891) 2026-05-28 18:04:16 +08:00
CityFun a332dfd3b3 fix: Fix the issue where runtime environments cannot be created when using a custom app repository. (#12887) 2026-05-28 11:54:02 +08:00
蘭 68f8d8d221 feat: add auto export feature for Ops Report (#12885) 2026-05-28 11:26:00 +08:00
王贺 4f78060d35 fix: tighten rbac and release resources (#12886) 2026-05-28 11:09:00 +08:00
ssongliu adb97730c1 fix: fix wildcard file copy issue. (#12883) 2026-05-28 10:30:38 +08:00
蘭 f1a0453615 fix: Fix some bugs in the operation and maintenance report (#12881) 2026-05-28 10:30:26 +08:00
CityFun e64cc875fe feat: add some translate (#12880) 2026-05-27 18:24:21 +08:00
ssongliu b1c028b786 fix: resolve node auto-upgrade failure (#12875) 2026-05-27 16:49:24 +08:00
ssongliu d0e3914a29 fix: resolve ssh service startup failure after enable (#12874) 2026-05-27 16:42:30 +08:00
蘭 fe7d1108cd ref: update dependencies for gopdf and gofpdi (#12873) 2026-05-27 15:07:42 +08:00
蘭andcopilot-swe-agent[bot] 462a3bbc83 feat: add node selection reports (#12871)
* feat: add node selection reports

* Merge dev-v2 and resolve PR conflicts

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
2026-05-27 14:09:50 +08:00
ssongliu b9dff99d63 feat: add login user display to login records (#12870) 2026-05-27 13:44:14 +08:00
崔健壮 612e67e4cc fix(ssl): keep panel certificate in sync after auto-renew (#12858)
The auto-renew flow in obtainSSL returned early when OpenResty was not
installed or `nginx -s reload` failed, skipping reloadSystemSSL. The new
certificate was persisted to the DB and written into website Nginx
configs, but the panel's own server.crt / server.key on disk and the
in-memory constant.CertStore were left pointing at the old material.
Because the cert was now fresh, subsequent cron ticks did not retry the
renewal, so the panel kept serving the stale cert until a user manually
re-applied it from 面板设置 → SSL.

Two changes:

1. agent/app/service/website_ssl.go
   reloadSystemSSL is now called unconditionally after a successful
   renewal, regardless of whether OpenResty is present or nginx reload
   succeeded. The function already short-circuits for non-panel SSLs,
   so this is safe.

2. agent/app/service/website_ssl.go + agent/cron/job/ssl.go
   Add SyncSystemSSL, invoked at the start of every renew cron tick.
   It compares the panel's on-disk cert/key with the WebsiteSSL row
   referenced by the SSLID setting and rewrites the files + notifies
   core when they diverge. This recovers existing installs that are
   already in the "DB ahead of disk" state and self-heals any future
   drift introduced by transient failures.
   https://github.com/1Panel-dev/1Panel/issues/12472
2026-05-27 11:58:40 +08:00
ssongliu 4f0838c98c fix: fix script library execution issue on child nodes (#12869) 2026-05-27 11:25:24 +08:00
ssongliu fe70a77264 feat: support auto popup task log after image deletion (#12868) 2026-05-27 11:25:02 +08:00
glmgbj233andssongliu 8ed33fd06a feat: validate generated terminal commands (#12826)
* Fix taint path 1 detected by Codex

Repository: 1Panel-dev_1Panel
Result: /home/hejunjie/llm_web_serve/find_github_project/codex_find_taint/batch_results_go_llm_full_mini/1Panel-dev_1Panel.json
Sink kind: command_exec

* Fix taint path 0 detected by Codex

Repository: 1Panel-dev_1Panel
Result: /home/hejunjie/llm_web_serve/find_github_project/codex_find_taint/batch_results_go_llm_full_mini/1Panel-dev_1Panel.json
Sink kind: command_exec

* Validate generated terminal commands

Keep generated output behind the intended trust boundary while preserving the normal safe workflow.

Add regression coverage for the unsafe flow and the expected safe behavior.

* Narrow terminal AI input validation

* refine terminal ai command validation

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-05-27 11:08:52 +08:00
ssongliu f5662769b3 fix: update node limit handling (#12867) 2026-05-27 09:46:00 +08:00
ssongliu 61a2e20798 fix: handle node admin action permissions (#12857) 2026-05-26 18:34:09 +08:00
ssongliu 7d20118f38 feat: add router expires alert (#12855) 2026-05-26 17:28:26 +08:00
CityFun 7307b9d6fb feat: Upgrade some dependencies. (#12854) 2026-05-26 15:25:08 +08:00
ssongliu c2971812a5 fix: align user list model and local dev config (#12853) 2026-05-26 14:31:24 +08:00
Rowan Chen 176a4bd34c Merge pull request #12829 from rowanchen-com/feat/lego-v5-and-dynadot
feat: upgrade lego to v5.1.0 and add Dynadot DNS provider
2026-05-26 14:18:17 +08:00
ssongliu 4a01726058 feat: guard file editor save and refresh ops report docs (#12852) 2026-05-26 13:48:19 +08:00
王贺 18a372e027 Update OWNERS (#12851) 2026-05-26 10:28:49 +08:00
蘭 483a2a867b feat: Optimize the export page of the operation and maintenance report (#12850) 2026-05-26 10:26:34 +08:00
ssongliu 1a0de94489 feat: update task list and sidebar behavior (#12839) 2026-05-26 09:33:52 +08:00
ssongliu 979c1588e1 fix: improve command output handling (#12837) 2026-05-26 09:33:41 +08:00
ssongliu e39f6b1003 feat: add setting permission controls (#12838) 2026-05-25 17:13:02 +08:00
mei2jun1 4443ed1b7f fix: nil pointer dereference when os.OpenFile fails (#12808) 2026-05-25 14:11:10 +08:00
ssongliu 81f11f13fe chore: remove unused docker compose dependency (#12835) 2026-05-25 11:38:42 +08:00
王贺 4fa14f055d fix: harden command execution helpers (#12834) 2026-05-25 11:38:31 +08:00
王贺 0fe9033d23 fix: update community edition auth defaults (#12828) 2026-05-23 21:17:28 +08:00
ssongliu e5ff53b1af fix: embed all web assets (#12827) 2026-05-23 12:46:20 +08:00
ssongliu d005e61c72 fix: update go sum (#12825) 2026-05-23 10:45:45 +08:00
王贺 9328a79b37 fix: update agent go sum (#12824) 2026-05-23 10:15:51 +08:00
ssongliu 56c7d5e3bd fix: refine rbac permission controls (#12821) 2026-05-23 09:44:11 +08:00
CityFun 0cefe8f6ad feat: Update some dependencies (#12820) 2026-05-22 22:37:08 +08:00
蘭 3759fd2dad feat: enhance ops report functionality and improve export options (#12819) 2026-05-22 18:17:01 +08:00
王贺 b35c771d16 fix: update mcp nav title (#12818) 2026-05-22 18:16:37 +08:00
王贺 3ce70cee4d chore: update skills hub locale (#12812) 2026-05-22 15:18:14 +08:00
CityFun 8dd27a3e7b feat: Update some dependencies (#12811) 2026-05-22 15:17:59 +08:00
CityFun c91769e364 feat: add some translate (#12806) 2026-05-21 18:20:06 +08:00
ssongliu ea7856d591 fix: update commercial edition i18n (#12804) 2026-05-21 18:19:52 +08:00
ssongliu 023040a814 fix: improve ssh log parsing (#12803) 2026-05-21 15:26:41 +08:00
王贺 13fdb6caa9 chore: update (#12802) 2026-05-21 14:52:39 +08:00
CityFun 81e1a41130 fix: Fixed issue with start frontend failed (#12800) 2026-05-21 14:42:45 +08:00
CityFun f3933155d6 Merge pull request #12799 from 1Panel-dev/dev-duo
merge to dev-v2
2026-05-21 12:41:48 +08:00
ssongliu 4e692b8667 fix: adjust file search layout (#12797) 2026-05-21 12:39:50 +08:00
蘭 cf5f1cdb4e feat: Operation and maintenance reports support alarm reports (#12794) 2026-05-21 12:39:50 +08:00
CityFun 46986fba24 fix: Resolve the agent upgrade issue (#12780) 2026-05-21 12:39:50 +08:00
ssongliu 4627e7c0af chore: add permission controls (#12775) 2026-05-21 12:39:50 +08:00
ssongliu 38174a0b9f fix: adjust permission dropdown behavior (#12774) 2026-05-21 12:39:50 +08:00
蘭 c0489245d9 feat: Optimize the UI and style of operation and maintenance reports (#12772) 2026-05-21 12:39:50 +08:00
王贺 bec23f793a fix: harden agent socket and permission checks (#12771)
* fix: harden agent socket and permission checks

* chore: remove agent socket test

* chore: remove redundant comments
2026-05-21 12:39:49 +08:00
ssongliu e7578a9fc5 fix: improve security entrance and user commands (#12770) 2026-05-21 12:39:49 +08:00
王贺 9d3313fe11 fix: allow refreshing skills hub routes (#12767) 2026-05-21 12:39:49 +08:00
ssongliu f41e5e3da5 chore: update website log docs (#12763) 2026-05-21 12:39:49 +08:00
ssongliu 540be68adf chore: update website log docs (#12762) 2026-05-21 12:39:49 +08:00
ssongliu 956bf0992a feat: split website log APIs (#12761) 2026-05-21 12:39:49 +08:00
ssongliuandCityFun d0faee4eeb feat: improve frontend permission handling (#12760)
* feat: change isProductPro logic (#12712)

* fix: tighten frontend RBAC permission guards (#12717)

* fix: tighten frontend RBAC permission guards

* feat: add permission directive coverage

* refactor frontend global store usage

* fix: harden file access and fallback handling

* feat: improve frontend permission handling

---------

Co-authored-by: CityFun <31820853+zhengkunwang223@users.noreply.github.com>
2026-05-21 12:39:49 +08:00
王贺 4517ae2f81 feat: support enterprise local skills hub (#12758) 2026-05-21 12:39:49 +08:00
CityFun c77260193f feat: Modify the model provider loading logic when creating an agent (#12757) 2026-05-21 12:39:49 +08:00
ssongliuandCityFun d57c998047 fix: serve frontend routes with fallback (#12752)
* feat: change isProductPro logic (#12712)

* fix: tighten frontend RBAC permission guards (#12717)

* fix: tighten frontend RBAC permission guards

* feat: add permission directive coverage

* refactor frontend global store usage

* serve frontend routes with fallback

---------

Co-authored-by: CityFun <31820853+zhengkunwang223@users.noreply.github.com>
2026-05-21 12:39:49 +08:00
CityFun 3a5d5bc6d7 feat: add some translate (#12746) 2026-05-21 12:39:49 +08:00
王贺 00afa748e0 feat: add ops report foundation (#12745)
* feat: add ops report foundation

* chore: restore local vite proxy

* chore: restore package lock

* chore: move ops report i18n scope

* chore: remove dashboard permission label

* chore: nest ops report i18n

* chore: update ops report menu i18n key

* chore: sync permission locale cleanup
2026-05-21 12:39:49 +08:00
ssongliu 8c9c7b9b1f chore: add license product i18n (#12739) 2026-05-21 12:39:49 +08:00
ssongliu 1e2dc42554 chore: update enterprise resource URL (#12735) 2026-05-21 12:39:49 +08:00
ssongliu cd621b9dba chore: add permission linkage i18n (#12732) 2026-05-21 12:39:49 +08:00
ssongliu d10afe7a5a chore: update node upgrade translations (#12730) 2026-05-21 12:39:49 +08:00
ssongliu 03fc9302d6 fix: align frontend permission checks with access mode (#12723) 2026-05-21 12:39:49 +08:00
CityFun d522d837ff feat: add icon for ai provider (#12722) 2026-05-21 12:39:49 +08:00
ssongliu aa10760658 feat: sync panel updates (#12721) 2026-05-21 12:39:49 +08:00
ssongliu db6e8841ec fix: adjust container action permission checks (#12718) 2026-05-21 12:39:49 +08:00
ssongliu 75258bb465 fix: tighten frontend RBAC permission guards (#12717)
* fix: tighten frontend RBAC permission guards

* feat: add permission directive coverage

* refactor frontend global store usage
2026-05-21 12:39:47 +08:00
CityFun 7c7a59cf21 feat: add some translate (#12716) 2026-05-21 12:37:33 +08:00
CityFun 6f38c4eb71 feat: change isProductPro logic (#12712) 2026-05-21 12:37:33 +08:00
CityFun 7abedeae22 feat: update some plugin (#12709) 2026-05-21 12:37:33 +08:00
CityFun afacefaccb feat: Add AI agent and benchmarking page URLs (#12696) 2026-05-21 12:37:33 +08:00
ssongliu e5b2b5d5dc fix: pass context to rar extraction and simplify sidebar message entry (#12697) 2026-05-21 12:37:33 +08:00
ssongliu 9508238805 feat: 修改 AI 代理的菜单位置 (#12695) 2026-05-21 12:37:33 +08:00
ssongliu 2c21bd7286 fix: handle stream auth responses (#12693) 2026-05-21 12:37:33 +08:00
CityFun 811ddcc26c fix: Fixed issue with install openclaw plugin failed (#12690) 2026-05-21 12:37:33 +08:00
ssongliu 6cd0373678 fix enterprise user cli updates (#12689) 2026-05-21 12:37:33 +08:00
ssongliu 11771b2b53 fix: improve ai benchmark i18n (#12686) 2026-05-21 12:37:33 +08:00
CityFun ff5c6e878e feat: add some translate (#12678) 2026-05-21 12:37:33 +08:00
ssongliu 1896aed973 feat: update host tools and settings APIs (#12674) 2026-05-21 12:37:32 +08:00
ssongliu 6dc5deb6d3 feat: support enterprise resource urls (#12669) 2026-05-21 12:37:32 +08:00
ssongliu d7242d526c feat: update mfa permissions and panel settings (#12665) 2026-05-21 12:37:32 +08:00
zhengkunwang223 22c3fc8c40 feat: add ai benchmark 2026-05-21 12:37:32 +08:00
ssongliu 8d44105f88 feat: normalize mfa and api config fields (#12642) 2026-05-21 12:37:32 +08:00
ssongliu 8fb8d97dcf feat: add offline environment setting (#12637) 2026-05-21 12:37:31 +08:00
ssongliu 6941014153 fix: refine enterprise license required flow (#12634)
* fix: adjust access control menu (#12633)

* fix: refine enterprise license required flow
2026-05-21 12:35:11 +08:00
ssongliuandCopilot bfd610d255 fix: adjust access control menu (#12633)
Co-authored-by: Copilot <copilot@github.com>
2026-05-21 12:35:11 +08:00
ssongliu 5c61217c78 chore: update xpack integration references (#12628) 2026-05-21 12:35:10 +08:00
ssongliu eb527857f3 fix: avoid license required before login (#12626) 2026-05-21 12:35:10 +08:00
ssongliu d55982bde0 refactor: update agent and core utilities (#12621) 2026-05-21 12:35:09 +08:00
zhengkunwang223 84a27de792 feat: add ai-proxy 2026-05-21 12:32:54 +08:00
ssongliu 111bec547c fix: avoid directives on non-element roots (#12597) 2026-05-21 12:32:54 +08:00
ssongliu db4f699b85 feat: record user in operation logs (#12596) 2026-05-21 12:32:54 +08:00
ssongliu 12f2d95265 chore: update license required handling (#12594) 2026-05-21 12:32:54 +08:00
ssongliu a917ca00a1 feat: refactor auth and xpack integration 2026-05-21 12:32:52 +08:00
ssongliu 69906046e8 feat: add xpackee permission-aware settings flow 2026-05-21 11:34:08 +08:00
ssongliu d9cedeca09 feat: update xpack integration behavior 2026-05-21 11:34:08 +08:00
ssongliu 0e28f27819 feat: Support multi-user login and node management 2026-05-21 11:34:06 +08:00
ssongliu 33fc7f14df fix: update license pro translations (#12781) 2026-05-20 18:38:21 +08:00
CityFun 7159aca226 fix: Fix the issue where deleting the host mapping in the runtime environment causes an error." (#12779) 2026-05-20 14:58:35 +08:00
ssongliu 2c5728e27e fix: add license free node count message (#12778) 2026-05-20 14:30:17 +08:00
ssongliu defb40702c fix snapshot recover tar extraction (#12777) 2026-05-20 14:12:54 +08:00
ssongliu b476df3b61 chore: update pro edition copy (#12776) 2026-05-20 13:55:04 +08:00
蘭 4bbd9b6f06 fix: Fix the issue of deleting project directories caused by failed creation of the running environment (#12756) 2026-05-18 18:08:13 +08:00
蘭 325b9c4d88 fix: Fix directory issue where file management loading does not exist (#12744) 2026-05-15 17:57:47 +08:00
蘭 76d965b3b7 feat: enhance file management UI and improve selection handling (#12740) 2026-05-15 11:35:15 +08:00
KOMATA 4b1b27abd8 feat: enhance upgrade process with space check and file handling (#12708)
* feat: enhance upgrade process with space check and file handling improvements

* fix: update minimum upgrade free space requirement to 500MB and simplify space check logic
2026-05-15 11:29:35 +08:00
CityFun f26b4f290d feat: add some translate (#12738) 2026-05-15 11:25:50 +08:00
蘭 5d2221203a fix: optimize directory size calculation with concurrency control (#12737) 2026-05-15 11:25:35 +08:00
蘭 0aff0d529e fix: improve error handling and refactor file history content retrieval (#12736) 2026-05-15 11:25:16 +08:00
CityFun 20f09b3a0c feat: add model downloader (#12694) 2026-05-09 17:17:34 +08:00
蘭 c392b72470 fix: Fix file decompression issue (#12684)
Refs #12675
2026-05-08 06:42:21 +00:00
王贺 50a70ef1e5 fix: skip large binary file history snapshots (#12683)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12681

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-05-08 06:40:22 +00:00
ssongliu 75266ef659 fix: pause home carousel while editing memo (#12680)
Refs #12679
2026-05-08 02:18:20 +00:00
王贺 b3c593b852 fix openwrt procd init script selection (#12670) 2026-05-07 18:28:30 +08:00
ssongliu 73530bb8b9 fix: route terminal websocket by operate node (#12676) 2026-05-07 18:28:17 +08:00
王贺 1b6e70964b fix: preserve upload file permissions (#12672) 2026-05-07 18:26:39 +08:00
王贺 61d42b05e9 fix: optimize dashboard monitor chart (#12673) 2026-05-07 18:25:27 +08:00
CityFun 79703e3c71 fix: resolve missing arrow in compose file comparison during app upgrade (#12663) 2026-05-06 07:38:15 +00:00
ssongliu aee4e4e282 fix: adjust sidebar collapse popover (#12662) 2026-05-06 07:36:15 +00:00
CityFun 744c7559f3 feat: add package-lock.json (#12661) 2026-05-06 14:47:56 +08:00
ssongliu d94c3ec9cc fix: adjust frontend panel sizing (#12660) 2026-05-06 06:22:15 +00:00
ssongliu f2625eab16 fix: refactor local agent proxy handling (#12659) 2026-05-06 06:20:14 +00:00
CityFun c6d498a9be style: remove table column sorting (#12658) 2026-05-06 03:26:14 +00:00
Sanjay Santhanam b06bc0a455 fix(cmd): use exec.LookPath in Which() so detection works without external 'which' (#12651)
cmd.Which() previously shelled out to `which <name>` to determine whether
a binary was on PATH. On distributions that do not ship a `which` package
by default — Arch Linux is the canonical example, but the same applies to
several minimal container images — `which` itself is missing, so every
call to Which() returned false and 1Panel reported core dependencies
(notably Docker) as 'not installed' even when they were running normally.

Switch the primary path to Go's exec.LookPath, which uses the process
PATH directly and has no external dependency. The original shell-out is
preserved as a fallback so any environment where the agent's PATH
differs from the user's interactive shell PATH (the original reason the
shell-out existed) keeps working unchanged.

Both copies are updated (agent/utils/cmd/cmd.go and core/utils/cmd/cmd.go)
and a small unit test is added to each package to guard the regression.

Fixes #12605

Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
2026-05-06 10:47:02 +08:00
Sanjay Santhanam 68411bddd9 fix(ssl): support IPv6 hosts in panel SSL self-signed certificate flow (#12652)
Enabling Panel SSL with the self-sign provider rejected IPv6 hosts with
"domain format invalid". Two coupled bugs caused this:

1. The frontend extracted the host from window.location.href with
   href.split('//')[1].split(':')[0]. For an IPv6 URL like
   https://[::1]:1234 that yields '[' \u2014 not a valid host \u2014 because
   the second split splits on the first colon inside the bracketed
   address. Use window.location.hostname, which natively returns the
   bracket-stripped IPv6 host.

2. The backend ObtainSSL flow used net.ParseIP(domain) directly. Even if
   the frontend sent the bracketed form ('[::1]'), net.ParseIP rejects
   brackets, so the value flowed into IsValidDomain() and failed the
   regex.

Add common.ParseIPLoose() that accepts both bare and bracketed IPv6 in
addition to bare IPv4. Use it at both call sites in ObtainSSL (renew
path and create path). A unit test guards the regression.

Files:
  - agent/utils/common/common.go               (new ParseIPLoose helper)
  - agent/utils/common/parse_ip_test.go        (12 cases, all green)
  - agent/app/service/website_ca.go            (call sites switched)
  - frontend/src/views/setting/safe/ssl/index.vue
                                                (host extraction fix)

Fixes #12646

Signed-off-by: Sanjay Santhanam <51058514+Sanjays2402@users.noreply.github.com>
2026-05-06 10:36:23 +08:00
Ran a2ac2e0b22 fix(agent/file): correct return type of NewIFileService (#12648)
`NewIFileService` returns the bare struct type `FileService`, but its
body returns a pointer (`&FileService{}`), and the function name suggests
it should return the interface (`IFileService`). The current signature
breaks `go build ./...` on the `agent` module:

    app/service/file.go:95:9: cannot use &FileService{} (value of type
        *FileService) as FileService value in return statement
    app/service/website_proxy.go:276:36: cannot call pointer method
        GetFileList on FileService

Both errors resolve when the constructor returns the interface, since
`*FileService` implements every method on `IFileService` (verified with
`go vet ./...`).

After this change, `go build ./...` and `go vet ./...` are clean on the
`agent` module.
2026-05-06 10:33:04 +08:00
ssongliu a5a5d9f1ac fix: adjust container log download timeout (#12632) 2026-04-29 09:41:53 +00:00
ssongliu 82876eac8d fix: close task log dialog state (#12631)
Refs #12595
2026-04-29 02:43:42 +00:00
ssongliu 19c9c0f4e5 fix: adjust agent terminal height (#12630)
Refs #12611
2026-04-29 02:41:42 +00:00
ssongliu 70f044dc50 fix: adjust home memo empty state (#12629)
Refs #12609
2026-04-29 02:39:41 +00:00
Waynexxxx 1c8418026c fix: clean up docker process in DownloadContainerLogs on timeout (#12607) 2026-04-28 15:47:03 +08:00
CityFun 17f8471c14 fix: Fix the issue where *.domain.com fails to redirect. (#12625) 2026-04-28 07:41:40 +00:00
CityFun 4deb2ea484 feat: add deepseek-v4-pro and deepseek-v4-flash model (#12622)
https://github.com/1Panel-dev/1Panel/issues/12614
2026-04-28 07:39:40 +00:00
蘭 d05317dbf5 feat: enhance VS Code connection instructions and add SSH setup script details (#12613)
#9695
2026-04-28 07:37:42 +00:00
蘭 1db6ccd72b fix: Fix the right-click and filtering function of Monaco editor (#12612)
#12593
2026-04-28 07:35:41 +00:00
ssongliu bc00760404 fix: fix home monitor chart loading (#12591) 2026-04-28 07:33:40 +00:00
蘭 e57dc1240b feat: add stop functionality for decompress tasks and improve UI for task management (#12582) 2026-04-28 07:31:40 +00:00
ssongliu 4c396d68ca fix: simplify login page i18n labels (#12584) 2026-04-23 18:22:51 +08:00
CityFun ab0fe4286a fix: fixed issue with model account invalid in hermes (#12590) 2026-04-23 18:01:54 +08:00
蘭 5ab758d626 fix: Fix the pagination issue in the historical version list (#12583) 2026-04-23 11:20:12 +08:00
王贺 3a0a6537f0 ci: skip add-labels for dependabot PRs
* ci: skip add-labels for dependabot PRs
2026-04-22 17:31:53 +08:00
蘭 4eda92f25c refactor: simplify pagination logic and update theme change listener (#12564) 2026-04-22 16:53:24 +08:00
王贺 94f8c5d2f1 chore: add dependabot config (#12565) 2026-04-22 16:53:12 +08:00
王贺 92598effc1 fix: keep app tags on one line (#12563) 2026-04-22 15:26:38 +08:00
CityFun 748cecaee8 feat: add fu-step (#12562) 2026-04-22 15:26:26 +08:00
王贺 b783034d0a refactor: refine translations (#12561) 2026-04-22 14:13:06 +08:00
CityFun 94a4e39793 feat: remove some config for hermes channel (#12560) 2026-04-22 13:43:24 +08:00
CityFun 1864dee591 style: change file history table style (#12559) 2026-04-22 03:03:01 +00:00
ssongliu 8c4ea85d02 feat: support fxplay resource URLs (#12558) 2026-04-22 03:01:00 +00:00
CityFun a714d5a84e feat: update hermes qq wecom channel config (#12555) 2026-04-21 19:49:32 +08:00
Wu MX 4f97a7890f feat: Enhance container data handling and status synchronization (#12554) 2026-04-21 18:45:50 +08:00
蘭 f15f91e5fa feat: add support for stopping compression tasks and enhance compression functionality (#12553) 2026-04-21 18:40:45 +08:00
CityFun 02bd500917 style: change table style (#12552) 2026-04-21 16:35:34 +08:00
CityFun 810c6fd232 feat: change hermes channel logic (#12551) 2026-04-21 16:35:24 +08:00
ssongliu d50a8983bc fix: adjust home dashboard styles (#12550) 2026-04-21 16:35:15 +08:00
ssongliu 89647304bb fix: adjust home dashboard styles (#12549) 2026-04-21 16:35:04 +08:00
蘭 9ebad13bf8 fix: Fix the error of switching between the current file and the historical version of the opened file (#12548) 2026-04-21 16:34:54 +08:00
王贺 cc0ac8cc43 fix mongodb root password sync (#12547)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 04:00:57 +00:00
王贺 9f5a1d842a fix mongodb password action (#12546)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:58:58 +00:00
王贺 beb33957be fix mongodb upload alert visibility (#12545)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:56:57 +00:00
蘭 ffcbbb705b fix: restore codicons and folding support in Monaco editor (#12544) 2026-04-21 03:54:58 +00:00
王贺 058bc71630 fix mongodb remote edit action (#12543)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:52:58 +00:00
王贺 0a38979bd9 fix mongodb uninstall cleanup (#12542)
#### What this PR does / why we need it?

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-21 03:50:57 +00:00
CityFun 5fa4a15914 style: Optimize the table styling. (#12540) 2026-04-20 22:21:45 +08:00
CityFun 81c551a11b feat: Optimize the change detection for application Compose files. (#12539) 2026-04-20 17:47:52 +08:00
CityFun 5681449353 fix: Optimize the website name badge style. (#12538) 2026-04-20 17:47:42 +08:00
CityFun 88849e6355 fix: Fix the issue where the directory can still be modified while editing the runtime environment. (#12537) 2026-04-20 17:47:31 +08:00
蘭 d6bbcdc32f fix: improve pagination handling and directory naming in recycle bin (#12535) 2026-04-20 06:44:56 +00:00
KOMATA abd1581c1d fix: enhance file and trash services with metadata validation (#12529)
* feat: Enhance file and recycle bin services with metadata handling and validation

* refactor: Rename recycle bin metadata handling to trash info and update related functions

* refactor: Remove orphan trash info pruning logic and related map usage in recycle bin service
2026-04-20 14:11:15 +08:00
CityFun cd30ec7632 feat: support session delete for hermes-agent (#12533) 2026-04-20 06:08:56 +00:00
蘭 646826769a feat: Enhance file sharing functions with optional headers for improved localization support (#12530) 2026-04-20 06:06:56 +00:00
卢亦之 074c0abb3b fix: correct listening status display on the firewall page (#12516)
The bug only appeared when the same process had both TCP and UDP sockets:
every listening port on that process was attributed to whichever protocol
was encountered first. For example, if a process listened on TCP 8000 and
UDP 8001, the UI could wrongly show TCP 8001 as in use while UDP 8001
looked unused.

Use (PID, conn.Type) as the cache key so each protocol has its own
ListeningProcess entry.
2026-04-20 11:27:18 +08:00
ssongliu 3a00e893bc fix: fix psession atomic alignment (#12528) 2026-04-20 03:26:55 +00:00
蘭 2f37677650 fix: Refactor file history management to include current content and improve error handling (#12527) 2026-04-20 02:58:56 +00:00
CityFun 12ae89923b feat: support skill management for hermes-agent (#12514) 2026-04-17 16:36:17 +08:00
蘭 031a203a74 fix: Enhance file history settings with default values and validation (#12512) 2026-04-17 16:05:57 +08:00
蘭 646b99974a feat: File editing supports Dockerfile language (#12511) 2026-04-17 16:05:46 +08:00
蘭 254ed954e9 feat: Add file history management features (#12510) 2026-04-16 19:11:05 +08:00
CityFun a0050dca09 feat: update golang Dependency (#12509) 2026-04-16 17:36:52 +08:00
王贺 cd80b4e6c3 fix(frontend): clean up build warnings (#12508) 2026-04-16 16:53:24 +08:00
CityFun df5d3d782c feat: Optimize the openclaw channel bot add flow (#12507) 2026-04-16 08:52:48 +00:00
CityFun 3c5b3c3467 feat: Optimize the Hermes WeChat login flow (#12506) 2026-04-16 16:42:21 +08:00
Copilotandwan92hen 2bdba42d87 docs: add translation contribution guide (#12505)
* docs: add translation contribution guide with reference PR and checklist

Agent-Logs-Url: https://github.com/1Panel-dev/1Panel/sessions/e6a41d10-557e-4ca1-a78b-18888a9f17be

Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>

* docs: clarify frontend vs backend locale file naming conventions

Agent-Logs-Url: https://github.com/1Panel-dev/1Panel/sessions/e6a41d10-557e-4ca1-a78b-18888a9f17be

Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>

* docs: update reference PR to #10352 (merged Spanish locale)

Agent-Logs-Url: https://github.com/1Panel-dev/1Panel/sessions/2b14f230-892a-412e-baed-07e67614d1e9

Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: wan92hen <27671436+wan92hen@users.noreply.github.com>
2026-04-16 15:00:11 +08:00
CityFun 0b146f7ff3 feat: add unbind website for agent (#12500) 2026-04-15 18:32:46 +08:00
BugKing c28a4f6d69 Remove link to development guideline from CONTRIBUTING.md (#12502)
Removed a link to the development guideline to streamline the contributing process.
2026-04-15 18:32:13 +08:00
CityFun f93a34acc4 fix(ai): Fixed issue with delete hermes-agent failed (#12499) 2026-04-15 08:24:46 +00:00
CityFun 4e78841bf6 feat(ai): support Hermes Agent chat sessions in 1Panel (#12497)
* feat(ai): support Hermes Agent chat sessions in 1Panel

* feat(ai): support Hermes Agent chat sessions in 1Panel

* feat(ai): change docker command
2026-04-15 15:44:20 +08:00
王贺 7f5bb58c72 fix: mongodb restore fixes (#12496)
* fix mongodb restore flow

* chore cleanup i18n and terminal style
2026-04-15 15:26:30 +08:00
王贺 6a28359ac2 fix mongodb route fallback (#12495) 2026-04-15 13:51:20 +08:00
王贺 59655695b5 fix(frontend): correct anti-leech component import path (#12494) 2026-04-15 11:29:27 +08:00
CityFun 786c5c1d7c feat: support hermes-agent (#12493) 2026-04-15 03:10:45 +00:00
KOMATAandssongliu 08388124b3 feat: Migrate to MinIO SDK and enhance S3 client with TLS and context (#12207)
* chore: update dependencies and migrate S3 client to MinIO SDK

- Removed AWS SDK dependency and replaced S3 client implementation with MinIO SDK.
- Updated various dependencies in go.mod and go.sum, including version upgrades for `klauspost/compress`, `minio/minio-go`, and `rs/xid`.
- Adjusted S3 client methods to utilize MinIO's API for bucket listing, object existence checks, uploads, downloads, and deletions.

* refactor: enhance S3 client with context management and TLS support

- Introduced context management with timeouts for S3 client operations to improve reliability.
- Added TLS configuration to support secure connections based on the endpoint scheme.
- Updated S3 client methods to utilize the new context and transport settings for enhanced performance and security.

---------

Co-authored-by: ssongliu <sloooop1x@gmail.com>
2026-04-15 10:00:08 +08:00
王贺 34ccc68b3a fix: clear firewall rule description (#12491)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12488

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-14 10:44:46 +00:00
王贺 97ef386f17 fix: correct tamper english i18n (#12490)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/12482
#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-14 10:42:45 +00:00
王贺 394339d7dc feat: support mongodb management (#12486)
#### What this PR does / why we need it?

Refs https://github.com/1Panel-dev/1Panel/issues/7759

#### Summary of your change

#### Please indicate you've done the following:

- [ ] Made sure tests are passing and test coverage is added if needed.
- [ ] Made sure commit message follow the rule of [Conventional Commits specification](https://www.conventionalcommits.org/).
- [ ] Considered the docs impact and opened a new docs issue or PR with docs changes if needed.
2026-04-14 10:40:44 +00:00
蘭 8362578355 fix: Resolve the issue of QR code sharing files not being able to download (#12485) 2026-04-14 07:12:44 +00:00
CityFun 6dc218cf80 perf: Optimize the frontend architecture. (#12479) 2026-04-13 18:36:28 +08:00
CityFun 89888cef73 feat: Optimize frontend structure (#12475) 2026-04-13 05:48:42 +00:00
ssongliu 4d06f444ac refactor: restore symlink-based xpack integration (#12473) 2026-04-13 10:34:59 +08:00
CityFun 6ea07421ac feat: change feishu channel logic (#12465) 2026-04-10 13:36:25 +08:00
蘭 326d52e311 fix: Fix some issues with sharing files (#12466) 2026-04-10 13:36:10 +08:00
CityFun 6e3f751dba feat: add model name required check for agent (#12463) 2026-04-10 03:30:40 +00:00
ssongliu 6982fd22d4 fix: extend agent role command timeout (#12464) 2026-04-10 03:28:39 +00:00
ssongliu 04fd456b63 fix: silence csrf forbidden toast (#12461) 2026-04-09 22:23:47 +08:00
ssongliu bea4bfeee8 feat: update ai agent config texts 2026-04-09 21:33:16 +08:00
蘭 fa6157b610 feat: enhance AI search functionality with response language support (#12460) 2026-04-09 19:00:26 +08:00
CityFun 30af26779c style: change openclaw channel style (#12459) 2026-04-09 10:48:38 +00:00
CityFun 16d5c00b9a fix: fix issue with openclaw discord config error (#12456) 2026-04-09 17:39:01 +08:00
蘭 3f14f8f30e feat: File management supports link share file (#12453) 2026-04-09 17:38:38 +08:00
ssongliu 218f92a960 fix: fallback to password login when mfa needs captcha (#12438) 2026-04-09 10:28:39 +08:00
ssongliu 75be8d3a23 fix: extend ai agent api timeouts (#12443) 2026-04-09 10:27:28 +08:00
ssongliu e2d8125b3d chore: shorten tamper translations (#12442) 2026-04-09 10:26:41 +08:00
CityFun 7ec3c517c2 feat: Modify the configuration of the OpenClaw DingTalk channel. (#12440) 2026-04-09 10:26:22 +08:00
CityFun e186e30d8c feat: remove deployment type for agent website binding (#12435) 2026-04-09 10:26:00 +08:00
CityFun 67a8ba2421 feat: Add app status sync logic for agent (#12436) 2026-04-08 08:36:40 +00:00
dependabot[bot] 24fc15beaf chore(deps): bump github.com/go-jose/go-jose/v4 in /agent (#12432)
Bumps [github.com/go-jose/go-jose/v4](https://github.com/go-jose/go-jose) from 4.1.3 to 4.1.4.
- [Release notes](https://github.com/go-jose/go-jose/releases)
- [Commits](https://github.com/go-jose/go-jose/compare/v4.1.3...v4.1.4)

---
updated-dependencies:
- dependency-name: github.com/go-jose/go-jose/v4
  dependency-version: 4.1.4
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-04-07 18:12:42 +08:00
王贺 c1fe70b53e chore: align build workflow runtime versions (#12434) 2026-04-07 18:11:20 +08:00
CityFun 30bf4a8d46 feat: add agent website check for delete (#12433) 2026-04-07 09:56:39 +00:00
CityFun 9e501ce02b feat: Modify the certificate auto-renewal logic. (#12430)
Refs https://github.com/1Panel-dev/1Panel/issues/12389
2026-04-07 09:54:38 +00:00
王贺 d74692a68b fix: optimize runtime script dropdown (#12431) 2026-04-07 17:45:09 +08:00
王贺 57fd80fb3d fix: optimize openresty other page layout (#12429) 2026-04-07 17:44:28 +08:00
KOMATA 39d2c34682 fix: ensure proper resource management in file operations (#12358)
* fix: Ensure proper resource management in file operations by closing files after creation and compression

* fix: Improve error handling for log file operations in MediaFile function
2026-04-07 17:35:31 +08:00
CityFun 3e4a48c892 feat: Modify the OpenClaw plugin installation parameters. (#12427) 2026-04-07 07:33:32 +00:00
CityFun 2c8f8f32fd feat: Add default external port access to application settings. (#12426) 2026-04-07 07:31:32 +00:00
maninhill 3cf6d9ac51 Update user count in README 2026-04-07 15:26:42 +08:00
BugKing 64d8fe8bd0 docs: Update README.md 2026-04-07 15:26:13 +08:00
蘭 8129260508 feat: File management supports AI search (#12415) 2026-04-03 22:11:01 +08:00
ssongliu a40786a7fa refactor: clean up ollama pull log parsing (#12414) 2026-04-03 22:10:47 +08:00
CityFun 368193a777 feat: Add website binding support for the agent (#12413) 2026-04-03 09:15:54 +00:00
ssongliu 61ef725892 feat: improve terminal ai runtime updates (#12409) 2026-04-03 07:58:13 +00:00
CityFun 0c49c41085 feat: change ai menu (#12407) 2026-04-03 07:56:12 +00:00
ssongliu 2a498de96e fix: tighten clean tree delete semantics (#12406)
Refs #12399
2026-04-03 07:54:12 +00:00
ssongliu d055cbb053 feat: improve terminal ai notice interaction 2026-04-03 14:28:09 +08:00
ssongliu 4b3f8a9fd0 style: refine app launcher card layout (#12404) 2026-04-03 11:23:27 +08:00
CityFun 4c26945910 feat: update default model config (#12403) 2026-04-02 10:30:07 +00:00
ssongliu 55b6104b56 fix: reset dashboard delta baseline after resume (#12402)
Refs #12067
2026-04-02 10:28:08 +00:00
CityFun 12dc8aae81 feat: Optimize Monaco editor loading (#12401) 2026-04-02 10:26:08 +00:00
ssongliu 37e2b93693 refactor: replace el-dialog with DialogPro (#12400) 2026-04-02 10:24:09 +00:00
ssongliu 62daf1e019 fix: improve ssh config file editing and include handling (#12398)
Refs #11899
2026-04-02 10:22:09 +00:00
CityFun f5e75d4bfa feat: The skill marketplace supports ClawHub China acceleration. (#12396) 2026-04-02 06:02:06 +00:00
ssongliu 32fed45c1e fix: skip proxy check when disabled (#12388)
Refs #12379
2026-04-01 11:22:08 +00:00
ssongliu 8b7597abef chore: upgrade grpc buildkit and x/image (#12387) 2026-04-01 11:20:08 +00:00
CityFun 01fb9a461b feat: OpenClaw supports configuring backup models. (#12386) 2026-04-01 11:18:09 +00:00
ssongliu bc022a4d21 refactor: simplify agent role binding inputs (#12385) 2026-04-01 11:16:09 +00:00
蘭 973cab2837 fix: Update ESLint plugin and solve the issue of code filter overflow in the development environment (#12383) 2026-04-01 09:14:09 +00:00
ssongliu 5ce155eee6 feat: add agent role binding management (#12382) 2026-04-01 09:12:07 +00:00
CityFun d83b49cc70 feat: Add more configuration options to channels. (#12380) 2026-04-01 08:24:08 +00:00
ssongliu ec981f8153 fix: harden login IP checks and entrance cookie (#12378) 2026-04-01 08:22:09 +00:00
ssongliu b2853277e7 fix: improve auth lock handling and login feedback (#12377) 2026-03-31 11:00:06 +00:00
ssongliu f573a0fa0e fix: improve request validation and session lifecycle (#12375) 2026-03-31 10:58:05 +00:00
CityFun f787734feb feat: Support uninstallation and upgrades of OpenClaw plugins. (#12373) 2026-03-31 10:08:05 +00:00
CityFun 52778f520a feat: QQ and Feishu use official plugins.QQ, Feishu, DingTalk, Telegram, and Discord support multiple accounts. (#12369)
feat: QQ and Feishu use official plugins.QQ, Feishu, DingTalk, Telegram, and Discord support multiple accounts.
2026-03-31 05:48:04 +00:00
ssongliu b5aba18248 fix: harden core memory session handling (#12366) 2026-03-30 14:30:06 +00:00
ssongliu bc9882da50 fix: guard mfa login with ip tracker (#12365) 2026-03-30 22:28:32 +08:00
ssongliu 0ba7e98e8d feat: add agent config role management (#12364) 2026-03-30 08:59:06 +00:00
王贺 e6dc44d135 chore: refine i18n (#12350)
* chore: refine i18n

* fix overview i18n

* feat: remove joinLabel funciton
2026-03-30 16:54:19 +08:00
蘭 a9f30e9f5f feat: Enhance Bark alert configuration options and visibility (#12362) 2026-03-30 06:55:05 +00:00
CityFun f3bf9d0905 feat: support remark for openclaw (#12361)
Refs https://github.com/1Panel-dev/1Panel/issues/12191
2026-03-30 06:53:05 +00:00
Alan 05ea384a00 feat: Add Bark as a new alert notification channel. (#12338) 2026-03-30 10:58:52 +08:00
maninhill 62c50aafe1 Add video introduction link to README
Added a link to a video introduction of 1Panel.
2026-03-27 09:59:17 +08:00
CityFun 6e052b8ef2 fix: Fixed issue with openclaw skill page return empty (#12347) 2026-03-26 17:57:07 +08:00
ssongliu 1d40bdc11b fix: resolve menu item wrapping issue (#12346) 2026-03-26 09:16:24 +00:00
CityFun 3b62197f29 fix: Fixed issue with openclaw skill page return empty (#12345) 2026-03-26 09:10:24 +00:00
997 changed files with 104384 additions and 21009 deletions
+14
View File
@@ -0,0 +1,14 @@
version: 2
updates:
- package-ecosystem: "gomod"
directories:
- "/agent"
- "/core"
schedule:
interval: "daily"
- package-ecosystem: "npm"
directory: "/frontend"
schedule:
interval: "daily"
+1 -1
View File
@@ -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
-69
View File
@@ -1,69 +0,0 @@
name: Build And Publish (OSS + R2)
on:
push:
tags:
- 'v*'
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Node
uses: actions/setup-node@v4
with:
node-version: '22.19.0'
- name: Build Web
run: |
cd frontend && npm install && npm run build:pro
env:
NODE_OPTIONS: --max-old-space-size=8192
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.24'
cache-dependency-path: |
core/go.sum
agent/go.sum
- name: Build Release
uses: goreleaser/goreleaser-action@v6
with:
distribution: goreleaser
version: '~> v2'
args: release --skip=publish --clean
- name: Upload Assets
uses: softprops/action-gh-release@v1
if: startsWith(github.ref, 'refs/tags/')
with:
draft: true
files: |
dist/*.tar.gz
dist/checksums.txt
- name: Setup OSSUTIL
uses: yizhoumo/setup-ossutil@v2
with:
endpoint: ${{ secrets.OSS_ENDPOINT }}
access-key-id: ${{ secrets.OSS_ACCESS_KEY_ID }}
access-key-secret: ${{ secrets.OSS_ACCESS_KEY_SECRET }}
ossutil-version: '1.7.18'
- name: Upload Assets to OSS
run: |
ossutil cp -r dist/ oss://resource-fit2cloud-com/1panel/package/v2/stable/${{ github.ref_name }}/release/ --include "*.tar.gz" --include "checksums.txt" --only-current-dir --force
- name: Setup Rclone
uses: AnimMouse/setup-rclone@v1
with:
rclone_config: ${{ secrets.RCLONE_CONFIG }}
- name: Upload to Cloudflare R2
run: |
rclone copy dist/ cloudflare_r2:package/v2/stable/${{ github.ref_name }}/release/ --include "*.tar.gz" --include "checksums.txt" --progress
+10 -7
View File
@@ -9,7 +9,6 @@ build/1panel-core
# Mac
.DS_Store
*/.DS_Store
# VS Code
.vscode
@@ -41,20 +40,23 @@ core/cmd/server/web/index.html
frontend/auto-imports.d.ts
frontend/components.d.ts
frontend/src/xpack
frontend/src/enterprise
agent/xpack
agent/enterprise
agent/router/entry_xpack.go
agent/router/entry_enterprise.go
agent/server/init_xpack.go
agent/server/init_enterprise.go
agent/utils/xpack/xpack.go
agent/utils/xpack/xpack_xpack.go
agent/utils/xpack/enterprise.go
core/xpack
core/enterprise
core/router/entry_xpack.go
core/router/entry_enterprise.go
core/server/init_xpack.go
core/server/init_enterprise.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
core/utils/xpack/enterprise.go
.history/
dist/
@@ -76,3 +78,4 @@ AGENTS.md
opencode.json
superpowers
.worktrees/
.codex/
-82
View File
@@ -1,82 +0,0 @@
# yaml-language-server: $schema=https://goreleaser.com/static/schema.json
# vim: set ts=2 sw=2 tw=0 fo=jcroql
version: 2
before:
hooks:
# - export NODE_OPTIONS="--max-old-space-size=8192"
# - make build_web
- chmod +x ./ci/script.sh
- ./ci/script.sh
- sed -i 's@ORIGINAL_VERSION=.*@ORIGINAL_VERSION=v{{ .Version }}@g' 1pctl
builds:
- id: agent
dir: agent
main: cmd/server/main.go
binary: 1panel-agent
flags:
- -tags=xpack
- -trimpath
ldflags:
- -w -s
env:
- CGO_ENABLED=0
goos:
- linux
goarm:
- 7
goarch:
- amd64
- arm64
- arm
- ppc64le
- s390x
- riscv64
- id: core
dir: core
main: cmd/server/main.go
binary: 1panel-core
flags:
- -tags=xpack
- -trimpath
ldflags:
- -w -s
env:
- CGO_ENABLED=0
goos:
- linux
goarm:
- 7
goarch:
- amd64
- arm64
- arm
- ppc64le
- s390x
- riscv64
archives:
- formats: [ 'tar.gz' ]
ids: [core, agent]
name_template: "1panel-v{{ .Version }}-{{ .Os }}-{{ .Arch }}{{- if .Arm }}v{{ .Arm }}{{ end }}"
wrap_in_directory: true
files:
- 1pctl
- install.sh
- 1panel-core.service
- 1panel-agent.service
- initscript/*
- lang/*
- GeoIP.mmdb
checksum:
name_template: 'checksums.txt'
changelog:
sort: asc
filters:
exclude:
- "^docs:"
- "^test:"
+4 -2
View File
@@ -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.
+2
View File
@@ -2,8 +2,10 @@ reviewers:
- wanghe-fit2cloud
- zhengkunwang223
- ssongliu
- lan-yonghui
approvers:
- wanghe-fit2cloud
- zhengkunwang223
- ssongliu
- lan-yonghui
+68 -29
View File
@@ -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
![UI Display](https://resource.1panel.pro/img/overview_en_v2.png)
![1Panel UI](https://resource.1panel.pro/img/overview_en_v2.png)
## 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
[![Star History Chart](https://api.star-history.com/svg?repos=1Panel-dev/1Panel&type=Date)](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).
+553 -20
View File
@@ -27,6 +27,90 @@ func (b *BaseApi) CreateAgent(c *gin.Context) {
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Batch install Agent
// @Accept json
// @Param request body dto.AgentBatchInstallReq true "request"
// @Success 200 {object} dto.AgentItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/batch/install [post]
func (b *BaseApi) BatchInstallAgent(c *gin.Context) {
var req dto.AgentBatchInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.BatchInstall(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Batch upgrade Agent
// @Accept json
// @Param request body dto.AgentBatchUpgradeReq true "request"
// @Success 200 {array} dto.AgentBatchUpgradeResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/batch/upgrade [post]
func (b *BaseApi) BatchUpgradeAgent(c *gin.Context) {
var req dto.AgentBatchUpgradeReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.BatchUpgrade(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Batch install Agent Skill
// @Accept json
// @Param request body dto.AgentBatchSkillInstallReq true "request"
// @Success 200 {array} dto.AgentBatchSkillInstallResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/batch/skill/install [post]
func (b *BaseApi) BatchInstallAgentSkill(c *gin.Context) {
var req dto.AgentBatchSkillInstallReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.BatchInstallSkill(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Batch operate Agent
// @Accept json
// @Param request body dto.AgentBatchOperateReq true "request"
// @Success 200 {array} dto.AgentBatchOperateResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/batch/operate [post]
func (b *BaseApi) BatchOperateAgent(c *gin.Context) {
var req dto.AgentBatchOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.BatchOperate(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Page Agents
// @Accept json
@@ -51,6 +135,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 +196,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
@@ -133,11 +319,72 @@ func (b *BaseApi) GetAgentOverview(c *gin.Context) {
}
// @Tags AI
// @Summary Get Providers
// @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 model account providers
// @Success 200 {array} dto.ProviderInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/providers [get]
// @Router /ai/accounts/providers [get]
func (b *BaseApi) GetAgentProviders(c *gin.Context) {
list, err := agentService.GetProviders()
if err != nil {
@@ -148,13 +395,13 @@ func (b *BaseApi) GetAgentProviders(c *gin.Context) {
}
// @Tags AI
// @Summary Create Agent account
// @Summary Create model account
// @Accept json
// @Param request body dto.AgentAccountCreateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts [post]
// @Router /ai/accounts [post]
func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
var req dto.AgentAccountCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -168,13 +415,13 @@ func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
}
// @Tags AI
// @Summary Update Agent account
// @Summary Update model account
// @Accept json
// @Param request body dto.AgentAccountUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/update [post]
// @Router /ai/accounts/update [post]
func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
var req dto.AgentAccountUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -188,13 +435,13 @@ func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
}
// @Tags AI
// @Summary Page Agent accounts
// @Summary Page model accounts
// @Accept json
// @Param request body dto.AgentAccountSearch true "request"
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/search [post]
// @Router /ai/accounts/search [post]
func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
var req dto.AgentAccountSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -212,13 +459,34 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
}
// @Tags AI
// @Summary List Agent account models
// @Summary Count model accounts by provider
// @Accept json
// @Param request body dto.AgentAccountProviderCountReq true "request"
// @Success 200 {object} map[string]int64
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/accounts/counts [post]
func (b *BaseApi) CountAgentAccountsByProviders(c *gin.Context) {
var req dto.AgentAccountProviderCountReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
counts, err := agentService.CountAccountsByProviders(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, counts)
}
// @Tags AI
// @Summary List model account models
// @Accept json
// @Param request body dto.AgentAccountModelReq true "request"
// @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models [post]
// @Router /ai/accounts/models [post]
func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -233,13 +501,13 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
}
// @Tags AI
// @Summary Create Agent account model
// @Summary Create model account model
// @Accept json
// @Param request body dto.AgentAccountModelCreateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/create [post]
// @Router /ai/accounts/models/create [post]
func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -253,13 +521,13 @@ func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
}
// @Tags AI
// @Summary Update Agent account model
// @Summary Update model account model
// @Accept json
// @Param request body dto.AgentAccountModelUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/update [post]
// @Router /ai/accounts/models/update [post]
func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -273,13 +541,13 @@ func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
}
// @Tags AI
// @Summary Delete Agent account model
// @Summary Delete model account model
// @Accept json
// @Param request body dto.AgentAccountModelDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/models/delete [post]
// @Router /ai/accounts/models/delete [post]
func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -293,13 +561,13 @@ func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
}
// @Tags AI
// @Summary Verify Agent account
// @Summary Verify model account
// @Accept json
// @Param request body dto.AgentAccountVerifyReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/verify [post]
// @Router /ai/accounts/verify [post]
func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
var req dto.AgentAccountVerifyReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -313,13 +581,13 @@ func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
}
// @Tags AI
// @Summary Delete Agent account
// @Summary Delete model account
// @Accept json
// @Param request body dto.AgentAccountDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/accounts/delete [post]
// @Router /ai/accounts/delete [post]
func (b *BaseApi) DeleteAgentAccount(c *gin.Context) {
var req dto.AgentAccountDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -332,6 +600,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 +969,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 +1031,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 +1071,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 +1337,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
+9 -42
View File
@@ -3,9 +3,6 @@ package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
"github.com/gin-gonic/gin"
)
@@ -163,37 +160,6 @@ func (b *BaseApi) DeleteOllamaModel(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Load gpu / xpu info
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/gpu/load [get]
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
ok, client := gpu.New()
if ok {
info, err := client.LoadGpuInfo()
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, info)
return
}
xpuOK, xpuClient := xpu.New()
if xpuOK {
info, err := xpuClient.LoadGpuInfo()
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, info)
return
}
helper.SuccessWithData(c, &common.GpuInfo{})
}
// @Tags AI
// @Summary Bind domain
// @Accept json
@@ -235,14 +201,15 @@ func (b *BaseApi) GetBindDomain(c *gin.Context) {
helper.SuccessWithData(c, res)
}
// Tags AI
// Summary Update bind domain
// Accept json
// Param request body dto.OllamaBindDomain true "request"
// Success 200
// Security ApiKeyAuth
// Security Timestamp
// Router /ai/domain/update [post]
// @Tags AI
// @Summary Update bind domain
// @Accept json
// @Param request body dto.OllamaBindDomain true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/domain/update [post]
// @x-panel-log {"bodyKeys":["domain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 Ollama 绑定域名 [domain]","formatEN":"update Ollama bind domain [domain]"}
func (b *BaseApi) UpdateBindDomain(c *gin.Context) {
var req dto.OllamaBindDomain
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+159 -4
View File
@@ -2,11 +2,24 @@ package v2
import (
"errors"
"net/url"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin"
)
const defaultAuditUser = "system"
// @Tags Alert
// @Summary Page alert
// @Accept json
// @Param request body dto.AlertSearch true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/search [post]
func (b *BaseApi) PageAlert(c *gin.Context) {
var req dto.AlertSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -32,12 +45,21 @@ func (b *BaseApi) GetAlerts(c *gin.Context) {
helper.SuccessWithData(c, alerts)
}
// @Tags Alert
// @Summary Create alert
// @Accept json
// @Param request body dto.AlertCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert [post]
// @x-panel-log {"bodyKeys":["title"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建告警任务 [title]","formatEN":"create alert [title]"}
func (b *BaseApi) CreateAlert(c *gin.Context) {
var req dto.AlertCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
err := alertService.CreateAlert(req)
err := alertService.CreateAlert(req, loadAuditUser(c))
if err != nil {
helper.InternalServer(c, err)
return
@@ -45,6 +67,15 @@ func (b *BaseApi) CreateAlert(c *gin.Context) {
helper.Success(c)
}
// @Tags Alert
// @Summary Delete alert
// @Accept json
// @Param request body dto.DeleteRequest true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警任务 [id]","formatEN":"delete alert [id]"}
func (b *BaseApi) DeleteAlert(c *gin.Context) {
var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -58,12 +89,21 @@ func (b *BaseApi) DeleteAlert(c *gin.Context) {
helper.Success(c)
}
// @Tags Alert
// @Summary Update alert
// @Accept json
// @Param request body dto.AlertUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/update [post]
// @x-panel-log {"bodyKeys":["title"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警任务 [title]","formatEN":"update alert [title]"}
func (b *BaseApi) UpdateAlert(c *gin.Context) {
var req dto.AlertUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlert(req); err != nil {
if err := alertService.UpdateAlert(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -84,6 +124,15 @@ func (b *BaseApi) GetAlert(c *gin.Context) {
helper.SuccessWithData(c, alert)
}
// @Tags Alert
// @Summary Update alert status
// @Accept json
// @Param request body dto.AlertUpdateStatus true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/status [post]
// @x-panel-log {"bodyKeys":["id","status"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警任务 [id] 状态 [status]","formatEN":"update alert [id] status [status]"}
func (b *BaseApi) UpdateAlertStatus(c *gin.Context) {
var req dto.AlertUpdateStatus
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -97,6 +146,12 @@ func (b *BaseApi) UpdateAlertStatus(c *gin.Context) {
helper.Success(c)
}
// @Tags Alert
// @Summary Get disks
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/disks/list [get]
func (b *BaseApi) GetDisks(c *gin.Context) {
alerts, err := alertService.GetDisks()
if err != nil {
@@ -106,6 +161,14 @@ func (b *BaseApi) GetDisks(c *gin.Context) {
helper.SuccessWithData(c, alerts)
}
// @Tags Alert
// @Summary Page alert logs
// @Accept json
// @Param request body dto.AlertLogSearch true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/logs/search [post]
func (b *BaseApi) PageAlertLogs(c *gin.Context) {
var req dto.AlertLogSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -122,6 +185,13 @@ func (b *BaseApi) PageAlertLogs(c *gin.Context) {
})
}
// @Tags Alert
// @Summary Clean alert logs
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/logs/clean [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"清空告警日志","formatEN":"clean alert logs"}
func (b *BaseApi) CleanAlertLogs(c *gin.Context) {
if err := alertService.CleanAlertLogs(); err != nil {
helper.InternalServer(c, err)
@@ -130,6 +200,12 @@ func (b *BaseApi) CleanAlertLogs(c *gin.Context) {
helper.Success(c)
}
// @Tags Alert
// @Summary Get clams
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/clams/list [get]
func (b *BaseApi) GetClams(c *gin.Context) {
clams, err := alertService.GetClams()
if err != nil {
@@ -139,6 +215,14 @@ func (b *BaseApi) GetClams(c *gin.Context) {
helper.SuccessWithData(c, clams)
}
// @Tags Alert
// @Summary Get cron jobs
// @Accept json
// @Param request body dto.CronJobReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/cronjob/list [post]
func (b *BaseApi) GetCronJobs(c *gin.Context) {
var req dto.CronJobReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -152,8 +236,18 @@ func (b *BaseApi) GetCronJobs(c *gin.Context) {
helper.SuccessWithData(c, cronJobs)
}
// @Tags Alert
// @Summary Get alert config
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/info [post]
func (b *BaseApi) GetAlertConfig(c *gin.Context) {
config, err := alertService.GetAlertConfig()
var req dto.AlertConfigQuery
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
config, err := alertService.GetAlertConfig(req)
if err != nil {
helper.InternalServer(c, err)
return
@@ -161,18 +255,71 @@ func (b *BaseApi) GetAlertConfig(c *gin.Context) {
helper.SuccessWithData(c, config)
}
// @Tags Alert
// @Summary Page alert config
// @Accept json
// @Param request body dto.AlertConfigPageReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/search [post]
func (b *BaseApi) PageAlertConfig(c *gin.Context) {
var req dto.AlertConfigPageReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
total, configs, err := alertService.PageAlertConfig(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, dto.PageResult{
Total: total,
Items: configs,
})
}
// @Tags Alert
// @Summary Update alert config
// @Accept json
// @Param request body dto.AlertConfigUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/update [post]
// @x-panel-log {"bodyKeys":["id","displayName"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新告警配置 [id][displayName]","formatEN":"update alert config [id][displayName]"}
func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
var req dto.AlertConfigUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := alertService.UpdateAlertConfig(req); err != nil {
if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
func loadAuditUser(c *gin.Context) string {
userName := strings.TrimSpace(c.GetHeader("X-Panel-User"))
if userName == "" {
return defaultAuditUser
}
if decoded, err := url.QueryUnescape(userName); err == nil {
return decoded
}
return userName
}
// @Tags Alert
// @Summary Delete alert config
// @Accept json
// @Param request body dto.DeleteRequest true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除告警配置 [id]","formatEN":"delete alert config [id]"}
func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -186,6 +333,14 @@ func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags Alert
// @Summary Test alert config
// @Accept json
// @Param request body dto.AlertConfigTest true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /alert/config/test [post]
func (b *BaseApi) TestAlertConfig(c *gin.Context) {
var req dto.AlertConfigTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+10 -4
View File
@@ -107,7 +107,7 @@ func (b *BaseApi) GetApp(c *gin.Context) {
// @Accept json
// @Param appId path integer true "app id"
// @Param version path string true "app 版本"
// @Param version path string true "app 类型"
// @Param type path string true "app 类型"
// @Success 200 {object} response.AppDetailDTO
// @Security ApiKeyAuth
// @Security Timestamp
@@ -131,7 +131,7 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
// @Tags App
// @Summary Get app detail by id
// @Accept json
// @Param appId path integer true "id"
// @Param id path integer true "id"
// @Success 200 {object} response.AppDetailDTO
// @Security ApiKeyAuth
// @Security Timestamp
@@ -172,6 +172,12 @@ func (b *BaseApi) InstallApp(c *gin.Context) {
helper.SuccessWithData(c, install)
}
// @Tags App
// @Summary Get app tags
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /apps/tags [get]
func (b *BaseApi) GetAppTags(c *gin.Context) {
tags, err := appService.GetAppTags(c)
if err != nil {
@@ -199,7 +205,7 @@ func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
// @Tags App
// @Summary Get app icon by app_id
// @Accept json
// @Param appId path integer true "app id"
// @Param key path string true "app key"
// @Success 200 {file} file "app icon"
// @Security ApiKeyAuth
// @Security Timestamp
@@ -237,7 +243,7 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
// @Tags App
// @Summary Search app detail by appkey and version
// @Accept json
// @Param appId path integer true "app key"
// @Param appKey path string true "app key"
// @Param version path string true "app version"
// @Success 200 {object} response.AppDetailSimpleDTO
// @Security ApiKeyAuth
+10 -2
View File
@@ -203,7 +203,6 @@ func (b *BaseApi) GetServices(c *gin.Context) {
// @Tags App
// @Summary Search app update version by install id
// @Accept json
// @Param appInstallId path integer true "request"
// @Success 200 {array} dto.AppVersion
// @Security ApiKeyAuth
// @Security Timestamp
@@ -267,7 +266,7 @@ func (b *BaseApi) GetDefaultConfig(c *gin.Context) {
// @Tags App
// @Summary Search params by appInstallId
// @Accept json
// @Param appInstallId path string true "request"
// @Param appInstallId path integer true "request"
// @Success 200 {object} response.AppConfig
// @Security ApiKeyAuth
// @Security Timestamp
@@ -350,6 +349,15 @@ func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
helper.SuccessWithData(c, info)
}
// @Tags App
// @Summary Update app install sort
// @Accept json
// @Param request body request.AppInstallSort true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /apps/installed/sort/update [post]
// @x-panel-log {"bodyKeys":[],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新已安装应用排序","formatEN":"update installed app sort"}
func (b *BaseApi) UpdateAppInstallSort(c *gin.Context) {
var req request.AppInstallSort
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+24 -2
View File
@@ -10,6 +10,13 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags Backup Account
// @Summary Check backup used
// @Param name path string true "name"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /backups/check/{name} [get]
func (b *BaseApi) CheckBackupUsed(c *gin.Context) {
name, err := helper.GetStrParamByKey(c, "name")
if err != nil {
@@ -32,7 +39,7 @@ func (b *BaseApi) CheckBackupUsed(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /backups/check [post]
// @Router /backups/conn/check [post]
func (b *BaseApi) CheckBackup(c *gin.Context) {
var req dto.BackupOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -136,7 +143,7 @@ func (b *BaseApi) DeleteBackup(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /backups/update [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新备份账号 [types]","formatEN":"update backup account [types]"}
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新备份账号 [type]","formatEN":"update backup account [type]"}
func (b *BaseApi) UpdateBackup(c *gin.Context) {
var req dto.BackupOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -415,6 +422,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 +487,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 +547,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)
+3 -3
View File
@@ -21,7 +21,7 @@ func (b *BaseApi) CreateClam(c *gin.Context) {
return
}
if err := clamService.Create(req); err != nil {
if err := clamService.Create(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -43,7 +43,7 @@ func (b *BaseApi) UpdateClam(c *gin.Context) {
return
}
if err := clamService.Update(req); err != nil {
if err := clamService.Update(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -123,7 +123,7 @@ func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /toolbox/clam/operate [post]
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Clam","formatEN":"[operation] FTP"}
// @x-panel-log {"bodyKeys":["operation"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operation] Clam","formatEN":"[operation] Clam"}
func (b *BaseApi) OperateClam(c *gin.Context) {
var req dto.Operate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+16
View File
@@ -5,6 +5,7 @@ import (
"net/url"
"path"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -394,6 +395,7 @@ func (b *BaseApi) ContainerInfo(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Container
// @Summary Load container limits
// @Success 200 {object} dto.ResourceLimit
// @Security ApiKeyAuth
@@ -408,6 +410,7 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Container
// @Summary Load container stats
// @Success 200 {array} dto.ContainerListStats
// @Security ApiKeyAuth
@@ -422,6 +425,7 @@ func (b *BaseApi) ContainerListStats(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Container
// @Summary Load container stats size
// @Accept json
// @Param request body dto.OperationWithName true "request"
@@ -662,6 +666,14 @@ func (b *BaseApi) Inspect(c *gin.Context) {
helper.SuccessWithData(c, result)
}
// @Tags Container
// @Summary Download container logs
// @Accept json
// @Param request body dto.ContainerLog true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /containers/download/log [post]
func (b *BaseApi) DownloadContainerLogs(c *gin.Context) {
var req dto.ContainerLog
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -901,6 +913,10 @@ func (b *BaseApi) LoadComposeEnv(c *gin.Context) {
// @Security Timestamp
// @Router /containers/search/log [get]
func (b *BaseApi) ContainerStreamLogs(c *gin.Context) {
if !strings.Contains(strings.ToLower(c.GetHeader("Accept")), "text/event-stream") {
helper.Success(c)
return
}
c.Header("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive")
+3 -3
View File
@@ -26,7 +26,7 @@ func (b *BaseApi) CreateCronjob(c *gin.Context) {
return
}
if err := cronjobService.Create(req); err != nil {
if err := cronjobService.Create(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -91,7 +91,7 @@ func (b *BaseApi) ImportCronjob(c *gin.Context) {
return
}
if err := cronjobService.Import(req.Cronjobs); err != nil {
if err := cronjobService.Import(req.Cronjobs, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
@@ -285,7 +285,7 @@ func (b *BaseApi) UpdateCronjob(c *gin.Context) {
return
}
if err := cronjobService.Update(req.ID, req); err != nil {
if err := cronjobService.Update(req.ID, req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err)
return
}
+3
View File
@@ -95,6 +95,7 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
// @Success 200 {array} dto.DatabaseOption
// @Security ApiKeyAuth
// @Security Timestamp
// @Param type path string true "type"
// @Router /databases/db/list/:type [get]
func (b *BaseApi) ListDatabase(c *gin.Context) {
dbType, err := helper.GetStrParamByKey(c, "type")
@@ -116,6 +117,7 @@ func (b *BaseApi) ListDatabase(c *gin.Context) {
// @Success 200 {array} dto.DatabaseItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Param type path string true "type"
// @Router /databases/db/item/:type [get]
func (b *BaseApi) LoadDatabaseItems(c *gin.Context) {
dbType, err := helper.GetStrParamByKey(c, "type")
@@ -137,6 +139,7 @@ func (b *BaseApi) LoadDatabaseItems(c *gin.Context) {
// @Success 200 {object} dto.DatabaseInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Param name path string true "name"
// @Router /databases/db/:name [get]
func (b *BaseApi) GetDatabase(c *gin.Context) {
name, err := helper.GetStrParamByKey(c, "name")
+294
View File
@@ -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)
}
+2 -1
View File
@@ -92,7 +92,7 @@ func (b *BaseApi) UpdatePostgresqlDescription(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/pg/privileges [post]
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] 用户 [username] 权限","formatEN":"Update [user] privileges of database [database]"}
// @x-panel-log {"bodyKeys":["database", "username"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新数据库 [database] 用户 [username] 权限","formatEN":"Update [username] privileges of database [database]"}
func (b *BaseApi) ChangePostgresqlPrivileges(c *gin.Context) {
var req dto.PostgresqlPrivileges
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -170,6 +170,7 @@ func (b *BaseApi) SearchPostgresql(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Param database path string true "database"
// @Router /databases/pg/:database/load [post]
func (b *BaseApi) LoadPostgresqlDBFromRemote(c *gin.Context) {
database, err := helper.GetStrParamByKey(c, "database")
+6
View File
@@ -74,6 +74,12 @@ func (b *BaseApi) LoadPersistenceConf(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Database Redis
// @Summary Check has cli
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /databases/redis/check [get]
func (b *BaseApi) CheckHasCli(c *gin.Context) {
helper.SuccessWithData(c, redisService.CheckHasCli())
}
+9 -6
View File
@@ -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()
+355 -18
View File
@@ -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
@@ -167,7 +193,7 @@ func (b *BaseApi) BatchDeleteFile(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/mode [post]
// @x-panel-log {"bodyKeys":["path","mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改权限 [paths] => [mode]","formatEN":"Change mode [paths] => [mode]"}
// @x-panel-log {"bodyKeys":["path","mode"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改权限 [path] => [mode]","formatEN":"Change mode [path] => [mode]"}
func (b *BaseApi) ChangeFileMode(c *gin.Context) {
var req request.FileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -189,7 +215,7 @@ func (b *BaseApi) ChangeFileMode(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/owner [post]
// @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改用户/组 [paths] => [user]/[group]","formatEN":"Change owner [paths] => [user]/[group]"}
// @x-panel-log {"bodyKeys":["path","user","group"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改用户/组 [path] => [user]/[group]","formatEN":"Change owner [path] => [user]/[group]"}
func (b *BaseApi) ChangeFileOwner(c *gin.Context) {
var req request.FileRoleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -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)
@@ -568,6 +640,10 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
// @Router /files/download [get]
func (b *BaseApi) Download(c *gin.Context) {
filePath := c.Query("path")
if files.ShouldDenySensitiveFileRead(filePath) {
helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
return
}
file, err := os.Open(filePath)
if err != nil {
helper.InternalServer(c, err)
@@ -603,6 +679,10 @@ func (b *BaseApi) DownloadChunkFiles(c *gin.Context) {
helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPathNotFound", nil)
return
}
if files.ShouldDenySensitiveFileRead(req.Path) {
helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
return
}
filePath := req.Path
fstFile, err := fileOp.OpenFile(filePath)
if err != nil {
@@ -721,12 +801,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 +829,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
}
_ = os.Remove(chunkPath)
}
_ = os.Chmod(dstFileName, mode)
return nil
}
@@ -852,7 +934,17 @@ var wsUpgrade = websocket.Upgrader{
},
}
// @Tags File
// @Summary Wget process
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/process [get]
func (b *BaseApi) WgetProcess(c *gin.Context) {
if !websocket.IsWebSocketUpgrade(c.Request) {
helper.Success(c)
return
}
ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
@@ -862,6 +954,12 @@ func (b *BaseApi) WgetProcess(c *gin.Context) {
go wsClient.Write()
}
// @Tags File
// @Summary Process keys
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/wget/process/keys [get]
func (b *BaseApi) ProcessKeys(c *gin.Context) {
res := &response.FileProcessKeys{}
keys := global.CACHE.PrefixScanKey("file-wget-")
@@ -881,16 +979,20 @@ func (b *BaseApi) ProcessKeys(c *gin.Context) {
// @Tags File
// @Summary Read file by Line
// @Param type path string true "type"
// @Param request body request.FileReadByLineReq true "request"
// @Success 200 {object} response.FileLineContent
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /files/read [post]
// @Router /files/read/{type} [post]
func (b *BaseApi) ReadFileByLine(c *gin.Context) {
var req request.FileReadByLineReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if readType := strings.TrimSpace(c.Param("type")); readType != "" {
req.Type = readType
}
res, err := fileService.ReadLogByLine(req)
if err != nil {
helper.InternalServer(c, err)
@@ -923,16 +1025,6 @@ func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) {
helper.Success(c)
}
func (b *BaseApi) GetPathByType(c *gin.Context) {
pathType, ok := c.Params.Get("type")
if !ok {
helper.BadRequest(c, errors.New("error pathType id in path"))
return
}
resPath := fileService.GetPathByType(pathType)
helper.SuccessWithData(c, resPath)
}
// @Tags File
// @Summary system mount
// @Accept json
@@ -1038,3 +1130,248 @@ 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
}
if files.ShouldDenySensitiveFileRead(filePath) {
helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
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)
}
+91
View 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} dto.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)
}
+1 -1
View File
@@ -236,7 +236,7 @@ func (b *BaseApi) UpdateAddrRule(c *gin.Context) {
// @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/firewall/filter/search [post]
// @Router /hosts/firewall/filter/rule/search [post]
func (b *BaseApi) SearchFilterRules(c *gin.Context) {
var req dto.SearchPageWithType
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+72
View File
@@ -0,0 +1,72 @@
package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/gpu/common"
"github.com/1Panel-dev/1Panel/agent/utils/ai_tools/xpu"
"github.com/gin-gonic/gin"
)
// @Tags AI
// @Summary Load gpu / xpu info
// @Accept json
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/gpu/load [get]
func (b *BaseApi) LoadGpuInfo(c *gin.Context) {
ok, client := gpu.New()
if ok {
info, err := client.LoadGpuInfo()
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, info)
return
}
xpuOK, xpuClient := xpu.New()
if xpuOK {
info, err := xpuClient.LoadGpuInfo()
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, info)
return
}
helper.SuccessWithData(c, &common.GpuInfo{})
}
// @Tags AI
// @Summary Get CPU options
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/gpu/options [get]
func (b *BaseApi) GetCPUOptions(c *gin.Context) {
helper.SuccessWithData(c, monitorService.LoadGPUOptions())
}
// @Tags Monitor
// @Summary Load monitor data
// @Param request body dto.MonitorGPUSearch true "request"
// @Success 200 {object} dto.MonitorGPUData
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/gpu/search [post]
func (b *BaseApi) LoadGPUMonitor(c *gin.Context) {
var req dto.MonitorGPUSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadGPUMonitorData(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
+6
View File
@@ -5,6 +5,12 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags Health
// @Summary Check health
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /health/check [get]
func (b *BaseApi) CheckHealth(c *gin.Context) {
helper.Success(c)
}
+76
View File
@@ -7,6 +7,15 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags Host
// @Summary Create host
// @Accept json
// @Param request body dto.HostOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts [post]
// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建主机 [name][addr]","formatEN":"create host [name][addr]"}
func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -21,6 +30,14 @@ func (b *BaseApi) CreateHost(c *gin.Context) {
helper.SuccessWithData(c, host)
}
// @Tags Host
// @Summary Test by info
// @Accept json
// @Param request body dto.HostConnTest true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/test/byinfo [post]
func (b *BaseApi) TestByInfo(c *gin.Context) {
var req dto.HostConnTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -30,6 +47,14 @@ func (b *BaseApi) TestByInfo(c *gin.Context) {
helper.SuccessWithData(c, hostService.TestByInfo(req))
}
// @Tags Host
// @Summary Test by ID
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/test/byid [post]
func (b *BaseApi) TestByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -39,6 +64,14 @@ func (b *BaseApi) TestByID(c *gin.Context) {
helper.SuccessWithData(c, hostService.TestLocalConn(req.ID))
}
// @Tags Host
// @Summary Host tree
// @Accept json
// @Param request body dto.SearchForTree true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tree [post]
func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -53,6 +86,14 @@ func (b *BaseApi) HostTree(c *gin.Context) {
helper.SuccessWithData(c, data)
}
// @Tags Host
// @Summary Search host
// @Accept json
// @Param request body dto.SearchPageWithGroup true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/search [post]
func (b *BaseApi) SearchHost(c *gin.Context) {
var req dto.SearchPageWithGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -68,6 +109,15 @@ func (b *BaseApi) SearchHost(c *gin.Context) {
helper.SuccessWithData(c, dto.PageResult{Items: list, Total: total})
}
// @Tags Host
// @Summary Delete host
// @Accept json
// @Param request body dto.OperateByIDs true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/del [post]
// @x-panel-log {"bodyKeys":["ids"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"ids","isList":true,"db":"hosts","output_column":"name","output_value":"names"}],"formatZH":"删除主机 [names]","formatEN":"delete host [names]"}
func (b *BaseApi) DeleteHost(c *gin.Context) {
var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -81,6 +131,15 @@ func (b *BaseApi) DeleteHost(c *gin.Context) {
helper.Success(c)
}
// @Tags Host
// @Summary Update host
// @Accept json
// @Param request body dto.HostOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/update [post]
// @x-panel-log {"bodyKeys":["name","addr"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新主机 [name][addr]","formatEN":"update host [name][addr]"}
func (b *BaseApi) UpdateHost(c *gin.Context) {
var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -140,6 +199,15 @@ func (b *BaseApi) UpdateHost(c *gin.Context) {
helper.SuccessWithData(c, hostItem)
}
// @Tags Host
// @Summary Update host group
// @Accept json
// @Param request body dto.ChangeGroup true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/update/group [post]
// @x-panel-log {"bodyKeys":["id","groupID"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"hosts","output_column":"name","output_value":"name"}],"formatZH":"更新主机 [name] 分组","formatEN":"update host [name] group"}
func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
var req dto.ChangeGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -153,6 +221,14 @@ func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
helper.Success(c)
}
// @Tags Host
// @Summary Get host by ID
// @Accept json
// @Param request body dto.OperateByID true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/info [post]
func (b *BaseApi) GetHostByID(c *gin.Context) {
var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+63 -16
View File
@@ -9,13 +9,13 @@ import (
// @Tags Host tool
// @Summary Get tool status
// @Accept json
// @Param request body request.HostToolReq true "request"
// @Param request body request.HostToolTypeReq true "request"
// @Success 200 {object} response.HostToolRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tool [post]
// @Router /hosts/tool/status [post]
func (b *BaseApi) GetToolStatus(c *gin.Context) {
var req request.HostToolReq
var req request.HostToolTypeReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -53,14 +53,14 @@ func (b *BaseApi) InitToolConfig(c *gin.Context) {
// @Tags Host tool
// @Summary Operate tool
// @Accept json
// @Param request body request.HostToolReq true "request"
// @Param request body request.HostToolOperateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tool/operate [post]
// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"}
func (b *BaseApi) OperateTool(c *gin.Context) {
var req request.HostToolReq
var req request.HostToolOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -75,19 +75,18 @@ func (b *BaseApi) OperateTool(c *gin.Context) {
// @Tags Host tool
// @Summary Get tool config
// @Accept json
// @Param request body request.HostToolConfig true "request"
// @Param request body request.HostToolTypeReq true "request"
// @Success 200 {object} response.HostToolConfig
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tool/config [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"}
func (b *BaseApi) OperateToolConfig(c *gin.Context) {
var req request.HostToolConfig
// @Router /hosts/tool/config/get [post]
func (b *BaseApi) GetToolConfig(c *gin.Context) {
var req request.HostToolTypeReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
config, err := hostToolService.OperateToolConfig(req)
config, err := hostToolService.GetToolConfig(req)
if err != nil {
helper.InternalServer(c, err)
return
@@ -95,6 +94,28 @@ func (b *BaseApi) OperateToolConfig(c *gin.Context) {
helper.SuccessWithData(c, config)
}
// @Tags Host tool
// @Summary Update tool config
// @Accept json
// @Param request body request.HostToolConfigUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tool/config/set [post]
// @x-panel-log {"bodyKeys":["type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 [type] 主机工具配置文件 ","formatEN":"update [type] tool config"}
func (b *BaseApi) UpdateToolConfig(c *gin.Context) {
var req request.HostToolConfigUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := hostToolService.UpdateToolConfig(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Host tool
// @Summary Create Supervisor process
// @Accept json
@@ -137,18 +158,44 @@ func (b *BaseApi) GetProcess(c *gin.Context) {
// @Tags Host tool
// @Summary Get Supervisor process config file
// @Accept json
// @Param request body request.SupervisorProcessFileReq true "request"
// @Param request body request.HostSupervisorProcessFileGetReq true "request"
// @Success 200 {string} content
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tool/supervisor/process/file [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
// @Router /hosts/tool/supervisor/process/file/get [post]
func (b *BaseApi) GetProcessFile(c *gin.Context) {
var req request.SupervisorProcessFileReq
var req request.HostSupervisorProcessFileGetReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := hostToolService.OperateSupervisorProcessFile(req)
content, err := hostToolService.GetSupervisorProcessFile(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, content)
}
// @Tags Host tool
// @Summary Operate Supervisor process config file
// @Accept json
// @Param request body request.HostSupervisorProcessFileOperateReq true "request"
// @Success 200 {string} content
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/tool/supervisor/process/file [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
func (b *BaseApi) OperateProcessFile(c *gin.Context) {
var req request.HostSupervisorProcessFileOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
content, err := hostToolService.OperateSupervisorProcessFile(request.SupervisorProcessFileReq{
Name: req.Name,
Operate: req.Operate,
Content: req.Content,
File: req.File,
})
if err != nil {
helper.InternalServer(c, err)
return
+13 -26
View File
@@ -31,27 +31,6 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
helper.SuccessWithDataGzipped(c, data)
}
// @Tags Monitor
// @Summary Load monitor data
// @Param request body dto.MonitorGPUSearch true "request"
// @Success 200 {object} dto.MonitorGPUData
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/gpu/search [post]
func (b *BaseApi) LoadGPUMonitor(c *gin.Context) {
var req dto.MonitorGPUSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := monitorService.LoadGPUMonitorData(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags Monitor
// @Summary Clean monitor data
// @Success 200
@@ -91,7 +70,7 @@ func (b *BaseApi) LoadMonitorSetting(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/setting/update [post]
// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改默认监控网卡 [name]-[value]","formatEN":"update default monitor [name]-[value]"}
// @x-panel-log {"bodyKeys":["key", "value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改默认监控网卡 [key]-[value]","formatEN":"update default monitor [key]-[value]"}
func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
var req dto.MonitorSettingUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -105,6 +84,12 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
helper.Success(c)
}
// @Tags Monitor
// @Summary Get network options
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/netoptions [get]
func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
netStat, _ := net.IOCounters(true)
var options []string
@@ -116,6 +101,12 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
helper.SuccessWithData(c, options)
}
// @Tags Monitor
// @Summary Get IO options
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/monitor/iooptions [get]
func (b *BaseApi) GetIOOptions(c *gin.Context) {
diskStat, _ := disk.IOCounters()
var options []string
@@ -126,7 +117,3 @@ func (b *BaseApi) GetIOOptions(c *gin.Context) {
sort.Strings(options)
helper.SuccessWithData(c, options)
}
func (b *BaseApi) GetCPUOptions(c *gin.Context) {
helper.SuccessWithData(c, monitorService.LoadGPUOptions())
}
+11
View File
@@ -5,9 +5,20 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket"
"github.com/gin-gonic/gin"
"github.com/gorilla/websocket"
)
// @Tags Process
// @Summary Process ws
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /process/ws [get]
func (b *BaseApi) ProcessWs(c *gin.Context) {
if !websocket.IsWebSocketUpgrade(c.Request) {
helper.Success(c)
return
}
ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
if err != nil {
return
+5 -4
View File
@@ -61,7 +61,7 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /runtimes/del [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [name]","formatEN":"Delete runtime [name]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除运行环境 [id]","formatEN":"Delete runtime [id]"}
func (b *BaseApi) DeleteRuntime(c *gin.Context) {
var req request.RuntimeDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -81,6 +81,7 @@ func (b *BaseApi) DeleteRuntime(c *gin.Context) {
// @Success 200 {array} dto.AppResource
// @Security ApiKeyAuth
// @Security Timestamp
// @Param id path integer true "id"
// @Router /runtimes/installed/delete/check/:id [get]
func (b *BaseApi) DeleteRuntimeCheck(c *gin.Context) {
runTimeId, err := helper.GetIntParamByKey(c, "id")
@@ -120,7 +121,7 @@ func (b *BaseApi) UpdateRuntime(c *gin.Context) {
// @Tags Runtime
// @Summary Get runtime
// @Accept json
// @Param id path string true "request"
// @Param id path integer true "request"
// @Success 200 {object} response.RuntimeDTO
// @Security ApiKeyAuth
// @Security Timestamp
@@ -168,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /runtimes/operate [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [name]","formatEN":"Operate runtime [name]"}
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [id]","formatEN":"Operate runtime [id]"}
func (b *BaseApi) OperateRuntime(c *gin.Context) {
var req request.RuntimeOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -242,7 +243,7 @@ func (b *BaseApi) SyncStatus(c *gin.Context) {
// @Tags Runtime
// @Summary Get php runtime extension
// @Accept json
// @Param id path string true "request"
// @Param id path integer true "request"
// @Success 200 {object} response.PHPExtensionRes
// @Security ApiKeyAuth
// @Security Timestamp
+108 -30
View File
@@ -1,15 +1,12 @@
package v2
import (
"encoding/json"
"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/model"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/gin-gonic/gin"
"github.com/pkg/errors"
)
// @Tags System Setting
@@ -27,6 +24,12 @@ func (b *BaseApi) GetSettingInfo(c *gin.Context) {
helper.SuccessWithData(c, setting)
}
// @Tags System Setting
// @Summary Get terminal AI setting info
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/terminal/ai/search [post]
func (b *BaseApi) GetTerminalAISettingInfo(c *gin.Context) {
setting, err := settingService.GetTerminalAIInfo()
if err != nil {
@@ -49,14 +52,14 @@ func (b *BaseApi) GetSystemAvailable(c *gin.Context) {
// @Tags System Setting
// @Summary Update system setting
// @Accept json
// @Param request body dto.SettingUpdate true "request"
// @Param request body dto.AgentSettingUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/update [post]
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统配置 [key] => [value]","formatEN":"update system setting [key] => [value]"}
func (b *BaseApi) UpdateSetting(c *gin.Context) {
var req dto.SettingUpdate
var req dto.AgentSettingUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
@@ -68,6 +71,15 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
helper.Success(c)
}
// @Tags System Setting
// @Summary Update terminal AI setting
// @Accept json
// @Param request body dto.TerminalAIInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/terminal/ai/update [post]
// @x-panel-log {"bodyKeys":["aiStatus","aiAccountId"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新终端 AI 设置 [aiStatus][aiAccountId]","formatEN":"update terminal AI setting [aiStatus][aiAccountId]"}
func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
var req dto.TerminalAIInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -81,6 +93,87 @@ func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
helper.Success(c)
}
// @Tags System Setting
// @Summary Get file manage AI setting info
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/files/ai/search [post]
func (b *BaseApi) GetFileManageAISettingInfo(c *gin.Context) {
setting, err := settingService.GetFileManageAIInfo()
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, setting)
}
// @Tags System Setting
// @Summary Update file manage AI setting
// @Accept json
// @Param request body dto.FileManageAIInfo true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/files/ai/update [post]
// @x-panel-log {"bodyKeys":["aiStatus","aiAccountId"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新文件管理 AI 设置 [aiStatus][aiAccountId]","formatEN":"update file manage AI setting [aiStatus][aiAccountId]"}
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 website dir
// @Success 200 {string} path
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/website/dir [get]
func (b *BaseApi) LoadWebsiteDir(c *gin.Context) {
helper.SuccessWithData(c, settingService.GetWebsiteDir())
}
// @Tags System Setting
// @Summary Load local backup dir
// @Success 200 {string} path
@@ -101,6 +194,12 @@ func (b *BaseApi) LoadLocalConn(c *gin.Context) {
helper.SuccessWithData(c, settingService.GetLocalConn())
}
// @Tags System Setting
// @Summary Check local conn
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/ssh/check [post]
func (b *BaseApi) CheckLocalConn(c *gin.Context) {
client, err := loadLocalConn()
if err == nil && client != nil {
@@ -116,7 +215,7 @@ func (b *BaseApi) CheckLocalConn(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/ssh/conn/default [post]
// @Router /settings/ssh/default [post]
// @x-panel-log {"bodyKeys":["defaultConn"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"本地终端默认连接 [defaultConn]","formatEN":"update system default conn [defaultConn]"}
func (b *BaseApi) SetDefaultIsConn(c *gin.Context) {
var req dto.SSHDefaultConn
@@ -166,12 +265,8 @@ func (b *BaseApi) SaveLocalConn(c *gin.Context) {
}
func loadLocalConn() (*ssh.SSHClient, error) {
connInfoInDB := settingService.GetSettingByKey("LocalSSHConn")
if len(connInfoInDB) == 0 {
return nil, errors.New("no such ssh conn info in db!")
}
var connInDB model.LocalConnInfo
if err := json.Unmarshal([]byte(connInfoInDB), &connInDB); err != nil {
connInDB, err := settingService.GetLocalConnForSSH()
if err != nil {
return nil, err
}
sshInfo := ssh.ConnInfo{
@@ -186,23 +281,6 @@ func loadLocalConn() (*ssh.SSHClient, error) {
return ssh.NewClient(sshInfo)
}
// @Tags System Setting
// @Summary Load system setting by key
// @Param key path string true "key"
// @Success 200 {object} dto.SettingInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /settings/get/{key} [get]
func (b *BaseApi) GetSettingByKey(c *gin.Context) {
key := c.Param("key")
if len(key) == 0 {
helper.BadRequest(c, errors.New("key is empty"))
return
}
value := settingService.GetSettingByKey(key)
helper.SuccessWithData(c, value)
}
// @Tags System Setting
// @Summary Save common description
// @Accept json
+2 -2
View File
@@ -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
}
+27
View File
@@ -3,6 +3,8 @@ package v2
import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/gin-gonic/gin"
)
@@ -30,6 +32,31 @@ func (b *BaseApi) PageTasks(c *gin.Context) {
})
}
// @Tags TaskLog
// @Summary Read task log by Line
// @Param request body request.TaskLogReadReq true "request"
// @Success 200 {object} response.FileLineContent
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /logs/tasks/read [post]
func (b *BaseApi) ReadTaskLogByLine(c *gin.Context) {
var req request.TaskLogReadReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
var res *response.FileLineContent
res, err := taskService.ReadByLine(req)
if err != nil {
helper.InternalServer(c, err)
return
}
if res.TotalLines > 100 {
helper.SuccessWithDataGzipped(c, res)
} else {
helper.SuccessWithData(c, res)
}
}
// @Tags TaskLog
// @Summary Get the number of executing tasks
// @Success 200 {integer} int64
+44
View File
@@ -6,6 +6,14 @@ import (
"github.com/gin-gonic/gin"
)
// @Tags TensorRT LLM
// @Summary Page TensorRT LLMs
// @Accept json
// @Param request body request.TensorRTLLMSearch true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/tensorrt/search [post]
func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
var req request.TensorRTLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -15,6 +23,15 @@ func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
helper.SuccessWithData(c, list)
}
// @Tags TensorRT LLM
// @Summary Create TensorRT LLM
// @Accept json
// @Param request body request.TensorRTLLMCreate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/tensorrt/create [post]
// @x-panel-log {"bodyKeys":["name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"创建 TensorRT LLM [name]","formatEN":"create TensorRT LLM [name]"}
func (b *BaseApi) CreateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -28,6 +45,15 @@ func (b *BaseApi) CreateTensorRTLLM(c *gin.Context) {
helper.Success(c)
}
// @Tags TensorRT LLM
// @Summary Update TensorRT LLM
// @Accept json
// @Param request body request.TensorRTLLMUpdate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/tensorrt/update [post]
// @x-panel-log {"bodyKeys":["id","name"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"更新 TensorRT LLM [id][name]","formatEN":"update TensorRT LLM [id][name]"}
func (b *BaseApi) UpdateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -41,6 +67,15 @@ func (b *BaseApi) UpdateTensorRTLLM(c *gin.Context) {
helper.Success(c)
}
// @Tags TensorRT LLM
// @Summary Delete TensorRT LLM
// @Accept json
// @Param request body request.TensorRTLLMDelete true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/tensorrt/delete [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"删除 TensorRT LLM [id]","formatEN":"delete TensorRT LLM [id]"}
func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -54,6 +89,15 @@ func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
helper.Success(c)
}
// @Tags TensorRT LLM
// @Summary Operate TensorRT LLM
// @Accept json
// @Param request body request.TensorRTLLMOperate true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/tensorrt/operate [post]
// @x-panel-log {"bodyKeys":["id","operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作 TensorRT LLM [id][operate]","formatEN":"operate TensorRT LLM [id][operate]"}
func (b *BaseApi) OperateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+155 -106
View File
@@ -8,7 +8,9 @@ import (
"strconv"
"time"
"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/model"
"github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -19,117 +21,52 @@ import (
"github.com/pkg/errors"
)
func (b *BaseApi) WsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return
}
defer wsConn.Close()
if global.CONF.Base.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
var client *ssh.SSHClient
if hostID > 0 {
host, err := service.GetHostInfo(uint(hostID))
if wshandleError(wsConn, errors.WithMessage(err, "load host info by id failed")) {
return
}
connInfo := ssh.ConnInfo{
Addr: host.Addr,
Port: int(host.Port),
User: host.User,
AuthMode: host.AuthMode,
Password: host.Password,
PrivateKey: []byte(host.PrivateKey),
}
if len(host.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(host.PassPhrase)
}
client, err = ssh.NewClient(connInfo)
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
} else {
client, err = loadLocalConn()
if wshandleError(wsConn, errors.WithMessage(err, "failed to set up the connection. Please check the host information")) {
return
}
}
defer client.Close()
command := c.DefaultQuery("command", "")
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
if wshandleError(wsConn, err) {
return
}
defer sws.Close()
quitChan := make(chan bool, 3)
sws.Start(quitChan)
go sws.Wait(quitChan)
<-quitChan
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
// @Tags Terminal
// @Summary Ws local terminal
// @Param command query string false "command"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/local [get]
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
}
func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
// @Tags Terminal
// @Summary Ws host SSH
// @Param id query integer false "id"
// @Param command query string false "command"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/ssh [get]
func (b *BaseApi) WsHostSSH(c *gin.Context) {
b.runSSHSession(c, func() (*ssh.SSHClient, error) {
hostID, _ := strconv.Atoi(c.DefaultQuery("id", "0"))
if hostID <= 0 {
return nil, errors.New("missing host id")
}
host, err := service.GetHostInfo(uint(hostID))
return newHostSSHClient(host, err)
}, c.DefaultQuery("command", ""))
}
// @Tags Terminal
// @Summary Ws container terminal
// @Param cols query integer false "cols"
// @Param rows query integer false "rows"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /hosts/terminal/container [get]
func (b *BaseApi) WsContainerTerminal(c *gin.Context) {
wsConn, cols, rows, ok := prepareTerminalSession(c)
if !ok {
return
}
defer wsConn.Close()
if global.CONF.Base.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return
}
source := c.Query("source")
var initCmd []string
switch source {
case "redis", "redis-cluster":
initCmd, err = loadRedisInitCmd(c, source)
case "ollama":
initCmd, err = loadOllamaInitCmd(c)
case "container":
initCmd, err = loadContainerInitCmd(c)
case "database":
initCmd, err = loadDatabaseInitCmd(c)
default:
if wshandleError(wsConn, fmt.Errorf("not support such source %s", source)) {
return
}
}
if wshandleError(wsConn, err) {
return
}
slave, err := terminal.NewCommand("docker", initCmd...)
slave, err := loadContainerTerminalCommand(c)
if wshandleError(wsConn, err) {
return
}
@@ -147,10 +84,112 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
<-quitChan
global.LOG.Info("websocket finished")
closeTerminalConn(wsConn)
}
func prepareTerminalSession(c *gin.Context) (*websocket.Conn, int, int, bool) {
if !websocket.IsWebSocketUpgrade(c.Request) {
helper.Success(c)
return nil, 0, 0, false
}
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil)
if err != nil {
global.LOG.Errorf("gin context http handler failed, err: %v", err)
return nil, 0, 0, false
}
if global.CONF.Base.IsDemo {
if wshandleError(wsConn, errors.New(" demo server, prohibit this operation!")) {
return nil, 0, 0, false
}
}
cols, err := strconv.Atoi(c.DefaultQuery("cols", "80"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param cols in request")) {
return nil, 0, 0, false
}
rows, err := strconv.Atoi(c.DefaultQuery("rows", "40"))
if wshandleError(wsConn, errors.WithMessage(err, "invalid param rows in request")) {
return nil, 0, 0, false
}
return wsConn, cols, rows, true
}
func (b *BaseApi) runSSHSession(c *gin.Context, connect func() (*ssh.SSHClient, error), command string) {
wsConn, cols, rows, ok := prepareTerminalSession(c)
if !ok {
return
}
defer wsConn.Close()
client, clientErr := connect()
if wshandleError(wsConn, errors.WithMessage(clientErr, "failed to set up the connection. Please check the host information")) {
return
}
defer client.Close()
sws, err := terminal.NewLogicSshWsSession(cols, rows, client.Client, wsConn, command)
if wshandleError(wsConn, err) {
return
}
defer sws.Close()
quitChan := make(chan bool, 3)
sws.Start(quitChan)
go sws.Wait(quitChan)
<-quitChan
closeTerminalConn(wsConn)
}
func closeTerminalConn(wsConn *websocket.Conn) {
dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt)
}
func newHostSSHClient(host *model.Host, err error) (*ssh.SSHClient, error) {
if err != nil {
return nil, errors.WithMessage(err, "load host info by id failed")
}
connInfo := ssh.ConnInfo{
Addr: host.Addr,
Port: int(host.Port),
User: host.User,
AuthMode: host.AuthMode,
Password: host.Password,
PrivateKey: []byte(host.PrivateKey),
}
if len(host.PassPhrase) != 0 {
connInfo.PassPhrase = []byte(host.PassPhrase)
}
return ssh.NewClient(connInfo)
}
func loadContainerTerminalCommand(c *gin.Context) (*terminal.LocalCommand, error) {
source := c.Query("source")
var (
initCmd []string
err error
)
switch source {
case "redis", "redis-cluster":
initCmd, err = loadRedisInitCmd(c, source)
case "ollama":
initCmd, err = loadOllamaInitCmd(c)
case "container":
initCmd, err = loadContainerInitCmd(c)
case "database":
initCmd, err = loadDatabaseInitCmd(c)
default:
return nil, fmt.Errorf("not support such source %s", source)
}
if err != nil {
return nil, err
}
return terminal.NewCommand("docker", initCmd...)
}
func loadRedisInitCmd(c *gin.Context, redisType string) ([]string, error) {
name := c.Query("name")
from := c.Query("from")
@@ -213,19 +252,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}
}
+42 -9
View File
@@ -181,6 +181,7 @@ func (b *BaseApi) GetWebsite(c *gin.Context) {
// @Success 200 {object} response.FileInfo
// @Security ApiKeyAuth
// @Security Timestamp
// @Param type path string true "type"
// @Router /websites/:id/config/:type [get]
func (b *BaseApi) GetWebsiteNginx(c *gin.Context) {
id, err := helper.GetParamID(c)
@@ -269,6 +270,7 @@ func (b *BaseApi) GetHTTPSConfig(c *gin.Context) {
// @Success 200 {object} response.WebsiteHTTPS
// @Security ApiKeyAuth
// @Security Timestamp
// @Param id path string true "id"
// @Router /websites/:id/https [post]
// @x-panel-log {"bodyKeys":["websiteId"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"websiteId","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"更新网站 [domain] https 配置","formatEN":"Update website https [domain] conf"}
func (b *BaseApi) UpdateHTTPSConfig(c *gin.Context) {
@@ -330,25 +332,45 @@ func (b *BaseApi) UpdateWebsiteNginxConfig(c *gin.Context) {
}
// @Tags Website
// @Summary Operate website log
// @Summary Get website log
// @Accept json
// @Param request body request.WebsiteLogReq true "request"
// @Param request body request.WebsiteLogSearchReq true "request"
// @Success 200 {object} response.WebsiteLog
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/log [post]
// @Router /websites/log/search [post]
func (b *BaseApi) GetWebsiteLog(c *gin.Context) {
var req request.WebsiteLogSearchReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := websiteService.GetWebsiteLog(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags Website
// @Summary Operate website log
// @Accept json
// @Param request body request.WebsiteLogReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/log/operate [post]
// @x-panel-log {"bodyKeys":["id", "operate"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"websites","output_column":"primary_domain","output_value":"domain"}],"formatZH":"[domain][operate] 日志","formatEN":"[domain][operate] logs"}
func (b *BaseApi) OpWebsiteLog(c *gin.Context) {
var req request.WebsiteLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := websiteService.OpWebsiteLog(req)
if err != nil {
if err := websiteService.OpWebsiteLog(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
helper.Success(c)
}
// @Tags Website
@@ -799,6 +821,7 @@ func (b *BaseApi) GetDirConfig(c *gin.Context) {
// @Success 200 {object} response.WebsiteHtmlRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Param type path string true "type"
// @Router /websites/default/html/:type [get]
func (b *BaseApi) GetDefaultHtml(c *gin.Context) {
resourceType, err := helper.GetStrParamByKey(c, "type")
@@ -838,11 +861,11 @@ func (b *BaseApi) UpdateDefaultHtml(c *gin.Context) {
// @Tags Website
// @Summary Get website upstreams
// @Accept json
// @Param request body request.WebsiteCommonReq true "request"
// @Param id path integer true "id"
// @Success 200 {array} dto.NginxUpstream
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/lbs [get]
// @Router /websites/{id}/lbs [get]
func (b *BaseApi) GetLoadBalances(c *gin.Context) {
id, err := helper.GetParamID(c)
if err != nil {
@@ -937,6 +960,15 @@ func (b *BaseApi) UpdateLoadBalanceFile(c *gin.Context) {
helper.Success(c)
}
// @Tags Website
// @Summary Change website group
// @Accept json
// @Param request body dto.UpdateGroup true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/group/change [post]
// @x-panel-log {"bodyKeys":["group","newGroup"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"网站分组 [group] 迁移到 [newGroup]","formatEN":"change website group [group] to [newGroup]"}
func (b *BaseApi) ChangeWebsiteGroup(c *gin.Context) {
var req dto.UpdateGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -969,6 +1001,7 @@ func (b *BaseApi) UpdateProxyCache(c *gin.Context) {
helper.Success(c)
}
// @Tags Website
// @Summary Get website proxy cache config
// @Accept json
// @Param id path int true "id"
@@ -1295,7 +1328,7 @@ func (b *BaseApi) UpdateStreamConfig(c *gin.Context) {
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/batch/https [post]
// @Router /websites/batch/ssl [post]
func (b *BaseApi) BatchSetHttps(c *gin.Context) {
var req request.BatchWebsiteHttps
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+9
View File
@@ -333,6 +333,15 @@ func (b *BaseApi) DownloadWebsiteSSL(c *gin.Context) {
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file)
}
// @Tags Website SSL
// @Summary Import master SSL
// @Accept json
// @Param request body model.WebsiteSSL true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/import [post]
// @x-panel-log {"bodyKeys":["primaryDomain"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"导入主节点证书 [primaryDomain]","formatEN":"import master SSL [primaryDomain]"}
func (b *BaseApi) ImportMasterSSL(c *gin.Context) {
var req model.WebsiteSSL
if err := helper.CheckBindAndValidate(&req, c); err != nil {
+444 -112
View File
@@ -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"`
@@ -26,31 +27,114 @@ type AgentCreateReq struct {
DockerCompose string `json:"dockerCompose"`
}
type AgentBatchInstallReq struct {
Name string `json:"name"`
Remark string `json:"remark"`
AppVersion string `json:"appVersion" 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 hermes-agent"`
Model string `json:"model"`
AccountID uint `json:"accountId"`
Token string `json:"token"`
TaskID string `json:"taskID"`
Advanced bool `json:"advanced"`
ContainerName string `json:"containerName"`
AllowPort bool `json:"allowPort"`
SpecifyIP string `json:"specifyIP"`
RestartPolicy string `json:"restartPolicy"`
CpuQuota float64 `json:"cpuQuota"`
MemoryLimit float64 `json:"memoryLimit"`
MemoryUnit string `json:"memoryUnit"`
PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
MasterAccountID uint `json:"masterAccountId"`
FallbackAccessHost string `json:"fallbackAccessHost"`
AccountSnapshot AgentAccountInfo `json:"accountSnapshot"`
AccountModels []AgentAccountModel `json:"accountModels"`
}
type AgentBatchUpgradeReq struct {
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
TargetVersion string `json:"targetVersion" validate:"required"`
Backup bool `json:"backup"`
PullImage bool `json:"pullImage"`
TaskID string `json:"taskID"`
}
type AgentBatchUpgradeResult struct {
AgentID uint `json:"agentID"`
AgentName string `json:"agentName"`
AppInstallID uint `json:"appInstallID"`
Success bool `json:"success"`
Skipped bool `json:"skipped"`
Message string `json:"message"`
}
type AgentBatchSkillInstallReq struct {
AgentType string `json:"agentType" validate:"required,oneof=openclaw hermes-agent"`
SkillName string `json:"skillName" validate:"required"`
PackagePath string `json:"packagePath" validate:"required"`
TaskID string `json:"taskID"`
}
type AgentBatchSkillInstallResult struct {
AgentID uint `json:"agentID"`
AgentName string `json:"agentName"`
AppInstallID uint `json:"appInstallID"`
Success bool `json:"success"`
Skipped bool `json:"skipped"`
Message string `json:"message"`
}
type AgentBatchOperateReq struct {
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
Operate string `json:"operate" validate:"required,oneof=start stop restart delete"`
ForceDelete bool `json:"forceDelete"`
TaskID string `json:"taskID"`
}
type AgentBatchOperateResult struct {
AgentID uint `json:"agentID"`
AgentName string `json:"agentName"`
AppInstallID uint `json:"appInstallID"`
Success bool `json:"success"`
Skipped bool `json:"skipped"`
Message string `json:"message"`
}
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 +147,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 +187,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"`
@@ -158,19 +352,24 @@ type AgentAccountSearch struct {
Name string `json:"name"`
}
type AgentAccountProviderCountReq struct {
Providers []string `json:"providers"`
}
type AgentAccountInfo struct {
ID uint `json:"id"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
ID uint `json:"id"`
MasterAccountID uint `json:"masterAccountId"`
Provider string `json:"provider"`
ProviderName string `json:"providerName"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
RememberAPIKey bool `json:"rememberApiKey"`
BaseURL string `json:"baseUrl"`
Models []AgentAccountModel `json:"models"`
APIType string `json:"apiType"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
}
type ProviderModelInfo struct {
@@ -195,12 +394,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 +413,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 +431,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 +687,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 local-hub"`
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 +724,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 local-hub"`
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"`
}
+19 -6
View File
@@ -2,8 +2,9 @@ package dto
import (
"encoding/json"
"github.com/1Panel-dev/1Panel/agent/app/model"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
)
type CreateOrUpdateAlert struct {
@@ -35,6 +36,15 @@ type AlertSearch struct {
Method string `json:"method"`
}
type AlertConfigQuery struct {
ExcludeTypes []string `json:"excludeTypes"`
}
type AlertConfigPageReq struct {
PageInfo
ExcludeTypes []string `json:"excludeTypes"`
}
type AlertDTO struct {
ID uint `json:"id"`
Type string `json:"type"`
@@ -46,6 +56,8 @@ type AlertDTO struct {
Status string `json:"status"`
SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"`
CreateUser string `json:"createUser"`
UpdateUser string `json:"updateUser"`
CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"`
}
@@ -281,11 +293,12 @@ type OfflineQueryRequest struct {
}
type AlertConfigUpdate struct {
ID uint `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Status string `json:"status"`
Config string `json:"config"`
ID uint `json:"id"`
Type string `json:"type"`
Title string `json:"title"`
Status string `json:"status"`
Config string `json:"config"`
DisplayName string `json:"displayName"`
}
type AlertConfigTest struct {
+11 -10
View File
@@ -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 {
+78 -1
View File
@@ -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"`
+1
View File
@@ -7,6 +7,7 @@ import (
type SearchTaskLogReq struct {
Status string `json:"status"`
Type string `json:"type"`
TaskID string `json:"taskID"`
PageInfo
}
+48 -1
View File
@@ -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 {
@@ -88,6 +121,7 @@ type FileWget struct {
Path string `json:"path" validate:"required"`
Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"`
}
type FileMove struct {
@@ -129,13 +163,20 @@ type FileRoleUpdate struct {
type FileReadByLineReq struct {
Page int `json:"page" validate:"required"`
PageSize int `json:"pageSize" validate:"required"`
Type string `json:"type" validate:"required"`
Type string `json:"type"`
ID uint `json:"ID"`
Name string `json:"name"`
Latest bool `json:"latest"`
TaskReq
}
type TaskLogReadReq struct {
Page int `json:"page" validate:"required,min=1"`
PageSize int `json:"pageSize" validate:"required,min=1"`
Latest bool `json:"latest"`
TaskReq
}
type TaskReq struct {
TaskID string `json:"taskID"`
TaskType string `json:"taskType"`
@@ -172,3 +213,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"`
}
+52
View File
@@ -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"`
}
+19 -8
View File
@@ -1,8 +1,12 @@
package request
type HostToolReq struct {
type HostToolTypeReq struct {
Type string `json:"type" validate:"required,oneof=supervisord"`
}
type HostToolOperateReq struct {
Type string `json:"type" validate:"required,oneof=supervisord"`
Operate string `json:"operate" validate:"oneof=status restart start stop"`
Operate string `json:"operate" validate:"required,oneof=restart start stop"`
}
type HostToolCreate struct {
@@ -15,13 +19,8 @@ type SupervisorConfig struct {
ServiceName string `json:"serviceName"`
}
type HostToolLogReq struct {
Type string `json:"type" validate:"required,oneof=supervisord"`
}
type HostToolConfig struct {
type HostToolConfigUpdate struct {
Type string `json:"type" validate:"required,oneof=supervisord"`
Operate string `json:"operate" validate:"oneof=get set"`
Content string `json:"content"`
}
@@ -43,3 +42,15 @@ type SupervisorProcessFileReq struct {
Content string `json:"content"`
File string `json:"file" validate:"required,oneof=out.log err.log config"`
}
type HostSupervisorProcessFileGetReq struct {
Name string `json:"name" validate:"required"`
File string `json:"file" validate:"required,oneof=out.log err.log config"`
}
type HostSupervisorProcessFileOperateReq struct {
Name string `json:"name" validate:"required"`
Operate string `json:"operate" validate:"required,oneof=clear update"`
Content string `json:"content"`
File string `json:"file" validate:"required,oneof=out.log err.log config"`
}
-1
View File
@@ -1 +0,0 @@
package request
+7 -2
View File
@@ -223,9 +223,14 @@ type WebsiteNginxUpdate struct {
}
type WebsiteLogReq struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required,oneof=enable disable delete"`
LogType string `json:"logType" validate:"required,oneof=access.log error.log"`
}
type WebsiteLogSearchReq struct {
ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"`
LogType string `json:"logType" validate:"required"`
LogType string `json:"logType" validate:"required,oneof=access.log error.log"`
Page int `json:"page"`
PageSize int `json:"pageSize"`
}
+4 -5
View File
@@ -6,7 +6,7 @@ type WebsiteSSLSearch struct {
dto.PageInfo
AcmeAccountID string `json:"acmeAccountID"`
Domain string `json:"domain"`
OrderBy string `json:"orderBy" validate:"omitempty,oneof=created_at expire_date"`
OrderBy string `json:"orderBy" validate:"omitempty,oneof=created_at updated_at expire_date"`
Order string `json:"order" validate:"omitempty,oneof=null ascending descending"`
}
@@ -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 {
@@ -62,7 +61,7 @@ type WebsiteSSLObtain struct {
type WebsiteAcmeAccountCreate struct {
Email string `json:"email" validate:"required"`
Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google custom"`
KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"`
KeyType string `json:"keyType" validate:"required,oneof=EC256 EC384 RSA2048 RSA3072 RSA4096 RSA8192"`
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
UseProxy bool `json:"useProxy"`
@@ -139,7 +138,7 @@ type WebsiteCACreate struct {
Organization string `json:"organization" validate:"required"`
OrganizationUint string `json:"organizationUint"`
Name string `json:"name" validate:"required"`
KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"`
KeyType string `json:"keyType" validate:"required,oneof=EC256 EC384 RSA2048 RSA3072 RSA4096 RSA8192"`
Province string `json:"province" `
City string `json:"city"`
}
@@ -147,7 +146,7 @@ type WebsiteCACreate struct {
type WebsiteCAObtain struct {
ID uint `json:"id" validate:"required"`
Domains string `json:"domains" validate:"required"`
KeyType string `json:"keyType" validate:"required,oneof=P256 P384 2048 3072 4096 8192"`
KeyType string `json:"keyType" validate:"required,oneof=EC256 EC384 RSA2048 RSA3072 RSA4096 RSA8192"`
Time int `json:"time" validate:"required"`
Unit string `json:"unit" validate:"required"`
PushDir bool `json:"pushDir"`
+38
View File
@@ -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"`
}
+31
View File
@@ -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"`
}
+15 -1
View File
@@ -23,7 +23,10 @@ type SettingInfo struct {
AppStoreLastModified string `json:"appStoreLastModified"`
AppStoreSyncStatus string `json:"appStoreSyncStatus"`
FileRecycleBin string `json:"fileRecycleBin"`
FileRecycleBin string `json:"fileRecycleBin"`
LocalSSHConnShow string `json:"localSSHConnShow"`
FirewallPortWhiteList string `json:"firewallPortWhiteList"`
}
type SettingUpdate struct {
@@ -31,6 +34,11 @@ type SettingUpdate struct {
Value string `json:"value"`
}
type AgentSettingUpdate struct {
Key string `json:"key" validate:"required,oneof=SystemIP DockerSockPath FileRecycleBin FirewallPortWhiteList"`
Value string `json:"value"`
}
type SyncTime struct {
NtpSite string `json:"ntpSite" validate:"required"`
}
@@ -55,6 +63,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 +104,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"`
+10 -3
View File
@@ -4,7 +4,6 @@ import "time"
type SSHUpdate struct {
Key string `json:"key" validate:"required"`
OldValue string `json:"oldValue"`
NewValue string `json:"newValue"`
}
@@ -50,10 +49,18 @@ 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"`
Status string `json:"Status" validate:"required,oneof=Success Failed All"`
Info string `json:"info"`
Status string `json:"Status" validate:"required,oneof=Success Failed All"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
}
type SSHHistory struct {
+2
View File
@@ -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"`
}
+9 -8
View File
@@ -2,14 +2,15 @@ package model
type AgentAccount struct {
BaseModel
Provider string `json:"provider"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"`
RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
Provider string `json:"provider"`
Name string `json:"name"`
APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"`
RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"`
Remark string `json:"remark"`
MasterAccountID uint `json:"masterAccountId" gorm:"index"`
}
func (AgentAccount) TableName() string {
+11 -7
View File
@@ -9,9 +9,11 @@ type Alert struct {
Count uint `gorm:"type:integer;not null" json:"count"`
Project string `gorm:"type:varchar(64)" json:"project"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Method string `gorm:"type:varchar(64);not null" json:"method"`
Method string `gorm:"type:text;not null" json:"method"`
SendCount uint `gorm:"type:integer" json:"sendCount"`
AdvancedParams string `gorm:"type:longText" json:"advancedParams"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
type AlertTask struct {
@@ -19,7 +21,7 @@ type AlertTask struct {
Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
}
type AlertLog struct {
@@ -34,15 +36,17 @@ type AlertLog struct {
Message string `gorm:"type:varchar(256);" json:"message"`
RecordId uint `gorm:"type:integer;" json:"recordId"`
LicenseId string `gorm:"type:varchar(256);not null;" json:"licenseId" `
Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"`
Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
}
type AlertConfig struct {
BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"`
Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"`
CreateUser string `gorm:"type:varchar(256)" json:"createUser"`
UpdateUser string `gorm:"type:varchar(256)" json:"updateUser"`
}
type LoginLog struct {
+12
View File
@@ -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"`
}
+19
View File
@@ -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"`
}
+14
View File
@@ -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"`
}
+1 -1
View File
@@ -8,7 +8,7 @@ type WebsiteAcmeAccount struct {
Type string `gorm:"not null;default:letsencrypt" json:"type"`
EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"`
KeyType string `gorm:"not null;default:2048" json:"keyType"`
KeyType string `gorm:"not null;default:RSA2048" json:"keyType"`
UseProxy bool `gorm:"default:false" json:"useProxy"`
CaDirURL string `json:"caDirURL"`
UseEAB bool `json:"useEAB"`
+1 -1
View File
@@ -5,5 +5,5 @@ type WebsiteCA struct {
CSR string `gorm:"not null;" json:"csr"`
Name string `gorm:"not null;" json:"name"`
PrivateKey string `gorm:"not null" json:"privateKey"`
KeyType string `gorm:"not null;default:2048" json:"keyType"`
KeyType string `gorm:"not null;default:RSA2048" json:"keyType"`
}
+35 -37
View File
@@ -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
View File
@@ -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
}
+7 -16
View File
@@ -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)
+8
View File
@@ -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 {
+60 -1
View File
@@ -1,16 +1,23 @@
package repo
import "github.com/1Panel-dev/1Panel/agent/app/model"
import (
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"gorm.io/gorm"
)
type AgentAccountRepo struct{}
type IAgentAccountRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error)
GetFirst(opts ...DBOption) (*model.AgentAccount, error)
WithByMasterAccountID(masterID uint) DBOption
Create(account *model.AgentAccount) error
Save(account *model.AgentAccount) error
DeleteByID(id uint) error
List(opts ...DBOption) ([]model.AgentAccount, error)
CountByProviders(providers []string) (map[string]int64, error)
}
func NewIAgentAccountRepo() IAgentAccountRepo {
@@ -34,6 +41,12 @@ func (a AgentAccountRepo) GetFirst(opts ...DBOption) (*model.AgentAccount, error
return &account, nil
}
func (a AgentAccountRepo) WithByMasterAccountID(masterID uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("master_account_id = ?", masterID)
}
}
func (a AgentAccountRepo) Create(account *model.AgentAccount) error {
return getDb().Create(account).Error
}
@@ -53,3 +66,49 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
}
return accounts, nil
}
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
normalizedProviders := normalizeProviders(providers)
counts := make(map[string]int64, len(normalizedProviders))
for _, provider := range normalizedProviders {
counts[provider] = 0
}
if len(normalizedProviders) == 0 {
return counts, nil
}
type providerCount struct {
Provider string
Count int64
}
var rows []providerCount
if err := getDb().
Model(&model.AgentAccount{}).
Select("provider, COUNT(*) as count").
Where("provider IN ?", normalizedProviders).
Group("provider").
Scan(&rows).Error; err != nil {
return nil, err
}
for _, row := range rows {
counts[row.Provider] = row.Count
}
return counts, nil
}
func normalizeProviders(providers []string) []string {
seen := make(map[string]struct{}, len(providers))
result := make([]string, 0, len(providers))
for _, provider := range providers {
provider = strings.TrimSpace(provider)
if provider == "" {
continue
}
if _, ok := seen[provider]; ok {
continue
}
seen[provider] = struct{}{}
result = append(result, provider)
}
return result
}
+235 -14
View File
@@ -1,11 +1,15 @@
package repo
import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
"google.golang.org/genproto/googleapis/type/date"
"gorm.io/gorm"
"strconv"
"time"
)
@@ -20,7 +24,8 @@ type IAlertRepo interface {
WithByCreateAt(date *date.Date) DBOption
WithByLicenseId(licenseId string) DBOption
WithByRecordId(recordId uint) DBOption
WithByMethod(method string) DBOption
WithByAlertMethodContainsConfigID(id uint) DBOption
WithByMethodConfigIDs(ids []uint) DBOption
Create(alert *model.Alert) error
Get(opts ...DBOption) (model.Alert, error)
@@ -47,11 +52,15 @@ type IAlertRepo interface {
GetLicensePushCount(method string) (uint, error)
GetConfig(opts ...DBOption) (model.AlertConfig, error)
GetConfigById(id uint) (model.AlertConfig, error)
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
CreateAlertConfig(config *model.AlertConfig) error
DeleteAlertConfig(opts ...DBOption) error
WithByTypeNotIn(types []string) DBOption
PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error)
SyncAll(data []model.AlertConfig) error
}
@@ -101,9 +110,20 @@ func (a *AlertRepo) WithByRecordId(recordId uint) DBOption {
}
}
func (a *AlertRepo) WithByMethod(method string) DBOption {
func (a *AlertRepo) WithByAlertMethodContainsConfigID(id uint) DBOption {
method := strconv.Itoa(int(id))
return func(g *gorm.DB) *gorm.DB {
return g.Where("method = ?", method)
return g.Where("(method = ? OR method LIKE ? OR method LIKE ? OR method LIKE ?)", method, method+",%", "%,"+method, "%,"+method+",%")
}
}
func (a *AlertRepo) WithByMethodConfigIDs(ids []uint) DBOption {
return func(g *gorm.DB) *gorm.DB {
methods := make([]string, 0, len(ids))
for _, id := range ids {
methods = append(methods, strconv.Itoa(int(id)))
}
return g.Where("method IN ?", methods)
}
}
@@ -306,32 +326,233 @@ func (a *AlertRepo) GetConfig(opts ...DBOption) (model.AlertConfig, error) {
return alertConfig, err
}
func (a *AlertRepo) GetConfigById(id uint) (model.AlertConfig, error) {
var config model.AlertConfig
err := global.AlertDB.First(&config, id).Error
return config, err
}
func (a *AlertRepo) WithByTypeNotIn(types []string) DBOption {
return func(g *gorm.DB) *gorm.DB {
return g.Where("`type` NOT IN (?)", types)
}
}
func (a *AlertRepo) PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error) {
var configs []model.AlertConfig
db := global.AlertDB.Model(&model.AlertConfig{})
for _, opt := range opts {
db = opt(db)
}
count := int64(0)
db = db.Count(&count)
err := db.Limit(size).Offset(size * (page - 1)).Find(&configs).Error
return count, configs, err
}
var singletonTypes = map[string]bool{
constant.CommonConfig: true,
}
func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
tx := global.AlertDB.Begin()
if tx.Error != nil {
return tx.Error
}
defer func() {
if r := recover(); r != nil {
tx.Rollback()
panic(r)
}
}()
var oldConfigs []model.AlertConfig
_ = tx.Find(&oldConfigs).Error
if err := tx.Find(&oldConfigs).Error; err != nil {
tx.Rollback()
return err
}
usedConfigIDs, err := loadUsedAlertConfigIDs(tx)
if err != nil {
tx.Rollback()
return err
}
oldConfigMap := make(map[string]uint)
oldConfigByType := make(map[string][]model.AlertConfig)
oldConfigByKey := make(map[string][]model.AlertConfig)
consumedConfigIDs := make(map[uint]struct{})
for _, item := range oldConfigs {
oldConfigMap[item.Type] = item.ID
if singletonTypes[item.Type] {
oldConfigMap[item.Type] = item.ID
continue
}
oldConfigByType[item.Type] = append(oldConfigByType[item.Type], item)
oldConfigByKey[alertConfigSyncKey(item)] = append(oldConfigByKey[alertConfigSyncKey(item)], item)
}
for _, item := range data {
if val, ok := oldConfigMap[item.Type]; ok {
item.ID = val
delete(oldConfigMap, item.Type)
} else {
item.ID = 0
if singletonTypes[item.Type] {
if val, ok := oldConfigMap[item.Type]; ok {
item.ID = val
delete(oldConfigMap, item.Type)
consumedConfigIDs[item.ID] = struct{}{}
} else {
item.ID = 0
}
if item.ID == 0 {
if err := tx.Create(&item).Error; err != nil {
tx.Rollback()
return err
}
} else if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
return err
}
continue
}
if err := tx.Model(model.AlertConfig{}).Where("id = ?", item.ID).Save(&item).Error; err != nil {
key := alertConfigSyncKey(item)
if matched, ok := popAlertConfigByKey(oldConfigByKey, key); ok {
item.ID = matched.ID
consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
return err
}
deleteAlertConfigByID(oldConfigByType, matched.ID)
continue
}
if matched, ok := popUnusedAlertConfigByType(oldConfigByType, usedConfigIDs, item.Type); ok {
item.ID = matched.ID
consumedConfigIDs[item.ID] = struct{}{}
if err := tx.Save(&item).Error; err != nil {
tx.Rollback()
return err
}
continue
}
item.ID = 0
if err := tx.Create(&item).Error; err != nil {
tx.Rollback()
return err
}
}
for _, val := range oldConfigMap {
if err := tx.Where("id = ?", val).Delete(&model.AlertConfig{}).Error; err != nil {
for _, item := range oldConfigs {
if _, used := usedConfigIDs[item.ID]; used {
continue
}
if _, kept := consumedConfigIDs[item.ID]; kept {
continue
}
if err := tx.Where("id = ?", item.ID).Delete(&model.AlertConfig{}).Error; err != nil {
tx.Rollback()
return err
}
}
tx.Commit()
if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
return nil
}
func loadUsedAlertConfigIDs(tx *gorm.DB) (map[uint]struct{}, error) {
var alerts []model.Alert
if err := tx.Select("method").Find(&alerts).Error; err != nil {
return nil, err
}
usedIDs := make(map[uint]struct{})
for _, alert := range alerts {
for _, item := range strings.Split(alert.Method, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
id, err := strconv.ParseUint(item, 10, 64)
if err != nil {
continue
}
usedIDs[uint(id)] = struct{}{}
}
}
return usedIDs, nil
}
func alertConfigSyncKey(item model.AlertConfig) string {
return item.Type + "::" + normalizeAlertConfigJSON(item.Config)
}
func normalizeAlertConfigJSON(config string) string {
trimmed := strings.TrimSpace(config)
if trimmed == "" {
return ""
}
var data any
if err := json.Unmarshal([]byte(trimmed), &data); err != nil {
return trimmed
}
buf, err := json.Marshal(data)
if err != nil {
return trimmed
}
return string(buf)
}
func popAlertConfigByKey(configMap map[string][]model.AlertConfig, key string) (model.AlertConfig, bool) {
items := configMap[key]
if len(items) == 0 {
return model.AlertConfig{}, false
}
item := items[0]
if len(items) == 1 {
delete(configMap, key)
} else {
configMap[key] = items[1:]
}
return item, true
}
func popUnusedAlertConfigByType(configMap map[string][]model.AlertConfig, usedConfigIDs map[uint]struct{}, configType string) (model.AlertConfig, bool) {
items := configMap[configType]
if len(items) == 0 {
return model.AlertConfig{}, false
}
for idx, item := range items {
if _, used := usedConfigIDs[item.ID]; used {
continue
}
if idx == 0 {
if len(items) == 1 {
delete(configMap, configType)
} else {
configMap[configType] = items[1:]
}
} else {
configMap[configType] = append(items[:idx], items[idx+1:]...)
}
return item, true
}
return model.AlertConfig{}, false
}
func deleteAlertConfigByID(configMap map[string][]model.AlertConfig, id uint) {
for key, items := range configMap {
for idx, item := range items {
if item.ID != id {
continue
}
if len(items) == 1 {
delete(configMap, key)
} else {
configMap[key] = append(items[:idx], items[idx+1:]...)
}
return
}
}
}
+18
View File
@@ -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)
+113
View File
@@ -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)
}
}
+121
View File
@@ -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)
}
}
+77
View File
@@ -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
}
+3
View File
@@ -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
View File
@@ -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)
+10
View File
@@ -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)
+20 -6
View File
@@ -2,8 +2,10 @@ package repo
import (
"context"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"gorm.io/gorm"
)
@@ -21,6 +23,7 @@ type ISSLRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error)
GetFirst(opts ...DBOption) (*model.WebsiteSSL, error)
List(opts ...DBOption) ([]model.WebsiteSSL, error)
TryMarkApplying(id uint) (bool, error)
Create(ctx context.Context, ssl *model.WebsiteSSL) error
Save(ssl *model.WebsiteSSL) error
DeleteBy(opts ...DBOption) error
@@ -76,12 +79,12 @@ func (w WebsiteSSLRepo) Page(page, size int, opts ...DBOption) (int64, []model.W
}
func (w WebsiteSSLRepo) GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) {
var website *model.WebsiteSSL
var website model.WebsiteSSL
db := getDb(opts...).Model(&model.WebsiteSSL{})
if err := db.Preload("AcmeAccount").Preload("DnsAccount").First(&website).Error; err != nil {
return website, err
return nil, err
}
return website, nil
return &website, nil
}
func (w WebsiteSSLRepo) List(opts ...DBOption) ([]model.WebsiteSSL, error) {
@@ -93,14 +96,25 @@ func (w WebsiteSSLRepo) List(opts ...DBOption) ([]model.WebsiteSSL, error) {
return websites, nil
}
func (w WebsiteSSLRepo) TryMarkApplying(id uint) (bool, error) {
db := getDb().Model(&model.WebsiteSSL{}).
Where("id = ? AND status <> ?", id, constant.SSLApply).
Updates(map[string]interface{}{
"status": constant.SSLApply,
"updated_at": time.Now(),
})
if db.Error != nil {
return false, db.Error
}
return db.RowsAffected > 0, nil
}
func (w WebsiteSSLRepo) Create(ctx context.Context, ssl *model.WebsiteSSL) error {
return getTx(ctx).Create(ssl).Error
}
func (w WebsiteSSLRepo) Save(ssl *model.WebsiteSSL) error {
return getDb().Model(&model.WebsiteSSL{BaseModel: model.BaseModel{
ID: ssl.ID,
}}).Save(&ssl).Error
return getDb().Save(ssl).Error
}
func (w WebsiteSSLRepo) SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error {
File diff suppressed because it is too large Load Diff
+527
View File
@@ -0,0 +1,527 @@
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{"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")
output, err := cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, 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{"openclaw", "agents", "delete", req.ID, "--force"}
if _, err = cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, 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{
"openclaw",
"agents",
action,
"--agent",
req.ID,
"--bind",
binding,
}
args = append(args, "--json")
_, err = cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, 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")
}
File diff suppressed because it is too large Load Diff
+731
View File
@@ -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
}
+591
View File
@@ -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,
)
}
+126
View File
@@ -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)
}
+215
View File
@@ -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
}
+1 -5
View File
@@ -102,11 +102,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 := cmd.RunDockerExecWithStdout(5*time.Minute, containerName, "openclaw", "skills", "list", "--json")
if err != nil {
return 0, err
}
+699 -45
View File
@@ -1,18 +1,36 @@
package service
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"encoding/json"
"fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"
"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"
"github.com/1Panel-dev/1Panel/agent/utils/common"
)
const clawhubGlobalRegistry = "https://clawhub.com"
const clawhubChinaRegistry = "https://mirror-cn.clawhub.com"
const localSkillHubSource = "local-hub"
const localSkillHubPublishedStatus = "published"
const clawHubSkillTmpSubDir = "1panel/tmp/clawhub-skills"
const hermesManagedSkillsDir = "/opt/data/skills"
type openclawSkillsList struct {
Skills []openclawSkillListItem `json:"skills"`
}
@@ -34,21 +52,33 @@ type skillhubSearchPayload struct {
Results []dto.AgentSkillSearchItem `json:"results"`
}
type localSkillHubItem struct {
ID uint
Name string
Status string
ApplicableAgent string
PackagePath string
}
var clawhubSearchLinePattern = regexp.MustCompile(`^(\S+)\s+(.+?)\s+\(([\d.]+)\)$`)
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`)
var safeLocalSkillDirNamePattern = regexp.MustCompile(`[^a-zA-Z0-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 := cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, "openclaw", "skills", "list", "--json")
if err != nil {
return nil, err
}
@@ -59,13 +89,25 @@ 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)
if req.Source == localSkillHubSource {
return nil, fmt.Errorf("local skills hub is provided by enterprise edition")
}
if global.CONF.Base.IsOffline {
return nil, fmt.Errorf("offline environment cannot access remote Skills Hub")
}
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 +119,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,20 +144,61 @@ func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error {
}
func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
_, install, err := a.loadOpenclawAgentAndInstall(req.AgentID)
if global.CONF.Base.IsOffline && req.Source != localSkillHubSource {
return fmt.Errorf("offline environment cannot access remote Skills Hub")
}
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return err
}
if req.Source == localSkillHubSource {
if !global.IsMaster {
return fmt.Errorf("local skills hub is only available on the master node")
}
hostPath, skillName, err := resolveLocalSkillPackage(agent.AgentType, req.Slug)
if err != nil {
return err
}
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil {
return err
}
installTask.AddSubTask("Install local skill", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
return installLocalSkillPackage(mgr, install.ContainerName, agent.AgentType, agent.ConfigPath, hostPath, skillName)
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
global.LOG.Errorf("install local skill failed: %v", err)
}
}()
return nil
}
installTask, err := task.NewTaskWithOps(req.Slug, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
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))
return mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawSkillInstallCommand(req.Source, req.Slug))
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
return installOpenclawSkill(mgr, install.ContainerName, req.Source, req.Slug)
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
@@ -125,9 +208,547 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
return nil
}
func installLocalSkillPackage(mgr *cmd.CommandHelper, containerName, agentType, configPath, hostPath, skillName string) error {
targetDir, err := resolveAgentLocalSkillDir(agentType)
if err != nil {
return err
}
tempDir, err := os.MkdirTemp("", "1panel-agent-skill-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
extractRoot := filepath.Join(tempDir, "extract")
if err := os.MkdirAll(extractRoot, 0755); err != nil {
return err
}
if err := extractLocalSkillPackage(hostPath, extractRoot); err != nil {
return err
}
copyRoot, installedSkillName, err := normalizeLocalSkillInstallRoot(extractRoot, filepath.Join(tempDir, "install"), skillName)
if err != nil {
return err
}
if err := mgr.Run("docker", "exec", containerName, "mkdir", "-p", targetDir); err != nil {
return err
}
if err := mgr.Run("docker", "cp", copyRoot+"/.", containerName+":"+targetDir); err != nil {
return err
}
if agentType == constant.AppOpenclaw {
return registerOpenclawLocalSkill(containerName, configPath, installedSkillName)
}
return nil
}
func resolveAgentLocalSkillDir(agentType string) (string, error) {
switch agentType {
case constant.AppOpenclaw:
return openclawManagedSkillsDir, nil
case constant.AppHermesAgent:
return hermesManagedSkillsDir, nil
default:
return "", fmt.Errorf("%s does not support", agentType)
}
}
func resolveLocalSkillPackage(agentType, skillID string) (string, string, error) {
skillID = strings.TrimSpace(skillID)
id, err := strconv.ParseUint(skillID, 10, 64)
if err != nil || id == 0 {
return "", "", fmt.Errorf("invalid local skill id")
}
dbPath := filepath.Join(global.CONF.Base.InstallDir, "1panel", "db", "enterprise.db")
if _, err := os.Stat(dbPath); err != nil {
return "", "", fmt.Errorf("local skills hub is unavailable")
}
db, err := common.GetDBWithPath(dbPath)
if err != nil {
return "", "", err
}
defer common.CloseDB(db)
var item localSkillHubItem
if err := db.Table("skill_hub_items").
Select("id,name,status,applicable_agent,package_path").
Where("id = ? AND status = ?", uint(id), localSkillHubPublishedStatus).
First(&item).Error; err != nil {
return "", "", fmt.Errorf("local skill is not published or does not exist")
}
if !matchLocalSkillAgent(item.ApplicableAgent, agentType) {
return "", "", fmt.Errorf("local skill does not support %s", agentType)
}
packagePath, err := validateLocalSkillPackagePath(item.PackagePath)
if err != nil {
return "", "", err
}
return packagePath, sanitizeLocalSkillDirName(item.Name, packagePath, skillID), nil
}
func matchLocalSkillAgent(applicableAgent, agentType string) bool {
applicableAgent = strings.TrimSpace(applicableAgent)
if applicableAgent == "" {
return true
}
for _, item := range strings.Split(applicableAgent, ",") {
normalized := normalizeLocalSkillAgent(strings.TrimSpace(item))
if normalized == "common" || normalized == normalizeLocalSkillAgent(agentType) {
return true
}
}
return false
}
func normalizeLocalSkillAgent(agentType string) string {
switch strings.ToLower(strings.TrimSpace(agentType)) {
case constant.AppHermesAgent:
return "hermes"
default:
return strings.ToLower(strings.TrimSpace(agentType))
}
}
func sanitizeLocalSkillDirName(name, packagePath, fallback string) string {
name = strings.TrimSpace(name)
if name == "" {
name = strings.TrimSuffix(filepath.Base(packagePath), localSkillPackageExtension(packagePath))
}
name = strings.Trim(safeLocalSkillDirNamePattern.ReplaceAllString(name, "-"), "-")
if name == "" || name == "." || name == ".." {
name = "skill-" + strings.TrimSpace(fallback)
}
if name == "skill-" {
return "skill"
}
return name
}
func validateLocalSkillPackagePath(packagePath string) (string, error) {
packagePath = strings.TrimSpace(packagePath)
if packagePath == "" {
return "", fmt.Errorf("skill package path is required")
}
if _, err := localSkillPackageFormat(packagePath); err != nil {
return "", err
}
absPath, err := filepath.Abs(packagePath)
if err != nil {
return "", err
}
resolvedPath, err := filepath.EvalSymlinks(absPath)
if err != nil {
return "", err
}
if !isLocalSkillPackageAllowedPath(resolvedPath) {
return "", fmt.Errorf("invalid local skill package path")
}
info, err := os.Stat(resolvedPath)
if err != nil {
return "", err
}
if info.IsDir() {
return "", fmt.Errorf("skill package must be a file")
}
return resolvedPath, nil
}
func isLocalSkillPackageAllowedPath(resolvedPath string) bool {
roots := []string{
filepath.Join(global.CONF.Base.InstallDir, "1panel", "uploads", "skills-hub"),
filepath.Join(global.CONF.Base.InstallDir, filepath.FromSlash(clawHubSkillTmpSubDir)),
}
for _, root := range roots {
if isPathInsideResolvedRoot(resolvedPath, root) {
return true
}
}
return false
}
func isPathInsideResolvedRoot(resolvedPath, root string) bool {
absRoot, err := filepath.Abs(root)
if err != nil {
return false
}
resolvedRoot, err := filepath.EvalSymlinks(absRoot)
if err != nil {
return false
}
rel, err := filepath.Rel(resolvedRoot, resolvedPath)
return err == nil && rel != "." && rel != ".." && !strings.HasPrefix(rel, ".."+string(os.PathSeparator))
}
func normalizeLocalSkillInstallRoot(extractRoot, installRoot, skillName string) (string, string, error) {
entries, err := os.ReadDir(extractRoot)
if err != nil {
return "", "", err
}
if len(entries) == 0 {
return "", "", fmt.Errorf("skill package is empty")
}
if len(entries) == 1 && entries[0].IsDir() {
return extractRoot, entries[0].Name(), nil
}
skillRoot := filepath.Join(installRoot, skillName)
if err := os.MkdirAll(skillRoot, 0755); err != nil {
return "", "", err
}
if err := copyLocalDirContents(extractRoot, skillRoot); err != nil {
return "", "", err
}
return installRoot, skillName, nil
}
func registerOpenclawLocalSkill(containerName, configPath, skillName string) error {
conf, err := readOpenclawConfig(configPath)
if err != nil {
return err
}
skillKey, err := getOpenclawSkillKey(containerName, skillName)
if err != nil {
return err
}
setOpenclawSkillEnabled(conf, skillKey, true)
return writeOpenclawConfigRaw(configPath, conf)
}
func extractLocalSkillPackage(packagePath, targetDir string) error {
format, err := localSkillPackageFormat(packagePath)
if err != nil {
return err
}
switch format {
case "zip":
return unzipLocalSkillPackage(packagePath, targetDir)
case "tar", "targz":
return untarLocalSkillPackage(packagePath, targetDir, format == "targz")
case "7z":
return un7zLocalSkillPackage(packagePath, targetDir)
default:
return fmt.Errorf("unsupported skill package format")
}
}
func unzipLocalSkillPackage(packagePath, targetDir string) error {
reader, err := zip.OpenReader(packagePath)
if err != nil {
return err
}
defer reader.Close()
root, err := filepath.Abs(targetDir)
if err != nil {
return err
}
for _, file := range reader.File {
name, err := safeLocalSkillZipEntryName(file.Name)
if err != nil {
return err
}
targetPath := filepath.Join(root, name)
rel, err := filepath.Rel(root, targetPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return fmt.Errorf("invalid zip entry: %s", file.Name)
}
if file.FileInfo().IsDir() {
if err := os.MkdirAll(targetPath, file.Mode()); err != nil {
return err
}
continue
}
if file.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("unsupported zip entry: %s", file.Name)
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
rc, err := file.Open()
if err != nil {
return err
}
dst, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, file.Mode())
if err != nil {
_ = rc.Close()
return err
}
_, copyErr := io.Copy(dst, rc)
closeErr := dst.Close()
_ = rc.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}
}
return nil
}
func untarLocalSkillPackage(packagePath, targetDir string, gzipped bool) error {
src, err := os.Open(packagePath)
if err != nil {
return err
}
defer src.Close()
var reader io.Reader = src
var gzipReader *gzip.Reader
if gzipped {
gzipReader, err = gzip.NewReader(src)
if err != nil {
return err
}
defer gzipReader.Close()
reader = gzipReader
}
root, err := filepath.Abs(targetDir)
if err != nil {
return err
}
tarReader := tar.NewReader(reader)
for {
header, err := tarReader.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
name, err := safeLocalSkillZipEntryName(header.Name)
if err != nil {
return err
}
targetPath := filepath.Join(root, name)
rel, err := filepath.Rel(root, targetPath)
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
return fmt.Errorf("invalid tar entry: %s", header.Name)
}
info := header.FileInfo()
if info.IsDir() {
if err := os.MkdirAll(targetPath, info.Mode()); err != nil {
return err
}
continue
}
if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA {
continue
}
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
return err
}
dst, err := os.OpenFile(targetPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, info.Mode())
if err != nil {
return err
}
_, copyErr := io.Copy(dst, tarReader)
closeErr := dst.Close()
if copyErr != nil {
return copyErr
}
if closeErr != nil {
return closeErr
}
}
return nil
}
func un7zLocalSkillPackage(packagePath, targetDir string) error {
if _, err := listLocal7zEntries(packagePath); err != nil {
return err
}
root, err := filepath.Abs(targetDir)
if err != nil {
return err
}
bin, err := findLocal7zBinary()
if err != nil {
return err
}
tempDir, err := os.MkdirTemp("", "1panel-agent-skill-7z-*")
if err != nil {
return err
}
defer os.RemoveAll(tempDir)
if err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Minute)).Run(bin, "x", "-y", "-o"+tempDir, packagePath); err != nil {
return err
}
if err := validateExtractedLocalSkillPackage(tempDir); err != nil {
return err
}
return copyLocalDirContents(tempDir, root)
}
func safeLocalSkillZipEntryName(name string) (string, error) {
clean := path.Clean(strings.ReplaceAll(name, "\\", "/"))
clean = strings.TrimLeft(clean, "/")
if clean == "." || clean == "" || clean == ".." || strings.HasPrefix(clean, "../") {
return "", fmt.Errorf("invalid zip entry: %s", name)
}
return clean, nil
}
func copyLocalDirContents(sourceRoot, targetRoot string) error {
return filepath.Walk(sourceRoot, func(sourcePath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if sourcePath == sourceRoot {
return nil
}
if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) {
return fmt.Errorf("unsupported skill package entry: %s", sourcePath)
}
rel, err := filepath.Rel(sourceRoot, sourcePath)
if err != nil {
return err
}
name, err := safeLocalSkillZipEntryName(rel)
if err != nil {
return err
}
targetPath := filepath.Join(targetRoot, name)
if info.IsDir() {
return os.MkdirAll(targetPath, info.Mode())
}
return copyLocalSkillFile(sourcePath, targetPath)
})
}
func copyLocalSkillFile(source, target string) error {
src, err := os.Open(source)
if err != nil {
return err
}
defer src.Close()
if err := os.MkdirAll(filepath.Dir(target), 0755); err != nil {
return err
}
dst, err := os.OpenFile(target, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return err
}
if _, err := io.Copy(dst, src); err != nil {
_ = dst.Close()
return err
}
return dst.Close()
}
func validateExtractedLocalSkillPackage(root string) error {
return filepath.Walk(root, func(currentPath string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if currentPath == root {
return nil
}
if info.Mode()&os.ModeSymlink != 0 || (!info.IsDir() && !info.Mode().IsRegular()) {
return fmt.Errorf("unsupported skill package entry: %s", currentPath)
}
return nil
})
}
func localSkillPackageExtension(name string) string {
lower := strings.ToLower(filepath.Base(strings.TrimSpace(name)))
if lower == "" || lower == "." {
return ""
}
if strings.HasSuffix(lower, ".tar.gz") {
return ".tar.gz"
}
return strings.ToLower(filepath.Ext(lower))
}
func localSkillPackageFormat(name string) (string, error) {
switch localSkillPackageExtension(name) {
case ".zip":
return "zip", nil
case ".7z":
return "7z", nil
case ".tar":
return "tar", nil
case ".tar.gz":
return "targz", nil
default:
return "", fmt.Errorf("only .zip, .7z, .tar, and .tar.gz skill packages are supported")
}
}
func findLocal7zBinary() (string, error) {
for _, name := range []string{"7z", "7za", "7zr"} {
if path, err := exec.LookPath(name); err == nil {
return path, nil
}
}
return "", fmt.Errorf("7z command is required to process .7z skill packages")
}
func listLocal7zEntries(packagePath string) ([]string, error) {
bin, err := findLocal7zBinary()
if err != nil {
return nil, err
}
output, err := exec.Command(bin, "l", "-slt", packagePath).CombinedOutput()
if err != nil {
if message := strings.TrimSpace(string(output)); message != "" {
return nil, fmt.Errorf("7z failed: %w: %s", err, message)
}
return nil, fmt.Errorf("7z failed: %w", err)
}
entries := make([]string, 0)
for _, line := range strings.Split(string(output), "\n") {
line = strings.TrimSpace(line)
if !strings.HasPrefix(line, "Path = ") {
continue
}
entry := strings.TrimSpace(strings.TrimPrefix(line, "Path = "))
if entry == "" || entry == packagePath || entry == filepath.Base(packagePath) {
continue
}
name, err := safeLocalSkillZipEntryName(entry)
if err != nil {
return nil, err
}
entries = append(entries, name)
}
if len(entries) == 0 {
return nil, fmt.Errorf("skill package is empty")
}
return entries, 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,19 +767,19 @@ 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 cmd.RunDockerExecWithStdout(2*time.Minute, containerName, "skillhub", "search", keyword, "--json")
default:
return cmd.RunDefaultWithStdoutBashCfAndTimeOut(
"docker exec %s clawhub search %q 2>&1",
30*time.Second,
containerName,
keyword,
)
return cmd.NewCommandMgr(cmd.WithTimeout(2*time.Minute)).
RunWithStdout(
"docker",
"exec",
"-e",
"CLAWHUB_REGISTRY="+resolveClawhubRegistry(source),
containerName,
"clawhub",
"search",
keyword,
)
}
}
@@ -188,7 +809,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,37 +821,48 @@ func parseClawhubSearchResult(output string) []dto.AgentSkillSearchItem {
Slug: matches[1],
Name: matches[2],
Score: matches[3],
Source: "clawhub",
Source: source,
})
}
return items
}
func buildOpenclawSkillInstallCommand(source, slug string) string {
func installOpenclawSkill(mgr *cmd.CommandHelper, containerName, source, slug string) error {
if err := mgr.Run("docker", "exec", containerName, "mkdir", "-p", openclawManagedSkillsDir); err != nil {
return err
}
switch source {
case "clawhub":
return fmt.Sprintf(
"mkdir -p %s && clawhub --workdir /home/node/.openclaw --dir skills install %q",
openclawManagedSkillsDir,
case "clawhub-global", "clawhub-cn":
return mgr.Run(
"docker",
"exec",
"-e",
"CLAWHUB_REGISTRY="+resolveClawhubRegistry(source),
containerName,
"clawhub",
"--workdir",
"/home/node/.openclaw",
"--dir",
"skills",
"install",
slug,
)
default:
return fmt.Sprintf(
"mkdir -p %s && skillhub --dir %s install %q",
openclawManagedSkillsDir,
openclawManagedSkillsDir,
slug,
)
return mgr.Run("docker", "exec", containerName, "skillhub", "--dir", openclawManagedSkillsDir, "install", slug)
}
}
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 := cmd.RunDockerExecWithStdout(2*time.Minute, containerName, "openclaw", "skills", "info", name, "--json")
if err != nil {
return "", err
}
@@ -238,16 +870,38 @@ 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 == "" {
return "", fmt.Errorf("skill %s does not have a skillKey", name)
return name, nil
}
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")
+203 -190
View File
@@ -42,8 +42,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
}
@@ -333,13 +333,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 +351,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 +380,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)
}
@@ -575,6 +578,9 @@ func checkAgentUpgradable(install model.AppInstall) bool {
if install.App.ID == 0 {
return false
}
if ignoreUpdate(install) {
return false
}
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(install.App.ID))
if err != nil || len(details) == 0 {
return false
@@ -653,7 +659,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 +686,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 +718,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 +741,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 +764,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 +803,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 +828,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 +862,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 +1003,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 +1020,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 +1040,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 +1052,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 +1090,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 +1115,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 +1277,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 +1309,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 +1457,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 +1492,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 +1513,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 +1562,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 +1611,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) {
+338
View File
@@ -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
}
+138 -9
View File
@@ -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"
)
@@ -73,7 +77,7 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
if err != nil {
return "", err
}
stdout, err := cmd.RunDefaultWithStdoutBashCf("docker exec %s ollama show %s", containerName, name)
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "show", name)
if err != nil {
return "", err
}
@@ -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)
@@ -134,7 +137,7 @@ func (u *AIToolService) Close(name string) error {
if err != nil {
return err
}
if err := cmd.RunDefaultBashCf("docker exec %s ollama stop %s", containerName, name); err != nil {
if err := cmd.NewCommandMgr().Run("docker", "exec", containerName, "ollama", "stop", name); err != nil {
return fmt.Errorf("handle ollama stop %s failed, %v", name, err)
}
return nil
@@ -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)
@@ -192,7 +194,7 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
}
for _, item := range ollamaList {
if item.Status != constant.StatusDeleted {
if err := cmd.RunDefaultBashCf("docker exec %s ollama rm %s", containerName, item.Name); err != nil && !req.ForceDelete {
if err := cmd.NewCommandMgr().Run("docker", "exec", containerName, "ollama", "rm", item.Name); err != nil && !req.ForceDelete {
return fmt.Errorf("handle ollama rm %s failed, %v", item.Name, err)
}
}
@@ -208,7 +210,7 @@ func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
if err != nil {
return nil, err
}
stdout, err := cmd.RunDefaultWithStdoutBashCf("docker exec %s ollama list", containerName)
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
if err != nil {
return nil, err
}
@@ -380,12 +382,15 @@ func LoadContainerName() (string, error) {
}
func loadModelSize(name string, containerName string) (string, error) {
stdout, err := cmd.RunDefaultWithStdoutBashCf("docker exec %s ollama list | grep %s", containerName, name)
stdout, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "ollama", "list")
if err != nil {
return "", err
}
lines := strings.Split(stdout, "\n")
for _, line := range lines {
if !strings.Contains(line, name) {
continue
}
parts := strings.Fields(line)
if len(parts) < 5 {
continue
@@ -394,3 +399,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
}
+213 -21
View File
@@ -3,6 +3,13 @@ package service
import (
"encoding/json"
"fmt"
"mime"
"sort"
"strconv"
"strings"
"sync"
"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"
@@ -15,24 +22,27 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/shirou/gopsutil/v4/disk"
"mime"
"sort"
"strings"
"sync"
"time"
)
type AlertService struct{}
var eeHiddenAlertTypes = []string{"licenseException", "panelUpdate", "panelPwdEndTime"}
var communityAlertMethodTypeNames = map[string]string{
constant.WeCom: "WeCom",
constant.DingTalk: "DingTalk",
constant.FeiShu: "FeiShu",
constant.SMS: "SMS",
}
type IAlertService interface {
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
GetAlerts() ([]dto.AlertDTO, error)
CreateAlert(create dto.AlertCreate) error
UpdateAlert(req dto.AlertUpdate) error
CreateAlert(create dto.AlertCreate, operator string) error
UpdateAlert(req dto.AlertUpdate, operator string) error
DeleteAlert(id uint) error
GetAlert(id uint) (dto.AlertDTO, error)
UpdateStatus(id uint, status string) error
ExternalUpdateAlert(req dto.AlertCreate) error
ExternalUpdateAlert(req dto.AlertCreate, operator string) error
GetDisks() ([]dto.DiskDTO, error)
PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error)
@@ -40,8 +50,9 @@ type IAlertService interface {
GetClams() ([]dto.ClamDTO, error)
GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
GetAlertConfig() ([]model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate) error
GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
DeleteAlertConfig(id uint) error
TestAlertConfig(req dto.AlertConfigTest) (bool, error)
}
@@ -55,6 +66,9 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
opts []repo.DBOption
result []dto.AlertDTO
)
if global.CONF.Base.IsEnterprise {
opts = append(opts, alertRepo.WithByTypeNotIn(eeHiddenAlertTypes))
}
if search.Status != "" {
opts = append(opts, repo.WithByStatus(search.Status))
}
@@ -81,6 +95,8 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
CreateUser: item.CreateUser,
UpdateUser: item.UpdateUser,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
@@ -112,6 +128,8 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
Status: item.Status,
SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams,
CreateUser: item.CreateUser,
UpdateUser: item.UpdateUser,
CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt,
})
@@ -120,7 +138,10 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
return result, err
}
func (a AlertService) CreateAlert(create dto.AlertCreate) error {
func (a AlertService) CreateAlert(create dto.AlertCreate, operator string) error {
if err := a.validateCommunityAlertMethod(create.Method); err != nil {
return err
}
var alertID uint
var alertInfo model.Alert
if create.Project != "" {
@@ -137,7 +158,7 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
return buserr.WithErr("ErrStructTransform", err)
}
upAlert.ID = alertID
err := a.UpdateAlert(upAlert)
err := a.UpdateAlert(upAlert, operator)
if err != nil {
return err
}
@@ -146,6 +167,8 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
if err := copier.Copy(&alertInfo, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err)
}
alertInfo.CreateUser = operator
alertInfo.UpdateUser = operator
if err := alertRepo.Create(&alertInfo); err != nil {
return err
@@ -156,7 +179,10 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
return nil
}
func (a AlertService) UpdateAlert(req dto.AlertUpdate) error {
func (a AlertService) UpdateAlert(req dto.AlertUpdate, operator string) error {
if err := a.validateCommunityAlertMethod(req.Method); err != nil {
return err
}
upMap := make(map[string]interface{})
upMap["id"] = req.ID
@@ -169,6 +195,7 @@ func (a AlertService) UpdateAlert(req dto.AlertUpdate) error {
upMap["status"] = req.Status
upMap["send_count"] = req.SendCount
upMap["advanced_params"] = req.AdvancedParams
upMap["update_user"] = operator
if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil {
return err
@@ -308,12 +335,25 @@ func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
func executeDiskCommand() (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
stdout, err := cmdMgr.RunWithStdoutBashC("df -hT -P | grep '/' | grep -v tmpfs | grep -v 'snap/core' | grep -v udev")
stdout, err := cmdMgr.RunWithStdout("df", "-hT", "-P")
if err != nil {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunWithStdoutBashC("df -lhT -P | grep '/' | grep -v tmpfs | grep -v 'snap/core' | grep -v udev")
stdout, err = cmdMgr2.RunWithStdout("df", "-lhT", "-P")
}
return stdout, err
if err != nil {
return stdout, err
}
var lines []string
for _, line := range strings.Split(stdout, "\n") {
if !strings.Contains(line, "/") || strings.Contains(line, "tmpfs") || strings.Contains(line, "snap/core") || strings.Contains(line, "udev") {
continue
}
lines = append(lines, line)
}
if len(lines) == 0 {
return "", nil
}
return strings.Join(lines, "\n"), nil
}
func shouldExclude(fields []string, mountPoint string, excludes map[string]struct{}) bool {
@@ -441,17 +481,40 @@ func (a AlertService) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
return cronJobs, err
}
func (a AlertService) GetAlertConfig() ([]model.AlertConfig, error) {
func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error) {
var (
opts []repo.DBOption
configs []model.AlertConfig
)
if len(req.ExcludeTypes) > 0 {
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
}
opts = append(opts, repo.WithByStatus(constant.AlertEnable))
configs, err := alertRepo.AlertConfigList(opts...)
return configs, err
}
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
func (a AlertService) PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error) {
opts := []repo.DBOption{
alertRepo.WithByTypeNotIn([]string{"common"}),
repo.WithOrderDesc("created_at"),
}
if len(req.ExcludeTypes) > 0 {
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
}
return alertRepo.PageAlertConfig(req.Page, req.PageSize, opts...)
}
func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error {
if err := a.validateCommunityAlertConfigType(req.Type); err != nil {
return err
}
if err := a.checkAlertConfigDisplayNameUnique(req); err != nil {
return err
}
if err := a.checkAlertConfigSMSPhoneUnique(req); err != nil {
return err
}
if req.ID != 0 {
upMap := make(map[string]interface{})
upMap["id"] = req.ID
@@ -459,6 +522,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
upMap["title"] = req.Title
upMap["status"] = req.Status
upMap["config"] = req.Config
upMap["update_user"] = operator
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
return err
}
@@ -467,6 +531,8 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
if err := copier.Copy(&alertConfig, &req); err != nil {
return buserr.WithErr("ErrStructTransform", err)
}
alertConfig.CreateUser = operator
alertConfig.UpdateUser = operator
if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil {
return err
}
@@ -475,7 +541,130 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
return nil
}
func (a AlertService) checkAlertConfigSMSPhoneUnique(req dto.AlertConfigUpdate) error {
if req.Type != constant.SMSConfig {
return nil
}
phone := alertConfigSMSPhone(req.Config)
configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type))
if err != nil {
return err
}
for _, config := range configs {
if req.ID != 0 && config.ID == req.ID {
continue
}
if alertConfigSMSPhone(config.Config) == phone {
return buserr.New("ErrAlertConfigPhoneExist")
}
}
return nil
}
func (a AlertService) checkAlertConfigDisplayNameUnique(req dto.AlertConfigUpdate) error {
displayName := alertConfigDisplayName(req.Type, req.Config)
if displayName == "" {
return nil
}
configs, err := alertRepo.AlertConfigList(alertRepo.WithByType(req.Type))
if err != nil {
return err
}
for _, config := range configs {
if req.ID != 0 && config.ID == req.ID {
continue
}
if alertConfigDisplayName(config.Type, config.Config) == displayName {
return buserr.New("ErrNameIsExist")
}
}
return nil
}
func (a AlertService) validateCommunityAlertMethod(method string) error {
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
return nil
}
if strings.TrimSpace(method) == "" {
return nil
}
for _, item := range strings.Split(method, ",") {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if configID, err := strconv.ParseUint(item, 10, 64); err == nil {
config, err := alertRepo.GetConfigById(uint(configID))
if err != nil {
return err
}
if _, ok := communityAlertMethodTypeNames[config.Type]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
continue
}
if _, ok := communityAlertMethodTypeNames[item]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
}
return nil
}
func (a AlertService) validateCommunityAlertConfigType(configType string) error {
if global.CONF.Base.IsEnterprise || global.CONF.Base.Edition == "cn" {
return nil
}
if _, ok := communityAlertMethodTypeNames[configType]; ok {
return buserr.WithErr("ErrAlertMethodNotSupported", nil)
}
return nil
}
func alertConfigDisplayName(configType, configData string) string {
switch configType {
case constant.Email, constant.WeCom, constant.DingTalk, constant.FeiShu, constant.Bark, constant.SMS:
var cfg struct {
DisplayName string `json:"displayName"`
}
if err := json.Unmarshal([]byte(configData), &cfg); err != nil {
return ""
}
return strings.TrimSpace(cfg.DisplayName)
default:
return ""
}
}
func alertConfigSMSPhone(configData string) string {
var cfg struct {
Phone string `json:"phone"`
}
if err := json.Unmarshal([]byte(configData), &cfg); err != nil {
return ""
}
return strings.TrimSpace(cfg.Phone)
}
func (a AlertService) DeleteAlertConfig(id uint) error {
_, err := alertRepo.GetConfigById(id)
if err != nil {
return err
}
usedAlerts, err := alertRepo.List(alertRepo.WithByAlertMethodContainsConfigID(id))
if err != nil {
return err
}
if len(usedAlerts) > 0 {
return buserr.New("ErrAlertConfigInUse")
}
return alertRepo.DeleteAlertConfig(repo.WithByID(id))
}
@@ -501,14 +690,17 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
Body: i18n.GetMsgByKey("TestAlert"),
IsHTML: false,
}
transport := xpack.LoadRequestTransport()
transport := xpack.MultiNodeProvider.LoadRequestTransport()
if err := email.SendMail(cfg, msg, transport); err != nil {
return false, err
}
return true, nil
}
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error {
func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate, operator string) error {
if err := a.validateCommunityAlertMethod(updateAlert.Method); err != nil {
return err
}
upMap := make(map[string]interface{})
var newStatus string
if updateAlert.SendCount == 0 {
@@ -552,7 +744,7 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error {
} else {
if updateAlert.Method != "" && updateAlert.Title != "" {
updateAlert.Status = newStatus
if err := a.CreateAlert(updateAlert); err != nil {
if err := a.CreateAlert(updateAlert, operator); err != nil {
return err
}
}
+234 -82
View File
@@ -4,9 +4,11 @@ import (
"encoding/json"
"fmt"
"math"
"net"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -23,7 +25,7 @@ import (
"github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net"
gnet "github.com/shirou/gopsutil/v4/net"
)
const (
@@ -34,7 +36,7 @@ const (
type AlertTaskHelper struct {
DiskIO chan []disk.IOCountersStat
NetIO chan []net.IOCountersStat
NetIO chan []gnet.IOCountersStat
}
type IAlertTaskHelper interface {
@@ -46,6 +48,7 @@ type IAlertTaskHelper interface {
var cpuLoad1, cpuLoad5, cpuLoad15 []float64
var memoryLoad1, memoryLoad5, memoryLoad15 []float64
var alertTaskMu sync.Mutex
var baseTypes = map[string]bool{"ssl": true, "siteEndTime": true, "panelPwdEndTime": true, "panelUpdate": true}
var resourceTypes = map[string]bool{"cpu": true, "memory": true, "disk": true, "load": true, "panelLogin": true, "sshLogin": true, "nodeException": true, "licenseException": true}
@@ -53,36 +56,53 @@ var resourceTypes = map[string]bool{"cpu": true, "memory": true, "disk": true, "
func NewIAlertTaskHelper() IAlertTaskHelper {
return &AlertTaskHelper{
DiskIO: make(chan []disk.IOCountersStat, 1),
NetIO: make(chan []net.IOCountersStat, 1),
NetIO: make(chan []gnet.IOCountersStat, 1),
}
}
func (m *AlertTaskHelper) StartTask() {
alertTaskMu.Lock()
defer alertTaskMu.Unlock()
m.startTaskLocked()
}
func (m *AlertTaskHelper) startTaskLocked() {
baseAlert, resourceAlert := m.getClassifiedAlerts()
if len(baseAlert) == 0 && len(resourceAlert) == 0 {
return
}
handleBaseAlerts(baseAlert)
handleResourceAlerts(resourceAlert)
handleBaseAlertsLocked(baseAlert)
handleResourceAlertsLocked(resourceAlert)
}
func (m *AlertTaskHelper) StopTask() {
stopBaseJob()
stopResourceJob()
alertTaskMu.Lock()
defer alertTaskMu.Unlock()
stopBaseJobLocked()
stopResourceJobLocked()
}
func (m *AlertTaskHelper) ResetTask() {
m.StopTask()
m.StartTask()
alertTaskMu.Lock()
defer alertTaskMu.Unlock()
stopBaseJobLocked()
stopResourceJobLocked()
m.startTaskLocked()
}
func (m *AlertTaskHelper) InitTask(alertType string) {
alertTaskMu.Lock()
defer alertTaskMu.Unlock()
m.initTaskLocked(alertType)
}
func (m *AlertTaskHelper) initTaskLocked(alertType string) {
resetAlertState(alertType)
if baseTypes[alertType] {
stopBaseJob()
stopBaseJobLocked()
} else if resourceTypes[alertType] {
stopResourceJob()
stopResourceJobLocked()
}
m.StartTask()
m.startTaskLocked()
}
func resetAlertState(alertType string) {
@@ -110,14 +130,14 @@ func (m *AlertTaskHelper) getClassifiedAlerts() (baseAlerts, resourceAlerts []dt
return
}
func handleBaseAlerts(baseAlerts []dto.AlertDTO) {
func handleBaseAlertsLocked(baseAlerts []dto.AlertDTO) {
if len(baseAlerts) == 0 {
stopBaseJob()
stopBaseJobLocked()
return
}
if global.AlertBaseJobID == 0 {
baseTask(baseAlerts)
jobID, err := global.Cron.AddFunc("*/30 * * * *", func() {
jobID, err := global.Cron.AddFunc("@every 30m", func() {
baseTask(baseAlerts)
})
if err != nil {
@@ -129,9 +149,9 @@ func handleBaseAlerts(baseAlerts []dto.AlertDTO) {
}
}
func handleResourceAlerts(resourceAlerts []dto.AlertDTO) {
func handleResourceAlertsLocked(resourceAlerts []dto.AlertDTO) {
if len(resourceAlerts) == 0 {
stopResourceJob()
stopResourceJobLocked()
return
}
if global.AlertResourceJobID == 0 {
@@ -147,7 +167,7 @@ func handleResourceAlerts(resourceAlerts []dto.AlertDTO) {
}
}
func stopBaseJob() {
func stopBaseJobLocked() {
if global.AlertBaseJobID != 0 {
global.Cron.Remove(global.AlertBaseJobID)
global.AlertBaseJobID = 0
@@ -155,7 +175,7 @@ func stopBaseJob() {
}
}
func stopResourceJob() {
func stopResourceJobLocked() {
if global.AlertResourceJobID != 0 {
global.Cron.Remove(global.AlertResourceJobID)
global.AlertResourceJobID = 0
@@ -174,10 +194,16 @@ func baseTask(baseAlert []dto.AlertDTO) {
case "siteEndTime":
loadWebsiteInfo(alert)
case "panelPwdEndTime":
if global.CONF.Base.IsEnterprise {
continue
}
if global.IsMaster {
loadPanelPwd(alert)
}
case "panelUpdate":
if global.CONF.Base.IsEnterprise {
continue
}
if global.IsMaster {
loadPanelUpdate(alert)
}
@@ -210,6 +236,9 @@ func resourceTask(resourceAlert []dto.AlertDTO) {
loadNodeException(alert)
}
case "licenseException":
if global.CONF.Base.IsEnterprise {
continue
}
if execute && global.IsMaster {
loadLicenseException(alert)
}
@@ -457,6 +486,7 @@ func loadPanelLogin(alert dto.AlertDTO) {
if err != nil {
global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err)
}
records = filterLoginLogsNotInWhitelist(records, whitelist)
if len(records) > 0 {
quota := strings.Join(func() []string {
var ips []string
@@ -506,6 +536,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
if err != nil {
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
}
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
if len(records) > 0 {
quota := strings.Join(records, "\n")
params := []dto.Param{
@@ -524,9 +555,63 @@ func loadSSHLogin(alert dto.AlertDTO) {
}
}
func filterLoginLogsNotInWhitelist(records []model.LoginLog, whitelist []string) []model.LoginLog {
filtered := make([]model.LoginLog, 0, len(records))
for _, record := range records {
if !isIPInWhitelist(record.IP, whitelist) {
filtered = append(filtered, record)
}
}
return filtered
}
func filterSSHLoginEntriesNotInWhitelist(records []string, whitelist []string) []string {
filtered := make([]string, 0, len(records))
for _, record := range records {
ip := record
if idx := strings.Index(record, "-"); idx >= 0 {
ip = record[:idx]
}
if !isIPInWhitelist(ip, whitelist) {
filtered = append(filtered, record)
}
}
return filtered
}
func isIPInWhitelist(ip string, whitelist []string) bool {
targetIP := net.ParseIP(strings.TrimSpace(ip))
if targetIP == nil {
return false
}
for _, item := range whitelist {
item = strings.TrimSpace(item)
if item == "" {
continue
}
if item == ip {
return true
}
if whiteIP := net.ParseIP(item); whiteIP != nil {
if whiteIP.Equal(targetIP) {
return true
}
continue
}
_, ipNet, err := net.ParseCIDR(item)
if err != nil {
continue
}
if ipNet.Contains(targetIP) {
return true
}
}
return false
}
func loadNodeException(alert dto.AlertDTO) {
// only master alert
failCount, err := xpack.GetNodeErrorAlert()
failCount, err := xpack.AlertProvider.GetNodeErrorAlert()
if err != nil {
global.LOG.Errorf("error getting node, err: %s", err)
return
@@ -555,7 +640,7 @@ func loadNodeException(alert dto.AlertDTO) {
func loadLicenseException(alert dto.AlertDTO) {
// only master alert
failCount, err := xpack.GetLicenseErrorAlert()
failCount, err := xpack.AlertProvider.GetLicenseErrorAlert()
if err != nil {
global.LOG.Errorf("error getting license, err: %s", err)
return
@@ -590,72 +675,139 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [
if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval {
for _, m := range methods {
m = strings.TrimSpace(m)
switch m {
case constant.SMS:
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
continue
}
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.SMS)
if !isValid {
continue
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
}
alertErr := xpack.CreateSMSAlertLog(alertType, alert, create, quotaType, params, constant.SMS)
if alertErr != nil {
global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error())
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.SMS)
case constant.Email:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, constant.Email)
if !isValid {
continue
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email)
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error())
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, constant.Email)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
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,
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
err := xpack.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, m, transport, agentInfo)
if err != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, m, err)
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m)
default:
if configId, err := strconv.ParseUint(m, 10, 64); err == nil {
sendAlertsByConfigId(alert, alertType, quota, quotaType, params, uint(configId))
} else {
sendAlertsByLegacyMethod(alert, alertType, quota, quotaType, params, m)
}
}
}
}
func sendAlertsByConfigId(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, configId uint) {
config, err := alertRepo.GetConfigById(configId)
if err != nil {
global.LOG.Errorf("alert config not found for id %d: %v", configId, err)
return
}
doSendAlert(alert, alertType, quota, quotaType, params, config)
}
func sendAlertsByLegacyMethod(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, method string) {
typeMap := map[string]string{
"mail": constant.Email,
constant.Bark: constant.Bark,
constant.SMS: constant.SMS,
}
configType, ok := typeMap[method]
if !ok {
configType = method
}
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
doSendAlert(alert, alertType, quota, quotaType, params, config)
}
func doSendAlert(alert dto.AlertDTO, alertType, quota, quotaType string, params []dto.Param, config model.AlertConfig) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
methodStr := strconv.Itoa(int(config.ID))
switch config.Type {
case constant.SMS:
if !alertUtil.CheckSMSSendLimit(config, methodStr) {
return
}
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
alertErr := xpack.AlertProvider.CreateSMSAlertLog(alertType, alert, create, quotaType, params, config, methodStr)
if alertErr != nil {
global.LOG.Infof("%s alert sms push faild, err: %v", alertType, alertErr.Error())
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.Email:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Email)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := alertUtil.CreateEmailAlertLog(create, alertInfo, params, transport, agentInfo, config)
if alertErr != nil {
global.LOG.Infof("%s alert email push faild, err: %v", alertType, alertErr.Error())
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.Bark:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, constant.Bark)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo, config)
if alertErr != nil {
global.LOG.Infof("%s alert %s push failed, err: %v", alertType, methodStr, alertErr.Error())
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, methodStr)
if !isValid {
return
}
create := dto.AlertLogCreate{
Type: alertType,
AlertId: alert.ID,
Count: todayCount + 1,
Method: methodStr,
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
alertErr := xpack.AlertProvider.CreateWebhookAlertLog(alertType, alert, create, quotaType, params, config, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert webhook %s push faild, err: %v", alertType, methodStr, alertErr)
return
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, methodStr)
}
}
// ------------------------------
func getRepoOptionsByProject(project string) []repo.DBOption {
var opts []repo.DBOption
+294 -88
View File
@@ -1,12 +1,16 @@
package service
import (
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/xpack"
"strings"
)
type AlertSender struct {
@@ -22,41 +26,92 @@ func NewAlertSender(alert dto.AlertDTO, quotaType string) *AlertSender {
}
func (s *AlertSender) Send(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",")
for _, method := range methods {
method = strings.TrimSpace(method)
switch method {
case constant.SMS:
s.sendSMS(quota, params)
case constant.Email:
s.sendEmail(quota, params)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
s.sendWebhook(quota, params, method)
}
}
s.sendByConfigIds(s.alert.Method, quota, params, false)
}
func (s *AlertSender) ResourceSend(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",")
for _, method := range methods {
method = strings.TrimSpace(method)
switch method {
case constant.SMS:
s.sendResourceSMS(quota, params)
case constant.Email:
s.sendResourceEmail(quota, params)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
s.sendResourceWebhook(quota, params, method)
s.sendByConfigIds(s.alert.Method, quota, params, true)
}
func (s *AlertSender) sendByConfigIds(methodStr string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo()
configIds := strings.Split(methodStr, ",")
for _, idStr := range configIds {
idStr = strings.TrimSpace(idStr)
configId, err := strconv.ParseUint(idStr, 10, 64)
if err != nil {
s.sendByLegacyMethod(idStr, quota, params, isResource)
continue
}
config, err := alertRepo.GetConfigById(uint(configId))
if err != nil {
global.LOG.Errorf("alert config not found for id %d: %v", configId, err)
continue
}
s.sendByConfig(config, quota, params, isResource)
}
}
func (s *AlertSender) sendByConfig(config model.AlertConfig, quota string, params []dto.Param, isResource bool) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
switch config.Type {
case constant.SMS:
if isResource {
s.sendResourceSMSWithConfig(config, quota, params)
} else {
s.sendSMSWithConfig(config, quota, params)
}
case constant.Email:
if isResource {
s.sendResourceEmailWithConfig(config, quota, params)
} else {
s.sendEmailWithConfig(config, quota, params)
}
case constant.Bark:
if isResource {
s.sendResourceBarkWithConfig(config, quota, params)
} else {
s.sendBarkWithConfig(config, quota, params)
}
case constant.WeCom, constant.DingTalk, constant.FeiShu:
if isResource {
s.sendResourceWebhookWithConfig(config, quota, params)
} else {
s.sendWebhookWithConfig(config, quota, params)
}
}
}
func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
alertRepo := repo.NewIAlertRepo()
typeMap := map[string]string{"mail": constant.Email, constant.Bark: constant.Bark, constant.SMS: constant.SMS}
configType := method
if mapped, ok := typeMap[method]; ok {
configType = mapped
}
config, err := alertRepo.GetConfig(alertRepo.WithByType(configType))
if err != nil {
global.LOG.Errorf("alert config not found for type %s: %v", configType, err)
return
}
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
s.sendByConfig(config, quota, params, isResource)
}
func (s *AlertSender) sendSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
method := strconv.Itoa(int(config.ID))
if !alertUtil.CheckSMSSendLimit(config, method) {
return
}
totalCount, isValid := s.canSendAlert(constant.SMS)
totalCount, isValid := s.canSendAlert(method)
if !isValid {
return
}
@@ -66,18 +121,31 @@ func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: method,
}
err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS)
err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method)
if err != nil {
global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method)
}
func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
totalCount, isValid := s.canSendAlert(constant.Email)
func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMS))
if err != nil {
return
}
s.sendSMSWithConfig(config, quota, params)
}
func (s *AlertSender) sendEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
@@ -89,66 +157,24 @@ func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo)
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config)
if err != nil {
global.LOG.Errorf("%s alert email push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
totalCount, isValid := s.canSendAlert(method)
if !isValid {
func (s *AlertSender) sendResourceEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
err := xpack.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method)
}
func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) {
return
}
todayCount, isValid := s.canResourceSendAlert(constant.SMS)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
}
if err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil {
global.LOG.Errorf("failed to send SMS alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS)
}
func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
todayCount, isValid := s.canResourceSendAlert(constant.Email)
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
@@ -160,18 +186,189 @@ func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo); err != nil {
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil {
global.LOG.Errorf("failed to send Email alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Email)
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig))
if err != nil {
return
}
s.sendEmailWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.EmailConfig))
if err != nil {
return
}
s.sendResourceEmailWithConfig(config, quota, params)
}
func (s *AlertSender) sendBarkWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID)))
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),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config)
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, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceBarkWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
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),
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil {
global.LOG.Errorf("failed to send Bark alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendBark(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark))
if err != nil {
return
}
s.sendBarkWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.Bark))
if err != nil {
return
}
s.sendResourceBarkWithConfig(config, quota, params)
}
func (s *AlertSender) sendWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
totalCount, isValid := s.canSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendResourceWebhookWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: strconv.Itoa(int(config.ID)),
}
transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := xpack.AlertProvider.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, config, transport, agentInfo); err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, config.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
}
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(method))
if err != nil {
return
}
s.sendWebhookWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, method string) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(method))
if err != nil {
return
}
s.sendResourceWebhookWithConfig(config, quota, params)
}
func (s *AlertSender) sendResourceSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
if !alertUtil.IsAlertConfigEnabled(config) {
return
}
method := strconv.Itoa(int(config.ID))
if !alertUtil.CheckSMSSendLimit(config, method) {
return
}
todayCount, isValid := s.canResourceSendAlert(method)
if !isValid {
return
@@ -182,16 +379,25 @@ func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, meth
Count: todayCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
Method: method,
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
if err := xpack.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo); err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err)
if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method); err != nil {
global.LOG.Errorf("failed to send SMS alert: %v", err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method)
}
func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) {
alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMSConfig))
if err != nil {
return
}
s.sendResourceSMSWithConfig(config, quota, params)
}
func (s *AlertSender) canSendAlert(method string) (uint, bool) {
todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method)
if err != nil {
+5 -4
View File
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -125,7 +126,7 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (*response.
lang := strings.ToLower(common.GetLang(ctx))
for _, ap := range apps {
if req.Type == "php" {
if !global.CONF.Base.IsOffLine && (ap.RequiredPanelVersion == 0 || !common.CompareAppVersion(common.GetSystemVersion(info.SystemVersion), fmt.Sprintf("%f", ap.RequiredPanelVersion))) {
if !global.CONF.Base.IsOffline && (ap.RequiredPanelVersion == 0 || !common.CompareAppVersion(common.GetSystemVersion(info.SystemVersion), fmt.Sprintf("%f", ap.RequiredPanelVersion))) {
continue
}
}
@@ -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() {
@@ -965,7 +966,7 @@ func deleteCustomApp() {
}
func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
if xpack.IsUseCustomApp() {
if xpack.MultiNodeProvider.IsUseCustomApp() {
return nil
}
+12 -3
View File
@@ -589,7 +589,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
continue
}
if common.CompareVersion(detail.Version, install.Version) {
canUpgrade := common.CompareVersion(detail.Version, install.Version)
if app.Key == vllmAppKeyForUpgrade {
currentImage := loadVllmImageFromEnv(install.Env)
canUpgrade = isVllmUpgradeCandidate(install.Version, detail.Version, currentImage)
}
if canUpgrade {
var newCompose string
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
filename := filepath.Base(detail.DownloadUrl)
@@ -853,6 +858,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 +879,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 +920,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
}
}
+4 -4
View File
@@ -82,7 +82,7 @@ func (a AppService) createSyncAppStoreTask(sharedCtx **appSyncContext) func(t *t
ctx := &appSyncContext{
task: t,
httpClient: http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.LoadRequestTransport()},
httpClient: http.Client{Timeout: time.Duration(constant.TimeOut20s) * time.Second, Transport: xpack.MultiNodeProvider.LoadRequestTransport()},
baseRemoteUrl: fmt.Sprintf("%s/%s/1panel", global.AppRepoURL(), global.CONF.Base.Mode),
systemVersion: setting.SystemVersion,
settingService: settingService,
@@ -279,9 +279,9 @@ func (c *appSyncContext) syncAppIconsAndDetails() error {
}()
var (
completed int
icon200Count int
icon304Count int
completed int
icon200Count int
icon304Count int
iconFailCount int
)
milestones := [4]int{totalWork / 4, totalWork / 2, totalWork * 3 / 4, totalWork}
+118 -17
View File
@@ -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))
}
@@ -663,7 +665,7 @@ func buildNginx(parentTask *task.Task) error {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute))
if err = cmdMgr.RunBashCf("docker compose -f %s build", nginxInstall.GetComposePath()); err != nil {
if err = cmdMgr.Run("docker", "compose", "-f", nginxInstall.GetComposePath(), "build"); err != nil {
return err
}
parentTask.LogSuccess(logStr)
@@ -680,6 +682,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
if install.App.Key == vllmAppKeyForUpgrade && !isVllmUpgradeVersionAllowed(install.Version, detail.Version, loadVllmImageFromEnv(install.Env)) {
return errors.New("vLLM can only upgrade within the same image type")
}
if install.Version == detail.Version {
return errors.New("two version is same")
}
@@ -746,9 +751,25 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
dockerCLi, _ := docker.NewClient()
if install.App.Key == vllmAppKeyForUpgrade {
envs := make(map[string]interface{})
if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
return err
}
image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version)
envs[vllmImageEnvKey] = image
paramByte, err := json.Marshal(envs)
if err != nil {
return err
}
install.Env = string(paramByte)
content = setVllmImageInEnvContent(content, image)
}
if req.PullImage {
composeContent := []byte(detail.DockerCompose)
if install.App.Key == vllmAppKeyForUpgrade {
composeContent = []byte(install.DockerCompose)
}
if req.DockerCompose != "" {
composeContent = []byte(req.DockerCompose)
}
@@ -756,18 +777,31 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil {
return err
}
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
t.Log(i18n.GetWithName("PullImageStart", image))
if err = dockerCLi.PullImageWithProcess(t, image); err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
exist, err := dockerCLi.ImageExists(image)
if err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
if !exist {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s not found", image))
return err
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
command := exec.Command("/bin/bash", "-c", fmt.Sprintf("cp -rn %s/* %s || true", detailDir, install.GetPath()))
_, _ = command.CombinedOutput()
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
if install.App.Key == constant.AppOpenresty {
installBuildDir := path.Join(install.GetPath(), "build")
detailBuildDir := path.Join(detailDir, "build")
@@ -806,9 +840,13 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
return err
}
if req.DockerCompose == "" {
newCompose, err = getUpgradeCompose(install, detail)
if err != nil {
return err
if install.App.Key == vllmAppKeyForUpgrade {
newCompose = install.DockerCompose
} else {
newCompose, err = getUpgradeCompose(install, detail)
if err != nil {
return err
}
}
} else {
newCompose = req.DockerCompose
@@ -883,7 +921,7 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
}
}
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskScopeApp, task.TaskUpgrade), upgradeApp, rollBackApp, 0, 1*time.Hour)
upgradeTask.AddSubTaskWithOps(task.GetTaskName(install.Name, task.TaskUpgrade, task.TaskScopeApp), upgradeApp, rollBackApp, 0, 1*time.Hour)
go func() {
err = upgradeTask.Execute()
@@ -1032,7 +1070,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 {
@@ -1127,7 +1165,7 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
task.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute), cmd.WithWorkDir(workDir))
if err := cmdMgr.RunBashC(scriptPath); err != nil {
if err := cmdMgr.Run("bash", scriptPath); err != nil {
task.LogFailedWithErr(logStr, err)
return err
}
@@ -1176,11 +1214,12 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error
if err != nil {
return err
}
imagePrefix := xpack.GetImagePrefix()
imagePrefix := xpack.MultiNodeProvider.GetImagePrefix()
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
defer dockerCLi.Close()
for _, image := range images {
if imagePrefix != "" {
lastSlashIndex := strings.LastIndex(image, "/")
@@ -1776,7 +1815,7 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName
if !serviceValid {
return buserr.New("ErrFileParse")
}
imagePreFix := xpack.GetImagePrefix()
imagePreFix := xpack.MultiNodeProvider.GetImagePrefix()
if imagePreFix != "" {
for _, service := range services {
serviceValue := service.(map[string]interface{})
@@ -1933,6 +1972,39 @@ func isHostModel(dockerCompose string) bool {
return false
}
func copyAppDetailMissing(fileOp files.FileOp, srcDir, dstDir string) error {
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
if strings.HasPrefix(entry.Name(), ".") {
continue
}
srcPath := path.Join(srcDir, entry.Name())
dstPath := path.Join(dstDir, entry.Name())
if !fileOp.Stat(dstPath) {
if entry.IsDir() {
if err := fileOp.CopyDir(srcPath, dstDir); err != nil {
return err
}
continue
}
if err := fileOp.CopyFile(srcPath, dstDir); err != nil {
return err
}
continue
}
if !entry.IsDir() {
continue
}
if err := copyAppDetailMissing(fileOp, srcPath, dstPath); err != nil {
return err
}
}
return nil
}
func getRestartPolicy(yml string) string {
var project docker.ComposeProject
if err := yaml.Unmarshal([]byte(yml), &project); err != nil {
@@ -2119,7 +2191,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
caRequest := request.WebsiteCAObtain{
ID: ca.ID,
Domains: "localhost",
KeyType: "4096",
KeyType: "RSA4096",
Time: 99,
Unit: "year",
Dir: sslDir,
@@ -2246,10 +2318,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), &currentCompose); 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 {
+3
View File
@@ -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
+57 -20
View File
@@ -5,6 +5,7 @@ import (
"encoding/json"
"fmt"
"io/fs"
"net/netip"
"os"
"path"
"sort"
@@ -595,35 +596,22 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return err
}
networkConfig, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig)
removeBridgeDriverIPAM(recoverCtx.client, networkConfig, extraNetworks)
createRes, err := recoverCtx.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, recoverCtx.targetName)
createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
if err != nil {
return err
}
recoverCtx.createdContainerID = createRes.ID
extraNames := make([]string, 0, len(extraNetworks))
for name := range extraNetworks {
extraNames = append(extraNames, name)
}
sort.Strings(extraNames)
for _, item := range extraNames {
if err := recoverCtx.client.NetworkConnect(ctx, item, recoverCtx.createdContainerID, extraNetworks[item]); err != nil {
return err
}
}
return nil
}
func removeBridgeDriverIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
func removeUnsupportedEndpointStaticIPAM(cli *client.Client, primary *network.NetworkingConfig, extras map[string]*network.EndpointSettings) {
if primary != nil {
removeBridgeDriverIPAMFromEndpoints(cli, primary.EndpointsConfig)
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, primary.EndpointsConfig)
}
removeBridgeDriverIPAMFromEndpoints(cli, extras)
removeUnsupportedEndpointStaticIPAMFromEndpoints(cli, extras)
}
func removeBridgeDriverIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
func removeUnsupportedEndpointStaticIPAMFromEndpoints(cli *client.Client, endpoints map[string]*network.EndpointSettings) {
for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil {
continue
@@ -632,10 +620,59 @@ func removeBridgeDriverIPAMFromEndpoints(cli *client.Client, endpoints map[strin
if err != nil {
continue
}
if info.Driver == "bridge" {
endpoint.IPAMConfig = nil
removeUnsupportedEndpointStaticIP(netName, info, endpoint)
}
}
func removeUnsupportedEndpointStaticIP(netName string, info network.Inspect, endpoint *network.EndpointSettings) {
if endpoint == nil || endpoint.IPAMConfig == nil {
return
}
if isDefaultBridgeNetwork(netName, info) {
endpoint.IPAMConfig = nil
return
}
if endpoint.IPAMConfig.IPv4Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv4Address, false) {
endpoint.IPAMConfig.IPv4Address = ""
}
if endpoint.IPAMConfig.IPv6Address != "" && !networkSupportsStaticIP(info, endpoint.IPAMConfig.IPv6Address, true) {
endpoint.IPAMConfig.IPv6Address = ""
}
if endpoint.IPAMConfig.IPv4Address == "" && endpoint.IPAMConfig.IPv6Address == "" {
endpoint.IPAMConfig = nil
}
}
func isDefaultBridgeNetwork(netName string, info network.Inspect) bool {
return info.Driver == "bridge" && (netName == "bridge" || info.Name == "bridge")
}
func networkSupportsStaticIP(info network.Inspect, ip string, isIPv6 bool) bool {
if ip == "" {
return true
}
addr, err := netip.ParseAddr(ip)
if err != nil {
return false
}
if addr.Is6() != isIPv6 {
return false
}
for _, item := range info.IPAM.Config {
if item.Subnet == "" {
continue
}
subnet, err := netip.ParsePrefix(item.Subnet)
if err != nil || subnet.Addr().Is6() != isIPv6 {
continue
}
if subnet.Contains(addr) {
return true
}
}
return false
}
func ensureContainerRecoverNetworks(recoverCtx *containerRecoverContext) error {
+588
View File
@@ -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)
}
}
+2 -1
View File
@@ -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
}
+4 -3
View File
@@ -97,7 +97,8 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
}
}
if err := cmd.RunDefaultBashCf("docker exec %s redis-cli -a %s --no-auth-warning save", redisInfo.ContainerName, redisInfo.Password); err != nil {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(30 * time.Minute))
if err := cmdMgr.Run("docker", "exec", redisInfo.ContainerName, "redis-cli", "-a", redisInfo.Password, "--no-auth-warning", "save"); err != nil {
return err
}
@@ -109,13 +110,13 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
return nil
}
if strings.HasSuffix(fileName, ".aof") {
if err := cmd.RunDefaultBashCf("docker cp %s:/data/appendonly.aof %s/%s", redisInfo.ContainerName, backupDir, fileName); err != nil {
if err := cmdMgr.Run("docker", "cp", redisInfo.ContainerName+":/data/appendonly.aof", path.Join(backupDir, fileName)); err != nil {
return err
}
return nil
}
if err := cmd.RunDefaultBashCf("docker cp %s:/data/dump.rdb %s/%s", redisInfo.ContainerName, backupDir, fileName); err != nil {
if err := cmdMgr.Run("docker", "cp", redisInfo.ContainerName+":/data/dump.rdb", path.Join(backupDir, fileName)); err != nil {
return err
}
return nil

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