diff --git a/LICENSE b/LICENSE index 3423cec..e69de29 100644 --- a/LICENSE +++ b/LICENSE @@ -1,661 +0,0 @@ - GNU AFFERO GENERAL PUBLIC LICENSE - Version 3, 19 November 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - Preamble - - The GNU Affero General Public License is a free, copyleft license for -software and other kinds of works, specifically designed to ensure -cooperation with the community in the case of network server software. - - The licenses for most software and other practical works are designed -to take away your freedom to share and change the works. By contrast, -our General Public Licenses are intended to guarantee your freedom to -share and change all versions of a program--to make sure it remains free -software for all its users. - - When we speak of free software, we are referring to freedom, not -price. Our General Public Licenses are designed to make sure that you -have the freedom to distribute copies of free software (and charge for -them if you wish), that you receive source code or can get it if you -want it, that you can change the software or use pieces of it in new -free programs, and that you know you can do these things. - - Developers that use our General Public Licenses protect your rights -with two steps: (1) assert copyright on the software, and (2) offer -you this License which gives you legal permission to copy, distribute -and/or modify the software. - - A secondary benefit of defending all users' freedom is that -improvements made in alternate versions of the program, if they -receive widespread use, become available for other developers to -incorporate. Many developers of free software are heartened and -encouraged by the resulting cooperation. However, in the case of -software used on network servers, this result may fail to come about. -The GNU General Public License permits making a modified version and -letting the public access it on a server without ever releasing its -source code to the public. - - The GNU Affero General Public License is designed specifically to -ensure that, in such cases, the modified source code becomes available -to the community. It requires the operator of a network server to -provide the source code of the modified version running there to the -users of that server. Therefore, public use of a modified version, on -a publicly accessible server, gives the public access to the source -code of the modified version. - - An older license, called the Affero General Public License and -published by Affero, was designed to accomplish similar goals. This is -a different license, not a version of the Affero GPL, but Affero has -released a new version of the Affero GPL which permits relicensing under -this license. - - The precise terms and conditions for copying, distribution and -modification follow. - - TERMS AND CONDITIONS - - 0. Definitions. - - "This License" refers to version 3 of the GNU Affero General Public License. - - "Copyright" also means copyright-like laws that apply to other kinds of -works, such as semiconductor masks. - - "The Program" refers to any copyrightable work licensed under this -License. Each licensee is addressed as "you". "Licensees" and -"recipients" may be individuals or organizations. - - To "modify" a work means to copy from or adapt all or part of the work -in a fashion requiring copyright permission, other than the making of an -exact copy. The resulting work is called a "modified version" of the -earlier work or a work "based on" the earlier work. - - A "covered work" means either the unmodified Program or a work based -on the Program. - - To "propagate" a work means to do anything with it that, without -permission, would make you directly or secondarily liable for -infringement under applicable copyright law, except executing it on a -computer or modifying a private copy. Propagation includes copying, -distribution (with or without modification), making available to the -public, and in some countries other activities as well. - - To "convey" a work means any kind of propagation that enables other -parties to make or receive copies. Mere interaction with a user through -a computer network, with no transfer of a copy, is not conveying. - - An interactive user interface displays "Appropriate Legal Notices" -to the extent that it includes a convenient and prominently visible -feature that (1) displays an appropriate copyright notice, and (2) -tells the user that there is no warranty for the work (except to the -extent that warranties are provided), that licensees may convey the -work under this License, and how to view a copy of this License. If -the interface presents a list of user commands or options, such as a -menu, a prominent item in the list meets this criterion. - - 1. Source Code. - - The "source code" for a work means the preferred form of the work -for making modifications to it. "Object code" means any non-source -form of a work. - - A "Standard Interface" means an interface that either is an official -standard defined by a recognized standards body, or, in the case of -interfaces specified for a particular programming language, one that -is widely used among developers working in that language. - - The "System Libraries" of an executable work include anything, other -than the work as a whole, that (a) is included in the normal form of -packaging a Major Component, but which is not part of that Major -Component, and (b) serves only to enable use of the work with that -Major Component, or to implement a Standard Interface for which an -implementation is available to the public in source code form. A -"Major Component", in this context, means a major essential component -(kernel, window system, and so on) of the specific operating system -(if any) on which the executable work runs, or a compiler used to -produce the work, or an object code interpreter used to run it. - - The "Corresponding Source" for a work in object code form means all -the source code needed to generate, install, and (for an executable -work) run the object code and to modify the work, including scripts to -control those activities. However, it does not include the work's -System Libraries, or general-purpose tools or generally available free -programs which are used unmodified in performing those activities but -which are not part of the work. For example, Corresponding Source -includes interface definition files associated with source files for -the work, and the source code for shared libraries and dynamically -linked subprograms that the work is specifically designed to require, -such as by intimate data communication or control flow between those -subprograms and other parts of the work. - - The Corresponding Source need not include anything that users -can regenerate automatically from other parts of the Corresponding -Source. - - The Corresponding Source for a work in source code form is that -same work. - - 2. Basic Permissions. - - All rights granted under this License are granted for the term of -copyright on the Program, and are irrevocable provided the stated -conditions are met. This License explicitly affirms your unlimited -permission to run the unmodified Program. The output from running a -covered work is covered by this License only if the output, given its -content, constitutes a covered work. This License acknowledges your -rights of fair use or other equivalent, as provided by copyright law. - - You may make, run and propagate covered works that you do not -convey, without conditions so long as your license otherwise remains -in force. You may convey covered works to others for the sole purpose -of having them make modifications exclusively for you, or provide you -with facilities for running those works, provided that you comply with -the terms of this License in conveying all material for which you do -not control copyright. Those thus making or running the covered works -for you must do so exclusively on your behalf, under your direction -and control, on terms that prohibit them from making any copies of -your copyrighted material outside their relationship with you. - - Conveying under any other circumstances is permitted solely under -the conditions stated below. Sublicensing is not allowed; section 10 -makes it unnecessary. - - 3. Protecting Users' Legal Rights From Anti-Circumvention Law. - - No covered work shall be deemed part of an effective technological -measure under any applicable law fulfilling obligations under article -11 of the WIPO copyright treaty adopted on 20 December 1996, or -similar laws prohibiting or restricting circumvention of such -measures. - - When you convey a covered work, you waive any legal power to forbid -circumvention of technological measures to the extent such circumvention -is effected by exercising rights under this License with respect to -the covered work, and you disclaim any intention to limit operation or -modification of the work as a means of enforcing, against the work's -users, your or third parties' legal rights to forbid circumvention of -technological measures. - - 4. Conveying Verbatim Copies. - - You may convey verbatim copies of the Program's source code as you -receive it, in any medium, provided that you conspicuously and -appropriately publish on each copy an appropriate copyright notice; -keep intact all notices stating that this License and any -non-permissive terms added in accord with section 7 apply to the code; -keep intact all notices of the absence of any warranty; and give all -recipients a copy of this License along with the Program. - - You may charge any price or no price for each copy that you convey, -and you may offer support or warranty protection for a fee. - - 5. Conveying Modified Source Versions. - - You may convey a work based on the Program, or the modifications to -produce it from the Program, in the form of source code under the -terms of section 4, provided that you also meet all of these conditions: - - a) The work must carry prominent notices stating that you modified - it, and giving a relevant date. - - b) The work must carry prominent notices stating that it is - released under this License and any conditions added under section - 7. This requirement modifies the requirement in section 4 to - "keep intact all notices". - - c) You must license the entire work, as a whole, under this - License to anyone who comes into possession of a copy. This - License will therefore apply, along with any applicable section 7 - additional terms, to the whole of the work, and all its parts, - regardless of how they are packaged. This License gives no - permission to license the work in any other way, but it does not - invalidate such permission if you have separately received it. - - d) If the work has interactive user interfaces, each must display - Appropriate Legal Notices; however, if the Program has interactive - interfaces that do not display Appropriate Legal Notices, your - work need not make them do so. - - A compilation of a covered work with other separate and independent -works, which are not by their nature extensions of the covered work, -and which are not combined with it such as to form a larger program, -in or on a volume of a storage or distribution medium, is called an -"aggregate" if the compilation and its resulting copyright are not -used to limit the access or legal rights of the compilation's users -beyond what the individual works permit. Inclusion of a covered work -in an aggregate does not cause this License to apply to the other -parts of the aggregate. - - 6. Conveying Non-Source Forms. - - You may convey a covered work in object code form under the terms -of sections 4 and 5, provided that you also convey the -machine-readable Corresponding Source under the terms of this License, -in one of these ways: - - a) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by the - Corresponding Source fixed on a durable physical medium - customarily used for software interchange. - - b) Convey the object code in, or embodied in, a physical product - (including a physical distribution medium), accompanied by a - written offer, valid for at least three years and valid for as - long as you offer spare parts or customer support for that product - model, to give anyone who possesses the object code either (1) a - copy of the Corresponding Source for all the software in the - product that is covered by this License, on a durable physical - medium customarily used for software interchange, for a price no - more than your reasonable cost of physically performing this - conveying of source, or (2) access to copy the - Corresponding Source from a network server at no charge. - - c) Convey individual copies of the object code with a copy of the - written offer to provide the Corresponding Source. This - alternative is allowed only occasionally and noncommercially, and - only if you received the object code with such an offer, in accord - with subsection 6b. - - d) Convey the object code by offering access from a designated - place (gratis or for a charge), and offer equivalent access to the - Corresponding Source in the same way through the same place at no - further charge. You need not require recipients to copy the - Corresponding Source along with the object code. If the place to - copy the object code is a network server, the Corresponding Source - may be on a different server (operated by you or a third party) - that supports equivalent copying facilities, provided you maintain - clear directions next to the object code saying where to find the - Corresponding Source. Regardless of what server hosts the - Corresponding Source, you remain obligated to ensure that it is - available for as long as needed to satisfy these requirements. - - e) Convey the object code using peer-to-peer transmission, provided - you inform other peers where the object code and Corresponding - Source of the work are being offered to the general public at no - charge under subsection 6d. - - A separable portion of the object code, whose source code is excluded -from the Corresponding Source as a System Library, need not be -included in conveying the object code work. - - A "User Product" is either (1) a "consumer product", which means any -tangible personal property which is normally used for personal, family, -or household purposes, or (2) anything designed or sold for incorporation -into a dwelling. In determining whether a product is a consumer product, -doubtful cases shall be resolved in favor of coverage. For a particular -product received by a particular user, "normally used" refers to a -typical or common use of that class of product, regardless of the status -of the particular user or of the way in which the particular user -actually uses, or expects or is expected to use, the product. A product -is a consumer product regardless of whether the product has substantial -commercial, industrial or non-consumer uses, unless such uses represent -the only significant mode of use of the product. - - "Installation Information" for a User Product means any methods, -procedures, authorization keys, or other information required to install -and execute modified versions of a covered work in that User Product from -a modified version of its Corresponding Source. The information must -suffice to ensure that the continued functioning of the modified object -code is in no case prevented or interfered with solely because -modification has been made. - - If you convey an object code work under this section in, or with, or -specifically for use in, a User Product, and the conveying occurs as -part of a transaction in which the right of possession and use of the -User Product is transferred to the recipient in perpetuity or for a -fixed term (regardless of how the transaction is characterized), the -Corresponding Source conveyed under this section must be accompanied -by the Installation Information. But this requirement does not apply -if neither you nor any third party retains the ability to install -modified object code on the User Product (for example, the work has -been installed in ROM). - - The requirement to provide Installation Information does not include a -requirement to continue to provide support service, warranty, or updates -for a work that has been modified or installed by the recipient, or for -the User Product in which it has been modified or installed. Access to a -network may be denied when the modification itself materially and -adversely affects the operation of the network or violates the rules and -protocols for communication across the network. - - Corresponding Source conveyed, and Installation Information provided, -in accord with this section must be in a format that is publicly -documented (and with an implementation available to the public in -source code form), and must require no special password or key for -unpacking, reading or copying. - - 7. Additional Terms. - - "Additional permissions" are terms that supplement the terms of this -License by making exceptions from one or more of its conditions. -Additional permissions that are applicable to the entire Program shall -be treated as though they were included in this License, to the extent -that they are valid under applicable law. If additional permissions -apply only to part of the Program, that part may be used separately -under those permissions, but the entire Program remains governed by -this License without regard to the additional permissions. - - When you convey a copy of a covered work, you may at your option -remove any additional permissions from that copy, or from any part of -it. (Additional permissions may be written to require their own -removal in certain cases when you modify the work.) You may place -additional permissions on material, added by you to a covered work, -for which you have or can give appropriate copyright permission. - - Notwithstanding any other provision of this License, for material you -add to a covered work, you may (if authorized by the copyright holders of -that material) supplement the terms of this License with terms: - - a) Disclaiming warranty or limiting liability differently from the - terms of sections 15 and 16 of this License; or - - b) Requiring preservation of specified reasonable legal notices or - author attributions in that material or in the Appropriate Legal - Notices displayed by works containing it; or - - c) Prohibiting misrepresentation of the origin of that material, or - requiring that modified versions of such material be marked in - reasonable ways as different from the original version; or - - d) Limiting the use for publicity purposes of names of licensors or - authors of the material; or - - e) Declining to grant rights under trademark law for use of some - trade names, trademarks, or service marks; or - - f) Requiring indemnification of licensors and authors of that - material by anyone who conveys the material (or modified versions of - it) with contractual assumptions of liability to the recipient, for - any liability that these contractual assumptions directly impose on - those licensors and authors. - - All other non-permissive additional terms are considered "further -restrictions" within the meaning of section 10. If the Program as you -received it, or any part of it, contains a notice stating that it is -governed by this License along with a term that is a further -restriction, you may remove that term. If a license document contains -a further restriction but permits relicensing or conveying under this -License, you may add to a covered work material governed by the terms -of that license document, provided that the further restriction does -not survive such relicensing or conveying. - - If you add terms to a covered work in accord with this section, you -must place, in the relevant source files, a statement of the -additional terms that apply to those files, or a notice indicating -where to find the applicable terms. - - Additional terms, permissive or non-permissive, may be stated in the -form of a separately written license, or stated as exceptions; -the above requirements apply either way. - - 8. Termination. - - You may not propagate or modify a covered work except as expressly -provided under this License. Any attempt otherwise to propagate or -modify it is void, and will automatically terminate your rights under -this License (including any patent licenses granted under the third -paragraph of section 11). - - However, if you cease all violation of this License, then your -license from a particular copyright holder is reinstated (a) -provisionally, unless and until the copyright holder explicitly and -finally terminates your license, and (b) permanently, if the copyright -holder fails to notify you of the violation by some reasonable means -prior to 60 days after the cessation. - - Moreover, your license from a particular copyright holder is -reinstated permanently if the copyright holder notifies you of the -violation by some reasonable means, this is the first time you have -received notice of violation of this License (for any work) from that -copyright holder, and you cure the violation prior to 30 days after -your receipt of the notice. - - Termination of your rights under this section does not terminate the -licenses of parties who have received copies or rights from you under -this License. If your rights have been terminated and not permanently -reinstated, you do not qualify to receive new licenses for the same -material under section 10. - - 9. Acceptance Not Required for Having Copies. - - You are not required to accept this License in order to receive or -run a copy of the Program. Ancillary propagation of a covered work -occurring solely as a consequence of using peer-to-peer transmission -to receive a copy likewise does not require acceptance. However, -nothing other than this License grants you permission to propagate or -modify any covered work. These actions infringe copyright if you do -not accept this License. Therefore, by modifying or propagating a -covered work, you indicate your acceptance of this License to do so. - - 10. Automatic Licensing of Downstream Recipients. - - Each time you convey a covered work, the recipient automatically -receives a license from the original licensors, to run, modify and -propagate that work, subject to this License. You are not responsible -for enforcing compliance by third parties with this License. - - An "entity transaction" is a transaction transferring control of an -organization, or substantially all assets of one, or subdividing an -organization, or merging organizations. If propagation of a covered -work results from an entity transaction, each party to that -transaction who receives a copy of the work also receives whatever -licenses to the work the party's predecessor in interest had or could -give under the previous paragraph, plus a right to possession of the -Corresponding Source of the work from the predecessor in interest, if -the predecessor has it or can get it with reasonable efforts. - - You may not impose any further restrictions on the exercise of the -rights granted or affirmed under this License. For example, you may -not impose a license fee, royalty, or other charge for exercise of -rights granted under this License, and you may not initiate litigation -(including a cross-claim or counterclaim in a lawsuit) alleging that -any patent claim is infringed by making, using, selling, offering for -sale, or importing the Program or any portion of it. - - 11. Patents. - - A "contributor" is a copyright holder who authorizes use under this -License of the Program or a work on which the Program is based. The -work thus licensed is called the contributor's "contributor version". - - A contributor's "essential patent claims" are all patent claims -owned or controlled by the contributor, whether already acquired or -hereafter acquired, that would be infringed by some manner, permitted -by this License, of making, using, or selling its contributor version, -but do not include claims that would be infringed only as a -consequence of further modification of the contributor version. For -purposes of this definition, "control" includes the right to grant -patent sublicenses in a manner consistent with the requirements of -this License. - - Each contributor grants you a non-exclusive, worldwide, royalty-free -patent license under the contributor's essential patent claims, to -make, use, sell, offer for sale, import and otherwise run, modify and -propagate the contents of its contributor version. - - In the following three paragraphs, a "patent license" is any express -agreement or commitment, however denominated, not to enforce a patent -(such as an express permission to practice a patent or covenant not to -sue for patent infringement). To "grant" such a patent license to a -party means to make such an agreement or commitment not to enforce a -patent against the party. - - If you convey a covered work, knowingly relying on a patent license, -and the Corresponding Source of the work is not available for anyone -to copy, free of charge and under the terms of this License, through a -publicly available network server or other readily accessible means, -then you must either (1) cause the Corresponding Source to be so -available, or (2) arrange to deprive yourself of the benefit of the -patent license for this particular work, or (3) arrange, in a manner -consistent with the requirements of this License, to extend the patent -license to downstream recipients. "Knowingly relying" means you have -actual knowledge that, but for the patent license, your conveying the -covered work in a country, or your recipient's use of the covered work -in a country, would infringe one or more identifiable patents in that -country that you have reason to believe are valid. - - If, pursuant to or in connection with a single transaction or -arrangement, you convey, or propagate by procuring conveyance of, a -covered work, and grant a patent license to some of the parties -receiving the covered work authorizing them to use, propagate, modify -or convey a specific copy of the covered work, then the patent license -you grant is automatically extended to all recipients of the covered -work and works based on it. - - A patent license is "discriminatory" if it does not include within -the scope of its coverage, prohibits the exercise of, or is -conditioned on the non-exercise of one or more of the rights that are -specifically granted under this License. You may not convey a covered -work if you are a party to an arrangement with a third party that is -in the business of distributing software, under which you make payment -to the third party based on the extent of your activity of conveying -the work, and under which the third party grants, to any of the -parties who would receive the covered work from you, a discriminatory -patent license (a) in connection with copies of the covered work -conveyed by you (or copies made from those copies), or (b) primarily -for and in connection with specific products or compilations that -contain the covered work, unless you entered into that arrangement, -or that patent license was granted, prior to 28 March 2007. - - Nothing in this License shall be construed as excluding or limiting -any implied license or other defenses to infringement that may -otherwise be available to you under applicable patent law. - - 12. No Surrender of Others' Freedom. - - If conditions are imposed on you (whether by court order, agreement or -otherwise) that contradict the conditions of this License, they do not -excuse you from the conditions of this License. If you cannot convey a -covered work so as to satisfy simultaneously your obligations under this -License and any other pertinent obligations, then as a consequence you may -not convey it at all. For example, if you agree to terms that obligate you -to collect a royalty for further conveying from those to whom you convey -the Program, the only way you could satisfy both those terms and this -License would be to refrain entirely from conveying the Program. - - 13. Remote Network Interaction; Use with the GNU General Public License. - - Notwithstanding any other provision of this License, if you modify the -Program, your modified version must prominently offer all users -interacting with it remotely through a computer network (if your version -supports such interaction) an opportunity to receive the Corresponding -Source of your version by providing access to the Corresponding Source -from a network server at no charge, through some standard or customary -means of facilitating copying of software. This Corresponding Source -shall include the Corresponding Source for any work covered by version 3 -of the GNU General Public License that is incorporated pursuant to the -following paragraph. - - Notwithstanding any other provision of this License, you have -permission to link or combine any covered work with a work licensed -under version 3 of the GNU General Public License into a single -combined work, and to convey the resulting work. The terms of this -License will continue to apply to the part which is the covered work, -but the work with which it is combined will remain governed by version -3 of the GNU General Public License. - - 14. Revised Versions of this License. - - The Free Software Foundation may publish revised and/or new versions of -the GNU Affero General Public License from time to time. Such new versions -will be similar in spirit to the present version, but may differ in detail to -address new problems or concerns. - - Each version is given a distinguishing version number. If the -Program specifies that a certain numbered version of the GNU Affero General -Public License "or any later version" applies to it, you have the -option of following the terms and conditions either of that numbered -version or of any later version published by the Free Software -Foundation. If the Program does not specify a version number of the -GNU Affero General Public License, you may choose any version ever published -by the Free Software Foundation. - - If the Program specifies that a proxy can decide which future -versions of the GNU Affero General Public License can be used, that proxy's -public statement of acceptance of a version permanently authorizes you -to choose that version for the Program. - - Later license versions may give you additional or different -permissions. However, no additional obligations are imposed on any -author or copyright holder as a result of your choosing to follow a -later version. - - 15. Disclaimer of Warranty. - - THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY -APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT -HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY -OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, -THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR -PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM -IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF -ALL NECESSARY SERVICING, REPAIR OR CORRECTION. - - 16. Limitation of Liability. - - IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING -WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS -THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY -GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE -USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF -DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD -PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), -EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF -SUCH DAMAGES. - - 17. Interpretation of Sections 15 and 16. - - If the disclaimer of warranty and limitation of liability provided -above cannot be given local legal effect according to their terms, -reviewing courts shall apply local law that most closely approximates -an absolute waiver of all civil liability in connection with the -Program, unless a warranty or assumption of liability accompanies a -copy of the Program in return for a fee. - - END OF TERMS AND CONDITIONS - - How to Apply These Terms to Your New Programs - - If you develop a new program, and you want it to be of the greatest -possible use to the public, the best way to achieve this is to make it -free software which everyone can redistribute and change under these terms. - - To do so, attach the following notices to the program. It is safest -to attach them to the start of each source file to most effectively -state the exclusion of warranty; and each file should have at least -the "copyright" line and a pointer to where the full notice is found. - - - Copyright (C) 2022-2099 AstrBot Plugin Authors - - This program is free software: you can redistribute it and/or modify - it under the terms of the GNU Affero General Public License as published - by the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This program is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU Affero General Public License for more details. - - You should have received a copy of the GNU Affero General Public License - along with this program. If not, see . - -Also add information on how to contact you by electronic and paper mail. - - If your software can interact with users remotely through a computer -network, you should also make sure that it provides a way for users to -get its source. For example, if your program is a web application, its -interface could display a "Source" link that leads users to an archive -of the code. There are many ways you could offer source, and different -solutions will be better for different programs; see section 13 for the -specific requirements. - - You should also get your employer (if you work as a programmer) or school, -if any, to sign a "copyright disclaimer" for the program, if necessary. -For more information on this, and how to apply and follow the GNU AGPL, see -. diff --git a/README.md b/README.md index ae9c1f0..27b1a48 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,127 @@ -# helloworld +
-AstrBot 插件模板 +# QQ群日常分析插件 -A template plugin for AstrBot plugin feature -# 支持 +[![Plugin Version](https://img.shields.io/badge/Latest_Version-1.1.0-blue.svg?style=for-the-badge&color=76bad9)](https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis) +[![AstrBot](https://img.shields.io/badge/AstrBot-Plugin-ff69b4?style=for-the-badge)](https://github.com/AstrBotDevs/AstrBot) +[![License](https://img.shields.io/badge/License-AGPL3.0-green.svg?style=for-the-badge)](LICENSE) -[帮助文档](https://astrbot.app) +_✨ 一个基于AstrBot的智能群聊分析插件,能够生成精美的群聊日常分析报告。[灵感来源](https://github.com/LSTM-Kirigaya/openmcp-tutorial/tree/main/qq-group-summary)。 ✨_ + +count +
+ + +## 功能特色 + +### 🎯 智能分析 +- **话题分析**: 使用LLM智能提取群聊中的热门话题和讨论要点 +- **用户画像**: 基于聊天行为分析用户特征,分配个性化称号 +- **统计数据**: 全面的群聊活跃度和参与度统计 + +### 📊 可视化报告 +- **PDF报告**: 生成专业的PDF格式分析报告(推荐) +- **精美图片**: 生成美观的HTML+CSS可视化报告 +- **多种格式**: 支持图片和文本输出格式 +- **用户头像**: 自动获取QQ头像,让报告更加生动 +- **详细数据**: 包含消息统计、时间分布、关键词等 + +### 🛠️ 灵活配置 +- **群组管理**: 支持指定特定群组启用功能 +- **参数调节**: 可自定义分析天数、消息数量等参数 + +## 效果 +![效果图](./demo.jpg) + +## 使用方法 + +### 基础命令 + +#### 群分析 +``` +/群分析 [天数] +``` +- 分析群聊近期活动 +- 天数可选,默认为1天 +- 例如:`/群分析 3` 分析最近3天的群聊 + +#### 分析设置 +``` +/分析设置 [操作] +``` +- `enable`: 为当前群启用分析功能 +- `disable`: 为当前群禁用分析功能 +- `status`: 查看当前群的启用状态 +- 例如:`/分析设置 enable` + +### 配置选项 + +| 配置项 | 说明 | 默认值 | +|--------|------|--------| +| enabled_groups | 启用分析的QQ群列表 | [] (所有群) | +| max_messages | 最大分析消息数量 | 1000 | +| analysis_days | 默认分析天数 | 1 | +| output_format | 输出格式 (pdf/image/text) | pdf | +| require_admin | 仅管理员可用 | false | +| topic_analysis_enabled | 启用话题分析 | true | +| user_title_analysis_enabled | 启用用户称号分析 | true | + +## 分析内容 + +### 📊 基础统计 +- 消息总数 +- 参与人数 +- 总字符数 +- 表情数量 +- 最活跃时段 + +### 💬 话题分析 +- 自动提取3-5个主要话题 +- 识别话题参与者 +- 总结讨论要点和结论 + +### 🏆 用户称号 +基于用户行为特征分配称号: +- **水群小能手**: 发言频繁的活跃用户 +- **技术专家**: 经常讨论技术话题 +- **夜猫子**: 深夜活跃用户 +- **表情包批发商**: 经常发表情 +- **沉默终结者**: 经常开启话题 +- **剧作家**: 平均发言长度很长 +- **互动达人**: 经常回复他人 +- **KOL**: 群内意见领袖 + +### 🔥 数据洞察 +- 热门关键词统计 +- 用户活跃度排行 +- 时间分布分析 +- MBTI性格类型推测 + +## 安装要求 + +### 基础要求 +- 已配置LLM提供商(用于智能分析) +- QQ平台适配器 + + +## 注意事项 + +1. **性能考虑**: 大量消息分析可能消耗较多LLM tokens +2. **数据准确性**: 分析结果基于可获取的群聊记录,可能不完全准确 + +## 更新日志 + +### v1.0.0 +- 初始版本发布 +- 支持基础群聊分析功能 +- 智能话题和用户称号分析 +- 精美可视化报告生成 + +## 许可证 + +MIT License + +## 贡献 + +欢迎提交Issue和Pull Request来改进这个插件! diff --git a/_conf_schema.json b/_conf_schema.json new file mode 100644 index 0000000..602b04a --- /dev/null +++ b/_conf_schema.json @@ -0,0 +1,78 @@ +{ + "enabled_groups": { + "type": "list", + "description": "启用分析功能的QQ群列表", + "default": [], + "hint": "填入允许使用分析功能的QQ群号,为空则所有群都可使用", + "items": { + "type": "string" + } + }, + "max_messages": { + "type": "int", + "description": "最大分析消息数量", + "default": 1000, + "hint": "单次分析的最大消息条数,建议500-2000" + }, + "analysis_days": { + "type": "int", + "description": "默认分析天数", + "default": 1, + "hint": "默认分析最近几天的消息,建议1-7天" + }, + "auto_analysis_time": { + "type": "string", + "description": "自动分析时间", + "default": "22:00", + "hint": "每日自动分析的时间,格式HH:MM" + }, + "enable_auto_analysis": { + "type": "bool", + "description": "启用自动分析", + "default": false, + "hint": "是否在指定时间自动进行群聊分析,启用需要在群内手动触发一次分析 /分析设置 test ,否则获取不到实例,不会自动分析" + }, + "output_format": { + "type": "string", + "description": "输出格式", + "default": "image", + "hint": "分析报告的输出格式:image(图片)、text(文本)" + }, + + "min_messages_threshold": { + "type": "int", + "description": "最小消息数阈值", + "default": 50, + "hint": "进行分析所需的最小消息数量" + }, + "topic_analysis_enabled": { + "type": "bool", + "description": "启用话题分析", + "default": true, + "hint": "是否使用LLM进行智能话题分析" + }, + "user_title_analysis_enabled": { + "type": "bool", + "description": "启用用户称号分析", + "default": true, + "hint": "是否使用LLM进行用户称号分析" + }, + "max_topics": { + "type": "int", + "description": "最大话题数量", + "default": 5, + "hint": "分析报告中显示的最大话题数量" + }, + "max_user_titles": { + "type": "int", + "description": "最大用户称号数量", + "default": 8, + "hint": "分析报告中显示的最大用户称号数量" + }, + "max_query_rounds": { + "type": "int", + "description": "最大消息查询轮数", + "default": 35, + "hint": "⚠️ 警告:增加此值会导致大量Token消耗!默认值35轮(有一定消耗)。设置过高可能导致LLM费用激增" + } +} diff --git a/config.json b/config.json new file mode 100644 index 0000000..2297161 --- /dev/null +++ b/config.json @@ -0,0 +1,16 @@ +{ + "enabled_groups": [ + "936617127" + ], + "max_messages": 1000, + "analysis_days": 1, + "auto_analysis_time": "19:41", + "enable_auto_analysis": true, + "output_format": "image", + "min_messages_threshold": 50, + "topic_analysis_enabled": true, + "user_title_analysis_enabled": true, + "max_topics": 5, + "max_user_titles": 8, + "max_query_rounds": 50 +} \ No newline at end of file diff --git a/demo.jpg b/demo.jpg new file mode 100644 index 0000000..0580e29 Binary files /dev/null and b/demo.jpg differ diff --git a/main.py b/main.py index 99ca4be..dcc312d 100644 --- a/main.py +++ b/main.py @@ -1,24 +1,1993 @@ -from astrbot.api.event import filter, AstrMessageEvent, MessageEventResult +""" +QQ群日常分析插件 +基于群聊记录生成精美的日常分析报告,包含话题总结、用户画像、统计数据等 +""" + +import json +import asyncio +import base64 +import aiohttp +from datetime import datetime, timedelta +from typing import List, Dict, Optional +from pathlib import Path +from dataclasses import dataclass +from collections import defaultdict, Counter + +from astrbot.api.event import filter from astrbot.api.star import Context, Star, register -from astrbot.api import logger +from astrbot.api import logger, AstrBotConfig +from astrbot.core.platform.sources.aiocqhttp.aiocqhttp_message_event import AiocqhttpMessageEvent -@register("helloworld", "YourName", "一个简单的 Hello World 插件", "1.0.0") -class MyPlugin(Star): - def __init__(self, context: Context): + +@dataclass +class SummaryTopic: + """话题总结数据结构""" + topic: str + contributors: List[str] + detail: str + + +@dataclass +class UserTitle: + """用户称号数据结构""" + name: str + qq: int + title: str + mbti: str + reason: str + + +@dataclass +class GoldenQuote: + """群聊金句数据结构""" + content: str + sender: str + reason: str + +@dataclass +class GroupStatistics: + """群聊统计数据结构""" + message_count: int + total_characters: int + participant_count: int + most_active_period: str + golden_quotes: List[GoldenQuote] + emoji_count: int + + +@register( + "astrbot_qq_group_daily_analysis", + "SXP-Simon", + "QQ群日常分析插件 - 生成精美的群聊日常分析报告", + "1.0.0", + "https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis" +) +class QQGroupDailyAnalysis(Star): + def __init__(self, context: Context, config: AstrBotConfig): super().__init__(context) + self.config = config - async def initialize(self): - """可选择实现异步的插件初始化方法,当实例化该插件类之后会自动调用该方法。""" - - # 注册指令的装饰器。指令名为 helloworld。注册成功后,发送 `/helloworld` 就会触发这个指令,并回复 `你好, {user_name}!` - @filter.command("helloworld") - async def helloworld(self, event: AstrMessageEvent): - """这是一个 hello world 指令""" # 这是 handler 的描述,将会被解析方便用户了解插件内容。建议填写。 - user_name = event.get_sender_name() - message_str = event.message_str # 用户发的纯文本消息字符串 - message_chain = event.get_messages() # 用户所发的消息的消息链 # from astrbot.api.message_components import * - logger.info(message_chain) - yield event.plain_result(f"Hello, {user_name}, 你发了 {message_str}!") # 发送一条纯文本消息 + # 先从配置文件加载配置(如果存在) + self._load_config_from_file() - async def terminate(self): - """可选择实现异步的插件销毁方法,当插件被卸载/停用时会调用。""" + # 然后从AstrBot配置系统读取(优先级更高) + self.enabled_groups = config.get("enabled_groups", getattr(self, 'enabled_groups', [])) + self.max_messages = config.get("max_messages", getattr(self, 'max_messages', 1000)) + self.analysis_days = config.get("analysis_days", getattr(self, 'analysis_days', 1)) + self.auto_analysis_time = config.get("auto_analysis_time", getattr(self, 'auto_analysis_time', "09:00")) + self.enable_auto_analysis = config.get("enable_auto_analysis", getattr(self, 'enable_auto_analysis', False)) + self.output_format = config.get("output_format", getattr(self, 'output_format', "image")) + + self.min_messages_threshold = config.get("min_messages_threshold", getattr(self, 'min_messages_threshold', 50)) + self.topic_analysis_enabled = config.get("topic_analysis_enabled", getattr(self, 'topic_analysis_enabled', True)) + self.user_title_analysis_enabled = config.get("user_title_analysis_enabled", getattr(self, 'user_title_analysis_enabled', True)) + self.max_topics = config.get("max_topics", getattr(self, 'max_topics', 5)) + self.max_user_titles = config.get("max_user_titles", getattr(self, 'max_user_titles', 8)) + self.max_query_rounds = config.get("max_query_rounds", getattr(self, 'max_query_rounds', 35)) + + # 启动定时任务 + self.scheduler_task = None + self.bot_instance = None # 保存bot实例用于自动分析 + self.bot_qq_id = None # 保存机器人QQ号,用于过滤机器人消息 + + # 延迟启动定时任务,给系统时间初始化 + if self.enable_auto_analysis: + asyncio.create_task(self._delayed_start_scheduler()) + + logger.info("QQ群日常分析插件已初始化") + + @filter.command("群分析") + async def analyze_group_daily(self, event: AiocqhttpMessageEvent, days: Optional[int] = None): + """ + 分析群聊日常活动 + 用法: /群分析 [天数] + """ + if not isinstance(event, AiocqhttpMessageEvent): + yield event.plain_result("❌ 此功能仅支持QQ群聊") + return + + group_id = event.get_group_id() + if not group_id: + yield event.plain_result("❌ 请在群聊中使用此命令") + return + + # 保存bot实例用于自动分析 + self.bot_instance = event.bot + + # 获取机器人QQ号 + await self._get_bot_qq_id() + + # 检查群组权限 + if self.enabled_groups and group_id not in self.enabled_groups: + yield event.plain_result("❌ 此群未启用日常分析功能") + return + + + + # 设置分析天数 + analysis_days = days if days and 1 <= days <= 7 else self.analysis_days + + yield event.plain_result(f"🔍 开始分析群聊近{analysis_days}天的活动,请稍候...") + + # 调试:输出当前配置 + logger.info(f"当前输出格式配置: {self.output_format}") + + try: + # 获取群聊消息 + messages = await self._fetch_group_messages(event, analysis_days) + if not messages: + yield event.plain_result("❌ 未找到足够的群聊记录,请确保群内有足够的消息历史") + return + + # 检查消息数量是否足够分析 + if len(messages) < self.min_messages_threshold: + yield event.plain_result(f"❌ 消息数量不足({len(messages)}条),至少需要{self.min_messages_threshold}条消息才能进行有效分析") + return + + yield event.plain_result(f"📊 已获取{len(messages)}条消息,正在进行智能分析...") + + # 进行分析 + analysis_result = await self._analyze_messages(messages, group_id) + + # 检查分析结果 + if not analysis_result or not analysis_result.get("statistics"): + yield event.plain_result("❌ 分析过程中出现错误,请稍后重试") + return + + # 生成报告 + if self.output_format == "image": + image_url = await self._generate_image_report(analysis_result, group_id) + if image_url: + yield event.image_result(image_url) + else: + # 如果图片生成失败,回退到文本报告 + logger.warning("图片报告生成失败,回退到文本报告") + text_report = await self._generate_text_report(analysis_result) + yield event.plain_result(f"⚠️ 图片报告生成失败,以下是文本版本:\n\n{text_report}") + else: + text_report = await self._generate_text_report(analysis_result) + yield event.plain_result(text_report) + + except Exception as e: + logger.error(f"群分析失败: {e}", exc_info=True) + yield event.plain_result(f"❌ 分析失败: {str(e)}。请检查网络连接和LLM配置,或联系管理员") + + @filter.command("分析设置") + async def analysis_settings(self, event: AiocqhttpMessageEvent, action: str = "status"): + """ + 管理分析设置 + 用法: /分析设置 [enable|disable|status|reload|test] + - enable: 启用当前群的分析功能 + - disable: 禁用当前群的分析功能 + - status: 查看当前状态 + - reload: 重新加载配置并重启定时任务 + - test: 测试自动分析功能 + """ + if not isinstance(event, AiocqhttpMessageEvent): + yield event.plain_result("❌ 此功能仅支持QQ群聊") + return + + group_id = event.get_group_id() + if not group_id: + yield event.plain_result("❌ 请在群聊中使用此命令") + return + + # 检查管理员权限 + if not await self._is_admin(event): + yield event.plain_result("❌ 仅群管理员可以修改设置") + return + + if action == "enable": + if group_id not in self.enabled_groups: + self.enabled_groups.append(group_id) + await self._save_config() + yield event.plain_result("✅ 已为当前群启用日常分析功能") + + # 重新加载配置并启动定时任务 + await self._reload_config_and_restart_scheduler() + else: + yield event.plain_result("ℹ️ 当前群已启用日常分析功能") + + elif action == "disable": + if group_id in self.enabled_groups: + self.enabled_groups.remove(group_id) + await self._save_config() + yield event.plain_result("✅ 已为当前群禁用日常分析功能") + else: + yield event.plain_result("ℹ️ 当前群未启用日常分析功能") + + elif action == "reload": + # 重新加载配置 + await self._reload_config_and_restart_scheduler() + yield event.plain_result("✅ 已重新加载配置并重启定时任务") + + elif action == "test": + # 测试自动分析功能 + if group_id not in self.enabled_groups: + yield event.plain_result("❌ 请先启用当前群的分析功能") + return + + yield event.plain_result("🧪 开始测试自动分析功能...") + + # 保存bot实例 + self.bot_instance = event.bot + + # 执行自动分析 + try: + await self._perform_auto_analysis_for_group(group_id) + yield event.plain_result("✅ 自动分析测试完成,请查看群消息") + except Exception as e: + yield event.plain_result(f"❌ 自动分析测试失败: {str(e)}") + + else: # status + status = "已启用" if group_id in self.enabled_groups else "未启用" + auto_status = "已启用" if self.enable_auto_analysis else "未启用" + scheduler_status = "运行中" if hasattr(self, 'scheduler_task') and self.scheduler_task and not self.scheduler_task.done() else "未运行" + + yield event.plain_result(f"""📊 当前群分析功能状态: +• 群分析功能: {status} +• 自动分析: {auto_status} ({self.auto_analysis_time}) +• 定时任务: {scheduler_status} +• 输出格式: {self.output_format} +• 最小消息数: {self.min_messages_threshold} +• 最大查询轮数: {self.max_query_rounds} + +💡 可用命令: enable, disable, status, reload, test""") + + async def _get_bot_qq_id(self): + """获取机器人QQ号""" + try: + if self.bot_instance and not self.bot_qq_id: + login_info = await self.bot_instance.api.call_action("get_login_info") + self.bot_qq_id = str(login_info.get("user_id", "")) + logger.info(f"获取到机器人QQ号: {self.bot_qq_id}") + except Exception as e: + logger.error(f"获取机器人QQ号失败: {e}") + + async def _is_admin(self, event: AiocqhttpMessageEvent) -> bool: + """检查是否为管理员 - 已简化为允许所有用户""" + # 允许所有用户使用设置功能 + return True + + async def _fetch_group_messages_unified(self, client, group_id: str, days: int) -> List[Dict]: + """统一的群聊消息获取方法""" + try: + if not client or not group_id: + logger.error(f"群 {group_id} 无效的客户端或群组ID") + return [] + + # 计算时间范围 + end_time = datetime.now() + start_time = end_time - timedelta(days=days) + + messages = [] + message_seq = 0 + query_rounds = 0 + max_rounds = self.max_query_rounds # 从配置读取最大查询轮数 + consecutive_failures = 0 + max_failures = 3 # 最大连续失败次数 + + logger.info(f"开始获取群 {group_id} 近 {days} 天的消息记录") + logger.info(f"时间范围: {start_time.strftime('%Y-%m-%d %H:%M:%S')} 到 {end_time.strftime('%Y-%m-%d %H:%M:%S')}") + + while len(messages) < self.max_messages and query_rounds < max_rounds: + try: + payloads = { + "group_id": group_id, + "message_seq": message_seq, + "count": 200, + "reverseOrder": True, + } + + result = await client.api.call_action("get_group_msg_history", **payloads) + + if not result or "messages" not in result: + logger.warning(f"群 {group_id} API返回无效结果: {result}") + consecutive_failures += 1 + if consecutive_failures >= max_failures: + break + continue + + round_messages = result.get("messages", []) + + if not round_messages: + logger.info(f"群 {group_id} 没有更多消息,结束获取") + break + + # 重置失败计数 + consecutive_failures = 0 + + # 过滤时间范围内的消息 + valid_messages_in_round = 0 + oldest_msg_time = None + + for msg in round_messages: + try: + msg_time = datetime.fromtimestamp(msg.get("time", 0)) + oldest_msg_time = msg_time # 记录最老的消息时间 + + # 过滤掉机器人自己的消息 + sender_id = str(msg.get("sender", {}).get("user_id", "")) + if self.bot_qq_id and sender_id == self.bot_qq_id: + continue + + if msg_time >= start_time and msg_time <= end_time: + messages.append(msg) + valid_messages_in_round += 1 + except Exception as msg_error: + logger.warning(f"群 {group_id} 处理单条消息失败: {msg_error}") + continue + + # 如果最老的消息时间已经超出范围,停止获取 + if oldest_msg_time and oldest_msg_time < start_time: + logger.info(f"群 {group_id} 已获取到时间范围外的消息,停止获取。共获取 {len(messages)} 条消息") + break + + if valid_messages_in_round == 0: + logger.warning(f"群 {group_id} 本轮未获取到有效消息") + break + + message_seq = round_messages[0]["message_id"] + query_rounds += 1 + + # 添加延迟避免请求过快 + if query_rounds % 5 == 0: + await asyncio.sleep(0.5) + + except Exception as e: + logger.error(f"群 {group_id} 获取消息失败 (第{query_rounds+1}轮): {e}") + consecutive_failures += 1 + if consecutive_failures >= max_failures: + logger.error(f"群 {group_id} 连续失败 {max_failures} 次,停止获取") + break + await asyncio.sleep(1) # 失败后等待1秒再重试 + + logger.info(f"群 {group_id} 消息获取完成,共获取 {len(messages)} 条消息,查询轮数: {query_rounds}") + return messages + + except Exception as e: + logger.error(f"群 {group_id} 获取群聊消息记录失败: {e}", exc_info=True) + return [] + + async def _fetch_group_messages(self, event: AiocqhttpMessageEvent, days: int) -> List[Dict]: + """获取群聊消息记录(手动分析)""" + return await self._fetch_group_messages_unified(event.bot, event.get_group_id(), days) + + async def _analyze_messages(self, messages: List[Dict], group_id: str) -> Dict: + """分析消息内容""" + # 基础统计 + stats = self._calculate_statistics(messages) + + # 用户活跃度分析 + user_analysis = self._analyze_users(messages) + + # 话题分析(根据配置决定是否启用) + topics = [] + if self.topic_analysis_enabled: + topics = await self._analyze_topics(messages) + + # 用户称号分析(根据配置决定是否启用) + user_titles = [] + if self.user_title_analysis_enabled: + user_titles = await self._analyze_user_titles(messages, user_analysis) + + # 群聊金句分析 + golden_quotes = await self._analyze_golden_quotes(messages) + stats.golden_quotes = golden_quotes + + return { + "group_id": group_id, + "analysis_time": datetime.now().isoformat(), + "statistics": stats, + "user_analysis": user_analysis, + "topics": topics, + "user_titles": user_titles, + "message_count": len(messages) + } + + def _calculate_statistics(self, messages: List[Dict]) -> GroupStatistics: + """计算基础统计数据""" + total_chars = 0 + participants = set() + hour_counts = defaultdict(int) + emoji_count = 0 + + for msg in messages: + sender_id = str(msg.get("sender", {}).get("user_id", "")) + participants.add(sender_id) + + # 统计时间分布 + msg_time = datetime.fromtimestamp(msg.get("time", 0)) + hour_counts[msg_time.hour] += 1 + + # 处理消息内容 + for content in msg.get("message", []): + if content.get("type") == "text": + text = content.get("data", {}).get("text", "") + total_chars += len(text) + elif content.get("type") == "face": + emoji_count += 1 + + # 找出最活跃时段 + most_active_hour = max(hour_counts.items(), key=lambda x: x[1])[0] if hour_counts else 0 + most_active_period = f"{most_active_hour:02d}:00-{(most_active_hour+1)%24:02d}:00" + + return GroupStatistics( + message_count=len(messages), + total_characters=total_chars, + participant_count=len(participants), + most_active_period=most_active_period, + golden_quotes=[], # 将在后续LLM分析中填充 + emoji_count=emoji_count + ) + + def _analyze_users(self, messages: List[Dict]) -> Dict[str, Dict]: + """分析用户活跃度""" + user_stats = defaultdict(lambda: { + "message_count": 0, + "char_count": 0, + "emoji_count": 0, + "nickname": "", + "hours": defaultdict(int), + "reply_count": 0 + }) + + for msg in messages: + sender = msg.get("sender", {}) + user_id = str(sender.get("user_id", "")) + nickname = sender.get("nickname", "") or sender.get("card", "") + + user_stats[user_id]["message_count"] += 1 + user_stats[user_id]["nickname"] = nickname + + # 统计时间分布 + msg_time = datetime.fromtimestamp(msg.get("time", 0)) + user_stats[user_id]["hours"][msg_time.hour] += 1 + + # 处理消息内容 + for content in msg.get("message", []): + if content.get("type") == "text": + text = content.get("data", {}).get("text", "") + user_stats[user_id]["char_count"] += len(text) + elif content.get("type") == "face": + user_stats[user_id]["emoji_count"] += 1 + elif content.get("type") == "reply": + user_stats[user_id]["reply_count"] += 1 + + return dict(user_stats) + + async def _get_user_avatar(self, user_id: str) -> Optional[str]: + """获取用户头像的base64编码""" + try: + avatar_url = f"https://q4.qlogo.cn/headimg_dl?dst_uin={user_id}&spec=640" + async with aiohttp.ClientSession() as client: + response = await client.get(avatar_url) + response.raise_for_status() + avatar_data = await response.read() + # 转换为base64编码 + avatar_base64 = base64.b64encode(avatar_data).decode('utf-8') + return f"data:image/jpeg;base64,{avatar_base64}" + except Exception as e: + logger.error(f"获取用户头像失败 {user_id}: {e}") + return None + + async def _analyze_topics(self, messages: List[Dict]) -> List[SummaryTopic]: + """使用LLM分析话题""" + try: + # 提取文本消息 + text_messages = [] + for msg in messages: + sender = msg.get("sender", {}) + nickname = sender.get("nickname", "") or sender.get("card", "") + msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M") + + for content in msg.get("message", []): + if content.get("type") == "text": + text = content.get("data", {}).get("text", "").strip() + if text and len(text) > 2: # 过滤太短的消息 + text_messages.append({ + "sender": nickname, + "time": msg_time, + "content": text + }) + + if not text_messages: + return [] + + # 限制消息数量以避免token过多 + if len(text_messages) > 100: + # 均匀采样 + step = len(text_messages) // 100 + text_messages = text_messages[::step] + + # 构建LLM提示词 + messages_text = "\n".join([ + f"[{msg['time']}] {msg['sender']}: {msg['content']}" + for msg in text_messages + ]) + + prompt = f""" +请分析以下群聊记录,提取出3-5个主要话题。对于每个话题,请提供: +1. 话题名称(简洁明了) +2. 主要参与者(最多3人) +3. 话题详细描述(包含关键信息和结论) + +群聊记录: +{messages_text} + +请以JSON格式返回,格式如下: +[ + {{ + "topic": "话题名称", + "contributors": ["参与者1", "参与者2"], + "detail": "详细描述话题内容、讨论要点和结论" + }} +] +""" + + # 调用LLM + provider = self.context.get_using_provider() + if not provider: + logger.warning("未配置LLM提供商,跳过话题分析") + return [] + + response = await provider.text_chat( + prompt=prompt, + max_tokens=2000, + temperature=0.3 + ) + + # 解析响应 + if hasattr(response, 'completion_text'): + result_text = response.completion_text + else: + result_text = str(response) + + # 尝试解析JSON + try: + import re + json_match = re.search(r'\[.*\]', result_text, re.DOTALL) + if json_match: + topics_data = json.loads(json_match.group()) + return [SummaryTopic(**topic) for topic in topics_data[:5]] + except: + pass + + return [] + + except Exception as e: + logger.error(f"话题分析失败: {e}") + return [] + + async def _analyze_user_titles(self, messages: List[Dict], user_analysis: Dict) -> List[UserTitle]: + """使用LLM分析用户称号""" + try: + # 准备用户数据 + user_summaries = [] + for user_id, stats in user_analysis.items(): + if stats["message_count"] < 5: # 过滤活跃度太低的用户 + continue + + # 分析用户特征 + night_messages = sum(stats["hours"][h] for h in range(0, 6)) + day_messages = stats["message_count"] - night_messages + avg_chars = stats["char_count"] / stats["message_count"] if stats["message_count"] > 0 else 0 + + user_summaries.append({ + "name": stats["nickname"], + "qq": int(user_id), + "message_count": stats["message_count"], + "avg_chars": round(avg_chars, 1), + "emoji_ratio": round(stats["emoji_count"] / stats["message_count"], 2), + "night_ratio": round(night_messages / stats["message_count"], 2), + "reply_ratio": round(stats["reply_count"] / stats["message_count"], 2) + }) + + if not user_summaries: + return [] + + # 按消息数量排序,取前8名 + user_summaries.sort(key=lambda x: x["message_count"], reverse=True) + user_summaries = user_summaries[:8] + + # 构建LLM提示词 + users_text = "\n".join([ + f"- {user['name']} (QQ:{user['qq']}): " + f"发言{user['message_count']}条, 平均{user['avg_chars']}字, " + f"表情比例{user['emoji_ratio']}, 夜间发言比例{user['night_ratio']}, " + f"回复比例{user['reply_ratio']}" + for user in user_summaries + ]) + + prompt = f""" +请为以下群友分配合适的称号和MBTI类型。每个人只能有一个称号,每个称号只能给一个人。 + +可选称号: +- 水群小能手: 发言频繁但内容轻松的人 +- 技术专家: 经常讨论技术话题的人 +- 夜猫子: 经常在深夜发言的人 +- 表情包批发商: 经常发表情的人 +- 沉默终结者: 经常开启话题的人 +- 剧作家: 平均发言长度很长的人 +- KOL: 在群里很有影响力的人 +- 互动达人: 经常回复别人的人 + +用户数据: +{users_text} + +请以JSON格式返回,格式如下: +[ + {{ + "name": "用户名", + "qq": 123456789, + "title": "称号", + "mbti": "MBTI类型", + "reason": "获得此称号的原因" + }} +] +""" + + # 调用LLM + provider = self.context.get_using_provider() + if not provider: + logger.warning("未配置LLM提供商,跳过用户称号分析") + return [] + + response = await provider.text_chat( + prompt=prompt, + max_tokens=1500, + temperature=0.5 + ) + + # 解析响应 + if hasattr(response, 'completion_text'): + result_text = response.completion_text + else: + result_text = str(response) + + # 尝试解析JSON + try: + import re + json_match = re.search(r'\[.*\]', result_text, re.DOTALL) + if json_match: + titles_data = json.loads(json_match.group()) + return [UserTitle(**title) for title in titles_data] + except: + pass + + return [] + + except Exception as e: + logger.error(f"用户称号分析失败: {e}") + return [] + + async def _analyze_golden_quotes(self, messages: List[Dict]) -> List[GoldenQuote]: + """使用LLM分析群聊金句""" + try: + # 提取有趣的文本消息 + interesting_messages = [] + for msg in messages: + sender = msg.get("sender", {}) + nickname = sender.get("nickname", "") or sender.get("card", "") + msg_time = datetime.fromtimestamp(msg.get("time", 0)).strftime("%H:%M") + + for content in msg.get("message", []): + if content.get("type") == "text": + text = content.get("data", {}).get("text", "").strip() + # 过滤长度适中、可能有趣的消息 + if 5 <= len(text) <= 100 and not text.startswith(("http", "www")): + interesting_messages.append({ + "sender": nickname, + "time": msg_time, + "content": text + }) + + if not interesting_messages: + return [] + + # 限制消息数量以避免token过多 + if len(interesting_messages) > 50: + # 均匀采样 + step = len(interesting_messages) // 50 + interesting_messages = interesting_messages[::step] + + # 构建LLM提示词 + messages_text = "\n".join([ + f"[{msg['time']}] {msg['sender']}: {msg['content']}" + for msg in interesting_messages + ]) + + prompt = f""" +请从以下群聊记录中挑选出3-5句最有趣、最有意思的"金句"。这些金句可以是: +- 幽默搞笑的发言 +- 富有哲理的话语 +- 意外的神回复 +- 有创意的表达 +- 让人印象深刻的句子 + +对于每个金句,请提供: +1. 原文内容 +2. 发言人昵称 +3. 选择理由(为什么觉得这句话有趣) + +群聊记录: +{messages_text} + +请以JSON格式返回,格式如下: +[ + {{ + "content": "金句原文", + "sender": "发言人昵称", + "reason": "选择这句话的理由" + }} +] +""" + + # 调用LLM + provider = self.context.get_using_provider() + if not provider: + logger.warning("未配置LLM提供商,跳过金句分析") + return [] + + response = await provider.text_chat( + prompt=prompt, + max_tokens=1500, + temperature=0.7 + ) + + # 解析响应 + if hasattr(response, 'completion_text'): + result_text = response.completion_text + else: + result_text = str(response) + + # 尝试解析JSON + try: + import re + json_match = re.search(r'\[.*\]', result_text, re.DOTALL) + if json_match: + quotes_data = json.loads(json_match.group()) + return [GoldenQuote(**quote) for quote in quotes_data[:5]] + except: + pass + + return [] + + except Exception as e: + logger.error(f"金句分析失败: {e}") + return [] + + async def _generate_image_report(self, analysis_result: Dict, group_id: str) -> Optional[str]: + """生成图片格式的分析报告""" + try: + # 准备渲染数据 + render_payload = await self._prepare_render_data(analysis_result) + + # 使用AstrBot内置的HTML渲染服务 + image_url = await self.html_render(self._get_html_template(), render_payload) + return image_url + + except Exception as e: + logger.error(f"生成图片报告失败: {e}") + return None + + + + async def _prepare_render_data(self, analysis_result: Dict) -> Dict: + """准备渲染数据""" + stats = analysis_result["statistics"] + topics = analysis_result["topics"] + user_titles = analysis_result["user_titles"] + + # 构建话题HTML + topics_html = "" + for i, topic in enumerate(topics[:self.max_topics], 1): + contributors_str = "、".join(topic.contributors) + topics_html += f""" +
+
+ {i} + {topic.topic} +
+
参与者: {contributors_str}
+
{topic.detail}
+
+ """ + + # 构建用户称号HTML(包含头像) + titles_html = "" + for title in user_titles[:self.max_user_titles]: + # 获取用户头像 + avatar_data = await self._get_user_avatar(str(title.qq)) + avatar_html = f'头像' if avatar_data else '
👤
' + + titles_html += f""" +
+ +
{title.reason}
+
+ """ + + # 构建金句HTML + quotes_html = "" + for quote in stats.golden_quotes[:5]: + quotes_html += f""" +
+
"{quote.content}"
+
—— {quote.sender}
+
{quote.reason}
+
+ """ + + # 返回扁平化的渲染数据 + return { + "current_date": datetime.now().strftime('%Y年%m月%d日'), + "current_datetime": datetime.now().strftime('%Y-%m-%d %H:%M:%S'), + "message_count": stats.message_count, + "participant_count": stats.participant_count, + "total_characters": stats.total_characters, + "emoji_count": stats.emoji_count, + "most_active_period": stats.most_active_period, + "topics_html": topics_html, + "titles_html": titles_html, + "quotes_html": quotes_html + } + + def _get_html_template(self) -> str: + """获取HTML模板""" + return """ + + + + + + 群聊日常分析报告 + + + + +
+
+

📊 群聊日常分析报告

+
{{ current_date }}
+
+ +
+
+

📈 基础统计

+
+
+
{{ message_count }}
+
消息总数
+
+
+
{{ participant_count }}
+
参与人数
+
+
+
{{ total_characters }}
+
总字符数
+
+
+
{{ emoji_count }}
+
表情数量
+
+
+ +
+
{{ most_active_period }}
+
最活跃时段
+
+
+ +
+

💬 热门话题

+ {{ topics_html | safe }} +
+ +
+

🏆 群友称号

+ {{ titles_html | safe }} +
+ +
+

💬 群聊金句

+ {{ quotes_html | safe }} +
+
+ + +
+ + + """ + + async def _create_html_report(self, analysis_result: Dict) -> str: + """创建HTML报告内容""" + stats = analysis_result["statistics"] + topics = analysis_result["topics"] + user_titles = analysis_result["user_titles"] + + # 构建话题HTML + topics_html = "" + for i, topic in enumerate(topics[:self.max_topics], 1): + contributors_str = "、".join(topic.contributors) + topics_html += f""" +
+
+ {i} + {topic.topic} +
+
参与者: {contributors_str}
+
{topic.detail}
+
+ """ + + # 构建用户称号HTML + titles_html = "" + for title in user_titles[:self.max_user_titles]: + titles_html += f""" +
+ +
{title.reason}
+
+ """ + + # 构建金句HTML + quotes_html = "" + for i, quote in enumerate(stats.golden_quotes[:5], 1): + quotes_html += f""" +
+
"{quote.content}"
+
—— {quote.sender}
+
{quote.reason}
+
+ """ + + # HTML模板 + html_template = f""" + + + + + + 群聊日常分析报告 + + + +
+
+

🎯 群聊日常分析报告

+
{datetime.now().strftime('%Y年%m月%d日')}
+
+ +
+
+

📊 基础统计

+
+
+
{stats.message_count}
+
消息总数
+
+
+
{stats.participant_count}
+
参与人数
+
+
+
{stats.total_characters}
+
总字符数
+
+
+
{stats.emoji_count}
+
表情数量
+
+
+
+
+
{stats.most_active_period}
+
最活跃时段
+
+
+
+ +
+

💬 热门话题

+ {topics_html} +
+ +
+

🏆 群友称号

+ {titles_html} +
+ +
+

💬 群聊金句

+ {quotes_html} +
+
+
+ + + """ + + return html_template + + async def _generate_text_report(self, analysis_result: Dict) -> str: + """生成文本格式的分析报告""" + stats = analysis_result["statistics"] + topics = analysis_result["topics"] + user_titles = analysis_result["user_titles"] + + report = f""" +🎯 群聊日常分析报告 +📅 {datetime.now().strftime('%Y年%m月%d日')} + +📊 基础统计 +• 消息总数: {stats.message_count} +• 参与人数: {stats.participant_count} +• 总字符数: {stats.total_characters} +• 表情数量: {stats.emoji_count} +• 最活跃时段: {stats.most_active_period} + +💬 热门话题 +""" + + for i, topic in enumerate(topics[:self.max_topics], 1): + contributors_str = "、".join(topic.contributors) + report += f"{i}. {topic.topic}\n" + report += f" 参与者: {contributors_str}\n" + report += f" {topic.detail}\n\n" + + report += "🏆 群友称号\n" + for title in user_titles[:self.max_user_titles]: + report += f"• {title.name} - {title.title} ({title.mbti})\n" + report += f" {title.reason}\n\n" + + report += "💬 群聊金句\n" + for i, quote in enumerate(stats.golden_quotes[:5], 1): + report += f"{i}. \"{quote.content}\" —— {quote.sender}\n" + report += f" {quote.reason}\n\n" + + return report + + + + async def _save_config(self): + """保存配置到文件""" + try: + config_file = Path("data/plugins/astrbot-qq-group-daily-analysis/config.json") + config_file.parent.mkdir(parents=True, exist_ok=True) + + config_data = { + "enabled_groups": self.enabled_groups, + "max_messages": self.max_messages, + "analysis_days": self.analysis_days, + "auto_analysis_time": self.auto_analysis_time, + "enable_auto_analysis": self.enable_auto_analysis, + "output_format": self.output_format, + + "min_messages_threshold": self.min_messages_threshold, + "topic_analysis_enabled": self.topic_analysis_enabled, + "user_title_analysis_enabled": self.user_title_analysis_enabled, + "max_topics": self.max_topics, + "max_user_titles": self.max_user_titles, + "max_query_rounds": self.max_query_rounds + } + + with open(config_file, 'w', encoding='utf-8') as f: + json.dump(config_data, f, ensure_ascii=False, indent=2) + + except Exception as e: + logger.error(f"保存配置失败: {e}") + + def _load_config_from_file(self): + """从配置文件加载配置""" + try: + config_file = Path("data/plugins/astrbot-qq-group-daily-analysis/config.json") + if config_file.exists(): + with open(config_file, 'r', encoding='utf-8') as f: + file_config = json.load(f) + + # 设置默认值 + self.enabled_groups = file_config.get("enabled_groups", []) + self.max_messages = file_config.get("max_messages", 1000) + self.analysis_days = file_config.get("analysis_days", 1) + self.auto_analysis_time = file_config.get("auto_analysis_time", "09:00") + self.enable_auto_analysis = file_config.get("enable_auto_analysis", False) + self.output_format = file_config.get("output_format", "image") + + self.min_messages_threshold = file_config.get("min_messages_threshold", 50) + self.topic_analysis_enabled = file_config.get("topic_analysis_enabled", True) + self.user_title_analysis_enabled = file_config.get("user_title_analysis_enabled", True) + self.max_topics = file_config.get("max_topics", 5) + self.max_user_titles = file_config.get("max_user_titles", 8) + self.max_query_rounds = file_config.get("max_query_rounds", 35) + + logger.info(f"从配置文件加载配置成功,自动分析: {self.enable_auto_analysis}, 时间: {self.auto_analysis_time}") + else: + # 设置默认值 + self.enabled_groups = [] + self.max_messages = 1000 + self.analysis_days = 1 + self.auto_analysis_time = "09:00" + self.enable_auto_analysis = False + self.output_format = "image" + + self.min_messages_threshold = 50 + self.topic_analysis_enabled = True + self.user_title_analysis_enabled = True + self.max_topics = 5 + self.max_user_titles = 8 + self.max_query_rounds = 35 + + except Exception as e: + logger.error(f"加载配置文件失败: {e}") + # 设置默认值 + self.enabled_groups = [] + self.max_messages = 1000 + self.analysis_days = 1 + self.auto_analysis_time = "09:00" + self.enable_auto_analysis = False + self.output_format = "image" + + self.min_messages_threshold = 50 + self.topic_analysis_enabled = True + self.user_title_analysis_enabled = True + self.max_topics = 5 + self.max_user_titles = 8 + self.max_query_rounds = 35 + + async def _reload_config_and_restart_scheduler(self): + """重新加载配置并重启调度器""" + try: + # 重新加载配置 + self._load_config_from_file() + logger.info(f"重新加载配置: 自动分析={self.enable_auto_analysis}") + + # 停止现有的调度器 + if hasattr(self, 'scheduler_task') and self.scheduler_task and not self.scheduler_task.done(): + self.scheduler_task.cancel() + logger.info("已停止现有的定时任务") + + # 如果启用了自动分析,启动新的调度器 + if self.enable_auto_analysis: + self.scheduler_task = asyncio.create_task(self._start_scheduler()) + logger.info("已启动新的定时任务") + + except Exception as e: + logger.error(f"重新加载配置失败: {e}") + + async def _start_scheduler(self): + """启动定时任务调度器""" + logger.info(f"启动定时任务调度器,自动分析时间: {self.auto_analysis_time}") + + while True: + try: + now = datetime.now() + target_time = datetime.strptime(self.auto_analysis_time, "%H:%M").replace( + year=now.year, month=now.month, day=now.day + ) + + # 如果今天的目标时间已过,设置为明天 + if now >= target_time: + target_time += timedelta(days=1) + + # 计算等待时间 + wait_seconds = (target_time - now).total_seconds() + logger.info(f"定时分析将在 {target_time.strftime('%Y-%m-%d %H:%M:%S')} 执行,等待 {wait_seconds:.0f} 秒") + + # 等待到目标时间 + await asyncio.sleep(wait_seconds) + + # 执行自动分析 + if self.enable_auto_analysis: + logger.info("开始执行定时分析") + await self._run_auto_analysis() + else: + logger.info("自动分析已禁用,跳过执行") + break + + except Exception as e: + logger.error(f"定时任务调度器错误: {e}") + # 等待5分钟后重试 + await asyncio.sleep(300) + + async def _run_auto_analysis(self): + """执行自动分析""" + try: + logger.info("开始执行自动群聊分析") + + # 为每个启用的群执行分析 + for group_id in self.enabled_groups: + try: + logger.info(f"为群 {group_id} 执行自动分析") + + # 这里需要模拟一个事件对象来调用分析功能 + # 由于自动分析没有真实的用户事件,我们直接调用内部方法 + await self._perform_auto_analysis_for_group(group_id) + + except Exception as e: + logger.error(f"群 {group_id} 自动分析失败: {e}") + + except Exception as e: + logger.error(f"自动分析执行失败: {e}") + + async def _perform_auto_analysis_for_group(self, group_id: str): + """为指定群执行自动分析""" + try: + # 尝试获取bot实例 + if not self.bot_instance: + self.bot_instance = await self._get_bot_instance() + + if not self.bot_instance: + logger.warning(f"群 {group_id} 自动分析跳过:未获取到bot实例,请先手动触发一次分析") + return + + # 确保有机器人QQ号 + if not self.bot_qq_id: + await self._get_bot_qq_id() + + logger.info(f"开始为群 {group_id} 执行自动分析") + + # 获取群聊消息 + messages = await self._fetch_group_messages_for_auto(group_id) + if not messages: + logger.warning(f"群 {group_id} 未获取到足够的消息记录") + return + + # 检查消息数量 + if len(messages) < self.min_messages_threshold: + logger.warning(f"群 {group_id} 消息数量不足({len(messages)}条),跳过分析") + return + + logger.info(f"群 {group_id} 获取到 {len(messages)} 条消息,开始分析") + + # 进行分析 + analysis_result = await self._analyze_messages(messages, group_id) + if not analysis_result: + logger.error(f"群 {group_id} 分析失败") + return + + # 生成报告 + if self.output_format == "image": + image_url = await self._generate_image_report(analysis_result, group_id) + if image_url: + # 发送分析报告到群 + await self._send_auto_analysis_result(group_id, image_url) + logger.info(f"群 {group_id} 自动分析完成,已发送图片报告") + else: + logger.error(f"群 {group_id} 图片报告生成失败") + else: + text_report = await self._generate_text_report(analysis_result) + await self._send_auto_analysis_text(group_id, text_report) + logger.info(f"群 {group_id} 自动分析完成,已发送文本报告") + + except Exception as e: + logger.error(f"群 {group_id} 自动分析执行失败: {e}", exc_info=True) + + async def _fetch_group_messages_for_auto(self, group_id: str) -> List[Dict]: + """为自动分析获取群聊消息(使用统一方法)""" + if not self.bot_instance: + logger.error(f"群 {group_id} 获取消息失败:缺少bot实例") + return [] + + return await self._fetch_group_messages_unified(self.bot_instance, group_id, self.analysis_days) + + async def _send_auto_analysis_result(self, group_id: str, image_url: str): + """发送自动分析的图片结果到群""" + try: + if not self.bot_instance: + return + + # 发送图片消息到群 + await self.bot_instance.api.call_action( + "send_group_msg", + group_id=group_id, + message=[{ + "type": "text", + "data": {"text": "📊 每日群聊分析报告已生成:"} + }, { + "type": "image", + "data": {"url": image_url} + }] + ) + + except Exception as e: + logger.error(f"发送自动分析结果到群 {group_id} 失败: {e}") + + async def _send_auto_analysis_text(self, group_id: str, text_report: str): + """发送自动分析的文本结果到群""" + try: + if not self.bot_instance: + return + + # 发送文本消息到群 + await self.bot_instance.api.call_action( + "send_group_msg", + group_id=group_id, + message=f"📊 每日群聊分析报告:\n\n{text_report}" + ) + + except Exception as e: + logger.error(f"发送自动分析文本到群 {group_id} 失败: {e}") + + async def _get_bot_instance(self): + """从Context获取bot实例""" + try: + # 如果已经有保存的实例,直接返回 + if self.bot_instance: + return self.bot_instance + + logger.info("尝试获取bot实例...") + + # 简化的获取逻辑,尝试常见的几种方式 + if hasattr(self.context, 'get_platforms') and callable(self.context.get_platforms): + platforms = self.context.get_platforms() + for platform in platforms: + if hasattr(platform, 'bot') and platform.bot: + logger.info(f"从平台获取到bot实例") + return platform.bot + + # 尝试从context的platforms属性获取 + if hasattr(self.context, 'platforms') and self.context.platforms: + for platform in self.context.platforms: + if hasattr(platform, 'bot') and platform.bot: + logger.info(f"从平台列表获取到bot实例") + return platform.bot + + logger.info("暂时无法获取bot实例,等待用户手动触发分析") + return None + + except Exception as e: + logger.error(f"获取bot实例失败: {e}") + return None + + async def _delayed_start_scheduler(self): + """延迟启动调度器,给系统时间初始化""" + try: + # 等待10秒让系统完全初始化 + await asyncio.sleep(10) + + # 尝试获取bot实例 + self.bot_instance = await self._get_bot_instance() + + if self.bot_instance: + logger.info("成功获取bot实例,启动定时任务") + # 获取机器人QQ号 + await self._get_bot_qq_id() + else: + logger.info("暂时未获取到bot实例,定时任务仍会启动。首次手动触发分析后将自动获取bot实例") + + # 启动调度器 + self.scheduler_task = asyncio.create_task(self._start_scheduler()) + + except Exception as e: + logger.error(f"延迟启动调度器失败: {e}") diff --git a/metadata.yaml b/metadata.yaml index c8d3f94..bbd461d 100644 --- a/metadata.yaml +++ b/metadata.yaml @@ -1,5 +1,18 @@ -name: helloworld # 这是你的插件的唯一识别名。 -desc: 这是 AstrBot 的默认插件,支持关键词回复。 # 插件简短描述 -version: v1.3 # 插件版本号。格式:v1.1.1 或者 v1.1 -author: Soulter # 作者 -repo: https://github.com/Soulter/helloworld # 插件的仓库地址 +name: astrbot_qq_group_daily_analysis # 这是你的插件的唯一识别名。 +desc: QQ群日常分析插件 - 生成精美的群聊分析报告,支持话题分析、用户称号、群聊金句等功能 # 插件简短描述 +help: | # 插件的帮助信息 + 智能分析群聊活动,生成精美的日常分析报告 + + 主要功能: + - 话题分析和总结 + - 用户称号分配 + - 群聊金句识别 + - 可视化报告 + - 定时自动分析 + + 命令: + /群分析 [天数] - 分析群聊活动 + /分析设置 [操作] - 管理设置(enable/disable/status/test) +version: v1.0.0 # 插件版本号。格式:v1.1.1 或者 v1.1 +author: SXP-Simon # 作者 +repo: https://github.com/SXP-Simon/astrbot-qq-group-daily-analysis # 插件的仓库地址 \ No newline at end of file