├── .github └── workflows │ └── build.yml ├── .gitignore ├── LICENSE ├── README.md ├── RELEASE_CHANGELOG.md ├── analysis_options.yaml ├── bin └── nonebot_flutter_webui_agent.dart ├── curl-test.sh ├── img └── logo.webp ├── pubspec.yaml └── utils ├── core.dart ├── deploy_bot.dart ├── global.dart ├── logger.dart ├── manage.dart ├── run_cmd.dart ├── user_config.dart └── ws_handler.dart /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Publish 7 | 8 | on: 9 | push: 10 | branches: 11 | - main 12 | tags: 13 | - 🏷️* 14 | 15 | jobs: 16 | windows: 17 | runs-on: windows-latest 18 | 19 | steps: 20 | - name: Checkout 21 | uses: actions/checkout@v4 22 | with: 23 | submodules: true 24 | - name: Setup Dart 25 | uses: dart-lang/setup-dart@v1 26 | 27 | - name: Compile 28 | run: | 29 | mkdir dist 30 | dart pub get 31 | dart compile exe bin/nonebot_flutter_webui_agent.dart -o dist/agent-windows.exe 32 | 33 | - name: Upload Dist Artifact 34 | uses: actions/upload-artifact@v4 35 | with: 36 | name: agent-windows 37 | path: dist/agent-windows.exe 38 | 39 | linux_amd64: 40 | runs-on: ubuntu-latest 41 | steps: 42 | - name: Checkout 43 | uses: actions/checkout@v4 44 | with: 45 | submodules: true 46 | - name: Setup Dart 47 | uses: dart-lang/setup-dart@v1 48 | 49 | - name: Compile 50 | run: | 51 | mkdir dist 52 | dart pub get 53 | dart compile exe bin/nonebot_flutter_webui_agent.dart -o dist/agent-linux-amd64 54 | 55 | - name: Upload Dist Artifact 56 | uses: actions/upload-artifact@v4 57 | with: 58 | name: agent-linux-amd64 59 | path: dist/agent-linux-amd64 60 | 61 | 62 | linux_arm64: 63 | runs-on: ubuntu-latest 64 | steps: 65 | 66 | - name: Checkout 67 | uses: actions/checkout@v4 68 | 69 | 70 | - name: Install Dependencies 71 | run: | 72 | sudo apt update 73 | sudo apt install aria2 unzip -y 74 | aria2c https://storage.googleapis.com/dart-archive/channels/beta/release/3.7.0-209.1.beta/sdk/dartsdk-linux-arm64-release.zip 75 | unzip dartsdk-linux-arm64-release.zip 76 | rm dartsdk-linux-arm64-release.zip 77 | 78 | 79 | - name: Use ARM64 Environment to Compile 80 | uses: uraimo/run-on-arch-action@v2 81 | with: 82 | arch: aarch64 83 | distro: ubuntu20.04 84 | run: | 85 | export PATH=$PATH:$GITHUB_WORKSPACE/dart-sdk/bin 86 | mkdir dist 87 | dart pub get 88 | dart compile exe bin/nonebot_flutter_webui_agent.dart -o dist/agent-linux-arm64 89 | 90 | 91 | 92 | - name: Upload Dist Artifact 93 | uses: actions/upload-artifact@v4 94 | with: 95 | name: agent-linux-arm64 96 | path: dist/agent-linux-arm64 97 | 98 | 99 | linux_arm: 100 | runs-on: ubuntu-latest 101 | steps: 102 | 103 | - name: Checkout 104 | uses: actions/checkout@v4 105 | 106 | 107 | - name: Install Dependencies 108 | run: | 109 | sudo apt update 110 | sudo apt install aria2 unzip -y 111 | aria2c https://storage.flutter-io.cn/dart-archive/channels/beta/release/3.7.0-209.1.beta/sdk/dartsdk-linux-arm-release.zip 112 | unzip dartsdk-linux-arm-release.zip 113 | rm dartsdk-linux-arm-release.zip 114 | 115 | 116 | - name: Use ARM Environment to Compile 117 | uses: uraimo/run-on-arch-action@v2 118 | with: 119 | arch: armv7 120 | distro: ubuntu20.04 121 | run: | 122 | export PATH=$PATH:$GITHUB_WORKSPACE/dart-sdk/bin 123 | mkdir dist 124 | dart pub get 125 | dart compile exe bin/nonebot_flutter_webui_agent.dart -o dist/agent-linux-arm 126 | 127 | 128 | 129 | - name: Upload Dist Artifact 130 | uses: actions/upload-artifact@v4 131 | with: 132 | name: agent-linux-arm 133 | path: dist/agent-linux-arm 134 | 135 | macos: 136 | runs-on: macos-13 137 | 138 | steps: 139 | - name: Checkout 140 | uses: actions/checkout@v4 141 | with: 142 | submodules: true 143 | - name: Setup Dart 144 | uses: dart-lang/setup-dart@v1 145 | 146 | - name: Compile 147 | run: | 148 | mkdir dist 149 | dart pub get 150 | dart compile exe bin/nonebot_flutter_webui_agent.dart -o dist/agent-macos 151 | 152 | - name: Upload Dist Artifact 153 | uses: actions/upload-artifact@v4 154 | with: 155 | name: agent-macos 156 | path: dist/agent-macos 157 | 158 | macos_M1: 159 | runs-on: macos-latest 160 | 161 | steps: 162 | - name: Checkout 163 | uses: actions/checkout@v4 164 | with: 165 | submodules: true 166 | - name: Setup Dart 167 | uses: dart-lang/setup-dart@v1 168 | 169 | - name: Compile 170 | run: | 171 | mkdir dist 172 | dart pub get 173 | dart compile exe bin/nonebot_flutter_webui_agent.dart -o dist/agent-macos-M1 174 | 175 | - name: Upload Dist Artifact 176 | uses: actions/upload-artifact@v4 177 | with: 178 | name: agent-macos-M1 179 | path: dist/agent-macos-M1 180 | 181 | 182 | publish_releases: 183 | name: publish 184 | runs-on: ubuntu-latest 185 | needs: 186 | - windows 187 | - linux_amd64 188 | - linux_arm64 189 | - linux_arm 190 | - macos 191 | - macos_M1 192 | 193 | 194 | steps: 195 | - name: Checkout Repo 196 | uses: actions/checkout@v4 197 | - name: Fetch Version 198 | run: | 199 | sudo snap install yq 200 | nbgui_version=$(yq eval '.version' pubspec.yaml) 201 | echo "NBGUI_VERSION=$nbgui_version" >> $GITHUB_ENV 202 | 203 | - name: Download Artifact (Windows) 204 | uses: actions/download-artifact@v4 205 | with: 206 | name: "agent-windows" 207 | path: dist/ 208 | 209 | 210 | - name: Download Artifact (Linux amd64) 211 | uses: actions/download-artifact@v4 212 | with: 213 | name: "agent-linux-amd64" 214 | path: dist/ 215 | 216 | - name: Download Artifact (MacOS) 217 | uses: actions/download-artifact@v4 218 | with: 219 | name: "agent-macos" 220 | path: dist/ 221 | 222 | - name: Download Artifact (MacOS M1) 223 | uses: actions/download-artifact@v4 224 | with: 225 | name: "agent-macos-M1" 226 | path: dist/ 227 | 228 | - name: Download Artifact (Linux arm64) 229 | uses: actions/download-artifact@v4 230 | with: 231 | name: "agent-linux-arm64" 232 | path: dist/ 233 | 234 | - name: Download Artifact (Linux arm) 235 | uses: actions/download-artifact@v4 236 | with: 237 | name: "agent-linux-arm" 238 | path: dist/ 239 | 240 | - name: Release 241 | uses: softprops/action-gh-release@v2 242 | with: 243 | tag_name: v${{ env.NBGUI_VERSION }} 244 | body_path: RELEASE_CHANGELOG.md 245 | prerelease: false 246 | draft: false 247 | generate_release_notes: true 248 | token: ${{ secrets.RELEASE_GITHUB_TOKEN }} 249 | files: | 250 | dist/agent-windows.exe 251 | dist/agent-linux-amd64 252 | dist/agent-linux-arm64 253 | dist/agent-linux-arm 254 | dist/agent-macos 255 | dist/agent-macos-M1 256 | 257 | 258 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://dart.dev/guides/libraries/private-files 2 | # Created by `dart pub` 3 | .dart_tool/ 4 | instance/ 5 | cutl-test.sh 6 | config.json 7 | bots/ 8 | pubspec.lock 9 | img/logo.webp.kra 10 | img/logo.webp~ -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | nonebot-flutter-gui
3 |
4 | 5 | # NoneBot Agent 6 |
7 | 8 | _✨ 使用 API 来管理你的 NoneBot ✨_ 9 | 10 | 11 | license 12 | 13 | 14 | QQ Chat Group 15 | 16 |
17 | 18 | 📖 文档地址 19 | 20 | 21 |
22 | 23 | 24 | 25 | ## 📖 介绍 26 | 27 | 使用 API 来管理你的 NoneBot,支持 HTTP 和 WebSocket 两种方式。 28 | 29 | 30 | ## ⚙️安装 31 | 32 | 即刻前往:[Release](https://github.com/NonebotGUI/nonebot-agent/releases) 33 | 34 | ## 配置 35 | 36 | [查看文档](https://webui.nbgui.top) 37 | 38 | ## 🚀 接口文档 39 | 40 | [HTTP API文档](https://webui.nbgui.top/advanced/http/total.html) 41 | 42 | ## 📑 支持的 API 43 | 44 | - [X] Bot 列表 45 | - [X] Bot 基本信息 46 | - [X] Bot 启动/停止 47 | - [X] Bot 日志 48 | - [X] Bot 重命名 49 | - [X] Bot 删除 50 | - [X] Bot 创建 51 | - [X] Bot 导入 52 | - [X] 版本信息 53 | - [X] 插件启用/禁用 54 | - [X] 插件安装 55 | - [X] 插件卸载 56 | - [X] 插件列表 57 | - [X] 适配器安装 58 | - [X] 驱动器安装 59 | - [ ] nbcli 本体管理 60 | - [X] env.* 配置 61 | - [ ] 文件管理 62 | 63 | 64 | ## 📄 许可 65 | 66 | GPL-3.0 license 67 | 68 | ## 🐧 QQ Group 69 | 70 | 欢迎加入我们的 QQ 群组! 71 | 72 | 73 | 972526136 74 | 75 | -------------------------------------------------------------------------------- /RELEASE_CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 🌈 Changelog 2025.04.05 🔧 2 | by @XTxiaoting14332 3 | 修复插件目录路径不正确的问题 -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the static analysis results for your project (errors, 2 | # warnings, and lints). 3 | # 4 | # This enables the 'recommended' set of lints from `package:lints`. 5 | # This set helps identify many issues that may lead to problems when running 6 | # or consuming Dart code, and enforces writing Dart using a single, idiomatic 7 | # style and format. 8 | # 9 | # If you want a smaller set of lints you can change this to specify 10 | # 'package:lints/core.yaml'. These are just the most critical lints 11 | # (the recommended set includes the core lints). 12 | # The core lints are also what is used by pub.dev for scoring packages. 13 | 14 | include: package:lints/recommended.yaml 15 | 16 | # Uncomment the following section to specify additional rules. 17 | 18 | # linter: 19 | # rules: 20 | # - camel_case_types 21 | 22 | # analyzer: 23 | # exclude: 24 | # - path/to/excluded/files/** 25 | 26 | # For more information about the core and recommended set of lints, see 27 | # https://dart.dev/go/core-lints 28 | 29 | # For additional information about configuring this file, see 30 | # https://dart.dev/guides/language/analysis-options 31 | -------------------------------------------------------------------------------- /bin/nonebot_flutter_webui_agent.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'dart:async'; 4 | import 'package:shelf/shelf.dart'; 5 | import 'package:shelf/shelf_io.dart' as io; 6 | import 'package:shelf_router/shelf_router.dart'; 7 | import '../utils/core.dart'; 8 | import '../utils/global.dart'; 9 | import '../utils/logger.dart'; 10 | import '../utils/manage.dart'; 11 | import '../utils/run_cmd.dart'; 12 | import '../utils/ws_handler.dart'; 13 | 14 | void main() { 15 | runZonedGuarded(() async { 16 | Logger.warn('NoneBot Agent will start in 3 seconds...'); 17 | await Future.delayed(const Duration(seconds: 3)); 18 | Logger.info('Welcome to NoneBot Agent!'); 19 | Logger.info('By 【夜风】NightWind(2125714976@qq.com)'); 20 | Logger.info('Released under the GPL-3 License.'); 21 | Logger.info('Version: ${AgentMain.version()}'); 22 | // 初始化服务器配置 23 | AgentMain.init(); 24 | if (AgentMain.wss()['enabled']) { 25 | Logger.info('wss enabled.'); 26 | Logger.info('Checking certificate files...'); 27 | if (!File(AgentMain.wss()['certpath']).existsSync()) { 28 | Logger.error('Certificate files not found!'); 29 | Logger.error('Please check your configuration file.'); 30 | await Future.delayed(const Duration(seconds: 3)); 31 | exit(1); 32 | } else { 33 | Logger.success('Load certificate: ${AgentMain.wss()['certpath']}'); 34 | } 35 | if (!File(AgentMain.wss()['keypath']).existsSync()) { 36 | Logger.error('Key files not found!'); 37 | Logger.error('Please check your configuration file.'); 38 | await Future.delayed(const Duration(seconds: 3)); 39 | exit(1); 40 | } else { 41 | Logger.success('Load key: ${AgentMain.wss()['keypath']}'); 42 | } 43 | } 44 | final String host = AgentMain.host(); 45 | final int port = AgentMain.port(); 46 | Logger.info("HTTP server is starting..."); 47 | Logger.info('Started server process [$pid]'); 48 | 49 | // 检查token 50 | final String token = AgentMain.token().toString(); 51 | 52 | // 设置SIGINT信号处理器 53 | ProcessSignal.sigint.watch().listen((signal) { 54 | Logger.info('Waiting for applications shutdown'); 55 | Logger.info('Application shutdown completed'); 56 | Logger.info('Finished server process [$pid]'); 57 | exit(0); 58 | }); 59 | 60 | ///监听 bots 文件夹 61 | Stream eventStream = Directory('bots/').watch(); 62 | eventStream.listen((FileSystemEvent event) { 63 | MainApp.botList = AgentMain.loadBots(); 64 | }); 65 | 66 | // 定义错误处理的中间件 67 | Middleware handleErrors() { 68 | return (Handler handler) { 69 | return (Request request) async { 70 | try { 71 | // 处理请求 72 | return await handler(request); 73 | } catch (e, stackTrace) { 74 | // 捕获错误并记录到 Logger 75 | Logger.error( 76 | 'Internal Server Error: $e\nStack Trace:\n$stackTrace'); 77 | // 返回 HTTP 500 错误 78 | return Response( 79 | 500, 80 | body: '{"error": "500 Internal Server Error"}', 81 | headers: {'Content-Type': 'application/json'}, 82 | ); 83 | } 84 | }; 85 | }; 86 | } 87 | 88 | // 创建路由 89 | var router = Router(); 90 | 91 | // 初始化鉴权中间件 92 | Middleware handleAuth({required String token}) { 93 | return (Handler handler) { 94 | return (Request request) async { 95 | final authHeader = request.headers['Authorization']; 96 | 97 | if (authHeader == null || authHeader != 'Bearer $token') { 98 | return Response( 99 | 401, 100 | body: '{"error": "401 Unauthorized!"}', 101 | headers: {'Content-Type': 'application/json'}, 102 | ); 103 | } 104 | return handler(request); 105 | }; 106 | }; 107 | } 108 | 109 | // 统一 Log 输出 110 | Middleware customLogRequests() { 111 | return (Handler innerHandler) { 112 | return (Request request) async { 113 | final watch = Stopwatch()..start(); 114 | final response = await innerHandler(request); 115 | final latency = watch.elapsed; 116 | Logger.api(request.method, response.statusCode, 117 | '${request.url}\t\t${latency.inMilliseconds}ms'); 118 | return response; 119 | }; 120 | }; 121 | } 122 | 123 | // 定义 API 路由 124 | // ping 125 | router.get('/nbgui/v1/ping', (Request request) { 126 | return Response.ok('pong!'); 127 | }); 128 | 129 | // 获取 Bot 列表 130 | router.get('/nbgui/v1/bot/list', (Request request) async { 131 | JsonEncoder encoder = JsonEncoder.withIndent(' '); 132 | String prettyJson = encoder.convert(MainApp.botList); 133 | 134 | return Response.ok(prettyJson, 135 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 136 | }); 137 | 138 | // 获取 Bot 信息 139 | router.get('/nbgui/v1/bot/info/', (Request request, String id) async { 140 | var bot = MainApp.botList.firstWhere( 141 | (bot) => bot['id'] == id, 142 | orElse: () => {'error': 'Bot Not Found!'}, 143 | ); 144 | var encoder = JsonEncoder.withIndent(' '); 145 | String prettyJson = encoder.convert(bot); 146 | return Response.ok(prettyJson, 147 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 148 | }); 149 | 150 | // 获取 Bot 日志 151 | router.get('/nbgui/v1/bot/log/', (Request request, String id) async { 152 | var log = await Bot.log(id); 153 | return Response.ok(log, 154 | headers: {'Content-Type': 'text/plain'}, encoding: utf8); 155 | }); 156 | 157 | // 启动 Bot 158 | router.get('/nbgui/v1/bot/run/', (Request request, String id) async { 159 | if (!Bot.status(id)) { 160 | Bot.run(id); 161 | Logger.success('Bot $id started!'); 162 | return Response.ok('{"status": "Bot $id started!"}', 163 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 164 | } else { 165 | Logger.error('Bot $id is already running!'); 166 | return Response.ok( 167 | '{"code": 1002, "error": "Bot $id is already running!"}', 168 | headers: {'Content-Type': 'application/json'}, 169 | encoding: utf8, 170 | ); 171 | } 172 | }); 173 | 174 | // 停止 Bot 175 | router.get('/nbgui/v1/bot/stop/', (Request request, String id) async { 176 | if (Bot.status(id)) { 177 | Bot.stop(id); 178 | Logger.success('Bot $id stopped!'); 179 | return Response.ok('{"status": "Bot $id stopped!"}', 180 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 181 | } else { 182 | Logger.error('Bot $id is not running!'); 183 | return Response.ok( 184 | '{"code": 1001, "error": "Bot $id is not running!"}', 185 | headers: {'Content-Type': 'application/json'}, 186 | encoding: utf8, 187 | ); 188 | } 189 | }); 190 | 191 | // 重启 Bot 192 | router.get('/nbgui/v1/bot/restart/', 193 | (Request request, String id) async { 194 | gOnOpen = id; 195 | if (Bot.status(id)) { 196 | Bot.stop(id); 197 | await Future.delayed(const Duration(seconds: 1), () { 198 | Bot.run(id); 199 | }); 200 | Logger.success('Bot $id restarted!'); 201 | return Response.ok('{"status": "Bot $id restarted!"}', 202 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 203 | } else { 204 | Logger.error('Bot $id is not running!'); 205 | return Response.ok( 206 | '{"code": 1001, "error": "Bot $id is not running!"}', 207 | headers: {'Content-Type': 'application/json'}, 208 | encoding: utf8, 209 | ); 210 | } 211 | }); 212 | 213 | // 导入 Bot 214 | router.post('/nbgui/v1/bot/import', (Request request) async { 215 | final body = await request.readAsString(); 216 | var bot = jsonDecode(body); 217 | String name = bot['name']; 218 | String path = bot['path']; 219 | // String protocolPath = bot['protocolPath']; 220 | // bool withProtocol = bot['withProtocol']; 221 | // String cmd = bot['cmd']; 222 | Bot.import(name, path, false, '', ''); 223 | Logger.success('Bot $name imported!'); 224 | return Response.ok('{"status": "Bot $name imported!"}', 225 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 226 | }); 227 | 228 | // 创建 Bot 229 | router.post('/nbgui/v1/bot/create', (Request request) async { 230 | final body = await request.readAsString(); 231 | var bot = jsonDecode(body); 232 | String path = bot['path']; 233 | String name = bot['name']; 234 | List drivers = bot['drivers']; 235 | List adapters = bot['adapters']; 236 | String template = bot['template']; 237 | String pluginDir = bot['pluginDir']; 238 | bool venv = bot['venv']; 239 | bool installDep = bot['installDep']; 240 | Logger.info('Bot $name start creating...'); 241 | runInstall( 242 | path, name, drivers, adapters, template, pluginDir, venv, installDep); 243 | return Response.ok('{"status": "Bot $name start creating..."}', 244 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 245 | }); 246 | 247 | // 删除 Bot 248 | router.delete('/nbgui/v1/bot/remove/', 249 | (Request request, String id) async { 250 | if (Bot.status(id)) { 251 | Bot.stop(id); 252 | } 253 | Bot.delete(id); 254 | Logger.success('Bot $id removed!'); 255 | return Response.ok('{"status": "Bot $id removed!"}', 256 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 257 | }); 258 | 259 | // 永久删除 Bot 260 | router.delete('/nbgui/v1/bot/delete/', 261 | (Request request, String id) async { 262 | if (Bot.status(id)) { 263 | Bot.stop(id); 264 | } 265 | Bot.deleteForever(id); 266 | Logger.success('Bot $id deleted!'); 267 | return Response.ok('{"status": "Bot $id deleted!"}', 268 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 269 | }); 270 | 271 | // 重命名 Bot 272 | router.post('/nbgui/v1/bot/rename/', 273 | (Request request, String id) async { 274 | final body = await request.readAsString(); 275 | var bot = jsonDecode(body); 276 | String name = bot['name']; 277 | Bot.rename(name, id); 278 | Logger.success('Bot $id renamed to $name!'); 279 | return Response.ok('{"status": "Bot $id renamed to $name!"}', 280 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 281 | }); 282 | 283 | // 获取系统状态 284 | router.get('/nbgui/v1/system/status', (Request request) async { 285 | return Response.ok(await System.status(), 286 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 287 | }); 288 | 289 | // 获取系统平台 290 | router.get('/nbgui/v1/system/platform', (Request request) async { 291 | return Response.ok(System.platform(), 292 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 293 | }); 294 | 295 | // 获取Agent版本信息 296 | router.get('/nbgui/v1/version', (Request request) async { 297 | Map version = { 298 | 'version': AgentMain.version(), 299 | 'nbcli': MainApp.nbcli.replaceAll('nb: ', '').replaceAll('\n', ''), 300 | 'python': MainApp.python, 301 | }; 302 | return Response.ok(jsonEncode(version), 303 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 304 | }); 305 | 306 | // 安装插件 307 | router.post('/nbgui/v1/plugin/install', (Request request) async { 308 | final body = await request.readAsString(); 309 | var plugin = jsonDecode(body); 310 | String name = plugin['name']; 311 | String id = plugin['id']; 312 | Plugin.install(name, id); 313 | Logger.success('Plugin $name in $id start installing.'); 314 | return Response.ok('{"status": "Plugin $name in $id start installing."}', 315 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 316 | }); 317 | 318 | // 卸载插件 319 | router.post('/nbgui/v1/plugin/uninstall', (Request request) async { 320 | final body = await request.readAsString(); 321 | var plugin = jsonDecode(body); 322 | String name = plugin['name']; 323 | String id = plugin['id']; 324 | Plugin.uninstall(name, id); 325 | Logger.success('Plugin $name in $id start uninstalling.'); 326 | return Response.ok( 327 | '{"status": "Plugin $name in $id start uninstalling."}', 328 | headers: {'Content-Type': 'application/json'}, 329 | encoding: utf8); 330 | }); 331 | 332 | // 获取插件列表 333 | router.get('/nbgui/v1/plugin/list/', (Request request, id) async { 334 | if (File('bots/$id.json').existsSync()) { 335 | return Response.ok(jsonEncode(Plugin.list(id)), 336 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 337 | } else { 338 | return Response.ok('{"code": 1003, "error": "Bot $id not found!"}', 339 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 340 | } 341 | }); 342 | 343 | // 获取已禁用的插件列表 344 | router.get('/nbgui/v1/plugin/disabled/', 345 | (Request request, String id) async { 346 | return Response.ok(Plugin.disabledList(id), 347 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 348 | }); 349 | 350 | // 禁用插件 351 | router.post('/nbgui/v1/plugin/disable', (Request request) async { 352 | final body = await request.readAsString(); 353 | var plugin = jsonDecode(body); 354 | String name = plugin['name']; 355 | String id = plugin['id']; 356 | Plugin.disable(name, id); 357 | Logger.success('Plugin $name in $id disabled.'); 358 | return Response.ok('{"status": "Plugin $name in $id disabled."}', 359 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 360 | }); 361 | 362 | // 启用插件 363 | router.post('/nbgui/v1/plugin/enable', (Request request) async { 364 | final body = await request.readAsString(); 365 | var plugin = jsonDecode(body); 366 | String name = plugin['name']; 367 | String id = plugin['id']; 368 | Plugin.enable(name, id); 369 | Logger.success('Plugin $name in $id enabled.'); 370 | return Response.ok('{"status": "Plugin $name in $id enabled."}', 371 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 372 | }); 373 | 374 | // 安装适配器 375 | router.post('/nbgui/v1/adapter/install', (Request request) async { 376 | final body = await request.readAsString(); 377 | var adapter = jsonDecode(body); 378 | String name = adapter['name']; 379 | String id = adapter['id']; 380 | Adapter.install(name, id); 381 | Logger.success('Adapter $name in $id start installing.'); 382 | return Response.ok('{"status": "Adapter $name in $id start installing."}', 383 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 384 | }); 385 | 386 | // 安装驱动器 387 | router.post('/nbgui/v1/driver/install', (Request request) async { 388 | final body = await request.readAsString(); 389 | var driver = jsonDecode(body); 390 | String name = driver['name']; 391 | String id = driver['id']; 392 | Driver.install(name, id); 393 | Logger.success('Driver $name in $id start installing.'); 394 | return Response.ok('{"status": "Driver $name in $id start installing."}', 395 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 396 | }); 397 | 398 | // 获取env文件内容 399 | router.get('/nbgui/v1/bot/env//', 400 | (Request request, String id, String env) async { 401 | if (['.env', '.env.prod', '.env.dev'].contains(env)) { 402 | if (File('${Bot.path(id)}/$env').existsSync()) { 403 | String content = File('${Bot.path(id)}/$env').readAsStringSync(); 404 | Map res = {'content': content}; 405 | return Response.ok(jsonEncode(res), 406 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 407 | } else { 408 | return Response.ok('{"code": 1004, "error": "Env $env not found!"}', 409 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 410 | } 411 | } else { 412 | return Response.ok('{"code": 1005, "error": "Not allowed operation!"}', 413 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 414 | } 415 | }); 416 | 417 | // 编辑env文件内容 418 | router.post('/nbgui/v1/bot/env//', 419 | (Request request, String id, String env) async { 420 | if (['.env', '.env.prod', '.env.dev'].contains(env)) { 421 | final body = await request.readAsString(); 422 | var data = jsonDecode(body); 423 | String content = data['content']; 424 | if (!File('${Bot.path(id)}/$env').existsSync()) { 425 | File('${Bot.path(id)}/$env').create(recursive: true); 426 | } 427 | File('${Bot.path(id)}/$env').writeAsStringSync(content); 428 | return Response.ok('{"status": "Env $env updated!"}', 429 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 430 | } else { 431 | return Response.ok('{"code": 1005, "error": "Not allowed operation!"}', 432 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 433 | } 434 | }); 435 | 436 | ///新增Env配置项 437 | router.post('/nbgui/v1/bot/env/add/', 438 | (Request request, String id) async { 439 | final body = await request.readAsString(); 440 | var data = jsonDecode(body); 441 | String key = data['varName']; 442 | String value = data['varValue']; 443 | String env = data['env']; 444 | if (['.env', '.env.prod', '.env.dev'].contains(env)) { 445 | if (File('${Bot.path(id)}/$env').existsSync()) { 446 | Env.add(id, env, key, value); 447 | return Response.ok('{"status": "Env $env updated!"}', 448 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 449 | } else { 450 | File('${Bot.path(id)}/$env').create(recursive: true); 451 | File('${Bot.path(id)}/$env').writeAsStringSync('$key=$value'); 452 | return Response.ok('{"status": "Env $env updated!"}', 453 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 454 | } 455 | } else { 456 | return Response.ok('{"code": 1005, "error": "Not allowed operation!"}', 457 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 458 | } 459 | }); 460 | 461 | ///删除Env配置项 462 | router.post('/nbgui/v1/bot/env/delete/', 463 | (Request request, String id) async { 464 | final body = await request.readAsString(); 465 | var data = jsonDecode(body); 466 | String key = data['varName']; 467 | String env = data['env']; 468 | if (['.env', '.env.prod', '.env.dev'].contains(env)) { 469 | if (File('${Bot.path(id)}/$env').existsSync()) { 470 | Env.delete(id, env, key); 471 | return Response.ok('{"status": "Env $env updated!"}', 472 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 473 | } else { 474 | return Response.ok('{"code": 1004, "error": "Env $env not found!"}', 475 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 476 | } 477 | } else { 478 | return Response.ok('{"code": 1005, "error": "Not allowed operation!"}', 479 | headers: {'Content-Type': 'application/json'}, encoding: utf8); 480 | } 481 | }); 482 | 483 | // WebSocket 路由 484 | router.get('/nbgui/v1/ws', (Request request) { 485 | return wsHandler(request); 486 | }); 487 | 488 | // 定义404错误处理 489 | router.all('/', (Request request) { 490 | return Response.notFound('404 Not Found: ${request.url}'); 491 | }); 492 | 493 | // 配置中间件 494 | var httpHandler = const Pipeline() 495 | .addMiddleware(customLogRequests()) 496 | .addMiddleware(handleAuth(token: token)) 497 | .addMiddleware(handleErrors()) 498 | .addHandler(router.call); 499 | 500 | // 启动服务器 501 | if (AgentMain.wss()['enabled']) { 502 | await io.serve((Request request) { 503 | if (request.url.path == 'nbgui/v1/ws') { 504 | return wsHandler(request); 505 | } 506 | return httpHandler(request); 507 | }, host, port, 508 | securityContext: SecurityContext() 509 | ..useCertificateChain(AgentMain.wss()['certpath']) 510 | ..usePrivateKey(AgentMain.wss()['keypath'])); 511 | } else { 512 | await io.serve((Request request) { 513 | if (request.url.path == 'nbgui/v1/ws') { 514 | return wsHandler(request); 515 | } 516 | return httpHandler(request); 517 | }, host, port); 518 | } 519 | 520 | if (AgentMain.wss()['enabled']) { 521 | if (host.contains(":")) { 522 | Logger.info( 523 | 'HTTPS server listening on https://[$host]:$port (Ctrl+C to quit)'); 524 | Logger.info( 525 | 'WebSocket server listening on wss://[$host]:$port/nbgui/v1/ws'); 526 | } else { 527 | Logger.info('Serving at https://$host:$port (Ctrl+C to quit)'); 528 | Logger.info( 529 | 'WebSocket server listening on wss://$host:$port/nbgui/v1/ws'); 530 | } 531 | } else { 532 | if (host.contains(":")) { 533 | Logger.info( 534 | 'HTTP server listening on http://[$host]:$port (Ctrl+C to quit)'); 535 | Logger.info( 536 | 'WebSocket server listening on ws://[$host]:$port/nbgui/v1/ws'); 537 | } else { 538 | Logger.info('Serving at http://$host:$port (Ctrl+C to quit)'); 539 | Logger.info( 540 | 'WebSocket server listening on ws://$host:$port/nbgui/v1/ws'); 541 | } 542 | } 543 | }, (error, stackTrace) { 544 | Logger.error('Unhandled Exception: $error\nStack Trace:\n$stackTrace'); 545 | }); 546 | } 547 | -------------------------------------------------------------------------------- /curl-test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | # 读取参数 5 | METHOD=$1 6 | ENDPOINT=$2 7 | DATA=$3 8 | 9 | # 根据请求类型决定是否发送数据 10 | if [ "$METHOD" = "GET" ]; then 11 | # GET请求,不发送数据 12 | curl -X "$METHOD" -H "Authorization: Bearer 114514" http://localhost:2519/nbgui/v1/$ENDPOINT 13 | else 14 | # POST或其他请求,发送数据 15 | curl -X "$METHOD" -H "Content-Type: application/json" -H "Authorization: Bearer 114514" -d "$DATA" http://localhost:2519/nbgui/v1/$ENDPOINT 16 | fi 17 | -------------------------------------------------------------------------------- /img/logo.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NonebotGUI/nonebot-agent/7f2dca89295550438dc5df1d310186374b5fc811/img/logo.webp -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: nonebot_flutter_webui_agent 2 | description: 通过 API 管理你的 NoneBot 3 | version: 0.1.6+3 4 | # repository: https://github.com/my_org/my_repo 5 | 6 | environment: 7 | sdk: ^3.3.1 8 | 9 | # Add regular dependencies here. 10 | dependencies: 11 | ansicolor: ^2.0.3 12 | http: ^1.2.2 13 | shelf: ^1.4.1 14 | shelf_router: ^1.1.4 15 | shelf_web_socket: ^2.0.0 16 | toml: ^0.16.0 17 | uuid: ^4.5.1 18 | web_socket_channel: ^3.0.1 19 | # path: ^1.8.0 20 | 21 | dev_dependencies: 22 | lints: ^3.0.0 23 | test: ^1.24.0 24 | -------------------------------------------------------------------------------- /utils/core.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'package:uuid/uuid.dart'; 4 | import 'global.dart'; 5 | import 'logger.dart'; 6 | import 'user_config.dart'; 7 | import 'package:http/http.dart' as http; 8 | 9 | class AgentMain { 10 | /// 软件版本 11 | static String version() { 12 | return '0.1.6+3'; 13 | } 14 | 15 | /// 初始化用户配置文件 16 | static Map _config() { 17 | File file = File('config.json'); 18 | String content = file.readAsStringSync(); 19 | return jsonDecode(content); 20 | } 21 | 22 | ///初始化应用程序 23 | static void init() async { 24 | Logger.rainbow('logo', 25 | ' _ _ ____ _ _ _ '); 26 | Logger.rainbow('logo', 27 | ' | \\ | | ___ _ __ ___| __ ) ___ | |_ / \\ __ _ ___ _ __ | |_ '); 28 | Logger.rainbow('logo', 29 | ' | \\| |/ _ \\| \'_ \\ / _ | _ \\ / _ \\| __| / _ \\ / _` |/ _ | \'_ \\| __|'); 30 | Logger.rainbow('logo', 31 | ' | |\\ | (_) | | | | __| |_) | (_) | |_ / ___ | (_| | __| | | | |_ '); 32 | Logger.rainbow('logo', 33 | ' |_| \\_|\\___/|_| |_|\\___|____/ \\___/ \\__| /_/ \\_\\__, |\\___|_| |_|\\__|'); 34 | Logger.rainbow('logo', 35 | ' |___/ '); 36 | File file = File('config.json'); 37 | if (!file.existsSync()) { 38 | Logger.warn("Config file not found, creating a new one..."); 39 | file.createSync(); 40 | const String content = ''' 41 | { 42 | "host": "0.0.0.0", 43 | "port": 2519, 44 | "token": "", 45 | "logMaxLines": 75, 46 | "python":"default", 47 | "nbcli":"default", 48 | "color":"light", 49 | "checkUpdate": true, 50 | "wss": { 51 | "enabled": false, 52 | "certpath": "", 53 | "keypath": "" 54 | } 55 | } 56 | '''; 57 | file.writeAsStringSync(content); 58 | Logger.success("Config file created successfully."); 59 | } 60 | Directory botDir = Directory('bots/'); 61 | Directory instanceDir = Directory('instance/'); 62 | Directory cacheDir = Directory('cache/'); 63 | if (!botDir.existsSync()) { 64 | botDir.createSync(); 65 | } 66 | if (!instanceDir.existsSync()) { 67 | instanceDir.createSync(); 68 | } 69 | if (!cacheDir.existsSync()) { 70 | cacheDir.createSync(); 71 | } 72 | MainApp.botList = AgentMain.loadBots(); 73 | await getPyVer(); 74 | await getnbcliver(); 75 | if (MainApp.python == '你似乎还没有安装python?') { 76 | Logger.warn('It seems that you have not installed python?'); 77 | } 78 | if (MainApp.nbcli == '你似乎还没有安装nb-cli?') { 79 | Logger.warn('It seems that you have not installed nb-cli?'); 80 | } 81 | if (UserConfig.checkUpdate()) { 82 | await check(); 83 | } 84 | } 85 | 86 | /// 获取主机 87 | static String host() { 88 | Map jsonMap = _config(); 89 | if (jsonMap.containsKey("host")) { 90 | String host = jsonMap['host'].toString(); 91 | return host; 92 | } else { 93 | return 'localhost'; 94 | } 95 | } 96 | 97 | /// 获取端口 98 | static int port() { 99 | Map jsonMap = _config(); 100 | if (jsonMap.containsKey("httpPort")) { 101 | int port = jsonMap['httpPort']; 102 | return port; 103 | } else { 104 | return 2519; 105 | } 106 | } 107 | 108 | /// wss是否启用(beta) 109 | static Map wss() { 110 | Map jsonMap = _config(); 111 | if (jsonMap.containsKey("wss")) { 112 | Map wss = jsonMap['wss']; 113 | return wss; 114 | } else { 115 | return {"enabled": false, "certpath": "", "keypath": ""}; 116 | } 117 | } 118 | 119 | /// 获取token 120 | static String? token() { 121 | Map jsonMap = _config(); 122 | if (jsonMap['token'].toString().isNotEmpty) { 123 | String token = jsonMap['token'].toString(); 124 | return token; 125 | } else { 126 | Logger.error("Token is empty, please set it in agent.json."); 127 | exit(1); 128 | } 129 | } 130 | 131 | /// 读取配置文件 132 | static List loadBots() { 133 | final jsonList = []; 134 | Directory dir = Directory('bots'); 135 | List files = dir.listSync(); 136 | for (FileSystemEntity file in files) { 137 | if (file is File && file.path.endsWith('.json')) { 138 | String contents = file.readAsStringSync(); 139 | var jsonObject = jsonDecode(contents); 140 | jsonList.add(jsonObject); 141 | } 142 | } 143 | 144 | return jsonList; 145 | } 146 | } 147 | 148 | /// 主机系统监控 149 | class System { 150 | /// 获取系统状态 151 | static status() async { 152 | // Linux 153 | if (Platform.isLinux) { 154 | // 获取 CPU 使用率 155 | var getCpuStatus = await Process.run( 156 | 'bash', 157 | [ 158 | '-c', 159 | 'top -bn1 | grep "Cpu(s)" | sed "s/.*, *\\([0-9.]*\\)%* id.*/\\1/" | awk \'{print 100 - \$1}\'' 160 | ], 161 | ); 162 | String cpuUsage = getCpuStatus.stdout.toString().trim(); 163 | 164 | // 获取内存使用率 165 | var getMemStatus = await Process.run( 166 | 'bash', 167 | ['-c', "LC_ALL='c' free | grep Mem | awk '{print \$3/\$2 * 100.0}'"], 168 | ); 169 | String memUsage = getMemStatus.stdout.toString().trim().substring(0, 4); 170 | 171 | return '{"cpu_usage": "$cpuUsage%", "ram_usage": "$memUsage%"}'; 172 | } 173 | 174 | // Windows 175 | if (Platform.isWindows) { 176 | // 获取 CPU 使用率 177 | String cpuCommand = 178 | 'Get-CimInstance Win32_Processor | Select-Object -ExpandProperty LoadPercentage'; 179 | var getCpuStatus = 180 | await Process.run('powershell', ['-Command', cpuCommand]); 181 | String cpuUsage = getCpuStatus.stdout.toString().trim(); 182 | 183 | // 获取内存使用率 184 | String memCommand = ''' 185 | Get-CimInstance Win32_OperatingSystem | 186 | Select-Object @{Name="MemoryUsage";Expression={"{0:N2}" -f ((\$_.TotalVisibleMemorySize - \$_.FreePhysicalMemory) / \$_.TotalVisibleMemorySize * 100)}} | 187 | Select-Object -ExpandProperty MemoryUsage 188 | '''; 189 | var getMemStatus = 190 | await Process.run('powershell', ['-Command', memCommand]); 191 | String memUsage = getMemStatus.stdout.toString().trim().substring(0, 4); 192 | 193 | return '{"cpu_usage": "$cpuUsage%", "ram_usage": "$memUsage%"}'; 194 | } 195 | 196 | if (Platform.isMacOS) { 197 | // 获取内存使用率 198 | const script = ''' 199 | pages_info=\$(vm_stat | grep "Pages" | awk '{print \$NF}' | sed 's/[^0-9]//g') 200 | active_pages=\$(echo "\$pages_info" | sed -n '2p') 201 | inactive_pages=\$(echo "\$pages_info" | sed -n '3p') 202 | wired_pages=\$(echo "\$pages_info" | sed -n '6p') 203 | active_pages=\${active_pages:-0} 204 | inactive_pages=\${inactive_pages:-0} 205 | wired_pages=\${wired_pages:-0} 206 | used_pages=\$((active_pages + inactive_pages + wired_pages)) 207 | total_mem=\$(sysctl hw.memsize | awk '{print \$2}') 208 | page_size=4096 209 | total_pages=\$((total_mem / page_size)) 210 | if [ \$total_pages -gt 0 ]; then 211 | echo "scale=2; \$used_pages * 100 / \$total_pages" | bc 212 | else 213 | echo "0" 214 | fi 215 | '''; 216 | 217 | // 获取 CPU 使用率 218 | var getCpuStatus = await Process.run( 219 | 'bash', 220 | ['-c', 'top -l 1 | grep "CPU usage" | awk \'{print \$3}\''], 221 | ); 222 | String cpuUsage = getCpuStatus.stdout.toString().trim(); 223 | 224 | // 获取内存使用率 225 | var getMemStatus = await Process.run( 226 | 'bash', 227 | ['-c', script], 228 | ); 229 | String memUsage = getMemStatus.stdout.toString().trim(); 230 | 231 | return '{"cpu_usage": "$cpuUsage%", "ram_usage": "$memUsage%"}'; 232 | } 233 | 234 | return '{"error": "Unsupported platform"}'; 235 | } 236 | 237 | /// 获取系统平台 238 | static platform() { 239 | if (Platform.isLinux) { 240 | return '{"platform": "Linux"}'; 241 | } 242 | if (Platform.isMacOS) { 243 | return '{"platform": "macOS"}'; 244 | } 245 | if (Platform.isWindows) { 246 | return '{"platform": "Windows"}'; 247 | } 248 | return {"error": "Unsupported platform"}; 249 | } 250 | } 251 | 252 | // 生成uuid 253 | String generateUUID() { 254 | var uuid = Uuid(); 255 | String v4 = uuid.v4(); 256 | String id = v4.replaceAll('-', ''); 257 | return id; 258 | } 259 | 260 | // 向客户端发送WebSocket消息 261 | void sendMessageToClients(String message) { 262 | for (var client in wsChannels) { 263 | client.sink.add(message); 264 | } 265 | } 266 | 267 | ///检查py 268 | Future getPyVer() async { 269 | try { 270 | ProcessResult results = 271 | await Process.run('${UserConfig.pythonPath()}', ['--version']); 272 | MainApp.python = results.stdout.trim(); 273 | return MainApp.python; 274 | } catch (e) { 275 | MainApp.python = '你似乎还没有安装python?'; 276 | return MainApp.python; 277 | } 278 | } 279 | 280 | ///检查nbcli 281 | Future getnbcliver() async { 282 | try { 283 | final ProcessResult results = 284 | await Process.run('${UserConfig.nbcliPath()}', ['-V']); 285 | MainApp.nbcli = results.stdout; 286 | return MainApp.nbcli; 287 | } catch (error) { 288 | MainApp.nbcli = '你似乎还没有安装nb-cli?'; 289 | return MainApp.nbcli; 290 | } 291 | } 292 | 293 | Future check() async { 294 | try { 295 | final response = await http.get(Uri.parse( 296 | 'https://api.github.com/repos/NonebotGUI/nonebot-agent/releases/latest')); 297 | if (response.statusCode == 200) { 298 | final jsonData = jsonDecode(response.body); 299 | final tagName = jsonData['tag_name']; 300 | final url = jsonData['html_url']; 301 | if (tagName.toString().replaceAll('v', '') != AgentMain.version()) { 302 | Logger.rainbow('New version', 303 | '################################################################'); 304 | Logger.rainbow('New version', 305 | '## ##'); 306 | Logger.rainbow('New version', 307 | '## A new version of Nonebot Agent is available! ##'); 308 | Logger.rainbow('New version', 309 | '## ##'); 310 | Logger.rainbow('New version', 311 | '################################################################'); 312 | Logger.info('New version found: $tagName'); 313 | Logger.info('To download the latest version, please visit: $url'); 314 | } 315 | } else { 316 | Logger.error('Failed to check for updates'); 317 | } 318 | } catch (e) { 319 | Logger.error('Failed to check for updates: $e'); 320 | } 321 | } 322 | -------------------------------------------------------------------------------- /utils/deploy_bot.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'core.dart'; 3 | import 'global.dart'; 4 | import 'user_config.dart'; 5 | 6 | ///部署Bot时的相关操作 7 | class DeployBot { 8 | ///写入requirements.txt 9 | static writeReq(String path, String name, drivers, adapters) { 10 | String driverList = drivers 11 | .map((d) => 'nonebot2[${d.toString().toLowerCase()}]') 12 | .join('\n'); 13 | String adapterList = adapters.join('\n'); 14 | 15 | File('$path/$name/requirements.txt').writeAsStringSync( 16 | '$driverList\n${adapterList.replaceAll('.v11', '').replaceAll('.v12', '')}'); 17 | return 'echo 写入依赖...'; 18 | } 19 | 20 | ///安装依赖 21 | static String install(String path, String name, bool venv, bool installDep) { 22 | if (!venv) return 'echo 虚拟环境已关闭,跳过...'; 23 | 24 | String requirementsPath = '$path/$name/requirements.txt'; 25 | if (!installDep) { 26 | return 'echo 跳过依赖安装...'; 27 | } 28 | 29 | String pipInstallCmd; 30 | if (Platform.isLinux || Platform.isMacOS) { 31 | pipInstallCmd = '$path/$name/.venv/bin/pip install -r $requirementsPath'; 32 | } else if (Platform.isWindows) { 33 | pipInstallCmd = 34 | '$path\\$name\\.venv\\Scripts\\pip.exe install -r $requirementsPath'; 35 | } else { 36 | pipInstallCmd = 37 | '${UserConfig.pythonPath()} -m pip install -r $requirementsPath'; 38 | } 39 | return pipInstallCmd; 40 | } 41 | 42 | ///创建虚拟环境 43 | static String createVENV(String path, String name, bool venv) { 44 | if (!venv) return 'echo 虚拟环境已关闭,跳过...'; 45 | 46 | String createVenvCmd = 47 | '${UserConfig.pythonPath()} -m venv $path/$name/.venv --prompt $name'; 48 | if (Platform.isWindows) { 49 | createVenvCmd = 50 | '${UserConfig.pythonPath()} -m venv $path\\$name\\.venv --prompt $name'; 51 | } 52 | return createVenvCmd; 53 | } 54 | 55 | static createVENVEcho(String path, String name) { 56 | String formattedPath = 57 | Platform.isWindows ? '$path\\$name\\.venv\\' : '$path/$name/.venv/'; 58 | return 'echo 在$formattedPath中创建虚拟环境'; 59 | } 60 | 61 | ///创建目录 62 | static createFolder( 63 | String path, String name, String template, String pluginDir) { 64 | Directory('$path/$name').createSync(recursive: true); 65 | Directory('bots/').createSync(recursive: true); 66 | 67 | if (template == 'simple(插件开发者)') { 68 | String pluginsPath = pluginDir == '在[bot名称]/[bot名称]下' 69 | ? '$path/$name/$name/plugins' 70 | : '$path/$name/src/plugins'; 71 | Directory(pluginsPath).createSync(recursive: true); 72 | } 73 | return 'echo 创建目录'; 74 | } 75 | 76 | ///写入.env文件 77 | static writeENV( 78 | String path, String name, String port, String template, List drivers) { 79 | String driverlist = drivers 80 | .map((driver) => '~${driver.toString().toLowerCase()}') 81 | .join('+'); 82 | String envContent; 83 | if (template == 'bootstrap(初学者或用户)') { 84 | envContent = port.isEmpty 85 | ? 'DRIVER=$driverlist' 86 | : 'DRIVER=$driverlist\nPORT=$port'; 87 | File('$path/$name/.env.prod').writeAsStringSync(envContent); 88 | } else if (template == 'simple(插件开发者)') { 89 | envContent = 'ENVIRONMENT=dev\nDRIVER=$driverlist'; 90 | File('$path/$name/.env').writeAsStringSync(envContent); 91 | File('$path/$name/.env.dev').writeAsStringSync( 92 | port.isNotEmpty ? 'LOG_LEVEL=DEBUG' : 'LOG_LEVEL=DEBUG\nPORT=$port'); 93 | File('$path/$name/.env.prod').createSync(); 94 | } 95 | return 'echo 写入.env文件'; 96 | } 97 | 98 | ///写入pyproject.toml 99 | static writePyProject( 100 | path, name, adapters, String template, String pluginDir) { 101 | String adapterList = adapters 102 | .map((adapter) => 103 | '{ name = "${adapter.replaceAll('nonebot-adapter-', '').replaceAll('.', ' ')}", module_name = "${adapter.replaceAll('-', '.').replaceAll('adapter', 'adapters')}" }') 104 | .join(','); 105 | 106 | String pyproject = ''' 107 | [project] 108 | name = "$name" 109 | version = "0.1.0" 110 | description = "$name" 111 | readme = "README.md" 112 | requires-python = ">=3.9, <4.0" 113 | 114 | [tool.nonebot] 115 | adapters = [ 116 | $adapterList 117 | ] 118 | plugins = [] 119 | plugin_dirs = ${template == 'simple(插件开发者)' ? (pluginDir == '在[bot名称]/[bot名称]下' ? '["$name/plugins"]' : '["src/plugins"]') : '[]'} 120 | builtin_plugins = ["echo"] 121 | '''; 122 | 123 | File('$path/$name/pyproject.toml').writeAsStringSync(pyproject); 124 | return "echo 写入pyproject.toml"; 125 | } 126 | 127 | ///写入Bot的json文件 128 | static writeBot( 129 | String name, String path, String type, String protocolPath, String cmd) { 130 | DateTime now = DateTime.now(); 131 | String time = 132 | "${now.year}年${now.month}月${now.day}日${now.hour}时${now.minute}分${now.second}秒"; 133 | String id = generateUUID(); 134 | 135 | String botInfo = ''' 136 | { 137 | "name": "$name", 138 | "path": "${path.replaceAll('\\', "\\\\")}${Platform.isWindows ? '\\\\' : '/'}$name", 139 | "time": "$time", 140 | "id": "$id", 141 | "isRunning": false, 142 | "pid": "Null", 143 | "type": "$type", 144 | "protocolPath": "$protocolPath", 145 | "cmd": "$cmd", 146 | "protocolPid": "Null", 147 | "protocolIsRunning": false 148 | } 149 | '''; 150 | 151 | File('bots/$id.json').writeAsStringSync(botInfo); 152 | return "echo 写入json"; 153 | } 154 | } 155 | 156 | ///协议端部署相关操作 157 | class DeployProtocol { 158 | ///设置协议端的cmd 159 | static setCmd(jsonMap) { 160 | if (Platform.isWindows) { 161 | FastDeploy.cmd = jsonMap['cmdWin']; 162 | if (FastDeploy.needQQ) { 163 | FastDeploy.cmd = 164 | FastDeploy.cmd.replaceAll('NBGUI.QQNUM', FastDeploy.botQQ); 165 | } 166 | } else if (Platform.isLinux || Platform.isMacOS) { 167 | FastDeploy.cmd = jsonMap['cmd']; 168 | if (FastDeploy.needQQ) { 169 | FastDeploy.cmd = 170 | FastDeploy.cmd.replaceAll('NBGUI.QQNUM', FastDeploy.botQQ); 171 | } 172 | } 173 | } 174 | 175 | ///写入协议端配置文件 176 | static Future writeConfig() async { 177 | if (FastDeploy.extDir.isEmpty) { 178 | print('extDir is null'); 179 | return; 180 | } 181 | 182 | // 配置文件绝对路径 183 | String path = '${FastDeploy.extDir}/${FastDeploy.configPath}'; 184 | File pcfg = File(FastDeploy.needQQ 185 | ? path.replaceAll('NBGUI.QQNUM', FastDeploy.botQQ) 186 | : path); 187 | 188 | // 将wsPort转为int类型 189 | String content = FastDeploy.botConfig 190 | .toString() 191 | .replaceAll('NBGUI.HOST:NBGUI.PORT', 192 | "${FastDeploy.wsHost}:${FastDeploy.wsPort}") 193 | .replaceAll('"NBGUI.PORT"', FastDeploy.wsPort) 194 | .replaceAll('NBGUI.HOST', FastDeploy.wsHost); 195 | 196 | await pcfg.writeAsString(content); 197 | 198 | if (Platform.isLinux || Platform.isMacOS) { 199 | // 给予执行权限 200 | await Process.run('chmod', ['+x', FastDeploy.cmd], 201 | workingDirectory: FastDeploy.extDir, runInShell: true); 202 | } 203 | } 204 | 205 | ///写入requirements.txt和pyproject.toml 206 | static writeReq(name, adapter, drivers) { 207 | drivers = drivers.toLowerCase(); 208 | String driverlist = 209 | drivers.split(',').map((driver) => 'nonebot2[$driver]').join(','); 210 | driverlist = driverlist.replaceAll(',', '\n'); 211 | String reqs = "$driverlist\n$adapter"; 212 | File('${FastDeploy.path}/requirements.txt').writeAsStringSync(reqs); 213 | if (FastDeploy.template == 'bootstrap(初学者或用户)') { 214 | String pyproject = ''' 215 | [project] 216 | name = "$name" 217 | version = "0.1.0" 218 | description = "$name" 219 | readme = "README.md" 220 | requires-python = ">=3.8, <4.0" 221 | 222 | [tool.nonebot] 223 | adapters = [ 224 | { name = "onebot v11", module_name = "nonebot.adapters.onebot.v11" } 225 | ] 226 | plugins = [] 227 | plugin_dirs = [] 228 | builtin_plugins = ["echo"] 229 | '''; 230 | File('${FastDeploy.path}/pyproject.toml').writeAsStringSync(pyproject); 231 | } else if (FastDeploy.template == 'simple(插件开发者)') { 232 | if (FastDeploy.pluginDir == '在src文件夹下') { 233 | String pyproject = ''' 234 | [project] 235 | name = "$name" 236 | version = "0.1.0" 237 | description = "$name" 238 | readme = "README.md" 239 | requires-python = ">=3.8, <4.0" 240 | 241 | [tool.nonebot] 242 | adapters = [ 243 | { name = "$adapter", module_name = "nonebot.adapters.onebot.v11" } 244 | ] 245 | plugins = [] 246 | plugin_dirs = ["src/plugins"] 247 | builtin_plugins = ["echo"] 248 | '''; 249 | File('${FastDeploy.path}/pyproject.toml').writeAsStringSync(pyproject); 250 | } else { 251 | String pyproject = ''' 252 | [project] 253 | name = "$name" 254 | version = "0.1.0" 255 | description = "$name" 256 | readme = "README.md" 257 | requires-python = ">=3.8, <4.0" 258 | 259 | [tool.nonebot] 260 | adapters = [ 261 | { name = "onebot v11", module_name = "nonebot.adapters.onebot.v11" } 262 | ] 263 | plugins = [] 264 | plugin_dirs = ["$name/plugins"] 265 | builtin_plugins = ["echo"] 266 | '''; 267 | File('${FastDeploy.path}/pyproject.toml').writeAsStringSync(pyproject); 268 | } 269 | } 270 | return 'echo 写入依赖...'; 271 | } 272 | } 273 | -------------------------------------------------------------------------------- /utils/global.dart: -------------------------------------------------------------------------------- 1 | import 'package:web_socket_channel/web_socket_channel.dart'; 2 | 3 | //全局变量 4 | 5 | late String gOnOpen = ""; 6 | List wsChannels = []; 7 | 8 | class MainApp { 9 | ///Bot列表 10 | static late List botList; 11 | 12 | ///用户python版本 13 | static late String python; 14 | 15 | ///用户nbcli版本 16 | static late String nbcli; 17 | } 18 | 19 | ///快速部署相关 20 | class FastDeploy { 21 | ///快速部署页面 22 | static late int page; 23 | 24 | ///编号 25 | static late int id; 26 | 27 | ///下载连接 28 | static late List dlLink; 29 | 30 | ///部署时是否启用虚拟环境 31 | static late bool venv; 32 | 33 | ///部署路径 34 | static late String path; 35 | 36 | ///选择的路径 37 | static late String selectPath; 38 | 39 | ///部署名称 40 | static late String name; 41 | 42 | ///Websocket主机 43 | static late String wsHost; 44 | 45 | ///Websocket端口 46 | static late String wsPort; 47 | 48 | ///协议端本体解压后的目录 49 | static late String extDir; 50 | 51 | ///协议端配置文件 52 | static late String botConfig; 53 | 54 | ///协议端配置文件是否包含QQ号 55 | static late bool needQQ; 56 | 57 | ///如果需要QQ号,那么Bot的QQ号是?(诶诶真麻烦) 58 | static late String botQQ; 59 | 60 | ///协议端配置文件的相对路径 61 | static late String configPath; 62 | 63 | ///协议端配置文件的名称 64 | static late String configName; 65 | 66 | ///配套安装的驱动器 67 | static late String driver; 68 | 69 | ///配套安装的适配器 70 | static late String adapter; 71 | 72 | ///启动协议端的命令 73 | static late String cmd; 74 | 75 | ///模板 76 | static late String template; 77 | 78 | ///插件存放位置 79 | static late String pluginDir; 80 | 81 | ///协议端文件名 82 | static late String protocolFileName; 83 | } 84 | 85 | ///创建Bot相关 86 | class Create { 87 | ///Bot名称 88 | static late String name; 89 | 90 | ///Bot路径 91 | static late String? path; 92 | 93 | ///是否启用虚拟环境 94 | static late bool venv; 95 | 96 | ///是否立刻安装依赖 97 | static late bool installDep; 98 | 99 | ///适配器 100 | static late String adapter; 101 | 102 | ///驱动器 103 | static late String driver; 104 | 105 | ///模板 106 | static late String template; 107 | 108 | ///插件存放位置 109 | static late String pluginDir; 110 | } 111 | -------------------------------------------------------------------------------- /utils/logger.dart: -------------------------------------------------------------------------------- 1 | import 'dart:io'; 2 | import 'dart:math'; 3 | import 'package:ansicolor/ansicolor.dart'; 4 | 5 | class Logger { 6 | // 使用 AnsiPen 来设置颜色 7 | static final AnsiPen _darkGreenPen = AnsiPen()..green(bold: true); 8 | static final AnsiPen _redPen = AnsiPen()..red(); 9 | static final AnsiPen _greenPen = AnsiPen()..green(); 10 | static final AnsiPen _yellowPen = AnsiPen()..yellow(); 11 | static final AnsiPen _bluePen = AnsiPen()..blue(); 12 | static final AnsiPen _purplePen = AnsiPen()..magenta(); 13 | static final AnsiPen _cyanPen = AnsiPen()..cyan(); 14 | static final AnsiPen _lightGreenPen = AnsiPen()..xterm(118); 15 | 16 | static AnsiPen _getRandomRainbowColor() { 17 | final random = Random(); 18 | return _rainbowColors[random.nextInt(_rainbowColors.length)]; 19 | } 20 | 21 | static String _applyRainbowEffect(String text) { 22 | final random = Random(); 23 | StringBuffer coloredText = StringBuffer(); 24 | 25 | for (var char in text.split('')) { 26 | AnsiPen randomColor = 27 | _rainbowColors[random.nextInt(_rainbowColors.length)]; 28 | coloredText.write(randomColor(char)); 29 | } 30 | 31 | return coloredText.toString(); 32 | } 33 | 34 | // 彩虹颜色数组 35 | static final List _rainbowColors = [ 36 | AnsiPen()..red(), 37 | AnsiPen()..green(), 38 | AnsiPen()..yellow(), 39 | AnsiPen()..blue(), 40 | AnsiPen()..magenta(), 41 | AnsiPen()..cyan(), 42 | AnsiPen()..white(), 43 | ]; 44 | 45 | static void _log(String logLevel, String message, AnsiPen levelColor) { 46 | String timestamp = _getTimestamp(); 47 | String agent = " NoneBot Agent "; 48 | String coloredTimestamp = _darkGreenPen(timestamp); 49 | String coloredLogLevel = levelColor(logLevel); 50 | String coloredAgent = _purplePen(agent); 51 | String coloredMessage = message; 52 | String coloredLog = 53 | '$coloredTimestamp [$coloredLogLevel] |$coloredAgent| $coloredMessage'; 54 | stdout.writeln(coloredLog); 55 | } 56 | 57 | static void _apiLog( 58 | String method, int statusCode, String message, AnsiPen levelColor) { 59 | String timestamp = _getTimestamp(); 60 | String agent = " NoneBot Agent "; 61 | String coloredTimestamp = _darkGreenPen(timestamp); 62 | String coloredLogLevel = levelColor(method); 63 | String coloredAgent = _purplePen(agent); 64 | String coloredMessage = message; 65 | String coloredLog = 66 | '$coloredTimestamp [$coloredLogLevel][$statusCode] |$coloredAgent| $coloredMessage'; 67 | stdout.writeln(coloredLog); 68 | } 69 | 70 | // 获取当前时间戳 71 | static String _getTimestamp() { 72 | DateTime now = DateTime.now(); 73 | return '${now.year.toString().padLeft(4, '0')}-${now.month.toString().padLeft(2, '0')}-${now.day.toString().padLeft(2, '0')} ' 74 | '${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}'; 75 | } 76 | 77 | /// ERROR级别日志 78 | static void error(String message) { 79 | _log("ERROR", message, _redPen); 80 | } 81 | 82 | /// INFO级别日志 83 | static void info(String message) { 84 | _log("INFO", message, _greenPen); 85 | } 86 | 87 | /// WARN级别日志 88 | static void warn(String message) { 89 | _log("WARN", message, _yellowPen); 90 | } 91 | 92 | /// DEBUG级别日志 93 | static void debug(String message) { 94 | _log("DEBUG", message, _bluePen); 95 | } 96 | 97 | /// SUCCESS级别日志 98 | static void success(String message) { 99 | _log("SUCCESS", message, _lightGreenPen); 100 | } 101 | 102 | /// API请求日志 103 | static void api(String method, int statusCode, String message) { 104 | _apiLog(method, statusCode, message, _cyanPen); 105 | } 106 | 107 | /// RAINBOW!!! 108 | static void rainbow(String logLevel, String message) { 109 | String timestamp = _getTimestamp(); 110 | String agent = " NoneBot Agent "; 111 | String coloredTimestamp = _darkGreenPen(timestamp); 112 | String rainbowLogLevel = _applyRainbowEffect(logLevel); 113 | String coloredAgent = _purplePen(agent); 114 | String rainbowMessage = _applyRainbowEffect(message); 115 | 116 | String coloredLog = 117 | '$coloredTimestamp [$rainbowLogLevel] |$coloredAgent| $rainbowMessage'; 118 | stdout.writeln(coloredLog); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /utils/manage.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'core.dart'; 4 | import 'global.dart'; 5 | import 'user_config.dart'; 6 | import 'package:toml/toml.dart'; 7 | 8 | class Bot { 9 | static Map _config(id) { 10 | File file = File('bots/$id.json'); 11 | String content = file.readAsStringSync(); 12 | return jsonDecode(content); 13 | } 14 | 15 | /// 获取Bot名称 16 | static String name(id) { 17 | Map jsonMap = _config(id); 18 | return jsonMap['name'].toString(); 19 | } 20 | 21 | /// 获取Bot的创建时间 22 | static String time(id) { 23 | Map jsonMap = _config(id); 24 | return jsonMap['time'].toString(); 25 | } 26 | 27 | /// 获取Bot的路径 28 | static String path(id) { 29 | Map jsonMap = _config(id); 30 | return jsonMap['path'].toString(); 31 | } 32 | 33 | /// 获取Bot日志 34 | static Future log(id) async { 35 | File file = File('${Bot.path(id)}/nbgui_stdout.log'); 36 | if (file.existsSync()) { 37 | List lines = file.readAsLinesSync(encoding: systemEncoding); 38 | int start = lines.length > UserConfig.logMaxLines() 39 | ? lines.length - UserConfig.logMaxLines() 40 | : 0; 41 | return lines.sublist(start).join('\n'); 42 | } else { 43 | return '[INFO] Welcome to NoneBot WebUI!'; 44 | } 45 | } 46 | 47 | /// 获取Bot运行状态 48 | static bool status(id) { 49 | Map jsonMap = _config(id); 50 | return jsonMap['isRunning']; 51 | } 52 | 53 | /// 获取Bot Pid 54 | static String pid(id) { 55 | Map jsonMap = _config(id); 56 | return jsonMap['pid'].toString(); 57 | } 58 | 59 | /// 直接抓取Bot日志的的Python Pid 60 | static pypid(path) { 61 | File file = File('$path/nbgui_stdout.log'); 62 | RegExp regex = RegExp(r'Started server process \[(\d+)\]'); 63 | Match? match = 64 | regex.firstMatch(file.readAsStringSync(encoding: systemEncoding)); 65 | if (match != null && match.groupCount >= 1) { 66 | String pid = match.group(1)!; 67 | return pid; 68 | } 69 | } 70 | 71 | /// 唤起Bot进程 72 | static Future run(id) async { 73 | File cfgFile = File('bots/$id.json'); 74 | final stdout = File('${Bot.path(id)}/nbgui_stdout.log'); 75 | final stderr = File('${Bot.path(id)}/nbgui_stderr.log'); 76 | Process process = await Process.start('${UserConfig.nbcliPath()}', ['run'], 77 | workingDirectory: Bot.path(id)); 78 | int pid = process.pid; 79 | 80 | /// 重写配置文件来更新状态 81 | Map jsonMap = jsonDecode(cfgFile.readAsStringSync()); 82 | jsonMap['pid'] = pid; 83 | jsonMap['isRunning'] = true; 84 | cfgFile.writeAsStringSync(jsonEncode(jsonMap)); 85 | 86 | final outputSink = stdout.openWrite(); 87 | final errorSink = stderr.openWrite(); 88 | 89 | // 直接监听原始字节输出 90 | process.stdout.listen((data) { 91 | outputSink.add(data); 92 | }); 93 | 94 | process.stderr.listen((data) { 95 | errorSink.add(data); 96 | }); 97 | } 98 | 99 | ///结束bot进程 100 | static stop(id) async { 101 | //读取配置文件 102 | File cfgFile = File('bots/$id.json'); 103 | Map botInfo = json.decode(cfgFile.readAsStringSync()); 104 | String pidString = botInfo['pid'].toString(); 105 | int pid = int.parse(pidString); 106 | Process.killPid(pid); 107 | 108 | ///更新配置文件 109 | botInfo['isRunning'] = false; 110 | botInfo['pid'] = 'Null'; 111 | cfgFile.writeAsStringSync(json.encode(botInfo)); 112 | //如果平台为Windows则释放端口 113 | if (Platform.isWindows) { 114 | await Process.start( 115 | "taskkill.exe", ['/f', '/pid', Bot.pypid(Bot.path(id)).toString()], 116 | runInShell: true); 117 | } 118 | } 119 | 120 | ///重命名Bot 121 | static void rename(name, id) { 122 | // 重写配置文件 123 | File botcfg = File('bots/$id.json'); 124 | Map jsonMap = jsonDecode(botcfg.readAsStringSync()); 125 | jsonMap['name'] = name; 126 | botcfg.writeAsStringSync(jsonEncode(jsonMap)); 127 | } 128 | 129 | ///获取stderr.log 130 | static String stderr(id) { 131 | File file = File('${Bot.path(id)}/nbgui_stderr.log'); 132 | if (file.existsSync()) { 133 | return file.readAsStringSync(); 134 | } else { 135 | return ''; 136 | } 137 | } 138 | 139 | ///清空stderr.log 140 | static void deleteStderr(id) { 141 | File file = File('${Bot.path(id)}/nbgui_stderr.log'); 142 | file.writeAsStringSync(''); 143 | } 144 | 145 | ///删除Bot 146 | static void delete(id) async { 147 | File('bots/$id.json').delete(); 148 | gOnOpen = ''; 149 | } 150 | 151 | ///彻底删除Bot 152 | static void deleteForever(id) async { 153 | String path = Bot.path(id); 154 | Directory(path).delete(recursive: true); 155 | File('bots/$id.json').delete(); 156 | } 157 | 158 | ///导入Bot 159 | static import(String name, String path, bool withProtocol, 160 | String protocolPath, String cmd) { 161 | DateTime now = DateTime.now(); 162 | String id = generateUUID(); 163 | String time = 164 | "${now.year}年${now.month}月${now.day}日${now.hour}时${now.minute}分${now.second}秒"; 165 | File cfgFile = File('bots/$id.json'); 166 | String type = withProtocol ? 'deployed' : 'imported'; 167 | Map botInfo = { 168 | "name": name, 169 | "path": path, 170 | "time": time, 171 | "id": id, 172 | "isRunning": false, 173 | "pid": "Null", 174 | "type": type, 175 | "protocolPath": protocolPath, 176 | "cmd": cmd, 177 | "protocolPid": "Null", 178 | "protocolIsRunning": false 179 | }; 180 | 181 | cfgFile.writeAsStringSync(jsonEncode(botInfo)); 182 | return "echo 写入json"; 183 | } 184 | } 185 | 186 | // 协议端相关操作 187 | class Protocol { 188 | static final File _configFile = File('bots/${gOnOpen}.json'); 189 | static Map _config() { 190 | File file = File('bots/${gOnOpen}.json'); 191 | String content = file.readAsStringSync(); 192 | return jsonDecode(content); 193 | } 194 | 195 | ///协议端路径 196 | static String path() { 197 | Map jsonMap = _config(); 198 | return jsonMap['protocolPath'].toString().replaceAll('\\\\', '\\'); 199 | } 200 | 201 | ///协议端运行状态 202 | static bool status() { 203 | Map jsonMap = _config(); 204 | bool protocolStatus = jsonMap['protocolIsRunning']; 205 | return protocolStatus; 206 | } 207 | 208 | ///协议端pid 209 | static String pid() { 210 | Map jsonMap = _config(); 211 | return jsonMap['protocolPid'].toString(); 212 | } 213 | 214 | ///协议端启动命令 215 | static String cmd() { 216 | Map jsonMap = _config(); 217 | return jsonMap['cmd'].toString(); 218 | } 219 | 220 | ///启动协议端进程 221 | static Future run() async { 222 | Directory.current = Directory(Protocol.path()); 223 | Map jsonMap = _config(); 224 | String ucmd = Protocol.cmd(); 225 | //分解cmd 226 | List cmdList = ucmd.split(' ').toList(); 227 | String pcmd = ''; 228 | List args = []; 229 | if (cmdList.length > 1) { 230 | pcmd = cmdList[0]; 231 | args = cmdList.sublist(1); 232 | } else { 233 | pcmd = cmdList[0]; 234 | args = []; 235 | } 236 | final stdout = File('${Protocol.path()}/nbgui_stdout.log'); 237 | final stderr = File('${Protocol.path()}/nbgui_stderr.log'); 238 | Process process = 239 | await Process.start(pcmd, args, workingDirectory: Protocol.path()); 240 | int pid = process.pid; 241 | 242 | /// 重写配置文件来更新状态 243 | jsonMap['protocolPid'] = pid; 244 | jsonMap['protocolIsRunning'] = true; 245 | _configFile.writeAsStringSync(jsonEncode(jsonMap)); 246 | 247 | final outputSink = stdout.openWrite(); 248 | final errorSink = stderr.openWrite(); 249 | 250 | // 直接监听原始字节输出 251 | process.stdout.listen((data) { 252 | outputSink.add(data); 253 | }); 254 | 255 | process.stderr.listen((data) { 256 | errorSink.add(data); 257 | }); 258 | } 259 | 260 | ///结束协议端进程 261 | static Future stop() async { 262 | Map botInfo = _config(); 263 | String pidString = botInfo['protocolPid'].toString(); 264 | int pid = int.parse(pidString); 265 | Process.killPid(pid, ProcessSignal.sigkill); 266 | //更新配置文件 267 | botInfo['protocolIsRunning'] = false; 268 | botInfo['protocolPid'] = 'Null'; 269 | _configFile.writeAsStringSync(json.encode(botInfo)); 270 | } 271 | 272 | ///更改协议端启动命令 273 | static void changeCmd(String cmd) { 274 | Map jsonMap = _config(); 275 | jsonMap['cmd'] = cmd; 276 | _configFile.writeAsStringSync(jsonEncode(jsonMap)); 277 | } 278 | } 279 | 280 | ///Cli 281 | class Cli { 282 | ///插件管理 283 | static plugin(mode, name) { 284 | if (mode == 'install') { 285 | String cmd = '${UserConfig.nbcliPath()} plugin install $name'; 286 | return cmd; 287 | } 288 | if (mode == 'uninstall') { 289 | String cmd = '${UserConfig.nbcliPath()} plugin uninstall $name -y'; 290 | return cmd; 291 | } 292 | return null; 293 | } 294 | 295 | ///适配器管理 296 | static adapter(mode, name) { 297 | if (mode == 'install') { 298 | String cmd = '${UserConfig.nbcliPath()} adapter install $name'; 299 | return cmd; 300 | } 301 | if (mode == 'uninstall') { 302 | String cmd = '${UserConfig.nbcliPath()} adapter uninstall $name -y'; 303 | return cmd; 304 | } 305 | return null; 306 | } 307 | 308 | ///驱动器管理 309 | static driver(mode, name) { 310 | if (mode == 'install') { 311 | String cmd = '${UserConfig.nbcliPath()} driver install $name'; 312 | return cmd; 313 | } 314 | if (mode == 'uninstall') { 315 | String cmd = '${UserConfig.nbcliPath()} driver uninstall $name -y'; 316 | return cmd; 317 | } 318 | return null; 319 | } 320 | 321 | ///CLI本体管理 322 | static self(mode, name) { 323 | if (mode == 'install') { 324 | String cmd = '${UserConfig.nbcliPath()} self install $name'; 325 | return cmd; 326 | } 327 | if (mode == 'uninstall') { 328 | String cmd = '${UserConfig.nbcliPath()} self uninstall $name -y'; 329 | return cmd; 330 | } 331 | if (mode == 'update') { 332 | String cmd = '${UserConfig.nbcliPath()} self update'; 333 | return cmd; 334 | } 335 | } 336 | } 337 | 338 | ///插件 339 | class Plugin { 340 | ///禁用插件 341 | static disable(name, id) { 342 | File disable = File('${Bot.path(id)}/.disabled_plugins'); 343 | if (!disable.existsSync()) { 344 | disable.createSync(); 345 | } 346 | File pyprojectFile = File('${Bot.path(id)}/pyproject.toml'); 347 | String pyprojectContent = pyprojectFile.readAsStringSync(); 348 | List linesWithoutComments = pyprojectContent 349 | .split('\n') 350 | .map((line) { 351 | int commentIndex = line.indexOf('#'); 352 | if (commentIndex != -1) { 353 | return line.substring(0, commentIndex).trim(); 354 | } 355 | return line; 356 | }) 357 | .where((line) => line.isNotEmpty) 358 | .toList(); 359 | String pyprojectWithoutComments = linesWithoutComments.join('\n'); 360 | var toml = TomlDocument.parse(pyprojectWithoutComments).toMap(); 361 | var nonebot = toml['tool']['nonebot']; 362 | List pluginsList = nonebot['plugins']; 363 | 364 | // 移除指定的插件 365 | pluginsList.remove(name); 366 | nonebot['plugins'] = pluginsList; 367 | 368 | // 手动更新 plugins 列表 369 | String updatedTomlContent = pyprojectContent.replaceFirstMapped( 370 | RegExp(r'plugins = \[([^\]]*)\]', dotAll: true), 371 | (match) => 372 | 'plugins = [${pluginsList.map((plugin) => '"$plugin"').join(', ')}]'); 373 | 374 | pyprojectFile.writeAsStringSync(updatedTomlContent); 375 | if (disable.readAsStringSync().isEmpty) { 376 | disable.writeAsStringSync(name); 377 | } else { 378 | disable.writeAsStringSync('${disable.readAsStringSync()}\n$name'); 379 | } 380 | } 381 | 382 | ///启用插件 383 | static enable(name, id) { 384 | File disable = File('${Bot.path(id)}/.disabled_plugins'); 385 | File pyprojectFile = File('${Bot.path(id)}/pyproject.toml'); 386 | String pyprojectContent = pyprojectFile.readAsStringSync(); 387 | var toml = TomlDocument.parse(pyprojectContent).toMap(); 388 | var nonebot = toml['tool']['nonebot']; 389 | List pluginsList = List.from(nonebot['plugins']); 390 | 391 | if (!pluginsList.contains(name)) { 392 | pluginsList.add(name); 393 | } 394 | 395 | nonebot['plugins'] = pluginsList; 396 | 397 | // 手动更新 plugins 列表 398 | String updatedTomlContent = pyprojectContent.replaceFirstMapped( 399 | RegExp(r'plugins = \[([^\]]*)\]', dotAll: true), 400 | (match) => 401 | 'plugins = [${pluginsList.map((plugin) => '"$plugin"').join(', ')}]'); 402 | 403 | pyprojectFile.writeAsStringSync(updatedTomlContent); 404 | String disabled = disable.readAsStringSync(); 405 | List disabledList = disabled.split('\n'); 406 | disabledList.remove(name); 407 | disable.writeAsStringSync(disabledList.join('\n')); 408 | } 409 | 410 | ///获取插件列表 411 | static List list(id) { 412 | File pyprojectFile = File('${Bot.path(id)}/pyproject.toml'); 413 | pyprojectFile.writeAsStringSync(pyprojectFile 414 | .readAsStringSync() 415 | .replaceAll('\r\n', '\n') 416 | .replaceAll('\r', '\n')); 417 | String pyprojectContent = 418 | pyprojectFile.readAsStringSync(encoding: systemEncoding); 419 | List linesWithoutComments = pyprojectContent 420 | .split('\n') 421 | .map((line) { 422 | int commentIndex = line.indexOf('#'); 423 | if (commentIndex != -1) { 424 | return line.substring(0, commentIndex).trim(); 425 | } 426 | return line; 427 | }) 428 | .where((line) => line.isNotEmpty) 429 | .toList(); 430 | String pyprojectWithoutComments = linesWithoutComments.join('\n'); 431 | // 解析 TOML 文件 432 | var toml = TomlDocument.parse(pyprojectWithoutComments).toMap(); 433 | var nonebot = toml['tool']['nonebot']; 434 | List pluginsList = nonebot['plugins']; 435 | 436 | return pluginsList; 437 | } 438 | 439 | /// 获取禁用插件列表 440 | static List disabledList(id) { 441 | File disable = File('${Bot.path(id)}/.disabled_plugins'); 442 | if (disable.existsSync()) { 443 | return disable.readAsStringSync().split('\n'); 444 | } else { 445 | disable.createSync(); 446 | return []; 447 | } 448 | } 449 | 450 | /// 安装 451 | static install(name, id) async { 452 | List commands = [Cli.plugin('install', name)]; 453 | for (String command in commands) { 454 | List args = command.split(' '); 455 | String executable = args.removeAt(0); 456 | Process process = await Process.start( 457 | executable, 458 | args, 459 | runInShell: true, 460 | workingDirectory: Bot.path(id), 461 | ); 462 | process.stdout.transform(utf8.decoder).listen((data) { 463 | Map res = { 464 | 'type': 'pluginInstallLog', 465 | 'data': data, 466 | }; 467 | sendMessageToClients(jsonEncode(res)); 468 | }); 469 | 470 | process.stderr.transform(utf8.decoder).listen((data) { 471 | Map res = { 472 | 'type': 'pluginInstallLog', 473 | 'data': data, 474 | }; 475 | sendMessageToClients(jsonEncode(res)); 476 | }); 477 | 478 | await process.exitCode; 479 | Map msg = { 480 | 'type': 'installPluginStatus', 481 | 'data': 'done', 482 | }; 483 | sendMessageToClients(jsonEncode(msg)); 484 | } 485 | } 486 | 487 | /// 卸载 488 | static uninstall(name, id) async { 489 | List commands = [Cli.plugin('uninstall', name)]; 490 | for (String command in commands) { 491 | List args = command.split(' '); 492 | String executable = args.removeAt(0); 493 | Process process = await Process.start( 494 | executable, 495 | args, 496 | runInShell: true, 497 | workingDirectory: Bot.path(id), 498 | ); 499 | process.stdout.transform(utf8.decoder).listen((data) { 500 | Map res = { 501 | 'type': 'pluginUninstallLog', 502 | 'data': data, 503 | }; 504 | sendMessageToClients(jsonEncode(res)); 505 | }); 506 | 507 | process.stderr.transform(utf8.decoder).listen((data) { 508 | Map res = { 509 | 'type': 'pluginUninstallLog', 510 | 'data': data, 511 | }; 512 | sendMessageToClients(jsonEncode(res)); 513 | }); 514 | 515 | await process.exitCode; 516 | } 517 | } 518 | } 519 | 520 | // 适配器 521 | class Adapter { 522 | /// 安装 523 | static install(name, id) async { 524 | List commands = [Cli.adapter('install', name)]; 525 | for (String command in commands) { 526 | List args = command.split(' '); 527 | String executable = args.removeAt(0); 528 | Process process = await Process.start( 529 | executable, 530 | args, 531 | runInShell: true, 532 | workingDirectory: Bot.path(id), 533 | ); 534 | process.stdout.transform(utf8.decoder).listen((data) { 535 | Map res = { 536 | 'type': 'adapterInstallLog', 537 | 'data': data, 538 | }; 539 | sendMessageToClients(jsonEncode(res)); 540 | }); 541 | 542 | process.stderr.transform(utf8.decoder).listen((data) { 543 | Map res = { 544 | 'type': 'adapterInstallLog', 545 | 'data': data, 546 | }; 547 | sendMessageToClients(jsonEncode(res)); 548 | }); 549 | 550 | await process.exitCode; 551 | Map msg = { 552 | 'type': 'installAdapterStatus', 553 | 'data': 'done', 554 | }; 555 | sendMessageToClients(jsonEncode(msg)); 556 | } 557 | } 558 | } 559 | 560 | /// 驱动器 561 | class Driver { 562 | /// 安装 563 | static install(name, id) async { 564 | List commands = [Cli.driver('install', name)]; 565 | for (String command in commands) { 566 | List args = command.split(' '); 567 | String executable = args.removeAt(0); 568 | Process process = await Process.start( 569 | executable, 570 | args, 571 | runInShell: true, 572 | workingDirectory: Bot.path(id), 573 | ); 574 | process.stdout.transform(utf8.decoder).listen((data) { 575 | Map res = { 576 | 'type': 'driverInstallLog', 577 | 'data': data, 578 | }; 579 | sendMessageToClients(jsonEncode(res)); 580 | }); 581 | 582 | process.stderr.transform(utf8.decoder).listen((data) { 583 | Map res = { 584 | 'type': 'driverInstallLog', 585 | 'data': data, 586 | }; 587 | sendMessageToClients(jsonEncode(res)); 588 | }); 589 | 590 | await process.exitCode; 591 | Map msg = { 592 | 'type': 'installDriverStatus', 593 | 'data': 'done', 594 | }; 595 | sendMessageToClients(jsonEncode(msg)); 596 | } 597 | } 598 | } 599 | 600 | // .env.* 601 | class Env { 602 | /// 读取.env文件 603 | static load(id, filename) { 604 | if (!File('${Bot.path(id)}/$filename').existsSync()) { 605 | File('${Bot.path(id)}/$filename').createSync(); 606 | } 607 | File file = File('${Bot.path(id)}/$filename'); 608 | return file.readAsStringSync(); 609 | } 610 | 611 | /// 新增配置项 612 | static add(id, filename, varName, varValue) { 613 | File file = File('${Bot.path(id)}/$filename'); 614 | file.writeAsStringSync('\n$varName=$varValue', mode: FileMode.append); 615 | } 616 | 617 | /// 删除配置项 618 | static delete(id, filename, varName) { 619 | File file = File('${Bot.path(id)}/$filename'); 620 | String content = file.readAsStringSync(); 621 | List lines = content.split('\n'); 622 | lines.removeWhere((line) => line.split('=').first.trim() == varName); 623 | file.writeAsStringSync(lines.join('\n')); 624 | } 625 | 626 | /// 修改文件 627 | static modify(id, filename, content) { 628 | File file = File('${Bot.path(id)}/$filename'); 629 | if (file.existsSync()) { 630 | file.writeAsStringSync(content); 631 | } else { 632 | file.createSync(); 633 | file.writeAsStringSync(content); 634 | } 635 | } 636 | } 637 | -------------------------------------------------------------------------------- /utils/run_cmd.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | import 'core.dart'; 4 | import 'deploy_bot.dart'; 5 | 6 | // 执行创建Bot命令 7 | void runInstall(String path, String name, drivers, adapters, String template, 8 | String pluginDir, bool venv, bool installDep) async { 9 | List commands = [ 10 | 'echo 开始创建Bot:$name', 11 | 'echo 读取配置...', 12 | DeployBot.createFolder(path, name, template, pluginDir), 13 | DeployBot.createVENVEcho(path, name), 14 | DeployBot.createVENV(path, name, venv), 15 | DeployBot.writeReq(path, name, drivers, adapters), 16 | 'echo 开始安装依赖...', 17 | DeployBot.install(path, name, venv, installDep), 18 | DeployBot.writePyProject(path, name, adapters, template, pluginDir), 19 | DeployBot.writeENV(path, name, "8080", template, drivers), 20 | DeployBot.writeBot(name, path, "default", "none", "none"), 21 | 'echo 安装完成,可退出' 22 | ]; 23 | 24 | for (String command in commands) { 25 | List args = command.split(' '); 26 | String executable = args.removeAt(0); 27 | Process process = await Process.start(executable, args, runInShell: true); 28 | process.stdout.transform(systemEncoding.decoder).listen((data) { 29 | Map msg = { 30 | "type": "installBotLog", 31 | "data": data, 32 | }; 33 | String res = jsonEncode(msg); 34 | sendMessageToClients(res); 35 | }); 36 | process.stderr.transform(systemEncoding.decoder).listen((data) { 37 | Map msg = { 38 | "type": "installBotLog", 39 | "data": data, 40 | }; 41 | String res = jsonEncode(msg); 42 | sendMessageToClients(res); 43 | }); 44 | await process.exitCode; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /utils/user_config.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:io'; 3 | 4 | /// 用户配置文件 5 | class UserConfig { 6 | static final File _configFile = File('config.json'); 7 | 8 | /// 初始化用户配置文件 9 | static Map _config() { 10 | File file = File('config.json'); 11 | String content = file.readAsStringSync(); 12 | return jsonDecode(content); 13 | } 14 | 15 | /// 是否自动检查更新 16 | static bool checkUpdate() { 17 | Map jsonMap = _config(); 18 | if (jsonMap.containsKey("checkUpdate")) { 19 | bool checkUpdate = jsonMap['checkUpdate']; 20 | return checkUpdate; 21 | } else { 22 | setCheckUpdate(true); 23 | return true; 24 | } 25 | } 26 | 27 | /// 设置是否自动检查更新 28 | static void setCheckUpdate(checkUpdate) { 29 | Map jsonMap = _config(); 30 | jsonMap['checkUpdate'] = checkUpdate; 31 | _configFile.writeAsStringSync(jsonEncode(jsonMap)); 32 | } 33 | 34 | /// Python路径 35 | static dynamic pythonPath() { 36 | Map jsonMap = _config(); 37 | String pyPath = jsonMap['python'].toString(); 38 | if (pyPath == 'default') { 39 | if (Platform.isLinux || Platform.isMacOS) { 40 | return 'python3'; 41 | } else if (Platform.isWindows) { 42 | return 'python.exe'; 43 | } 44 | } else { 45 | return pyPath.replaceAll('\\', '\\\\'); 46 | } 47 | } 48 | 49 | /// 设置Python路径 50 | static void setPythonPath(pythonPath) { 51 | Map jsonMap = _config(); 52 | jsonMap['python'] = pythonPath; 53 | _configFile.writeAsStringSync(jsonEncode(jsonMap)); 54 | } 55 | 56 | /// NoneBot-CLI路径 57 | static dynamic nbcliPath() { 58 | Map jsonMap = _config(); 59 | String nbcliPath = jsonMap['nbcli'].toString(); 60 | if (nbcliPath == 'default') { 61 | return 'nb'; 62 | } else { 63 | return nbcliPath.replaceAll('\\', '\\\\'); 64 | } 65 | } 66 | 67 | /// 设置NoneBot-CLI路径 68 | static void setNbcliPath(nbcliPath) { 69 | Map jsonMap = _config(); 70 | jsonMap['nbcli'] = nbcliPath; 71 | _configFile.writeAsStringSync(jsonEncode(jsonMap)); 72 | } 73 | 74 | /// Bot日志最大行数 75 | static int logMaxLines() { 76 | Map jsonMap = _config(); 77 | if (jsonMap.containsKey("logMaxLines")) { 78 | int logMaxLines = jsonMap['logMaxLines']; 79 | return logMaxLines; 80 | } else { 81 | setLogMaxLines(75); 82 | return 75; 83 | } 84 | } 85 | 86 | /// 设置Bot日志最大行数 87 | static void setLogMaxLines(logMaxLines) { 88 | Map jsonMap = _config(); 89 | jsonMap['logMaxLines'] = logMaxLines; 90 | _configFile.writeAsStringSync(jsonEncode(jsonMap)); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /utils/ws_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'package:shelf_web_socket/shelf_web_socket.dart'; 3 | import 'core.dart'; 4 | import 'global.dart'; 5 | import 'logger.dart'; 6 | import 'manage.dart'; 7 | import 'run_cmd.dart'; 8 | 9 | // WebSocket 服务 10 | var wsHandler = webSocketHandler((webSocket) async { 11 | // 监听客户端消息,并处理错误 12 | wsChannels.add(webSocket); 13 | Logger.success( 14 | 'Websocket connection established. ${wsChannels.length} connections now.'); 15 | webSocket.stream.listen( 16 | (message) async { 17 | message = message.toString().trim(); 18 | try { 19 | var body = message.split('?token='); 20 | String msg = body[0]; 21 | String token = body[1]; 22 | try { 23 | if (body.length != 2) { 24 | webSocket.sink.add('{ "error": "400 Bad Request!" }'); 25 | return; 26 | } 27 | if (token != AgentMain.token().toString()) { 28 | webSocket.sink.add('{ "error": "401 Unauthorized!" }'); 29 | return; 30 | } 31 | if (body.length == 2 && token == AgentMain.token().toString()) { 32 | switch (msg) { 33 | case 'ping': 34 | String res = '{"type": "pong", "data": "pong!"}'; 35 | webSocket.sink.add(res); 36 | break; 37 | 38 | // 版本信息 39 | case 'version': 40 | Map version = { 41 | 'version': AgentMain.version(), 42 | 'nbcli': 43 | MainApp.nbcli.replaceAll('nb: ', '').replaceAll('\n', ''), 44 | 'python': MainApp.python, 45 | }; 46 | Map response = { 47 | "type": "version", 48 | "data": version, 49 | }; 50 | String res = jsonEncode(response); 51 | webSocket.sink.add(res); 52 | 53 | // Bot列表 54 | case 'botList': 55 | Map response = { 56 | "type": "botList", 57 | "data": MainApp.botList 58 | }; 59 | String prettyJson = jsonEncode(response); 60 | webSocket.sink.add(prettyJson); 61 | 62 | break; 63 | 64 | // 系统占用 65 | case 'system': 66 | String res = 67 | '{"type": "systemStatus", "data": ${await System.status()}}'; 68 | webSocket.sink.add(res); 69 | break; 70 | // 平台信息 71 | case 'platform': 72 | String res = 73 | '{"type": "platformInfo", "data": ${await System.platform()}}'; 74 | webSocket.sink.add(res); 75 | break; 76 | 77 | // Bot信息 78 | case var botInfo when botInfo.startsWith('botInfo/'): 79 | var id = botInfo.split('/')[1]; 80 | var bot = MainApp.botList.firstWhere( 81 | (bot) => bot['id'] == id, 82 | orElse: () => {"error": "Bot Not Found!"}, 83 | ); 84 | JsonEncoder encoder = JsonEncoder.withIndent(' '); 85 | String prettyJson = encoder.convert(bot); 86 | String res = '{"type": "botInfo", "data": $prettyJson}'; 87 | webSocket.sink.add(res); 88 | //Logger.debug(res); 89 | break; 90 | 91 | // Bot日志 92 | case var botLog when botLog.startsWith('bot/log/'): 93 | var id = botLog.split('/')[2]; 94 | var log = await Bot.log(id); 95 | // 96 | Map response = {"type": "botLog", "data": log}; 97 | String res = jsonEncode(response); 98 | webSocket.sink.add(res); 99 | break; 100 | 101 | // 启动Bot 102 | case var botStart when botStart.startsWith('bot/run/'): 103 | var id = botStart.split('/')[2]; 104 | if (!Bot.status(id)) { 105 | Bot.run(id); 106 | Logger.success('Bot $id started!'); 107 | String res = 108 | '{"type": "startBot", "data": "{\\"status\\": \\"Bot $id started!\\"}"}'; 109 | webSocket.sink.add(res); 110 | } else { 111 | Logger.error('Bot $id is already running!'); 112 | String res = 113 | '{"type": "startBot", "data": "{\\"code\\": 1002, \\"error\\": \\"Bot $id is already running!\\"}"}'; 114 | webSocket.sink.add(res); 115 | } 116 | break; 117 | 118 | // 停止Bot 119 | case var botStop when botStop.startsWith('bot/stop/'): 120 | var id = botStop.split('/')[2]; 121 | if (Bot.status(id)) { 122 | Bot.stop(id); 123 | Logger.success('Bot $id stopped!'); 124 | String res = 125 | '{"type": "stopBot", "data": "{\\"status\\": \\"Bot $id stopped!\\"}"}'; 126 | webSocket.sink.add(res); 127 | } else { 128 | Logger.error('Bot $id is not running!'); 129 | String res = 130 | '{"type": "stopBot", "data": "{\\"code\\": 1001, \\"error\\": \\"Bot $id is not running!\\"}"}'; 131 | webSocket.sink.add(res); 132 | } 133 | break; 134 | 135 | // 重启Bot 136 | case var botRestart when botRestart.startsWith('bot/restart/'): 137 | var id = botRestart.split('/')[2]; 138 | gOnOpen = id; 139 | if (Bot.status(id)) { 140 | Bot.stop(id); 141 | await Future.delayed(const Duration(seconds: 1), () { 142 | Bot.run(id); 143 | }); 144 | Logger.success('Bot $id restarted!'); 145 | String res = 146 | '{"type": "restartBot", "data": "{\\"status\\": \\"Bot $id restarted!\\"}"}'; 147 | webSocket.sink.add(res); 148 | } else { 149 | Logger.error('Bot $id is not running!'); 150 | String res = 151 | '{"type": "restartBot", "data": "{\\"code\\": 1001, \\"error\\": \\"Bot $id is not running!\\"}"}'; 152 | webSocket.sink.add(res); 153 | } 154 | break; 155 | 156 | // 导入Bot 157 | case var importBot when importBot.startsWith('bot/import'): 158 | var bot = importBot.split('?data=')[1]; 159 | var botJson = jsonDecode(bot); 160 | String name = botJson['name']; 161 | String path = botJson['path']; 162 | String protocolPath = botJson['protocolPath']; 163 | bool withProtocol = botJson['withProtocol']; 164 | String cmd = botJson['cmd']; 165 | Bot.import(name, path, withProtocol, protocolPath, cmd); 166 | Logger.success('Bot $name imported!'); 167 | Map response = { 168 | "type": "importBot", 169 | "data": {"status": "Bot $name imported!"} 170 | }; 171 | String res = jsonEncode(response); 172 | webSocket.sink.add(res); 173 | 174 | // 创建Bot 175 | case var createBot when createBot.startsWith('bot/create'): 176 | var bot = createBot.split('?data=')[1]; 177 | var botJson = jsonDecode(bot); 178 | String name = botJson['name']; 179 | String path = botJson['path']; 180 | List drivers = botJson['drivers']; 181 | List adapters = botJson['adapters']; 182 | String template = botJson['template']; 183 | String pluginDir = botJson['pluginDir']; 184 | bool venv = botJson['venv']; 185 | bool installDep = botJson['installDep']; 186 | runInstall(path, name, drivers, adapters, template, pluginDir, 187 | venv, installDep); 188 | Logger.info('Bot $name start creating...'); 189 | Map response = { 190 | "type": "createBot", 191 | "data": {"status": "Bot $name start creating..."} 192 | }; 193 | String res = jsonEncode(response); 194 | webSocket.sink.add(res); 195 | 196 | // 删除Bot 197 | case var deleteBot when deleteBot.startsWith('bot/remove/'): 198 | var id = deleteBot.split('/')[2]; 199 | gOnOpen = id; 200 | Bot.delete(id); 201 | Logger.success('Bot $id removed!'); 202 | Map response = { 203 | "type": "deleteBot", 204 | "data": {"status": "Bot $id removed!"} 205 | }; 206 | String res = jsonEncode(response); 207 | webSocket.sink.add(res); 208 | break; 209 | 210 | // 彻底删除Bot 211 | case var deleteBot when deleteBot.startsWith('bot/delete/'): 212 | var id = deleteBot.split('/')[2]; 213 | gOnOpen = id; 214 | Bot.deleteForever(id); 215 | Logger.success('Bot $id deleted!'); 216 | Map response = { 217 | "type": "deleteBot", 218 | "data": {"status": "Bot $id deleted!"} 219 | }; 220 | String res = jsonEncode(response); 221 | webSocket.sink.add(res); 222 | break; 223 | 224 | // 重命名Bot 225 | case var renameBot when renameBot.startsWith('bot/rename'): 226 | var data = renameBot.split('?data=')[1]; 227 | var botJson = jsonDecode(data); 228 | String id = botJson['id']; 229 | String name = botJson['name']; 230 | gOnOpen = id; 231 | Bot.rename(name, id); 232 | Logger.success('Bot $id renamed to $name!'); 233 | Map response = { 234 | "type": "renameBot", 235 | "data": {"status": "Bot $id renamed to $name!"} 236 | }; 237 | String res = jsonEncode(response); 238 | webSocket.sink.add(res); 239 | break; 240 | 241 | // 安装插件 242 | case var installPlugin 243 | when installPlugin.startsWith('plugin/install'): 244 | var plugin = installPlugin.split('?data=')[1]; 245 | var pluginJson = jsonDecode(plugin); 246 | String name = pluginJson['name']; 247 | String id = pluginJson['id']; 248 | Plugin.install(name, id); 249 | Logger.success('Plugin $name installed!'); 250 | Map response = { 251 | "type": "installPlugin", 252 | "data": {"status": "Plugin $name start installing..."} 253 | }; 254 | String res = jsonEncode(response); 255 | webSocket.sink.add(res); 256 | break; 257 | 258 | // 卸载插件 259 | case var uninstallPlugin 260 | when uninstallPlugin.startsWith('plugin/uninstall'): 261 | var plugin = uninstallPlugin.split('?data=')[1]; 262 | var pluginJson = jsonDecode(plugin); 263 | String id = pluginJson['id']; 264 | String name = pluginJson['name']; 265 | Plugin.uninstall(name, id); 266 | Logger.success('Plugin $name uninstalled!'); 267 | Map response = { 268 | "type": "uninstallPlugin", 269 | "data": {"status": "Plugin $name start uninstalling..."} 270 | }; 271 | String res = jsonEncode(response); 272 | webSocket.sink.add(res); 273 | break; 274 | 275 | // 禁用插件 276 | case var disablePlugin 277 | when disablePlugin.startsWith('plugin/disable?data='): 278 | var plugin = disablePlugin.split('?data=')[1]; 279 | var pluginJson = jsonDecode(plugin); 280 | String id = pluginJson['id']; 281 | String name = pluginJson['name']; 282 | Plugin.disable(name, id); 283 | Logger.success('Plugin $name disabled!'); 284 | Map response = { 285 | "type": "disablePlugin", 286 | "data": {"status": "Plugin $name disabled!"} 287 | }; 288 | String res = jsonEncode(response); 289 | webSocket.sink.add(res); 290 | break; 291 | 292 | // 启用插件 293 | case var enablePlugin 294 | when enablePlugin.startsWith('plugin/enable?data='): 295 | var plugin = enablePlugin.split('?data=')[1]; 296 | var pluginJson = jsonDecode(plugin); 297 | String id = pluginJson['id']; 298 | String name = pluginJson['name']; 299 | Plugin.enable(name, id); 300 | Logger.success('Plugin $name enabled!'); 301 | Map response = { 302 | "type": "enablePlugin", 303 | "data": {"status": "Plugin $name enabled!"} 304 | }; 305 | String res = jsonEncode(response); 306 | webSocket.sink.add(res); 307 | break; 308 | 309 | // 获取插件列表 310 | case var pluginList when pluginList.startsWith('plugin/list/'): 311 | var id = pluginList.split('/')[2]; 312 | var plugins = Plugin.list(id); 313 | Map response = {"type": "pluginList", "data": plugins}; 314 | String res = jsonEncode(response); 315 | webSocket.sink.add(res); 316 | break; 317 | 318 | // 获取被禁用的插件列表 319 | case var pluginList 320 | when pluginList.startsWith('plugin/disabledList/'): 321 | var id = pluginList.split('/')[2]; 322 | var plugins = Plugin.disabledList(id); 323 | Map response = {"type": "disabledPluginList", "data": plugins}; 324 | String res = jsonEncode(response); 325 | webSocket.sink.add(res); 326 | break; 327 | 328 | // 安装适配器 329 | case var installAdapter 330 | when installAdapter.startsWith('adapter/install'): 331 | var adapter = installAdapter.split('?data=')[1]; 332 | var adapterJson = jsonDecode(adapter); 333 | String name = adapterJson['name']; 334 | String id = adapterJson['id']; 335 | Adapter.install(name, id); 336 | Logger.success('Adapter $name installed!'); 337 | Map response = { 338 | "type": "installAdapter", 339 | "data": {"status": "Adapter $name start installing..."} 340 | }; 341 | String res = jsonEncode(response); 342 | webSocket.sink.add(res); 343 | break; 344 | 345 | // 获取env文件内容 346 | case var getEnv when getEnv.startsWith('env/load/'): 347 | var id = getEnv.split('/')[2]; 348 | var filename = getEnv.split('/')[3]; 349 | if (['.env', '.env.prod', '.env.dev'].contains(filename)) { 350 | var env = Env.load(id, filename); 351 | Map response = {"type": "envContent", "data": env}; 352 | String res = jsonEncode(response); 353 | webSocket.sink.add(res); 354 | } else { 355 | Map response = { 356 | "type": "envContent", 357 | "data": {"error": "Not allowed operation!"} 358 | }; 359 | String res = jsonEncode(response); 360 | webSocket.sink.add(res); 361 | } 362 | break; 363 | 364 | // 编辑env文件内容 365 | case var editEnv when editEnv.startsWith('env/edit'): 366 | var data = editEnv.split('?data=')[1]; 367 | var id = jsonDecode(data)['id']; 368 | var filename = jsonDecode(data)['filename']; 369 | var content = jsonDecode(data)['content']; 370 | if (['.env', '.env.prod', '.env.dev'].contains(filename)) { 371 | Env.modify(id, filename, content); 372 | Map response = { 373 | "type": "editEnv", 374 | "data": {"status": "Env file $filename edited!"} 375 | }; 376 | String res = jsonEncode(response); 377 | webSocket.sink.add(res); 378 | } else { 379 | Map response = { 380 | "type": "editEnv", 381 | "data": {"error": "Not allowed operation!"} 382 | }; 383 | String res = jsonEncode(response); 384 | webSocket.sink.add(res); 385 | } 386 | break; 387 | 388 | // 新增env配置项 389 | case var addEnv when addEnv.startsWith('env/add'): 390 | var data = addEnv.split('?data=')[1]; 391 | var id = jsonDecode(data)['id']; 392 | var filename = jsonDecode(data)['filename']; 393 | var key = jsonDecode(data)['varName']; 394 | var value = jsonDecode(data)['varValue']; 395 | if (['.env', '.env.prod', '.env.dev'].contains(filename)) { 396 | Env.add(id, filename, key, value); 397 | Map response = { 398 | "type": "addEnv", 399 | "data": {"status": "Env file $filename added!"} 400 | }; 401 | String res = jsonEncode(response); 402 | webSocket.sink.add(res); 403 | } else { 404 | Map response = { 405 | "type": "addEnv", 406 | "data": {"error": "Not allowed operation!"} 407 | }; 408 | String res = jsonEncode(response); 409 | webSocket.sink.add(res); 410 | } 411 | break; 412 | 413 | // 删除env配置项 414 | case var deleteEnv when deleteEnv.startsWith('env/delete'): 415 | var data = deleteEnv.split('?data=')[1]; 416 | var id = jsonDecode(data)['id']; 417 | var filename = jsonDecode(data)['filename']; 418 | var key = jsonDecode(data)['varName']; 419 | if (['.env', '.env.prod', '.env.dev'].contains(filename)) { 420 | Env.delete(id, filename, key); 421 | Map response = { 422 | "type": "deleteEnv", 423 | "data": {"status": "Env file $filename deleted!"} 424 | }; 425 | String res = jsonEncode(response); 426 | webSocket.sink.add(res); 427 | } else { 428 | Map response = { 429 | "type": "deleteEnv", 430 | "data": {"error": "Not allowed operation!"} 431 | }; 432 | String res = jsonEncode(response); 433 | webSocket.sink.add(res); 434 | } 435 | break; 436 | 437 | // 未知命令 438 | default: 439 | Map response = { 440 | "type": "unknownCommand", 441 | "data": "Unknown Command!" 442 | }; 443 | String res = jsonEncode(response); 444 | webSocket.sink.add(res); 445 | break; 446 | } 447 | } 448 | } catch (e, stackTrace) { 449 | Logger.error('Error handling message: $e\nStack Trace:\n$stackTrace'); 450 | webSocket.sink.add('Error processing your request.$e'); 451 | } 452 | } catch (e) { 453 | String res = '{"type": "Unauthorized", "data": "401 Unauthorized!"}'; 454 | webSocket.sink.add(res); 455 | } 456 | }, 457 | onError: (error, stackTrace) { 458 | Logger.error('$error\nStack Trace:\n$stackTrace'); 459 | webSocket.sink.add('Error processing your request.$error'); 460 | }, 461 | cancelOnError: true, 462 | onDone: () { 463 | wsChannels.remove(webSocket); 464 | Logger.info( 465 | 'Websocket connection closed. ${wsChannels.length} connections now.'); 466 | }, 467 | ); 468 | }); 469 | --------------------------------------------------------------------------------