Compare commits

...
287 Commits
Author SHA1 Message Date
ssongliu 582cf1f533 feat: update vm and file resources (#13210) 2026-07-07 14:33:34 +08:00
ssongliu b8a4e8e9e0 chore: update xpack menu migration (#13207) 2026-07-06 11:14:29 +08:00
ssongliu 4a462aecc7 fix: reset firewall import dialogs on open (#13198) 2026-07-03 16:51:08 +08:00
ssongliu 74f221b895 fix: use svg renderer for container monitor charts (#13197) 2026-07-03 16:50:47 +08:00
ssongliu cbcb628bff fix: normalize firewall import addresses (#13196) 2026-07-03 16:37:03 +08:00
蘭 f602645e31 feat: Implement code editor layout with splitter for improved UI (#13195) 2026-07-03 16:01:44 +08:00
CityFun ec9e609f10 feat: add some translate (#13194) 2026-07-03 15:59:35 +08:00
ssongliu 1c1f3c7761 feat: support importing all firewall rules (#13192)
* feat: support importing all firewall rules

* fix: preserve selected firewall import action
2026-07-03 15:59:22 +08:00
ssongliu fae49284b7 fix: support mariadb manual backup args (#13193) 2026-07-03 15:58:15 +08:00
蘭 072a608a63 feat: File editor supports right-click operation (#13191) 2026-07-03 15:04:04 +08:00
蘭 1e6fd9c3a0 ref: Optimize menu settings (#13190) 2026-07-03 12:10:47 +08:00
ssongliu 23200cbe85 fix: support mariadb cronjob backup args (#13188) 2026-07-03 11:41:52 +08:00
ssongliu 08604e8c02 fix: fix dashboard running time calculation (#13187) 2026-07-03 11:41:37 +08:00
蘭 62a472d2d1 ref: refactor code editor theme management (#13186) 2026-07-03 11:41:22 +08:00
ssongliu 2c84665e8b chore: organize i18n language files (#13183) 2026-07-02 21:13:22 +08:00
蘭 616bd6830c feat: add menu accordion setting and update related components (#13182) 2026-07-02 21:13:07 +08:00
ssongliu e55ea96f00 feat: support virtual machine (#13171)
* feat: support virtual machine

* feat: add vm operation logs
2026-07-02 15:07:48 +08:00
蘭 aa676e139c feat: implement code editor theme management and synchronization (#13169) 2026-07-02 14:49:52 +08:00
ssongliu 7708fea58d feat: update operation log content (#13166) 2026-07-01 18:14:33 +08:00
ssongliu 34bdff01ad fix: allow ssl push on selected node (#13165) 2026-07-01 17:04:00 +08:00
CityFun 5a49579ddf feat: Optimize error messages for HTTP certificate requests (#13164) 2026-07-01 16:19:57 +08:00
CityFun b98a2b06c9 feat: Add multiple dimensions to website search (#13163) 2026-07-01 16:19:43 +08:00
ssongliu 9073b16baa feat: support compose rebuild operation (#13162) 2026-07-01 16:19:25 +08:00
ssongliu 2ec4db8d7e feat: adjust batch application installation API (#13161) 2026-07-01 16:19:12 +08:00
CityFun fe9628f91c feat: Add TaskLog for MCP Servers (#13160) 2026-07-01 16:18:59 +08:00
蘭 18a469e4be fix: Enhance Python language support in Monaco editor (#13158) 2026-07-01 16:18:42 +08:00
蘭 8c691cc074 fix: Resolve the issue of inability to access mobile web login (#13157) 2026-07-01 16:18:26 +08:00
ssongliu 2cd31a47f8 fix: complete operation logs (#13155) 2026-06-30 22:22:20 +08:00
CityFun d97aa9c2a0 feat: Optimize SSH session performance on Ubuntu 25. (#13152)
* feat: Optimize SSH session performance on Ubuntu 25.

* feat: Optimize SSH session performance on Ubuntu 25.

* feat: Optimize SSH session performance on Ubuntu 25.

* feat: Optimize SSH session performance on Ubuntu 25.
2026-06-30 18:36:28 +08:00
ssongliu f31f0a89ff feat: support certificate synchronization (#13153) 2026-06-30 18:31:45 +08:00
CityFun 0a1621166e feat: Unify the default website and default site under the same button. (#13149) 2026-06-30 15:56:25 +08:00
蘭 8623aa5318 feat: Enhance API authentication with HMAC-SHA256 support and user role management (#13148) 2026-06-30 14:16:36 +08:00
CityFun ec50599526 feat: Node runtime environment supports configuring whether to install node_modules. (#13147) 2026-06-30 14:16:22 +08:00
CityFun 458a30c520 feat: Add a description for syncing WAF IP groups. (#13143) 2026-06-29 18:42:02 +08:00
Andrea Leone 9a660db4c3 feat: add Ionos DNS Provider (#13024)
* feat: add Ionos DNS Provider

* Fix typo string formatting for APIKey in Ionos config
2026-06-29 16:11:33 +08:00
Reza Alipour Kondori f4fc71d32b add some translate and update Persian (#13132)
* Add Persian (fa) translations

* Update index.ts

Add fa to LOCALE_LOADERS

* Update fu.ts

Add fa item

* Update index.vue

enable Persian (fa) in language selector

* Update login-form.vue

enable Persian (fa) in language selector

* Create fa.yaml

add fa backend translations

* Update i18n.go

Add fa to langFiles variable

* Add Persian (fa) Translation to agent

* Add Persian README file

Add Persian (fa) README.fa.md to project

* fix(i18n): add Persian (fa) to backend validation and login dropdown

* Update README.fa.md

* Upade link in readme
2026-06-29 15:25:56 +08:00
王贺 e45a8185e3 fix container upgrade network IP handling (#13126) 2026-06-26 18:38:11 +08:00
CityFun 7be9189b3c fix: Fixed issue with reload ssl failed (#13129) 2026-06-26 18:08:38 +08:00
蘭 bdf13c01f6 ref: Improve support for Persian (fa) language (#13128) 2026-06-26 18:08:22 +08:00
王贺 902bc90aef fix: Fix updating the application host binding IP (#13127) 2026-06-26 18:08:08 +08:00
Octopusandocto-patch 27ba50d8be feat: add MiniMax M3 model to provider catalog (#13119)
Add the latest MiniMax-M3 flagship model to the MiniMax provider model
list and set it as the default. M3 offers a 1M context window, 128K max
output, reasoning, and image input. Drop the older M2.5 entries while
keeping M2.7 as an alternative, and update the account verification probe
to use M3.

Co-authored-by: octo-patch <266937838+octo-patch@users.noreply.github.com>
2026-06-26 17:55:04 +08:00
Reza Alipour Kondori fb483ecb90 Add Persian (fa) translations (#13100)
* Add Persian (fa) translations

* Update index.ts

Add fa to LOCALE_LOADERS

* Update fu.ts

Add fa item

* Update index.vue

enable Persian (fa) in language selector

* Update login-form.vue

enable Persian (fa) in language selector

* Create fa.yaml

add fa backend translations

* Update i18n.go

Add fa to langFiles variable

* Add Persian (fa) Translation to agent
2026-06-26 17:17:32 +08:00
蘭 2de7079dd2 feat: add new localization strings for skill management features (#13124) 2026-06-26 16:17:57 +08:00
CityFun 2e4ac604a8 fix: Fix incomplete log download issue (#13123) 2026-06-26 16:17:40 +08:00
CityFun 46af80142e feat: add username/password support for hermes-agent (#13121)
* feat: add username/password support for hermes-agent

* feat: add username/password support for hermes-agent

* feat: add username/password support for hermes-agent
2026-06-26 16:17:23 +08:00
CityFun 677b47dcdb fix: Fixed issue with openclaw config telegram failed (#13114) 2026-06-25 18:40:34 +08:00
王贺 56cccdf7c0 refactor: remove skills hub server leftovers (#13115) 2026-06-25 15:09:21 +08:00
蘭 3e0d0bb809 feat: add quick toggle comment functionality in the editor (#13107) 2026-06-24 18:23:51 +08:00
ssongliu 86aa6c09c6 fix: resolve snapshot extraction directory missing issue (#13104) 2026-06-24 18:23:37 +08:00
蘭 f67e1d897c ref: add some translate (#13102) 2026-06-24 18:23:23 +08:00
蘭 9e518daa62 ref: add some translate (#13093) 2026-06-23 14:05:48 +08:00
CityFun 2e5455e655 feat: Add “Remove old images during app upgrade” configuration to the App Store. (#13092) 2026-06-23 14:05:33 +08:00
CityFun 868f1db4c5 feat: Create and delete runtime environments, and add task logs. (#13081) 2026-06-18 18:46:32 +08:00
王贺 efd84b1e1b feat: support skills hub server frontend (#13074) 2026-06-18 16:57:53 +08:00
CityFun 5591fe50f0 chore: optimize runtime environment container configuration (#13071)
* chore: optimize runtime environment container configuration

* chore: optimize runtime environment container configuration
2026-06-17 19:09:31 +08:00
王贺 5e0a598799 feat: update waf prompt translations (#13070) 2026-06-17 17:00:28 +08:00
CityFun 45d7b30abc feat: Add support for syncing self-signed certificates and manually u… (#13068)
* feat: Add support for syncing self-signed certificates and manually uploaded certificates to other nodes.

* feat: Add support for syncing self-signed certificates and manually
2026-06-17 17:00:15 +08:00
ssongliu 46af88e1d0 feat: support app and image synchronization (#13067) 2026-06-17 15:41:09 +08:00
CityFun 7f331f5503 fix: 解决证书推送到其他节点报错的问题 (#13065) 2026-06-17 15:31:51 +08:00
igophper 7b74e09d30 fix: manage expanded node states in the tree view component (#13064) 2026-06-17 12:36:23 +08:00
蘭 01e4455110 refactor: Remove error logging for missing alert config in alert handling (#13060) 2026-06-17 11:08:45 +08:00
CityFun 922a2d21c8 feat: add protocolVersion param for MCP Server (#13057)
* feat: add protocolVersion param for MCP Server

* feat: add protocolVersion param for MCP Server
2026-06-16 16:47:27 +08:00
CityFun 439c4794b7 feat: Optimize the vLLM display (#13056) 2026-06-15 16:42:00 +08:00
ssongliu 5ac7f16b67 feat: optimize image deletion logs (#13053) 2026-06-15 15:41:46 +08:00
蘭 bfb79066b4 fix: Adjust the default values of alert attributes (#13052) 2026-06-15 15:41:33 +08:00
蘭 19602b42fb feat: add QR code verification endpoint to password expiration checks (#13049) 2026-06-15 15:15:09 +08:00
ssongliu f987af264f feat: disable script library auto-sync on startup (#13047) 2026-06-15 13:46:24 +08:00
ssongliu a6c76dffc3 feat: add snapshot rollback prompt (#13045) 2026-06-15 13:46:13 +08:00
CityFun a2eed66bc4 fix: fixed issue with refresh agent page failed (#13035) 2026-06-12 18:14:35 +08:00
CityFun a49b9b132d feat: update app upgrade logic (#13032) 2026-06-12 17:44:34 +08:00
ssongliu e8505a249a fix: resolve password expiration issue in multi-user mode (#13030) 2026-06-12 14:16:16 +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
862 changed files with 81494 additions and 73284 deletions
-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: '24.11.1'
- 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.25'
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
+16 -2
View File
@@ -40,10 +40,23 @@ core/cmd/server/web/index.html
frontend/auto-imports.d.ts frontend/auto-imports.d.ts
frontend/components.d.ts frontend/components.d.ts
frontend/src/xpack frontend/src/xpack
frontend/src/xpack-ee frontend/src/enterprise
agent/xpack 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/enterprise.go
core/xpack core/xpack
core/xpack-ee 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/enterprise.go
.history/ .history/
dist/ dist/
@@ -65,3 +78,4 @@ AGENTS.md
opencode.json opencode.json
superpowers superpowers
.worktrees/ .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:"
+2
View File
@@ -2,8 +2,10 @@ reviewers:
- wanghe-fit2cloud - wanghe-fit2cloud
- zhengkunwang223 - zhengkunwang223
- ssongliu - ssongliu
- lan-yonghui
approvers: approvers:
- wanghe-fit2cloud - wanghe-fit2cloud
- zhengkunwang223 - zhengkunwang223
- ssongliu - ssongliu
- lan-yonghui
+2 -1
View File
@@ -25,7 +25,7 @@
<a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a> <a href="/docs/README.pt-br.md"><img alt="Português (Brasil)" src="https://img.shields.io/badge/Português (Brasil)-d9d9d9"></a>
<a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a> <a href="/docs/README.ar.md"><img alt="العربية" src="https://img.shields.io/badge/العربية-d9d9d9"></a>
<a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a> <a href="/docs/README.de.md"><img alt="Deutsch" src="https://img.shields.io/badge/Deutsch-d9d9d9"></a>
<a href="/docs/README.es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a> <a href="/docs/README.es-es.md"><img alt="Español" src="https://img.shields.io/badge/Español-d9d9d9"></a>
<a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a> <a href="/docs/README.fr.md"><img alt="français" src="https://img.shields.io/badge/français-d9d9d9"></a>
<a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a> <a href="/docs/README.ko.md"><img alt="한국어" src="https://img.shields.io/badge/한국어-d9d9d9"></a>
<a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a> <a href="/docs/README.id.md"><img alt="Bahasa Indonesia" src="https://img.shields.io/badge/Bahasa Indonesia-d9d9d9"></a>
@@ -33,6 +33,7 @@
<a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a> <a href="/docs/README.tr.md"><img alt="Türkçe" src="https://img.shields.io/badge/Türkçe-d9d9d9"></a>
<a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a> <a href="/docs/README.ru.md"><img alt="Русский" src="https://img.shields.io/badge/Русский-d9d9d9"></a>
<a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a> <a href="/docs/README.ms.md"><img alt="Bahasa Melayu" src="https://img.shields.io/badge/Bahasa Melayu-d9d9d9"></a>
<a href="/docs/README.fa.md"><img alt="Persian" src="https://img.shields.io/badge/%D9%81%D8%A7%D8%B1%D8%B3%DB%8C-d9d9d9"></a>
</p> </p>
--- ---
+125 -20
View File
@@ -27,6 +27,90 @@ func (b *BaseApi) CreateAgent(c *gin.Context) {
helper.SuccessWithData(c, res) 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 // @Tags AI
// @Summary Page Agents // @Summary Page Agents
// @Accept json // @Accept json
@@ -296,11 +380,11 @@ func (b *BaseApi) DeleteHermesChatSession(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Get Providers // @Summary Get model account providers
// @Success 200 {array} dto.ProviderInfo // @Success 200 {array} dto.ProviderInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/providers [get] // @Router /ai/accounts/providers [get]
func (b *BaseApi) GetAgentProviders(c *gin.Context) { func (b *BaseApi) GetAgentProviders(c *gin.Context) {
list, err := agentService.GetProviders() list, err := agentService.GetProviders()
if err != nil { if err != nil {
@@ -311,13 +395,13 @@ func (b *BaseApi) GetAgentProviders(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Create Agent account // @Summary Create model account
// @Accept json // @Accept json
// @Param request body dto.AgentAccountCreateReq true "request" // @Param request body dto.AgentAccountCreateReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts [post] // @Router /ai/accounts [post]
func (b *BaseApi) CreateAgentAccount(c *gin.Context) { func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
var req dto.AgentAccountCreateReq var req dto.AgentAccountCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -331,13 +415,13 @@ func (b *BaseApi) CreateAgentAccount(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Update Agent account // @Summary Update model account
// @Accept json // @Accept json
// @Param request body dto.AgentAccountUpdateReq true "request" // @Param request body dto.AgentAccountUpdateReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/update [post] // @Router /ai/accounts/update [post]
func (b *BaseApi) UpdateAgentAccount(c *gin.Context) { func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
var req dto.AgentAccountUpdateReq var req dto.AgentAccountUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -351,13 +435,13 @@ func (b *BaseApi) UpdateAgentAccount(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Page Agent accounts // @Summary Page model accounts
// @Accept json // @Accept json
// @Param request body dto.AgentAccountSearch true "request" // @Param request body dto.AgentAccountSearch true "request"
// @Success 200 {object} dto.PageResult // @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/search [post] // @Router /ai/accounts/search [post]
func (b *BaseApi) PageAgentAccounts(c *gin.Context) { func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
var req dto.AgentAccountSearch var req dto.AgentAccountSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -375,13 +459,34 @@ func (b *BaseApi) PageAgentAccounts(c *gin.Context) {
} }
// @Tags AI // @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 // @Accept json
// @Param request body dto.AgentAccountModelReq true "request" // @Param request body dto.AgentAccountModelReq true "request"
// @Success 200 {array} dto.AgentAccountModel // @Success 200 {array} dto.AgentAccountModel
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/models [post] // @Router /ai/accounts/models [post]
func (b *BaseApi) GetAgentAccountModels(c *gin.Context) { func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
var req dto.AgentAccountModelReq var req dto.AgentAccountModelReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -396,13 +501,13 @@ func (b *BaseApi) GetAgentAccountModels(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Create Agent account model // @Summary Create model account model
// @Accept json // @Accept json
// @Param request body dto.AgentAccountModelCreateReq true "request" // @Param request body dto.AgentAccountModelCreateReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/models/create [post] // @Router /ai/accounts/models/create [post]
func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) { func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelCreateReq var req dto.AgentAccountModelCreateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -416,13 +521,13 @@ func (b *BaseApi) CreateAgentAccountModel(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Update Agent account model // @Summary Update model account model
// @Accept json // @Accept json
// @Param request body dto.AgentAccountModelUpdateReq true "request" // @Param request body dto.AgentAccountModelUpdateReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/models/update [post] // @Router /ai/accounts/models/update [post]
func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) { func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelUpdateReq var req dto.AgentAccountModelUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -436,13 +541,13 @@ func (b *BaseApi) UpdateAgentAccountModel(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Delete Agent account model // @Summary Delete model account model
// @Accept json // @Accept json
// @Param request body dto.AgentAccountModelDeleteReq true "request" // @Param request body dto.AgentAccountModelDeleteReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/models/delete [post] // @Router /ai/accounts/models/delete [post]
func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) { func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
var req dto.AgentAccountModelDeleteReq var req dto.AgentAccountModelDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -456,13 +561,13 @@ func (b *BaseApi) DeleteAgentAccountModel(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Verify Agent account // @Summary Verify model account
// @Accept json // @Accept json
// @Param request body dto.AgentAccountVerifyReq true "request" // @Param request body dto.AgentAccountVerifyReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/verify [post] // @Router /ai/accounts/verify [post]
func (b *BaseApi) VerifyAgentAccount(c *gin.Context) { func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
var req dto.AgentAccountVerifyReq var req dto.AgentAccountVerifyReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -476,13 +581,13 @@ func (b *BaseApi) VerifyAgentAccount(c *gin.Context) {
} }
// @Tags AI // @Tags AI
// @Summary Delete Agent account // @Summary Delete model account
// @Accept json // @Accept json
// @Param request body dto.AgentAccountDeleteReq true "request" // @Param request body dto.AgentAccountDeleteReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /ai/agents/accounts/delete [post] // @Router /ai/accounts/delete [post]
func (b *BaseApi) DeleteAgentAccount(c *gin.Context) { func (b *BaseApi) DeleteAgentAccount(c *gin.Context) {
var req dto.AgentAccountDeleteReq var req dto.AgentAccountDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+9 -42
View File
@@ -3,9 +3,6 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/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" "github.com/gin-gonic/gin"
) )
@@ -163,37 +160,6 @@ func (b *BaseApi) DeleteOllamaModel(c *gin.Context) {
helper.Success(c) 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 // @Tags AI
// @Summary Bind domain // @Summary Bind domain
// @Accept json // @Accept json
@@ -235,14 +201,15 @@ func (b *BaseApi) GetBindDomain(c *gin.Context) {
helper.SuccessWithData(c, res) helper.SuccessWithData(c, res)
} }
// Tags AI // @Tags AI
// Summary Update bind domain // @Summary Update bind domain
// Accept json // @Accept json
// Param request body dto.OllamaBindDomain true "request" // @Param request body dto.OllamaBindDomain true "request"
// Success 200 // @Success 200
// Security ApiKeyAuth // @Security ApiKeyAuth
// Security Timestamp // @Security Timestamp
// Router /ai/domain/update [post] // @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) { func (b *BaseApi) UpdateBindDomain(c *gin.Context) {
var req dto.OllamaBindDomain var req dto.OllamaBindDomain
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+159 -4
View File
@@ -2,11 +2,24 @@ package v2
import ( import (
"errors" "errors"
"net/url"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/gin-gonic/gin" "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) { func (b *BaseApi) PageAlert(c *gin.Context) {
var req dto.AlertSearch var req dto.AlertSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -32,12 +45,21 @@ func (b *BaseApi) GetAlerts(c *gin.Context) {
helper.SuccessWithData(c, alerts) 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) { func (b *BaseApi) CreateAlert(c *gin.Context) {
var req dto.AlertCreate var req dto.AlertCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
err := alertService.CreateAlert(req) err := alertService.CreateAlert(req, loadAuditUser(c))
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -45,6 +67,15 @@ func (b *BaseApi) CreateAlert(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) DeleteAlert(c *gin.Context) {
var req dto.DeleteRequest var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -58,12 +89,21 @@ func (b *BaseApi) DeleteAlert(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) UpdateAlert(c *gin.Context) {
var req dto.AlertUpdate var req dto.AlertUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := alertService.UpdateAlert(req); err != nil { if err := alertService.UpdateAlert(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -84,6 +124,15 @@ func (b *BaseApi) GetAlert(c *gin.Context) {
helper.SuccessWithData(c, alert) 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) { func (b *BaseApi) UpdateAlertStatus(c *gin.Context) {
var req dto.AlertUpdateStatus var req dto.AlertUpdateStatus
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -97,6 +146,12 @@ func (b *BaseApi) UpdateAlertStatus(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) GetDisks(c *gin.Context) {
alerts, err := alertService.GetDisks() alerts, err := alertService.GetDisks()
if err != nil { if err != nil {
@@ -106,6 +161,14 @@ func (b *BaseApi) GetDisks(c *gin.Context) {
helper.SuccessWithData(c, alerts) 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) { func (b *BaseApi) PageAlertLogs(c *gin.Context) {
var req dto.AlertLogSearch var req dto.AlertLogSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil { 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) { func (b *BaseApi) CleanAlertLogs(c *gin.Context) {
if err := alertService.CleanAlertLogs(); err != nil { if err := alertService.CleanAlertLogs(); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -130,6 +200,12 @@ func (b *BaseApi) CleanAlertLogs(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) GetClams(c *gin.Context) {
clams, err := alertService.GetClams() clams, err := alertService.GetClams()
if err != nil { if err != nil {
@@ -139,6 +215,14 @@ func (b *BaseApi) GetClams(c *gin.Context) {
helper.SuccessWithData(c, clams) 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) { func (b *BaseApi) GetCronJobs(c *gin.Context) {
var req dto.CronJobReq var req dto.CronJobReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -152,8 +236,18 @@ func (b *BaseApi) GetCronJobs(c *gin.Context) {
helper.SuccessWithData(c, cronJobs) 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) { 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 { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -161,18 +255,71 @@ func (b *BaseApi) GetAlertConfig(c *gin.Context) {
helper.SuccessWithData(c, config) 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) { func (b *BaseApi) UpdateAlertConfig(c *gin.Context) {
var req dto.AlertConfigUpdate var req dto.AlertConfigUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if err := alertService.UpdateAlertConfig(req); err != nil { if err := alertService.UpdateAlertConfig(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.Success(c) 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) { func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
var req dto.DeleteRequest var req dto.DeleteRequest
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -186,6 +333,14 @@ func (b *BaseApi) DeleteAlertConfig(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) TestAlertConfig(c *gin.Context) {
var req dto.AlertConfigTest var req dto.AlertConfigTest
if err := helper.CheckBindAndValidate(&req, c); err != nil { 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 // @Accept json
// @Param appId path integer true "app id" // @Param appId path integer true "app id"
// @Param version path string true "app 版本" // @Param version path string true "app 版本"
// @Param version path string true "app 类型" // @Param type path string true "app 类型"
// @Success 200 {object} response.AppDetailDTO // @Success 200 {object} response.AppDetailDTO
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -131,7 +131,7 @@ func (b *BaseApi) GetAppDetail(c *gin.Context) {
// @Tags App // @Tags App
// @Summary Get app detail by id // @Summary Get app detail by id
// @Accept json // @Accept json
// @Param appId path integer true "id" // @Param id path integer true "id"
// @Success 200 {object} response.AppDetailDTO // @Success 200 {object} response.AppDetailDTO
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -172,6 +172,12 @@ func (b *BaseApi) InstallApp(c *gin.Context) {
helper.SuccessWithData(c, install) 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) { func (b *BaseApi) GetAppTags(c *gin.Context) {
tags, err := appService.GetAppTags(c) tags, err := appService.GetAppTags(c)
if err != nil { if err != nil {
@@ -199,7 +205,7 @@ func (b *BaseApi) GetAppListUpdate(c *gin.Context) {
// @Tags App // @Tags App
// @Summary Get app icon by app_id // @Summary Get app icon by app_id
// @Accept json // @Accept json
// @Param appId path integer true "app id" // @Param key path string true "app key"
// @Success 200 {file} file "app icon" // @Success 200 {file} file "app icon"
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -237,7 +243,7 @@ func (b *BaseApi) GetAppIcon(c *gin.Context) {
// @Tags App // @Tags App
// @Summary Search app detail by appkey and version // @Summary Search app detail by appkey and version
// @Accept json // @Accept json
// @Param appId path integer true "app key" // @Param appKey path string true "app key"
// @Param version path string true "app version" // @Param version path string true "app version"
// @Success 200 {object} response.AppDetailSimpleDTO // @Success 200 {object} response.AppDetailSimpleDTO
// @Security ApiKeyAuth // @Security ApiKeyAuth
+10 -2
View File
@@ -203,7 +203,6 @@ func (b *BaseApi) GetServices(c *gin.Context) {
// @Tags App // @Tags App
// @Summary Search app update version by install id // @Summary Search app update version by install id
// @Accept json // @Accept json
// @Param appInstallId path integer true "request"
// @Success 200 {array} dto.AppVersion // @Success 200 {array} dto.AppVersion
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -267,7 +266,7 @@ func (b *BaseApi) GetDefaultConfig(c *gin.Context) {
// @Tags App // @Tags App
// @Summary Search params by appInstallId // @Summary Search params by appInstallId
// @Accept json // @Accept json
// @Param appInstallId path string true "request" // @Param appInstallId path integer true "request"
// @Success 200 {object} response.AppConfig // @Success 200 {object} response.AppConfig
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -350,6 +349,15 @@ func (b *BaseApi) GetAppInstallInfo(c *gin.Context) {
helper.SuccessWithData(c, info) 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) { func (b *BaseApi) UpdateAppInstallSort(c *gin.Context) {
var req request.AppInstallSort var req request.AppInstallSort
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+15 -3
View File
@@ -10,6 +10,13 @@ import (
"github.com/gin-gonic/gin" "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) { func (b *BaseApi) CheckBackupUsed(c *gin.Context) {
name, err := helper.GetStrParamByKey(c, "name") name, err := helper.GetStrParamByKey(c, "name")
if err != nil { if err != nil {
@@ -32,7 +39,7 @@ func (b *BaseApi) CheckBackupUsed(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /backups/check [post] // @Router /backups/conn/check [post]
func (b *BaseApi) CheckBackup(c *gin.Context) { func (b *BaseApi) CheckBackup(c *gin.Context) {
var req dto.BackupOperate var req dto.BackupOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -136,7 +143,7 @@ func (b *BaseApi) DeleteBackup(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /backups/update [post] // @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) { func (b *BaseApi) UpdateBackup(c *gin.Context) {
var req dto.BackupOperate var req dto.BackupOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -406,10 +413,15 @@ func (b *BaseApi) Backup(c *gin.Context) {
switch req.Type { switch req.Type {
case "app": case "app":
if _, err := backupService.AppBackup(req); err != nil { record, err := backupService.AppBackup(req)
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
if req.IsImmediate {
helper.SuccessWithData(c, record)
return
}
case "mysql", "mariadb", constant.AppMysqlCluster: case "mysql", "mariadb", constant.AppMysqlCluster:
if err := backupService.MysqlBackup(req); err != nil { if err := backupService.MysqlBackup(req); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
+3 -3
View File
@@ -21,7 +21,7 @@ func (b *BaseApi) CreateClam(c *gin.Context) {
return return
} }
if err := clamService.Create(req); err != nil { if err := clamService.Create(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -43,7 +43,7 @@ func (b *BaseApi) UpdateClam(c *gin.Context) {
return return
} }
if err := clamService.Update(req); err != nil { if err := clamService.Update(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -123,7 +123,7 @@ func (b *BaseApi) LoadClamBaseInfo(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /toolbox/clam/operate [post] // @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) { func (b *BaseApi) OperateClam(c *gin.Context) {
var req dto.Operate var req dto.Operate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+16
View File
@@ -5,6 +5,7 @@ import (
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -394,6 +395,7 @@ func (b *BaseApi) ContainerInfo(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Container
// @Summary Load container limits // @Summary Load container limits
// @Success 200 {object} dto.ResourceLimit // @Success 200 {object} dto.ResourceLimit
// @Security ApiKeyAuth // @Security ApiKeyAuth
@@ -408,6 +410,7 @@ func (b *BaseApi) LoadResourceLimit(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Container
// @Summary Load container stats // @Summary Load container stats
// @Success 200 {array} dto.ContainerListStats // @Success 200 {array} dto.ContainerListStats
// @Security ApiKeyAuth // @Security ApiKeyAuth
@@ -422,6 +425,7 @@ func (b *BaseApi) ContainerListStats(c *gin.Context) {
helper.SuccessWithData(c, data) helper.SuccessWithData(c, data)
} }
// @Tags Container
// @Summary Load container stats size // @Summary Load container stats size
// @Accept json // @Accept json
// @Param request body dto.OperationWithName true "request" // @Param request body dto.OperationWithName true "request"
@@ -662,6 +666,14 @@ func (b *BaseApi) Inspect(c *gin.Context) {
helper.SuccessWithData(c, result) 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) { func (b *BaseApi) DownloadContainerLogs(c *gin.Context) {
var req dto.ContainerLog var req dto.ContainerLog
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -901,6 +913,10 @@ func (b *BaseApi) LoadComposeEnv(c *gin.Context) {
// @Security Timestamp // @Security Timestamp
// @Router /containers/search/log [get] // @Router /containers/search/log [get]
func (b *BaseApi) ContainerStreamLogs(c *gin.Context) { 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("Content-Type", "text/event-stream")
c.Header("Cache-Control", "no-cache") c.Header("Cache-Control", "no-cache")
c.Header("Connection", "keep-alive") c.Header("Connection", "keep-alive")
+3 -3
View File
@@ -26,7 +26,7 @@ func (b *BaseApi) CreateCronjob(c *gin.Context) {
return return
} }
if err := cronjobService.Create(req); err != nil { if err := cronjobService.Create(req, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -91,7 +91,7 @@ func (b *BaseApi) ImportCronjob(c *gin.Context) {
return return
} }
if err := cronjobService.Import(req.Cronjobs); err != nil { if err := cronjobService.Import(req.Cronjobs, loadAuditUser(c)); err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
@@ -285,7 +285,7 @@ func (b *BaseApi) UpdateCronjob(c *gin.Context) {
return 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) helper.InternalServer(c, err)
return return
} }
+3
View File
@@ -95,6 +95,7 @@ func (b *BaseApi) SearchDatabase(c *gin.Context) {
// @Success 200 {array} dto.DatabaseOption // @Success 200 {array} dto.DatabaseOption
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param type path string true "type"
// @Router /databases/db/list/:type [get] // @Router /databases/db/list/:type [get]
func (b *BaseApi) ListDatabase(c *gin.Context) { func (b *BaseApi) ListDatabase(c *gin.Context) {
dbType, err := helper.GetStrParamByKey(c, "type") dbType, err := helper.GetStrParamByKey(c, "type")
@@ -116,6 +117,7 @@ func (b *BaseApi) ListDatabase(c *gin.Context) {
// @Success 200 {array} dto.DatabaseItem // @Success 200 {array} dto.DatabaseItem
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param type path string true "type"
// @Router /databases/db/item/:type [get] // @Router /databases/db/item/:type [get]
func (b *BaseApi) LoadDatabaseItems(c *gin.Context) { func (b *BaseApi) LoadDatabaseItems(c *gin.Context) {
dbType, err := helper.GetStrParamByKey(c, "type") dbType, err := helper.GetStrParamByKey(c, "type")
@@ -137,6 +139,7 @@ func (b *BaseApi) LoadDatabaseItems(c *gin.Context) {
// @Success 200 {object} dto.DatabaseInfo // @Success 200 {object} dto.DatabaseInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param name path string true "name"
// @Router /databases/db/:name [get] // @Router /databases/db/:name [get]
func (b *BaseApi) GetDatabase(c *gin.Context) { func (b *BaseApi) GetDatabase(c *gin.Context) {
name, err := helper.GetStrParamByKey(c, "name") name, err := helper.GetStrParamByKey(c, "name")
+2 -1
View File
@@ -92,7 +92,7 @@ func (b *BaseApi) UpdatePostgresqlDescription(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /databases/pg/privileges [post] // @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) { func (b *BaseApi) ChangePostgresqlPrivileges(c *gin.Context) {
var req dto.PostgresqlPrivileges var req dto.PostgresqlPrivileges
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -170,6 +170,7 @@ func (b *BaseApi) SearchPostgresql(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param database path string true "database"
// @Router /databases/pg/:database/load [post] // @Router /databases/pg/:database/load [post]
func (b *BaseApi) LoadPostgresqlDBFromRemote(c *gin.Context) { func (b *BaseApi) LoadPostgresqlDBFromRemote(c *gin.Context) {
database, err := helper.GetStrParamByKey(c, "database") database, err := helper.GetStrParamByKey(c, "database")
+6
View File
@@ -74,6 +74,12 @@ func (b *BaseApi) LoadPersistenceConf(c *gin.Context) {
helper.SuccessWithData(c, data) 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) { func (b *BaseApi) CheckHasCli(c *gin.Context) {
helper.SuccessWithData(c, redisService.CheckHasCli()) helper.SuccessWithData(c, redisService.CheckHasCli())
} }
+68 -18
View File
@@ -193,7 +193,7 @@ func (b *BaseApi) BatchDeleteFile(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /files/mode [post] // @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) { func (b *BaseApi) ChangeFileMode(c *gin.Context) {
var req request.FileCreate var req request.FileCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -215,7 +215,7 @@ func (b *BaseApi) ChangeFileMode(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /files/owner [post] // @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) { func (b *BaseApi) ChangeFileOwner(c *gin.Context) {
var req request.FileRoleUpdate var req request.FileRoleUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -292,6 +292,26 @@ func (b *BaseApi) DeCompressFile(c *gin.Context) {
helper.Success(c) 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 // @Tags File
// @Summary Load file content // @Summary Load file content
// @Accept json // @Accept json
@@ -391,7 +411,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
helper.BadRequest(c, errors.New("error paths in request")) helper.BadRequest(c, errors.New("error paths in request"))
return return
} }
dir := path.Dir(paths[0]) dir := path.Clean(paths[0])
_, err = os.Stat(dir) _, err = os.Stat(dir)
if err != nil && os.IsNotExist(err) { if err != nil && os.IsNotExist(err) {
@@ -432,8 +452,14 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
} }
_ = os.Chown(dstDir, uid, gid) _ = 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" tmpFilename := dstFilename + ".tmp"
if err := c.SaveUploadedFile(file, tmpFilename); err != nil { if err := c.SaveUploadedFile(file, tmpFilename, dstDirMode); err != nil {
_ = os.Remove(tmpFilename) _ = os.Remove(tmpFilename)
e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err) e := fmt.Errorf("upload [%s] file failed, err: %v", file.Filename, err)
failures[file.Filename] = e failures[file.Filename] = e
@@ -456,7 +482,7 @@ func (b *BaseApi) UploadFiles(c *gin.Context) {
if statErr == nil { if statErr == nil {
_ = os.Chmod(dstFilename, dstInfo.Mode()) _ = os.Chmod(dstFilename, dstInfo.Mode())
} else { } else {
_ = os.Chmod(dstFilename, mode) _ = os.Chmod(dstFilename, dstFileMode)
} }
if uid != -1 && gid != -1 { if uid != -1 && gid != -1 {
_ = os.Chown(dstFilename, uid, gid) _ = os.Chown(dstFilename, uid, gid)
@@ -614,6 +640,10 @@ func (b *BaseApi) MoveFile(c *gin.Context) {
// @Router /files/download [get] // @Router /files/download [get]
func (b *BaseApi) Download(c *gin.Context) { func (b *BaseApi) Download(c *gin.Context) {
filePath := c.Query("path") filePath := c.Query("path")
if files.ShouldDenySensitiveFileRead(filePath) {
helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
return
}
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -649,6 +679,10 @@ func (b *BaseApi) DownloadChunkFiles(c *gin.Context) {
helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPathNotFound", nil) helper.ErrorWithDetail(c, http.StatusInternalServerError, "ErrPathNotFound", nil)
return return
} }
if files.ShouldDenySensitiveFileRead(req.Path) {
helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
return
}
filePath := req.Path filePath := req.Path
fstFile, err := fileOp.OpenFile(filePath) fstFile, err := fileOp.OpenFile(filePath)
if err != nil { if err != nil {
@@ -767,12 +801,13 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
return err return err
} }
} }
if dstDirInfo, err := os.Stat(dstDir); err == nil {
mode = dstDirInfo.Mode().Perm()
}
dstFileName := filepath.Join(dstDir, fileName) dstFileName := filepath.Join(dstDir, fileName)
dstInfo, statErr := os.Stat(dstFileName) dstInfo, statErr := os.Stat(dstFileName)
if statErr == nil { if statErr == nil {
mode = dstInfo.Mode() mode = dstInfo.Mode()
} else {
mode = 0644
} }
if overwrite { if overwrite {
_ = os.Remove(dstFileName) _ = os.Remove(dstFileName)
@@ -794,6 +829,7 @@ func mergeChunks(fileName string, fileDir string, dstDir string, chunkCount int,
} }
_ = os.Remove(chunkPath) _ = os.Remove(chunkPath)
} }
_ = os.Chmod(dstFileName, mode)
return nil return nil
} }
@@ -898,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) { 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) ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
return return
@@ -908,6 +954,12 @@ func (b *BaseApi) WgetProcess(c *gin.Context) {
go wsClient.Write() 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) { func (b *BaseApi) ProcessKeys(c *gin.Context) {
res := &response.FileProcessKeys{} res := &response.FileProcessKeys{}
keys := global.CACHE.PrefixScanKey("file-wget-") keys := global.CACHE.PrefixScanKey("file-wget-")
@@ -927,16 +979,20 @@ func (b *BaseApi) ProcessKeys(c *gin.Context) {
// @Tags File // @Tags File
// @Summary Read file by Line // @Summary Read file by Line
// @Param type path string true "type"
// @Param request body request.FileReadByLineReq true "request" // @Param request body request.FileReadByLineReq true "request"
// @Success 200 {object} response.FileLineContent // @Success 200 {object} response.FileLineContent
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /files/read [post] // @Router /files/read/{type} [post]
func (b *BaseApi) ReadFileByLine(c *gin.Context) { func (b *BaseApi) ReadFileByLine(c *gin.Context) {
var req request.FileReadByLineReq var req request.FileReadByLineReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
if readType := strings.TrimSpace(c.Param("type")); readType != "" {
req.Type = readType
}
res, err := fileService.ReadLogByLine(req) res, err := fileService.ReadLogByLine(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
@@ -969,16 +1025,6 @@ func (b *BaseApi) BatchChangeModeAndOwner(c *gin.Context) {
helper.Success(c) 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 // @Tags File
// @Summary system mount // @Summary system mount
// @Accept json // @Accept json
@@ -1310,6 +1356,10 @@ func (b *BaseApi) DownloadFileShare(c *gin.Context) {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
if files.ShouldDenySensitiveFileRead(filePath) {
helper.InternalServer(c, buserr.New("ErrSensitiveFileRead"))
return
}
file, err := os.Open(filePath) file, err := os.Open(filePath)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
+1 -1
View File
@@ -11,7 +11,7 @@ import (
// @Summary Load file history list // @Summary Load file history list
// @Accept json // @Accept json
// @Param request body request.FileHistorySearchReq true "request" // @Param request body request.FileHistorySearchReq true "request"
// @Success 200 {object} response.PageResult // @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /files/history/search [post] // @Router /files/history/search [post]
+1 -1
View File
@@ -236,7 +236,7 @@ func (b *BaseApi) UpdateAddrRule(c *gin.Context) {
// @Success 200 {object} dto.PageResult // @Success 200 {object} dto.PageResult
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/firewall/filter/search [post] // @Router /hosts/firewall/filter/rule/search [post]
func (b *BaseApi) SearchFilterRules(c *gin.Context) { func (b *BaseApi) SearchFilterRules(c *gin.Context) {
var req dto.SearchPageWithType var req dto.SearchPageWithType
if err := helper.CheckBindAndValidate(&req, c); err != nil { 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" "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) { func (b *BaseApi) CheckHealth(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
+76
View File
@@ -7,6 +7,15 @@ import (
"github.com/gin-gonic/gin" "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) { func (b *BaseApi) CreateHost(c *gin.Context) {
var req dto.HostOperate var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -21,6 +30,14 @@ func (b *BaseApi) CreateHost(c *gin.Context) {
helper.SuccessWithData(c, host) 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) { func (b *BaseApi) TestByInfo(c *gin.Context) {
var req dto.HostConnTest var req dto.HostConnTest
if err := helper.CheckBindAndValidate(&req, c); err != nil { 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)) 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) { func (b *BaseApi) TestByID(c *gin.Context) {
var req dto.OperateByID var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil { 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)) 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) { func (b *BaseApi) HostTree(c *gin.Context) {
var req dto.SearchForTree var req dto.SearchForTree
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -53,6 +86,14 @@ func (b *BaseApi) HostTree(c *gin.Context) {
helper.SuccessWithData(c, data) 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) { func (b *BaseApi) SearchHost(c *gin.Context) {
var req dto.SearchPageWithGroup var req dto.SearchPageWithGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil { 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}) 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) { func (b *BaseApi) DeleteHost(c *gin.Context) {
var req dto.OperateByIDs var req dto.OperateByIDs
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -81,6 +131,15 @@ func (b *BaseApi) DeleteHost(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) UpdateHost(c *gin.Context) {
var req dto.HostOperate var req dto.HostOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -140,6 +199,15 @@ func (b *BaseApi) UpdateHost(c *gin.Context) {
helper.SuccessWithData(c, hostItem) 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) { func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
var req dto.ChangeGroup var req dto.ChangeGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -153,6 +221,14 @@ func (b *BaseApi) UpdateHostGroup(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) GetHostByID(c *gin.Context) {
var req dto.OperateByID var req dto.OperateByID
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+63 -16
View File
@@ -9,13 +9,13 @@ import (
// @Tags Host tool // @Tags Host tool
// @Summary Get tool status // @Summary Get tool status
// @Accept json // @Accept json
// @Param request body request.HostToolReq true "request" // @Param request body request.HostToolTypeReq true "request"
// @Success 200 {object} response.HostToolRes // @Success 200 {object} response.HostToolRes
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/tool [post] // @Router /hosts/tool/status [post]
func (b *BaseApi) GetToolStatus(c *gin.Context) { func (b *BaseApi) GetToolStatus(c *gin.Context) {
var req request.HostToolReq var req request.HostToolTypeReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
@@ -53,14 +53,14 @@ func (b *BaseApi) InitToolConfig(c *gin.Context) {
// @Tags Host tool // @Tags Host tool
// @Summary Operate tool // @Summary Operate tool
// @Accept json // @Accept json
// @Param request body request.HostToolReq true "request" // @Param request body request.HostToolOperateReq true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/tool/operate [post] // @Router /hosts/tool/operate [post]
// @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"} // @x-panel-log {"bodyKeys":["operate","type"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] [type] ","formatEN":"[operate] [type]"}
func (b *BaseApi) OperateTool(c *gin.Context) { func (b *BaseApi) OperateTool(c *gin.Context) {
var req request.HostToolReq var req request.HostToolOperateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
@@ -75,19 +75,18 @@ func (b *BaseApi) OperateTool(c *gin.Context) {
// @Tags Host tool // @Tags Host tool
// @Summary Get tool config // @Summary Get tool config
// @Accept json // @Accept json
// @Param request body request.HostToolConfig true "request" // @Param request body request.HostToolTypeReq true "request"
// @Success 200 {object} response.HostToolConfig // @Success 200 {object} response.HostToolConfig
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/tool/config [post] // @Router /hosts/tool/config/get [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] 主机工具配置文件 ","formatEN":"[operate] tool config"} func (b *BaseApi) GetToolConfig(c *gin.Context) {
func (b *BaseApi) OperateToolConfig(c *gin.Context) { var req request.HostToolTypeReq
var req request.HostToolConfig
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
config, err := hostToolService.OperateToolConfig(req) config, err := hostToolService.GetToolConfig(req)
if err != nil { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
@@ -95,6 +94,28 @@ func (b *BaseApi) OperateToolConfig(c *gin.Context) {
helper.SuccessWithData(c, config) 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 // @Tags Host tool
// @Summary Create Supervisor process // @Summary Create Supervisor process
// @Accept json // @Accept json
@@ -137,18 +158,44 @@ func (b *BaseApi) GetProcess(c *gin.Context) {
// @Tags Host tool // @Tags Host tool
// @Summary Get Supervisor process config file // @Summary Get Supervisor process config file
// @Accept json // @Accept json
// @Param request body request.SupervisorProcessFileReq true "request" // @Param request body request.HostSupervisorProcessFileGetReq true "request"
// @Success 200 {string} content // @Success 200 {string} content
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/tool/supervisor/process/file [post] // @Router /hosts/tool/supervisor/process/file/get [post]
// @x-panel-log {"bodyKeys":["operate"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"[operate] Supervisor 进程文件 ","formatEN":"[operate] Supervisor Process Config file"}
func (b *BaseApi) GetProcessFile(c *gin.Context) { func (b *BaseApi) GetProcessFile(c *gin.Context) {
var req request.SupervisorProcessFileReq var req request.HostSupervisorProcessFileGetReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return 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 { if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
+63
View File
@@ -23,6 +23,27 @@ func (b *BaseApi) PageMcpServers(c *gin.Context) {
helper.SuccessWithData(c, list) helper.SuccessWithData(c, list)
} }
// @Tags McpServer
// @Summary Load mcp server detail
// @Accept json
// @Param request body request.McpServerDetail true "request"
// @Success 200 {object} response.McpServerDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/detail [post]
func (b *BaseApi) LoadMcpServerDetail(c *gin.Context) {
var req request.McpServerDetail
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.Detail(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Create mcp server // @Summary Create mcp server
// @Accept json // @Accept json
@@ -107,6 +128,48 @@ func (b *BaseApi) OperateMcpServer(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags McpServer
// @Summary Sync mcp server status
// @Accept json
// @Param request body request.McpServerStatusSync true "request"
// @Success 200 {array} response.McpServerStatusDTO
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/status/sync [post]
func (b *BaseApi) SyncMcpServerStatus(c *gin.Context) {
var req request.McpServerStatusSync
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.SyncStatus(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer
// @Summary Test mcp server connection
// @Accept json
// @Param request body request.McpServerConnectionTest true "request"
// @Success 200 {object} response.McpServerConnectionTestRes
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/mcp/server/connection/test [post]
func (b *BaseApi) TestMcpServerConnection(c *gin.Context) {
var req request.McpServerConnectionTest
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := mcpServerService.TestConnection(req)
if err != nil {
helper.InternalServer(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags McpServer // @Tags McpServer
// @Summary Bind Domain for mcp server // @Summary Bind Domain for mcp server
// @Accept json // @Accept json
+13 -26
View File
@@ -31,27 +31,6 @@ func (b *BaseApi) LoadMonitor(c *gin.Context) {
helper.SuccessWithDataGzipped(c, data) 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 // @Tags Monitor
// @Summary Clean monitor data // @Summary Clean monitor data
// @Success 200 // @Success 200
@@ -91,7 +70,7 @@ func (b *BaseApi) LoadMonitorSetting(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /hosts/monitor/setting/update [post] // @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) { func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
var req dto.MonitorSettingUpdate var req dto.MonitorSettingUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -105,6 +84,12 @@ func (b *BaseApi) UpdateMonitorSetting(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
netStat, _ := net.IOCounters(true) netStat, _ := net.IOCounters(true)
var options []string var options []string
@@ -116,6 +101,12 @@ func (b *BaseApi) GetNetworkOptions(c *gin.Context) {
helper.SuccessWithData(c, options) 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) { func (b *BaseApi) GetIOOptions(c *gin.Context) {
diskStat, _ := disk.IOCounters() diskStat, _ := disk.IOCounters()
var options []string var options []string
@@ -126,7 +117,3 @@ func (b *BaseApi) GetIOOptions(c *gin.Context) {
sort.Strings(options) sort.Strings(options)
helper.SuccessWithData(c, 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" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket" websocket2 "github.com/1Panel-dev/1Panel/agent/utils/websocket"
"github.com/gin-gonic/gin" "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) { 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) ws, err := wsUpgrade.Upgrade(c.Writer, c.Request, nil)
if err != nil { if err != nil {
return return
+5 -4
View File
@@ -61,7 +61,7 @@ func (b *BaseApi) CreateRuntime(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /runtimes/del [post] // @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) { func (b *BaseApi) DeleteRuntime(c *gin.Context) {
var req request.RuntimeDelete var req request.RuntimeDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -81,6 +81,7 @@ func (b *BaseApi) DeleteRuntime(c *gin.Context) {
// @Success 200 {array} dto.AppResource // @Success 200 {array} dto.AppResource
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param id path integer true "id"
// @Router /runtimes/installed/delete/check/:id [get] // @Router /runtimes/installed/delete/check/:id [get]
func (b *BaseApi) DeleteRuntimeCheck(c *gin.Context) { func (b *BaseApi) DeleteRuntimeCheck(c *gin.Context) {
runTimeId, err := helper.GetIntParamByKey(c, "id") runTimeId, err := helper.GetIntParamByKey(c, "id")
@@ -120,7 +121,7 @@ func (b *BaseApi) UpdateRuntime(c *gin.Context) {
// @Tags Runtime // @Tags Runtime
// @Summary Get runtime // @Summary Get runtime
// @Accept json // @Accept json
// @Param id path string true "request" // @Param id path integer true "request"
// @Success 200 {object} response.RuntimeDTO // @Success 200 {object} response.RuntimeDTO
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
@@ -168,7 +169,7 @@ func (b *BaseApi) GetNodePackageRunScript(c *gin.Context) {
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /runtimes/operate [post] // @Router /runtimes/operate [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"操作运行环境 [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) { func (b *BaseApi) OperateRuntime(c *gin.Context) {
var req request.RuntimeOperate var req request.RuntimeOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -242,7 +243,7 @@ func (b *BaseApi) SyncStatus(c *gin.Context) {
// @Tags Runtime // @Tags Runtime
// @Summary Get php runtime extension // @Summary Get php runtime extension
// @Accept json // @Accept json
// @Param id path string true "request" // @Param id path integer true "request"
// @Success 200 {object} response.PHPExtensionRes // @Success 200 {object} response.PHPExtensionRes
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
+51 -30
View File
@@ -1,16 +1,12 @@
package v2 package v2
import ( import (
"encoding/json"
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/ssh" "github.com/1Panel-dev/1Panel/agent/utils/ssh"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/pkg/errors"
) )
// @Tags System Setting // @Tags System Setting
@@ -28,6 +24,12 @@ func (b *BaseApi) GetSettingInfo(c *gin.Context) {
helper.SuccessWithData(c, setting) 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) { func (b *BaseApi) GetTerminalAISettingInfo(c *gin.Context) {
setting, err := settingService.GetTerminalAIInfo() setting, err := settingService.GetTerminalAIInfo()
if err != nil { if err != nil {
@@ -50,14 +52,14 @@ func (b *BaseApi) GetSystemAvailable(c *gin.Context) {
// @Tags System Setting // @Tags System Setting
// @Summary Update system setting // @Summary Update system setting
// @Accept json // @Accept json
// @Param request body dto.SettingUpdate true "request" // @Param request body dto.AgentSettingUpdate true "request"
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /settings/update [post] // @Router /settings/update [post]
// @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统配置 [key] => [value]","formatEN":"update system setting [key] => [value]"} // @x-panel-log {"bodyKeys":["key","value"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"修改系统配置 [key] => [value]","formatEN":"update system setting [key] => [value]"}
func (b *BaseApi) UpdateSetting(c *gin.Context) { func (b *BaseApi) UpdateSetting(c *gin.Context) {
var req dto.SettingUpdate var req dto.AgentSettingUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
@@ -69,6 +71,15 @@ func (b *BaseApi) UpdateSetting(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
var req dto.TerminalAIInfo var req dto.TerminalAIInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -82,6 +93,12 @@ func (b *BaseApi) UpdateTerminalAISetting(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) GetFileManageAISettingInfo(c *gin.Context) {
setting, err := settingService.GetFileManageAIInfo() setting, err := settingService.GetFileManageAIInfo()
if err != nil { if err != nil {
@@ -91,6 +108,15 @@ func (b *BaseApi) GetFileManageAISettingInfo(c *gin.Context) {
helper.SuccessWithData(c, setting) 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) { func (b *BaseApi) UpdateFileManageAISetting(c *gin.Context) {
var req dto.FileManageAIInfo var req dto.FileManageAIInfo
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -138,6 +164,16 @@ func (b *BaseApi) UpdateFileHistorySetting(c *gin.Context) {
helper.Success(c) 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 // @Tags System Setting
// @Summary Load local backup dir // @Summary Load local backup dir
// @Success 200 {string} path // @Success 200 {string} path
@@ -158,6 +194,12 @@ func (b *BaseApi) LoadLocalConn(c *gin.Context) {
helper.SuccessWithData(c, settingService.GetLocalConn()) 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) { func (b *BaseApi) CheckLocalConn(c *gin.Context) {
client, err := loadLocalConn() client, err := loadLocalConn()
if err == nil && client != nil { if err == nil && client != nil {
@@ -173,7 +215,7 @@ func (b *BaseApi) CheckLocalConn(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @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]"} // @x-panel-log {"bodyKeys":["defaultConn"],"paramKeys":[],"BeforeFunctions":[],"formatZH":"本地终端默认连接 [defaultConn]","formatEN":"update system default conn [defaultConn]"}
func (b *BaseApi) SetDefaultIsConn(c *gin.Context) { func (b *BaseApi) SetDefaultIsConn(c *gin.Context) {
var req dto.SSHDefaultConn var req dto.SSHDefaultConn
@@ -223,12 +265,8 @@ func (b *BaseApi) SaveLocalConn(c *gin.Context) {
} }
func loadLocalConn() (*ssh.SSHClient, error) { func loadLocalConn() (*ssh.SSHClient, error) {
connInfoInDB := settingService.GetSettingByKey("LocalSSHConn") connInDB, err := settingService.GetLocalConnForSSH()
if len(connInfoInDB) == 0 { if err != nil {
return nil, errors.New("no such ssh conn info in db!")
}
var connInDB model.LocalConnInfo
if err := json.Unmarshal([]byte(connInfoInDB), &connInDB); err != nil {
return nil, err return nil, err
} }
sshInfo := ssh.ConnInfo{ sshInfo := ssh.ConnInfo{
@@ -243,23 +281,6 @@ func loadLocalConn() (*ssh.SSHClient, error) {
return ssh.NewClient(sshInfo) 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 // @Tags System Setting
// @Summary Save common description // @Summary Save common description
// @Accept json // @Accept json
+27
View File
@@ -3,6 +3,8 @@ package v2
import ( import (
"github.com/1Panel-dev/1Panel/agent/app/api/v2/helper" "github.com/1Panel-dev/1Panel/agent/app/api/v2/helper"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
"github.com/1Panel-dev/1Panel/agent/app/dto/response"
"github.com/gin-gonic/gin" "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 // @Tags TaskLog
// @Summary Get the number of executing tasks // @Summary Get the number of executing tasks
// @Success 200 {integer} int64 // @Success 200 {integer} int64
+44
View File
@@ -6,6 +6,14 @@ import (
"github.com/gin-gonic/gin" "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) { func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
var req request.TensorRTLLMSearch var req request.TensorRTLLMSearch
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -15,6 +23,15 @@ func (b *BaseApi) PageTensorRTLLMs(c *gin.Context) {
helper.SuccessWithData(c, list) 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) { func (b *BaseApi) CreateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMCreate var req request.TensorRTLLMCreate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -28,6 +45,15 @@ func (b *BaseApi) CreateTensorRTLLM(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) UpdateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMUpdate var req request.TensorRTLLMUpdate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -41,6 +67,15 @@ func (b *BaseApi) UpdateTensorRTLLM(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMDelete var req request.TensorRTLLMDelete
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -54,6 +89,15 @@ func (b *BaseApi) DeleteTensorRTLLM(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) OperateTensorRTLLM(c *gin.Context) {
var req request.TensorRTLLMOperate var req request.TensorRTLLMOperate
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+144 -105
View File
@@ -8,7 +8,9 @@ import (
"strconv" "strconv"
"time" "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/dto"
"github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/service" "github.com/1Panel-dev/1Panel/agent/app/service"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
@@ -19,117 +21,52 @@ import (
"github.com/pkg/errors" "github.com/pkg/errors"
) )
func (b *BaseApi) WsSSH(c *gin.Context) { // @Tags Terminal
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) // @Summary Ws local terminal
if err != nil { // @Param command query string false "command"
global.LOG.Errorf("gin context http handler failed, err: %v", err) // @Success 200
return // @Security ApiKeyAuth
} // @Security Timestamp
defer wsConn.Close() // @Router /hosts/terminal/local [get]
func (b *BaseApi) WsLocalTerminal(c *gin.Context) {
if global.CONF.Base.IsDemo { b.runSSHSession(c, loadLocalConn, c.DefaultQuery("command", ""))
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)
} }
func (b *BaseApi) ContainerWsSSH(c *gin.Context) { // @Tags Terminal
wsConn, err := upGrader.Upgrade(c.Writer, c.Request, nil) // @Summary Ws host SSH
if err != nil { // @Param id query integer false "id"
global.LOG.Errorf("gin context http handler failed, err: %v", err) // @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 return
} }
defer wsConn.Close() defer wsConn.Close()
if global.CONF.Base.IsDemo { slave, err := loadContainerTerminalCommand(c)
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...)
if wshandleError(wsConn, err) { if wshandleError(wsConn, err) {
return return
} }
@@ -147,10 +84,112 @@ func (b *BaseApi) ContainerWsSSH(c *gin.Context) {
<-quitChan <-quitChan
global.LOG.Info("websocket finished") 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) dt := time.Now().Add(time.Second)
_ = wsConn.WriteControl(websocket.CloseMessage, nil, dt) _ = 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) { func loadRedisInitCmd(c *gin.Context, redisType string) ([]string, error) {
name := c.Query("name") name := c.Query("name")
from := c.Query("from") from := c.Query("from")
+42 -9
View File
@@ -181,6 +181,7 @@ func (b *BaseApi) GetWebsite(c *gin.Context) {
// @Success 200 {object} response.FileInfo // @Success 200 {object} response.FileInfo
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param type path string true "type"
// @Router /websites/:id/config/:type [get] // @Router /websites/:id/config/:type [get]
func (b *BaseApi) GetWebsiteNginx(c *gin.Context) { func (b *BaseApi) GetWebsiteNginx(c *gin.Context) {
id, err := helper.GetParamID(c) id, err := helper.GetParamID(c)
@@ -269,6 +270,7 @@ func (b *BaseApi) GetHTTPSConfig(c *gin.Context) {
// @Success 200 {object} response.WebsiteHTTPS // @Success 200 {object} response.WebsiteHTTPS
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param id path string true "id"
// @Router /websites/:id/https [post] // @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"} // @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) { func (b *BaseApi) UpdateHTTPSConfig(c *gin.Context) {
@@ -330,25 +332,45 @@ func (b *BaseApi) UpdateWebsiteNginxConfig(c *gin.Context) {
} }
// @Tags Website // @Tags Website
// @Summary Operate website log // @Summary Get website log
// @Accept json // @Accept json
// @Param request body request.WebsiteLogReq true "request" // @Param request body request.WebsiteLogSearchReq true "request"
// @Success 200 {object} response.WebsiteLog // @Success 200 {object} response.WebsiteLog
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @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"} // @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) { func (b *BaseApi) OpWebsiteLog(c *gin.Context) {
var req request.WebsiteLogReq var req request.WebsiteLogReq
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
return return
} }
res, err := websiteService.OpWebsiteLog(req) if err := websiteService.OpWebsiteLog(req); err != nil {
if err != nil {
helper.InternalServer(c, err) helper.InternalServer(c, err)
return return
} }
helper.SuccessWithData(c, res) helper.Success(c)
} }
// @Tags Website // @Tags Website
@@ -799,6 +821,7 @@ func (b *BaseApi) GetDirConfig(c *gin.Context) {
// @Success 200 {object} response.WebsiteHtmlRes // @Success 200 {object} response.WebsiteHtmlRes
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Param type path string true "type"
// @Router /websites/default/html/:type [get] // @Router /websites/default/html/:type [get]
func (b *BaseApi) GetDefaultHtml(c *gin.Context) { func (b *BaseApi) GetDefaultHtml(c *gin.Context) {
resourceType, err := helper.GetStrParamByKey(c, "type") resourceType, err := helper.GetStrParamByKey(c, "type")
@@ -838,11 +861,11 @@ func (b *BaseApi) UpdateDefaultHtml(c *gin.Context) {
// @Tags Website // @Tags Website
// @Summary Get website upstreams // @Summary Get website upstreams
// @Accept json // @Accept json
// @Param request body request.WebsiteCommonReq true "request" // @Param id path integer true "id"
// @Success 200 {array} dto.NginxUpstream // @Success 200 {array} dto.NginxUpstream
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /websites/lbs [get] // @Router /websites/{id}/lbs [get]
func (b *BaseApi) GetLoadBalances(c *gin.Context) { func (b *BaseApi) GetLoadBalances(c *gin.Context) {
id, err := helper.GetParamID(c) id, err := helper.GetParamID(c)
if err != nil { if err != nil {
@@ -937,6 +960,15 @@ func (b *BaseApi) UpdateLoadBalanceFile(c *gin.Context) {
helper.Success(c) 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) { func (b *BaseApi) ChangeWebsiteGroup(c *gin.Context) {
var req dto.UpdateGroup var req dto.UpdateGroup
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
@@ -969,6 +1001,7 @@ func (b *BaseApi) UpdateProxyCache(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Website
// @Summary Get website proxy cache config // @Summary Get website proxy cache config
// @Accept json // @Accept json
// @Param id path int true "id" // @Param id path int true "id"
@@ -1295,7 +1328,7 @@ func (b *BaseApi) UpdateStreamConfig(c *gin.Context) {
// @Success 200 // @Success 200
// @Security ApiKeyAuth // @Security ApiKeyAuth
// @Security Timestamp // @Security Timestamp
// @Router /websites/batch/https [post] // @Router /websites/batch/ssl [post]
func (b *BaseApi) BatchSetHttps(c *gin.Context) { func (b *BaseApi) BatchSetHttps(c *gin.Context) {
var req request.BatchWebsiteHttps var req request.BatchWebsiteHttps
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+34
View File
@@ -210,6 +210,27 @@ func (b *BaseApi) UpdateWebsiteSSL(c *gin.Context) {
helper.Success(c) helper.Success(c)
} }
// @Tags Website SSL
// @Summary Push ssl to nodes
// @Accept json
// @Param request body request.WebsiteSSLPush true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /websites/ssl/push [post]
// @x-panel-log {"bodyKeys":["id"],"paramKeys":[],"BeforeFunctions":[{"input_column":"id","input_value":"id","isList":false,"db":"website_ssls","output_column":"primary_domain","output_value":"domain"}],"formatZH":"推送证书到节点 [domain]","formatEN":"Push ssl to nodes [domain]"}
func (b *BaseApi) PushWebsiteSSLToNode(c *gin.Context) {
var req request.WebsiteSSLPush
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := websiteSSLService.PushToNode(req); err != nil {
helper.InternalServer(c, err)
return
}
helper.Success(c)
}
// @Tags Website SSL // @Tags Website SSL
// @Summary Upload ssl // @Summary Upload ssl
// @Accept json // @Accept json
@@ -248,6 +269,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
var req request.WebsiteSSLFileUpload var req request.WebsiteSSLFileUpload
req.Description = c.PostForm("description") req.Description = c.PostForm("description")
req.Nodes = c.PostForm("nodes")
req.PushNode, _ = strconv.ParseBool(c.PostForm("pushNode"))
sslID := c.PostForm("sslID") sslID := c.PostForm("sslID")
if sslID != "" { if sslID != "" {
req.SSLID, _ = strconv.ParseUint(sslID, 10, 64) req.SSLID, _ = strconv.ParseUint(sslID, 10, 64)
@@ -283,6 +306,8 @@ func (b *BaseApi) UploadSSLFile(c *gin.Context) {
Certificate: string(certificateContent), Certificate: string(certificateContent),
Description: req.Description, Description: req.Description,
SSLID: uint(req.SSLID), SSLID: uint(req.SSLID),
PushNode: req.PushNode,
Nodes: req.Nodes,
} }
if err := websiteSSLService.Upload(uploadReq); err != nil { if err := websiteSSLService.Upload(uploadReq); err != nil {
@@ -333,6 +358,15 @@ func (b *BaseApi) DownloadWebsiteSSL(c *gin.Context) {
http.ServeContent(c.Writer, c.Request, info.Name(), info.ModTime(), file) 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) { func (b *BaseApi) ImportMasterSSL(c *gin.Context) {
var req model.WebsiteSSL var req model.WebsiteSSL
if err := helper.CheckBindAndValidate(&req, c); err != nil { if err := helper.CheckBindAndValidate(&req, c); err != nil {
+136 -48
View File
@@ -3,28 +3,110 @@ package dto
import "time" import "time"
type AgentCreateReq struct { type AgentCreateReq struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Remark string `json:"remark"` Remark string `json:"remark"`
AppVersion string `json:"appVersion" validate:"required"` AppVersion string `json:"appVersion" validate:"required"`
WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"` WebUIPort int `json:"webUIPort" validate:"required,min=1,max=65535"`
BridgePort int `json:"bridgePort"` BridgePort int `json:"bridgePort"`
AllowedOrigins []string `json:"allowedOrigins"` AllowedOrigins []string `json:"allowedOrigins"`
AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"` AgentType string `json:"agentType" validate:"required,oneof=openclaw copaw hermes-agent"`
Model string `json:"model"` Model string `json:"model"`
AccountID uint `json:"accountId"` AccountID uint `json:"accountId"`
Token string `json:"token"` Token string `json:"token"`
TaskID string `json:"taskID"` DashboardUsername string `json:"dashboardUsername"`
Advanced bool `json:"advanced"` DashboardPassword string `json:"dashboardPassword"`
ContainerName string `json:"containerName"` TaskID string `json:"taskID"`
AllowPort bool `json:"allowPort"` Advanced bool `json:"advanced"`
SpecifyIP string `json:"specifyIP"` ContainerName string `json:"containerName"`
RestartPolicy string `json:"restartPolicy"` AllowPort bool `json:"allowPort"`
CpuQuota float64 `json:"cpuQuota"` SpecifyIP string `json:"specifyIP"`
MemoryLimit float64 `json:"memoryLimit"` RestartPolicy string `json:"restartPolicy"`
MemoryUnit string `json:"memoryUnit"` CpuQuota float64 `json:"cpuQuota"`
PullImage bool `json:"pullImage"` MemoryLimit float64 `json:"memoryLimit"`
EditCompose bool `json:"editCompose"` MemoryUnit string `json:"memoryUnit"`
DockerCompose string `json:"dockerCompose"` PullImage bool `json:"pullImage"`
EditCompose bool `json:"editCompose"`
DockerCompose string `json:"dockerCompose"`
}
type AgentBatchInstallReq struct {
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"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
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 { type AgentItem struct {
@@ -41,6 +123,8 @@ type AgentItem struct {
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
Token string `json:"token"` Token string `json:"token"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
AppInstallID uint `json:"appInstallId"` AppInstallID uint `json:"appInstallId"`
@@ -274,19 +358,24 @@ type AgentAccountSearch struct {
Name string `json:"name"` Name string `json:"name"`
} }
type AgentAccountProviderCountReq struct {
Providers []string `json:"providers"`
}
type AgentAccountInfo struct { type AgentAccountInfo struct {
ID uint `json:"id"` ID uint `json:"id"`
Provider string `json:"provider"` MasterAccountID uint `json:"masterAccountId"`
ProviderName string `json:"providerName"` Provider string `json:"provider"`
Name string `json:"name"` ProviderName string `json:"providerName"`
APIKey string `json:"apiKey"` Name string `json:"name"`
RememberAPIKey bool `json:"rememberApiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` RememberAPIKey bool `json:"rememberApiKey"`
Models []AgentAccountModel `json:"models"` BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"` Models []AgentAccountModel `json:"models"`
Verified bool `json:"verified"` APIType string `json:"apiType"`
Remark string `json:"remark"` Verified bool `json:"verified"`
CreatedAt time.Time `json:"createdAt"` Remark string `json:"remark"`
CreatedAt time.Time `json:"createdAt"`
} }
type ProviderModelInfo struct { type ProviderModelInfo struct {
@@ -324,11 +413,6 @@ type AgentFeishuConfigUpdateReq struct {
Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"` Bots []AgentFeishuBot `json:"bots" validate:"required,min=1"`
} }
type AgentFeishuPairingApproveReq struct {
AgentID uint `json:"agentId" validate:"required"`
PairingCode string `json:"pairingCode" validate:"required"`
}
type AgentFeishuConfig struct { type AgentFeishuConfig struct {
Enabled bool `json:"enabled"` Enabled bool `json:"enabled"`
ThreadSession bool `json:"threadSession"` ThreadSession bool `json:"threadSession"`
@@ -577,16 +661,20 @@ type AgentSecurityConfig struct {
} }
type AgentOtherConfigUpdateReq struct { type AgentOtherConfigUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
UserTimezone string `json:"userTimezone" validate:"required"` UserTimezone string `json:"userTimezone" validate:"required"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry" validate:"required"` NPMRegistry string `json:"npmRegistry" validate:"required"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentOtherConfig struct { type AgentOtherConfig struct {
UserTimezone string `json:"userTimezone"` UserTimezone string `json:"userTimezone"`
BrowserEnabled bool `json:"browserEnabled"` BrowserEnabled bool `json:"browserEnabled"`
NPMRegistry string `json:"npmRegistry"` NPMRegistry string `json:"npmRegistry"`
DashboardUsername string `json:"dashboardUsername"`
DashboardPassword string `json:"dashboardPassword"`
} }
type AgentConfigFileReq struct { type AgentConfigFileReq struct {
@@ -604,7 +692,7 @@ type AgentConfigFile struct {
type AgentSkillSearchReq struct { type AgentSkillSearchReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"` Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh local-hub"`
Keyword string `json:"keyword" validate:"required"` Keyword string `json:"keyword" validate:"required"`
} }
@@ -641,7 +729,7 @@ type AgentSkillUpdateReq struct {
type AgentSkillInstallReq struct { type AgentSkillInstallReq struct {
AgentID uint `json:"agentId" validate:"required"` AgentID uint `json:"agentId" validate:"required"`
Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh"` Source string `json:"source" validate:"required,oneof=clawhub-global clawhub-cn skillhub official skills-sh local-hub"`
Slug string `json:"slug" validate:"required"` Slug string `json:"slug" validate:"required"`
TaskID string `json:"taskID" validate:"required"` TaskID string `json:"taskID" validate:"required"`
} }
+19 -6
View File
@@ -2,8 +2,9 @@ package dto
import ( import (
"encoding/json" "encoding/json"
"github.com/1Panel-dev/1Panel/agent/app/model"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/model"
) )
type CreateOrUpdateAlert struct { type CreateOrUpdateAlert struct {
@@ -35,6 +36,15 @@ type AlertSearch struct {
Method string `json:"method"` Method string `json:"method"`
} }
type AlertConfigQuery struct {
ExcludeTypes []string `json:"excludeTypes"`
}
type AlertConfigPageReq struct {
PageInfo
ExcludeTypes []string `json:"excludeTypes"`
}
type AlertDTO struct { type AlertDTO struct {
ID uint `json:"id"` ID uint `json:"id"`
Type string `json:"type"` Type string `json:"type"`
@@ -46,6 +56,8 @@ type AlertDTO struct {
Status string `json:"status"` Status string `json:"status"`
SendCount uint `json:"sendCount"` SendCount uint `json:"sendCount"`
AdvancedParams string `json:"advancedParams"` AdvancedParams string `json:"advancedParams"`
CreateUser string `json:"createUser"`
UpdateUser string `json:"updateUser"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
UpdatedAt time.Time `json:"updatedAt"` UpdatedAt time.Time `json:"updatedAt"`
} }
@@ -281,11 +293,12 @@ type OfflineQueryRequest struct {
} }
type AlertConfigUpdate struct { type AlertConfigUpdate struct {
ID uint `json:"id"` ID uint `json:"id"`
Type string `json:"type"` Type string `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Status string `json:"status"` Status string `json:"status"`
Config string `json:"config"` Config string `json:"config"`
DisplayName string `json:"displayName"`
} }
type AlertConfigTest struct { type AlertConfigTest struct {
+1
View File
@@ -132,6 +132,7 @@ type Locale struct {
Ko string `json:"ko"` Ko string `json:"ko"`
Tr string `json:"tr"` Tr string `json:"tr"`
Es string `json:"es-es" yaml:"es-es"` Es string `json:"es-es" yaml:"es-es"`
Fa string `json:"fa"`
} }
type AppForm struct { type AppForm struct {
+9 -8
View File
@@ -65,14 +65,15 @@ type UploadForRecover struct {
} }
type CommonBackup struct { type CommonBackup struct {
Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"` Type string `json:"type" validate:"required,oneof=app mysql mariadb redis website postgresql mongodb mysql-cluster postgresql-cluster redis-cluster container compose"`
Name string `json:"name"` Name string `json:"name"`
DetailName string `json:"detailName"` DetailName string `json:"detailName"`
Secret string `json:"secret"` Secret string `json:"secret"`
StopBefore bool `json:"stopBefore"` IsImmediate bool `json:"isImmediate"`
TaskID string `json:"taskID"` StopBefore bool `json:"stopBefore"`
FileName string `json:"fileName"` TaskID string `json:"taskID"`
Args []string `json:"args"` FileName string `json:"fileName"`
Args []string `json:"args"`
Description string `json:"description"` Description string `json:"description"`
} }
+1 -1
View File
@@ -319,7 +319,7 @@ type ComposeCreate struct {
type ComposeOperation struct { type ComposeOperation struct {
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
Path string `json:"path"` Path string `json:"path"`
Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete"` Operation string `json:"operation" validate:"required,oneof=up start restart stop down delete rebuild"`
WithFile bool `json:"withFile"` WithFile bool `json:"withFile"`
Force bool `json:"force"` Force bool `json:"force"`
} }
+10 -2
View File
@@ -79,8 +79,9 @@ type NodeCurrent struct {
} }
type DashboardCurrent struct { type DashboardCurrent struct {
Uptime uint64 `json:"uptime"` Uptime uint64 `json:"uptime"`
TimeSinceUptime string `json:"timeSinceUptime"` TimeSinceUptime string `json:"timeSinceUptime"`
RunningTime RunningTime `json:"runningTime"`
Procs uint64 `json:"procs"` Procs uint64 `json:"procs"`
@@ -128,6 +129,13 @@ type DashboardCurrent struct {
ShotTime time.Time `json:"shotTime"` ShotTime time.Time `json:"shotTime"`
} }
type RunningTime struct {
Days uint64 `json:"days"`
Hours uint64 `json:"hours"`
Minutes uint64 `json:"minutes"`
Seconds uint64 `json:"seconds"`
}
type AppLauncherSync struct { type AppLauncherSync struct {
Keys []string `json:"keys"` Keys []string `json:"keys"`
} }
+1
View File
@@ -99,6 +99,7 @@ type AppInstallUpgrade struct {
DetailID uint `json:"detailId"` DetailID uint `json:"detailId"`
Backup bool `json:"backup"` Backup bool `json:"backup"`
PullImage bool `json:"pullImage"` PullImage bool `json:"pullImage"`
DeleteImage bool `json:"deleteImage"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
} }
+14 -1
View File
@@ -90,6 +90,11 @@ type FileDeCompress struct {
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
Secret string `json:"secret"` Secret string `json:"secret"`
TaskID string `json:"taskID"`
}
type FileDeCompressStopReq struct {
TaskID string `json:"taskID" validate:"required"`
} }
type FileEdit struct { type FileEdit struct {
@@ -116,6 +121,7 @@ type FileWget struct {
Path string `json:"path" validate:"required"` Path string `json:"path" validate:"required"`
Name string `json:"name" validate:"required"` Name string `json:"name" validate:"required"`
IgnoreCertificate bool `json:"ignoreCertificate"` IgnoreCertificate bool `json:"ignoreCertificate"`
UseProxy bool `json:"useProxy"`
} }
type FileMove struct { type FileMove struct {
@@ -157,13 +163,20 @@ type FileRoleUpdate struct {
type FileReadByLineReq struct { type FileReadByLineReq struct {
Page int `json:"page" validate:"required"` Page int `json:"page" validate:"required"`
PageSize int `json:"pageSize" validate:"required"` PageSize int `json:"pageSize" validate:"required"`
Type string `json:"type" validate:"required"` Type string `json:"type"`
ID uint `json:"ID"` ID uint `json:"ID"`
Name string `json:"name"` Name string `json:"name"`
Latest bool `json:"latest"` Latest bool `json:"latest"`
TaskReq 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 { type TaskReq struct {
TaskID string `json:"taskID"` TaskID string `json:"taskID"`
TaskType string `json:"taskType"` TaskType string `json:"taskType"`
+19 -8
View File
@@ -1,8 +1,12 @@
package request 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"` 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 { type HostToolCreate struct {
@@ -15,13 +19,8 @@ type SupervisorConfig struct {
ServiceName string `json:"serviceName"` ServiceName string `json:"serviceName"`
} }
type HostToolLogReq struct { type HostToolConfigUpdate struct {
Type string `json:"type" validate:"required,oneof=supervisord"`
}
type HostToolConfig struct {
Type string `json:"type" validate:"required,oneof=supervisord"` Type string `json:"type" validate:"required,oneof=supervisord"`
Operate string `json:"operate" validate:"oneof=get set"`
Content string `json:"content"` Content string `json:"content"`
} }
@@ -43,3 +42,15 @@ type SupervisorProcessFileReq struct {
Content string `json:"content"` Content string `json:"content"`
File string `json:"file" validate:"required,oneof=out.log err.log config"` 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"`
}
+15
View File
@@ -21,6 +21,9 @@ type McpServerCreate struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport" validate:"required"` OutputTransport string `json:"outputTransport" validate:"required"`
Type string `json:"type" validate:"required"` Type string `json:"type" validate:"required"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
TaskID string `json:"taskID"`
} }
type McpServerUpdate struct { type McpServerUpdate struct {
@@ -32,11 +35,23 @@ type McpServerDelete struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
} }
type McpServerDetail struct {
ID uint `json:"id" validate:"required"`
}
type McpServerStatusSync struct {
IDs []uint `json:"ids"`
}
type McpServerOperate struct { type McpServerOperate struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` Operate string `json:"operate" validate:"required"`
} }
type McpServerConnectionTest struct {
ID uint `json:"id" validate:"required"`
}
type McpBindDomain struct { type McpBindDomain struct {
Domain string `json:"domain" validate:"required"` Domain string `json:"domain" validate:"required"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
+8 -3
View File
@@ -21,13 +21,14 @@ type RuntimeCreate struct {
Source string `json:"source"` Source string `json:"source"`
CodeDir string `json:"codeDir"` CodeDir string `json:"codeDir"`
Remark string `json:"remark"` Remark string `json:"remark"`
TaskID string `json:"taskID"`
Params map[string]interface{} `json:"params"` Params map[string]interface{} `json:"params"`
NodeConfig NodeConfig
} }
type NodeConfig struct { type NodeConfig struct {
Install bool `json:"install"` Install *bool `json:"install"`
Clean bool `json:"clean"` Clean bool `json:"clean"`
ExposedPorts []ExposedPort `json:"exposedPorts"` ExposedPorts []ExposedPort `json:"exposedPorts"`
Environments []Environment `json:"environments"` Environments []Environment `json:"environments"`
@@ -42,12 +43,14 @@ type Environment struct {
type Volume struct { type Volume struct {
Source string `json:"source"` Source string `json:"source"`
Target string `json:"target"` Target string `json:"target"`
Mode string `json:"mode"`
} }
type ExposedPort struct { type ExposedPort struct {
HostPort int `json:"hostPort"` HostPort int `json:"hostPort"`
ContainerPort int `json:"containerPort"` ContainerPort int `json:"containerPort"`
HostIP string `json:"hostIP"` HostIP string `json:"hostIP"`
Protocol string `json:"protocol"`
} }
type ExtraHost struct { type ExtraHost struct {
@@ -56,8 +59,10 @@ type ExtraHost struct {
} }
type RuntimeDelete struct { type RuntimeDelete struct {
ID uint `json:"id"` ID uint `json:"id"`
ForceDelete bool `json:"forceDelete"` ForceDelete bool `json:"forceDelete"`
DeleteImage bool `json:"deleteImage"`
TaskID string `json:"taskID"`
} }
type RuntimeUpdate struct { type RuntimeUpdate struct {
+7 -2
View File
@@ -223,9 +223,14 @@ type WebsiteNginxUpdate struct {
} }
type WebsiteLogReq 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"` ID uint `json:"id" validate:"required"`
Operate string `json:"operate" validate:"required"` LogType string `json:"logType" validate:"required,oneof=access.log error.log"`
LogType string `json:"logType" validate:"required"`
Page int `json:"page"` Page int `json:"page"`
PageSize int `json:"pageSize"` PageSize int `json:"pageSize"`
} }
+18 -4
View File
@@ -6,7 +6,7 @@ type WebsiteSSLSearch struct {
dto.PageInfo dto.PageInfo
AcmeAccountID string `json:"acmeAccountID"` AcmeAccountID string `json:"acmeAccountID"`
Domain string `json:"domain"` 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"` Order string `json:"order" validate:"omitempty,oneof=null ascending descending"`
} }
@@ -61,7 +61,7 @@ type WebsiteSSLObtain struct {
type WebsiteAcmeAccountCreate struct { type WebsiteAcmeAccountCreate struct {
Email string `json:"email" validate:"required"` Email string `json:"email" validate:"required"`
Type string `json:"type" validate:"required,oneof=letsencrypt zerossl buypass google custom"` 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"` EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"` EabHmacKey string `json:"eabHmacKey"`
UseProxy bool `json:"useProxy"` UseProxy bool `json:"useProxy"`
@@ -126,6 +126,16 @@ type WebsiteSSLUpload struct {
Type string `json:"type" validate:"required,oneof=paste local"` Type string `json:"type" validate:"required,oneof=paste local"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
Description string `json:"description"` Description string `json:"description"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
}
type WebsiteSSLPush struct {
ID uint `json:"id" validate:"required"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
TaskID string `json:"taskID" validate:"required"`
Sync bool `json:"sync"`
} }
type WebsiteCASearch struct { type WebsiteCASearch struct {
@@ -138,7 +148,7 @@ type WebsiteCACreate struct {
Organization string `json:"organization" validate:"required"` Organization string `json:"organization" validate:"required"`
OrganizationUint string `json:"organizationUint"` OrganizationUint string `json:"organizationUint"`
Name string `json:"name" validate:"required"` 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" ` Province string `json:"province" `
City string `json:"city"` City string `json:"city"`
} }
@@ -146,7 +156,7 @@ type WebsiteCACreate struct {
type WebsiteCAObtain struct { type WebsiteCAObtain struct {
ID uint `json:"id" validate:"required"` ID uint `json:"id" validate:"required"`
Domains string `json:"domains" 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"` Time int `json:"time" validate:"required"`
Unit string `json:"unit" validate:"required"` Unit string `json:"unit" validate:"required"`
PushDir bool `json:"pushDir"` PushDir bool `json:"pushDir"`
@@ -157,6 +167,8 @@ type WebsiteCAObtain struct {
Description string `json:"description"` Description string `json:"description"`
ExecShell bool `json:"execShell"` ExecShell bool `json:"execShell"`
Shell string `json:"shell"` Shell string `json:"shell"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
type WebsiteCARenew struct { type WebsiteCARenew struct {
@@ -167,4 +179,6 @@ type WebsiteSSLFileUpload struct {
Type string `json:"type"` Type string `json:"type"`
Description string `json:"description"` Description string `json:"description"`
SSLID uint64 `json:"sslID"` SSLID uint64 `json:"sslID"`
PushNode bool `json:"pushNode"`
Nodes string `json:"nodes"`
} }
+30 -28
View File
@@ -104,34 +104,36 @@ type AppDetail struct {
} }
type AppInstallDTO struct { type AppInstallDTO struct {
ID uint `json:"id"` ID uint `json:"id"`
Name string `json:"name"` Name string `json:"name"`
AppID uint `json:"appID"` AppID uint `json:"appID"`
AppDetailID uint `json:"appDetailID"` AppDetailID uint `json:"appDetailID"`
Version string `json:"version"` Version string `json:"version"`
Status string `json:"status"` Status string `json:"status"`
Message string `json:"message"` Message string `json:"message"`
HttpPort int `json:"httpPort"` HttpPort int `json:"httpPort"`
HttpsPort int `json:"httpsPort"` HttpsPort int `json:"httpsPort"`
Path string `json:"path"` Path string `json:"path"`
CanUpdate bool `json:"canUpdate"` CanUpdate bool `json:"canUpdate"`
Icon string `json:"icon"` Icon string `json:"icon"`
AppName string `json:"appName"` AppName string `json:"appName"`
Ready int `json:"ready"` Ready int `json:"ready"`
Total int `json:"total"` Total int `json:"total"`
AppKey string `json:"appKey"` AppKey string `json:"appKey"`
AppType string `json:"appType"` AppType string `json:"appType"`
AppStatus string `json:"appStatus"` AppStatus string `json:"appStatus"`
DockerCompose string `json:"dockerCompose"` DockerCompose string `json:"dockerCompose"`
WebUI string `json:"webUI"` WebUI string `json:"webUI"`
CreatedAt time.Time `json:"createdAt"` CreatedAt time.Time `json:"createdAt"`
Favorite bool `json:"favorite"` Favorite bool `json:"favorite"`
SortOrder int `json:"sortOrder"` SortOrder int `json:"sortOrder"`
App AppDetail `json:"app"` App AppDetail `json:"app"`
Container string `json:"container"` Container string `json:"container"`
IsEdit bool `json:"isEdit"` IsEdit bool `json:"isEdit"`
LinkDB bool `json:"linkDB"` LinkDB bool `json:"linkDB"`
ServiceName string `json:"serviceName"` ResourceKeys []string `json:"resourceKeys"`
ServiceName string `json:"serviceName"`
Env map[string]interface{} `json:"env"`
} }
type AppInstallInfo struct { type AppInstallInfo struct {
+14
View File
@@ -16,6 +16,12 @@ type McpServerDTO struct {
Volumes []request.Volume `json:"volumes"` Volumes []request.Volume `json:"volumes"`
} }
type McpServerStatusDTO struct {
ID uint `json:"id"`
Status string `json:"status"`
Message string `json:"message"`
}
type McpBindDomainRes struct { type McpBindDomainRes struct {
Domain string `json:"domain"` Domain string `json:"domain"`
SSLID uint `json:"sslID"` SSLID uint `json:"sslID"`
@@ -24,3 +30,11 @@ type McpBindDomainRes struct {
WebsiteID uint `json:"websiteID"` WebsiteID uint `json:"websiteID"`
ConnUrl string `json:"connUrl"` ConnUrl string `json:"connUrl"`
} }
type McpServerConnectionTestRes struct {
Success bool `json:"success"`
Endpoint string `json:"endpoint"`
OutputTransport string `json:"outputTransport"`
ProtocolVersion string `json:"protocolVersion,omitempty"`
Message string `json:"message"`
}
+9 -1
View File
@@ -23,7 +23,10 @@ type SettingInfo struct {
AppStoreLastModified string `json:"appStoreLastModified"` AppStoreLastModified string `json:"appStoreLastModified"`
AppStoreSyncStatus string `json:"appStoreSyncStatus"` AppStoreSyncStatus string `json:"appStoreSyncStatus"`
FileRecycleBin string `json:"fileRecycleBin"` FileRecycleBin string `json:"fileRecycleBin"`
LocalSSHConnShow string `json:"localSSHConnShow"`
FirewallPortWhiteList string `json:"firewallPortWhiteList"`
} }
type SettingUpdate struct { type SettingUpdate struct {
@@ -31,6 +34,11 @@ type SettingUpdate struct {
Value string `json:"value"` 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 { type SyncTime struct {
NtpSite string `json:"ntpSite" validate:"required"` NtpSite string `json:"ntpSite" validate:"required"`
} }
+4 -2
View File
@@ -57,8 +57,10 @@ type SSHConfUpdate struct {
} }
type SearchSSHLog struct { type SearchSSHLog struct {
PageInfo PageInfo
Info string `json:"info"` Info string `json:"info"`
Status string `json:"Status" validate:"required,oneof=Success Failed All"` Status string `json:"Status" validate:"required,oneof=Success Failed All"`
StartTime time.Time `json:"startTime"`
EndTime time.Time `json:"endTime"`
} }
type SSHHistory struct { type SSHHistory struct {
+9 -8
View File
@@ -2,14 +2,15 @@ package model
type AgentAccount struct { type AgentAccount struct {
BaseModel BaseModel
Provider string `json:"provider"` Provider string `json:"provider"`
Name string `json:"name"` Name string `json:"name"`
APIKey string `json:"apiKey"` APIKey string `json:"apiKey"`
BaseURL string `json:"baseUrl"` BaseURL string `json:"baseUrl"`
APIType string `json:"apiType"` APIType string `json:"apiType"`
RememberAPIKey bool `json:"rememberApiKey"` RememberAPIKey bool `json:"rememberApiKey"`
Verified bool `json:"verified"` Verified bool `json:"verified"`
Remark string `json:"remark"` Remark string `json:"remark"`
MasterAccountID uint `json:"masterAccountId" gorm:"index"`
} }
func (AgentAccount) TableName() string { func (AgentAccount) TableName() string {
+11 -7
View File
@@ -9,9 +9,11 @@ type Alert struct {
Count uint `gorm:"type:integer;not null" json:"count"` Count uint `gorm:"type:integer;not null" json:"count"`
Project string `gorm:"type:varchar(64)" json:"project"` Project string `gorm:"type:varchar(64)" json:"project"`
Status string `gorm:"type:varchar(64);not null" json:"status"` 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"` SendCount uint `gorm:"type:integer" json:"sendCount"`
AdvancedParams string `gorm:"type:longText" json:"advancedParams"` 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 { type AlertTask struct {
@@ -19,7 +21,7 @@ type AlertTask struct {
Type string `gorm:"type:varchar(64);not null" json:"type"` Type string `gorm:"type:varchar(64);not null" json:"type"`
Quota string `gorm:"type:varchar(64)" json:"quota"` Quota string `gorm:"type:varchar(64)" json:"quota"`
QuotaType string `gorm:"type:varchar(64)" json:"quotaType"` QuotaType string `gorm:"type:varchar(64)" json:"quotaType"`
Method string `gorm:"type:varchar(64);not null;default:'sms'" json:"method"` Method string `gorm:"type:varchar(128);not null;default:'sms'" json:"method"`
} }
type AlertLog struct { type AlertLog struct {
@@ -34,15 +36,17 @@ type AlertLog struct {
Message string `gorm:"type:varchar(256);" json:"message"` Message string `gorm:"type:varchar(256);" json:"message"`
RecordId uint `gorm:"type:integer;" json:"recordId"` RecordId uint `gorm:"type:integer;" json:"recordId"`
LicenseId string `gorm:"type:varchar(256);not null;" json:"licenseId" ` 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 { type AlertConfig struct {
BaseModel BaseModel
Type string `gorm:"type:varchar(64);not null" json:"type"` Type string `gorm:"type:varchar(64);not null" json:"type"`
Title string `gorm:"type:varchar(64);not null" json:"title"` Title string `gorm:"type:varchar(64);not null" json:"title"`
Status string `gorm:"type:varchar(64);not null" json:"status"` Status string `gorm:"type:varchar(64);not null" json:"status"`
Config string `gorm:"type:varchar(256);not null" json:"config"` Config string `gorm:"type: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 { type LoginLog struct {
+2
View File
@@ -18,4 +18,6 @@ type McpServer struct {
StreamableHttpPath string `json:"streamableHttpPath"` StreamableHttpPath string `json:"streamableHttpPath"`
OutputTransport string `json:"outputTransport"` OutputTransport string `json:"outputTransport"`
Type string `json:"type"` Type string `json:"type"`
GatewayImage string `json:"gatewayImage"`
ProtocolVersion string `json:"protocolVersion"`
} }
+1 -1
View File
@@ -8,7 +8,7 @@ type WebsiteAcmeAccount struct {
Type string `gorm:"not null;default:letsencrypt" json:"type"` Type string `gorm:"not null;default:letsencrypt" json:"type"`
EabKid string `json:"eabKid"` EabKid string `json:"eabKid"`
EabHmacKey string `json:"eabHmacKey"` 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"` UseProxy bool `gorm:"default:false" json:"useProxy"`
CaDirURL string `json:"caDirURL"` CaDirURL string `json:"caDirURL"`
UseEAB bool `json:"useEAB"` UseEAB bool `json:"useEAB"`
+1 -1
View File
@@ -5,5 +5,5 @@ type WebsiteCA struct {
CSR string `gorm:"not null;" json:"csr"` CSR string `gorm:"not null;" json:"csr"`
Name string `gorm:"not null;" json:"name"` Name string `gorm:"not null;" json:"name"`
PrivateKey string `gorm:"not null" json:"privateKey"` 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"`
} }
+3 -4
View File
@@ -83,8 +83,8 @@ var catalog = map[string]Meta{
Input: []string{"text"}, Input: []string{"text"},
}, },
Models: []Model{ Models: []Model{
{ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"}, {ID: "deepseek/deepseek-v4-flash", Name: "deepseek-v4-flash"},
{ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner", MaxTokens: 65536, ContextWindow: 131072, Reasoning: true}, {ID: "deepseek/deepseek-v4-pro", Name: "deepseek-v4-pro"},
}, },
}, },
"bailian-coding-plan": { "bailian-coding-plan": {
@@ -163,10 +163,9 @@ var catalog = map[string]Meta{
Input: []string{"text"}, Input: []string{"text"},
}, },
Models: []Model{ Models: []Model{
{ID: "minimax/MiniMax-M3", Name: "MiniMax M3", ContextWindow: 1000000, MaxTokens: 128000, Reasoning: true, Input: []string{"text", "image"}},
{ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"},
{ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}, {ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"},
{ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true},
{ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"},
}, },
}, },
"xiaomi": { "xiaomi": {
+1 -1
View File
@@ -131,7 +131,7 @@ func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest {
request.URL = base + "/v1/messages" request.URL = base + "/v1/messages"
} }
request.Body = mustJSON(map[string]interface{}{ request.Body = mustJSON(map[string]interface{}{
"model": "MiniMax-M2.5", "model": "MiniMax-M3",
"max_tokens": 1, "max_tokens": 1,
"messages": []map[string]interface{}{{ "messages": []map[string]interface{}{{
"role": "user", "role": "user",
+60 -1
View File
@@ -1,16 +1,23 @@
package repo 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 AgentAccountRepo struct{}
type IAgentAccountRepo interface { type IAgentAccountRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error) Page(page, size int, opts ...DBOption) (int64, []model.AgentAccount, error)
GetFirst(opts ...DBOption) (*model.AgentAccount, error) GetFirst(opts ...DBOption) (*model.AgentAccount, error)
WithByMasterAccountID(masterID uint) DBOption
Create(account *model.AgentAccount) error Create(account *model.AgentAccount) error
Save(account *model.AgentAccount) error Save(account *model.AgentAccount) error
DeleteByID(id uint) error DeleteByID(id uint) error
List(opts ...DBOption) ([]model.AgentAccount, error) List(opts ...DBOption) ([]model.AgentAccount, error)
CountByProviders(providers []string) (map[string]int64, error)
} }
func NewIAgentAccountRepo() IAgentAccountRepo { func NewIAgentAccountRepo() IAgentAccountRepo {
@@ -34,6 +41,12 @@ func (a AgentAccountRepo) GetFirst(opts ...DBOption) (*model.AgentAccount, error
return &account, nil 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 { func (a AgentAccountRepo) Create(account *model.AgentAccount) error {
return getDb().Create(account).Error return getDb().Create(account).Error
} }
@@ -53,3 +66,49 @@ func (a AgentAccountRepo) List(opts ...DBOption) ([]model.AgentAccount, error) {
} }
return accounts, nil return accounts, nil
} }
func (a AgentAccountRepo) CountByProviders(providers []string) (map[string]int64, error) {
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 package repo
import ( import (
"encoding/json"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"google.golang.org/genproto/googleapis/type/date" "google.golang.org/genproto/googleapis/type/date"
"gorm.io/gorm" "gorm.io/gorm"
"strconv"
"time" "time"
) )
@@ -20,7 +24,8 @@ type IAlertRepo interface {
WithByCreateAt(date *date.Date) DBOption WithByCreateAt(date *date.Date) DBOption
WithByLicenseId(licenseId string) DBOption WithByLicenseId(licenseId string) DBOption
WithByRecordId(recordId uint) DBOption WithByRecordId(recordId uint) DBOption
WithByMethod(method string) DBOption WithByAlertMethodContainsConfigID(id uint) DBOption
WithByMethodConfigIDs(ids []uint) DBOption
Create(alert *model.Alert) error Create(alert *model.Alert) error
Get(opts ...DBOption) (model.Alert, error) Get(opts ...DBOption) (model.Alert, error)
@@ -47,11 +52,15 @@ type IAlertRepo interface {
GetLicensePushCount(method string) (uint, error) GetLicensePushCount(method string) (uint, error)
GetConfig(opts ...DBOption) (model.AlertConfig, error) GetConfig(opts ...DBOption) (model.AlertConfig, error)
GetConfigById(id uint) (model.AlertConfig, error)
AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error) AlertConfigList(opts ...DBOption) ([]model.AlertConfig, error)
UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error UpdateAlertConfig(maps map[string]interface{}, opts ...DBOption) error
CreateAlertConfig(config *model.AlertConfig) error CreateAlertConfig(config *model.AlertConfig) error
DeleteAlertConfig(opts ...DBOption) error DeleteAlertConfig(opts ...DBOption) error
WithByTypeNotIn(types []string) DBOption
PageAlertConfig(page, size int, opts ...DBOption) (int64, []model.AlertConfig, error)
SyncAll(data []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 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 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 { func (a *AlertRepo) SyncAll(data []model.AlertConfig) error {
tx := global.AlertDB.Begin() 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 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) 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 { 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 { for _, item := range data {
if val, ok := oldConfigMap[item.Type]; ok { if singletonTypes[item.Type] {
item.ID = val if val, ok := oldConfigMap[item.Type]; ok {
delete(oldConfigMap, item.Type) item.ID = val
} else { delete(oldConfigMap, item.Type)
item.ID = 0 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() tx.Rollback()
return err return err
} }
} }
for _, val := range oldConfigMap { for _, item := range oldConfigs {
if err := tx.Where("id = ?", val).Delete(&model.AlertConfig{}).Error; err != nil { 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() tx.Rollback()
return err return err
} }
} }
tx.Commit() if err := tx.Commit().Error; err != nil {
tx.Rollback()
return err
}
return nil 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
}
}
}
+14
View File
@@ -17,6 +17,7 @@ type IWebsiteRepo interface {
WithGroupID(groupId uint) DBOption WithGroupID(groupId uint) DBOption
WithDefaultServer() DBOption WithDefaultServer() DBOption
WithDomainLike(domain string) DBOption WithDomainLike(domain string) DBOption
WithSearchKeyword(keyword string, ids []uint) DBOption
WithRuntimeID(runtimeID uint) DBOption WithRuntimeID(runtimeID uint) DBOption
WithParentID(websiteID uint) DBOption WithParentID(websiteID uint) DBOption
WithType(websiteType string) DBOption WithType(websiteType string) DBOption
@@ -76,6 +77,19 @@ func (w *WebsiteRepo) WithDomainLike(domain string) DBOption {
} }
} }
func (w *WebsiteRepo) WithSearchKeyword(keyword string, ids []uint) DBOption {
return func(db *gorm.DB) *gorm.DB {
if keyword == "" {
return db
}
keyword = "%" + keyword + "%"
if len(ids) == 0 {
return db.Where("(primary_domain like ? OR alias like ?)", keyword, keyword)
}
return db.Where("(primary_domain like ? OR alias like ? OR id in (?))", keyword, keyword, ids)
}
}
func (w *WebsiteRepo) WithAlias(alias string) DBOption { func (w *WebsiteRepo) WithAlias(alias string) DBOption {
return func(db *gorm.DB) *gorm.DB { return func(db *gorm.DB) *gorm.DB {
return db.Where("alias = ?", alias) return db.Where("alias = ?", alias)
+20 -6
View File
@@ -2,8 +2,10 @@ package repo
import ( import (
"context" "context"
"time"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/constant"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -21,6 +23,7 @@ type ISSLRepo interface {
Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error) Page(page, size int, opts ...DBOption) (int64, []model.WebsiteSSL, error)
GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) GetFirst(opts ...DBOption) (*model.WebsiteSSL, error)
List(opts ...DBOption) ([]model.WebsiteSSL, error) List(opts ...DBOption) ([]model.WebsiteSSL, error)
TryMarkApplying(id uint) (bool, error)
Create(ctx context.Context, ssl *model.WebsiteSSL) error Create(ctx context.Context, ssl *model.WebsiteSSL) error
Save(ssl *model.WebsiteSSL) error Save(ssl *model.WebsiteSSL) error
DeleteBy(opts ...DBOption) 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) { func (w WebsiteSSLRepo) GetFirst(opts ...DBOption) (*model.WebsiteSSL, error) {
var website *model.WebsiteSSL var website model.WebsiteSSL
db := getDb(opts...).Model(&model.WebsiteSSL{}) db := getDb(opts...).Model(&model.WebsiteSSL{})
if err := db.Preload("AcmeAccount").Preload("DnsAccount").First(&website).Error; err != nil { 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) { 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 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 { func (w WebsiteSSLRepo) Create(ctx context.Context, ssl *model.WebsiteSSL) error {
return getTx(ctx).Create(ssl).Error return getTx(ctx).Create(ssl).Error
} }
func (w WebsiteSSLRepo) Save(ssl *model.WebsiteSSL) error { func (w WebsiteSSLRepo) Save(ssl *model.WebsiteSSL) error {
return getDb().Model(&model.WebsiteSSL{BaseModel: model.BaseModel{ return getDb().Save(ssl).Error
ID: ssl.ID,
}}).Save(&ssl).Error
} }
func (w WebsiteSSLRepo) SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error { func (w WebsiteSSLRepo) SaveByMap(ssl *model.WebsiteSSL, params map[string]interface{}) error {
+490 -20
View File
@@ -3,6 +3,7 @@ package service
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"os" "os"
"path" "path"
@@ -16,6 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/app/task"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
@@ -30,6 +32,10 @@ import (
type IAgentService interface { type IAgentService interface {
Create(req dto.AgentCreateReq) (*dto.AgentItem, error) Create(req dto.AgentCreateReq) (*dto.AgentItem, error)
BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error)
BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error)
BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error)
BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error)
Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error)
DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error) DeleteCheck(req dto.AgentIDReq) ([]dto.AppResource, error)
Delete(req dto.AgentDeleteReq) error Delete(req dto.AgentDeleteReq) error
@@ -69,6 +75,7 @@ type IAgentService interface {
UpdateAccount(req dto.AgentAccountUpdateReq) error UpdateAccount(req dto.AgentAccountUpdateReq) error
SyncAgentsByAccount(account *model.AgentAccount) error SyncAgentsByAccount(account *model.AgentAccount) error
PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.AgentAccountInfo, error)
CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error)
GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error)
CreateAccountModel(req dto.AgentAccountModelCreateReq) error CreateAccountModel(req dto.AgentAccountModelCreateReq) error
UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error UpdateAccountModel(req dto.AgentAccountModelUpdateReq) error
@@ -98,6 +105,16 @@ type IAgentService interface {
ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error
} }
type batchUpgradePlan struct {
ctx batchAgentInstallContext
req request.AppInstallUpgrade
}
type batchAgentInstallContext struct {
agent model.Agent
install model.AppInstall
}
const ( const (
defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser" defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/openclaw-browser"
defaultBrowserProfile = "openclaw" defaultBrowserProfile = "openclaw"
@@ -127,7 +144,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 { if installs, _ := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(req.Name)); len(installs) > 0 {
return nil, buserr.New("ErrNameIsExist") return nil, buserr.New("ErrNameIsExist")
} }
if !xpack.IsXpack() { if !xpack.MultiNodeProvider.IsXpack() {
count, _, err := agentRepo.Page(1, 1) count, _, err := agentRepo.Page(1, 1)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -159,6 +176,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
var allowedOrigins []string var allowedOrigins []string
var account *model.AgentAccount var account *model.AgentAccount
var installHooks *appInstallHooks var installHooks *appInstallHooks
var hermesAuth hermesDashboardAuth
if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent { if agentType == constant.AppOpenclaw || agentType == constant.AppHermesAgent {
if req.AccountID == 0 { if req.AccountID == 0 {
@@ -205,9 +223,13 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
}, },
} }
} else if agentType == constant.AppHermesAgent { } else if agentType == constant.AppHermesAgent {
hermesAuth = normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
installHooks = &appInstallHooks{ installHooks = &appInstallHooks{
AfterCopyData: func(appInstall *model.AppInstall) error { AfterCopyData: func(appInstall *model.AppInstall) error {
return prepareHermesInstallFiles(appInstall, account, storedModel) if err := prepareHermesInstallFiles(appInstall, account, storedModel); err != nil {
return err
}
return writeHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"), hermesAuth, false)
}, },
} }
} }
@@ -231,6 +253,10 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
params["API_KEY"] = apiKey params["API_KEY"] = apiKey
params["OPENCLAW_GATEWAY_TOKEN"] = token params["OPENCLAW_GATEWAY_TOKEN"] = token
} }
if agentType == constant.AppHermesAgent {
params[hermesDashboardUsernameEnvKey] = hermesAuth.Username
params[hermesDashboardPasswordEnvKey] = hermesAuth.Password
}
if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" { if req.EditCompose && strings.TrimSpace(req.DockerCompose) == "" {
return nil, buserr.New("ErrAgentComposeRequired") return nil, buserr.New("ErrAgentComposeRequired")
@@ -290,6 +316,433 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) {
return &item, nil return &item, nil
} }
func (a AgentService) BatchInstall(req dto.AgentBatchInstallReq) (*dto.AgentItem, error) {
accountID, err := a.ensureBatchInstallAccount(req)
if err != nil {
return nil, err
}
createReq := buildCreateReqFromBatchInstallReq(req)
createReq.AccountID = accountID
if strings.TrimSpace(createReq.Name) == "" {
name, err := buildBatchInstallAgentName(createReq.AgentType)
if err != nil {
return nil, err
}
createReq.Name = name
}
if createReq.AgentType == constant.AppOpenclaw && len(createReq.AllowedOrigins) == 0 {
allowedOrigin, err := buildBatchInstallOpenclawAllowedOrigin(req)
if err != nil {
return nil, err
}
createReq.AllowedOrigins = []string{allowedOrigin}
}
return a.Create(createReq)
}
func (a AgentService) BatchUpgrade(req dto.AgentBatchUpgradeReq) ([]dto.AgentBatchUpgradeResult, error) {
plans, results, err := buildBatchUpgradePlans(req)
if err != nil {
return nil, err
}
for _, plan := range plans {
result := dto.AgentBatchUpgradeResult{
AgentID: plan.ctx.agent.ID,
AgentName: plan.ctx.agent.Name,
AppInstallID: plan.ctx.agent.AppInstallID,
}
if err := upgradeInstall(plan.req); err != nil {
result.Message = err.Error()
} else {
result.Success = true
}
results = append(results, result)
}
return results, nil
}
func (a AgentService) BatchInstallSkill(req dto.AgentBatchSkillInstallReq) ([]dto.AgentBatchSkillInstallResult, error) {
if req.AgentType != constant.AppOpenclaw && req.AgentType != constant.AppHermesAgent {
return nil, fmt.Errorf("%s does not support skill batch install", req.AgentType)
}
packagePath, err := validateLocalSkillPackagePath(req.PackagePath)
if err != nil {
return nil, err
}
if format, err := localSkillPackageFormat(packagePath); err != nil {
return nil, err
} else if format != "zip" {
return nil, fmt.Errorf("only .zip skill packages can be installed")
}
skillName := sanitizeLocalSkillDirName(req.SkillName, packagePath, req.SkillName)
agents, err := listBatchAgents(req.AgentType)
if err != nil {
return nil, err
}
results := make([]dto.AgentBatchSkillInstallResult, 0, len(agents))
for _, agent := range agents {
result := dto.AgentBatchSkillInstallResult{
AgentID: agent.ID,
AgentName: agent.Name,
AppInstallID: agent.AppInstallID,
}
ctx, message := loadBatchAgentInstall(agent, req.AgentType)
if message != "" {
result.Message = message
results = append(results, result)
continue
}
install := ctx.install
result.AppInstallID = install.ID
if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading {
result.Message = fmt.Sprintf("agent status is %s", install.Status)
results = append(results, result)
continue
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
result.Message = err.Error()
results = append(results, result)
continue
}
installTask, err := task.NewTaskWithOps(skillName, task.TaskInstall, task.TaskScopeAI, buildBatchSkillInstallTaskID(req.TaskID, agent.ID), agent.ID)
if err != nil {
result.Message = err.Error()
results = append(results, result)
continue
}
currentAgent := ctx.agent
currentInstall := ctx.install
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, currentInstall.ContainerName, currentAgent.AgentType, currentAgent.ConfigPath, packagePath, skillName)
}, nil)
go func() {
if err := installTask.Execute(); err != nil {
global.LOG.Errorf("batch install local skill failed: %v", err)
}
}()
result.Success = true
results = append(results, result)
}
return results, nil
}
func (a AgentService) BatchOperate(req dto.AgentBatchOperateReq) ([]dto.AgentBatchOperateResult, error) {
operate := constant.AppOperate(strings.TrimSpace(req.Operate))
if operate != constant.Start && operate != constant.Stop && operate != constant.Restart && operate != constant.Delete {
return nil, fmt.Errorf("operate %s is not supported", req.Operate)
}
agents, err := listBatchAgents(req.AgentType)
if err != nil {
return nil, err
}
results := make([]dto.AgentBatchOperateResult, 0, len(agents))
for _, agent := range agents {
result := dto.AgentBatchOperateResult{
AgentID: agent.ID,
AgentName: agent.Name,
AppInstallID: agent.AppInstallID,
}
if operate == constant.Delete {
if err := a.Delete(dto.AgentDeleteReq{
ID: agent.ID,
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
ForceDelete: req.ForceDelete,
}); err != nil {
result.Message = err.Error()
} else {
result.Success = true
}
results = append(results, result)
continue
}
ctx, message := loadBatchAgentInstall(agent, req.AgentType)
if message != "" {
result.Message = message
results = append(results, result)
continue
}
install := ctx.install
result.AppInstallID = install.ID
if message := batchOperateSkipMessage(operate, install.Status); message != "" {
result.Success = true
result.Skipped = true
result.Message = message
results = append(results, result)
continue
}
if err := NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID,
Operate: operate,
TaskID: buildBatchOperateTaskID(req.TaskID, agent.ID),
}); err != nil {
result.Message = err.Error()
} else {
result.Success = true
}
results = append(results, result)
}
return results, nil
}
func listBatchAgents(agentType string) ([]model.Agent, error) {
return agentRepo.List(func(db *gorm.DB) *gorm.DB {
return db.Where("agent_type = ?", agentType).Order("id ASC")
})
}
func loadBatchAgentInstall(agent model.Agent, agentType string) (batchAgentInstallContext, string) {
ctx := batchAgentInstallContext{agent: agent}
if agent.AppInstallID == 0 {
return ctx, "agent app install id is empty"
}
install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID))
if err != nil {
return ctx, err.Error()
}
ctx.install = install
if install.App.Key != agentType {
return ctx, fmt.Sprintf("app key %s does not match agent type %s", install.App.Key, agentType)
}
return ctx, ""
}
func buildBatchUpgradePlans(req dto.AgentBatchUpgradeReq) ([]batchUpgradePlan, []dto.AgentBatchUpgradeResult, error) {
agents, err := listBatchAgents(req.AgentType)
if err != nil {
return nil, nil, err
}
plans := make([]batchUpgradePlan, 0, len(agents))
results := make([]dto.AgentBatchUpgradeResult, 0)
for _, agent := range agents {
result := dto.AgentBatchUpgradeResult{
AgentID: agent.ID,
AgentName: agent.Name,
AppInstallID: agent.AppInstallID,
}
ctx, message := loadBatchAgentInstall(agent, req.AgentType)
if message != "" {
result.Message = message
results = append(results, result)
continue
}
install := ctx.install
result.AppInstallID = install.ID
if install.Status == constant.StatusInstalling || install.Status == constant.StatusUpgrading {
result.Message = fmt.Sprintf("agent status is %s", install.Status)
results = append(results, result)
continue
}
if install.Version == req.TargetVersion {
result.Success = true
result.Skipped = true
result.Message = "already target version"
results = append(results, result)
continue
}
detail, err := appDetailRepo.GetFirst(appDetailRepo.WithAppId(install.AppId), appDetailRepo.WithVersion(req.TargetVersion))
if err != nil {
result.Message = err.Error()
results = append(results, result)
continue
}
if detail.ID == 0 {
result.Message = fmt.Sprintf("target version %s not found", req.TargetVersion)
results = append(results, result)
continue
}
plans = append(plans, batchUpgradePlan{
ctx: ctx,
req: request.AppInstallUpgrade{
InstallID: install.ID,
DetailID: detail.ID,
Backup: req.Backup,
PullImage: req.PullImage,
TaskID: buildBatchUpgradeTaskID(req.TaskID, install.ID),
},
})
}
return plans, results, nil
}
func buildBatchUpgradeTaskID(taskID string, appInstallID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-upgrade-%d-%d", appInstallID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, appInstallID)
}
func buildBatchSkillInstallTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-skill-install-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func buildBatchOperateTaskID(taskID string, agentID uint) string {
taskID = strings.TrimSpace(taskID)
if taskID == "" {
taskID = fmt.Sprintf("batch-operate-%d-%d", agentID, time.Now().UnixNano())
}
return fmt.Sprintf("%s-%d", taskID, agentID)
}
func batchOperateSkipMessage(operate constant.AppOperate, status string) string {
switch status {
case constant.StatusInstalling, constant.StatusUpgrading, constant.StatusUninstalling, constant.StatusRebuilding:
return fmt.Sprintf("agent status is %s", status)
}
switch operate {
case constant.Start:
if status == constant.StatusRunning || status == constant.StatusStarting || status == constant.StatusRestarting {
return fmt.Sprintf("agent status is %s", status)
}
case constant.Stop:
if status != constant.StatusRunning {
return fmt.Sprintf("agent status is %s", status)
}
case constant.Restart:
if status == constant.StatusStarting {
return fmt.Sprintf("agent status is %s", status)
}
}
return ""
}
func buildCreateReqFromBatchInstallReq(req dto.AgentBatchInstallReq) dto.AgentCreateReq {
return dto.AgentCreateReq{
Name: req.Name,
Remark: req.Remark,
AppVersion: req.AppVersion,
WebUIPort: req.WebUIPort,
BridgePort: req.BridgePort,
AllowedOrigins: req.AllowedOrigins,
AgentType: req.AgentType,
Model: req.Model,
AccountID: req.AccountID,
Token: req.Token,
DashboardUsername: req.DashboardUsername,
DashboardPassword: req.DashboardPassword,
TaskID: req.TaskID,
Advanced: req.Advanced,
ContainerName: req.ContainerName,
AllowPort: req.AllowPort,
SpecifyIP: req.SpecifyIP,
RestartPolicy: req.RestartPolicy,
CpuQuota: req.CpuQuota,
MemoryLimit: req.MemoryLimit,
MemoryUnit: req.MemoryUnit,
PullImage: req.PullImage,
EditCompose: req.EditCompose,
DockerCompose: req.DockerCompose,
}
}
func buildBatchInstallAgentName(agentType string) (string, error) {
prefix := strings.ToLower(strings.TrimSpace(agentType))
if prefix == "" {
prefix = "agent"
}
for i := 1; i <= 10000; i++ {
name := fmt.Sprintf("%s-%d", prefix, i)
exists, err := agentNameExists(name)
if err != nil {
return "", err
}
if !exists {
return name, nil
}
}
return "", fmt.Errorf("no available agent name")
}
func agentNameExists(name string) (bool, error) {
if exist, err := agentRepo.GetFirst(repo.WithByLowerName(name)); err == nil && exist != nil && exist.ID > 0 {
return true, nil
} else if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return false, err
}
installs, err := appInstallRepo.ListBy(context.Background(), repo.WithByLowerName(name))
if err != nil {
return false, err
}
return len(installs) > 0, nil
}
func buildBatchInstallOpenclawAllowedOrigin(req dto.AgentBatchInstallReq) (string, error) {
host := ""
if value, err := settingRepo.GetValueByKey("SystemIP"); err == nil {
host = strings.TrimSpace(value)
}
if host == "" {
host = strings.TrimSpace(req.FallbackAccessHost)
}
if host == "" {
host = "127.0.0.1"
}
return buildOpenclawAllowedOrigin(openclawAllowedOriginScheme(req.AppVersion), host, req.WebUIPort)
}
func (a AgentService) ensureBatchInstallAccount(req dto.AgentBatchInstallReq) (uint, error) {
if !requiresAgentAccount(req.AgentType) {
return 0, nil
}
if req.MasterAccountID == 0 {
return 0, buserr.New("ErrInvalidParams")
}
snapshot := req.AccountSnapshot
account, err := agentAccountRepo.GetFirst(agentAccountRepo.WithByMasterAccountID(req.MasterAccountID))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
if account == nil && global.IsMaster {
account, err = agentAccountRepo.GetFirst(repo.WithByID(req.MasterAccountID))
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return 0, err
}
}
if account == nil {
account = &model.AgentAccount{
MasterAccountID: req.MasterAccountID,
}
}
account.MasterAccountID = req.MasterAccountID
account.Provider = snapshot.Provider
account.Name = snapshot.Name
account.APIKey = snapshot.APIKey
account.RememberAPIKey = snapshot.RememberAPIKey
account.BaseURL = snapshot.BaseURL
account.APIType = snapshot.APIType
account.Remark = snapshot.Remark
account.Verified = true
initialModels, err := buildInitialAgentAccountModels(account, req.AccountModels)
if err != nil {
return 0, err
}
if err := global.DB.Transaction(func(tx *gorm.DB) error {
if account.ID == 0 {
if err := tx.Create(account).Error; err != nil {
return err
}
} else {
if err := tx.Save(account).Error; err != nil {
return err
}
}
return replacePersistedAgentAccountModelsWithTx(tx, account.ID, initialModels)
}); err != nil {
return 0, err
}
return account.ID, nil
}
func requiresAgentAccount(agentType string) bool {
return agentType != constant.AppCopaw
}
func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) { func setAgentWebUIParams(params map[string]interface{}, agentType, appVersion string, webUIPort int) {
if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) { if agentType == constant.AppOpenclaw && isOpenclawHTTPSWindowVersion(appVersion) {
params["PANEL_APP_PORT_HTTPS"] = webUIPort params["PANEL_APP_PORT_HTTPS"] = webUIPort
@@ -652,18 +1105,19 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
apiKey = item.APIKey apiKey = item.APIKey
} }
items = append(items, dto.AgentAccountInfo{ items = append(items, dto.AgentAccountInfo{
ID: item.ID, ID: item.ID,
Provider: item.Provider, MasterAccountID: item.MasterAccountID,
ProviderName: providercatalog.DisplayName(item.Provider), Provider: item.Provider,
Name: item.Name, ProviderName: providercatalog.DisplayName(item.Provider),
APIKey: apiKey, Name: item.Name,
RememberAPIKey: item.RememberAPIKey, APIKey: apiKey,
BaseURL: item.BaseURL, RememberAPIKey: item.RememberAPIKey,
Models: nil, BaseURL: item.BaseURL,
APIType: item.APIType, Models: nil,
Verified: item.Verified, APIType: item.APIType,
Remark: item.Remark, Verified: item.Verified,
CreatedAt: item.CreatedAt, Remark: item.Remark,
CreatedAt: item.CreatedAt,
}) })
} }
for i := range items { for i := range items {
@@ -676,6 +1130,10 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age
return count, items, nil return count, items, nil
} }
func (a AgentService) CountAccountsByProviders(req dto.AgentAccountProviderCountReq) (map[string]int64, error) {
return agentAccountRepo.CountByProviders(req.Providers)
}
func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) { func (a AgentService) GetAccountModels(req dto.AgentAccountModelReq) ([]dto.AgentAccountModel, error) {
account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID)) account, err := agentAccountRepo.GetFirst(repo.WithByID(req.AccountID))
if err != nil { if err != nil {
@@ -880,10 +1338,13 @@ func (a AgentService) GetOtherConfig(req dto.AgentIDReq) (*dto.AgentOtherConfig,
if err != nil { if err != nil {
return nil, err return nil, err
} }
auth := readHermesDashboardAuthFromInstall(install)
return &dto.AgentOtherConfig{ return &dto.AgentOtherConfig{
UserTimezone: cfg.Timezone, UserTimezone: cfg.Timezone,
BrowserEnabled: true, BrowserEnabled: true,
NPMRegistry: "https://registry.npmjs.org/", NPMRegistry: "https://registry.npmjs.org/",
DashboardUsername: auth.Username,
DashboardPassword: auth.Password,
}, nil }, nil
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -908,12 +1369,21 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
if err != nil { if err != nil {
return err return err
} }
previousAuth := readHermesDashboardAuthFromInstall(install)
nextAuth := normalizeHermesDashboardAuth(req.DashboardUsername, req.DashboardPassword)
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil { if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
return err return err
} }
if err := writeHermesDashboardAuthEnv(path.Join(install.GetPath(), ".env"), nextAuth, true); err != nil {
return err
}
operate := constant.Restart
if previousAuth.Username != nextAuth.Username || previousAuth.Password != nextAuth.Password {
operate = constant.Rebuild
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{ return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID, InstallId: install.ID,
Operate: constant.Restart, Operate: operate,
}) })
} }
if err := ensureContainerRunning(install.ContainerName); err != nil { if err := ensureContainerRunning(install.ContainerName); err != nil {
@@ -990,7 +1460,7 @@ func validateAgentConfigFileContent(agentType, content string) error {
} }
func getOpenclawNPMRegistry(containerName string) (string, error) { func getOpenclawNPMRegistry(containerName string) (string, error) {
registry, err := runDockerExecWithStdout(20*time.Second, containerName, "npm", "get", "registry") registry, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "npm", "get", "registry")
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -1002,7 +1472,7 @@ func getOpenclawNPMRegistry(containerName string) (string, error) {
} }
func setOpenclawNPMRegistry(containerName, registry string) error { func setOpenclawNPMRegistry(containerName, registry string) error {
return cmd.RunDefaultBashCf("docker exec %s npm set registry %q", containerName, registry) return cmd.NewCommandMgr().Run("docker", "exec", containerName, "npm", "set", "registry", registry)
} }
func (a AgentService) loadAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) { func (a AgentService) loadAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) {
+5 -11
View File
@@ -23,7 +23,7 @@ func (a AgentService) CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCrea
return nil, err return nil, err
} }
args := []string{"exec", install.ContainerName, "openclaw", "agents", "add", req.Name} args := []string{"openclaw", "agents", "add", req.Name}
workspace := "/home/node/.openclaw/workspace-agent_" + req.Name workspace := "/home/node/.openclaw/workspace-agent_" + req.Name
agentDir := "/home/node/.openclaw/agents/" + req.Name agentDir := "/home/node/.openclaw/agents/" + req.Name
args = append(args, "--workspace", workspace) args = append(args, "--workspace", workspace)
@@ -43,8 +43,7 @@ func (a AgentService) CreateRole(req dto.AgentRoleCreateReq) (*dto.AgentRoleCrea
args = append(args, "--agent-dir", agentDir) args = append(args, "--agent-dir", agentDir)
args = append(args, "--non-interactive", "--json") args = append(args, "--non-interactive", "--json")
mgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute)) output, err := cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, args...)
output, err := mgr.RunWithStdout("docker", args...)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -125,10 +124,8 @@ func (a AgentService) DeleteRole(req dto.AgentRoleDeleteReq) error {
return buserr.New("ErrRecordNotFound") return buserr.New("ErrRecordNotFound")
} }
args := []string{"exec", install.ContainerName, "openclaw", "agents", "delete", req.ID, "--force"} args := []string{"openclaw", "agents", "delete", req.ID, "--force"}
if _, err = cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, args...); err != nil {
mgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute))
if _, err = mgr.RunWithStdout("docker", args...); err != nil {
return err return err
} }
if target.Workspace != "" { if target.Workspace != "" {
@@ -171,8 +168,6 @@ func (a AgentService) operateRoleBinding(req dto.AgentRoleBindReq, action string
return buserr.New("ErrInvalidParams") return buserr.New("ErrInvalidParams")
} }
args := []string{ args := []string{
"exec",
install.ContainerName,
"openclaw", "openclaw",
"agents", "agents",
action, action,
@@ -182,8 +177,7 @@ func (a AgentService) operateRoleBinding(req dto.AgentRoleBindReq, action string
binding, binding,
} }
args = append(args, "--json") args = append(args, "--json")
mgr := cmd.NewCommandMgr(cmd.WithTimeout(5 * time.Minute)) _, err = cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, args...)
_, err = mgr.RunWithStdout("docker", args...)
return err return err
} }
+79 -29
View File
@@ -372,14 +372,14 @@ func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute)) mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if req.Type == "qqbot" { if req.Type == "qqbot" {
legacyPluginPath := path.Join(openclawPluginBaseDir, "qqbot") legacyPluginPath := path.Join(openclawPluginBaseDir, "qqbot")
if err := mgr.RunBashCf("docker exec %s test -d %s", install.ContainerName, legacyPluginPath); err == nil { if err := mgr.Run("docker", "exec", install.ContainerName, "test", "-d", legacyPluginPath); err == nil {
if _, err := mgr.RunWithStdout("docker", "exec", "-i", install.ContainerName, "sh", "-c", buildOpenclawPluginUninstallScript("qqbot")); err != nil { if err := uninstallOpenclawPlugin(mgr, install.ContainerName, "qqbot"); err != nil {
return err return err
} }
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
} }
} }
if _, err := mgr.RunWithStdout("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawPluginInstallScript(spec, pluginID)); err != nil { if err := installOpenclawPlugin(mgr, install.ContainerName, spec, pluginID); err != nil {
return err return err
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -415,11 +415,11 @@ func (a AgentService) UpgradePlugin(req dto.AgentPluginUpgradeReq) error {
} }
upgradeTask.AddSubTask("Upgrade OpenClaw plugin", func(t *task.Task) error { upgradeTask.AddSubTask("Upgrade OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute)) mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if _, err := mgr.RunWithStdout("docker", "exec", "-i", install.ContainerName, "sh", "-c", buildOpenclawPluginUninstallScript(pluginID)); err != nil { if err := uninstallOpenclawPlugin(mgr, install.ContainerName, pluginID); err != nil {
return err return err
} }
time.Sleep(2 * time.Second) time.Sleep(2 * time.Second)
if _, err := mgr.RunWithStdout("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawPluginInstallScript(spec, pluginID)); err != nil { if err := installOpenclawPlugin(mgr, install.ContainerName, spec, pluginID); err != nil {
return err return err
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -455,7 +455,7 @@ func (a AgentService) UninstallPlugin(req dto.AgentPluginUninstallReq) error {
} }
uninstallTask.AddSubTask("Uninstall OpenClaw plugin", func(t *task.Task) error { uninstallTask.AddSubTask("Uninstall OpenClaw plugin", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute)) mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute))
if _, err := mgr.RunWithStdout("docker", "exec", "-i", install.ContainerName, "sh", "-c", buildOpenclawPluginUninstallScript(pluginID)); err != nil { if err := uninstallOpenclawPlugin(mgr, install.ContainerName, pluginID); err != nil {
return err return err
} }
conf, err := readOpenclawConfig(agent.ConfigPath) conf, err := readOpenclawConfig(agent.ConfigPath)
@@ -487,7 +487,7 @@ func (a AgentService) LoginWeixinChannel(req dto.AgentWeixinLoginReq) error {
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
return mgr.Run("docker", buildHermesWeixinLoginArgs(install.ContainerName)...) return mgr.Run("docker", buildHermesWeixinLoginArgs(install.ContainerName)...)
} }
return mgr.RunBashCf("docker exec %s openclaw channels login --channel openclaw-weixin", install.ContainerName) return mgr.Run("docker", "exec", install.ContainerName, "openclaw", "channels", "login", "--channel", "openclaw-weixin")
}, nil) }, nil)
if agent.AgentType == constant.AppHermesAgent { if agent.AgentType == constant.AppHermesAgent {
loginTask.AddSubTask("Restart Hermes-Agent container", func(t *task.Task) error { loginTask.AddSubTask("Restart Hermes-Agent container", func(t *task.Task) error {
@@ -584,17 +584,26 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
return validateHermesPairingApproveResult(output, err) return validateHermesPairingApproveResult(output, err)
} }
if req.AccountID != "" { if req.AccountID != "" {
return cmd.RunDefaultBashCf( return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).Run(
"docker exec %s openclaw pairing approve %s %q --account %q", "docker",
"exec",
install.ContainerName, install.ContainerName,
"openclaw",
"pairing",
"approve",
req.Type, req.Type,
req.PairingCode, req.PairingCode,
"--account",
req.AccountID, req.AccountID,
) )
} }
return cmd.RunDefaultBashCf( return cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).Run(
"docker exec %s openclaw pairing approve %s %q", "docker",
"exec",
install.ContainerName, install.ContainerName,
"openclaw",
"pairing",
"approve",
req.Type, req.Type,
req.PairingCode, req.PairingCode,
) )
@@ -802,9 +811,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
result.RequireMention = result.GroupPolicy == "allowlist" result.RequireMention = result.GroupPolicy == "allowlist"
result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"]) result.GroupAllowFrom = extractStringList(telegram["groupAllowFrom"])
result.Proxy = extractStringValue(telegram["proxy"]) result.Proxy = extractStringValue(telegram["proxy"])
if streaming := extractStringValue(telegram["streaming"]); streaming != "" { result.Streaming = normalizeTelegramStreamingMode(telegram["streaming"], result.Streaming)
result.Streaming = streaming
}
accounts := childMap(telegram, "accounts") accounts := childMap(telegram, "accounts")
if len(accounts) == 0 { if len(accounts) == 0 {
botToken := extractStringValue(telegram["botToken"]) botToken := extractStringValue(telegram["botToken"])
@@ -830,7 +837,7 @@ func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig
BotToken: extractStringValue(account["botToken"]), BotToken: extractStringValue(account["botToken"]),
DmPolicy: extractStringValue(account["dmPolicy"]), DmPolicy: extractStringValue(account["dmPolicy"]),
GroupPolicy: extractStringValue(account["groupPolicy"]), GroupPolicy: extractStringValue(account["groupPolicy"]),
Streaming: extractStringValue(account["streaming"]), Streaming: normalizeTelegramStreamingMode(account["streaming"], result.Streaming),
}) })
} }
result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots)) result.DefaultAccount = normalizeDefaultAccount(extractStringValue(telegram["defaultAccount"]), getTelegramBotAccountIDs(bots))
@@ -870,7 +877,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
} else { } else {
delete(telegram, "proxy") delete(telegram, "proxy")
} }
telegram["streaming"] = config.Streaming telegram["streaming"] = buildTelegramStreamingConfig(config.Streaming)
accounts := make(map[string]interface{}, len(config.Bots)) accounts := make(map[string]interface{}, len(config.Bots))
for _, bot := range config.Bots { for _, bot := range config.Bots {
account := map[string]interface{}{ account := map[string]interface{}{
@@ -879,7 +886,7 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
"botToken": bot.BotToken, "botToken": bot.BotToken,
"dmPolicy": bot.DmPolicy, "dmPolicy": bot.DmPolicy,
"groupPolicy": bot.GroupPolicy, "groupPolicy": bot.GroupPolicy,
"streaming": bot.Streaming, "streaming": buildTelegramStreamingConfig(bot.Streaming),
} }
if bot.DmPolicy == "open" { if bot.DmPolicy == "open" {
account["allowFrom"] = []string{"*"} account["allowFrom"] = []string{"*"}
@@ -890,6 +897,29 @@ func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConf
delete(telegram, "botToken") delete(telegram, "botToken")
} }
func normalizeTelegramStreamingMode(value interface{}, defaultMode string) string {
mode := defaultMode
switch typed := value.(type) {
case string:
mode = typed
case map[string]interface{}:
mode = extractStringValue(typed["mode"])
}
mode = strings.ToLower(strings.TrimSpace(mode))
switch mode {
case "off", "partial", "block", "progress":
return mode
default:
return "partial"
}
}
func buildTelegramStreamingConfig(mode string) map[string]interface{} {
return map[string]interface{}{
"mode": normalizeTelegramStreamingMode(mode, "partial"),
}
}
func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig {
result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"} result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", AllowFrom: []string{}, RequireMention: false, GroupPolicy: "open"}
discord := getChannelConfig(conf, "discord") discord := getChannelConfig(conf, "discord")
@@ -1289,20 +1319,40 @@ func appendPluginAllow(conf map[string]interface{}, pluginID string) {
plugins["allow"] = append(allow, pluginID) plugins["allow"] = append(allow, pluginID)
} }
func buildOpenclawPluginInstallScript(spec, pluginID string) string { func installOpenclawPlugin(mgr *cmd.CommandHelper, containerName, spec, pluginID string) error {
return fmt.Sprintf( workdir := path.Join(openclawPluginPackageTmpDir, pluginID)
"set -e; workdir=%s/%s; rm -rf \"$workdir\"; mkdir -p \"$workdir\"; cd \"$workdir\"; npm pack --silent %q >/dev/null 2>&1; pkg=$(find \"$workdir\" -maxdepth 1 -type f -name '*.tgz' | head -n 1); printf '%%s\\n' \"$pkg\"; openclaw plugins install \"$pkg\" --dangerously-force-unsafe-install; rm -rf \"$workdir\"", defer func() {
openclawPluginPackageTmpDir, _ = mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir)
pluginID, }()
spec, if err := mgr.Run("docker", "exec", containerName, "rm", "-rf", workdir); err != nil {
) return err
}
if err := mgr.Run("docker", "exec", containerName, "mkdir", "-p", workdir); err != nil {
return err
}
if err := mgr.Run("docker", "exec", "-w", workdir, containerName, "npm", "pack", "--silent", spec); err != nil {
return err
}
pkgPath, err := cmd.RunDockerExecWithStdout(10*time.Minute, containerName, "find", workdir, "-maxdepth", "1", "-type", "f", "-name", "*.tgz", "-print", "-quit")
if err != nil {
return err
}
pkgPath = strings.TrimSpace(pkgPath)
if pkgPath == "" {
return fmt.Errorf("openclaw plugin package not found")
}
return mgr.Run("docker", "exec", containerName, "openclaw", "plugins", "install", pkgPath, "--dangerously-force-unsafe-install")
} }
func buildOpenclawPluginUninstallScript(pluginID string) string { func uninstallOpenclawPlugin(mgr *cmd.CommandHelper, containerName, pluginID string) error {
return fmt.Sprintf( _, err := mgr.RunPipe(
"set +e; printf 'yes\\n' | openclaw plugins uninstall %s; code=$?; if [ \"$code\" -eq 137 ]; then exit 0; fi; exit \"$code\"", cmd.PipeCommand{Name: "printf", Args: []string{"yes\n"}},
pluginID, cmd.PipeCommand{Name: "docker", Args: []string{"exec", "-i", containerName, "openclaw", "plugins", "uninstall", pluginID}},
) )
if err != nil && strings.Contains(err.Error(), "exit status 137") {
return nil
}
return err
} }
func resolvePluginMeta(pluginType string) (string, string, error) { func resolvePluginMeta(pluginType string) (string, string, error) {
@@ -1357,7 +1407,7 @@ func loadOpenclawPluginLatestVersion(containerName, pluginType string) (string,
if err != nil { if err != nil {
return "", err return "", err
} }
output, err := runDockerExecWithStdout(20*time.Second, containerName, "npm", "view", spec, "version", "--json") output, err := cmd.RunDockerExecWithStdout(20*time.Second, containerName, "npm", "view", spec, "version", "--json")
if err != nil { if err != nil {
return "", err return "", err
} }
+125 -35
View File
@@ -13,14 +13,19 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/joho/godotenv"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const hermesWorkspaceDir = "/opt/data/workspace" const hermesWorkspaceDir = "/opt/data/workspace"
const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes" const hermesExecutablePath = "/opt/hermes/.venv/bin/hermes"
const hermesDashboardUsernameEnvKey = "HERMES_DASHBOARD_USERNAME"
const hermesDashboardPasswordEnvKey = "HERMES_DASHBOARD_PASSWORD"
type hermesDashboardAuth struct {
Username string
Password string
}
type hermesConfig struct { type hermesConfig struct {
Model hermesModelConfig `yaml:"model"` Model hermesModelConfig `yaml:"model"`
@@ -77,11 +82,15 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
if err != nil { if err != nil {
return err return err
} }
cfg["model"] = map[string]interface{}{ model := map[string]interface{}{
"default": resolveHermesModel(account.Provider, provider, modelName), "default": resolveHermesModel(account.Provider, provider, modelName),
"provider": provider, "provider": provider,
"base_url": account.BaseURL, "base_url": account.BaseURL,
} }
if provider == "custom" && account.APIKey != "" {
model["api_key"] = "${CUSTOM_API_KEY}"
}
cfg["model"] = model
cfg["terminal"] = map[string]interface{}{ cfg["terminal"] = map[string]interface{}{
"backend": "local", "backend": "local",
"cwd": hermesWorkspaceDir, "cwd": hermesWorkspaceDir,
@@ -94,7 +103,7 @@ func writeHermesConfig(confDir string, account *model.AgentAccount, modelName st
if err := writeHermesConfigMap(configPath, cfg); err != nil { if err := writeHermesConfigMap(configPath, cfg); err != nil {
return err return err
} }
return writeHermesEnv(path.Join(confDir, ".env"), resolveHermesEnvEntries(account)) return writeHermesModelEnv(path.Join(confDir, ".env"), account)
} }
func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName string) error { func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName string) error {
@@ -108,6 +117,52 @@ func prepareHermesInstallFiles(appInstall *model.AppInstall, account *model.Agen
return files.NewFileOp().ChownR(dataDir, "1000", "1000", true) return files.NewFileOp().ChownR(dataDir, "1000", "1000", true)
} }
func normalizeHermesDashboardAuth(username, password string) hermesDashboardAuth {
auth := hermesDashboardAuth{
Username: strings.TrimSpace(username),
Password: strings.TrimSpace(password),
}
if auth.Username == "" {
auth.Username = "admin"
}
if auth.Password == "" {
auth.Password = common.RandStr(8)
}
return auth
}
func writeHermesDashboardAuthEnv(envPath string, auth hermesDashboardAuth, overwrite bool) error {
return upsertAgentEnv(envPath, map[string]string{
hermesDashboardUsernameEnvKey: auth.Username,
hermesDashboardPasswordEnvKey: auth.Password,
}, []string{
hermesDashboardUsernameEnvKey,
hermesDashboardPasswordEnvKey,
}, overwrite)
}
func readHermesDashboardAuthEnv(envPath string) (hermesDashboardAuth, error) {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return hermesDashboardAuth{}, err
}
return hermesDashboardAuth{
Username: strings.TrimSpace(envMap[hermesDashboardUsernameEnvKey]),
Password: strings.TrimSpace(envMap[hermesDashboardPasswordEnvKey]),
}, nil
}
func readHermesDashboardAuthFromInstall(appInstall *model.AppInstall) hermesDashboardAuth {
if appInstall == nil || appInstall.ID == 0 {
return hermesDashboardAuth{}
}
auth, err := readHermesDashboardAuthEnv(path.Join(appInstall.GetPath(), ".env"))
if err != nil {
return hermesDashboardAuth{}
}
return auth
}
func readHermesConfig(configPath string) (*hermesConfig, error) { func readHermesConfig(configPath string) (*hermesConfig, error) {
content, err := files.NewFileOp().GetContent(configPath) content, err := files.NewFileOp().GetContent(configPath)
if err != nil { if err != nil {
@@ -148,7 +203,7 @@ func writeHermesConfigMap(configPath string, cfg map[string]interface{}) error {
} }
func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) { func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -205,7 +260,7 @@ func readHermesTelegramChannelConfig(confDir string) (*dto.AgentTelegramConfig,
func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error { func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -225,7 +280,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
envMap["TELEGRAM_ALLOWED_USERS"] = allow envMap["TELEGRAM_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"TELEGRAM_BOT_TOKEN", "TELEGRAM_BOT_TOKEN",
"TELEGRAM_ALLOWED_USERS", "TELEGRAM_ALLOWED_USERS",
"TELEGRAM_ALLOW_ALL_USERS", "TELEGRAM_ALLOW_ALL_USERS",
@@ -244,7 +299,7 @@ func writeHermesTelegramChannelConfig(confDir string, config dto.AgentTelegramCo
} }
func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) { func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -296,7 +351,7 @@ func readHermesDiscordChannelConfig(confDir string) (*dto.AgentDiscordConfig, er
func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error { func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -316,7 +371,7 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
envMap["DISCORD_ALLOWED_USERS"] = allow envMap["DISCORD_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"DISCORD_BOT_TOKEN", "DISCORD_BOT_TOKEN",
"DISCORD_ALLOWED_USERS", "DISCORD_ALLOWED_USERS",
"DISCORD_ALLOW_ALL_USERS", "DISCORD_ALLOW_ALL_USERS",
@@ -336,14 +391,14 @@ func writeHermesDiscordChannelConfig(confDir string, config dto.AgentDiscordConf
func deleteHermesEnvKeys(confDir string, keys ...string) error { func deleteHermesEnvKeys(confDir string, keys ...string) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
for _, key := range keys { for _, key := range keys {
delete(envMap, key) delete(envMap, key)
} }
return writeHermesEnvMap(envPath, envMap, keys) return writeAgentEnvMap(envPath, envMap, keys)
} }
func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error { func deleteHermesConfigSections(confDir string, topLevelKeys []string, platformKeys []string) error {
@@ -458,6 +513,14 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
if account == nil { if account == nil {
return 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 apiKey := account.APIKey
baseURL := account.BaseURL baseURL := account.BaseURL
entries := make([]hermesEnvEntry, 0, 4) entries := make([]hermesEnvEntry, 0, 4)
@@ -519,44 +582,71 @@ func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
return entries return entries
} }
func writeHermesEnv(envPath string, entries []hermesEnvEntry) error { func writeHermesModelEnv(envPath string, account *model.AgentAccount) error {
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
for _, key := range hermesManagedModelEnvKeys() {
delete(envMap, key)
}
entries := resolveHermesEnvEntries(account)
order := make([]string, 0, len(entries))
for _, entry := range entries { for _, entry := range entries {
if entry.Key == "" || entry.Value == "" { if entry.Key == "" || entry.Value == "" {
continue continue
} }
envMap[entry.Key] = entry.Value envMap[entry.Key] = entry.Value
}
order := make([]string, 0, len(entries))
for _, entry := range entries {
if entry.Key == "" {
continue
}
order = append(order, entry.Key) order = append(order, entry.Key)
} }
return writeHermesEnvMap(envPath, envMap, order) return writeAgentEnvMap(envPath, envMap, order)
} }
func readHermesEnvMap(envPath string) (map[string]string, error) { func hermesManagedModelEnvKeys() []string {
fileOp := files.NewFileOp() keys := []string{
if !fileOp.Stat(envPath) { "OPENROUTER_API_KEY",
return map[string]string{}, nil "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",
} }
envMap, err := godotenv.Read(envPath) seen := make(map[string]struct{}, len(keys))
if err != nil { result := make([]string, 0, len(keys))
return nil, err appendKey := func(key string) {
if key == "" {
return
}
if _, ok := seen[key]; ok {
return
}
seen[key] = struct{}{}
result = append(result, key)
} }
return envMap, nil for _, key := range keys {
} appendKey(key)
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) for _, meta := range providercatalog.All() {
appendKey(meta.EnvKey)
}
return result
} }
func splitHermesEnvList(value string) []string { func splitHermesEnvList(value string) []string {
+13 -13
View File
@@ -34,7 +34,7 @@ print('Restart the Hermes-Agent container to apply the new Weixin settings.')
` `
func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) { func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -78,7 +78,7 @@ func readHermesQQBotChannelConfig(confDir string) (*dto.AgentQQBotConfig, error)
func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error { func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -102,7 +102,7 @@ func writeHermesQQBotChannelConfig(confDir string, config dto.AgentQQBotConfig)
envMap["QQ_GROUP_ALLOWED_USERS"] = allow envMap["QQ_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"QQ_APP_ID", "QQ_APP_ID",
"QQ_CLIENT_SECRET", "QQ_CLIENT_SECRET",
"QQ_ALLOW_ALL_USERS", "QQ_ALLOW_ALL_USERS",
@@ -163,7 +163,7 @@ func deleteHermesQQBotChannelConfig(confDir string) error {
} }
func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) { func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -194,7 +194,7 @@ func readHermesWecomChannelConfig(confDir string) (*dto.AgentWecomConfig, error)
func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error { func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -229,7 +229,7 @@ func writeHermesWecomChannelConfig(confDir string, config dto.AgentWecomConfig)
envMap["WECOM_GROUP_ALLOWED_USERS"] = allow envMap["WECOM_GROUP_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"WECOM_BOT_ID", "WECOM_BOT_ID",
"WECOM_SECRET", "WECOM_SECRET",
"WECOM_DM_POLICY", "WECOM_DM_POLICY",
@@ -282,7 +282,7 @@ func deleteHermesWecomChannelConfig(confDir string) error {
} }
func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) { func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -332,7 +332,7 @@ func readHermesDingTalkChannelConfig(confDir string) (*dto.AgentDingTalkConfig,
func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error { func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -356,7 +356,7 @@ func writeHermesDingTalkChannelConfig(confDir string, config dto.AgentDingTalkCo
} else if config.DmPolicy == "open" { } else if config.DmPolicy == "open" {
envMap["DINGTALK_ALLOW_ALL_USERS"] = "true" envMap["DINGTALK_ALLOW_ALL_USERS"] = "true"
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"DINGTALK_CLIENT_ID", "DINGTALK_CLIENT_ID",
"DINGTALK_CLIENT_SECRET", "DINGTALK_CLIENT_SECRET",
"DINGTALK_ALLOW_ALL_USERS", "DINGTALK_ALLOW_ALL_USERS",
@@ -398,7 +398,7 @@ func deleteHermesDingTalkChannelConfig(confDir string) error {
} }
func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) { func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -452,7 +452,7 @@ func readHermesFeishuChannelConfig(confDir string) (*dto.AgentFeishuConfig, erro
func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error { func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig) error {
envPath := path.Join(confDir, ".env") envPath := path.Join(confDir, ".env")
envMap, err := readHermesEnvMap(envPath) envMap, err := readAgentEnvMap(envPath)
if err != nil { if err != nil {
return err return err
} }
@@ -488,7 +488,7 @@ func writeHermesFeishuChannelConfig(confDir string, config dto.AgentFeishuConfig
envMap["FEISHU_ALLOWED_USERS"] = allow envMap["FEISHU_ALLOWED_USERS"] = allow
} }
} }
if err := writeHermesEnvMap(envPath, envMap, []string{ if err := writeAgentEnvMap(envPath, envMap, []string{
"FEISHU_APP_ID", "FEISHU_APP_ID",
"FEISHU_APP_SECRET", "FEISHU_APP_SECRET",
"FEISHU_DOMAIN", "FEISHU_DOMAIN",
@@ -529,7 +529,7 @@ func deleteHermesFeishuChannelConfig(confDir string) error {
} }
func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) { func readHermesWeixinChannelConfig(confDir string) (*dto.AgentWeixinConfig, error) {
envMap, err := readHermesEnvMap(path.Join(confDir, ".env")) envMap, err := readAgentEnvMap(path.Join(confDir, ".env"))
if err != nil { if err != nil {
return nil, err return nil, err
} }
+2 -1
View File
@@ -10,6 +10,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
) )
const ( const (
@@ -101,7 +102,7 @@ func countOpenclawConfiguredChannels(conf map[string]interface{}) int {
} }
func loadOpenclawOverviewSkillStats(containerName string) (int, error) { func loadOpenclawOverviewSkillStats(containerName string) (int, error) {
output, err := runDockerExecWithStdout(5*time.Minute, containerName, "sh", "-c", "openclaw skills list --json 2>&1") output, err := cmd.RunDockerExecWithStdout(5*time.Minute, containerName, "openclaw", "skills", "list", "--json")
if err != nil { if err != nil {
return 0, err return 0, err
} }
+595 -23
View File
@@ -1,9 +1,18 @@
package service package service
import ( import (
"archive/tar"
"archive/zip"
"compress/gzip"
"encoding/json" "encoding/json"
"fmt" "fmt"
"io"
"os"
"os/exec"
"path"
"path/filepath"
"regexp" "regexp"
"strconv"
"strings" "strings"
"time" "time"
@@ -12,10 +21,15 @@ import (
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common"
) )
const clawhubGlobalRegistry = "https://clawhub.com" const clawhubGlobalRegistry = "https://clawhub.com"
const clawhubChinaRegistry = "https://mirror-cn.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 { type openclawSkillsList struct {
Skills []openclawSkillListItem `json:"skills"` Skills []openclawSkillListItem `json:"skills"`
@@ -38,8 +52,17 @@ type skillhubSearchPayload struct {
Results []dto.AgentSkillSearchItem `json:"results"` 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 clawhubSearchLinePattern = regexp.MustCompile(`^(\S+)\s+(.+?)\s+\(([\d.]+)\)$`)
var ansiEscapePattern = regexp.MustCompile(`\x1b\[[0-9;?]*[ -/]*[@-~]`) 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) { func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID) agent, install, err := a.loadAgentAndInstall(req.AgentID)
@@ -55,7 +78,7 @@ func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, erro
if agent.AgentType != constant.AppOpenclaw { if agent.AgentType != constant.AppOpenclaw {
return nil, fmt.Errorf("%s does not support", agent.AgentType) return nil, fmt.Errorf("%s does not support", agent.AgentType)
} }
output, err := runDockerExecWithStdout(5*time.Minute, install.ContainerName, "sh", "-c", "openclaw skills list --json 2>&1") output, err := cmd.RunDockerExecWithStdout(5*time.Minute, install.ContainerName, "openclaw", "skills", "list", "--json")
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -66,6 +89,12 @@ func (a AgentService) ListSkills(req dto.AgentIDReq) ([]dto.AgentSkillItem, erro
} }
func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) { func (a AgentService) SearchSkills(req dto.AgentSkillSearchReq) ([]dto.AgentSkillSearchItem, error) {
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) agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -115,6 +144,9 @@ func (a AgentService) UpdateSkill(req dto.AgentSkillUpdateReq) error {
} }
func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error { func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
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) agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil { if err != nil {
return err return err
@@ -122,6 +154,29 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
if err := ensureContainerRunning(install.ContainerName); err != nil { if err := ensureContainerRunning(install.ContainerName); err != nil {
return err 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) installTask, err := task.NewTaskWithOps(req.Slug, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID)
if err != nil { if err != nil {
return err return err
@@ -143,7 +198,7 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
} }
installTask.AddSubTask("Install OpenClaw skill", func(t *task.Task) error { installTask.AddSubTask("Install OpenClaw skill", func(t *task.Task) error {
mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute)) mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(20*time.Minute))
return mgr.Run("docker", "exec", install.ContainerName, "sh", "-c", buildOpenclawSkillInstallCommand(req.Source, req.Slug)) return installOpenclawSkill(mgr, install.ContainerName, req.Source, req.Slug)
}, nil) }, nil)
go func() { go func() {
if err := installTask.Execute(); err != nil { if err := installTask.Execute(); err != nil {
@@ -153,6 +208,513 @@ func (a AgentService) InstallSkill(req dto.AgentSkillInstallReq) error {
return nil 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 { func (a AgentService) UninstallSkill(req dto.AgentSkillUninstallReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID) agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil { if err != nil {
@@ -205,15 +767,19 @@ func parseOpenclawSkillsList(output string) ([]dto.AgentSkillItem, error) {
func loadOpenclawSkillSearchOutput(containerName, source, keyword string) (string, error) { func loadOpenclawSkillSearchOutput(containerName, source, keyword string) (string, error) {
switch source { switch source {
case "skillhub": case "skillhub":
return runDockerExecWithStdout(2*time.Minute, containerName, "skillhub", "search", keyword, "--json") return cmd.RunDockerExecWithStdout(2*time.Minute, containerName, "skillhub", "search", keyword, "--json")
default: default:
return runDockerExecWithStdout( return cmd.NewCommandMgr(cmd.WithTimeout(2*time.Minute)).
2*time.Minute, RunWithStdout(
containerName, "docker",
"sh", "exec",
"-c", "-e",
fmt.Sprintf("CLAWHUB_REGISTRY=%q clawhub search %q", resolveClawhubRegistry(source), keyword), "CLAWHUB_REGISTRY="+resolveClawhubRegistry(source),
) containerName,
"clawhub",
"search",
keyword,
)
} }
} }
@@ -261,22 +827,28 @@ func parseClawhubSearchResult(output, source string) []dto.AgentSkillSearchItem
return items 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 { switch source {
case "clawhub-global", "clawhub-cn": case "clawhub-global", "clawhub-cn":
return fmt.Sprintf( return mgr.Run(
"mkdir -p %s && CLAWHUB_REGISTRY=%q clawhub --workdir /home/node/.openclaw --dir skills install %q", "docker",
openclawManagedSkillsDir, "exec",
resolveClawhubRegistry(source), "-e",
"CLAWHUB_REGISTRY="+resolveClawhubRegistry(source),
containerName,
"clawhub",
"--workdir",
"/home/node/.openclaw",
"--dir",
"skills",
"install",
slug, slug,
) )
default: default:
return fmt.Sprintf( return mgr.Run("docker", "exec", containerName, "skillhub", "--dir", openclawManagedSkillsDir, "install", slug)
"mkdir -p %s && skillhub --dir %s install %q",
openclawManagedSkillsDir,
openclawManagedSkillsDir,
slug,
)
} }
} }
@@ -290,7 +862,7 @@ func resolveClawhubRegistry(source string) string {
} }
func getOpenclawSkillKey(containerName, name string) (string, error) { func getOpenclawSkillKey(containerName, name string) (string, error) {
output, err := runDockerExecWithStdout(2*time.Minute, containerName, "sh", "-c", fmt.Sprintf("openclaw skills info %q --json 2>&1", name)) output, err := cmd.RunDockerExecWithStdout(2*time.Minute, containerName, "openclaw", "skills", "info", name, "--json")
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -307,7 +879,7 @@ func parseOpenclawSkillKey(name, output string) (string, error) {
return "", err return "", err
} }
if payload.SkillKey == "" { if payload.SkillKey == "" {
return "", fmt.Errorf("skill %s does not have a skillKey", name) return name, nil
} }
return payload.SkillKey, nil return payload.SkillKey, nil
} }
+53 -12
View File
@@ -19,10 +19,11 @@ import (
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
agentenv "github.com/1Panel-dev/1Panel/agent/utils/env"
"github.com/1Panel-dev/1Panel/agent/utils/files" "github.com/1Panel-dev/1Panel/agent/utils/files"
"github.com/1Panel-dev/1Panel/agent/utils/req_helper" "github.com/1Panel-dev/1Panel/agent/utils/req_helper"
"github.com/joho/godotenv"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -60,11 +61,6 @@ func ensureContainerRunning(containerName string) error {
return nil return nil
} }
func runDockerExecWithStdout(timeout time.Duration, containerName string, args ...string) (string, error) {
commandArgs := append([]string{"exec", containerName}, args...)
return cmd.NewCommandMgr(cmd.WithTimeout(timeout)).RunWithStdout("docker", commandArgs...)
}
func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) { func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) {
resolvedAPIKey := strings.TrimSpace(apiKey) resolvedAPIKey := strings.TrimSpace(apiKey)
resolvedBaseURL := strings.TrimSpace(baseURL) resolvedBaseURL := strings.TrimSpace(baseURL)
@@ -382,6 +378,11 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map
item.BridgePort = toInt(bridge) item.BridgePort = toInt(bridge)
} }
} }
if agentType == constant.AppHermesAgent {
auth := readHermesDashboardAuthFromInstall(appInstall)
item.DashboardUsername = auth.Username
item.DashboardPassword = auth.Password
}
} }
return item return item
} }
@@ -584,6 +585,9 @@ func checkAgentUpgradable(install model.AppInstall) bool {
if install.App.ID == 0 { if install.App.ID == 0 {
return false return false
} }
if ignoreUpdate(install) {
return false
}
details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(install.App.ID)) details, err := appDetailRepo.GetBy(appDetailRepo.WithAppId(install.App.ID))
if err != nil || len(details) == 0 { if err != nil || len(details) == 0 {
return false return false
@@ -863,13 +867,15 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName,
if err := writeOpenclawConfigRaw(configPath, conf); err != nil { if err := writeOpenclawConfigRaw(configPath, conf); err != nil {
return err return err
} }
envPath := path.Join(confDir, ".env") envMap := map[string]string{
lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)} "OPENCLAW_GATEWAY_TOKEN": token,
if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey))
} }
content := strings.Join(lines, "\n") + "\n" order := []string{"OPENCLAW_GATEWAY_TOKEN"}
return fileOp.SaveFile(envPath, content, 0600) if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && account.APIKey != "" {
envMap[envKey] = account.APIKey
order = append(order, envKey)
}
return writeAgentEnvMap(path.Join(confDir, ".env"), envMap, order)
} }
func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) {
@@ -1527,6 +1533,41 @@ func readInstallEnv(envStr string) map[string]interface{} {
return data return data
} }
func readAgentEnvMap(envPath string) (map[string]string, error) {
fileOp := files.NewFileOp()
if !fileOp.Stat(envPath) {
return map[string]string{}, nil
}
envMap, err := godotenv.Read(envPath)
if err != nil {
return nil, err
}
return envMap, nil
}
func writeAgentEnvMap(envPath string, envMap map[string]string, order []string) error {
if len(envMap) == 0 {
return files.NewFileOp().SaveFile(envPath, "", 0600)
}
return agentenv.WriteWithOrder(envMap, envPath, order)
}
func upsertAgentEnv(envPath string, values map[string]string, order []string, overwrite bool) error {
envMap, err := readAgentEnvMap(envPath)
if err != nil {
return err
}
for key, value := range values {
if key == "" {
continue
}
if overwrite || strings.TrimSpace(envMap[key]) == "" {
envMap[key] = value
}
}
return writeAgentEnvMap(envPath, envMap, order)
}
func maskKey(value string) string { func maskKey(value string) string {
trim := strings.TrimSpace(value) trim := strings.TrimSpace(value)
if len(trim) <= 6 { if len(trim) <= 6 {
+8 -5
View File
@@ -77,7 +77,7 @@ func (u *AIToolService) LoadDetail(name string) (string, error) {
if err != nil { if err != nil {
return "", err 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 { if err != nil {
return "", err return "", err
} }
@@ -137,7 +137,7 @@ func (u *AIToolService) Close(name string) error {
if err != nil { if err != nil {
return err 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 fmt.Errorf("handle ollama stop %s failed, %v", name, err)
} }
return nil return nil
@@ -194,7 +194,7 @@ func (u *AIToolService) Delete(req dto.ForceDelete) error {
} }
for _, item := range ollamaList { for _, item := range ollamaList {
if item.Status != constant.StatusDeleted { 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) return fmt.Errorf("handle ollama rm %s failed, %v", item.Name, err)
} }
} }
@@ -210,7 +210,7 @@ func (u *AIToolService) Sync() ([]dto.OllamaModelDropList, error) {
if err != nil { if err != nil {
return nil, err 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 { if err != nil {
return nil, err return nil, err
} }
@@ -382,12 +382,15 @@ func LoadContainerName() (string, error) {
} }
func loadModelSize(name string, containerName string) (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 { if err != nil {
return "", err return "", err
} }
lines := strings.Split(stdout, "\n") lines := strings.Split(stdout, "\n")
for _, line := range lines { for _, line := range lines {
if !strings.Contains(line, name) {
continue
}
parts := strings.Fields(line) parts := strings.Fields(line)
if len(parts) < 5 { if len(parts) < 5 {
continue continue
+213 -21
View File
@@ -3,6 +3,13 @@ package service
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"mime"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/model" "github.com/1Panel-dev/1Panel/agent/app/model"
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
@@ -15,24 +22,27 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/email" "github.com/1Panel-dev/1Panel/agent/utils/email"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
"mime"
"sort"
"strings"
"sync"
"time"
) )
type AlertService struct{} 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 { type IAlertService interface {
PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error) PageAlert(req dto.AlertSearch) (int64, []dto.AlertDTO, error)
GetAlerts() ([]dto.AlertDTO, error) GetAlerts() ([]dto.AlertDTO, error)
CreateAlert(create dto.AlertCreate) error CreateAlert(create dto.AlertCreate, operator string) error
UpdateAlert(req dto.AlertUpdate) error UpdateAlert(req dto.AlertUpdate, operator string) error
DeleteAlert(id uint) error DeleteAlert(id uint) error
GetAlert(id uint) (dto.AlertDTO, error) GetAlert(id uint) (dto.AlertDTO, error)
UpdateStatus(id uint, status string) error UpdateStatus(id uint, status string) error
ExternalUpdateAlert(req dto.AlertCreate) error ExternalUpdateAlert(req dto.AlertCreate, operator string) error
GetDisks() ([]dto.DiskDTO, error) GetDisks() ([]dto.DiskDTO, error)
PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error) PageAlertLogs(req dto.AlertLogSearch) (int64, []dto.AlertLogDTO, error)
@@ -40,8 +50,9 @@ type IAlertService interface {
GetClams() ([]dto.ClamDTO, error) GetClams() ([]dto.ClamDTO, error)
GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error) GetCronJobs(req dto.CronJobReq) ([]dto.CronJobDTO, error)
GetAlertConfig() ([]model.AlertConfig, error) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate) error PageAlertConfig(req dto.AlertConfigPageReq) (int64, []model.AlertConfig, error)
UpdateAlertConfig(req dto.AlertConfigUpdate, operator string) error
DeleteAlertConfig(id uint) error DeleteAlertConfig(id uint) error
TestAlertConfig(req dto.AlertConfigTest) (bool, error) TestAlertConfig(req dto.AlertConfigTest) (bool, error)
} }
@@ -55,6 +66,9 @@ func (a AlertService) PageAlert(search dto.AlertSearch) (int64, []dto.AlertDTO,
opts []repo.DBOption opts []repo.DBOption
result []dto.AlertDTO result []dto.AlertDTO
) )
if global.CONF.Base.IsEnterprise {
opts = append(opts, alertRepo.WithByTypeNotIn(eeHiddenAlertTypes))
}
if search.Status != "" { if search.Status != "" {
opts = append(opts, repo.WithByStatus(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, Status: item.Status,
SendCount: item.SendCount, SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams, AdvancedParams: item.AdvancedParams,
CreateUser: item.CreateUser,
UpdateUser: item.UpdateUser,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt, UpdatedAt: item.UpdatedAt,
}) })
@@ -112,6 +128,8 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
Status: item.Status, Status: item.Status,
SendCount: item.SendCount, SendCount: item.SendCount,
AdvancedParams: item.AdvancedParams, AdvancedParams: item.AdvancedParams,
CreateUser: item.CreateUser,
UpdateUser: item.UpdateUser,
CreatedAt: item.CreatedAt, CreatedAt: item.CreatedAt,
UpdatedAt: item.UpdatedAt, UpdatedAt: item.UpdatedAt,
}) })
@@ -120,7 +138,10 @@ func (a AlertService) GetAlerts() ([]dto.AlertDTO, error) {
return result, err 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 alertID uint
var alertInfo model.Alert var alertInfo model.Alert
if create.Project != "" { if create.Project != "" {
@@ -137,7 +158,7 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
return buserr.WithErr("ErrStructTransform", err) return buserr.WithErr("ErrStructTransform", err)
} }
upAlert.ID = alertID upAlert.ID = alertID
err := a.UpdateAlert(upAlert) err := a.UpdateAlert(upAlert, operator)
if err != nil { if err != nil {
return err return err
} }
@@ -146,6 +167,8 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
if err := copier.Copy(&alertInfo, &create); err != nil { if err := copier.Copy(&alertInfo, &create); err != nil {
return buserr.WithErr("ErrStructTransform", err) return buserr.WithErr("ErrStructTransform", err)
} }
alertInfo.CreateUser = operator
alertInfo.UpdateUser = operator
if err := alertRepo.Create(&alertInfo); err != nil { if err := alertRepo.Create(&alertInfo); err != nil {
return err return err
@@ -156,7 +179,10 @@ func (a AlertService) CreateAlert(create dto.AlertCreate) error {
return nil 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 := make(map[string]interface{})
upMap["id"] = req.ID upMap["id"] = req.ID
@@ -169,6 +195,7 @@ func (a AlertService) UpdateAlert(req dto.AlertUpdate) error {
upMap["status"] = req.Status upMap["status"] = req.Status
upMap["send_count"] = req.SendCount upMap["send_count"] = req.SendCount
upMap["advanced_params"] = req.AdvancedParams upMap["advanced_params"] = req.AdvancedParams
upMap["update_user"] = operator
if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil { if err := alertRepo.Update(upMap, repo.WithByID(req.ID)); err != nil {
return err return err
@@ -308,12 +335,25 @@ func (a AlertService) GetDisks() ([]dto.DiskDTO, error) {
func executeDiskCommand() (string, error) { func executeDiskCommand() (string, error) {
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second)) 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 { if err != nil {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second)) 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 { 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 return cronJobs, err
} }
func (a AlertService) GetAlertConfig() ([]model.AlertConfig, error) { func (a AlertService) GetAlertConfig(req dto.AlertConfigQuery) ([]model.AlertConfig, error) {
var ( var (
opts []repo.DBOption opts []repo.DBOption
configs []model.AlertConfig configs []model.AlertConfig
) )
if len(req.ExcludeTypes) > 0 {
opts = append(opts, alertRepo.WithByTypeNotIn(req.ExcludeTypes))
}
opts = append(opts, repo.WithByStatus(constant.AlertEnable)) opts = append(opts, repo.WithByStatus(constant.AlertEnable))
configs, err := alertRepo.AlertConfigList(opts...) configs, err := alertRepo.AlertConfigList(opts...)
return configs, err 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 { if req.ID != 0 {
upMap := make(map[string]interface{}) upMap := make(map[string]interface{})
upMap["id"] = req.ID upMap["id"] = req.ID
@@ -459,6 +522,7 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
upMap["title"] = req.Title upMap["title"] = req.Title
upMap["status"] = req.Status upMap["status"] = req.Status
upMap["config"] = req.Config upMap["config"] = req.Config
upMap["update_user"] = operator
if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil { if err := alertRepo.UpdateAlertConfig(upMap, repo.WithByID(req.ID)); err != nil {
return err return err
} }
@@ -467,6 +531,8 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
if err := copier.Copy(&alertConfig, &req); err != nil { if err := copier.Copy(&alertConfig, &req); err != nil {
return buserr.WithErr("ErrStructTransform", err) return buserr.WithErr("ErrStructTransform", err)
} }
alertConfig.CreateUser = operator
alertConfig.UpdateUser = operator
if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil { if err := alertRepo.CreateAlertConfig(&alertConfig); err != nil {
return err return err
} }
@@ -475,7 +541,130 @@ func (a AlertService) UpdateAlertConfig(req dto.AlertConfigUpdate) error {
return nil 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 { 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)) return alertRepo.DeleteAlertConfig(repo.WithByID(id))
} }
@@ -501,14 +690,17 @@ func (a AlertService) TestAlertConfig(req dto.AlertConfigTest) (bool, error) {
Body: i18n.GetMsgByKey("TestAlert"), Body: i18n.GetMsgByKey("TestAlert"),
IsHTML: false, IsHTML: false,
} }
transport := xpack.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
if err := email.SendMail(cfg, msg, transport); err != nil { if err := email.SendMail(cfg, msg, transport); err != nil {
return false, err return false, err
} }
return true, nil 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{}) upMap := make(map[string]interface{})
var newStatus string var newStatus string
if updateAlert.SendCount == 0 { if updateAlert.SendCount == 0 {
@@ -552,7 +744,7 @@ func (a AlertService) ExternalUpdateAlert(updateAlert dto.AlertCreate) error {
} else { } else {
if updateAlert.Method != "" && updateAlert.Title != "" { if updateAlert.Method != "" && updateAlert.Title != "" {
updateAlert.Status = newStatus updateAlert.Status = newStatus
if err := a.CreateAlert(updateAlert); err != nil { if err := a.CreateAlert(updateAlert, operator); err != nil {
return err return err
} }
} }
+233 -104
View File
@@ -4,9 +4,11 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"math" "math"
"net"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
"sync"
"time" "time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
@@ -23,7 +25,7 @@ import (
"github.com/shirou/gopsutil/v4/disk" "github.com/shirou/gopsutil/v4/disk"
"github.com/shirou/gopsutil/v4/load" "github.com/shirou/gopsutil/v4/load"
"github.com/shirou/gopsutil/v4/mem" "github.com/shirou/gopsutil/v4/mem"
"github.com/shirou/gopsutil/v4/net" gnet "github.com/shirou/gopsutil/v4/net"
) )
const ( const (
@@ -34,7 +36,7 @@ const (
type AlertTaskHelper struct { type AlertTaskHelper struct {
DiskIO chan []disk.IOCountersStat DiskIO chan []disk.IOCountersStat
NetIO chan []net.IOCountersStat NetIO chan []gnet.IOCountersStat
} }
type IAlertTaskHelper interface { type IAlertTaskHelper interface {
@@ -46,6 +48,7 @@ type IAlertTaskHelper interface {
var cpuLoad1, cpuLoad5, cpuLoad15 []float64 var cpuLoad1, cpuLoad5, cpuLoad15 []float64
var memoryLoad1, memoryLoad5, memoryLoad15 []float64 var memoryLoad1, memoryLoad5, memoryLoad15 []float64
var alertTaskMu sync.Mutex
var baseTypes = map[string]bool{"ssl": true, "siteEndTime": true, "panelPwdEndTime": true, "panelUpdate": true} 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} 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 { func NewIAlertTaskHelper() IAlertTaskHelper {
return &AlertTaskHelper{ return &AlertTaskHelper{
DiskIO: make(chan []disk.IOCountersStat, 1), DiskIO: make(chan []disk.IOCountersStat, 1),
NetIO: make(chan []net.IOCountersStat, 1), NetIO: make(chan []gnet.IOCountersStat, 1),
} }
} }
func (m *AlertTaskHelper) StartTask() { func (m *AlertTaskHelper) StartTask() {
alertTaskMu.Lock()
defer alertTaskMu.Unlock()
m.startTaskLocked()
}
func (m *AlertTaskHelper) startTaskLocked() {
baseAlert, resourceAlert := m.getClassifiedAlerts() baseAlert, resourceAlert := m.getClassifiedAlerts()
if len(baseAlert) == 0 && len(resourceAlert) == 0 { if len(baseAlert) == 0 && len(resourceAlert) == 0 {
return return
} }
handleBaseAlerts(baseAlert) handleBaseAlertsLocked(baseAlert)
handleResourceAlerts(resourceAlert) handleResourceAlertsLocked(resourceAlert)
} }
func (m *AlertTaskHelper) StopTask() { func (m *AlertTaskHelper) StopTask() {
stopBaseJob() alertTaskMu.Lock()
stopResourceJob() defer alertTaskMu.Unlock()
stopBaseJobLocked()
stopResourceJobLocked()
} }
func (m *AlertTaskHelper) ResetTask() { func (m *AlertTaskHelper) ResetTask() {
m.StopTask() alertTaskMu.Lock()
m.StartTask() defer alertTaskMu.Unlock()
stopBaseJobLocked()
stopResourceJobLocked()
m.startTaskLocked()
} }
func (m *AlertTaskHelper) InitTask(alertType string) { func (m *AlertTaskHelper) InitTask(alertType string) {
alertTaskMu.Lock()
defer alertTaskMu.Unlock()
m.initTaskLocked(alertType)
}
func (m *AlertTaskHelper) initTaskLocked(alertType string) {
resetAlertState(alertType) resetAlertState(alertType)
if baseTypes[alertType] { if baseTypes[alertType] {
stopBaseJob() stopBaseJobLocked()
} else if resourceTypes[alertType] { } else if resourceTypes[alertType] {
stopResourceJob() stopResourceJobLocked()
} }
m.StartTask() m.startTaskLocked()
} }
func resetAlertState(alertType string) { func resetAlertState(alertType string) {
@@ -110,14 +130,14 @@ func (m *AlertTaskHelper) getClassifiedAlerts() (baseAlerts, resourceAlerts []dt
return return
} }
func handleBaseAlerts(baseAlerts []dto.AlertDTO) { func handleBaseAlertsLocked(baseAlerts []dto.AlertDTO) {
if len(baseAlerts) == 0 { if len(baseAlerts) == 0 {
stopBaseJob() stopBaseJobLocked()
return return
} }
if global.AlertBaseJobID == 0 { if global.AlertBaseJobID == 0 {
baseTask(baseAlerts) baseTask(baseAlerts)
jobID, err := global.Cron.AddFunc("*/30 * * * *", func() { jobID, err := global.Cron.AddFunc("@every 30m", func() {
baseTask(baseAlerts) baseTask(baseAlerts)
}) })
if err != nil { 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 { if len(resourceAlerts) == 0 {
stopResourceJob() stopResourceJobLocked()
return return
} }
if global.AlertResourceJobID == 0 { if global.AlertResourceJobID == 0 {
@@ -147,7 +167,7 @@ func handleResourceAlerts(resourceAlerts []dto.AlertDTO) {
} }
} }
func stopBaseJob() { func stopBaseJobLocked() {
if global.AlertBaseJobID != 0 { if global.AlertBaseJobID != 0 {
global.Cron.Remove(global.AlertBaseJobID) global.Cron.Remove(global.AlertBaseJobID)
global.AlertBaseJobID = 0 global.AlertBaseJobID = 0
@@ -155,7 +175,7 @@ func stopBaseJob() {
} }
} }
func stopResourceJob() { func stopResourceJobLocked() {
if global.AlertResourceJobID != 0 { if global.AlertResourceJobID != 0 {
global.Cron.Remove(global.AlertResourceJobID) global.Cron.Remove(global.AlertResourceJobID)
global.AlertResourceJobID = 0 global.AlertResourceJobID = 0
@@ -174,10 +194,16 @@ func baseTask(baseAlert []dto.AlertDTO) {
case "siteEndTime": case "siteEndTime":
loadWebsiteInfo(alert) loadWebsiteInfo(alert)
case "panelPwdEndTime": case "panelPwdEndTime":
if global.CONF.Base.IsEnterprise {
continue
}
if global.IsMaster { if global.IsMaster {
loadPanelPwd(alert) loadPanelPwd(alert)
} }
case "panelUpdate": case "panelUpdate":
if global.CONF.Base.IsEnterprise {
continue
}
if global.IsMaster { if global.IsMaster {
loadPanelUpdate(alert) loadPanelUpdate(alert)
} }
@@ -210,6 +236,9 @@ func resourceTask(resourceAlert []dto.AlertDTO) {
loadNodeException(alert) loadNodeException(alert)
} }
case "licenseException": case "licenseException":
if global.CONF.Base.IsEnterprise {
continue
}
if execute && global.IsMaster { if execute && global.IsMaster {
loadLicenseException(alert) loadLicenseException(alert)
} }
@@ -457,6 +486,7 @@ func loadPanelLogin(alert dto.AlertDTO) {
if err != nil { if err != nil {
global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err) global.LOG.Errorf("Failed to check recent failed ip login logs: %v", err)
} }
records = filterLoginLogsNotInWhitelist(records, whitelist)
if len(records) > 0 { if len(records) > 0 {
quota := strings.Join(func() []string { quota := strings.Join(func() []string {
var ips []string var ips []string
@@ -506,6 +536,7 @@ func loadSSHLogin(alert dto.AlertDTO) {
if err != nil { if err != nil {
global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err) global.LOG.Errorf("Failed to check recent failed ip ssh login logs: %v", err)
} }
records = filterSSHLoginEntriesNotInWhitelist(records, whitelist)
if len(records) > 0 { if len(records) > 0 {
quota := strings.Join(records, "\n") quota := strings.Join(records, "\n")
params := []dto.Param{ params := []dto.Param{
@@ -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) { func loadNodeException(alert dto.AlertDTO) {
// only master alert // only master alert
failCount, err := xpack.GetNodeErrorAlert() failCount, err := xpack.AlertProvider.GetNodeErrorAlert()
if err != nil { if err != nil {
global.LOG.Errorf("error getting node, err: %s", err) global.LOG.Errorf("error getting node, err: %s", err)
return return
@@ -555,7 +640,7 @@ func loadNodeException(alert dto.AlertDTO) {
func loadLicenseException(alert dto.AlertDTO) { func loadLicenseException(alert dto.AlertDTO) {
// only master alert // only master alert
failCount, err := xpack.GetLicenseErrorAlert() failCount, err := xpack.AlertProvider.GetLicenseErrorAlert()
if err != nil { if err != nil {
global.LOG.Errorf("error getting license, err: %s", err) global.LOG.Errorf("error getting license, err: %s", err)
return return
@@ -590,94 +675,138 @@ func sendAlerts(alert dto.AlertDTO, alertType, quota, quotaType string, params [
if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval { if newDate.IsZero() || calculateMinutesDifference(newDate) > ResourceAlertInterval {
for _, m := range methods { for _, m := range methods {
m = strings.TrimSpace(m) m = strings.TrimSpace(m)
switch m { if configId, err := strconv.ParseUint(m, 10, 64); err == nil {
case constant.SMS: sendAlertsByConfigId(alert, alertType, quota, quotaType, params, uint(configId))
if !alertUtil.CheckSMSSendLimit(constant.SMS) { } else {
continue sendAlertsByLegacyMethod(alert, alertType, quota, quotaType, params, m)
}
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)
case constant.Bark:
todayCount, isValid := canSendAlertToday(alertType, quotaType, alert.SendCount, m)
if !isValid {
continue
}
var create = dto.AlertLogCreate{
Type: alertUtil.GetCronJobType(alert.Type),
AlertId: alert.ID,
Count: todayCount + 1,
}
alertInfo := alert
alertInfo.Type = alertType
create.AlertRule = alertUtil.ProcessAlertRule(alert)
create.AlertDetail = alertUtil.ProcessAlertDetail(alertInfo, quotaType, params, m)
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
alertErr := alertUtil.CreateBarkAlertLog(create, alertInfo, params, transport, agentInfo)
if alertErr != nil {
global.LOG.Infof("%s alert %s push failed, err: %v", alertType, m, alertErr.Error())
continue
}
alertUtil.CreateNewAlertTask(quota, alertType, quotaType, m)
default:
} }
} }
} }
} }
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 {
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 { func getRepoOptionsByProject(project string) []repo.DBOption {
var opts []repo.DBOption var opts []repo.DBOption
+276 -124
View File
@@ -1,12 +1,16 @@
package service package service
import ( import (
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto" "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/constant"
"github.com/1Panel-dev/1Panel/agent/global" "github.com/1Panel-dev/1Panel/agent/global"
alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert" alertUtil "github.com/1Panel-dev/1Panel/agent/utils/alert"
"github.com/1Panel-dev/1Panel/agent/utils/xpack" "github.com/1Panel-dev/1Panel/agent/utils/xpack"
"strings"
) )
type AlertSender struct { type AlertSender struct {
@@ -22,115 +26,90 @@ func NewAlertSender(alert dto.AlertDTO, quotaType string) *AlertSender {
} }
func (s *AlertSender) Send(quota string, params []dto.Param) { func (s *AlertSender) Send(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",") s.sendByConfigIds(s.alert.Method, quota, params, false)
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.Bark:
s.sendBark(quota, params)
case constant.WeCom, constant.DingTalk, constant.FeiShu:
s.sendWebhook(quota, params, method)
}
}
} }
func (s *AlertSender) ResourceSend(quota string, params []dto.Param) { func (s *AlertSender) ResourceSend(quota string, params []dto.Param) {
methods := strings.Split(s.alert.Method, ",") s.sendByConfigIds(s.alert.Method, quota, params, true)
for _, method := range methods { }
method = strings.TrimSpace(method)
switch method { func (s *AlertSender) sendByConfigIds(methodStr string, quota string, params []dto.Param, isResource bool) {
case constant.SMS: alertRepo := repo.NewIAlertRepo()
s.sendResourceSMS(quota, params) configIds := strings.Split(methodStr, ",")
case constant.Email: for _, idStr := range configIds {
s.sendResourceEmail(quota, params) idStr = strings.TrimSpace(idStr)
case constant.Bark: configId, err := strconv.ParseUint(idStr, 10, 64)
s.sendResourceBark(quota, params) if err != nil {
case constant.WeCom, constant.DingTalk, constant.FeiShu: s.sendByLegacyMethod(idStr, quota, params, isResource)
s.sendResourceWebhook(quota, params, method) 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) { func (s *AlertSender) sendByLegacyMethod(method string, quota string, params []dto.Param, isResource bool) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) { alertRepo := repo.NewIAlertRepo()
return 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))
totalCount, isValid := s.canSendAlert(constant.SMS)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
}
err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS)
if err != nil { if err != nil {
global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err)
return return
} }
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS) if !alertUtil.IsAlertConfigEnabled(config) {
return
}
s.sendByConfig(config, quota, params, isResource)
} }
func (s *AlertSender) sendEmail(quota string, params []dto.Param) { func (s *AlertSender) sendSMSWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
totalCount, isValid := s.canSendAlert(constant.Email) if !alertUtil.IsAlertConfigEnabled(config) {
if !isValid { return
}
method := strconv.Itoa(int(config.ID))
if !alertUtil.CheckSMSSendLimit(config, method) {
return 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.Email),
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo)
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)
}
func (s *AlertSender) sendBark(quota string, params []dto.Param) {
totalCount, isValid := s.canSendAlert(constant.Bark)
if !isValid {
return
}
create := dto.AlertLogCreate{
Status: constant.AlertSuccess,
Count: totalCount + 1,
AlertId: s.alert.ID,
Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
}
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo()
err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo)
if err != nil {
global.LOG.Errorf("%s alert bark push failed: %v", s.alert.Type, err)
return
}
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark)
}
func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method string) {
totalCount, isValid := s.canSendAlert(method) totalCount, isValid := s.canSendAlert(method)
if !isValid { if !isValid {
return return
@@ -141,43 +120,60 @@ func (s *AlertSender) sendWebhook(quota string, params []dto.Param, method strin
Count: totalCount + 1, Count: totalCount + 1,
AlertId: s.alert.ID, AlertId: s.alert.ID,
Type: s.alert.Type, Type: s.alert.Type,
Method: method,
} }
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo() err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method)
err := xpack.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo)
if err != nil { if err != nil {
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err) global.LOG.Errorf("%s alert sms push failed: %v", s.alert.Type, err)
return return
} }
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method) alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method)
} }
func (s *AlertSender) sendResourceSMS(quota string, params []dto.Param) { func (s *AlertSender) sendSMS(quota string, params []dto.Param) {
if !alertUtil.CheckSMSSendLimit(constant.SMS) { alertRepo := repo.NewIAlertRepo()
config, err := alertRepo.GetConfig(alertRepo.WithByType(constant.SMS))
if err != nil {
return return
} }
s.sendSMSWithConfig(config, quota, params)
}
todayCount, isValid := s.canResourceSendAlert(constant.SMS) 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 { if !isValid {
return return
} }
create := dto.AlertLogCreate{ create := dto.AlertLogCreate{
Status: constant.AlertSuccess, Status: constant.AlertSuccess,
Count: todayCount + 1, Count: totalCount + 1,
AlertId: s.alert.ID, AlertId: s.alert.ID,
Type: s.alert.Type, Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
Method: strconv.Itoa(int(config.ID)),
} }
if err := xpack.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, constant.SMS); err != nil { transport := xpack.MultiNodeProvider.LoadRequestTransport()
global.LOG.Errorf("failed to send SMS alert: %v", err) 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 return
} }
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.SMS) alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, strconv.Itoa(int(config.ID)))
} }
func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) { func (s *AlertSender) sendResourceEmailWithConfig(config model.AlertConfig, quota string, params []dto.Param) {
todayCount, isValid := s.canResourceSendAlert(constant.Email) if !alertUtil.IsAlertConfigEnabled(config) {
return
}
todayCount, isValid := s.canResourceSendAlert(strconv.Itoa(int(config.ID)))
if !isValid { if !isValid {
return return
} }
@@ -189,19 +185,70 @@ func (s *AlertSender) sendResourceEmail(quota string, params []dto.Param) {
Type: s.alert.Type, Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert), AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email), AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Email),
Method: strconv.Itoa(int(config.ID)),
} }
transport := xpack.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo); err != nil { if err := alertUtil.CreateEmailAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil {
global.LOG.Errorf("failed to send Email alert: %v", err) global.LOG.Errorf("failed to send Email alert: %v", err)
return 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) sendResourceBark(quota string, params []dto.Param) { func (s *AlertSender) sendEmail(quota string, params []dto.Param) {
todayCount, isValid := s.canResourceSendAlert(constant.Bark) 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 { if !isValid {
return return
} }
@@ -213,18 +260,114 @@ func (s *AlertSender) sendResourceBark(quota string, params []dto.Param) {
Type: s.alert.Type, Type: s.alert.Type,
AlertRule: alertUtil.ProcessAlertRule(s.alert), AlertRule: alertUtil.ProcessAlertRule(s.alert),
AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark), AlertDetail: alertUtil.ProcessAlertDetail(s.alert, quota, params, constant.Bark),
Method: strconv.Itoa(int(config.ID)),
} }
transport := xpack.LoadRequestTransport() transport := xpack.MultiNodeProvider.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo() agentInfo, _ := xpack.MultiNodeProvider.GetAgentInfo()
if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo); err != nil { if err := alertUtil.CreateBarkAlertLog(create, s.alert, params, transport, agentInfo, config); err != nil {
global.LOG.Errorf("failed to send Bark alert: %v", err) global.LOG.Errorf("failed to send Bark alert: %v", err)
return return
} }
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, constant.Bark) 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) { 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) todayCount, isValid := s.canResourceSendAlert(method)
if !isValid { if !isValid {
return return
@@ -235,16 +378,25 @@ func (s *AlertSender) sendResourceWebhook(quota string, params []dto.Param, meth
Count: todayCount + 1, Count: todayCount + 1,
AlertId: s.alert.ID, AlertId: s.alert.ID,
Type: s.alert.Type, Type: s.alert.Type,
Method: method,
} }
transport := xpack.LoadRequestTransport()
agentInfo, _ := xpack.GetAgentInfo() if err := xpack.AlertProvider.CreateSMSAlertLog(s.alert.Type, s.alert, create, quota, params, config, method); err != nil {
if err := xpack.CreateWebhookAlertLog(s.alert.Type, s.alert, create, quota, params, method, transport, agentInfo); err != nil { global.LOG.Errorf("failed to send SMS alert: %v", err)
global.LOG.Errorf("%s alert %s webhook push failed: %v", s.alert.Type, method, err)
return return
} }
alertUtil.CreateNewAlertTask(quota, s.alert.Type, s.quotaType, method) 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) { func (s *AlertSender) canSendAlert(method string) (uint, bool) {
todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method) todayCount, totalCount, err := alertRepo.LoadTaskCount(s.alert.Type, s.quotaType, method)
if err != nil { if err != nil {
+3 -3
View File
@@ -126,7 +126,7 @@ func (a AppService) PageApp(ctx *gin.Context, req request.AppSearch) (*response.
lang := strings.ToLower(common.GetLang(ctx)) lang := strings.ToLower(common.GetLang(ctx))
for _, ap := range apps { for _, ap := range apps {
if req.Type == "php" { 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 continue
} }
} }
@@ -896,7 +896,7 @@ func getAppFromRepo(downloadPath string) error {
return err 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 return err
} }
defer func() { defer func() {
@@ -966,7 +966,7 @@ func deleteCustomApp() {
} }
func (a AppService) SyncAppListFromRemote(taskID string) (err error) { func (a AppService) SyncAppListFromRemote(taskID string) (err error) {
if xpack.IsUseCustomApp() { if xpack.MultiNodeProvider.IsUseCustomApp() {
return nil return nil
} }
+7 -1
View File
@@ -294,6 +294,7 @@ func (a *AppInstallService) Operate(req request.AppInstalledOperate) error {
DetailID: req.DetailId, DetailID: req.DetailId,
Backup: req.Backup, Backup: req.Backup,
PullImage: req.PullImage, PullImage: req.PullImage,
DeleteImage: req.DeleteImage,
DockerCompose: req.DockerCompose, DockerCompose: req.DockerCompose,
TaskID: req.TaskID, TaskID: req.TaskID,
} }
@@ -589,7 +590,12 @@ func (a *AppInstallService) GetUpdateVersions(req request.AppUpdateVersion) ([]d
if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate { if common.IsCrossVersion(install.Version, detail.Version) && !app.CrossVersionUpdate {
continue 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 var newCompose string
if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() { if req.UpdateVersion != "" && req.UpdateVersion == detail.Version && detail.DockerCompose == "" && !app.IsLocalApp() {
filename := filepath.Base(detail.DownloadUrl) filename := filepath.Base(detail.DownloadUrl)
+4 -4
View File
@@ -82,7 +82,7 @@ func (a AppService) createSyncAppStoreTask(sharedCtx **appSyncContext) func(t *t
ctx := &appSyncContext{ ctx := &appSyncContext{
task: t, 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), baseRemoteUrl: fmt.Sprintf("%s/%s/1panel", global.AppRepoURL(), global.CONF.Base.Mode),
systemVersion: setting.SystemVersion, systemVersion: setting.SystemVersion,
settingService: settingService, settingService: settingService,
@@ -279,9 +279,9 @@ func (c *appSyncContext) syncAppIconsAndDetails() error {
}() }()
var ( var (
completed int completed int
icon200Count int icon200Count int
icon304Count int icon304Count int
iconFailCount int iconFailCount int
) )
milestones := [4]int{totalWork / 4, totalWork / 2, totalWork * 3 / 4, totalWork} milestones := [4]int{totalWork / 4, totalWork / 2, totalWork * 3 / 4, totalWork}
+231 -63
View File
@@ -89,7 +89,15 @@ func checkPort(key string, params map[string]interface{}) (int, error) {
return 0, nil return 0, nil
} }
func isPortInUse(port int, protocol string) bool {
return common.ScanPortWithProto(port, normalizeComposeProtocol(protocol))
}
func checkPortExist(port int) error { func checkPortExist(port int) error {
return checkPortExistWithProtocol(port, "")
}
func checkPortExistWithProtocol(port int, protocol string) error {
errMap := make(map[string]interface{}) errMap := make(map[string]interface{})
errMap["port"] = port errMap["port"] = port
appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port)) appInstall, _ := appInstallRepo.GetFirst(appInstallRepo.WithPort(port))
@@ -110,7 +118,7 @@ func checkPortExist(port int) error {
errMap["name"] = domain.Domain errMap["name"] = domain.Domain
return buserr.WithMap("ErrPortExist", errMap, nil) return buserr.WithMap("ErrPortExist", errMap, nil)
} }
if common.ScanPort(port) { if isPortInUse(port, protocol) {
return buserr.WithDetail("ErrPortInUsed", port, nil) return buserr.WithDetail("ErrPortInUsed", port, nil)
} }
return nil return nil
@@ -357,33 +365,13 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return err return err
} }
if deleteReq.DeleteImage { if deleteReq.DeleteImage {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
content, err := op.GetContent(install.GetEnvPath()) content, err := op.GetContent(install.GetEnvPath())
if err != nil { if err != nil {
return err return err
} }
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose)) if err = deleteAppImagesByCompose(t, content, []byte(install.DockerCompose), nil); err != nil {
if err != nil {
return err return err
} }
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil {
imgStr := delImageStr + image
t.Log(imgStr)
if err = client.DeleteImage(imageID); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(delImageStr + image)
}
}
} }
} }
tx, ctx := helper.GetTxAndContext() tx, ctx := helper.GetTxAndContext()
@@ -468,6 +456,69 @@ func deleteAppInstall(deleteReq request.AppInstallDelete) error {
return nil return nil
} }
type appImageID struct {
name string
id string
}
func getAppImageIDsByCompose(client docker.Client, envContent, composeContent []byte) ([]appImageID, error) {
images, err := docker.GetImagesFromDockerCompose(envContent, composeContent)
if err != nil {
return nil, err
}
imageIDs := make([]appImageID, 0, len(images))
for _, image := range images {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
imageIDs = append(imageIDs, appImageID{name: image, id: imageID})
}
}
return imageIDs, nil
}
func deleteAppImagesByCompose(t *task.Task, envContent, composeContent []byte, excludeImages []string) error {
client, err := docker.NewClient()
if err != nil {
return err
}
defer client.Close()
imageIDs, err := getAppImageIDsByCompose(client, envContent, composeContent)
if err != nil {
return err
}
return deleteAppImagesByIDs(t, client, imageIDs, excludeImages)
}
func deleteAppImagesByIDs(t *task.Task, client docker.Client, imageIDs []appImageID, excludeImages []string) error {
delImageStr := i18n.GetMsgByKey("TaskDelete") + i18n.GetMsgByKey("Image")
excludeImageIDs := make(map[string]struct{}, len(excludeImages))
for _, image := range excludeImages {
imageID, err := client.GetImageIDByName(image)
if err == nil && imageID != "" {
excludeImageIDs[imageID] = struct{}{}
}
}
deletedImageIDs := make(map[string]struct{}, len(imageIDs))
for _, image := range imageIDs {
if _, ok := excludeImageIDs[image.id]; ok {
continue
}
if _, ok := deletedImageIDs[image.id]; ok {
continue
}
deletedImageIDs[image.id] = struct{}{}
imgStr := delImageStr + image.name
t.Log(imgStr)
if err := client.DeleteImage(image.id); err != nil {
t.LogFailedWithErr(imgStr, err)
continue
}
t.LogSuccess(imgStr)
}
return nil
}
func deleteLink(del dto.DelAppLink) error { func deleteLink(del dto.DelAppLink) error {
install := del.Install install := del.Install
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(install.ID))
@@ -665,7 +716,7 @@ func buildNginx(parentTask *task.Task) error {
logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image")) logStr := fmt.Sprintf("%s %s", i18n.GetMsgByKey("TaskBuild"), i18n.GetMsgByKey("Image"))
parentTask.LogStart(logStr) parentTask.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTask(*parentTask), cmd.WithTimeout(60*time.Minute)) 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 return err
} }
parentTask.LogSuccess(logStr) parentTask.LogSuccess(logStr)
@@ -682,6 +733,9 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err 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 { if install.Version == detail.Version {
return errors.New("two version is same") return errors.New("two version is same")
} }
@@ -748,28 +802,23 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
if err != nil { if err != nil {
return err return err
} }
dockerCLi, _ := docker.NewClient() oldEnvContent := append([]byte(nil), content...)
if req.PullImage { oldDockerCompose := install.DockerCompose
composeContent := []byte(detail.DockerCompose) if install.App.Key == vllmAppKeyForUpgrade {
if req.DockerCompose != "" { envs := make(map[string]interface{})
composeContent = []byte(req.DockerCompose) if err = json.Unmarshal([]byte(install.Env), &envs); err != nil {
return err
} }
images, err := docker.GetImagesFromDockerCompose(content, composeContent) image := buildVllmUpgradeImage(loadVllmImageFromEnv(install.Env), oldVersion, detail.Version)
envs[vllmImageEnvKey] = image
paramByte, err := json.Marshal(envs)
if err != nil { if err != nil {
return err return err
} }
for _, image := range images { install.Env = string(paramByte)
t.Log(i18n.GetWithName("PullImageStart", image)) content = setVllmImageInEnvContent(content, image)
if err = dockerCLi.PullImageWithProcess(t, image); err != nil {
err = buserr.WithNameAndErr("ErrDockerPullImage", "", err)
return err
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
} }
_ = copyAppDetailMissing(fileOp, detailDir, install.GetPath())
command := exec.Command("/bin/bash", "-c", fmt.Sprintf("cp -rn %s/* %s || true", detailDir, install.GetPath()))
_, _ = command.CombinedOutput()
if install.App.Key == constant.AppOpenresty { if install.App.Key == constant.AppOpenresty {
installBuildDir := path.Join(install.GetPath(), "build") installBuildDir := path.Join(install.GetPath(), "build")
detailBuildDir := path.Join(detailDir, "build") detailBuildDir := path.Join(detailDir, "build")
@@ -808,9 +857,13 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
return err return err
} }
if req.DockerCompose == "" { if req.DockerCompose == "" {
newCompose, err = getUpgradeCompose(install, detail) if install.App.Key == vllmAppKeyForUpgrade {
if err != nil { newCompose = install.DockerCompose
return err } else {
newCompose, err = getUpgradeCompose(install, detail)
if err != nil {
return err
}
} }
} else { } else {
newCompose = req.DockerCompose newCompose = req.DockerCompose
@@ -820,6 +873,46 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
install.Version = detail.Version install.Version = detail.Version
install.AppDetailId = req.DetailID install.AppDetailId = req.DetailID
var oldImageIDs []appImageID
if req.DeleteImage {
dockerCLi, err := docker.NewClient()
if err != nil {
return err
}
oldImageIDs, err = getAppImageIDsByCompose(dockerCLi, oldEnvContent, []byte(oldDockerCompose))
dockerCLi.Close()
if err != nil {
return err
}
}
if req.PullImage {
images, err := docker.GetImagesFromDockerCompose(content, []byte(install.DockerCompose))
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 pullErr := dockerCLi.PullImageWithProcess(t, image); pullErr != nil {
if exist, _ := dockerCLi.ImageExists(image); exist {
t.Log(i18n.GetMsgByKey("UseExistImage"))
continue
}
return buserr.WithNameAndErr("ErrDockerPullImage", "", pullErr)
}
exist, err := dockerCLi.ImageExists(image)
if err != nil || !exist {
return buserr.WithNameAndErr("ErrDockerPullImage", "", fmt.Errorf("image %s does not exist after pull: %v", image, err))
}
t.LogSuccess(i18n.GetMsgByKey("PullImage"))
}
}
if out, err := compose.Down(install.GetComposePath()); err != nil { if out, err := compose.Down(install.GetComposePath()); err != nil {
if out != "" { if out != "" {
upErr = errors.New(out) upErr = errors.New(out)
@@ -870,7 +963,31 @@ func upgradeInstall(req request.AppInstallUpgrade) error {
} }
t.LogSuccess(logStr) t.LogSuccess(logStr)
install.Status = constant.StatusRunning install.Status = constant.StatusRunning
return appInstallRepo.Save(context.Background(), &install) if err = appInstallRepo.Save(context.Background(), &install); err != nil {
return err
}
if req.DeleteImage {
newEnvContent, err := fileOp.GetContent(install.GetEnvPath())
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return nil
}
excludeImages, err := docker.GetImagesFromDockerCompose(newEnvContent, []byte(install.DockerCompose))
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return nil
}
dockerCLi, err := docker.NewClient()
if err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
return nil
}
defer dockerCLi.Close()
if err = deleteAppImagesByIDs(t, dockerCLi, oldImageIDs, excludeImages); err != nil {
t.LogFailedWithErr(i18n.GetMsgByKey("TaskDelete")+i18n.GetMsgByKey("Image"), err)
}
}
return nil
} }
rollBackApp := func(t *task.Task) { rollBackApp := func(t *task.Task) {
@@ -885,7 +1002,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() { go func() {
err = upgradeTask.Execute() err = upgradeTask.Execute()
@@ -1034,7 +1151,7 @@ func downloadApp(app model.App, appDetail model.AppDetail, appInstall *model.App
} }
return 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 { if logger == nil {
global.LOG.Errorf("decompress app[%s] error %v", app.Name, err) global.LOG.Errorf("decompress app[%s] error %v", app.Name, err)
} else { } else {
@@ -1129,7 +1246,7 @@ func runScript(task *task.Task, appInstall *model.AppInstall, operate string) er
task.LogStart(logStr) task.LogStart(logStr)
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute), cmd.WithWorkDir(workDir)) 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) task.LogFailedWithErr(logStr, err)
return err return err
} }
@@ -1178,11 +1295,12 @@ func upApp(task *task.Task, appInstall *model.AppInstall, pullImages bool) error
if err != nil { if err != nil {
return err return err
} }
imagePrefix := xpack.GetImagePrefix() imagePrefix := xpack.MultiNodeProvider.GetImagePrefix()
dockerCLi, err := docker.NewClient() dockerCLi, err := docker.NewClient()
if err != nil { if err != nil {
return err return err
} }
defer dockerCLi.Close()
for _, image := range images { for _, image := range images {
if imagePrefix != "" { if imagePrefix != "" {
lastSlashIndex := strings.LastIndex(image, "/") lastSlashIndex := strings.LastIndex(image, "/")
@@ -1596,6 +1714,9 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
synAppInstall(containersMap, &installed, false) synAppInstall(containersMap, &installed, false)
} }
resourceKeys := getAppInstallResourceKeys(installed.ID)
envMap := make(map[string]interface{})
_ = json.Unmarshal([]byte(installed.Env), &envMap)
installDTO := response.AppInstallDTO{ installDTO := response.AppInstallDTO{
ID: installed.ID, ID: installed.ID,
Name: installed.Name, Name: installed.Name,
@@ -1617,14 +1738,16 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
Website: installed.App.Website, Website: installed.App.Website,
Document: installed.App.Document, Document: installed.App.Document,
}, },
Favorite: installed.Favorite, Favorite: installed.Favorite,
SortOrder: installed.SortOrder, SortOrder: installed.SortOrder,
Container: installed.ContainerName, Container: installed.ContainerName,
ServiceName: strings.ToLower(installed.ServiceName), ServiceName: strings.ToLower(installed.ServiceName),
ResourceKeys: resourceKeys,
Env: envMap,
} }
if !updated && !checkUpdate { if !updated && !checkUpdate {
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
continue continue
} }
@@ -1632,7 +1755,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if installed.Version == "latest" { if installed.Version == "latest" {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1661,7 +1784,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
if len(versions) == 0 { if len(versions) == 0 {
if checkUpdate { if checkUpdate {
installDTO.CanUpdate = false installDTO.CanUpdate = false
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
} }
continue continue
@@ -1693,7 +1816,7 @@ func handleInstalled(appInstallList []model.AppInstall, updated, sync, checkUpda
res = append(res, installDTO) res = append(res, installDTO)
} }
} else if checkUpdate { } else if checkUpdate {
installDTO.LinkDB = hasLinkDB(installed.ID) installDTO.LinkDB = hasLinkDBFromKeys(resourceKeys)
res = append(res, installDTO) res = append(res, installDTO)
} }
} }
@@ -1778,7 +1901,7 @@ func addDockerComposeCommonParam(composeMap map[string]interface{}, serviceName
if !serviceValid { if !serviceValid {
return buserr.New("ErrFileParse") return buserr.New("ErrFileParse")
} }
imagePreFix := xpack.GetImagePrefix() imagePreFix := xpack.MultiNodeProvider.GetImagePrefix()
if imagePreFix != "" { if imagePreFix != "" {
for _, service := range services { for _, service := range services {
serviceValue := service.(map[string]interface{}) serviceValue := service.(map[string]interface{})
@@ -1935,6 +2058,39 @@ func isHostModel(dockerCompose string) bool {
return false 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 { func getRestartPolicy(yml string) string {
var project docker.ComposeProject var project docker.ComposeProject
if err := yaml.Unmarshal([]byte(yml), &project); err != nil { if err := yaml.Unmarshal([]byte(yml), &project); err != nil {
@@ -2121,7 +2277,7 @@ func handleSSLConfig(appInstall *model.AppInstall, hasDefaultWebsite bool, sslRe
caRequest := request.WebsiteCAObtain{ caRequest := request.WebsiteCAObtain{
ID: ca.ID, ID: ca.ID,
Domains: "localhost", Domains: "localhost",
KeyType: "4096", KeyType: "RSA4096",
Time: 99, Time: 99,
Unit: "year", Unit: "year",
Dir: sslDir, Dir: sslDir,
@@ -2227,13 +2383,25 @@ func needsUpdate(localTag *model.Tag, remoteTag dto.Tag, translations string) bo
} }
func hasLinkDB(installID uint) bool { func hasLinkDB(installID uint) bool {
return hasLinkDBFromKeys(getAppInstallResourceKeys(installID))
}
func getAppInstallResourceKeys(installID uint) []string {
resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID)) resources, _ := appInstallResourceRepo.GetBy(appInstallResourceRepo.WithAppInstallId(installID))
keys := make([]string, 0, len(resources))
for _, resource := range resources {
keys = append(keys, resource.Key)
}
return keys
}
func hasLinkDBFromKeys(resourceKeys []string) bool {
hasDB := false hasDB := false
if len(resources) > 0 { if len(resourceKeys) > 0 {
for _, resource := range resources { for _, resourceKey := range resourceKeys {
if resource.Key == constant.AppPostgres || resource.Key == constant.AppMysql || if resourceKey == constant.AppPostgres || resourceKey == constant.AppMysql ||
resource.Key == constant.AppMariaDB || resource.Key == constant.AppMysqlCluster || resourceKey == constant.AppMariaDB || resourceKey == constant.AppMysqlCluster ||
resource.Key == constant.AppPostgresql || resource.Key == constant.AppPostgresqlCluster { resourceKey == constant.AppPostgresql || resourceKey == constant.AppPostgresqlCluster {
hasDB = true hasDB = true
break break
} }
+22 -3
View File
@@ -63,11 +63,30 @@ func (u *BackupService) AppBackup(req dto.CommonBackup) (*model.BackupRecord, er
return nil, err return nil, err
} }
if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil { if !req.IsImmediate {
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err) if err = handleAppBackup(&install, nil, record.ID, backupDir, fileName, "", req.Secret, req.TaskID); err != nil {
return nil, err backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
global.LOG.Errorf("backup app %s failed, err: %v", req.DetailName, err)
return nil, err
}
return record, nil
} }
backupTask, err := task.NewTaskWithOps(install.Name, task.TaskBackup, task.TaskScopeBackup, req.TaskID, install.ID)
if err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
if err = doAppBackup(&install, backupTask, backupDir, fileName, "", req.Secret); err != nil {
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusFailed, "message": err.Error()})
record.Status = constant.StatusFailed
record.Message = err.Error()
return nil, err
}
backupRepo.UpdateRecordByMap(record.ID, map[string]interface{}{"status": constant.StatusSuccess})
record.Status = constant.StatusSuccess
return record, nil return record, nil
} }
+57 -25
View File
@@ -5,6 +5,7 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"io/fs" "io/fs"
"net/netip"
"os" "os"
"path" "path"
"sort" "sort"
@@ -595,35 +596,22 @@ func stepRecreateContainer(recoverCtx *containerRecoverContext, taskItem *task.T
return err return err
} }
networkConfig, extraNetworks := buildContainerRecoverNetworkConfig(recoverCtx.inspectInfo.NetworkSettings, hostConfig) createRes, err := createContainerWithOldNetworks(ctx, recoverCtx.client, config, hostConfig, recoverCtx.inspectInfo.NetworkSettings, recoverCtx.targetName)
removeBridgeDriverIPAM(recoverCtx.client, networkConfig, extraNetworks)
createRes, err := recoverCtx.client.ContainerCreate(ctx, config, hostConfig, networkConfig, nil, recoverCtx.targetName)
if err != nil { if err != nil {
return err return err
} }
recoverCtx.createdContainerID = createRes.ID 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 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 { 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 { for netName, endpoint := range endpoints {
if endpoint == nil || endpoint.IPAMConfig == nil { if endpoint == nil || endpoint.IPAMConfig == nil {
continue continue
@@ -632,10 +620,59 @@ func removeBridgeDriverIPAMFromEndpoints(cli *client.Client, endpoints map[strin
if err != nil { if err != nil {
continue continue
} }
if info.Driver == "bridge" { removeUnsupportedEndpointStaticIP(netName, info, endpoint)
endpoint.IPAMConfig = nil }
}
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 { func ensureContainerRecoverNetworks(recoverCtx *containerRecoverContext) error {
@@ -745,11 +782,6 @@ func buildContainerRecoverNetworkConfig(networkSettings *container.NetworkSettin
IPv4Address: endpoint.IPAMConfig.IPv4Address, IPv4Address: endpoint.IPAMConfig.IPv4Address,
IPv6Address: endpoint.IPAMConfig.IPv6Address, IPv6Address: endpoint.IPAMConfig.IPv6Address,
} }
} else if endpoint.IPAddress != "" || endpoint.GlobalIPv6Address != "" {
endpointSetting.IPAMConfig = &network.EndpointIPAMConfig{
IPv4Address: endpoint.IPAddress,
IPv6Address: endpoint.GlobalIPv6Address,
}
} }
if name == primaryName { if name == primaryName {
config.EndpointsConfig[name] = endpointSetting config.EndpointsConfig[name] = endpointSetting
+2 -1
View File
@@ -1,6 +1,7 @@
package service package service
import ( import (
"context"
"fmt" "fmt"
"os" "os"
"path" "path"
@@ -263,7 +264,7 @@ func loadSqlFile(file string) (string, error) {
_ = os.RemoveAll(dstDir) _ = os.RemoveAll(dstDir)
return "", err return "", err
} }
if err := archiver.Extract(file, dstDir, ""); err != nil { if err := archiver.Extract(context.Background(), file, dstDir, ""); err != nil {
_ = os.RemoveAll(dstDir) _ = os.RemoveAll(dstDir)
return "", err 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 return err
} }
@@ -109,13 +110,13 @@ func handleRedisBackup(redisInfo *repo.RootInfo, parentTask *task.Task, recordID
return nil return nil
} }
if strings.HasSuffix(fileName, ".aof") { 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 err
} }
return nil 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 err
} }
return nil return nil
+1 -1
View File
@@ -178,7 +178,7 @@ func handleWebsiteRecover(website *model.Website, parentTask *task.Task, recover
if err = fileOp.TarGzExtractPro(fmt.Sprintf("%s/%s.web.tar.gz", tmpPath, website.Alias), GetOpenrestyDir(SitesRootDir), ""); err != nil { if err = fileOp.TarGzExtractPro(fmt.Sprintf("%s/%s.web.tar.gz", tmpPath, website.Alias), GetOpenrestyDir(SitesRootDir), ""); err != nil {
return err return err
} }
if err := cmd.RunDefaultBashCf("docker exec -i %s nginx -s reload", nginxInfo.ContainerName); err != nil { if err := cmd.NewCommandMgr().Run("docker", "exec", "-i", nginxInfo.ContainerName, "nginx", "-s", "reload"); err != nil {
return err return err
} }
oldWebsite.ID = website.ID oldWebsite.ID = website.ID
+13 -12
View File
@@ -36,8 +36,8 @@ type IClamService interface {
LoadBaseInfo() (dto.ClamBaseInfo, error) LoadBaseInfo() (dto.ClamBaseInfo, error)
Operate(operate string) error Operate(operate string) error
SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error) SearchWithPage(search dto.SearchClamWithPage) (int64, interface{}, error)
Create(req dto.ClamCreate) error Create(req dto.ClamCreate, operator string) error
Update(req dto.ClamUpdate) error Update(req dto.ClamUpdate, operator string) error
UpdateStatus(id uint, status string) error UpdateStatus(id uint, status string) error
Delete(req dto.ClamDelete) error Delete(req dto.ClamDelete) error
HandleOnce(id uint) error HandleOnce(id uint) error
@@ -86,8 +86,9 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
baseInfo.IsActive = false baseInfo.IsActive = false
} }
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
if baseInfo.IsActive { if baseInfo.IsActive {
version, err := cmd.RunDefaultWithStdoutBashC("clamdscan --version") version, err := cmdMgr.RunWithStdout("clamdscan", "--version")
if err == nil { if err == nil {
if strings.Contains(version, "/") { if strings.Contains(version, "/") {
baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") baseInfo.Version = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
@@ -99,7 +100,7 @@ func (c *ClamService) LoadBaseInfo() (dto.ClamBaseInfo, error) {
_ = clam.CheckWithStopAll(false, clamRepo) _ = clam.CheckWithStopAll(false, clamRepo)
} }
if baseInfo.FreshIsActive { if baseInfo.FreshIsActive {
version, err := cmd.RunDefaultWithStdoutBashC("freshclam --version") version, err := cmdMgr.RunWithStdout("freshclam", "--version")
if err == nil { if err == nil {
if strings.Contains(version, "/") { if strings.Contains(version, "/") {
baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ") baseInfo.FreshVersion = strings.TrimPrefix(strings.Split(version, "/")[0], "ClamAV ")
@@ -164,7 +165,7 @@ func (c *ClamService) SearchWithPage(req dto.SearchClamWithPage) (int64, interfa
return total, datas, err return total, datas, err
} }
func (c *ClamService) Create(req dto.ClamCreate) error { func (c *ClamService) Create(req dto.ClamCreate, operator string) error {
clam, _ := clamRepo.Get(repo.WithByName(req.Name)) clam, _ := clamRepo.Get(repo.WithByName(req.Name))
if clam.ID != 0 { if clam.ID != 0 {
return buserr.New("ErrRecordExist") return buserr.New("ErrRecordExist")
@@ -179,7 +180,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
clam.InfectedDir = "" clam.InfectedDir = ""
} }
if len(req.Spec) != 0 { if len(req.Spec) != 0 {
entryID, err := xpack.StartClam(&clam, false) entryID, err := xpack.MultiNodeProvider.StartClam(&clam, false)
if err != nil { if err != nil {
return err return err
} }
@@ -198,7 +199,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
Project: strconv.Itoa(int(clam.ID)), Project: strconv.Itoa(int(clam.ID)),
Status: constant.AlertEnable, Status: constant.AlertEnable,
} }
err := NewIAlertService().CreateAlert(createAlert) err := NewIAlertService().CreateAlert(createAlert, operator)
if err != nil { if err != nil {
return err return err
} }
@@ -206,7 +207,7 @@ func (c *ClamService) Create(req dto.ClamCreate) error {
return nil return nil
} }
func (c *ClamService) Update(req dto.ClamUpdate) error { func (c *ClamService) Update(req dto.ClamUpdate, operator string) error {
if cmd.CheckIllegal(req.Path) { if cmd.CheckIllegal(req.Path) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
@@ -232,7 +233,7 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
upMap["entry_id"] = 0 upMap["entry_id"] = 0
} }
if len(req.Spec) != 0 && clam.Status != constant.StatusDisable { if len(req.Spec) != 0 && clam.Status != constant.StatusDisable {
newEntryID, err := xpack.StartClam(&clamItem, true) newEntryID, err := xpack.MultiNodeProvider.StartClam(&clamItem, true)
if err != nil { if err != nil {
return err return err
} }
@@ -259,7 +260,7 @@ func (c *ClamService) Update(req dto.ClamUpdate) error {
Type: "clams", Type: "clams",
Project: strconv.Itoa(int(clam.ID)), Project: strconv.Itoa(int(clam.ID)),
} }
err := NewIAlertService().ExternalUpdateAlert(updateAlert) err := NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil { if err != nil {
return err return err
} }
@@ -276,7 +277,7 @@ func (c *ClamService) UpdateStatus(id uint, status string) error {
err error err error
) )
if status == constant.StatusEnable { if status == constant.StatusEnable {
entryID, err = xpack.StartClam(&clam, true) entryID, err = xpack.MultiNodeProvider.StartClam(&clam, true)
if err != nil { if err != nil {
return err return err
} }
@@ -493,7 +494,7 @@ func (c *ClamService) loadConfigPath(confType string) string {
func handleAlert(infectedFiles, clamName string, clamId uint) { func handleAlert(infectedFiles, clamName string, clamId uint) {
itemInfected, _ := strconv.Atoi(strings.TrimSpace(infectedFiles)) itemInfected, _ := strconv.Atoi(strings.TrimSpace(infectedFiles))
if itemInfected < 0 { if itemInfected <= 0 {
return return
} }
pushAlert := dto.PushAlert{ pushAlert := dto.PushAlert{
+95 -55
View File
@@ -15,6 +15,7 @@ import (
"os/exec" "os/exec"
"path" "path"
"path/filepath" "path/filepath"
"regexp"
"sort" "sort"
"strconv" "strconv"
"strings" "strings"
@@ -32,7 +33,6 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/common"
"github.com/1Panel-dev/1Panel/agent/utils/docker" "github.com/1Panel-dev/1Panel/agent/utils/docker"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/docker/docker/api/types" "github.com/docker/docker/api/types"
"github.com/docker/docker/api/types/build" "github.com/docker/docker/api/types/build"
"github.com/docker/docker/api/types/container" "github.com/docker/docker/api/types/container"
@@ -53,6 +53,8 @@ import (
type ContainerService struct{} type ContainerService struct{}
var containerLogAnsiRegex = regexp.MustCompile("\x1b\\[[0-9;?]*[A-Za-z]|\x1b=|\x1b>")
type IContainerService interface { type IContainerService interface {
Page(req dto.PageContainer) (int64, interface{}, error) Page(req dto.PageContainer) (int64, interface{}, error)
List() []dto.ContainerOptions List() []dto.ContainerOptions
@@ -297,14 +299,14 @@ func (u *ContainerService) ContainerListStats() ([]dto.ContainerListStats, error
if err != nil { if err != nil {
return nil, err return nil, err
} }
var datas []dto.ContainerListStats datas := make([]dto.ContainerListStats, len(list))
var wg sync.WaitGroup var wg sync.WaitGroup
wg.Add(len(list)) wg.Add(len(list))
for i := 0; i < len(list); i++ { for i := 0; i < len(list); i++ {
go func(item container.Summary) { go func(index int, item container.Summary) {
datas = append(datas, loadCpuAndMem(client, item.ID)) datas[index] = loadCpuAndMem(client, item.ID)
wg.Done() wg.Done()
}(list[i]) }(i, list[i])
} }
wg.Wait() wg.Wait()
return datas, nil return datas, nil
@@ -527,6 +529,7 @@ func (u *ContainerService) ContainerCreate(req dto.ContainerOperate, inThread bo
if err != nil { if err != nil {
return err return err
} }
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed")) taskItem.Log(i18n.GetMsgByKey("ContainerCreateFailed"))
@@ -675,6 +678,7 @@ func (u *ContainerService) ContainerUpdate(req dto.ContainerOperate) error {
reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) reCreateAfterUpdate(req.Name, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
return err return err
} }
removeUnsupportedEndpointStaticIPAM(client, networkConf, nil)
con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name) con, err := client.ContainerCreate(ctx, config, hostConf, networkConf, &v1.Platform{}, req.Name)
if err != nil { if err != nil {
@@ -738,20 +742,13 @@ func (u *ContainerService) ContainerUpgrade(req dto.ContainerUpgrade) error {
config := oldContainer.Config config := oldContainer.Config
config.Image = req.Image config.Image = req.Image
hostConf := oldContainer.HostConfig hostConf := oldContainer.HostConfig
var networkConf network.NetworkingConfig
if oldContainer.NetworkSettings != nil {
for networkKey := range oldContainer.NetworkSettings.Networks {
networkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
}
err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true}) err := client.ContainerRemove(ctx, item, container.RemoveOptions{Force: true})
taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err) taskItem.LogWithStatus(i18n.GetWithName("ContainerRemoveOld", item), err)
if err != nil { if err != nil {
return err return err
} }
con, err := client.ContainerCreate(ctx, config, hostConf, &networkConf, &v1.Platform{}, item) con, err := createContainerWithOldNetworks(ctx, client, config, hostConf, oldContainer.NetworkSettings, item)
if err != nil { if err != nil {
taskItem.Log(i18n.GetMsgByKey("ContainerRecreate")) taskItem.Log(i18n.GetMsgByKey("ContainerRecreate"))
reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings) reCreateAfterUpdate(item, client, oldContainer.Config, oldContainer.HostConfig, oldContainer.NetworkSettings)
@@ -1041,6 +1038,7 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
if cmd.CheckIllegal(container, since, tail) { if cmd.CheckIllegal(container, since, tail) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
ctx := c.Request.Context()
commandArg := []string{"logs", container} commandArg := []string{"logs", container}
dockerCommand := global.CONF.DockerConfig.Command dockerCommand := global.CONF.DockerConfig.Command
if containerType == "compose" { if containerType == "compose" {
@@ -1070,23 +1068,33 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
} }
var dockerCmd *exec.Cmd var dockerCmd *exec.Cmd
if containerType == "compose" && dockerCommand == "docker-compose" { if containerType == "compose" && dockerCommand == "docker-compose" {
dockerCmd = exec.Command("docker-compose", commandArg...) dockerCmd = exec.CommandContext(ctx, "docker-compose", commandArg...)
} else { } else {
dockerCmd = exec.Command("docker", commandArg...) dockerCmd = exec.CommandContext(ctx, "docker", commandArg...)
} }
dockerCmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
stdout, err := dockerCmd.StdoutPipe() stdout, err := dockerCmd.StdoutPipe()
if err != nil { if err != nil {
_ = dockerCmd.Process.Signal(syscall.SIGTERM)
return err return err
} }
dockerCmd.Stderr = dockerCmd.Stdout dockerCmd.Stderr = dockerCmd.Stdout
if err := dockerCmd.Start(); err != nil { if err := dockerCmd.Start(); err != nil {
_ = dockerCmd.Process.Signal(syscall.SIGTERM)
return err return err
} }
done := make(chan struct{})
go func() {
select {
case <-ctx.Done():
killContainerLogProcess(dockerCmd)
case <-done:
}
}()
defer close(done)
tempFile, err := os.CreateTemp("", "cmd_output_*.txt") tempFile, err := os.CreateTemp("", "cmd_output_*.txt")
if err != nil { if err != nil {
killContainerLogProcess(dockerCmd)
_ = dockerCmd.Wait()
return err return err
} }
defer tempFile.Close() defer tempFile.Close()
@@ -1095,31 +1103,19 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
global.LOG.Errorf("os.Remove() failed: %v", err) global.LOG.Errorf("os.Remove() failed: %v", err)
} }
}() }()
errCh := make(chan error) copyErr := copyContainerLogOutput(tempFile, stdout)
go func() { waitErr := dockerCmd.Wait()
scanner := bufio.NewScanner(stdout) if copyErr != nil {
var ansiRegex = re.GetRegex(re.AnsiEscapePattern) return copyErr
for scanner.Scan() { }
line := scanner.Text() if waitErr != nil {
cleanLine := ansiRegex.ReplaceAllString(line, "") if ctx.Err() != nil {
if _, err := tempFile.WriteString(cleanLine + "\n"); err != nil { return ctx.Err()
errCh <- err
return
}
} }
if err := scanner.Err(); err != nil { return waitErr
errCh <- err }
return if _, err := tempFile.Seek(0, io.SeekStart); err != nil {
} return err
errCh <- nil
}()
select {
case err := <-errCh:
if err != nil {
global.LOG.Errorf("Error: %v", err)
}
case <-time.After(3 * time.Second):
global.LOG.Errorf("Timeout reached")
} }
info, _ := tempFile.Stat() info, _ := tempFile.Stat()
@@ -1129,6 +1125,36 @@ func (u *ContainerService) DownloadContainerLogs(containerType, container, since
return nil return nil
} }
func copyContainerLogOutput(dst io.Writer, src io.Reader) error {
reader := bufio.NewReader(src)
for {
line, err := reader.ReadString('\n')
if len(line) > 0 {
cleanLine := containerLogAnsiRegex.ReplaceAllString(line, "")
if _, writeErr := io.WriteString(dst, cleanLine); writeErr != nil {
return writeErr
}
}
if err != nil {
if err == io.EOF {
return nil
}
return err
}
}
}
func killContainerLogProcess(command *exec.Cmd) {
if command == nil || command.Process == nil {
return
}
if pgid, err := syscall.Getpgid(command.Process.Pid); err == nil {
_ = syscall.Kill(-pgid, syscall.SIGKILL)
return
}
_ = command.Process.Kill()
}
func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) { func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error) {
client, err := docker.NewDockerClient() client, err := docker.NewDockerClient()
if err != nil { if err != nil {
@@ -1163,7 +1189,7 @@ func (u *ContainerService) ContainerStats(id string) (*dto.ContainerStats, error
func (u *ContainerService) LoadUsers(req dto.OperationWithName) []string { func (u *ContainerService) LoadUsers(req dto.OperationWithName) []string {
var users []string var users []string
std, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("docker", "exec", req.Name, "cat", "/etc/passwd") std, err := cmd.RunDockerExecWithStdout(20*time.Second, req.Name, "cat", "/etc/passwd")
if err != nil { if err != nil {
return users return users
} }
@@ -1436,7 +1462,7 @@ func (u *ContainerService) DownloadContainerFile(req dto.ContainerFileReq) (io.R
fileName = "container-file" fileName = "container-file"
} }
if stat.Mode.IsDir() { if stat.Mode.IsDir() {
if _, err := runContainerCommand(cli, req.ContainerID, []string{"sh", "-c", "command -v tar >/dev/null 2>&1"}); err != nil { if _, err := runContainerCommand(cli, req.ContainerID, []string{"tar", "--help"}); err != nil {
_ = cli.Close() _ = cli.Close()
return nil, "", "", fmt.Errorf("tar command not found in container") return nil, "", "", fmt.Errorf("tar command not found in container")
} }
@@ -1616,9 +1642,8 @@ func toContainerFileInfo(filePath string, stat container.PathStat, isDir bool) d
} }
func isContainerDir(cli *client.Client, containerID, targetPath string) (bool, error) { func isContainerDir(cli *client.Client, containerID, targetPath string) (bool, error) {
_, err := runContainerCommand(cli, containerID, []string{ checkPath := strings.TrimSuffix(targetPath, "/") + "/."
"sh", "-c", "[ -d \"$1\" ]", "sh", targetPath, _, err := runContainerCommand(cli, containerID, []string{"ls", "-d", "--", checkPath})
})
if err != nil { if err != nil {
return false, err return false, err
} }
@@ -1948,15 +1973,7 @@ func loadConfigInfo(isCreate bool, req dto.ContainerOperate, oldContainer *conta
func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *container.NetworkSettings) { func reCreateAfterUpdate(name string, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkConf *container.NetworkSettings) {
ctx := context.Background() ctx := context.Background()
var oldNetworkConf network.NetworkingConfig oldContainer, err := createContainerWithOldNetworks(ctx, client, config, hostConf, networkConf, name)
if networkConf != nil {
for networkKey := range networkConf.Networks {
oldNetworkConf.EndpointsConfig = map[string]*network.EndpointSettings{networkKey: {}}
break
}
}
oldContainer, err := client.ContainerCreate(ctx, config, hostConf, &oldNetworkConf, &v1.Platform{}, name)
if err != nil { if err != nil {
global.LOG.Errorf("recreate after container update failed, err: %v", err) global.LOG.Errorf("recreate after container update failed, err: %v", err)
return return
@@ -1967,6 +1984,29 @@ func reCreateAfterUpdate(name string, client *client.Client, config *container.C
global.LOG.Info("recreate after container update successful") global.LOG.Info("recreate after container update successful")
} }
func createContainerWithOldNetworks(ctx context.Context, client *client.Client, config *container.Config, hostConf *container.HostConfig, networkSettings *container.NetworkSettings, name string) (container.CreateResponse, error) {
networkConf, extraNetworks := buildContainerRecoverNetworkConfig(networkSettings, hostConf)
removeUnsupportedEndpointStaticIPAM(client, networkConf, extraNetworks)
created, err := client.ContainerCreate(ctx, config, hostConf, networkConf, nil, name)
if err != nil {
return created, err
}
extraNames := make([]string, 0, len(extraNetworks))
for item := range extraNetworks {
extraNames = append(extraNames, item)
}
sort.Strings(extraNames)
for _, item := range extraNames {
if err := client.NetworkConnect(ctx, item, created.ID, extraNetworks[item]); err != nil {
_ = client.ContainerRemove(ctx, created.ID, container.RemoveOptions{Force: true})
return created, err
}
}
return created, nil
}
func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper { func loadVolumeBinds(binds []container.MountPoint) []dto.VolumeHelper {
var datas []dto.VolumeHelper var datas []dto.VolumeHelper
for _, bind := range binds { for _, bind := range binds {
+4
View File
@@ -270,6 +270,10 @@ func (u *ContainerService) ComposeOperation(req dto.ComposeOperation) error {
if stdout, err := compose.Up(req.Path); err != nil { if stdout, err := compose.Up(req.Path); err != nil {
return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err) return fmt.Errorf("docker-compose up failed, std: %s, err: %v", stdout, err)
} }
} else if req.Operation == "rebuild" {
if stdout, err := compose.DownAndUp(req.Path); err != nil {
return fmt.Errorf("docker-compose rebuild failed, std: %s, err: %v", stdout, err)
}
} else { } else {
if stdout, err := compose.Operate(req.Path, req.Operation); err != nil { if stdout, err := compose.Operate(req.Path, req.Operation); err != nil {
return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err) return fmt.Errorf("docker-compose %s failed, std: %s, err: %v", req.Operation, stdout, err)
+9 -9
View File
@@ -27,10 +27,10 @@ type CronjobService struct{}
type ICronjobService interface { type ICronjobService interface {
SearchWithPage(search dto.PageCronjob) (int64, interface{}, error) SearchWithPage(search dto.PageCronjob) (int64, interface{}, error)
SearchRecords(search dto.SearchRecord) (int64, interface{}, error) SearchRecords(search dto.SearchRecord) (int64, interface{}, error)
Create(cronjobDto dto.CronjobOperate) error Create(cronjobDto dto.CronjobOperate, operator string) error
LoadNextHandle(spec string) ([]string, error) LoadNextHandle(spec string) ([]string, error)
HandleOnce(id uint) error HandleOnce(id uint) error
Update(id uint, req dto.CronjobOperate) error Update(id uint, req dto.CronjobOperate, operator string) error
UpdateStatus(id uint, status string) error UpdateStatus(id uint, status string) error
UpdateGroup(req dto.ChangeGroup) error UpdateGroup(req dto.ChangeGroup) error
Delete(req dto.CronjobBatchDelete) error Delete(req dto.CronjobBatchDelete) error
@@ -39,7 +39,7 @@ type ICronjobService interface {
HandleStop(id uint) error HandleStop(id uint) error
Export(req dto.OperateByIDs) (string, error) Export(req dto.OperateByIDs) (string, error)
Import(req []dto.CronjobTrans) error Import(req []dto.CronjobTrans, operator string) error
LoadScriptOptions() []dto.ScriptOptions LoadScriptOptions() []dto.ScriptOptions
LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error) LoadInfo(req dto.OperateByID) (*dto.CronjobOperate, error)
@@ -212,7 +212,7 @@ func (u *CronjobService) Export(req dto.OperateByIDs) (string, error) {
return string(itemJson), nil return string(itemJson), nil
} }
func (u *CronjobService) Import(req []dto.CronjobTrans) error { func (u *CronjobService) Import(req []dto.CronjobTrans, operator string) error {
for _, item := range req { for _, item := range req {
cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name)) cronjobItem, _ := cronjobRepo.Get(repo.WithByName(item.Name))
if cronjobItem.ID != 0 { if cronjobItem.ID != 0 {
@@ -405,7 +405,7 @@ func (u *CronjobService) Import(req []dto.CronjobTrans) error {
Project: strconv.Itoa(int(cronjob.ID)), Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable, Status: constant.AlertEnable,
} }
_ = NewIAlertService().CreateAlert(createAlert) _ = NewIAlertService().CreateAlert(createAlert, operator)
} }
} }
return nil return nil
@@ -558,7 +558,7 @@ func (u *CronjobService) HandleOnce(id uint) error {
return nil return nil
} }
func (u *CronjobService) Create(req dto.CronjobOperate) error { func (u *CronjobService) Create(req dto.CronjobOperate, operator string) error {
cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name)) cronjob, _ := cronjobRepo.Get(repo.WithByName(req.Name))
if cronjob.ID != 0 { if cronjob.ID != 0 {
return buserr.New("ErrRecordExist") return buserr.New("ErrRecordExist")
@@ -607,7 +607,7 @@ func (u *CronjobService) Create(req dto.CronjobOperate) error {
Project: strconv.Itoa(int(cronjob.ID)), Project: strconv.Itoa(int(cronjob.ID)),
Status: constant.AlertEnable, Status: constant.AlertEnable,
} }
err := NewIAlertService().CreateAlert(createAlert) err := NewIAlertService().CreateAlert(createAlert, operator)
if err != nil { if err != nil {
return err return err
} }
@@ -678,7 +678,7 @@ func (u *CronjobService) Delete(req dto.CronjobBatchDelete) error {
return nil return nil
} }
func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error { func (u *CronjobService) Update(id uint, req dto.CronjobOperate, operator string) error {
var cronjob model.Cronjob var cronjob model.Cronjob
if err := copier.Copy(&cronjob, &req); err != nil { if err := copier.Copy(&cronjob, &req); err != nil {
return buserr.WithDetail("ErrStructTransform", err.Error(), nil) return buserr.WithDetail("ErrStructTransform", err.Error(), nil)
@@ -756,7 +756,7 @@ func (u *CronjobService) Update(id uint, req dto.CronjobOperate) error {
Type: cronjob.Type, Type: cronjob.Type,
Project: strconv.Itoa(int(cronModel.ID)), Project: strconv.Itoa(int(cronModel.ID)),
} }
err = NewIAlertService().ExternalUpdateAlert(updateAlert) err = NewIAlertService().ExternalUpdateAlert(updateAlert, operator)
if err != nil { if err != nil {
return err return err
} }
+2 -2
View File
@@ -391,7 +391,7 @@ func (u *CronjobService) handleCutWebsiteLog(cronjob *model.Cronjob, startTime t
func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error { func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error {
cmdMgr := cmd.NewCommandMgr() cmdMgr := cmd.NewCommandMgr()
if err := cmdMgr.RunBashCf("tar -czf %s -C %s %s", dstFilePath, websiteLogDir, strings.Join([]string{"access.log", "error.log"}, " ")); err != nil { if err := cmdMgr.Run("tar", "-czf", dstFilePath, "-C", websiteLogDir, "access.log", "error.log"); err != nil {
dstDir := pathUtils.Dir(dstFilePath) dstDir := pathUtils.Dir(dstFilePath)
if err = fileOp.Copy(pathUtils.Join(websiteLogDir, "access.log"), dstDir); err != nil { if err = fileOp.Copy(pathUtils.Join(websiteLogDir, "access.log"), dstDir); err != nil {
return err return err
@@ -399,7 +399,7 @@ func backupLogFile(dstFilePath, websiteLogDir string, fileOp files.FileOp) error
if err = fileOp.Copy(pathUtils.Join(websiteLogDir, "error.log"), dstDir); err != nil { if err = fileOp.Copy(pathUtils.Join(websiteLogDir, "error.log"), dstDir); err != nil {
return err return err
} }
if err = cmdMgr.RunBashCf("tar -czf %s -C %s %s", dstFilePath, dstDir, strings.Join([]string{"access.log", "error.log"}, " ")); err != nil { if err = cmdMgr.Run("tar", "-czf", dstFilePath, "-C", dstDir, "access.log", "error.log"); err != nil {
return err return err
} }
_ = fileOp.DeleteFile(pathUtils.Join(dstDir, "access.log")) _ = fileOp.DeleteFile(pathUtils.Join(dstDir, "access.log"))
+22 -4
View File
@@ -60,7 +60,9 @@ func (u *DashboardService) Restart(operation string) error {
case "system": case "system":
{ {
go func() { go func() {
if err := cmd.RunDefaultBashCf("%s reboot", cmd.SudoHandleCmd()); err != nil { cmdMgr := cmd.NewCommandMgr()
err := cmdMgr.RunWithOptionalSudo("reboot")
if err != nil {
global.LOG.Errorf("handle reboot failed, %v", err) global.LOG.Errorf("handle reboot failed, %v", err)
} }
}() }()
@@ -185,6 +187,7 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
hostInfo, _ := psutil.HOST.GetHostInfo(false) hostInfo, _ := psutil.HOST.GetHostInfo(false)
currentInfo.Uptime = hostInfo.Uptime currentInfo.Uptime = hostInfo.Uptime
currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout) currentInfo.TimeSinceUptime = time.Unix(int64(hostInfo.BootTime), 0).Format(constant.DateTimeLayout)
currentInfo.RunningTime = loadRunningTime(hostInfo.Uptime)
currentInfo.Procs = hostInfo.Procs currentInfo.Procs = hostInfo.Procs
currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false) currentInfo.CPUTotal, _ = psutil.CPUInfo.GetLogicalCores(false)
@@ -265,6 +268,15 @@ func (u *DashboardService) LoadCurrentInfo(ioOption string, netOption string) *d
return &currentInfo return &currentInfo
} }
func loadRunningTime(uptime uint64) dto.RunningTime {
return dto.RunningTime{
Days: uptime / 86400,
Hours: (uptime % 86400) / 3600,
Minutes: (uptime % 3600) / 60,
Seconds: uptime % 60,
}
}
func (u *DashboardService) LoadTopCPU() []dto.Process { func (u *DashboardService) LoadTopCPU() []dto.Process {
return loadTopCPU() return loadTopCPU()
} }
@@ -429,11 +441,17 @@ type diskInfo struct {
func loadDiskInfo() []dto.DiskInfo { func loadDiskInfo() []dto.DiskInfo {
var datas []dto.DiskInfo var datas []dto.DiskInfo
cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second)) cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(2 * time.Second))
format := `awk 'NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}'` format := `NR>1 && !/tmpfs|snap\/core|udev/ {printf "%s\t%s\t%s\t%s\t%s\t%s\t%s\n", $1, $2, $3, $4, $5, $6, $7}`
stdout, err := cmdMgr.RunWithStdout("bash", "-c", `timeout 2 df -hT -P | `+format) stdout, err := cmdMgr.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-hT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}},
)
if err != nil { if err != nil {
cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second)) cmdMgr2 := cmd.NewCommandMgr(cmd.WithTimeout(1 * time.Second))
stdout, err = cmdMgr2.RunWithStdout("bash", "-c", `timeout 1 df -lhT -P | `+format) stdout, err = cmdMgr2.RunPipe(
cmd.PipeCommand{Name: "df", Args: []string{"-lhT", "-P"}},
cmd.PipeCommand{Name: "awk", Args: []string{format}},
)
if err != nil { if err != nil {
return datas return datas
} }
+1 -1
View File
@@ -17,7 +17,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/jinzhu/copier" "github.com/jinzhu/copier"
"go.mongodb.org/mongo-driver/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
type MongodbService struct{} type MongodbService struct{}
+8 -5
View File
@@ -82,7 +82,7 @@ func (u *DeviceService) LoadBaseInfo() (dto.DeviceBaseInfo, error) {
} }
func (u *DeviceService) LoadTimeZone() ([]string, error) { func (u *DeviceService) LoadTimeZone() ([]string, error) {
std, err := cmd.NewCommandMgr(cmd.WithTimeout(10 * time.Minute)).RunWithStdoutBashC("timedatectl list-timezones") std, err := cmd.NewCommandMgr(cmd.WithTimeout(10*time.Minute)).RunWithStdout("timedatectl", "list-timezones")
if err != nil { if err != nil {
return []string{}, err return []string{}, err
} }
@@ -223,7 +223,9 @@ func (u *DeviceService) UpdatePasswd(req dto.ChangePasswd) error {
if cmd.CheckIllegal(req.User, req.Passwd) { if cmd.CheckIllegal(req.User, req.Passwd) {
return buserr.New("ErrCmdIllegal") return buserr.New("ErrCmdIllegal")
} }
if err := cmd.RunDefaultBashCf("%s echo '%s:%s' | %s chpasswd", cmd.SudoHandleCmd(), req.User, req.Passwd, cmd.SudoHandleCmd()); err != nil { cmdItem := cmd.ExecCommandWithOptionalSudo("chpasswd")
cmdItem.Stdin = strings.NewReader(req.User + ":" + req.Passwd)
if err := cmdItem.Run(); err != nil {
if strings.Contains(err.Error(), "does not exist") { if strings.Contains(err.Error(), "does not exist") {
return buserr.New("ErrNotExistUser") return buserr.New("ErrNotExistUser")
} }
@@ -393,7 +395,7 @@ func loadHosts() []dto.HostHelper {
} }
func loadHostname() string { func loadHostname() string {
std, err := cmd.RunDefaultWithStdoutBashC("hostname") std, err := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)).RunWithStdout("hostname")
if err != nil { if err != nil {
return "" return ""
} }
@@ -401,7 +403,7 @@ func loadHostname() string {
} }
func loadUser() string { func loadUser() string {
std, err := cmd.RunDefaultWithStdoutBashC("whoami") std, err := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)).RunWithStdout("whoami")
if err != nil { if err != nil {
return "" return ""
} }
@@ -410,7 +412,8 @@ func loadUser() string {
func loadSwap() []dto.SwapHelper { func loadSwap() []dto.SwapHelper {
var data []dto.SwapHelper var data []dto.SwapHelper
std, err := cmd.RunDefaultWithStdoutBashCf("%s swapon --summary", cmd.SudoHandleCmd()) cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
std, err := cmdMgr.RunWithOptionalSudoAndStdout("swapon", "--summary")
if err != nil { if err != nil {
return data return data
} }
+2 -2
View File
@@ -223,7 +223,7 @@ func doSystemClean(taskItem *task.Task) func(t *task.Task) error {
} }
} }
dropWithExclude(path.Join(global.Dir.BaseDir, uploadPath), []string{"theme"}, taskItem, &size, &fileCount) dropWithExclude(path.Join(global.Dir.BaseDir, uploadPath), []string{"theme", "skills-hub"}, taskItem, &size, &fileCount)
dropWithTask(path.Join(global.Dir.BaseDir, downloadPath), taskItem, &size, &fileCount) dropWithTask(path.Join(global.Dir.BaseDir, downloadPath), taskItem, &size, &fileCount)
logFiles, _ := os.ReadDir(global.Dir.LogDir) logFiles, _ := os.ReadDir(global.Dir.LogDir)
@@ -863,7 +863,7 @@ func loadTreeWithAllFile(isCheck bool, originalPath, treeType, pathItem string,
return lists return lists
} }
for _, file := range files { for _, file := range files {
if treeType == "upload" && (file.Name() == "theme" && file.IsDir()) { if treeType == "upload" && ((file.Name() == "theme" || file.Name() == "skills-hub") && file.IsDir()) {
continue continue
} }
if treeType == "system_log" && (file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir()) { if treeType == "system_log" && (file.Name() == "1Panel-Core.log" || file.Name() == "1Panel.log" || file.IsDir()) {
+3 -2
View File
@@ -30,7 +30,8 @@ func NewIDiskService() IDiskService {
func (s *DiskService) GetCompleteDiskInfo() (*response.CompleteDiskInfo, error) { func (s *DiskService) GetCompleteDiskInfo() (*response.CompleteDiskInfo, error) {
var diskInfos []response.DiskBasicInfo var diskInfos []response.DiskBasicInfo
output, err := cmd.RunDefaultWithStdoutBashC("lsblk -J -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA") cmdMgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second))
output, err := cmdMgr.RunWithStdout("lsblk", "-J", "-o", "NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA")
if err == nil { if err == nil {
diskInfos, err = parseLsblkJsonOutput(output) diskInfos, err = parseLsblkJsonOutput(output)
if err == nil { if err == nil {
@@ -38,7 +39,7 @@ func (s *DiskService) GetCompleteDiskInfo() (*response.CompleteDiskInfo, error)
return &result, nil return &result, nil
} }
} }
output, err = cmd.RunDefaultWithStdoutBashC("lsblk -P -o NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA") output, err = cmdMgr.RunWithStdout("lsblk", "-P", "-o", "NAME,SIZE,TYPE,MOUNTPOINT,FSTYPE,MODEL,SERIAL,TRAN,ROTA")
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to run lsblk command: %v", err) return nil, fmt.Errorf("failed to run lsblk command: %v", err)
} }
+10 -4
View File
@@ -4,11 +4,13 @@ import (
"bufio" "bufio"
"encoding/json" "encoding/json"
"fmt" "fmt"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"os" "os"
"os/exec" "os/exec"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/utils/re"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/app/dto/response" "github.com/1Panel-dev/1Panel/agent/app/dto/response"
@@ -386,10 +388,14 @@ func getParentDevice(device string) string {
} }
func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) { func getDiskUsageInfo(device string) (size, used, avail string, usePercent int, err error) {
output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("df -h %s | tail -1", device)) output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("df", "-h", device)
if err != nil { if err != nil {
return "", "", "", 0, nil return "", "", "", 0, nil
} }
lines := strings.Split(strings.TrimSpace(output), "\n")
if len(lines) > 1 {
output = lines[len(lines)-1]
}
fields := strings.Fields(output) fields := strings.Fields(output)
if len(fields) >= 5 { if len(fields) >= 5 {
@@ -515,7 +521,7 @@ func removeFromFstab(mountPoint string) error {
} }
func getFilesystemType(device string) (string, error) { func getFilesystemType(device string) (string, error) {
output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("blkid -o value -s TYPE %s", device)) output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("blkid", "-o", "value", "-s", "TYPE", device)
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -575,7 +581,7 @@ func parseSizeToBytes(sizeStr string) int64 {
} }
func getDeviceUUID(device string) (string, error) { func getDeviceUUID(device string) (string, error) {
output, err := cmd.RunDefaultWithStdoutBashC(fmt.Sprintf("blkid -s UUID -o value %s", device)) output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("blkid", "-s", "UUID", "-o", "value", device)
if err != nil { if err != nil {
return "", err return "", err
} }
+4 -3
View File
@@ -8,6 +8,7 @@ import (
"os" "os"
"path" "path"
"strings" "strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/constant"
@@ -89,8 +90,8 @@ func (u *DockerService) LoadDockerConf() (*dto.DaemonJsonConf, error) {
data.Version = itemVersion.Version data.Version = itemVersion.Version
} }
data.IsSwarm = false data.IsSwarm = false
stdout2, _ := cmd.RunDefaultWithStdoutBashC("docker info | grep Swarm") stdout2, _ := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("docker", "info")
if string(stdout2) == " Swarm: active\n" { if strings.Contains(stdout2, "Swarm: active") {
data.IsSwarm = true data.IsSwarm = true
} }
if _, err := os.Stat(constant.DaemonJsonPath); err != nil { if _, err := os.Stat(constant.DaemonJsonPath); err != nil {
@@ -423,7 +424,7 @@ func validateDockerConfig() error {
if !cmd.Which("dockerd") { if !cmd.Which("dockerd") {
return nil return nil
} }
stdout, err := cmd.RunDefaultWithStdoutBashC("dockerd --validate") stdout, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout("dockerd", "--validate")
if strings.Contains(stdout, "unknown flag: --validate") { if strings.Contains(stdout, "unknown flag: --validate") {
return nil return nil
} }
@@ -0,0 +1,84 @@
package service
import (
"fmt"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/app/dto/request"
)
func composePortEnvKeys(index int) (containerPort, hostPort, hostIP, protocol string) {
return fmt.Sprintf("CONTAINER_PORT_%d", index),
fmt.Sprintf("HOST_PORT_%d", index),
fmt.Sprintf("HOST_IP_%d", index),
fmt.Sprintf("PORT_PROTOCOL_%d", index)
}
func isComposePortEnvKey(key string) bool {
return strings.HasPrefix(key, "CONTAINER_PORT_") ||
strings.HasPrefix(key, "HOST_PORT_") ||
strings.HasPrefix(key, "HOST_IP_") ||
strings.HasPrefix(key, "PORT_PROTOCOL_")
}
func formatComposePortMapping(hostIP, hostPort, containerPort, protocol string) string {
return fmt.Sprintf("${%s}:${%s}:${%s}/%s", hostIP, hostPort, containerPort, normalizeComposeProtocol(protocol))
}
func normalizeComposeProtocol(protocol string) string {
switch strings.ToLower(strings.TrimSpace(protocol)) {
case "udp":
return "udp"
default:
return "tcp"
}
}
func formatComposeVolume(source, target, mode string) string {
return fmt.Sprintf("%s:%s:%s", source, target, normalizeComposeVolumeMode(mode))
}
func normalizeComposeVolumeMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case "ro":
return "ro"
default:
return "rw"
}
}
func loadComposeExposedPortsFromEnv(envs map[string]string, defaultHostIP string, strict bool) ([]request.ExposedPort, error) {
var ports []request.ExposedPort
for key, value := range envs {
if !strings.HasPrefix(key, "CONTAINER_PORT_") {
continue
}
index := strings.TrimPrefix(key, "CONTAINER_PORT_")
containerPort, err := strconv.Atoi(value)
if err != nil {
if strict {
return nil, err
}
continue
}
hostPort, err := strconv.Atoi(envs["HOST_PORT_"+index])
if err != nil {
if strict {
return nil, err
}
continue
}
hostIP := envs["HOST_IP_"+index]
if hostIP == "" {
hostIP = defaultHostIP
}
ports = append(ports, request.ExposedPort{
ContainerPort: containerPort,
HostPort: hostPort,
HostIP: hostIP,
Protocol: normalizeComposeProtocol(envs["PORT_PROTOCOL_"+index]),
})
}
return ports, nil
}
+267 -31
View File
@@ -49,6 +49,8 @@ import (
type FileService struct { type FileService struct {
} }
const fileHistorySnapshotMaxSize = 10 * 1024 * 1024
type IFileService interface { type IFileService interface {
GetFileList(op request.FileOption) (response.FileInfo, error) GetFileList(op request.FileOption) (response.FileInfo, error)
SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error) SearchUploadWithPage(req request.SearchUploadWithPage) (int64, interface{}, error)
@@ -59,6 +61,7 @@ type IFileService interface {
Compress(c request.FileCompress) error Compress(c request.FileCompress) error
StopCompress(taskID string) error StopCompress(taskID string) error
DeCompress(c request.FileDeCompress) error DeCompress(c request.FileDeCompress) error
StopDeCompress(taskID string) error
GetContent(op request.FileContentReq) (response.FileInfo, error) GetContent(op request.FileContentReq) (response.FileInfo, error)
GetPreviewContent(op request.FileContentReq) (response.FileInfo, error) GetPreviewContent(op request.FileContentReq) (response.FileInfo, error)
SaveContent(edit request.FileEdit) error SaveContent(edit request.FileEdit) error
@@ -73,7 +76,6 @@ type IFileService interface {
BatchChangeModeAndOwner(op request.FileRoleReq) error BatchChangeModeAndOwner(op request.FileRoleReq) error
ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error) ReadLogByLine(req request.FileReadByLineReq) (*response.FileLineContent, error)
GetPathByType(pathType string) string
BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo
GetHostMount() []dto.DiskInfo GetHostMount() []dto.DiskInfo
GetUsersAndGroups() (*response.UserGroupResponse, error) GetUsersAndGroups() (*response.UserGroupResponse, error)
@@ -472,6 +474,16 @@ func preflightCompressTool(compressType files.CompressType) error {
} }
} }
func preflightDecompressTool(decompressType files.CompressType) error {
switch decompressType {
case files.Rar, files.X7z:
_, err := files.NewExtractShellArchiver(decompressType)
return err
default:
return nil
}
}
func (f *FileService) StopCompress(taskID string) error { func (f *FileService) StopCompress(taskID string) error {
if cancel, ok := global.TaskCtxMap[taskID]; ok { if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel() cancel()
@@ -480,15 +492,186 @@ func (f *FileService) StopCompress(taskID string) error {
return buserr.New("TaskNotFound") return buserr.New("TaskNotFound")
} }
func (f *FileService) StopDeCompress(taskID string) error {
if cancel, ok := global.TaskCtxMap[taskID]; ok {
cancel()
return nil
}
return buserr.New("TaskNotFound")
}
func (f *FileService) DeCompress(c request.FileDeCompress) error { func (f *FileService) DeCompress(c request.FileDeCompress) error {
fo := files.NewFileOp() fo := files.NewFileOp()
if c.Type == "tar" && len(c.Secret) != 0 { if c.Type == "tar" && len(c.Secret) != 0 {
c.Type = "tar.gz" c.Type = "tar.gz"
} }
return fo.Decompress(c.Path, c.Dst, files.CompressType(c.Type), c.Secret) if err := preflightDecompressTool(files.CompressType(c.Type)); err != nil {
return err
}
taskItem, err := task.NewTask(c.Path, task.TaskExec, task.TaskScopeTask, c.TaskID, 1)
if err != nil {
return err
}
go func() {
taskItem.AddSubTask(c.Path, func(t *task.Task) error {
t.LogStart(c.Path)
dstExisted := fo.Stat(c.Dst)
parentDir := filepath.Dir(c.Dst)
if !fo.Stat(parentDir) {
if err := fo.CreateDir(parentDir, constant.DirPerm); err != nil {
return err
}
}
tempDst, err := os.MkdirTemp(parentDir, ".decompress-*")
if err != nil {
return err
}
success := false
defer func() {
_ = os.RemoveAll(tempDst)
if !success && !dstExisted {
_ = os.RemoveAll(c.Dst)
}
}()
if err := fo.Decompress(t.TaskCtx, c.Path, tempDst, files.CompressType(c.Type), c.Secret); err != nil {
return err
}
if err := fo.CreateDir(c.Dst, constant.DirPerm); err != nil {
return err
}
if err := copyDecompressTree(t.TaskCtx, tempDst, c.Dst); err != nil {
return err
}
success = true
return nil
}, nil)
_ = taskItem.Execute()
}()
return nil
}
func copyDecompressTree(ctx context.Context, srcDir, dstDir string) error {
entries, err := os.ReadDir(srcDir)
if err != nil {
return err
}
for _, entry := range entries {
if err := copyDecompressEntry(ctx, filepath.Join(srcDir, entry.Name()), filepath.Join(dstDir, entry.Name())); err != nil {
return err
}
}
return nil
}
func copyDecompressEntry(ctx context.Context, srcPath, dstPath string) (retErr error) {
if err := ctx.Err(); err != nil {
return err
}
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
if info.Mode()&os.ModeSymlink != 0 {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
target, err := os.Readlink(srcPath)
if err != nil {
return err
}
if err := os.Symlink(target, dstPath); err != nil {
return err
}
return applyDecompressOwnership(srcPath, dstPath)
}
if info.IsDir() {
dstInfo, err := os.Lstat(dstPath)
keepExistingDir := err == nil && dstInfo.IsDir() && dstInfo.Mode()&os.ModeSymlink == 0
if err != nil && !os.IsNotExist(err) {
return err
}
if !keepExistingDir {
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(dstPath, info.Mode().Perm()); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
}
entries, err := os.ReadDir(srcPath)
if err != nil {
return err
}
for _, entry := range entries {
if err := copyDecompressEntry(ctx, filepath.Join(srcPath, entry.Name()), filepath.Join(dstPath, entry.Name())); err != nil {
return err
}
}
if keepExistingDir {
return nil
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
if err := os.RemoveAll(dstPath); err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(dstPath), constant.DirPerm); err != nil {
return err
}
srcFile, err := os.Open(srcPath)
if err != nil {
return err
}
defer srcFile.Close()
dstFile, err := os.OpenFile(dstPath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode().Perm())
if err != nil {
return err
}
defer func() {
if cerr := dstFile.Close(); cerr != nil && retErr == nil {
retErr = cerr
}
}()
if _, err := io.Copy(dstFile, srcFile); err != nil {
return err
}
if err := applyDecompressOwnership(srcPath, dstPath); err != nil {
return err
}
return os.Chtimes(dstPath, info.ModTime(), info.ModTime())
}
func applyDecompressOwnership(srcPath, dstPath string) error {
info, err := os.Lstat(srcPath)
if err != nil {
return err
}
stat, ok := info.Sys().(*unix.Stat_t)
if !ok {
return nil
}
if info.Mode()&os.ModeSymlink != 0 {
return os.Lchown(dstPath, int(stat.Uid), int(stat.Gid))
}
return os.Chown(dstPath, int(stat.Uid), int(stat.Gid))
} }
func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) { func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo, error) {
if files.ShouldDenySensitiveFileRead(op.Path) {
return response.FileInfo{}, buserr.New("ErrSensitiveFileRead")
}
info, err := files.NewFileInfo(files.FileOption{ info, err := files.NewFileInfo(files.FileOption{
Path: op.Path, Path: op.Path,
Expand: true, Expand: true,
@@ -525,6 +708,9 @@ func (f *FileService) GetContent(op request.FileContentReq) (response.FileInfo,
} }
func (f *FileService) GetPreviewContent(op request.FileContentReq) (response.FileInfo, error) { func (f *FileService) GetPreviewContent(op request.FileContentReq) (response.FileInfo, error) {
if files.ShouldDenySensitiveFileRead(op.Path) {
return response.FileInfo{}, buserr.New("ErrSensitiveFileRead")
}
info, err := files.NewFileInfo(files.FileOption{ info, err := files.NewFileInfo(files.FileOption{
Path: op.Path, Path: op.Path,
Expand: false, Expand: false,
@@ -534,6 +720,9 @@ func (f *FileService) GetPreviewContent(op request.FileContentReq) (response.Fil
return response.FileInfo{}, err return response.FileInfo{}, err
} }
if files.IsBinaryPreviewFile(info.MimeType, info.Extension) {
return response.FileInfo{}, buserr.New("ErrFileCanNotRead")
}
if files.IsBlockDevice(info.FileMode) { if files.IsBlockDevice(info.FileMode) {
return response.FileInfo{FileInfo: *info}, nil return response.FileInfo{FileInfo: *info}, nil
} }
@@ -629,13 +818,12 @@ func (f *FileService) ChangeName(req request.FileRename) error {
return buserr.New("ErrInvalidChar") return buserr.New("ErrInvalidChar")
} }
fo := files.NewFileOp() fo := files.NewFileOp()
info, _ := files.NewFileInfo(files.FileOption{Path: req.OldName, Expand: false}) content, mode, shouldRecordHistory := readEditableFileHistoryContent(req.OldName)
content, _ := os.ReadFile(req.OldName)
if err := fo.Rename(req.OldName, req.NewName); err != nil { if err := fo.Rename(req.OldName, req.NewName); err != nil {
return err return err
} }
if info != nil && !info.IsDir { if shouldRecordHistory {
if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, info.FileMode, req.OldName, req.NewName); histErr != nil { if histErr := historyService.RecordOperation(fileHistoryOpRename, req.OldName, content, mode, req.OldName, req.NewName); histErr != nil {
global.LOG.Warnf("record file rename history failed for %s: %v", req.OldName, histErr) global.LOG.Warnf("record file rename history failed for %s: %v", req.OldName, histErr)
} }
} }
@@ -645,7 +833,23 @@ func (f *FileService) ChangeName(req request.FileRename) error {
func (f *FileService) Wget(w request.FileWget) (string, error) { func (f *FileService) Wget(w request.FileWget) (string, error) {
fo := files.NewFileOp() fo := files.NewFileOp()
key := "file-wget-" + common.GetUuid() key := "file-wget-" + common.GetUuid()
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, w.IgnoreCertificate) options := files.DownloadOptions{
IgnoreCertificate: w.IgnoreCertificate,
}
if w.UseProxy {
systemProxy, err := NewISettingService().GetSystemProxy()
if err != nil {
return "", err
}
options.Proxy = &files.DownloadProxyConfig{
Type: systemProxy.Type,
URL: systemProxy.URL,
Port: systemProxy.Port,
User: systemProxy.User,
Password: systemProxy.Password,
}
}
return key, fo.DownloadFileWithProcess(w.Url, filepath.Join(w.Path, w.Name), key, options)
} }
func (f *FileService) MvFile(m request.FileMove) error { func (f *FileService) MvFile(m request.FileMove) error {
@@ -665,21 +869,15 @@ func (f *FileService) MvFile(m request.FileMove) error {
path string path string
content []byte content []byte
mode os.FileMode mode os.FileMode
isDir bool record bool
}
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
for _, oldPath := range m.OldPaths {
content, _ := os.ReadFile(oldPath)
mode := os.FileMode(0640)
isDir := false
if info, err := files.NewFileInfo(files.FileOption{Path: oldPath, Expand: false}); err == nil {
mode = info.FileMode
isDir = info.IsDir
}
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, isDir: isDir})
} }
var errs []error var errs []error
if m.Type == "cut" { if m.Type == "cut" {
snapshots := make([]moveSnapshot, 0, len(m.OldPaths))
for _, oldPath := range m.OldPaths {
content, mode, record := readEditableFileHistoryContent(oldPath)
snapshots = append(snapshots, moveSnapshot{path: oldPath, content: content, mode: mode, record: record})
}
if len(m.CoverPaths) > 0 { if len(m.CoverPaths) > 0 {
for _, src := range m.CoverPaths { for _, src := range m.CoverPaths {
if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil { if err := fo.CopyAndReName(src, m.NewPath, "", true); err != nil {
@@ -692,7 +890,7 @@ func (f *FileService) MvFile(m request.FileMove) error {
return err return err
} }
for _, snapshot := range snapshots { for _, snapshot := range snapshots {
if !snapshot.isDir { if snapshot.record {
targetPath := buildHistoryMoveTargetPath(m.NewPath, m.Name, snapshot.path, len(m.OldPaths)) targetPath := buildHistoryMoveTargetPath(m.NewPath, m.Name, snapshot.path, len(m.OldPaths))
if histErr := historyService.RecordOperation(fileHistoryOpMove, snapshot.path, snapshot.content, snapshot.mode, snapshot.path, targetPath); histErr != nil { if histErr := historyService.RecordOperation(fileHistoryOpMove, snapshot.path, snapshot.content, snapshot.mode, snapshot.path, targetPath); histErr != nil {
global.LOG.Warnf("record file move history failed for %s: %v", snapshot.path, histErr) global.LOG.Warnf("record file move history failed for %s: %v", snapshot.path, histErr)
@@ -728,6 +926,42 @@ func (f *FileService) MvFile(m request.FileMove) error {
return nil return nil
} }
func readEditableFileHistoryContent(filePath string) ([]byte, os.FileMode, bool) {
info, err := os.Lstat(filePath)
if err != nil {
return nil, 0640, false
}
mode := info.Mode()
if mode.IsDir() || mode&os.ModeSymlink != 0 || !mode.IsRegular() || files.IsBlockDevice(mode) || info.Size() > fileHistorySnapshotMaxSize {
return nil, mode, false
}
if files.IsBinaryPreviewFile(files.GetMimeType(filePath), filepath.Ext(info.Name())) {
return nil, mode, false
}
file, err := os.Open(filePath)
if err != nil {
return nil, mode, false
}
defer file.Close()
headBuf := make([]byte, 1024)
n, err := file.Read(headBuf)
if err != nil && err != io.EOF {
return nil, mode, false
}
if n > 0 && files.DetectBinary(headBuf[:n]) {
return nil, mode, false
}
if _, err := file.Seek(0, 0); err != nil {
return nil, mode, false
}
content, err := io.ReadAll(io.LimitReader(file, fileHistorySnapshotMaxSize+1))
if err != nil || int64(len(content)) > fileHistorySnapshotMaxSize {
return nil, mode, false
}
return content, mode, true
}
func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) string { func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) string {
if strings.TrimSpace(dst) == "" { if strings.TrimSpace(dst) == "" {
return sourcePath return sourcePath
@@ -739,6 +973,11 @@ func buildHistoryMoveTargetPath(dst, name, sourcePath string, sourceCount int) s
} }
func (f *FileService) FileDownload(d request.FileDownload) (string, error) { func (f *FileService) FileDownload(d request.FileDownload) (string, error) {
for _, p := range d.Paths {
if files.ShouldDenySensitiveFileRead(p) {
return "", buserr.New("ErrSensitiveFileRead")
}
}
filePath := d.Paths[0] filePath := d.Paths[0]
if d.Compress { if d.Compress {
tempPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().UnixNano())) tempPath := filepath.Join(os.TempDir(), fmt.Sprintf("%d", time.Now().UnixNano()))
@@ -870,6 +1109,14 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
logFilePath, _ = ini_conf.GetIniValue(configPath, "supervisord", "logfile") logFilePath, _ = ini_conf.GetIniValue(configPath, "supervisord", "logfile")
case constant.Supervisor: case constant.Supervisor:
logFilePath = path.Join(global.Dir.DataDir, "tools", "supervisord", "log", req.Name) logFilePath = path.Join(global.Dir.DataDir, "tools", "supervisord", "log", req.Name)
case "ai-proxy":
safeName := path.Base(req.Name)
if safeName != req.Name || strings.Contains(safeName, "..") {
return nil, buserr.New("ErrInvalidParams")
}
logFilePath = path.Join(global.Dir.LogDir, "ai", safeName)
default:
return nil, buserr.WithName("ErrNotSupportType", req.Type)
} }
file, err := os.Open(logFilePath) file, err := os.Open(logFilePath)
@@ -915,17 +1162,6 @@ func (f *FileService) ReadLogByLine(req request.FileReadByLineReq) (*response.Fi
return res, nil return res, nil
} }
func (f *FileService) GetPathByType(pathType string) string {
if pathType == "websiteDir" {
value, _ := settingRepo.GetValueByKey("WEBSITE_DIR")
if value == "" {
return path.Join(global.Dir.BaseDir, "1panel", "www")
}
return value
}
return ""
}
func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo { func (f *FileService) BatchCheckFiles(req request.FilePathsCheck) []response.ExistFileInfo {
fileList := make([]response.ExistFileInfo, 0, len(req.Paths)) fileList := make([]response.ExistFileInfo, 0, len(req.Paths))
for _, filePath := range req.Paths { for _, filePath := range req.Paths {
+4
View File
@@ -20,6 +20,7 @@ import (
"github.com/1Panel-dev/1Panel/agent/app/repo" "github.com/1Panel-dev/1Panel/agent/app/repo"
"github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/buserr"
"github.com/1Panel-dev/1Panel/agent/utils/encrypt" "github.com/1Panel-dev/1Panel/agent/utils/encrypt"
"github.com/1Panel-dev/1Panel/agent/utils/files"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -131,6 +132,9 @@ func (s *FileShareService) Create(req request.FileShareCreate) (*response.FileSh
if path == "" || strings.Contains(path, "..") { if path == "" || strings.Contains(path, "..") {
return nil, buserr.New("ErrFileSharePath") return nil, buserr.New("ErrFileSharePath")
} }
if files.ShouldDenySensitiveFileRead(path) {
return nil, buserr.New("ErrSensitiveFileRead")
}
info, err := os.Stat(path) info, err := os.Stat(path)
if err != nil || info.IsDir() { if err != nil || info.IsDir() {
return nil, buserr.New("ErrFileSharePath") return nil, buserr.New("ErrFileSharePath")
+38 -23
View File
@@ -186,6 +186,9 @@ func (u *FirewallService) OperateFirewall(req dto.FirewallOperation) error {
if err := client.Restart(); err != nil { if err := client.Restart(); err != nil {
return err return err
} }
if err := u.addPortsBeforeStart(client); err != nil {
return err
}
needRestartDocker = true needRestartDocker = true
case "disableBanPing": case "disableBanPing":
if err := firewall.UpdatePingStatus("0"); err != nil { if err := firewall.UpdatePingStatus("0"); err != nil {
@@ -219,7 +222,7 @@ func (u *FirewallService) OperatePortRule(req dto.PortRuleOperate, reload bool)
req.Chain = iptables.Chain1PanelBasic req.Chain = iptables.Chain1PanelBasic
} }
protos := strings.Split(req.Protocol, "/") protos := strings.Split(req.Protocol, "/")
itemAddress := strings.Split(strings.TrimSuffix(req.Address, ","), ",") itemAddress := splitFirewallRuleAddresses(req.Address)
if client.Name() == "ufw" { if client.Name() == "ufw" {
if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") { if strings.Contains(req.Port, ",") || strings.Contains(req.Port, "-") {
@@ -512,6 +515,7 @@ func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.Po
if err := copier.Copy(&fireInfo, &req); err != nil { if err := copier.Copy(&fireInfo, &req); err != nil {
return err return err
} }
fireInfo.Address = normalizeFirewallRuleAddress(fireInfo.Address)
if client.Name() == "ufw" { if client.Name() == "ufw" {
if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") { if len(fireInfo.Address) != 0 && !strings.EqualFold(fireInfo.Address, "Anywhere") {
@@ -526,6 +530,26 @@ func (u *FirewallService) operatePort(client firewall.FirewallClient, req dto.Po
return client.Port(fireInfo, req.Operation) return client.Port(fireInfo, req.Operation)
} }
func splitFirewallRuleAddresses(address string) []string {
parts := strings.Split(strings.TrimSuffix(address, ","), ",")
addresses := make([]string, 0, len(parts))
for _, part := range parts {
addresses = append(addresses, normalizeFirewallRuleAddress(part))
}
if len(addresses) == 0 {
return []string{""}
}
return addresses
}
func normalizeFirewallRuleAddress(address string) string {
address = strings.TrimSpace(address)
if strings.EqualFold(address, "Anywhere") {
return ""
}
return address
}
type portOfApp struct { type portOfApp struct {
AppName string AppName string
HttpPort string HttpPort string
@@ -579,30 +603,21 @@ func (u *FirewallService) cleanUnUsedData(client firewall.FirewallClient) {
} }
func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error { func (u *FirewallService) addPortsBeforeStart(client firewall.FirewallClient) error {
if !global.IsMaster { if client.Name() == "iptables" {
if err := client.Port(fireClient.FireInfo{Port: global.CONF.Base.Port, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil { isInit, _ := iptables.LoadInitStatus("iptables", "base")
if !isInit {
return nil
}
return syncIptablesFirewallPortWhiteList(true)
}
portWhiteList, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
for _, item := range portWhiteList {
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "add"); err != nil {
return err return err
} }
} else {
var portSetting model.Setting
_ = global.CoreDB.Where("key = ?", "ServerPort").First(&portSetting).Error
if len(portSetting.Value) != 0 {
if err := client.Port(fireClient.FireInfo{Port: portSetting.Value, Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
return err
}
}
}
if err := client.Port(fireClient.FireInfo{Port: loadSSHPort(), Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
return err
}
if err := client.Port(fireClient.FireInfo{Port: "80", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
return err
}
if err := client.Port(fireClient.FireInfo{Port: "443", Protocol: "tcp", Strategy: "accept"}, "add"); err != nil {
return err
}
if err := client.Port(fireClient.FireInfo{Port: "443", Protocol: "udp", Strategy: "accept"}, "add"); err != nil {
return err
} }
return client.Reload() return client.Reload()
+175
View File
@@ -0,0 +1,175 @@
package service
import (
"fmt"
"strconv"
"strings"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/firewall"
fireClient "github.com/1Panel-dev/1Panel/agent/utils/firewall/client"
"github.com/1Panel-dev/1Panel/agent/utils/firewall/client/iptables"
)
type firewallPortWhitelist struct {
Port string
Protocol string
}
func loadConfiguredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
value, err := settingRepo.GetValueByKey(constant.FirewallPortWhiteList)
if err != nil {
value = constant.FirewallPortWhiteListValue
if err := settingRepo.UpdateOrCreate(constant.FirewallPortWhiteList, value); err != nil {
return nil, err
}
}
return parseFirewallPortWhiteList(value)
}
func loadFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
portWhiteList, err := loadConfiguredFirewallPortWhiteList()
if err != nil {
return nil, err
}
requiredPorts, err := loadRequiredFirewallPortWhiteList()
if err != nil {
return nil, err
}
return normalizeFirewallPortWhiteList(append(portWhiteList, requiredPorts...)), nil
}
func loadRequiredFirewallPortWhiteList() ([]firewallPortWhitelist, error) {
panelPort := LoadPanelPort()
if panelPort == "" {
return nil, fmt.Errorf("find 1panel service port failed")
}
return normalizeFirewallPortWhiteList([]firewallPortWhitelist{
{Port: panelPort, Protocol: "tcp"},
{Port: loadSSHPort(), Protocol: "tcp"},
}), nil
}
func parseFirewallPortWhiteList(value string) ([]firewallPortWhitelist, error) {
items := strings.FieldsFunc(value, func(r rune) bool {
return r == ',' || r == '\n' || r == ';' || r == ' '
})
ports := make([]firewallPortWhitelist, 0, len(items))
exists := make(map[string]struct{})
for _, item := range items {
item = strings.TrimSpace(item)
if item == "" {
continue
}
port, protocol, ok := strings.Cut(item, "/")
if !ok {
protocol = "tcp"
}
port = strings.TrimSpace(port)
protocol = strings.ToLower(strings.TrimSpace(protocol))
if protocol != "tcp" && protocol != "udp" {
return nil, fmt.Errorf("invalid firewall port whitelist protocol: %s", item)
}
portNum, err := strconv.Atoi(port)
if err != nil || portNum < 1 || portNum > 65535 {
return nil, fmt.Errorf("invalid firewall port whitelist: %s", item)
}
key := fmt.Sprintf("%d/%s", portNum, protocol)
if _, ok := exists[key]; ok {
continue
}
exists[key] = struct{}{}
ports = append(ports, firewallPortWhitelist{Port: strconv.Itoa(portNum), Protocol: protocol})
}
return ports, nil
}
func normalizeFirewallPortWhiteList(portWhiteList []firewallPortWhitelist) []firewallPortWhitelist {
ports := make([]firewallPortWhitelist, 0, len(portWhiteList))
exists := make(map[string]struct{})
for _, item := range portWhiteList {
if item.Port == "" {
continue
}
key := fmt.Sprintf("%s/%s", item.Port, item.Protocol)
if _, ok := exists[key]; ok {
continue
}
exists[key] = struct{}{}
ports = append(ports, item)
}
return ports
}
func syncFirewallPortWhiteListAfterUpdate(oldValue string) error {
client, err := firewall.NewFirewallClient()
if err != nil {
return err
}
if client.Name() == "iptables" {
isInit, _ := iptables.LoadInitStatus("iptables", "base")
if !isInit {
return nil
}
oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
if err != nil {
return err
}
return syncIptablesFirewallPortWhiteList(true, oldPortWhiteList)
}
isActive, _ := client.Status()
if !isActive {
return nil
}
portWhiteList, err := loadFirewallPortWhiteList()
if err != nil {
return err
}
oldPortWhiteList, err := parseFirewallPortWhiteList(oldValue)
if err != nil {
return err
}
requiredPorts, err := loadRequiredFirewallPortWhiteList()
if err != nil {
return err
}
oldPortWhiteList = normalizeFirewallPortWhiteList(append(oldPortWhiteList, requiredPorts...))
return syncFirewallClientPortWhiteList(client, oldPortWhiteList, portWhiteList)
}
func syncFirewallClientPortWhiteList(client firewall.FirewallClient, oldPortWhiteList, portWhiteList []firewallPortWhitelist) error {
oldPorts := firewallPortWhiteListMap(oldPortWhiteList)
newPorts := firewallPortWhiteListMap(portWhiteList)
for _, item := range oldPortWhiteList {
key := firewallPortWhiteListKey(item)
if _, ok := newPorts[key]; ok {
continue
}
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "remove"); err != nil {
return err
}
}
for _, item := range portWhiteList {
key := firewallPortWhiteListKey(item)
if _, ok := oldPorts[key]; ok {
continue
}
if err := client.Port(fireClient.FireInfo{Port: item.Port, Protocol: item.Protocol, Strategy: "accept"}, "add"); err != nil {
return err
}
}
return client.Reload()
}
func firewallPortWhiteListMap(portWhiteList []firewallPortWhitelist) map[string]struct{} {
ports := make(map[string]struct{})
for _, item := range portWhiteList {
ports[firewallPortWhiteListKey(item)] = struct{}{}
}
return ports
}
func firewallPortWhiteListKey(item firewallPortWhitelist) string {
return item.Port + "/" + item.Protocol
}

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