├── .gitignore ├── LICENSE ├── README.md ├── apps ├── Actionbar.js ├── Help.js ├── Main.js ├── Private.js ├── Setting.js ├── Status.js ├── Subtitle.js ├── Title.js └── Update.js ├── components ├── Config.js ├── Rcon.js ├── Render.js ├── SendMsg.js ├── Version.js └── WebSocket.js ├── config └── config_default.yaml ├── guoba.support.js ├── index.js ├── model ├── init.js └── path.js ├── package.json └── resources ├── common ├── base.css ├── base.less ├── common.css ├── common.less ├── font │ ├── HYWH-65W.ttf │ ├── HYWH-65W.woff │ ├── NZBZ.ttf │ ├── NZBZ.woff │ ├── tttgbnumber.ttf │ ├── tttgbnumber.woff │ └── 华文中宋.TTF └── layout │ ├── default.html │ └── elem.html ├── fonts └── Minecraft.ttf ├── help ├── icon.png ├── imgs │ ├── bg.jpg │ ├── config.js │ └── main.png ├── index.css ├── index.html ├── index.less ├── version-info.css ├── version-info.html └── version-info.less └── readme └── girl.png /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | config/config/*.yaml 3 | apps/Test.js -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU AFFERO GENERAL PUBLIC LICENSE 2 | Version 3, 19 November 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 Affero General Public License is a free, copyleft license for 11 | software and other kinds of works, specifically designed to ensure 12 | cooperation with the community in the case of network server software. 13 | 14 | The licenses for most software and other practical works are designed 15 | to take away your freedom to share and change the works. By contrast, 16 | our General Public Licenses are intended to guarantee your freedom to 17 | share and change all versions of a program--to make sure it remains free 18 | software for all its users. 19 | 20 | When we speak of free software, we are referring to freedom, not 21 | price. Our General Public Licenses are designed to make sure that you 22 | have the freedom to distribute copies of free software (and charge for 23 | them if you wish), that you receive source code or can get it if you 24 | want it, that you can change the software or use pieces of it in new 25 | free programs, and that you know you can do these things. 26 | 27 | Developers that use our General Public Licenses protect your rights 28 | with two steps: (1) assert copyright on the software, and (2) offer 29 | you this License which gives you legal permission to copy, distribute 30 | and/or modify the software. 31 | 32 | A secondary benefit of defending all users' freedom is that 33 | improvements made in alternate versions of the program, if they 34 | receive widespread use, become available for other developers to 35 | incorporate. Many developers of free software are heartened and 36 | encouraged by the resulting cooperation. However, in the case of 37 | software used on network servers, this result may fail to come about. 38 | The GNU General Public License permits making a modified version and 39 | letting the public access it on a server without ever releasing its 40 | source code to the public. 41 | 42 | The GNU Affero General Public License is designed specifically to 43 | ensure that, in such cases, the modified source code becomes available 44 | to the community. It requires the operator of a network server to 45 | provide the source code of the modified version running there to the 46 | users of that server. Therefore, public use of a modified version, on 47 | a publicly accessible server, gives the public access to the source 48 | code of the modified version. 49 | 50 | An older license, called the Affero General Public License and 51 | published by Affero, was designed to accomplish similar goals. This is 52 | a different license, not a version of the Affero GPL, but Affero has 53 | released a new version of the Affero GPL which permits relicensing under 54 | this license. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | TERMS AND CONDITIONS 60 | 61 | 0. Definitions. 62 | 63 | "This License" refers to version 3 of the GNU Affero General Public License. 64 | 65 | "Copyright" also means copyright-like laws that apply to other kinds of 66 | works, such as semiconductor masks. 67 | 68 | "The Program" refers to any copyrightable work licensed under this 69 | License. Each licensee is addressed as "you". "Licensees" and 70 | "recipients" may be individuals or organizations. 71 | 72 | To "modify" a work means to copy from or adapt all or part of the work 73 | in a fashion requiring copyright permission, other than the making of an 74 | exact copy. The resulting work is called a "modified version" of the 75 | earlier work or a work "based on" the earlier work. 76 | 77 | A "covered work" means either the unmodified Program or a work based 78 | on the Program. 79 | 80 | To "propagate" a work means to do anything with it that, without 81 | permission, would make you directly or secondarily liable for 82 | infringement under applicable copyright law, except executing it on a 83 | computer or modifying a private copy. Propagation includes copying, 84 | distribution (with or without modification), making available to the 85 | public, and in some countries other activities as well. 86 | 87 | To "convey" a work means any kind of propagation that enables other 88 | parties to make or receive copies. Mere interaction with a user through 89 | a computer network, with no transfer of a copy, is not conveying. 90 | 91 | An interactive user interface displays "Appropriate Legal Notices" 92 | to the extent that it includes a convenient and prominently visible 93 | feature that (1) displays an appropriate copyright notice, and (2) 94 | tells the user that there is no warranty for the work (except to the 95 | extent that warranties are provided), that licensees may convey the 96 | work under this License, and how to view a copy of this License. If 97 | the interface presents a list of user commands or options, such as a 98 | menu, a prominent item in the list meets this criterion. 99 | 100 | 1. Source Code. 101 | 102 | The "source code" for a work means the preferred form of the work 103 | for making modifications to it. "Object code" means any non-source 104 | form of a work. 105 | 106 | A "Standard Interface" means an interface that either is an official 107 | standard defined by a recognized standards body, or, in the case of 108 | interfaces specified for a particular programming language, one that 109 | is widely used among developers working in that language. 110 | 111 | The "System Libraries" of an executable work include anything, other 112 | than the work as a whole, that (a) is included in the normal form of 113 | packaging a Major Component, but which is not part of that Major 114 | Component, and (b) serves only to enable use of the work with that 115 | Major Component, or to implement a Standard Interface for which an 116 | implementation is available to the public in source code form. A 117 | "Major Component", in this context, means a major essential component 118 | (kernel, window system, and so on) of the specific operating system 119 | (if any) on which the executable work runs, or a compiler used to 120 | produce the work, or an object code interpreter used to run it. 121 | 122 | The "Corresponding Source" for a work in object code form means all 123 | the source code needed to generate, install, and (for an executable 124 | work) run the object code and to modify the work, including scripts to 125 | control those activities. However, it does not include the work's 126 | System Libraries, or general-purpose tools or generally available free 127 | programs which are used unmodified in performing those activities but 128 | which are not part of the work. For example, Corresponding Source 129 | includes interface definition files associated with source files for 130 | the work, and the source code for shared libraries and dynamically 131 | linked subprograms that the work is specifically designed to require, 132 | such as by intimate data communication or control flow between those 133 | subprograms and other parts of the work. 134 | 135 | The Corresponding Source need not include anything that users 136 | can regenerate automatically from other parts of the Corresponding 137 | Source. 138 | 139 | The Corresponding Source for a work in source code form is that 140 | same work. 141 | 142 | 2. Basic Permissions. 143 | 144 | All rights granted under this License are granted for the term of 145 | copyright on the Program, and are irrevocable provided the stated 146 | conditions are met. This License explicitly affirms your unlimited 147 | permission to run the unmodified Program. The output from running a 148 | covered work is covered by this License only if the output, given its 149 | content, constitutes a covered work. This License acknowledges your 150 | rights of fair use or other equivalent, as provided by copyright law. 151 | 152 | You may make, run and propagate covered works that you do not 153 | convey, without conditions so long as your license otherwise remains 154 | in force. You may convey covered works to others for the sole purpose 155 | of having them make modifications exclusively for you, or provide you 156 | with facilities for running those works, provided that you comply with 157 | the terms of this License in conveying all material for which you do 158 | not control copyright. Those thus making or running the covered works 159 | for you must do so exclusively on your behalf, under your direction 160 | and control, on terms that prohibit them from making any copies of 161 | your copyrighted material outside their relationship with you. 162 | 163 | Conveying under any other circumstances is permitted solely under 164 | the conditions stated below. Sublicensing is not allowed; section 10 165 | makes it unnecessary. 166 | 167 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 168 | 169 | No covered work shall be deemed part of an effective technological 170 | measure under any applicable law fulfilling obligations under article 171 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 172 | similar laws prohibiting or restricting circumvention of such 173 | measures. 174 | 175 | When you convey a covered work, you waive any legal power to forbid 176 | circumvention of technological measures to the extent such circumvention 177 | is effected by exercising rights under this License with respect to 178 | the covered work, and you disclaim any intention to limit operation or 179 | modification of the work as a means of enforcing, against the work's 180 | users, your or third parties' legal rights to forbid circumvention of 181 | technological measures. 182 | 183 | 4. Conveying Verbatim Copies. 184 | 185 | You may convey verbatim copies of the Program's source code as you 186 | receive it, in any medium, provided that you conspicuously and 187 | appropriately publish on each copy an appropriate copyright notice; 188 | keep intact all notices stating that this License and any 189 | non-permissive terms added in accord with section 7 apply to the code; 190 | keep intact all notices of the absence of any warranty; and give all 191 | recipients a copy of this License along with the Program. 192 | 193 | You may charge any price or no price for each copy that you convey, 194 | and you may offer support or warranty protection for a fee. 195 | 196 | 5. Conveying Modified Source Versions. 197 | 198 | You may convey a work based on the Program, or the modifications to 199 | produce it from the Program, in the form of source code under the 200 | terms of section 4, provided that you also meet all of these conditions: 201 | 202 | a) The work must carry prominent notices stating that you modified 203 | it, and giving a relevant date. 204 | 205 | b) The work must carry prominent notices stating that it is 206 | released under this License and any conditions added under section 207 | 7. This requirement modifies the requirement in section 4 to 208 | "keep intact all notices". 209 | 210 | c) You must license the entire work, as a whole, under this 211 | License to anyone who comes into possession of a copy. This 212 | License will therefore apply, along with any applicable section 7 213 | additional terms, to the whole of the work, and all its parts, 214 | regardless of how they are packaged. This License gives no 215 | permission to license the work in any other way, but it does not 216 | invalidate such permission if you have separately received it. 217 | 218 | d) If the work has interactive user interfaces, each must display 219 | Appropriate Legal Notices; however, if the Program has interactive 220 | interfaces that do not display Appropriate Legal Notices, your 221 | work need not make them do so. 222 | 223 | A compilation of a covered work with other separate and independent 224 | works, which are not by their nature extensions of the covered work, 225 | and which are not combined with it such as to form a larger program, 226 | in or on a volume of a storage or distribution medium, is called an 227 | "aggregate" if the compilation and its resulting copyright are not 228 | used to limit the access or legal rights of the compilation's users 229 | beyond what the individual works permit. Inclusion of a covered work 230 | in an aggregate does not cause this License to apply to the other 231 | parts of the aggregate. 232 | 233 | 6. Conveying Non-Source Forms. 234 | 235 | You may convey a covered work in object code form under the terms 236 | of sections 4 and 5, provided that you also convey the 237 | machine-readable Corresponding Source under the terms of this License, 238 | in one of these ways: 239 | 240 | a) Convey the object code in, or embodied in, a physical product 241 | (including a physical distribution medium), accompanied by the 242 | Corresponding Source fixed on a durable physical medium 243 | customarily used for software interchange. 244 | 245 | b) Convey the object code in, or embodied in, a physical product 246 | (including a physical distribution medium), accompanied by a 247 | written offer, valid for at least three years and valid for as 248 | long as you offer spare parts or customer support for that product 249 | model, to give anyone who possesses the object code either (1) a 250 | copy of the Corresponding Source for all the software in the 251 | product that is covered by this License, on a durable physical 252 | medium customarily used for software interchange, for a price no 253 | more than your reasonable cost of physically performing this 254 | conveying of source, or (2) access to copy the 255 | Corresponding Source from a network server at no charge. 256 | 257 | c) Convey individual copies of the object code with a copy of the 258 | written offer to provide the Corresponding Source. This 259 | alternative is allowed only occasionally and noncommercially, and 260 | only if you received the object code with such an offer, in accord 261 | with subsection 6b. 262 | 263 | d) Convey the object code by offering access from a designated 264 | place (gratis or for a charge), and offer equivalent access to the 265 | Corresponding Source in the same way through the same place at no 266 | further charge. You need not require recipients to copy the 267 | Corresponding Source along with the object code. If the place to 268 | copy the object code is a network server, the Corresponding Source 269 | may be on a different server (operated by you or a third party) 270 | that supports equivalent copying facilities, provided you maintain 271 | clear directions next to the object code saying where to find the 272 | Corresponding Source. Regardless of what server hosts the 273 | Corresponding Source, you remain obligated to ensure that it is 274 | available for as long as needed to satisfy these requirements. 275 | 276 | e) Convey the object code using peer-to-peer transmission, provided 277 | you inform other peers where the object code and Corresponding 278 | Source of the work are being offered to the general public at no 279 | charge under subsection 6d. 280 | 281 | A separable portion of the object code, whose source code is excluded 282 | from the Corresponding Source as a System Library, need not be 283 | included in conveying the object code work. 284 | 285 | A "User Product" is either (1) a "consumer product", which means any 286 | tangible personal property which is normally used for personal, family, 287 | or household purposes, or (2) anything designed or sold for incorporation 288 | into a dwelling. In determining whether a product is a consumer product, 289 | doubtful cases shall be resolved in favor of coverage. For a particular 290 | product received by a particular user, "normally used" refers to a 291 | typical or common use of that class of product, regardless of the status 292 | of the particular user or of the way in which the particular user 293 | actually uses, or expects or is expected to use, the product. A product 294 | is a consumer product regardless of whether the product has substantial 295 | commercial, industrial or non-consumer uses, unless such uses represent 296 | the only significant mode of use of the product. 297 | 298 | "Installation Information" for a User Product means any methods, 299 | procedures, authorization keys, or other information required to install 300 | and execute modified versions of a covered work in that User Product from 301 | a modified version of its Corresponding Source. The information must 302 | suffice to ensure that the continued functioning of the modified object 303 | code is in no case prevented or interfered with solely because 304 | modification has been made. 305 | 306 | If you convey an object code work under this section in, or with, or 307 | specifically for use in, a User Product, and the conveying occurs as 308 | part of a transaction in which the right of possession and use of the 309 | User Product is transferred to the recipient in perpetuity or for a 310 | fixed term (regardless of how the transaction is characterized), the 311 | Corresponding Source conveyed under this section must be accompanied 312 | by the Installation Information. But this requirement does not apply 313 | if neither you nor any third party retains the ability to install 314 | modified object code on the User Product (for example, the work has 315 | been installed in ROM). 316 | 317 | The requirement to provide Installation Information does not include a 318 | requirement to continue to provide support service, warranty, or updates 319 | for a work that has been modified or installed by the recipient, or for 320 | the User Product in which it has been modified or installed. Access to a 321 | network may be denied when the modification itself materially and 322 | adversely affects the operation of the network or violates the rules and 323 | protocols for communication across the network. 324 | 325 | Corresponding Source conveyed, and Installation Information provided, 326 | in accord with this section must be in a format that is publicly 327 | documented (and with an implementation available to the public in 328 | source code form), and must require no special password or key for 329 | unpacking, reading or copying. 330 | 331 | 7. Additional Terms. 332 | 333 | "Additional permissions" are terms that supplement the terms of this 334 | License by making exceptions from one or more of its conditions. 335 | Additional permissions that are applicable to the entire Program shall 336 | be treated as though they were included in this License, to the extent 337 | that they are valid under applicable law. If additional permissions 338 | apply only to part of the Program, that part may be used separately 339 | under those permissions, but the entire Program remains governed by 340 | this License without regard to the additional permissions. 341 | 342 | When you convey a copy of a covered work, you may at your option 343 | remove any additional permissions from that copy, or from any part of 344 | it. (Additional permissions may be written to require their own 345 | removal in certain cases when you modify the work.) You may place 346 | additional permissions on material, added by you to a covered work, 347 | for which you have or can give appropriate copyright permission. 348 | 349 | Notwithstanding any other provision of this License, for material you 350 | add to a covered work, you may (if authorized by the copyright holders of 351 | that material) supplement the terms of this License with terms: 352 | 353 | a) Disclaiming warranty or limiting liability differently from the 354 | terms of sections 15 and 16 of this License; or 355 | 356 | b) Requiring preservation of specified reasonable legal notices or 357 | author attributions in that material or in the Appropriate Legal 358 | Notices displayed by works containing it; or 359 | 360 | c) Prohibiting misrepresentation of the origin of that material, or 361 | requiring that modified versions of such material be marked in 362 | reasonable ways as different from the original version; or 363 | 364 | d) Limiting the use for publicity purposes of names of licensors or 365 | authors of the material; or 366 | 367 | e) Declining to grant rights under trademark law for use of some 368 | trade names, trademarks, or service marks; or 369 | 370 | f) Requiring indemnification of licensors and authors of that 371 | material by anyone who conveys the material (or modified versions of 372 | it) with contractual assumptions of liability to the recipient, for 373 | any liability that these contractual assumptions directly impose on 374 | those licensors and authors. 375 | 376 | All other non-permissive additional terms are considered "further 377 | restrictions" within the meaning of section 10. If the Program as you 378 | received it, or any part of it, contains a notice stating that it is 379 | governed by this License along with a term that is a further 380 | restriction, you may remove that term. If a license document contains 381 | a further restriction but permits relicensing or conveying under this 382 | License, you may add to a covered work material governed by the terms 383 | of that license document, provided that the further restriction does 384 | not survive such relicensing or conveying. 385 | 386 | If you add terms to a covered work in accord with this section, you 387 | must place, in the relevant source files, a statement of the 388 | additional terms that apply to those files, or a notice indicating 389 | where to find the applicable terms. 390 | 391 | Additional terms, permissive or non-permissive, may be stated in the 392 | form of a separately written license, or stated as exceptions; 393 | the above requirements apply either way. 394 | 395 | 8. Termination. 396 | 397 | You may not propagate or modify a covered work except as expressly 398 | provided under this License. Any attempt otherwise to propagate or 399 | modify it is void, and will automatically terminate your rights under 400 | this License (including any patent licenses granted under the third 401 | paragraph of section 11). 402 | 403 | However, if you cease all violation of this License, then your 404 | license from a particular copyright holder is reinstated (a) 405 | provisionally, unless and until the copyright holder explicitly and 406 | finally terminates your license, and (b) permanently, if the copyright 407 | holder fails to notify you of the violation by some reasonable means 408 | prior to 60 days after the cessation. 409 | 410 | Moreover, your license from a particular copyright holder is 411 | reinstated permanently if the copyright holder notifies you of the 412 | violation by some reasonable means, this is the first time you have 413 | received notice of violation of this License (for any work) from that 414 | copyright holder, and you cure the violation prior to 30 days after 415 | your receipt of the notice. 416 | 417 | Termination of your rights under this section does not terminate the 418 | licenses of parties who have received copies or rights from you under 419 | this License. If your rights have been terminated and not permanently 420 | reinstated, you do not qualify to receive new licenses for the same 421 | material under section 10. 422 | 423 | 9. Acceptance Not Required for Having Copies. 424 | 425 | You are not required to accept this License in order to receive or 426 | run a copy of the Program. Ancillary propagation of a covered work 427 | occurring solely as a consequence of using peer-to-peer transmission 428 | to receive a copy likewise does not require acceptance. However, 429 | nothing other than this License grants you permission to propagate or 430 | modify any covered work. These actions infringe copyright if you do 431 | not accept this License. Therefore, by modifying or propagating a 432 | covered work, you indicate your acceptance of this License to do so. 433 | 434 | 10. Automatic Licensing of Downstream Recipients. 435 | 436 | Each time you convey a covered work, the recipient automatically 437 | receives a license from the original licensors, to run, modify and 438 | propagate that work, subject to this License. You are not responsible 439 | for enforcing compliance by third parties with this License. 440 | 441 | An "entity transaction" is a transaction transferring control of an 442 | organization, or substantially all assets of one, or subdividing an 443 | organization, or merging organizations. If propagation of a covered 444 | work results from an entity transaction, each party to that 445 | transaction who receives a copy of the work also receives whatever 446 | licenses to the work the party's predecessor in interest had or could 447 | give under the previous paragraph, plus a right to possession of the 448 | Corresponding Source of the work from the predecessor in interest, if 449 | the predecessor has it or can get it with reasonable efforts. 450 | 451 | You may not impose any further restrictions on the exercise of the 452 | rights granted or affirmed under this License. For example, you may 453 | not impose a license fee, royalty, or other charge for exercise of 454 | rights granted under this License, and you may not initiate litigation 455 | (including a cross-claim or counterclaim in a lawsuit) alleging that 456 | any patent claim is infringed by making, using, selling, offering for 457 | sale, or importing the Program or any portion of it. 458 | 459 | 11. Patents. 460 | 461 | A "contributor" is a copyright holder who authorizes use under this 462 | License of the Program or a work on which the Program is based. The 463 | work thus licensed is called the contributor's "contributor version". 464 | 465 | A contributor's "essential patent claims" are all patent claims 466 | owned or controlled by the contributor, whether already acquired or 467 | hereafter acquired, that would be infringed by some manner, permitted 468 | by this License, of making, using, or selling its contributor version, 469 | but do not include claims that would be infringed only as a 470 | consequence of further modification of the contributor version. For 471 | purposes of this definition, "control" includes the right to grant 472 | patent sublicenses in a manner consistent with the requirements of 473 | this License. 474 | 475 | Each contributor grants you a non-exclusive, worldwide, royalty-free 476 | patent license under the contributor's essential patent claims, to 477 | make, use, sell, offer for sale, import and otherwise run, modify and 478 | propagate the contents of its contributor version. 479 | 480 | In the following three paragraphs, a "patent license" is any express 481 | agreement or commitment, however denominated, not to enforce a patent 482 | (such as an express permission to practice a patent or covenant not to 483 | sue for patent infringement). To "grant" such a patent license to a 484 | party means to make such an agreement or commitment not to enforce a 485 | patent against the party. 486 | 487 | If you convey a covered work, knowingly relying on a patent license, 488 | and the Corresponding Source of the work is not available for anyone 489 | to copy, free of charge and under the terms of this License, through a 490 | publicly available network server or other readily accessible means, 491 | then you must either (1) cause the Corresponding Source to be so 492 | available, or (2) arrange to deprive yourself of the benefit of the 493 | patent license for this particular work, or (3) arrange, in a manner 494 | consistent with the requirements of this License, to extend the patent 495 | license to downstream recipients. "Knowingly relying" means you have 496 | actual knowledge that, but for the patent license, your conveying the 497 | covered work in a country, or your recipient's use of the covered work 498 | in a country, would infringe one or more identifiable patents in that 499 | country that you have reason to believe are valid. 500 | 501 | If, pursuant to or in connection with a single transaction or 502 | arrangement, you convey, or propagate by procuring conveyance of, a 503 | covered work, and grant a patent license to some of the parties 504 | receiving the covered work authorizing them to use, propagate, modify 505 | or convey a specific copy of the covered work, then the patent license 506 | you grant is automatically extended to all recipients of the covered 507 | work and works based on it. 508 | 509 | A patent license is "discriminatory" if it does not include within 510 | the scope of its coverage, prohibits the exercise of, or is 511 | conditioned on the non-exercise of one or more of the rights that are 512 | specifically granted under this License. You may not convey a covered 513 | work if you are a party to an arrangement with a third party that is 514 | in the business of distributing software, under which you make payment 515 | to the third party based on the extent of your activity of conveying 516 | the work, and under which the third party grants, to any of the 517 | parties who would receive the covered work from you, a discriminatory 518 | patent license (a) in connection with copies of the covered work 519 | conveyed by you (or copies made from those copies), or (b) primarily 520 | for and in connection with specific products or compilations that 521 | contain the covered work, unless you entered into that arrangement, 522 | or that patent license was granted, prior to 28 March 2007. 523 | 524 | Nothing in this License shall be construed as excluding or limiting 525 | any implied license or other defenses to infringement that may 526 | otherwise be available to you under applicable patent law. 527 | 528 | 12. No Surrender of Others' Freedom. 529 | 530 | If conditions are imposed on you (whether by court order, agreement or 531 | otherwise) that contradict the conditions of this License, they do not 532 | excuse you from the conditions of this License. If you cannot convey a 533 | covered work so as to satisfy simultaneously your obligations under this 534 | License and any other pertinent obligations, then as a consequence you may 535 | not convey it at all. For example, if you agree to terms that obligate you 536 | to collect a royalty for further conveying from those to whom you convey 537 | the Program, the only way you could satisfy both those terms and this 538 | License would be to refrain entirely from conveying the Program. 539 | 540 | 13. Remote Network Interaction; Use with the GNU General Public License. 541 | 542 | Notwithstanding any other provision of this License, if you modify the 543 | Program, your modified version must prominently offer all users 544 | interacting with it remotely through a computer network (if your version 545 | supports such interaction) an opportunity to receive the Corresponding 546 | Source of your version by providing access to the Corresponding Source 547 | from a network server at no charge, through some standard or customary 548 | means of facilitating copying of software. This Corresponding Source 549 | shall include the Corresponding Source for any work covered by version 3 550 | of the GNU General Public License that is incorporated pursuant to the 551 | following paragraph. 552 | 553 | Notwithstanding any other provision of this License, you have 554 | permission to link or combine any covered work with a work licensed 555 | under version 3 of the GNU General Public License into a single 556 | combined work, and to convey the resulting work. The terms of this 557 | License will continue to apply to the part which is the covered work, 558 | but the work with which it is combined will remain governed by version 559 | 3 of the GNU General Public License. 560 | 561 | 14. Revised Versions of this License. 562 | 563 | The Free Software Foundation may publish revised and/or new versions of 564 | the GNU Affero General Public License from time to time. Such new versions 565 | will be similar in spirit to the present version, but may differ in detail to 566 | address new problems or concerns. 567 | 568 | Each version is given a distinguishing version number. If the 569 | Program specifies that a certain numbered version of the GNU Affero General 570 | Public License "or any later version" applies to it, you have the 571 | option of following the terms and conditions either of that numbered 572 | version or of any later version published by the Free Software 573 | Foundation. If the Program does not specify a version number of the 574 | GNU Affero General Public License, you may choose any version ever published 575 | by the Free Software Foundation. 576 | 577 | If the Program specifies that a proxy can decide which future 578 | versions of the GNU Affero General Public License can be used, that proxy's 579 | public statement of acceptance of a version permanently authorizes you 580 | to choose that version for the Program. 581 | 582 | Later license versions may give you additional or different 583 | permissions. However, no additional obligations are imposed on any 584 | author or copyright holder as a result of your choosing to follow a 585 | later version. 586 | 587 | 15. Disclaimer of Warranty. 588 | 589 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 590 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 591 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 592 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 593 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 594 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 595 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 596 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 597 | 598 | 16. Limitation of Liability. 599 | 600 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 601 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 602 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 603 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 604 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 605 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 606 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 607 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 608 | SUCH DAMAGES. 609 | 610 | 17. Interpretation of Sections 15 and 16. 611 | 612 | If the disclaimer of warranty and limitation of liability provided 613 | above cannot be given local legal effect according to their terms, 614 | reviewing courts shall apply local law that most closely approximates 615 | an absolute waiver of all civil liability in connection with the 616 | Program, unless a warranty or assumption of liability accompanies a 617 | copy of the Program in return for a fee. 618 | 619 | END OF TERMS AND CONDITIONS 620 | 621 | How to Apply These Terms to Your New Programs 622 | 623 | If you develop a new program, and you want it to be of the greatest 624 | possible use to the public, the best way to achieve this is to make it 625 | free software which everyone can redistribute and change under these terms. 626 | 627 | To do so, attach the following notices to the program. It is safest 628 | to attach them to the start of each source file to most effectively 629 | state the exclusion of warranty; and each file should have at least 630 | the "copyright" line and a pointer to where the full notice is found. 631 | 632 | 633 | Copyright (C) 634 | 635 | This program is free software: you can redistribute it and/or modify 636 | it under the terms of the GNU Affero General Public License as published 637 | by the Free Software Foundation, either version 3 of the License, or 638 | (at your option) any later version. 639 | 640 | This program is distributed in the hope that it will be useful, 641 | but WITHOUT ANY WARRANTY; without even the implied warranty of 642 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 643 | GNU Affero General Public License for more details. 644 | 645 | You should have received a copy of the GNU Affero General Public License 646 | along with this program. If not, see . 647 | 648 | Also add information on how to contact you by electronic and paper mail. 649 | 650 | If your software can interact with users remotely through a computer 651 | network, you should also make sure that it provides a way for users to 652 | get its source. For example, if your program is a web application, its 653 | interface could display a "Source" link that leads users to an archive 654 | of the code. There are many ways you could offer source, and different 655 | solutions will be better for different programs; see section 13 for the 656 | specific requirements. 657 | 658 | You should also get your employer (if you work as a programmer) or school, 659 | if any, to sign a "copyright disclaimer" for the program, if necessary. 660 | For more information on this, and how to apply and follow the GNU AGPL, see 661 | . -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | # MC-PLUGIN🍐 6 | 7 | - 一个适用于 [Yunzai 系列机器人框架](https://github.com/yhArcadia/Yunzai-Bot-plugins-index) 的 Minecraft Server 消息互通插件 8 | 9 | - 移植于 17TheWord 大佬的 [nonebot-plugin-mcqq](https://github.com/17TheWord/nonebot-plugin-mcqq),在使用 Nonebot 的同学请传送 10 | 11 | - **使用中遇到问题请加 QQ 群咨询:[707331865](https://qm.qq.com/q/TXTIS9KhO2)** 12 | 13 | > [!TIP] 14 | > 群里开了个 Minecraft 服务器,发现一个很好的消息互通插件,就是 17TheWord 大佬的 [nonebot-plugin-mcqq](https://github.com/17TheWord/nonebot-plugin-mcqq),但发现 Yunzai 没有,于是把插件移植了过来 15 | 16 | ## 安装插件 17 | 18 | #### 1. 克隆仓库 19 | 20 | ``` 21 | git clone https://github.com/CikeyQi/mc-plugin.git ./plugins/mc-plugin 22 | ``` 23 | 24 | > [!NOTE] 25 | > 如果你的网络环境较差,无法连接到 Github,可以使用 [GitHub Proxy](https://ghproxy.link/) 提供的文件代理加速下载服务 26 | 27 | #### 2. 安装依赖 28 | 29 | ``` 30 | pnpm install --filter=mc-plugin 31 | ``` 32 | 33 | ## 插件配置 34 | 35 | > [!WARNING] 36 | > 非常不建议手动修改配置文件,本插件已兼容 [Guoba-plugin](https://github.com/guoba-yunzai/guoba-plugin) ,请使用锅巴插件对配置项进行修改 37 | 38 | - 请查看文档:[Wiki](https://github.com/CikeyQi/mc-plugin/wiki),请按照 Wiki 中的说明进行配置 39 | 40 | ## 功能列表 41 | 42 | 请使用 `#mc帮助` 获取完整帮助 43 | 44 | - [x] 玩家加入 / 离开服务器消息 45 | - [x] 玩家聊天信息发送到群内 46 | - [x] 玩家死亡信息 47 | - [x] 群内使用指令 48 | - [x] 群员聊天文本发送到服务器 49 | - [x] 特殊消息支持 50 | - [x] 多服务器连接 51 | - [x] 断线自动重连 52 | - [x] 正向 / 反向 WebSocket 连接 53 | - [x] 使用 [@kitUIN/ChatImage](https://github.com/kitUIN/ChatImage) 在游戏内显示图片 54 | 55 | ## 常见问题 56 | 57 | 1. 什么环境才能使用本插件? 58 | - 需要机器人所在服务器和 Minecraft 服务器任意一个可以被另一个访问(在同一内网或至少其中一个有公网) 59 | 2. 支持哪些服务端? 60 | - `Spigot端`,`Velocity端`,`Fabric端`,`Forge端`,`NeoForge` 均支持 61 | 62 | ## 支持与贡献 63 | 64 | 如果你喜欢这个项目,请不妨点个 Star🌟,这是对开发者最大的动力, 当然,你可以对我 [爱发电](https://afdian.net/a/sumoqi) 赞助,呜咪~❤️ 65 | 66 | 有意见或者建议也欢迎提交 [Issues](https://github.com/CikeyQi/mc-plugin/issues) 和 [Pull requests](https://github.com/CikeyQi/mc-plugin/pulls)。 67 | 68 | ## 相关项目 69 | 70 | - [nonebot-plugin-mcqq](https://github.com/17TheWord/nonebot-plugin-mcqq):基于 NoneBot 的与 Minecraft Server 互通消息的插件 71 | 72 | ## 许可证 73 | 74 | 本项目使用 [GNU AGPLv3](https://choosealicense.com/licenses/agpl-3.0/) 作为开源许可证。 -------------------------------------------------------------------------------- /apps/Actionbar.js: -------------------------------------------------------------------------------- 1 | import plugin from "../../../lib/plugins/plugin.js"; 2 | import RconManager from "../components/Rcon.js"; 3 | import WebSocketManager from "../components/WebSocket.js"; 4 | import Config from "../components/Config.js"; 5 | 6 | const LOG_PREFIX_CLIENT = logger.blue('[Minecraft Client] '); 7 | const LOG_PREFIX_RCON = logger.blue('[Minecraft RCON] '); 8 | const LOG_PREFIX_WS = logger.blue('[Minecraft WebSocket] '); 9 | 10 | export class ActionBar extends plugin { 11 | constructor() { 12 | super({ 13 | name: "MCQQ-发送动作栏标题", 14 | event: "message", 15 | priority: 1008, 16 | rule: [ 17 | { 18 | reg: "#?mcab (.*)", 19 | fnc: "actionBar", 20 | }, 21 | ], 22 | }); 23 | } 24 | 25 | async actionBar(e) { 26 | if (!e.isGroup) { 27 | return false; 28 | } 29 | 30 | const globalConfig = await Config.getConfig(); 31 | const { mc_qq_server_list: serverList, debug_mode: debugMode } = globalConfig; 32 | 33 | if (!serverList || serverList.length === 0) { 34 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + '无服务器配置,跳过同步'); 35 | return false; 36 | } 37 | 38 | const targetServers = serverList.filter(serverCfg => 39 | serverCfg.group_list?.some(groupId => groupId == e.group_id) 40 | ); 41 | 42 | if (targetServers.length === 0) { 43 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + `群 ${e.group_id} 未关联任何服务器,跳过同步`); 44 | return false; 45 | } 46 | 47 | for (const serverCfg of targetServers) { 48 | const serverName = serverCfg.server_name; 49 | const rconConnection = RconManager.activeConnections?.[serverName]; 50 | const wsConnection = WebSocketManager.activeSockets?.[serverName]; 51 | 52 | if (!rconConnection && !wsConnection) { 53 | if (debugMode) { 54 | logger.mark(LOG_PREFIX_CLIENT + logger.yellow(serverName) + ' 未连接 (RCON 和 WebSocket 均不可用)'); 55 | } 56 | continue; 57 | } 58 | 59 | const [, message] = e.msg.match(this.rule[0].reg); 60 | 61 | if (wsConnection) { 62 | const wsPayload = JSON.stringify({ 63 | api: "send_actionbar", 64 | data: { message: message }, 65 | echo: String(Date.now()) 66 | }); 67 | try { 68 | wsConnection.send(wsPayload); 69 | if (debugMode) { 70 | logger.mark(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息 (WebSocket): ${message}`); 71 | } 72 | } catch (error) { 73 | if (debugMode) logger.error(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息失败 (WebSocket): ${error.message}`); 74 | if (rconConnection) { 75 | if (debugMode) logger.info(LOG_PREFIX_WS + `WebSocket发送失败,尝试使用RCON发送到 ${serverName}`); 76 | const response = await rconConnection.send(`title @a actionbar {"text":"${message}"}`); 77 | if (response === null && debugMode) { 78 | logger.warn(LOG_PREFIX_RCON + `title 命令发送到 ${logger.green(serverName)} 失败或无响应`); 79 | } 80 | } 81 | } 82 | } else if (rconConnection) { 83 | const response = await rconConnection.send(`title @a actionbar {"text":"${message}"}`); 84 | if (response === null && debugMode) { 85 | logger.warn(LOG_PREFIX_RCON + `title 命令发送到 ${logger.green(serverName)} 失败或无响应`); 86 | } 87 | } else { 88 | if (debugMode) logger.warn(LOG_PREFIX_CLIENT + `${serverName} 无可用连接方式 (WebSocket/RCON) 来同步聊天消息`); 89 | } 90 | } 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apps/Help.js: -------------------------------------------------------------------------------- 1 | import plugin from '../../../lib/plugins/plugin.js' 2 | import Render from '../components/Render.js' 3 | import { style } from '../resources/help/imgs/config.js' 4 | import _ from 'lodash' 5 | 6 | export class help extends plugin { 7 | constructor() { 8 | super({ 9 | /** 功能名称 */ 10 | name: 'MCQQ-插件帮助', 11 | event: 'message', 12 | /** 优先级,数字越小等级越高 */ 13 | priority: 1009, 14 | rule: [ 15 | { 16 | /** 命令正则匹配 */ 17 | reg: '^#?mc帮助$', 18 | /** 执行方法 */ 19 | fnc: 'sendHelpPic' 20 | } 21 | ] 22 | }) 23 | } 24 | async sendHelpPic(e) { 25 | const helpCfg = { 26 | "themeSet": false, 27 | "title": "#mc-plugin帮助", 28 | "subTitle": "Yunzai-Bot & mc-plugin", 29 | "colWidth": 265, 30 | "theme": "all", 31 | "themeExclude": [ 32 | "default" 33 | ], 34 | "colCount": 2, 35 | "bgBlur": true 36 | } 37 | const helpList = [ 38 | { 39 | "group": "MC设置", 40 | "list": [ 41 | { 42 | "icon": 1, 43 | "title": "#mc状态", 44 | "desc": "查看连接状态" 45 | }, 46 | { 47 | "icon": 2, 48 | "title": "#mc开启同步<服务器名称>", 49 | "desc": "将当前群聊与指定服务器同步" 50 | }, 51 | { 52 | "icon": 3, 53 | "title": "#mc关闭同步<服务器名称>", 54 | "desc": "关闭当前群聊与服务器同步" 55 | }, 56 | { 57 | "icon": 4, 58 | "title": "#mc重连", 59 | "desc": "手动重连服务器" 60 | }, 61 | { 62 | "icon": 5, 63 | "title": "#mcab <文本>", 64 | "desc": "发送动作栏标题" 65 | }, 66 | { 67 | "icon": 6, 68 | "title": "#mcp <文本>", 69 | "desc": "发送私聊消息" 70 | }, 71 | { 72 | "icon": 7, 73 | "title": "#mct <文本>", 74 | "desc": "发送标题" 75 | }, 76 | { 77 | "icon": 8, 78 | "title": "#mcst <文本>", 79 | "desc": "发送子标题" 80 | } 81 | ], 82 | } 83 | ] 84 | let helpGroup = [] 85 | _.forEach(helpList, (group) => { 86 | _.forEach(group.list, (help) => { 87 | let icon = help.icon * 1 88 | if (!icon) { 89 | help.css = 'display:none' 90 | } else { 91 | let x = (icon - 1) % 10 92 | let y = (icon - x - 1) / 10 93 | help.css = `background-position:-${x * 50}px -${y * 50}px` 94 | } 95 | }) 96 | helpGroup.push(group) 97 | }) 98 | 99 | let themeData = await this.getThemeData(helpCfg, helpCfg) 100 | return await Render.render('help/index', { 101 | helpCfg, 102 | helpGroup, 103 | ...themeData, 104 | element: 'default' 105 | }, { e, scale: 1.6 }) 106 | } 107 | 108 | async getThemeCfg() { 109 | let resPath = '{{_res_path}}/help/imgs/' 110 | return { 111 | main: `${resPath}/main.png`, 112 | bg: `${resPath}/bg.jpg`, 113 | style: style 114 | } 115 | } 116 | 117 | async getThemeData(diyStyle, sysStyle) { 118 | let helpConfig = _.extend({}, sysStyle, diyStyle) 119 | let colCount = Math.min(5, Math.max(parseInt(helpConfig?.colCount) || 3, 2)) 120 | let colWidth = Math.min(500, Math.max(100, parseInt(helpConfig?.colWidth) || 265)) 121 | let width = Math.min(2500, Math.max(800, colCount * colWidth + 30)) 122 | let theme = await this.getThemeCfg() 123 | let themeStyle = theme.style || {} 124 | let ret = [` 125 | body{background-image:url(${theme.bg});width:${width}px;} 126 | .container{background-image:url(${theme.main});width:${width}px;} 127 | .help-table .td,.help-table .th{width:${100 / colCount}%} 128 | `] 129 | let css = function (sel, css, key, def, fn) { 130 | let val = (function () { 131 | for (let idx in arguments) { 132 | if (!_.isUndefined(arguments[idx])) { 133 | return arguments[idx] 134 | } 135 | } 136 | })(themeStyle[key], diyStyle[key], sysStyle[key], def) 137 | if (fn) { 138 | val = fn(val) 139 | } 140 | ret.push(`${sel}{${css}:${val}}`) 141 | } 142 | css('.help-title,.help-group', 'color', 'fontColor', '#ceb78b') 143 | css('.help-title,.help-group', 'text-shadow', 'fontShadow', 'none') 144 | css('.help-desc', 'color', 'descColor', '#eee') 145 | css('.cont-box', 'background', 'contBgColor', 'rgba(43, 52, 61, 0.8)') 146 | css('.cont-box', 'backdrop-filter', 'contBgBlur', 3, (n) => diyStyle.bgBlur === false ? 'none' : `blur(${n}px)`) 147 | css('.help-group', 'background', 'headerBgColor', 'rgba(34, 41, 51, .4)') 148 | css('.help-table .tr:nth-child(odd)', 'background', 'rowBgColor1', 'rgba(34, 41, 51, .2)') 149 | css('.help-table .tr:nth-child(even)', 'background', 'rowBgColor2', 'rgba(34, 41, 51, .4)') 150 | return { 151 | style: ``, 152 | colCount 153 | } 154 | } 155 | } -------------------------------------------------------------------------------- /apps/Main.js: -------------------------------------------------------------------------------- 1 | import plugin from '../../../lib/plugins/plugin.js'; 2 | import RconManager from '../components/Rcon.js'; 3 | import WebSocketManager from '../components/WebSocket.js'; 4 | import Config from '../components/Config.js'; 5 | 6 | const LOG_PREFIX_CLIENT = logger.blue('[Minecraft Client] '); 7 | const LOG_PREFIX_RCON = logger.blue('[Minecraft RCON] '); 8 | const LOG_PREFIX_WS = logger.blue('[Minecraft WebSocket] '); 9 | 10 | export class Main extends plugin { 11 | constructor() { 12 | super({ 13 | name: 'MCQQ-消息同步', 14 | event: 'message', 15 | priority: 1009, 16 | rule: [ 17 | { 18 | reg: '', 19 | fnc: 'handleSync', 20 | log: false, 21 | }, 22 | ], 23 | }); 24 | } 25 | 26 | async handleSync(e) { 27 | if (!e.isGroup) { 28 | return false; 29 | } 30 | 31 | const globalConfig = await Config.getConfig(); 32 | const { mc_qq_server_list: serverList, debug_mode: debugMode } = globalConfig; 33 | 34 | if (!serverList || serverList.length === 0) { 35 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + '无服务器配置,跳过同步'); 36 | return false; 37 | } 38 | 39 | const targetServers = serverList.filter(serverCfg => 40 | serverCfg.group_list?.some(groupId => groupId == e.group_id) 41 | ); 42 | 43 | if (targetServers.length === 0) { 44 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + `群 ${e.group_id} 未关联任何服务器,跳过同步`); 45 | return false; 46 | } 47 | 48 | for (const serverCfg of targetServers) { 49 | const serverName = serverCfg.server_name; 50 | const rconConnection = RconManager.activeConnections?.[serverName]; 51 | const wsConnection = WebSocketManager.activeSockets?.[serverName]; 52 | 53 | if (!rconConnection && !wsConnection) { 54 | if (debugMode) { 55 | logger.mark(LOG_PREFIX_CLIENT + logger.yellow(serverName) + ' 未连接 (RCON 和 WebSocket 均不可用)'); 56 | } 57 | continue; 58 | } 59 | 60 | const isCommand = e.msg?.startsWith(serverCfg.command_header); 61 | const canExecuteCommand = serverCfg.command_user?.some(user => user == e.user_id) || e.isMaster; 62 | 63 | if (isCommand && canExecuteCommand) { 64 | await this._handleServerCommand(e, serverCfg, rconConnection, debugMode); 65 | } else if (!isCommand) { 66 | await this._handleChatMessageSync(e, serverCfg, wsConnection, rconConnection, globalConfig); 67 | } 68 | } 69 | 70 | return false; 71 | } 72 | 73 | async _handleServerCommand(e, serverCfg, rconConn, debugMode) { 74 | const serverName = serverCfg.server_name; 75 | 76 | if (!rconConn) { 77 | await e.reply(`${serverName} 的 RCON 未连接,无法执行服务器命令`); 78 | if (debugMode) logger.warn(LOG_PREFIX_RCON + `${serverName} RCON 未连接,命令执行中止`); 79 | return; 80 | } 81 | 82 | const command = e.msg.substring(serverCfg.command_header.length); 83 | if (debugMode) { 84 | logger.mark(LOG_PREFIX_RCON + `向 ${logger.green(serverName)} 发送命令: ${logger.yellow(command)}`); 85 | } 86 | 87 | let response = await rconConn.send(command) 88 | 89 | if (response !== null) { 90 | if (serverCfg.mask_word) { 91 | try { 92 | response = response.replace(new RegExp(serverCfg.mask_word, "g"), ''); 93 | } catch (err) { 94 | if (debugMode) logger.error(LOG_PREFIX_RCON + `屏蔽词正则错误 ${err.message}`); 95 | } 96 | } 97 | await e.reply(response); 98 | if (debugMode) { 99 | logger.mark(LOG_PREFIX_RCON + `${logger.green(serverName)} 返回消息: ${logger.green(response)}`); 100 | } 101 | } else { 102 | if (debugMode) logger.warn(LOG_PREFIX_RCON + `命令发送到 ${logger.green(serverName)} 未收到响应或失败`); 103 | } 104 | } 105 | 106 | _formatMinecraftMessage(e, globalConfig) { 107 | const { mc_qq_send_group_name: prefixGroup, mc_qq_say_way: saySuffix, mc_qq_chat_image_enable: imageAsCICode } = globalConfig; 108 | let messagePrefix = `${prefixGroup ? `[${e.group_name}] ` : ''}[${e.sender.nickname}] ${saySuffix || '说:'} `; 109 | 110 | e.message.forEach(element => { 111 | switch (element.type) { 112 | case 'text': 113 | messagePrefix += element.text.replace(/\r/g, "").replace(/\n/g, "\n * "); 114 | break; 115 | case 'image': 116 | if (imageAsCICode) { 117 | messagePrefix += `[[CICode,url=${element.url},name=图片]]`; 118 | } else { 119 | messagePrefix += `[图片]`; 120 | } 121 | break; 122 | default: 123 | messagePrefix += `[${element.type}] ${element.text || ''}`; 124 | break; 125 | } 126 | }); 127 | return messagePrefix; 128 | } 129 | 130 | async _handleChatMessageSync(e, serverCfg, wsConn, rconConn, globalConfig) { 131 | const serverName = serverCfg.server_name; 132 | const { debug_mode: debugMode } = globalConfig; 133 | 134 | const message = this._formatMinecraftMessage(e, globalConfig); 135 | 136 | if (wsConn) { 137 | const wsPayload = JSON.stringify({ 138 | api: "send_msg", 139 | data: { message: message }, 140 | echo: String(Date.now()) 141 | }); 142 | try { 143 | wsConn.send(wsPayload); 144 | if (debugMode) { 145 | logger.mark(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息 (WebSocket): ${message}`); 146 | } 147 | } catch (error) { 148 | if (debugMode) logger.error(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息失败 (WebSocket): ${error.message}`); 149 | if (rconConn) { 150 | if (debugMode) logger.info(LOG_PREFIX_WS + `WebSocket发送失败,尝试使用RCON发送到 ${serverName}`); 151 | await this._sendChatMessageViaRcon(message, serverName, rconConn, debugMode); 152 | } 153 | } 154 | } else if (rconConn) { 155 | await this._sendChatMessageViaRcon(message, serverName, rconConn, debugMode); 156 | } else { 157 | if (debugMode) logger.warn(LOG_PREFIX_CLIENT + `${serverName} 无可用连接方式 (WebSocket/RCON) 来同步聊天消息`); 158 | } 159 | } 160 | 161 | async _sendChatMessageViaRcon(message, serverName, rconConn, debugMode) { 162 | const tellrawCommand = `tellraw @a {"text":"${message}"}`; 163 | 164 | if (debugMode) { 165 | logger.mark(LOG_PREFIX_RCON + `向 ${logger.green(serverName)} 发送消息 (RCON tellraw): ${tellrawCommand}`); 166 | } 167 | const response = await rconConn.send(tellrawCommand); 168 | if (response === null && debugMode) { 169 | logger.warn(LOG_PREFIX_RCON + `tellraw 命令发送到 ${logger.green(serverName)} 失败或无响应`); 170 | } 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /apps/Private.js: -------------------------------------------------------------------------------- 1 | import plugin from "../../../lib/plugins/plugin.js"; 2 | import RconManager from "../components/Rcon.js"; 3 | import WebSocketManager from "../components/WebSocket.js"; 4 | import Config from "../components/Config.js"; 5 | 6 | const LOG_PREFIX_CLIENT = logger.blue('[Minecraft Client] '); 7 | const LOG_PREFIX_RCON = logger.blue('[Minecraft RCON] '); 8 | const LOG_PREFIX_WS = logger.blue('[Minecraft WebSocket] '); 9 | 10 | export class Private extends plugin { 11 | constructor() { 12 | super({ 13 | name: "MCQQ-发送私聊消息", 14 | event: "message", 15 | priority: 1008, 16 | rule: [ 17 | { 18 | reg: "#?mcp (.*) (.*)", 19 | fnc: "private", 20 | }, 21 | ], 22 | }); 23 | } 24 | 25 | async private(e) { 26 | if (!e.isGroup) { 27 | return false; 28 | } 29 | 30 | const globalConfig = await Config.getConfig(); 31 | const { mc_qq_server_list: serverList, debug_mode: debugMode } = globalConfig; 32 | 33 | if (!serverList || serverList.length === 0) { 34 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + '无服务器配置,跳过同步'); 35 | return false; 36 | } 37 | 38 | const targetServers = serverList.filter(serverCfg => 39 | serverCfg.group_list?.some(groupId => groupId == e.group_id) 40 | ); 41 | 42 | if (targetServers.length === 0) { 43 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + `群 ${e.group_id} 未关联任何服务器,跳过同步`); 44 | return false; 45 | } 46 | 47 | for (const serverCfg of targetServers) { 48 | const serverName = serverCfg.server_name; 49 | const rconConnection = RconManager.activeConnections?.[serverName]; 50 | const wsConnection = WebSocketManager.activeSockets?.[serverName]; 51 | 52 | if (!rconConnection && !wsConnection) { 53 | if (debugMode) { 54 | logger.mark(LOG_PREFIX_CLIENT + logger.yellow(serverName) + ' 未连接 (RCON 和 WebSocket 均不可用)'); 55 | } 56 | continue; 57 | } 58 | 59 | const [, nickname, message] = e.msg.match(this.rule[0].reg); 60 | 61 | if (wsConnection) { 62 | const wsPayload = JSON.stringify({ 63 | api: "send_private_msg", 64 | data: { nickname: nickname, message: message }, 65 | echo: String(Date.now()) 66 | }); 67 | try { 68 | wsConnection.send(wsPayload); 69 | if (debugMode) { 70 | logger.mark(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息 (WebSocket): ${message}`); 71 | } 72 | } catch (error) { 73 | if (debugMode) logger.error(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息失败 (WebSocket): ${error.message}`); 74 | if (rconConnection) { 75 | if (debugMode) logger.info(LOG_PREFIX_WS + `WebSocket发送失败,尝试使用RCON发送到 ${serverName}`); 76 | const response = await rconConnection.send(`tell ${nickname} ${message}`); 77 | if (response === null && debugMode) { 78 | logger.warn(LOG_PREFIX_RCON + `tell 命令发送到 ${logger.green(serverName)} 失败或无响应`); 79 | } 80 | } 81 | } 82 | } else if (rconConnection) { 83 | const response = await rconConnection.send(`tell ${nickname} ${message}`); 84 | console.log(response); 85 | if (response === null && debugMode) { 86 | logger.warn(LOG_PREFIX_RCON + `tell 命令发送到 ${logger.green(serverName)} 失败或无响应`); 87 | } 88 | } else { 89 | if (debugMode) logger.warn(LOG_PREFIX_CLIENT + `${serverName} 无可用连接方式 (WebSocket/RCON) 来同步聊天消息`); 90 | } 91 | } 92 | return true; 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /apps/Setting.js: -------------------------------------------------------------------------------- 1 | import plugin from '../../../lib/plugins/plugin.js' 2 | import WebSocket from '../components/WebSocket.js' 3 | import RconManager from '../components/Rcon.js' 4 | import Config from '../components/Config.js' 5 | 6 | export class Setting extends plugin { 7 | constructor() { 8 | super({ 9 | /** 功能名称 */ 10 | name: 'MCQQ-设置同步', 11 | event: 'message', 12 | /** 优先级,数字越小等级越高 */ 13 | priority: 1009, 14 | rule: [ 15 | { 16 | /** 命令正则匹配 */ 17 | reg: '#?mc(开启|关闭)同步(.*)$', 18 | /** 执行方法 */ 19 | fnc: 'setting', 20 | /** 主人权限 */ 21 | permission: 'master' 22 | }, 23 | { 24 | /** 命令正则匹配 */ 25 | reg: '#?mc重连$', 26 | /** 执行方法 */ 27 | fnc: 'reconnect', 28 | /** 主人权限 */ 29 | permission: 'master' 30 | } 31 | ] 32 | }) 33 | } 34 | 35 | async setting(e) { 36 | 37 | if (!e.group_id) { 38 | await e.reply('请在群内使用此功能') 39 | return true 40 | } 41 | 42 | const [_, operation, name] = e.msg.match(this.rule[0].reg) 43 | const server_name = name?.trim() 44 | 45 | if (!server_name) { 46 | await e.reply('请输入要同步的服务器名称,如 #mc开启同步Server1') 47 | return true 48 | } 49 | 50 | const config = Config.getConfig() 51 | if (!config.mc_qq_server_list.length) { 52 | await e.reply('请先在配置文件中添加服务器信息') 53 | return true 54 | } 55 | 56 | const index = config.mc_qq_server_list.findIndex(s => s.server_name === server_name) 57 | if (index === -1) { 58 | await e.reply(`未找到服务器「${server_name}」,发送[#mc状态]查看列表`); 59 | return true 60 | } 61 | const server = config.mc_qq_server_list[index] 62 | 63 | const isEnable = operation === '开启' 64 | 65 | if (isEnable) { 66 | server.group_list = [...new Set([...(server.group_list || []), e.group_id.toString()])] 67 | server.bot_self_id = [...new Set([...(server.bot_self_id || []), e.self_id.toString()])] 68 | server.rcon_able = true 69 | await e.reply(`✅ 已开启与 ${server_name} 的同步`) 70 | } else { 71 | server.group_list = (server.group_list || []).filter(g => g !== e.group_id.toString()) 72 | server.bot_self_id = (server.bot_self_id || []).filter(id => id !== e.self_id.toString()) 73 | server.rcon_able = !!server.group_list.length 74 | await e.reply(`⛔ 已关闭与 ${server_name} 的同步`) 75 | } 76 | 77 | Config.setConfig(config); 78 | return true 79 | } 80 | 81 | async reconnect(e) { 82 | await e.reply('正在重连全部已掉线服务器,请稍后...') 83 | 84 | await WebSocket._initializeAsync() 85 | await RconManager._initializeConnectionsAsync() 86 | 87 | return true 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /apps/Status.js: -------------------------------------------------------------------------------- 1 | import plugin from '../../../lib/plugins/plugin.js' 2 | import WebSocket from '../components/WebSocket.js' 3 | import RconManager from '../components/Rcon.js' 4 | import Config from '../components/Config.js' 5 | 6 | export class Status extends plugin { 7 | constructor() { 8 | super({ 9 | /** 功能名称 */ 10 | name: 'MCQQ-连接状态', 11 | event: 'message', 12 | /** 优先级,数字越小等级越高 */ 13 | priority: 1009, 14 | rule: [ 15 | { 16 | /** 命令正则匹配 */ 17 | reg: '#?mc状态$', 18 | /** 执行方法 */ 19 | fnc: 'status', 20 | } 21 | ] 22 | }) 23 | } 24 | 25 | async status(e) { 26 | try { 27 | const activeSockets = WebSocket.activeSockets 28 | const activeConnections = RconManager.activeConnections 29 | const config = await Config.getConfig().mc_qq_server_list 30 | 31 | let msg = `当前连接状态:\n` 32 | 33 | config.forEach(async (item) => { 34 | msg += '\n'; 35 | msg += `┌ 服务器名称:${item.server_name}\n`; 36 | msg += `├ WebSocket连接状态:${activeSockets[item.server_name] ? '已连接' : '未连接'}\n`; 37 | msg += `└ Rcon连接状态:${item.rcon_able ? (activeConnections[item.server_name] ? '已连接' : '未连接') : '已关闭'}\n`; 38 | }) 39 | 40 | await e.reply(msg) 41 | } catch (error) { 42 | logger.error(error) 43 | await e.reply('查询失败,请检查配置文件') 44 | } 45 | 46 | return true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /apps/Subtitle.js: -------------------------------------------------------------------------------- 1 | import plugin from "../../../lib/plugins/plugin.js"; 2 | import RconManager from "../components/Rcon.js"; 3 | import WebSocketManager from "../components/WebSocket.js"; 4 | import Config from "../components/Config.js"; 5 | 6 | const LOG_PREFIX_CLIENT = logger.blue('[Minecraft Client] '); 7 | const LOG_PREFIX_RCON = logger.blue('[Minecraft RCON] '); 8 | const LOG_PREFIX_WS = logger.blue('[Minecraft WebSocket] '); 9 | 10 | export class Subtitle extends plugin { 11 | constructor() { 12 | super({ 13 | name: "MCQQ-发送子标题", 14 | event: "message", 15 | priority: 1008, 16 | rule: [ 17 | { 18 | reg: "#?mcst (.*)", 19 | fnc: "subTitle", 20 | }, 21 | ], 22 | }); 23 | } 24 | 25 | async subTitle(e) { 26 | if (!e.isGroup) { 27 | return false; 28 | } 29 | 30 | const globalConfig = await Config.getConfig(); 31 | const { mc_qq_server_list: serverList, debug_mode: debugMode } = globalConfig; 32 | 33 | if (!serverList || serverList.length === 0) { 34 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + '无服务器配置,跳过同步'); 35 | return false; 36 | } 37 | 38 | const targetServers = serverList.filter(serverCfg => 39 | serverCfg.group_list?.some(groupId => groupId == e.group_id) 40 | ); 41 | 42 | if (targetServers.length === 0) { 43 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + `群 ${e.group_id} 未关联任何服务器,跳过同步`); 44 | return false; 45 | } 46 | 47 | for (const serverCfg of targetServers) { 48 | const serverName = serverCfg.server_name; 49 | const rconConnection = RconManager.activeConnections?.[serverName]; 50 | const wsConnection = WebSocketManager.activeSockets?.[serverName]; 51 | 52 | if (!rconConnection && !wsConnection) { 53 | if (debugMode) { 54 | logger.mark(LOG_PREFIX_CLIENT + logger.yellow(serverName) + ' 未连接 (RCON 和 WebSocket 均不可用)'); 55 | } 56 | continue; 57 | } 58 | 59 | const [, message] = e.msg.match(this.rule[0].reg); 60 | 61 | if (wsConnection) { 62 | const wsPayload = JSON.stringify({ 63 | api: "send_title", 64 | data: { title: "", subtitle: message }, 65 | echo: String(Date.now()) 66 | }); 67 | try { 68 | wsConnection.send(wsPayload); 69 | if (debugMode) { 70 | logger.mark(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息 (WebSocket): ${message}`); 71 | } 72 | } catch (error) { 73 | if (debugMode) logger.error(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息失败 (WebSocket): ${error.message}`); 74 | if (rconConnection) { 75 | if (debugMode) logger.info(LOG_PREFIX_WS + `WebSocket发送失败,尝试使用RCON发送到 ${serverName}`); 76 | const response = await rconConnection.send(`title @a subtitle {"text":"${message}"}`); 77 | await rconConnection.send(`title @a title {"text":""}`); 78 | if (response === null && debugMode) { 79 | logger.warn(LOG_PREFIX_RCON + `title 命令发送到 ${logger.green(serverName)} 失败或无响应`); 80 | } 81 | } 82 | } 83 | } else if (rconConnection) { 84 | const response = await rconConnection.send(`title @a subtitle {"text":"${message}"}`); 85 | await rconConnection.send(`title @a title {"text":""}`); 86 | if (response === null && debugMode) { 87 | logger.warn(LOG_PREFIX_RCON + `title 命令发送到 ${logger.green(serverName)} 失败或无响应`); 88 | } 89 | } else { 90 | if (debugMode) logger.warn(LOG_PREFIX_CLIENT + `${serverName} 无可用连接方式 (WebSocket/RCON) 来同步聊天消息`); 91 | } 92 | } 93 | return true; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /apps/Title.js: -------------------------------------------------------------------------------- 1 | import plugin from "../../../lib/plugins/plugin.js"; 2 | import RconManager from "../components/Rcon.js"; 3 | import WebSocketManager from "../components/WebSocket.js"; 4 | import Config from "../components/Config.js"; 5 | 6 | const LOG_PREFIX_CLIENT = logger.blue('[Minecraft Client] '); 7 | const LOG_PREFIX_RCON = logger.blue('[Minecraft RCON] '); 8 | const LOG_PREFIX_WS = logger.blue('[Minecraft WebSocket] '); 9 | 10 | export class Title extends plugin { 11 | constructor() { 12 | super({ 13 | name: "MCQQ-发送标题", 14 | event: "message", 15 | priority: 1008, 16 | rule: [ 17 | { 18 | reg: "#?mct (.*)", 19 | fnc: "title", 20 | }, 21 | ], 22 | }); 23 | } 24 | 25 | async title(e) { 26 | if (!e.isGroup) { 27 | return false; 28 | } 29 | 30 | const globalConfig = await Config.getConfig(); 31 | const { mc_qq_server_list: serverList, debug_mode: debugMode } = globalConfig; 32 | 33 | if (!serverList || serverList.length === 0) { 34 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + '无服务器配置,跳过同步'); 35 | return false; 36 | } 37 | 38 | const targetServers = serverList.filter(serverCfg => 39 | serverCfg.group_list?.some(groupId => groupId == e.group_id) 40 | ); 41 | 42 | if (targetServers.length === 0) { 43 | if (debugMode) logger.info(LOG_PREFIX_CLIENT + `群 ${e.group_id} 未关联任何服务器,跳过同步`); 44 | return false; 45 | } 46 | 47 | for (const serverCfg of targetServers) { 48 | const serverName = serverCfg.server_name; 49 | const rconConnection = RconManager.activeConnections?.[serverName]; 50 | const wsConnection = WebSocketManager.activeSockets?.[serverName]; 51 | 52 | if (!rconConnection && !wsConnection) { 53 | if (debugMode) { 54 | logger.mark(LOG_PREFIX_CLIENT + logger.yellow(serverName) + ' 未连接 (RCON 和 WebSocket 均不可用)'); 55 | } 56 | continue; 57 | } 58 | 59 | const [, message] = e.msg.match(this.rule[0].reg); 60 | 61 | if (wsConnection) { 62 | const wsPayload = JSON.stringify({ 63 | api: "send_title", 64 | data: { title: message }, 65 | echo: String(Date.now()) 66 | }); 67 | try { 68 | wsConnection.send(wsPayload); 69 | if (debugMode) { 70 | logger.mark(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息 (WebSocket): ${message}`); 71 | } 72 | } catch (error) { 73 | if (debugMode) logger.error(LOG_PREFIX_WS + `向 ${logger.green(serverName)} 发送消息失败 (WebSocket): ${error.message}`); 74 | if (rconConnection) { 75 | if (debugMode) logger.info(LOG_PREFIX_WS + `WebSocket发送失败,尝试使用RCON发送到 ${serverName}`); 76 | const response = await rconConnection.send(`title @a title {"text":"${message}"}`); 77 | if (response === null && debugMode) { 78 | logger.warn(LOG_PREFIX_RCON + `title 命令发送到 ${logger.green(serverName)} 失败或无响应`); 79 | } 80 | } 81 | } 82 | } else if (rconConnection) { 83 | const response = await rconConnection.send(`title @a title {"text":"${message}"}`); 84 | if (response === null && debugMode) { 85 | logger.warn(LOG_PREFIX_RCON + `title 命令发送到 ${logger.green(serverName)} 失败或无响应`); 86 | } 87 | } else { 88 | if (debugMode) logger.warn(LOG_PREFIX_CLIENT + `${serverName} 无可用连接方式 (WebSocket/RCON) 来同步聊天消息`); 89 | } 90 | } 91 | return true; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /apps/Update.js: -------------------------------------------------------------------------------- 1 | import plugin from '../../../lib/plugins/plugin.js' 2 | import { createRequire } from 'module' 3 | import lodash from 'lodash' 4 | import { Restart } from '../../other/restart.js' 5 | 6 | const require = createRequire(import.meta.url) 7 | const { exec, execSync } = require('child_process') 8 | 9 | // 是否在更新中 10 | let uping = false 11 | 12 | /** 13 | * 处理插件更新 14 | */ 15 | export class Update extends plugin { 16 | constructor () { 17 | super({ 18 | name: 'MCQQ-更新插件', 19 | event: 'message', 20 | priority: 1009, 21 | rule: [ 22 | { 23 | reg: '^#mc((插件)?(强制)?更新| update)$', 24 | fnc: 'update' 25 | } 26 | ] 27 | }) 28 | } 29 | 30 | /** 31 | * rule - 更新插件 32 | * @returns 33 | */ 34 | async update () { 35 | if (!this.e.isMaster) return false 36 | 37 | /** 检查是否正在更新中 */ 38 | if (uping) { 39 | await this.reply('已有命令更新中..请勿重复操作') 40 | return 41 | } 42 | 43 | /** 检查git安装 */ 44 | if (!(await this.checkGit())) return 45 | 46 | const isForce = this.e.msg.includes('强制') 47 | 48 | /** 执行更新 */ 49 | await this.runUpdate(isForce) 50 | 51 | /** 是否需要重启 */ 52 | if (this.isUp) { 53 | await this.reply('更新完毕,正在重启云崽以应用更新') 54 | setTimeout(() => this.restart(), 2000) 55 | } 56 | } 57 | 58 | restart () { 59 | new Restart(this.e).restart() 60 | } 61 | 62 | /** 63 | * 更新 64 | * @param {boolean} isForce 是否为强制更新 65 | * @returns 66 | */ 67 | async runUpdate (isForce) { 68 | let command = 'git -C ./plugins/mc-plugin/ pull --no-rebase' 69 | if (isForce) { 70 | command = `git -C ./plugins/mc-plugin/ checkout . && ${command}` 71 | this.e.reply('正在执行强制更新操作,请稍等') 72 | } else { 73 | this.e.reply('正在执行更新操作,请稍等') 74 | } 75 | /** 获取上次提交的commitId,用于获取日志时判断新增的更新日志 */ 76 | this.oldCommitId = await this.getcommitId('mc-plugin') 77 | uping = true 78 | const ret = await this.execSync(command) 79 | uping = false 80 | 81 | if (ret.error) { 82 | logger.mark(`${this.e.logFnc} 更新失败:mc-plugin`) 83 | this.gitErr(ret.error, ret.stdout) 84 | return false 85 | } 86 | 87 | /** 获取插件提交的最新时间 */ 88 | const time = await this.getTime('mc-plugin') 89 | 90 | if (/(Already up[ -]to[ -]date|已经是最新的)/.test(ret.stdout)) { 91 | await this.reply(`mc-plugin已经是最新版本\n最后更新时间:${time}`) 92 | } else { 93 | await this.reply(`mc-plugin\n最后更新时间:${time}`) 94 | this.isUp = true 95 | /** 获取mc-plugin的更新日志 */ 96 | const log = await this.getLog('mc-plugin') 97 | await this.reply(log) 98 | } 99 | 100 | logger.mark(`${this.e.logFnc} 最后更新时间:${time}`) 101 | 102 | return true 103 | } 104 | 105 | /** 106 | * 获取mc-plugin的更新日志 107 | * @param {string} plugin 插件名称 108 | * @returns 109 | */ 110 | async getLog (plugin = '') { 111 | const cm = `cd ./plugins/${plugin}/ && git log -20 --oneline --pretty=format:"%h||[%cd] %s" --date=format:"%m-%d %H:%M"` 112 | 113 | let logAll 114 | try { 115 | logAll = await execSync(cm, { encoding: 'utf-8' }) 116 | } catch (error) { 117 | logger.error(error.toString()) 118 | this.reply(error.toString()) 119 | } 120 | 121 | if (!logAll) return false 122 | 123 | logAll = logAll.split('\n') 124 | 125 | let log = [] 126 | for (let str of logAll) { 127 | str = str.split('||') 128 | if (str[0] == this.oldCommitId) break 129 | if (str[1].includes('Merge branch')) continue 130 | log.push(str[1]) 131 | } 132 | const line = log.length 133 | log = log.join('\n\n') 134 | 135 | if (log.length <= 0) return '' 136 | 137 | let end = '' 138 | end = 139 | '更多详细信息,请前往github查看\nhttps://github.com/CikeyQi/mc-plugin/commits/main' 140 | 141 | log = await this.makeForwardMsg(`mc-plugin更新日志,共${line}条`, log, end) 142 | 143 | return log 144 | } 145 | 146 | /** 147 | * 获取上次提交的commitId 148 | * @param {string} plugin 插件名称 149 | * @returns 150 | */ 151 | async getcommitId (plugin = '') { 152 | const cm = `git -C ./plugins/${plugin}/ rev-parse --short HEAD` 153 | 154 | let commitId = await execSync(cm, { encoding: 'utf-8' }) 155 | commitId = lodash.trim(commitId) 156 | 157 | return commitId 158 | } 159 | 160 | /** 161 | * 获取本次更新插件的最后一次提交时间 162 | * @param {string} plugin 插件名称 163 | * @returns 164 | */ 165 | async getTime (plugin = '') { 166 | const cm = `cd ./plugins/${plugin}/ && git log -1 --oneline --pretty=format:"%cd" --date=format:"%m-%d %H:%M"` 167 | 168 | let time = '' 169 | try { 170 | time = await execSync(cm, { encoding: 'utf-8' }) 171 | time = lodash.trim(time) 172 | } catch (error) { 173 | logger.error(error.toString()) 174 | time = '获取时间失败' 175 | } 176 | return time 177 | } 178 | 179 | /** 180 | * 制作转发消息 181 | * @param {string} title 标题 - 首条消息 182 | * @param {string} msg 日志信息 183 | * @param {string} end 最后一条信息 184 | * @returns 185 | */ 186 | async makeForwardMsg (title, msg, end) { 187 | let nickname = (this.e.bot ?? Bot).nickname 188 | if (this.e.isGroup) { 189 | let info = await (this.e.bot ?? Bot).getGroupMemberInfo(this.e.group_id, (this.e.bot ?? Bot).uin) 190 | nickname = info.card || info.nickname 191 | } 192 | let userInfo = { 193 | user_id: (this.e.bot ?? Bot).uin, 194 | nickname 195 | } 196 | 197 | let forwardMsg = [ 198 | { 199 | ...userInfo, 200 | message: title 201 | }, 202 | { 203 | ...userInfo, 204 | message: msg 205 | } 206 | ] 207 | 208 | if (end) { 209 | forwardMsg.push({ 210 | ...userInfo, 211 | message: end 212 | }) 213 | } 214 | 215 | /** 制作转发内容 */ 216 | if (this.e.group?.makeForwardMsg) { 217 | forwardMsg = await this.e.group.makeForwardMsg(forwardMsg) 218 | } else if (this.e?.friend?.makeForwardMsg) { 219 | forwardMsg = await this.e.friend.makeForwardMsg(forwardMsg) 220 | } else { 221 | return msg.join('\n') 222 | } 223 | 224 | let dec = 'mc-plugin 更新日志' 225 | /** 处理描述 */ 226 | if (typeof (forwardMsg.data) === 'object') { 227 | let detail = forwardMsg.data?.meta?.detail 228 | if (detail) { 229 | detail.news = [{ text: dec }] 230 | } 231 | } else { 232 | forwardMsg.data = forwardMsg.data 233 | .replace(/\n/g, '') 234 | .replace(/(.+?)<\/title>/g, '___') 235 | .replace(/___+/, `${dec}`) 236 | } 237 | 238 | return forwardMsg 239 | } 240 | 241 | /** 242 | * 处理更新失败的相关函数 243 | * @param {string} err 244 | * @param {string} stdout 245 | * @returns 246 | */ 247 | async gitErr (err, stdout) { 248 | const msg = '更新失败!' 249 | const errMsg = err.toString() 250 | stdout = stdout.toString() 251 | 252 | if (errMsg.includes('Timed out')) { 253 | const remote = errMsg.match(/'(.+?)'/g)[0].replace(/'/g, '') 254 | await this.reply(msg + `\n连接超时:${remote}`) 255 | return 256 | } 257 | 258 | if (/Failed to connect|unable to access/g.test(errMsg)) { 259 | const remote = errMsg.match(/'(.+?)'/g)[0].replace(/'/g, '') 260 | await this.reply(msg + `\n连接失败:${remote}`) 261 | return 262 | } 263 | 264 | if (errMsg.includes('be overwritten by merge')) { 265 | await this.reply( 266 | msg + 267 | `存在冲突:\n${errMsg}\n` + 268 | '请解决冲突后再更新,或者执行#强制更新,放弃本地修改' 269 | ) 270 | return 271 | } 272 | 273 | if (stdout.includes('CONFLICT')) { 274 | await this.reply([ 275 | msg + '存在冲突\n', 276 | errMsg, 277 | stdout, 278 | '\n请解决冲突后再更新,或者执行#强制更新,放弃本地修改' 279 | ]) 280 | return 281 | } 282 | 283 | await this.reply([errMsg, stdout]) 284 | } 285 | 286 | /** 287 | * 异步执行git相关命令 288 | * @param {string} cmd git命令 289 | * @returns 290 | */ 291 | async execSync (cmd) { 292 | return new Promise((resolve, reject) => { 293 | exec(cmd, { windowsHide: true }, (error, stdout, stderr) => { 294 | resolve({ error, stdout, stderr }) 295 | }) 296 | }) 297 | } 298 | 299 | /** 300 | * 检查git是否安装 301 | * @returns 302 | */ 303 | async checkGit () { 304 | const ret = await execSync('git --version', { encoding: 'utf-8' }) 305 | if (!ret || !ret.includes('git version')) { 306 | await this.reply('请先安装git') 307 | return false 308 | } 309 | return true 310 | } 311 | } -------------------------------------------------------------------------------- /components/Config.js: -------------------------------------------------------------------------------- 1 | import YAML from 'yaml' 2 | import fs from 'fs' 3 | import { pluginRoot } from '../model/path.js' 4 | 5 | const LOG_PREFIX_CONFIG = logger.blue('[Minecraft Config] '); 6 | 7 | class Config { 8 | 9 | getConfig() { 10 | try { 11 | const config_data = YAML.parse( 12 | fs.readFileSync(`${pluginRoot}/config/config/config.yaml`, 'utf-8') 13 | ) 14 | return config_data 15 | } catch (error) { 16 | logger.mark(LOG_PREFIX_CONFIG + ' 读取 config.yaml 失败' + error.message) 17 | return false 18 | } 19 | } 20 | 21 | getDefConfig() { 22 | try { 23 | const config_default_data = YAML.parse( 24 | fs.readFileSync(`${pluginRoot}/config/config_default.yaml`, 'utf-8') 25 | ) 26 | return config_default_data 27 | } catch (error) { 28 | logger.mark(LOG_PREFIX_CONFIG + ' 读取 config_default.yaml 失败' + error.message) 29 | return false 30 | } 31 | } 32 | 33 | setConfig(config_data) { 34 | try { 35 | fs.writeFileSync( 36 | `${pluginRoot}/config/config/config.yaml`, 37 | YAML.stringify(config_data), 38 | ) 39 | return true 40 | } catch (error) { 41 | logger.mark(LOG_PREFIX_CONFIG + ' 写入 config.yaml 失败' + error.message) 42 | return false 43 | } 44 | } 45 | } 46 | 47 | export default new Config() 48 | -------------------------------------------------------------------------------- /components/Rcon.js: -------------------------------------------------------------------------------- 1 | import { Rcon } from 'rcon-client'; 2 | import Config from './Config.js'; 3 | 4 | const RCON_LOG_PREFIX = logger.blue('[Minecraft RCON] '); 5 | 6 | class RconManager { 7 | 8 | constructor() { 9 | this.activeConnections = {}; 10 | this._initializeConnectionsAsync(); 11 | } 12 | 13 | async _initializeConnectionsAsync() { 14 | try { 15 | const config = await Config.getConfig(); 16 | if (!config) { 17 | logger.error(RCON_LOG_PREFIX + '无法获取配置,RCON服务无法启动连接'); 18 | return; 19 | } 20 | 21 | const { mc_qq_server_list: rconServerList } = config; 22 | if (!rconServerList || !Array.isArray(rconServerList)) { 23 | logger.info(RCON_LOG_PREFIX + '未配置RCON服务器列表或格式不正确'); 24 | return; 25 | } 26 | 27 | rconServerList.forEach(serverCfg => { 28 | if (serverCfg.rcon_able && serverCfg.rcon_host && serverCfg.rcon_port && serverCfg.rcon_password && serverCfg.server_name) { 29 | if (this.activeConnections[serverCfg.server_name]) { 30 | logger.info(RCON_LOG_PREFIX + `已存在到 ${serverCfg.server_name} 的RCON连接,跳过`); 31 | } else { 32 | this._establishConnection(serverCfg); 33 | } 34 | } else if (serverCfg.rcon_able) { 35 | logger.warn(RCON_LOG_PREFIX + `RCON服务器配置 ${serverCfg.server_name || '未命名'} 不完整,跳过`); 36 | } 37 | }); 38 | 39 | } catch (error) { 40 | logger.error(RCON_LOG_PREFIX + `初始化RCON连接失败: ${error.message}`); 41 | } 42 | } 43 | 44 | async _establishConnection(serverCfg, retries = 0) { 45 | const { server_name: serverName, rcon_host: host, rcon_port: port, rcon_password: password, rcon_max_attempts: maxRetries = 3 } = serverCfg; 46 | 47 | logger.info(RCON_LOG_PREFIX + `尝试连接到 ${serverName} (${host}:${port})... (尝试次数: ${retries + 1})`); 48 | 49 | const rcon = new Rcon({ 50 | host: host, 51 | port: port, 52 | password: password, 53 | timeout: 10000 54 | }); 55 | 56 | try { 57 | await rcon.connect(); 58 | logger.mark(RCON_LOG_PREFIX + logger.green(serverName) + ' RCON连接成功'); 59 | this.activeConnections[serverName] = rcon; 60 | 61 | rcon.on('end', () => { 62 | logger.mark( 63 | RCON_LOG_PREFIX + 64 | logger.yellow(serverName) + 65 | ' RCON连接已断开' 66 | ); 67 | if (this.activeConnections[serverName] === rcon) { 68 | delete this.activeConnections[serverName]; 69 | } 70 | 71 | logger.info(RCON_LOG_PREFIX + `${serverName} 将尝试重新连接...`); 72 | 73 | if (serverCfg.rcon_able) { 74 | setTimeout(() => this._establishConnection(serverCfg, 0), 5000); 75 | } 76 | 77 | }); 78 | 79 | rcon.on('error', (err) => { 80 | logger.error( 81 | RCON_LOG_PREFIX + 82 | logger.red(serverName) + 83 | ` RCON连接发生错误: ${err.message}` 84 | ); 85 | 86 | if (this.activeConnections[serverName] === rcon) { 87 | delete this.activeConnections[serverName]; 88 | } 89 | rcon.end(); 90 | }); 91 | 92 | } catch (error) { 93 | logger.mark( 94 | RCON_LOG_PREFIX + 95 | logger.red(serverName) + 96 | ` RCON连接失败: ${error.message}` 97 | ); 98 | if (this.activeConnections[serverName] === rcon) { 99 | delete this.activeConnections[serverName]; 100 | } 101 | 102 | if (retries < maxRetries) { 103 | logger.info(RCON_LOG_PREFIX + `${serverName} 将在5秒后尝试重新连接... (剩余尝试: ${maxRetries - retries})`); 104 | setTimeout(() => this._establishConnection(serverCfg, retries + 1), 5000); 105 | } else { 106 | logger.error(RCON_LOG_PREFIX + logger.red(serverName) + ` RCON连接失败,已达到最大重连次数 (${maxRetries + 1}),请检查RCON服务是否正常运行`); 107 | } 108 | } 109 | } 110 | } 111 | 112 | export default new RconManager(); -------------------------------------------------------------------------------- /components/Render.js: -------------------------------------------------------------------------------- 1 | import Version from './Version.js' 2 | import { pluginRoot } from '../model/path.js' 3 | import fs from 'fs' 4 | 5 | function scale(pct = 1) { 6 | let scale = 100 7 | scale = Math.min(2, Math.max(0.5, scale / 100)) 8 | pct = pct * scale 9 | return `style=transform:scale(${pct})` 10 | } 11 | 12 | const Render = { 13 | async render(path, params, cfg) { 14 | let { e } = cfg 15 | if (!e.runtime) { 16 | console.log('未找到e.runtime,请升级至最新版Yunzai') 17 | } 18 | 19 | let BotName = Version.isMiao ? 'Miao-Yunzai' : 'Yunzai-Bot' 20 | let currentVersion = null 21 | const package_path = `${pluginRoot}/package.json` 22 | try { 23 | const package_json = JSON.parse(fs.readFileSync(package_path, 'utf-8')) 24 | if (package_json.version) { 25 | currentVersion = package_json.version 26 | } 27 | } catch (err) { 28 | console.log('读取package.json失败', err) 29 | } 30 | return e.runtime.render('mc-plugin', path, params, { 31 | retType: cfg.retMsgId ? 'msgId' : 'default', 32 | beforeRender({ data }) { 33 | let pluginName = '' 34 | if (data.pluginName !== false) { 35 | pluginName = ` & ${data.pluginName || 'mc-plugin'}` 36 | if (data.pluginVersion !== false) { 37 | pluginName += `${currentVersion}` 38 | } 39 | } 40 | let resPath = data.pluResPath 41 | const layoutPath = process.cwd() + '/plugins/mc-plugin/resources/common/layout/' 42 | return { 43 | ...data, 44 | _res_path: resPath, 45 | _mc_path: resPath, 46 | _layout_path: layoutPath, 47 | defaultLayout: layoutPath + 'default.html', 48 | elemLayout: layoutPath + 'elem.html', 49 | sys: { 50 | scale: scale(cfg.scale || 1) 51 | }, 52 | copyright: `Created By ${BotName}${Version.yunzai}${pluginName}`, 53 | pageGotoParams: { 54 | waitUntil: 'networkidle2' 55 | } 56 | } 57 | } 58 | }) 59 | } 60 | } 61 | 62 | export default Render -------------------------------------------------------------------------------- /components/SendMsg.js: -------------------------------------------------------------------------------- 1 | import Config from "./Config.js"; 2 | 3 | const formatMsg = (jsonData, showServer) => { 4 | let message = showServer ? `[${jsonData.server_name}] ` : ""; 5 | const nickname = jsonData.player?.nickname; 6 | 7 | const messageHandlers = { 8 | 'join': () => `${nickname} 加入了游戏`, 9 | 'quit': () => `${nickname} 退出了游戏`, 10 | 'death': () => jsonData.message, 11 | 'player_command': () => `${nickname} 使用命令 ${jsonData.message}`, 12 | 'achievement': () => { 13 | if (!jsonData.advancement?.display?.title) return null; 14 | return `${nickname} 达成了进度 ${jsonData.advancement.display.title}`; 15 | }, 16 | 'chat': () => { 17 | const { mc_qq_say_way: sayConnector } = Config.getConfig(); 18 | return `${nickname} ${sayConnector} ${jsonData.message}`; 19 | } 20 | }; 21 | 22 | const handler = messageHandlers[jsonData.sub_type]; 23 | 24 | if (handler) { 25 | const subMessage = handler(); 26 | return subMessage === null ? null : message + subMessage; 27 | } else { 28 | logger.info(`服务器 ${jsonData.server_name} 的未知的上报类型 ${jsonData.sub_type}`); 29 | return null; 30 | } 31 | }; 32 | 33 | export default function processAndSend(rawMessage) { 34 | const { mc_qq_display_server_name: showServer, mc_qq_server_list: serverList } = Config.getConfig(); 35 | let jsonData; 36 | 37 | try { 38 | jsonData = JSON.parse(rawMessage); 39 | } catch (error) { 40 | logger.error(`解析消息失败: ${error.message}. 原始消息: ${rawMessage}`); 41 | return; 42 | } 43 | 44 | let messageContent = formatMsg(jsonData, showServer); 45 | 46 | if (!messageContent) return; 47 | 48 | const serverCfg = serverList.find(s => s.server_name === jsonData.server_name); 49 | 50 | if (serverCfg) { 51 | const { bot_self_id: botIds, group_list: groupIds, mask_word: censorWord } = serverCfg; 52 | 53 | if (censorWord) { 54 | messageContent = messageContent.replace(new RegExp(censorWord, "g"), ''); 55 | } 56 | 57 | if (!botIds || botIds.length === 0) { 58 | logger.error(`服务器 ${jsonData.server_name} 未配置推送机器人ID,忽略消息`); 59 | return; 60 | } 61 | 62 | botIds.forEach(botId => groupIds.forEach(groupId => { 63 | try { 64 | console.log(messageContent) 65 | const [, url] = messageContent.match(/\[\[CICode,.*?url=([^,$$]+).*?\]\]/) || []; 66 | console.log(url) 67 | const msg = url ? [messageContent.replace(/\[\[CICode,.*?\]\]/, '').trim(), segment.image(url)] : messageContent; 68 | Bot[botId].pickGroup(groupId).sendMsg(msg); 69 | logger.info(`机器人 ${botId} 发送到群 ${groupId}${url ? ' (含图片)' : ''}`); 70 | } catch (e) { 71 | logger.error(`机器人 ${botId} 发送到群 ${groupId} 失败: ${e.message}`); 72 | } 73 | })); 74 | } else { 75 | logger.info(`服务器 ${jsonData.server_name} 未在配置文件中找到,忽略消息`); 76 | } 77 | } -------------------------------------------------------------------------------- /components/Version.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | 3 | let packageJson = JSON.parse(fs.readFileSync('package.json', 'utf8')) 4 | 5 | const getLine = function (line) { 6 | line = line.replace(/(^\s*\*|\r)/g, '') 7 | line = line.replace(/\s*`([^`]+`)/g, '$1') 8 | line = line.replace(/`\s*/g, '') 9 | line = line.replace(/\s*\*\*([^\*]+\*\*)/g, '$1') 10 | line = line.replace(/\*\*\s*/g, '') 11 | line = line.replace(/ⁿᵉʷ/g, '') 12 | return line 13 | } 14 | 15 | const yunzaiVersion = packageJson.version 16 | const isMiao = packageJson.name === 'miao-yunzai' 17 | const isTrss = Array.isArray(Bot.uin) ? true : false 18 | 19 | let Version = { 20 | isMiao, 21 | isTrss, 22 | get version() { 23 | return currentVersion 24 | }, 25 | get yunzai() { 26 | return yunzaiVersion 27 | } 28 | } 29 | 30 | export default Version 31 | -------------------------------------------------------------------------------- /components/WebSocket.js: -------------------------------------------------------------------------------- 1 | import { WebSocketServer, WebSocket } from 'ws'; 2 | import Config from './Config.js'; 3 | import processAndSend from './SendMsg.js'; 4 | 5 | const WS_LOG_PREFIX = logger.blue('[Minecraft WebSocket] '); 6 | 7 | class WebSocketManager { 8 | 9 | constructor() { 10 | this.activeSockets = {}; 11 | this._initializeAsync(); 12 | } 13 | 14 | async _initializeAsync() { 15 | try { 16 | const config = await Config.getConfig(); 17 | if (!config) { 18 | logger.error(WS_LOG_PREFIX + '无法获取配置,WebSocket服务无法启动'); 19 | return; 20 | } 21 | 22 | if (config.mc_qq_ws_server) { 23 | this._startLocalServer(config); 24 | } 25 | 26 | this._connectToRemoteServers(config); 27 | 28 | } catch (error) { 29 | logger.error(WS_LOG_PREFIX + `初始化失败: ${error.message}`); 30 | } 31 | } 32 | 33 | _startLocalServer(config) { 34 | const { mc_qq_ws_port: wsPort, mc_qq_ws_url: wsPath, mc_qq_ws_password: wsPassword, debug_mode: debugMode } = config; 35 | 36 | if (!wsPort || !wsPath) { 37 | logger.error(WS_LOG_PREFIX + 'WebSocket服务器端口或路径未配置,无法启动'); 38 | return; 39 | } 40 | 41 | const wss = new WebSocketServer({ port: wsPort, path: wsPath }); 42 | 43 | wss.on('listening', () => { 44 | logger.mark( 45 | WS_LOG_PREFIX + 46 | '监听地址:' + 47 | logger.green(`ws://localhost:${wsPort}${wsPath}`) 48 | ); 49 | }); 50 | 51 | wss.on('connection', (ws, request) => { 52 | let remoteName 53 | try { 54 | remoteName = decodeURIComponent(request.headers['x-self-name']); 55 | let authToken = decodeURIComponent(request.headers['authorization']); 56 | authToken = authToken.replace(/^Bearer\s*/i, ''); 57 | 58 | if (!remoteName) { 59 | ws.close(1008, 'Invalid remote name'); 60 | return; 61 | } 62 | 63 | if (wsPassword && authToken !== wsPassword) { 64 | ws.close(1008, 'Invalid token'); 65 | logger.mark( 66 | WS_LOG_PREFIX + 67 | logger.yellow(remoteName) + 68 | ' 尝试连接,但令牌无效,已拒绝' 69 | ); 70 | return; 71 | } 72 | 73 | if (this.activeSockets[remoteName]) { 74 | ws.close(1000, 'Duplicate connection'); 75 | logger.mark( 76 | WS_LOG_PREFIX + 77 | logger.yellow(remoteName) + 78 | ' 尝试连接,但已存在同名连接,已拒绝' 79 | ); 80 | return; 81 | } 82 | 83 | logger.mark(WS_LOG_PREFIX + logger.green(remoteName) + ' 已连接'); 84 | this.activeSockets[remoteName] = ws; 85 | 86 | ws.on('message', (message) => { 87 | if (debugMode) { 88 | logger.mark( 89 | WS_LOG_PREFIX + 90 | logger.green(remoteName) + 91 | ' 收到消息:' + message 92 | ); 93 | } 94 | processAndSend(message.toString()); 95 | }); 96 | 97 | ws.on('close', (code, reason) => { 98 | logger.mark( 99 | WS_LOG_PREFIX + 100 | logger.yellow(remoteName) + 101 | ` 已断开 Code: ${code}, Reason: ${reason || 'N/A'}` 102 | ); 103 | delete this.activeSockets[remoteName]; 104 | }); 105 | 106 | ws.on('error', (error) => { 107 | logger.error( 108 | WS_LOG_PREFIX + 109 | logger.red(remoteName) + 110 | ` 连接出错 ${error.message}` 111 | ); 112 | delete this.activeSockets[remoteName]; 113 | }); 114 | 115 | } catch (error) { 116 | logger.error(WS_LOG_PREFIX + `处理来自 ${remoteName ? logger.red(remoteName) : '未知服务器'} 的新连接时出错: ${error.message}`); 117 | if (ws.readyState === WebSocket.OPEN) { 118 | ws.close(1011, 'Internal server error'); 119 | } 120 | } 121 | }); 122 | 123 | wss.on('error', (error) => { 124 | logger.error(WS_LOG_PREFIX + `本地服务器错误: ${error.message}`); 125 | }); 126 | } 127 | 128 | _connectToRemoteServers(config) { 129 | const { mc_qq_server_list: remoteServers, debug_mode: debugMode } = config; 130 | 131 | if (!remoteServers || !Array.isArray(remoteServers)) { 132 | logger.info(WS_LOG_PREFIX + '未配置远程服务器列表或格式不正确'); 133 | return; 134 | } 135 | 136 | remoteServers.forEach(serverCfg => { 137 | if (serverCfg.ws_able && serverCfg.ws_url && serverCfg.server_name) { 138 | if (this.activeSockets[serverCfg.server_name]) { 139 | logger.info(WS_LOG_PREFIX + `已存在到 ${serverCfg.server_name} 的连接,跳过`); 140 | } else { 141 | this._establishClientConnection(serverCfg, debugMode); 142 | } 143 | } else if (serverCfg.ws_able) { 144 | logger.warn(WS_LOG_PREFIX + `远程服务器配置 ${serverCfg.server_name || '未命名'} 不完整,跳过`); 145 | } 146 | }); 147 | } 148 | 149 | async _establishClientConnection(serverCfg, globalDebug, retries = 0) { 150 | const { server_name: serverName, ws_url: serverUrl, ws_password: serverToken, ws_max_attempts: maxRetries = 3 } = serverCfg; 151 | 152 | logger.info(WS_LOG_PREFIX + `尝试连接到 ${serverName} (${serverUrl})... (尝试次数: ${retries + 1})`); 153 | 154 | const headers = { 155 | 'X-Self-Name': encodeURIComponent(serverName), 156 | 'Authorization': serverToken ? `Bearer ${encodeURIComponent(serverToken)}` : undefined 157 | }; 158 | Object.keys(headers).forEach(key => headers[key] === undefined && delete headers[key]); 159 | 160 | const ws = new WebSocket(serverUrl, { headers }); 161 | 162 | ws.on('open', () => { 163 | logger.mark(WS_LOG_PREFIX + logger.green(serverName) + ' 已连接'); 164 | this.activeSockets[serverName] = ws; 165 | }); 166 | 167 | ws.on('message', (message) => { 168 | if (globalDebug) { 169 | logger.mark( 170 | WS_LOG_PREFIX + 171 | logger.green(serverName) + 172 | ' 收到消息:' + 173 | logger.green(message.toString()) 174 | ); 175 | } 176 | processAndSend(message.toString()); 177 | }); 178 | 179 | ws.on('close', (code, reason) => { 180 | logger.mark( 181 | WS_LOG_PREFIX + 182 | logger.yellow(serverName) + 183 | ` 连接已断开 Code: ${code}, Reason: ${reason || 'N/A'}` 184 | ); 185 | delete this.activeSockets[serverName]; 186 | 187 | if (retries < maxRetries) { 188 | logger.info(WS_LOG_PREFIX + `${serverName} 将在5秒后尝试重新连接... (剩余尝试: ${maxRetries - retries})`); 189 | setTimeout(() => { 190 | this._establishClientConnection(serverCfg, globalDebug, retries + 1); 191 | }, 5000); 192 | } else { 193 | logger.error(WS_LOG_PREFIX + logger.red(serverName) + ` 已达到最大重连次数 (${maxRetries + 1}),放弃连接`); 194 | } 195 | }); 196 | 197 | ws.on('error', (error) => { 198 | logger.error( 199 | WS_LOG_PREFIX + 200 | logger.red(serverName) + 201 | ` 连接错误 ${error.message}` 202 | ); 203 | if (ws.readyState !== WebSocket.CLOSED && ws.readyState !== WebSocket.CLOSING) { 204 | if (this.activeSockets[serverName]) { 205 | delete this.activeSockets[serverName]; 206 | if (retries < maxRetries) { 207 | logger.info(WS_LOG_PREFIX + `${serverName} 因错误将尝试重新连接...`); 208 | setTimeout(() => { 209 | this._establishClientConnection(serverCfg, globalDebug, retries + 1); 210 | }, 5000); 211 | } else { 212 | logger.error(WS_LOG_PREFIX + logger.red(serverName) + ` 因错误达到最大重连次数,放弃连接`); 213 | } 214 | } 215 | } 216 | }); 217 | } 218 | } 219 | 220 | export default new WebSocketManager(); -------------------------------------------------------------------------------- /config/config_default.yaml: -------------------------------------------------------------------------------- 1 | mc_qq_ws_server: true 2 | mc_qq_ws_url: /minecraft/ws 3 | mc_qq_ws_port: 8080 4 | mc_qq_ws_password: "" 5 | mc_qq_send_group_name: true 6 | mc_qq_display_server_name: true 7 | mc_qq_say_way: 说: 8 | mc_qq_chat_image_enable: false 9 | mc_qq_server_list: [] 10 | debug_mode: true 11 | -------------------------------------------------------------------------------- /guoba.support.js: -------------------------------------------------------------------------------- 1 | import Config from "./components/Config.js"; 2 | import lodash from "lodash"; 3 | import path from "path"; 4 | import { pluginRoot } from "./model/path.js"; 5 | 6 | export function supportGuoba() { 7 | return { 8 | pluginInfo: { 9 | name: 'mc-plugin', 10 | title: '我的世界插件', 11 | author: ['@CikeyQi', '@erzaozi'], 12 | authorLink: ['https://github.com/erzaozi', 'https://github.com/CikeyQi'], 13 | link: 'https://github.com/CikeyQi/mc-plugin', 14 | isV3: true, 15 | isV2: false, 16 | showInMenu: true, 17 | description: '基于 Yunzai 的 Minecraft 消息互通插件', 18 | // 显示图标,此为个性化配置 19 | // 图标可在 https://icon-sets.iconify.design 这里进行搜索 20 | icon: 'noto:video-game', 21 | // 图标颜色,例:#FF0000 或 rgb(255, 0, 0) 22 | iconColor: '#1bb61e', 23 | // 如果想要显示成图片,也可以填写图标路径(绝对路径) 24 | iconPath: path.join(pluginRoot, 'resources/readme/girl.png'), 25 | }, 26 | configInfo: { 27 | schemas: [ 28 | { 29 | component: "Divider", 30 | label: "反向 WebSocket 相关配置", 31 | componentProps: { 32 | orientation: "left", 33 | plain: true, 34 | }, 35 | }, 36 | { 37 | field: "mc_qq_ws_server", 38 | label: "WebSocket服务", 39 | bottomHelpMessage: "启用反向 WebSocket 服务", 40 | component: "Switch", 41 | }, 42 | { 43 | field: "mc_qq_ws_url", 44 | label: "WebSocket路由", 45 | bottomHelpMessage: "*非必要请不要修改此项", 46 | component: "Input", 47 | componentProps: { 48 | placeholder: '例:/minecraft/ws', 49 | }, 50 | }, 51 | { 52 | field: "mc_qq_ws_port", 53 | label: "WebSocket端口", 54 | bottomHelpMessage: "反向 WebSocket 服务监听端口", 55 | component: "InputNumber", 56 | componentProps: { 57 | placeholder: '例:8080', 58 | min: 1, 59 | max: 65535, 60 | step: 1, 61 | }, 62 | }, 63 | { 64 | field: "mc_qq_ws_password", 65 | label: "WebSocket密钥", 66 | bottomHelpMessage: "反向 WebSocket 服务 Access Token", 67 | component: "InputPassword", 68 | componentProps: { 69 | placeholder: '请输入密钥', 70 | visible: false, 71 | }, 72 | }, 73 | { 74 | component: "Divider", 75 | label: "信息格式 相关配置", 76 | componentProps: { 77 | orientation: "left", 78 | plain: true, 79 | }, 80 | }, 81 | { 82 | field: "mc_qq_send_group_name", 83 | label: "发送群组名称", 84 | bottomHelpMessage: "是否向MC发送群组名称", 85 | component: "Switch", 86 | }, 87 | { 88 | field: "mc_qq_display_server_name", 89 | label: "发送服务器名称", 90 | bottomHelpMessage: "是否向QQ发送服务器名称", 91 | component: "Switch", 92 | }, 93 | { 94 | field: "mc_qq_say_way", 95 | label: "修饰用户发言", 96 | bottomHelpMessage: "请输入修饰词", 97 | component: "Input", 98 | componentProps: { 99 | placeholder: '例:说:', 100 | }, 101 | }, 102 | { 103 | field: "mc_qq_chat_image_enable", 104 | label: "发送图片", 105 | bottomHelpMessage: "搭配ChatImage,可以让图片在游戏内显示", 106 | component: "Switch", 107 | }, 108 | { 109 | component: "Divider", 110 | label: "服务器 相关配置", 111 | componentProps: { 112 | orientation: "left", 113 | plain: true, 114 | }, 115 | }, 116 | { 117 | field: "mc_qq_server_list", 118 | label: "服务器群组配置列表", 119 | bottomHelpMessage: "请配置群组列表后再使用插件", 120 | component: "GSubForm", 121 | componentProps: { 122 | multiple: true, 123 | schemas: [ 124 | { 125 | field: "server_name", 126 | label: "服务器名称", 127 | bottomHelpMessage: "请输入服务器名称", 128 | component: "Input", 129 | required: true, 130 | componentProps: { 131 | placeholder: '需要与服务器端配置一致,且不能有重复', 132 | }, 133 | }, 134 | { 135 | component: "Divider", 136 | label: "正向 WebSocket 相关配置", 137 | componentProps: { 138 | orientation: "left", 139 | plain: true, 140 | }, 141 | }, 142 | { 143 | field: "ws_able", 144 | label: "WebSocket服务", 145 | bottomHelpMessage: "启用正向 WebSocket 服务", 146 | component: "Switch", 147 | }, 148 | { 149 | field: "ws_url", 150 | label: "WebSocket地址", 151 | bottomHelpMessage: "正向 WebSocket 连接地址", 152 | component: "Input", 153 | componentProps: { 154 | placeholder: '例:ws://127.0.0.1:8081', 155 | }, 156 | }, 157 | { 158 | field: "ws_password", 159 | label: "WebSocket密钥", 160 | bottomHelpMessage: "正向 WebSocket 服务 Access Token", 161 | component: "Input", 162 | componentProps: { 163 | placeholder: '请输入密钥', 164 | }, 165 | }, 166 | { 167 | field: "ws_max_attempts", 168 | label: "断连重试次数", 169 | bottomHelpMessage: "正向 WebSocket 重连最大尝试次数", 170 | component: "InputNumber", 171 | componentProps: { 172 | placeholder: '例:3', 173 | min: 1, 174 | max: 999999, 175 | step: 1, 176 | }, 177 | }, 178 | { 179 | component: "Divider", 180 | label: "Rcon 相关配置", 181 | componentProps: { 182 | orientation: "left", 183 | plain: true, 184 | }, 185 | }, 186 | { 187 | field: "rcon_able", 188 | label: "是否启用Rcon", 189 | bottomHelpMessage: "若需要向服务器发送指令,请启用Rcon", 190 | component: "Switch", 191 | }, 192 | { 193 | field: "rcon_host", 194 | label: "Rcon地址", 195 | bottomHelpMessage: "请输入Rcon地址", 196 | component: "Input", 197 | componentProps: { 198 | placeholder: '例:127.0.0.1', 199 | }, 200 | }, 201 | { 202 | field: "rcon_port", 203 | label: "Rcon端口", 204 | bottomHelpMessage: "请输入Rcon端口", 205 | component: "InputNumber", 206 | componentProps: { 207 | placeholder: '例:25575', 208 | min: 1, 209 | max: 65535, 210 | step: 1, 211 | }, 212 | }, 213 | { 214 | field: "rcon_password", 215 | label: "Rcon密码", 216 | bottomHelpMessage: "请输入Rcon密码", 217 | component: "InputPassword", 218 | componentProps: { 219 | placeholder: '与server.properties中的rcon.password一致', 220 | visible: false, 221 | }, 222 | }, 223 | { 224 | field: "rcon_max_attempts", 225 | label: "断连重试次数", 226 | bottomHelpMessage: "Rcon 重连最大尝试次数", 227 | component: "InputNumber", 228 | componentProps: { 229 | placeholder: '例:3', 230 | min: 1, 231 | max: 999999, 232 | step: 1, 233 | }, 234 | }, 235 | { 236 | component: "Divider", 237 | label: "群组同步 相关配置", 238 | componentProps: { 239 | orientation: "left", 240 | plain: true, 241 | }, 242 | }, 243 | { 244 | field: "group_list", 245 | label: "开启同步群组列表", 246 | bottomHelpMessage: "此群组将与当前服务器同步消息", 247 | component: "GTags", 248 | required: true, 249 | componentProps: { 250 | placeholder: '请输入群组ID', 251 | allowAdd: true, 252 | allowDel: true, 253 | showPrompt: true, 254 | promptProps: { 255 | content: '请输入群组ID', 256 | placeholder: '例:551081559', 257 | okText: '添加', 258 | rules: [ 259 | { required: true, message: '群组ID不能为空' }, 260 | ], 261 | }, 262 | valueParser: ((value) => value.split(',') || []), 263 | }, 264 | }, 265 | { 266 | field: "bot_self_id", 267 | label: "推送机器人列表", 268 | bottomHelpMessage: "将使用此机器人向群组发送消息", 269 | component: "GTags", 270 | required: true, 271 | componentProps: { 272 | placeholder: '请输入机器人ID', 273 | allowAdd: true, 274 | allowDel: true, 275 | showPrompt: true, 276 | promptProps: { 277 | content: '请输入机器人ID', 278 | placeholder: '例:10001', 279 | okText: '添加', 280 | rules: [ 281 | { required: true, message: '群组ID不能为空' }, 282 | ], 283 | }, 284 | valueParser: ((value) => value.split(',') || []), 285 | }, 286 | }, 287 | { 288 | field: "command_header", 289 | label: "指令前缀", 290 | bottomHelpMessage: "请输入指令前缀", 291 | component: "Input", 292 | required: true, 293 | componentProps: { 294 | placeholder: '例:/', 295 | }, 296 | }, 297 | { 298 | field: "command_user", 299 | label: "可用指令的用户", 300 | bottomHelpMessage: "请输入可用指令的用户ID", 301 | component: "GTags", 302 | required: true, 303 | componentProps: { 304 | placeholder: '请输入可用指令的用户ID', 305 | allowAdd: true, 306 | allowDel: true, 307 | showPrompt: true, 308 | promptProps: { 309 | content: '请输入可用指令的用户ID', 310 | placeholder: '例:10001', 311 | okText: '添加', 312 | rules: [ 313 | { required: true, message: '用户ID不能为空' }, 314 | ], 315 | }, 316 | valueParser: ((value) => value.split(',') || []), 317 | }, 318 | }, 319 | { 320 | field: "mask_word", 321 | label: "屏蔽词正则表达式", 322 | bottomHelpMessage: "请输入屏蔽词正则表达式", 323 | component: "Input", 324 | componentProps: { 325 | placeholder: '例:§.', 326 | }, 327 | }, 328 | ], 329 | }, 330 | }, 331 | { 332 | field: "debug_mode", 333 | label: "调试模式", 334 | bottomHelpMessage: "是否开启调试模式", 335 | component: "Switch", 336 | }, 337 | ], 338 | getConfigData() { 339 | let config = Config.getConfig() 340 | return config 341 | }, 342 | 343 | setConfigData(data, { Result }) { 344 | let config = {} 345 | for (let [keyPath, value] of Object.entries(data)) { 346 | lodash.set(config, keyPath, value) 347 | } 348 | config = lodash.merge({}, Config.getConfig(), config) 349 | config.mc_qq_server_list = data['mc_qq_server_list'] 350 | Config.setConfig(config) 351 | return Result.ok({}, '保存成功~') 352 | }, 353 | }, 354 | } 355 | } -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | import fs from 'node:fs'; 2 | import Init from './model/init.js'; 3 | 4 | if (!global.segment) { 5 | global.segment = (await import("oicq")).segment; 6 | } 7 | 8 | let ret = []; 9 | 10 | logger.info(logger.yellow("- 正在载入 MC-PLUGIN")); 11 | 12 | const files = fs 13 | .readdirSync('./plugins/mc-plugin/apps') 14 | .filter((file) => file.endsWith('.js')); 15 | 16 | files.forEach((file) => { 17 | ret.push(import(`./apps/${file}`)) 18 | }) 19 | 20 | ret = await Promise.allSettled(ret); 21 | 22 | let apps = {}; 23 | for (let i in files) { 24 | let name = files[i].replace('.js', ''); 25 | 26 | if (ret[i].status !== 'fulfilled') { 27 | logger.error(`载入插件错误:${logger.red(name)}`); 28 | logger.error(ret[i].reason); 29 | continue; 30 | } 31 | apps[name] = ret[i].value[Object.keys(ret[i].value)[0]]; 32 | } 33 | 34 | logger.info(logger.green("- MC-PLUGIN 载入成功")); 35 | logger.info(logger.magenta(`- 欢迎加入新组织【貓娘樂園🍥🏳️⚧️】(群号 707331865)`)); 36 | 37 | export { apps }; -------------------------------------------------------------------------------- /model/init.js: -------------------------------------------------------------------------------- 1 | import fs from 'fs' 2 | import path from 'path' 3 | import Config from '../components/Config.js' 4 | import { pluginRoot } from '../model/path.js' 5 | 6 | const LOG_PREFIX_INIT = logger.blue('[Minecraft Initialize] '); 7 | 8 | class ConfigManager { 9 | constructor() { 10 | this.configDir = path.join(pluginRoot, 'config') 11 | this.defaultConfigPath = path.join(this.configDir, 'config_default.yaml') 12 | this.userConfigPath = path.join(this.configDir, 'config', 'config.yaml') 13 | } 14 | 15 | initialize() { 16 | try { 17 | this.ensureDefaultConfigExists() 18 | this.createUserConfigIfMissing() 19 | this.synchronizeConfig() 20 | } catch (error) { 21 | logger.mark(LOG_PREFIX_INIT + ' 初始化失败' + error.message) 22 | throw error 23 | } 24 | } 25 | 26 | ensureDefaultConfigExists() { 27 | if (!fs.existsSync(this.defaultConfigPath)) { 28 | throw new Error('默认设置文件不存在,请检查或重新安装插件') 29 | } 30 | } 31 | 32 | createUserConfigIfMissing() { 33 | const userConfigDir = path.dirname(this.userConfigPath) 34 | 35 | try { 36 | if (!fs.existsSync(this.userConfigPath)) { 37 | logger.mark(LOG_PREFIX_INIT + ' 配置文件不存在,将使用默认设置文件') 38 | fs.mkdirSync(userConfigDir, { recursive: true }) 39 | fs.copyFileSync(this.defaultConfigPath, this.userConfigPath) 40 | } 41 | } catch (error) { 42 | throw new Error(`配置文件创建失败: ${error.message}`) 43 | } 44 | } 45 | 46 | synchronizeConfig() { 47 | const defaultConfig = Config.getDefConfig() 48 | const userConfig = Config.getConfig() 49 | 50 | // 合并默认配置到用户配置 51 | Object.entries(defaultConfig).forEach(([key, value]) => { 52 | if (!(key in userConfig)) { 53 | userConfig[key] = value 54 | } 55 | }) 56 | 57 | // 清理无效配置项 58 | const userKeys = Object.keys(userConfig) 59 | const validKeys = new Set(Object.keys(defaultConfig)) 60 | userKeys.forEach(key => { 61 | if (!validKeys.has(key)) { 62 | delete userConfig[key] 63 | } 64 | }) 65 | 66 | Config.setConfig(userConfig) 67 | } 68 | } 69 | 70 | class Init { 71 | constructor() { 72 | this.configManager = new ConfigManager() 73 | this.configManager.initialize() 74 | } 75 | } 76 | 77 | export default new Init() -------------------------------------------------------------------------------- /model/path.js: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | 3 | const _path = process.cwd().replace(/\\/g, '/') 4 | 5 | // 插件名 6 | const pluginName = path.basename(path.join(import.meta.url, '../../')) 7 | // 插件根目录 8 | const pluginRoot = path.join(_path, 'plugins', pluginName) 9 | // 插件资源目录 10 | const pluginResources = path.join(pluginRoot, 'resources') 11 | 12 | export { _path, pluginName, pluginRoot, pluginResources } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mc-plugin", 3 | "version": "3.0.0", 4 | "description": "基于Yunzai-Bot的与Minecraft Server互通消息的插件", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "type": "module", 10 | "module": "index.js", 11 | "dependencies": { 12 | "ws": "^8.13.0", 13 | "rcon-client": "^4.2.4" 14 | }, 15 | "keywords": [ 16 | "Yunzai-Bot", 17 | "云崽", 18 | "Minecraft", 19 | "我的世界" 20 | ], 21 | "author": "@CikeyQi", 22 | "license": "ISC" 23 | } -------------------------------------------------------------------------------- /resources/common/base.css: -------------------------------------------------------------------------------- 1 | .font-ys { 2 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 3 | } 4 | .font-nzbz { 5 | font-family: Number, "印品南征北战NZBZ体", NZBZ, PingFangSC-Medium, "PingFang SC", sans-serif; 6 | } 7 | /*# sourceMappingURL=base.css.map */ -------------------------------------------------------------------------------- /resources/common/base.less: -------------------------------------------------------------------------------- 1 | .font-YS { 2 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 3 | } 4 | 5 | .font-NZBZ { 6 | font-family: Number, "印品南征北战NZBZ体", NZBZ, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 7 | } -------------------------------------------------------------------------------- /resources/common/common.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'Number'; 3 | src: url("./font/tttgbnumber.woff") format('woff'), url("./font/tttgbnumber.ttf") format('truetype'); 4 | } 5 | @font-face { 6 | font-family: 'NZBZ'; 7 | src: url("./font/NZBZ.woff") format('woff'), url("./font/NZBZ.ttf") format('truetype'); 8 | } 9 | @font-face { 10 | font-family: 'YS'; 11 | src: url("./font/HYWH-65W.woff") format('woff'), url("./font/HYWH-65W.ttf") format('truetype'); 12 | } 13 | .font-YS { 14 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 15 | } 16 | .font-NZBZ { 17 | font-family: Number, "印品南征北战NZBZ体", NZBZ, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 18 | } 19 | * { 20 | margin: 0; 21 | padding: 0; 22 | box-sizing: border-box; 23 | -webkit-user-select: none; 24 | user-select: none; 25 | } 26 | body { 27 | font-size: 18px; 28 | color: #1e1f20; 29 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 30 | transform: scale(1.4); 31 | transform-origin: 0 0; 32 | width: 600px; 33 | } 34 | .container { 35 | width: 600px; 36 | padding: 20px 15px 10px 15px; 37 | background-size: contain; 38 | } 39 | .head-box { 40 | border-radius: 15px; 41 | padding: 10px 20px; 42 | position: relative; 43 | color: #fff; 44 | margin-top: 30px; 45 | } 46 | .head-box .title { 47 | font-family: Number, "印品南征北战NZBZ体", NZBZ, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 48 | font-size: 36px; 49 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, 0.9); 50 | } 51 | .head-box .title .label { 52 | display: inline-block; 53 | margin-left: 10px; 54 | } 55 | .head-box .genshin_logo { 56 | position: absolute; 57 | top: 1px; 58 | right: 15px; 59 | width: 97px; 60 | } 61 | .head-box .label { 62 | font-size: 16px; 63 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, 0.9); 64 | } 65 | .head-box .label span { 66 | color: #d3bc8e; 67 | padding: 0 2px; 68 | } 69 | .notice { 70 | color: #888; 71 | font-size: 12px; 72 | text-align: right; 73 | padding: 12px 5px 5px; 74 | } 75 | .notice-center { 76 | color: #fff; 77 | text-align: center; 78 | margin-bottom: 10px; 79 | text-shadow: 1px 1px 1px #333; 80 | } 81 | .copyright { 82 | font-size: 14px; 83 | text-align: center; 84 | color: #fff; 85 | position: relative; 86 | padding-left: 10px; 87 | text-shadow: 1px 1px 1px #000; 88 | margin: 10px 0; 89 | } 90 | .copyright .version { 91 | color: #d3bc8e; 92 | display: inline-block; 93 | padding: 0 3px; 94 | } 95 | /* */ 96 | .cons { 97 | display: inline-block; 98 | vertical-align: middle; 99 | padding: 0 5px; 100 | border-radius: 4px; 101 | } 102 | .cons-0 { 103 | background: #666; 104 | color: #fff; 105 | } 106 | .cons-n0 { 107 | background: #404949; 108 | color: #fff; 109 | } 110 | .cons-1 { 111 | background: #5cbac2; 112 | color: #fff; 113 | } 114 | .cons-2 { 115 | background: #339d61; 116 | color: #fff; 117 | } 118 | .cons-3 { 119 | background: #3e95b9; 120 | color: #fff; 121 | } 122 | .cons-4 { 123 | background: #3955b7; 124 | color: #fff; 125 | } 126 | .cons-5 { 127 | background: #531ba9cf; 128 | color: #fff; 129 | } 130 | .cons-6 { 131 | background: #ff5722; 132 | color: #fff; 133 | } 134 | .cons2-0 { 135 | border-radius: 4px; 136 | background: #666; 137 | color: #fff; 138 | } 139 | .cons2-1 { 140 | border-radius: 4px; 141 | background: #71b1b7; 142 | color: #fff; 143 | } 144 | .cons2-2 { 145 | border-radius: 4px; 146 | background: #369961; 147 | color: #fff; 148 | } 149 | .cons2-3 { 150 | border-radius: 4px; 151 | background: #4596b9; 152 | color: #fff; 153 | } 154 | .cons2-4 { 155 | border-radius: 4px; 156 | background: #4560b9; 157 | color: #fff; 158 | } 159 | .cons2-5 { 160 | border-radius: 4px; 161 | background: #531ba9cf; 162 | color: #fff; 163 | } 164 | .cons2-6 { 165 | border-radius: 4px; 166 | background: #ff5722; 167 | color: #fff; 168 | } 169 | /******** Fetter ********/ 170 | .fetter { 171 | width: 50px; 172 | height: 50px; 173 | display: inline-block; 174 | background: url('./item/fetter.png'); 175 | background-size: auto 100%; 176 | } 177 | .fetter.fetter1 { 178 | background-position: 0% 0; 179 | } 180 | .fetter.fetter2 { 181 | background-position: 11.11111111% 0; 182 | } 183 | .fetter.fetter3 { 184 | background-position: 22.22222222% 0; 185 | } 186 | .fetter.fetter4 { 187 | background-position: 33.33333333% 0; 188 | } 189 | .fetter.fetter5 { 190 | background-position: 44.44444444% 0; 191 | } 192 | .fetter.fetter6 { 193 | background-position: 55.55555556% 0; 194 | } 195 | .fetter.fetter7 { 196 | background-position: 66.66666667% 0; 197 | } 198 | .fetter.fetter8 { 199 | background-position: 77.77777778% 0; 200 | } 201 | .fetter.fetter9 { 202 | background-position: 88.88888889% 0; 203 | } 204 | .fetter.fetter10 { 205 | background-position: 100% 0; 206 | } 207 | /******** ELEM ********/ 208 | .elem-hydro .talent-icon { 209 | background-image: url("./bg/talent-hydro.png"); 210 | } 211 | .elem-hydro .elem-bg, 212 | .hydro-bg { 213 | background-image: url("./bg/bg-hydro.jpg"); 214 | } 215 | .elem-anemo .talent-icon { 216 | background-image: url("./bg/talent-anemo.png"); 217 | } 218 | .elem-anemo .elem-bg, 219 | .anemo-bg { 220 | background-image: url("./bg/bg-anemo.jpg"); 221 | } 222 | .elem-cryo .talent-icon { 223 | background-image: url("./bg/talent-cryo.png"); 224 | } 225 | .elem-cryo .elem-bg, 226 | .cryo-bg { 227 | background-image: url("./bg/bg-cryo.jpg"); 228 | } 229 | .elem-electro .talent-icon { 230 | background-image: url("./bg/talent-electro.png"); 231 | } 232 | .elem-electro .elem-bg, 233 | .electro-bg { 234 | background-image: url("./bg/bg-electro.jpg"); 235 | } 236 | .elem-geo .talent-icon { 237 | background-image: url("./bg/talent-geo.png"); 238 | } 239 | .elem-geo .elem-bg, 240 | .geo-bg { 241 | background-image: url("./bg/bg-geo.jpg"); 242 | } 243 | .elem-pyro .talent-icon { 244 | background-image: url("./bg/talent-pyro.png"); 245 | } 246 | .elem-pyro .elem-bg, 247 | .pyro-bg { 248 | background-image: url("./bg/bg-pyro.jpg"); 249 | } 250 | .elem-dendro .talent-icon { 251 | background-image: url("./bg/talent-dendro.png"); 252 | } 253 | .elem-dendro .elem-bg, 254 | .dendro-bg { 255 | background-image: url("./bg/bg-dendro.jpg"); 256 | } 257 | /* cont */ 258 | .cont { 259 | border-radius: 10px; 260 | background: url("../common/cont/card-bg.png") top left repeat-x; 261 | background-size: auto 100%; 262 | margin: 5px 15px 5px 10px; 263 | position: relative; 264 | box-shadow: 0 0 1px 0 #ccc, 2px 2px 4px 0 rgba(50, 50, 50, 0.8); 265 | overflow: hidden; 266 | color: #fff; 267 | font-size: 16px; 268 | } 269 | .cont-title { 270 | background: rgba(0, 0, 0, 0.4); 271 | box-shadow: 0 0 1px 0 #fff; 272 | color: #d3bc8e; 273 | padding: 10px 20px; 274 | text-align: left; 275 | border-radius: 10px 10px 0 0; 276 | } 277 | .cont-title span { 278 | font-size: 12px; 279 | color: #aaa; 280 | margin-left: 10px; 281 | font-weight: normal; 282 | } 283 | .cont-title.border-less { 284 | background: linear-gradient(rgba(0, 0, 0, 0.5), rgba(0, 0, 0, 0)); 285 | box-shadow: none; 286 | padding-bottom: 5px; 287 | } 288 | .cont-body { 289 | padding: 10px 15px; 290 | font-size: 12px; 291 | background: rgba(0, 0, 0, 0.5); 292 | box-shadow: 0 0 1px 0 #fff; 293 | font-weight: normal; 294 | } 295 | .cont-footer { 296 | padding: 10px 15px; 297 | font-size: 12px; 298 | background: rgba(0, 0, 0, 0.5); 299 | font-weight: normal; 300 | } 301 | .cont > ul.cont-msg { 302 | display: block; 303 | padding: 5px 10px; 304 | background: rgba(0, 0, 0, 0.5); 305 | } 306 | ul.cont-msg, 307 | .cont-footer ul { 308 | padding-left: 15px; 309 | } 310 | ul.cont-msg li, 311 | .cont-footer ul li { 312 | margin: 5px 0; 313 | margin-left: 15px; 314 | } 315 | ul.cont-msg li strong, 316 | .cont-footer ul li strong { 317 | font-weight: normal; 318 | margin: 0 2px; 319 | color: #d3bc8e; 320 | } 321 | .cont-table { 322 | display: table; 323 | width: 100%; 324 | } 325 | .cont-table .tr { 326 | display: table-row; 327 | } 328 | .cont-table .tr:nth-child(even) { 329 | background: rgba(0, 0, 0, 0.4); 330 | } 331 | .cont-table .tr:nth-child(odd) { 332 | background: rgba(50, 50, 50, 0.4); 333 | } 334 | .cont-table .tr > div, 335 | .cont-table .tr > td { 336 | display: table-cell; 337 | box-shadow: 0 0 1px 0 #fff; 338 | } 339 | .cont-table .tr > div.value-full { 340 | display: table; 341 | width: 200%; 342 | } 343 | .cont-table .tr > div.value-none { 344 | box-shadow: none; 345 | } 346 | .cont-table .thead { 347 | text-align: center; 348 | } 349 | .cont-table .thead > div, 350 | .cont-table .thead > td { 351 | color: #d3bc8e; 352 | background: rgba(0, 0, 0, 0.4); 353 | line-height: 40px; 354 | height: 40px; 355 | } 356 | .cont-table .title, 357 | .cont-table .th { 358 | color: #d3bc8e; 359 | padding-right: 15px; 360 | text-align: right; 361 | background: rgba(0, 0, 0, 0.4); 362 | min-width: 100px; 363 | vertical-align: middle; 364 | } 365 | .logo { 366 | font-size: 18px; 367 | text-align: center; 368 | color: #fff; 369 | margin: 20px 0 10px 0; 370 | } 371 | /* item-icon */ 372 | .item-icon { 373 | width: 100%; 374 | height: 100%; 375 | border-radius: 4px; 376 | position: relative; 377 | overflow: hidden; 378 | } 379 | .item-icon .img { 380 | width: 100%; 381 | height: 100%; 382 | display: block; 383 | background-size: contain; 384 | background-position: center; 385 | background-repeat: no-repeat; 386 | } 387 | .item-icon.artis .img { 388 | width: 84%; 389 | height: 84%; 390 | margin: 8%; 391 | } 392 | .item-icon.star1 { 393 | background-image: url("../common/item/bg1.png"); 394 | } 395 | .item-icon.opacity-bg.star1 { 396 | background-image: url("../common/item/bg1-o.png"); 397 | } 398 | .item-icon.star2 { 399 | background-image: url("../common/item/bg2.png"); 400 | } 401 | .item-icon.opacity-bg.star2 { 402 | background-image: url("../common/item/bg2-o.png"); 403 | } 404 | .item-icon.star3 { 405 | background-image: url("../common/item/bg3.png"); 406 | } 407 | .item-icon.opacity-bg.star3 { 408 | background-image: url("../common/item/bg3-o.png"); 409 | } 410 | .item-icon.star4 { 411 | background-image: url("../common/item/bg4.png"); 412 | } 413 | .item-icon.opacity-bg.star4 { 414 | background-image: url("../common/item/bg4-o.png"); 415 | } 416 | .item-icon.star5 { 417 | background-image: url("../common/item/bg5.png"); 418 | } 419 | .item-icon.opacity-bg.star5 { 420 | background-image: url("../common/item/bg5-o.png"); 421 | } 422 | .item-icon.star-w { 423 | background: #fff; 424 | } 425 | .item-list { 426 | display: flex; 427 | } 428 | .item-list .item-card { 429 | width: 70px; 430 | background: #e7e5d9; 431 | } 432 | .item-list .item-icon { 433 | border-bottom-left-radius: 0; 434 | border-bottom-right-radius: 12px; 435 | } 436 | .item-list .item-title { 437 | color: #222; 438 | font-size: 13px; 439 | text-align: center; 440 | padding: 2px; 441 | white-space: nowrap; 442 | overflow: hidden; 443 | } 444 | .item-list .item-icon { 445 | height: initial; 446 | } 447 | .item-list .item-badge { 448 | position: absolute; 449 | display: block; 450 | left: 0; 451 | top: 0; 452 | background: rgba(0, 0, 0, 0.6); 453 | font-size: 12px; 454 | color: #fff; 455 | padding: 4px 5px 3px; 456 | border-radius: 0 0 6px 0; 457 | } 458 | /*# sourceMappingURL=common.css.map */ -------------------------------------------------------------------------------- /resources/common/common.less: -------------------------------------------------------------------------------- 1 | .font(@name, @file) { 2 | @font-face { 3 | font-family: @name; 4 | src: url("./font/@{file}.woff") format('woff'), url("./font/@{file}.ttf") format('truetype'); 5 | } 6 | } 7 | 8 | .font('Number', 'tttgbnumber'); 9 | .font('NZBZ', 'NZBZ'); 10 | .font('YS', 'HYWH-65W'); 11 | 12 | @import "base.less"; 13 | 14 | * { 15 | margin: 0; 16 | padding: 0; 17 | box-sizing: border-box; 18 | -webkit-user-select: none; 19 | user-select: none; 20 | } 21 | 22 | body { 23 | font-size: 18px; 24 | color: #1e1f20; 25 | font-family: Number, "汉仪文黑-65W", YS, PingFangSC-Medium, "PingFang SC", sans-serif; 26 | transform: scale(1.4); 27 | transform-origin: 0 0; 28 | width: 600px; 29 | } 30 | 31 | .container { 32 | width: 600px; 33 | padding: 20px 15px 10px 15px; 34 | background-size: contain; 35 | } 36 | 37 | 38 | .head-box { 39 | border-radius: 15px; 40 | padding: 10px 20px; 41 | position: relative; 42 | color: #fff; 43 | margin-top: 30px; 44 | 45 | .title { 46 | .font-NZBZ; 47 | font-size: 36px; 48 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, .9); 49 | 50 | .label { 51 | display: inline-block; 52 | margin-left: 10px; 53 | } 54 | } 55 | 56 | .genshin_logo { 57 | position: absolute; 58 | top: 1px; 59 | right: 15px; 60 | width: 97px; 61 | } 62 | 63 | .label { 64 | font-size: 16px; 65 | text-shadow: 0 0 1px #000, 1px 1px 3px rgba(0, 0, 0, .9); 66 | 67 | span { 68 | color: #d3bc8e; 69 | padding: 0 2px; 70 | } 71 | } 72 | } 73 | 74 | 75 | .notice { 76 | color: #888; 77 | font-size: 12px; 78 | text-align: right; 79 | padding: 12px 5px 5px; 80 | } 81 | 82 | .notice-center { 83 | color: #fff; 84 | text-align: center; 85 | margin-bottom: 10px; 86 | text-shadow: 1px 1px 1px #333; 87 | } 88 | 89 | .copyright { 90 | font-size: 14px; 91 | text-align: center; 92 | color: #fff; 93 | position: relative; 94 | padding-left: 10px; 95 | text-shadow: 1px 1px 1px #000; 96 | margin: 10px 0; 97 | 98 | .version { 99 | color: #d3bc8e; 100 | display: inline-block; 101 | padding: 0 3px; 102 | } 103 | } 104 | 105 | 106 | /* */ 107 | 108 | .cons { 109 | display: inline-block; 110 | vertical-align: middle; 111 | padding: 0 5px; 112 | border-radius: 4px; 113 | } 114 | 115 | 116 | .cons(@idx, @bg, @color:#fff) { 117 | .cons-@{idx} { 118 | background: @bg; 119 | color: @color; 120 | } 121 | } 122 | 123 | .cons(0, #666); 124 | .cons(n0, #404949); 125 | .cons(1, #5cbac2); 126 | .cons(2, #339d61); 127 | .cons(3, #3e95b9); 128 | .cons(4, #3955b7); 129 | .cons(5, #531ba9cf); 130 | .cons(6, #ff5722); 131 | 132 | .cons2(@idx, @bg, @color:#fff) { 133 | .cons2-@{idx} { 134 | border-radius: 4px; 135 | background: @bg; 136 | color: @color; 137 | } 138 | } 139 | 140 | .cons2(0, #666); 141 | .cons2(1, #71b1b7); 142 | .cons2(2, #369961); 143 | .cons2(3, #4596b9); 144 | .cons2(4, #4560b9); 145 | .cons2(5, #531ba9cf); 146 | .cons2(6, #ff5722); 147 | 148 | /******** Fetter ********/ 149 | 150 | .fetter { 151 | width: 50px; 152 | height: 50px; 153 | display: inline-block; 154 | background: url('./item/fetter.png'); 155 | background-size: auto 100%; 156 | @fetters: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10; 157 | each(@fetters, { 158 | &.fetter@{value} { 159 | background-position: (-100%/9)+(100%/9)*@value 0; 160 | } 161 | }) 162 | } 163 | 164 | /******** ELEM ********/ 165 | 166 | @elems: hydro, anemo, cryo, electro, geo, pyro, dendro; 167 | 168 | each(@elems, { 169 | .elem-@{value} .talent-icon { 170 | background-image: url("./bg/talent-@{value}.png"); 171 | } 172 | 173 | .elem-@{value} .elem-bg, 174 | .@{value}-bg { 175 | background-image: url("./bg/bg-@{value}.jpg"); 176 | } 177 | }) 178 | 179 | 180 | /* cont */ 181 | 182 | .cont { 183 | border-radius: 10px; 184 | background: url("../common/cont/card-bg.png") top left repeat-x; 185 | background-size: auto 100%; 186 | // backdrop-filter: blur(3px); 187 | margin: 5px 15px 5px 10px; 188 | position: relative; 189 | box-shadow: 0 0 1px 0 #ccc, 2px 2px 4px 0 rgba(50, 50, 50, .8); 190 | overflow: hidden; 191 | color: #fff; 192 | font-size: 16px; 193 | } 194 | 195 | 196 | .cont-title { 197 | background: rgba(0, 0, 0, .4); 198 | box-shadow: 0 0 1px 0 #fff; 199 | color: #d3bc8e; 200 | padding: 10px 20px; 201 | text-align: left; 202 | border-radius: 10px 10px 0 0; 203 | 204 | span { 205 | font-size: 12px; 206 | color: #aaa; 207 | margin-left: 10px; 208 | font-weight: normal; 209 | } 210 | 211 | &.border-less { 212 | background: linear-gradient(rgba(0, 0, 0, .5), rgba(0, 0, 0, 0)); 213 | box-shadow: none; 214 | padding-bottom: 5px; 215 | } 216 | } 217 | 218 | .cont-body { 219 | padding: 10px 15px; 220 | font-size: 12px; 221 | background: rgba(0, 0, 0, 0.5); 222 | box-shadow: 0 0 1px 0 #fff; 223 | font-weight: normal; 224 | } 225 | 226 | 227 | .cont-footer { 228 | padding: 10px 15px; 229 | font-size: 12px; 230 | background: rgba(0, 0, 0, 0.5); 231 | font-weight: normal; 232 | } 233 | 234 | .cont > ul.cont-msg { 235 | display: block; 236 | padding: 5px 10px; 237 | background: rgba(0, 0, 0, 0.5); 238 | } 239 | 240 | ul.cont-msg, .cont-footer ul { 241 | padding-left: 15px; 242 | 243 | li { 244 | margin: 5px 0; 245 | margin-left: 15px; 246 | 247 | strong { 248 | font-weight: normal; 249 | margin: 0 2px; 250 | color: #d3bc8e; 251 | } 252 | } 253 | } 254 | 255 | .cont-table { 256 | display: table; 257 | width: 100%; 258 | } 259 | 260 | .cont-table .tr { 261 | display: table-row; 262 | } 263 | 264 | .cont-table .tr:nth-child(even) { 265 | background: rgba(0, 0, 0, .4); 266 | } 267 | 268 | .cont-table .tr:nth-child(odd) { 269 | background: rgba(50, 50, 50, .4); 270 | } 271 | 272 | .cont-table .tr > div, 273 | .cont-table .tr > td { 274 | display: table-cell; 275 | box-shadow: 0 0 1px 0 #fff; 276 | } 277 | 278 | .cont-table .tr > div.value-full { 279 | display: table; 280 | width: 200%; 281 | } 282 | 283 | .cont-table .tr > div.value-none { 284 | box-shadow: none; 285 | } 286 | 287 | .cont-table .thead { 288 | text-align: center; 289 | } 290 | 291 | .cont-table .thead > div, 292 | .cont-table .thead > td { 293 | color: #d3bc8e; 294 | background: rgba(0, 0, 0, .4); 295 | line-height: 40px; 296 | height: 40px; 297 | } 298 | 299 | 300 | .cont-table .title, 301 | .cont-table .th { 302 | color: #d3bc8e; 303 | padding-right: 15px; 304 | text-align: right; 305 | background: rgba(0, 0, 0, .4); 306 | min-width: 100px; 307 | vertical-align: middle; 308 | } 309 | 310 | .logo { 311 | font-size: 18px; 312 | text-align: center; 313 | color: #fff; 314 | margin: 20px 0 10px 0; 315 | } 316 | 317 | /* item-icon */ 318 | .item-icon { 319 | width: 100%; 320 | height: 100%; 321 | border-radius: 4px; 322 | position: relative; 323 | overflow: hidden; 324 | 325 | .img { 326 | width: 100%; 327 | height: 100%; 328 | display: block; 329 | background-size: contain; 330 | background-position: center; 331 | background-repeat: no-repeat; 332 | } 333 | 334 | &.artis { 335 | .img { 336 | width: 84%; 337 | height: 84%; 338 | margin: 8%; 339 | } 340 | } 341 | 342 | @stars: 1, 2, 3, 4, 5; 343 | each(@stars, { 344 | &.star@{value} { 345 | background-image: url("../common/item/bg@{value}.png"); 346 | } 347 | &.opacity-bg.star@{value} { 348 | background-image: url("../common/item/bg@{value}-o.png"); 349 | } 350 | }) 351 | 352 | &.star-w { 353 | background: #fff; 354 | } 355 | } 356 | 357 | .item-list { 358 | display: flex; 359 | 360 | .item-card { 361 | width: 70px; 362 | background: #e7e5d9; 363 | } 364 | 365 | .item-icon { 366 | border-bottom-left-radius: 0; 367 | border-bottom-right-radius: 12px; 368 | } 369 | 370 | .item-title { 371 | color: #222; 372 | font-size: 13px; 373 | text-align: center; 374 | padding: 2px; 375 | white-space: nowrap; 376 | overflow: hidden; 377 | } 378 | 379 | .item-icon { 380 | height: initial; 381 | } 382 | 383 | .item-badge { 384 | position: absolute; 385 | display: block; 386 | left: 0; 387 | top: 0; 388 | background: rgba(0, 0, 0, 0.6); 389 | font-size: 12px; 390 | color: #fff; 391 | padding: 4px 5px 3px; 392 | border-radius: 0 0 6px 0; 393 | } 394 | } -------------------------------------------------------------------------------- /resources/common/font/HYWH-65W.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/HYWH-65W.ttf -------------------------------------------------------------------------------- /resources/common/font/HYWH-65W.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/HYWH-65W.woff -------------------------------------------------------------------------------- /resources/common/font/NZBZ.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/NZBZ.ttf -------------------------------------------------------------------------------- /resources/common/font/NZBZ.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/NZBZ.woff -------------------------------------------------------------------------------- /resources/common/font/tttgbnumber.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/tttgbnumber.ttf -------------------------------------------------------------------------------- /resources/common/font/tttgbnumber.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/tttgbnumber.woff -------------------------------------------------------------------------------- /resources/common/font/华文中宋.TTF: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/common/font/华文中宋.TTF -------------------------------------------------------------------------------- /resources/common/layout/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | miao-plugin 12 | {{block 'css'}} 13 | {{/block}} 14 | 15 | 16 | 17 | {{block 'main'}}{{/block}} 18 | {{@copyright || sys?.copyright}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/common/layout/elem.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | mc-plugin 12 | {{block 'css'}} 13 | {{/block}} 14 | 15 | 16 | 17 | {{block 'main'}}{{/block}} 18 | {{@copyright || sys?.copyright}} 19 | 20 | 21 | -------------------------------------------------------------------------------- /resources/fonts/Minecraft.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/fonts/Minecraft.ttf -------------------------------------------------------------------------------- /resources/help/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/help/icon.png -------------------------------------------------------------------------------- /resources/help/imgs/bg.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/help/imgs/bg.jpg -------------------------------------------------------------------------------- /resources/help/imgs/config.js: -------------------------------------------------------------------------------- 1 | export const style = { 2 | // 主文字颜色 3 | fontColor: '#ceb78b', 4 | // 主文字阴影: 横向距离 垂直距离 阴影大小 阴影颜色 5 | // fontShadow: '0px 0px 1px rgba(6, 21, 31, .9)', 6 | fontShadow: 'none', 7 | // 描述文字颜色 8 | descColor: '#eee', 9 | 10 | /* 面板整体底色,会叠加在标题栏及帮助行之下,方便整体帮助有一个基础底色 11 | * 若无需此项可将rgba最后一位置为0即为完全透明 12 | * 注意若综合透明度较低,或颜色与主文字颜色过近或太透明可能导致阅读困难 */ 13 | contBgColor: 'rgba(6, 21, 31, .5)', 14 | 15 | // 面板底图毛玻璃效果,数字越大越模糊,0-10 ,可为小数 16 | contBgBlur: 3, 17 | 18 | // 板块标题栏底色 19 | headerBgColor: 'rgba(6, 21, 31, .4)', 20 | // 帮助奇数行底色 21 | rowBgColor1: 'rgba(6, 21, 31, .2)', 22 | // 帮助偶数行底色 23 | rowBgColor2: 'rgba(6, 21, 31, .35)' 24 | } 25 | -------------------------------------------------------------------------------- /resources/help/imgs/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/help/imgs/main.png -------------------------------------------------------------------------------- /resources/help/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 830px; 4 | background: url("../common/theme/bg-01.jpg"); 5 | } 6 | .container { 7 | background: url(../common/theme/main-01.png) top left no-repeat; 8 | background-size: 100% auto; 9 | width: 830px; 10 | } 11 | .head-box { 12 | margin: 60px 0 0 0; 13 | padding-bottom: 0; 14 | } 15 | .head-box .title { 16 | font-size: 50px; 17 | } 18 | .cont-box { 19 | border-radius: 15px; 20 | margin-top: 20px; 21 | margin-bottom: 20px; 22 | overflow: hidden; 23 | box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15); 24 | position: relative; 25 | } 26 | .help-group { 27 | font-size: 18px; 28 | font-weight: bold; 29 | padding: 15px 15px 10px 20px; 30 | } 31 | .help-table { 32 | text-align: center; 33 | border-collapse: collapse; 34 | margin: 0; 35 | border-radius: 0 0 10px 10px; 36 | display: table; 37 | overflow: hidden; 38 | width: 100%; 39 | color: #fff; 40 | } 41 | .help-table .tr { 42 | display: table-row; 43 | } 44 | .help-table .td, 45 | .help-table .th { 46 | font-size: 14px; 47 | display: table-cell; 48 | box-shadow: 0 0 1px 0 #888 inset; 49 | padding: 12px 0 12px 50px; 50 | line-height: 24px; 51 | position: relative; 52 | text-align: left; 53 | } 54 | .help-table .tr:last-child .td { 55 | padding-bottom: 12px; 56 | } 57 | .help-table .th { 58 | background: rgba(34, 41, 51, 0.5); 59 | } 60 | .help-icon { 61 | width: 40px; 62 | height: 40px; 63 | display: block; 64 | position: absolute; 65 | background: url("icon.png") 0 0 no-repeat; 66 | background-size: 500px auto; 67 | border-radius: 5px; 68 | left: 6px; 69 | top: 12px; 70 | transform: scale(0.85); 71 | } 72 | .help-title { 73 | display: block; 74 | color: #d3bc8e; 75 | font-size: 16px; 76 | line-height: 24px; 77 | } 78 | .help-desc { 79 | display: block; 80 | font-size: 13px; 81 | line-height: 18px; 82 | } 83 | /*# sourceMappingURL=index.css.map */ -------------------------------------------------------------------------------- /resources/help/index.html: -------------------------------------------------------------------------------- 1 | {{extend defaultLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | <% style = style.replace(/{{_res_path}}/g, _res_path) %> 6 | {{@style}} 7 | {{/block}} 8 | 9 | {{block 'main'}} 10 | 11 | 12 | 13 | {{helpCfg.title||"使用帮助"}} 14 | {{helpCfg.subTitle || "Yunzai-Bot & Miao-Plugin"}} 15 | 16 | 17 | 18 | {{each helpGroup group}} 19 | {{set len = group?.list?.length || 0 }} 20 | 21 | {{group.group}} 22 | {{if len > 0}} 23 | 24 | 25 | {{each group.list help idx}} 26 | 27 | 28 | {{help.title}} 29 | {{help.desc}} 30 | 31 | {{if idx%colCount === colCount-1 && idx>0 && idx< len-1}} 32 | 33 | 34 | {{/if}} 35 | {{/each}} 36 | <% for(let i=(len-1)%colCount; i< colCount-1 ; i++){ %> 37 | 38 | <% } %> 39 | 40 | 41 | {{/if}} 42 | 43 | {{/each}} 44 | {{/block}} -------------------------------------------------------------------------------- /resources/help/index.less: -------------------------------------------------------------------------------- 1 | body { 2 | transform: scale(1); 3 | width: 830px; 4 | background: url("../common/theme/bg-01.jpg"); 5 | } 6 | 7 | .container { 8 | background: url(../common/theme/main-01.png) top left no-repeat; 9 | background-size: 100% auto; 10 | width: 830px; 11 | } 12 | 13 | .head-box { 14 | margin: 60px 0 0 0; 15 | padding-bottom: 0; 16 | } 17 | 18 | .head-box .title { 19 | font-size: 50px; 20 | } 21 | 22 | .cont-box { 23 | border-radius: 15px; 24 | margin-top: 20px; 25 | margin-bottom: 20px; 26 | overflow: hidden; 27 | box-shadow: 0 5px 10px 0 rgb(0 0 0 / 15%); 28 | position: relative; 29 | } 30 | 31 | .help-group { 32 | font-size: 18px; 33 | font-weight: bold; 34 | padding: 15px 15px 10px 20px; 35 | } 36 | 37 | .help-table { 38 | text-align: center; 39 | border-collapse: collapse; 40 | margin: 0; 41 | border-radius: 0 0 10px 10px; 42 | display: table; 43 | overflow: hidden; 44 | width: 100%; 45 | color: #fff; 46 | } 47 | 48 | .help-table .tr { 49 | display: table-row; 50 | } 51 | 52 | .help-table .td, 53 | .help-table .th { 54 | font-size: 14px; 55 | display: table-cell; 56 | box-shadow: 0 0 1px 0 #888 inset; 57 | padding: 12px 0 12px 50px; 58 | line-height: 24px; 59 | position: relative; 60 | text-align: left; 61 | } 62 | 63 | .help-table .tr:last-child .td { 64 | padding-bottom: 12px; 65 | } 66 | 67 | .help-table .th { 68 | background: rgba(34, 41, 51, .5) 69 | } 70 | 71 | .help-icon { 72 | width: 40px; 73 | height: 40px; 74 | display: block; 75 | position: absolute; 76 | background: url("icon.png") 0 0 no-repeat; 77 | background-size: 500px auto; 78 | border-radius: 5px; 79 | left: 6px; 80 | top: 12px; 81 | transform: scale(0.85); 82 | } 83 | 84 | .help-title { 85 | display: block; 86 | color: #d3bc8e; 87 | font-size: 16px; 88 | line-height: 24px; 89 | } 90 | 91 | .help-desc { 92 | display: block; 93 | font-size: 13px; 94 | line-height: 18px; 95 | } 96 | 97 | -------------------------------------------------------------------------------- /resources/help/version-info.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | user-select: none; 6 | } 7 | body { 8 | font-size: 18px; 9 | color: #1e1f20; 10 | transform: scale(1.3); 11 | transform-origin: 0 0; 12 | width: 600px; 13 | } 14 | .container { 15 | width: 600px; 16 | padding: 10px 0 10px 0; 17 | background-size: 100% 100%; 18 | } 19 | .log-cont { 20 | background-size: cover; 21 | margin: 5px 15px 5px 10px; 22 | border-radius: 10px; 23 | } 24 | .log-cont .cont { 25 | margin: 0; 26 | } 27 | .log-cont .cont-title { 28 | font-size: 16px; 29 | padding: 10px 20px 6px; 30 | } 31 | .log-cont .cont-title.current-version { 32 | font-size: 20px; 33 | } 34 | .log-cont ul { 35 | font-size: 14px; 36 | padding-left: 20px; 37 | } 38 | .log-cont ul li { 39 | margin: 3px 0; 40 | } 41 | .log-cont ul.sub-log-ul li { 42 | margin: 1px 0; 43 | } 44 | .log-cont .cmd { 45 | color: #d3bc8e; 46 | display: inline-block; 47 | border-radius: 3px; 48 | background: rgba(0, 0, 0, 0.5); 49 | padding: 0 3px; 50 | margin: 1px 2px; 51 | } 52 | .log-cont .strong { 53 | color: #24d5cd; 54 | } 55 | .log-cont .new { 56 | display: inline-block; 57 | width: 18px; 58 | margin: 0 -3px 0 1px; 59 | } 60 | .log-cont .new:before { 61 | content: "NEW"; 62 | display: inline-block; 63 | transform: scale(0.6); 64 | transform-origin: 0 0; 65 | color: #d3bc8e; 66 | white-space: nowrap; 67 | } 68 | .dev-cont { 69 | background: none; 70 | } 71 | .dev-cont .cont-title { 72 | background: rgba(0, 0, 0, 0.7); 73 | } 74 | .dev-cont .cont-body { 75 | background: rgba(0, 0, 0, 0.5); 76 | } 77 | .dev-cont .cont-body.dev-info { 78 | background: rgba(0, 0, 0, 0.2); 79 | } 80 | .dev-cont .strong { 81 | font-size: 15px; 82 | } 83 | /*# sourceMappingURL=version-info.css.map */ -------------------------------------------------------------------------------- /resources/help/version-info.html: -------------------------------------------------------------------------------- 1 | {{extend elemLayout}} 2 | 3 | {{block 'css'}} 4 | 5 | {{/block}} 6 | 7 | {{block 'main'}} 8 | {{each changelogs ds idx}} 9 | 10 | {{set v = ds.version }} 11 | {{set isDev = v[v.length-1] === 'v'}} 12 | 13 | {{if idx === 0 }} 14 | 当前版本 {{v}} 15 | {{else}} 16 | {{name || 'mc-plugin'}}版本 {{v}} 17 | {{/if}} 18 | 19 | 20 | {{each ds.logs log}} 21 | 22 | {{@log.title}} 23 | {{if log.logs.length > 0}} 24 | 25 | {{each log.logs ls}} 26 | {{@ls}} 27 | {{/each}} 28 | 29 | {{/if}} 30 | 31 | {{/each}} 32 | 33 | 34 | 35 | 36 | {{/each}} 37 | {{/block}} -------------------------------------------------------------------------------- /resources/help/version-info.less: -------------------------------------------------------------------------------- 1 | .linear-bg(@color) { 2 | background-image: linear-gradient(to right, @color, @color 80%, fade(@color, 0) 100%); 3 | } 4 | 5 | * { 6 | margin: 0; 7 | padding: 0; 8 | box-sizing: border-box; 9 | user-select: none; 10 | } 11 | 12 | body { 13 | font-size: 18px; 14 | color: #1e1f20; 15 | transform: scale(1.3); 16 | transform-origin: 0 0; 17 | width: 600px; 18 | } 19 | 20 | .container { 21 | width: 600px; 22 | padding: 10px 0 10px 0; 23 | background-size: 100% 100%; 24 | 25 | } 26 | 27 | .log-cont { 28 | background-size: cover; 29 | margin: 5px 15px 5px 10px; 30 | border-radius: 10px; 31 | 32 | .cont { 33 | margin: 0; 34 | } 35 | 36 | .cont-title { 37 | font-size: 16px; 38 | padding: 10px 20px 6px; 39 | 40 | &.current-version { 41 | font-size: 20px; 42 | } 43 | } 44 | 45 | .cont-body { 46 | } 47 | 48 | ul { 49 | font-size: 14px; 50 | padding-left: 20px; 51 | 52 | li { 53 | margin: 3px 0; 54 | } 55 | 56 | &.sub-log-ul { 57 | li { 58 | margin: 1px 0; 59 | } 60 | } 61 | } 62 | 63 | .cmd { 64 | color: #d3bc8e; 65 | display: inline-block; 66 | border-radius: 3px; 67 | background: rgba(0, 0, 0, 0.5); 68 | padding: 0 3px; 69 | margin: 1px 2px; 70 | } 71 | 72 | .strong { 73 | color: #24d5cd; 74 | } 75 | 76 | .new { 77 | display: inline-block; 78 | width: 18px; 79 | margin: 0 -3px 0 1px; 80 | } 81 | 82 | .new:before { 83 | content: "NEW"; 84 | display: inline-block; 85 | transform: scale(0.6); 86 | transform-origin: 0 0; 87 | color: #d3bc8e; 88 | white-space: nowrap; 89 | } 90 | } 91 | 92 | .dev-cont { 93 | background: none; 94 | 95 | .cont-title { 96 | background: rgba(0, 0, 0, .7); 97 | } 98 | 99 | .cont-body { 100 | background: rgba(0, 0, 0, .5); 101 | 102 | &.dev-info { 103 | background: rgba(0, 0, 0, .2); 104 | } 105 | } 106 | 107 | .strong { 108 | font-size: 15px; 109 | } 110 | } -------------------------------------------------------------------------------- /resources/readme/girl.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/CikeyQi/mc-plugin/0806fa6e329e4f00acfd8eca45c1319681b5b142/resources/readme/girl.png --------------------------------------------------------------------------------
{{@log.title}}