├── .ignore ├── LICENSE ├── README.md ├── manifest.json ├── manifest_schema.json └── src ├── assests ├── markdown │ ├── re.png │ └── repeat.png └── plus_one.svg ├── main.js ├── preload.js ├── renderer.js └── utils ├── backendLogUtils.js └── rendererUtils.js /.ignore: -------------------------------------------------------------------------------- 1 | .idea -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Eclipse Public License - v 2.0 2 | 3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE 4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION 5 | OF THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 6 | 7 | 1. DEFINITIONS 8 | 9 | "Contribution" means: 10 | 11 | a) in the case of the initial Contributor, the initial content 12 | Distributed under this Agreement, and 13 | 14 | b) in the case of each subsequent Contributor: 15 | i) changes to the Program, and 16 | ii) additions to the Program; 17 | where such changes and/or additions to the Program originate from 18 | and are Distributed by that particular Contributor. A Contribution 19 | "originates" from a Contributor if it was added to the Program by 20 | such Contributor itself or anyone acting on such Contributor's behalf. 21 | Contributions do not include changes or additions to the Program that 22 | are not Modified Works. 23 | 24 | "Contributor" means any person or entity that Distributes the Program. 25 | 26 | "Licensed Patents" mean patent claims licensable by a Contributor which 27 | are necessarily infringed by the use or sale of its Contribution alone 28 | or when combined with the Program. 29 | 30 | "Program" means the Contributions Distributed in accordance with this 31 | Agreement. 32 | 33 | "Recipient" means anyone who receives the Program under this Agreement 34 | or any Secondary License (as applicable), including Contributors. 35 | 36 | "Derivative Works" shall mean any work, whether in Source Code or other 37 | form, that is based on (or derived from) the Program and for which the 38 | editorial revisions, annotations, elaborations, or other modifications 39 | represent, as a whole, an original work of authorship. 40 | 41 | "Modified Works" shall mean any work in Source Code or other form that 42 | results from an addition to, deletion from, or modification of the 43 | contents of the Program, including, for purposes of clarity any new file 44 | in Source Code form that contains any contents of the Program. Modified 45 | Works shall not include works that contain only declarations, 46 | interfaces, types, classes, structures, or files of the Program solely 47 | in each case in order to link to, bind by name, or subclass the Program 48 | or Modified Works thereof. 49 | 50 | "Distribute" means the acts of a) distributing or b) making available 51 | in any manner that enables the transfer of a copy. 52 | 53 | "Source Code" means the form of a Program preferred for making 54 | modifications, including but not limited to software source code, 55 | documentation source, and configuration files. 56 | 57 | "Secondary License" means either the GNU General Public License, 58 | Version 2.0, or any later versions of that license, including any 59 | exceptions or additional permissions as identified by the initial 60 | Contributor. 61 | 62 | 2. GRANT OF RIGHTS 63 | 64 | a) Subject to the terms of this Agreement, each Contributor hereby 65 | grants Recipient a non-exclusive, worldwide, royalty-free copyright 66 | license to reproduce, prepare Derivative Works of, publicly display, 67 | publicly perform, Distribute and sublicense the Contribution of such 68 | Contributor, if any, and such Derivative Works. 69 | 70 | b) Subject to the terms of this Agreement, each Contributor hereby 71 | grants Recipient a non-exclusive, worldwide, royalty-free patent 72 | license under Licensed Patents to make, use, sell, offer to sell, 73 | import and otherwise transfer the Contribution of such Contributor, 74 | if any, in Source Code or other form. This patent license shall 75 | apply to the combination of the Contribution and the Program if, at 76 | the time the Contribution is added by the Contributor, such addition 77 | of the Contribution causes such combination to be covered by the 78 | Licensed Patents. The patent license shall not apply to any other 79 | combinations which include the Contribution. No hardware per se is 80 | licensed hereunder. 81 | 82 | c) Recipient understands that although each Contributor grants the 83 | licenses to its Contributions set forth herein, no assurances are 84 | provided by any Contributor that the Program does not infringe the 85 | patent or other intellectual property rights of any other entity. 86 | Each Contributor disclaims any liability to Recipient for claims 87 | brought by any other entity based on infringement of intellectual 88 | property rights or otherwise. As a condition to exercising the 89 | rights and licenses granted hereunder, each Recipient hereby 90 | assumes sole responsibility to secure any other intellectual 91 | property rights needed, if any. For example, if a third party 92 | patent license is required to allow Recipient to Distribute the 93 | Program, it is Recipient's responsibility to acquire that license 94 | before distributing the Program. 95 | 96 | d) Each Contributor represents that to its knowledge it has 97 | sufficient copyright rights in its Contribution, if any, to grant 98 | the copyright license set forth in this Agreement. 99 | 100 | e) Notwithstanding the terms of any Secondary License, no 101 | Contributor makes additional grants to any Recipient (other than 102 | those set forth in this Agreement) as a result of such Recipient's 103 | receipt of the Program under the terms of a Secondary License 104 | (if permitted under the terms of Section 3). 105 | 106 | 3. REQUIREMENTS 107 | 108 | 3.1 If a Contributor Distributes the Program in any form, then: 109 | 110 | a) the Program must also be made available as Source Code, in 111 | accordance with section 3.2, and the Contributor must accompany 112 | the Program with a statement that the Source Code for the Program 113 | is available under this Agreement, and informs Recipients how to 114 | obtain it in a reasonable manner on or through a medium customarily 115 | used for software exchange; and 116 | 117 | b) the Contributor may Distribute the Program under a license 118 | different than this Agreement, provided that such license: 119 | i) effectively disclaims on behalf of all other Contributors all 120 | warranties and conditions, express and implied, including 121 | warranties or conditions of title and non-infringement, and 122 | implied warranties or conditions of merchantability and fitness 123 | for a particular purpose; 124 | 125 | ii) effectively excludes on behalf of all other Contributors all 126 | liability for damages, including direct, indirect, special, 127 | incidental and consequential damages, such as lost profits; 128 | 129 | iii) does not attempt to limit or alter the recipients' rights 130 | in the Source Code under section 3.2; and 131 | 132 | iv) requires any subsequent distribution of the Program by any 133 | party to be under a license that satisfies the requirements 134 | of this section 3. 135 | 136 | 3.2 When the Program is Distributed as Source Code: 137 | 138 | a) it must be made available under this Agreement, or if the 139 | Program (i) is combined with other material in a separate file or 140 | files made available under a Secondary License, and (ii) the initial 141 | Contributor attached to the Source Code the notice described in 142 | Exhibit A of this Agreement, then the Program may be made available 143 | under the terms of such Secondary Licenses, and 144 | 145 | b) a copy of this Agreement must be included with each copy of 146 | the Program. 147 | 148 | 3.3 Contributors may not remove or alter any copyright, patent, 149 | trademark, attribution notices, disclaimers of warranty, or limitations 150 | of liability ("notices") contained within the Program from any copy of 151 | the Program which they Distribute, provided that Contributors may add 152 | their own appropriate notices. 153 | 154 | 4. COMMERCIAL DISTRIBUTION 155 | 156 | Commercial distributors of software may accept certain responsibilities 157 | with respect to end users, business partners and the like. While this 158 | license is intended to facilitate the commercial use of the Program, 159 | the Contributor who includes the Program in a commercial product 160 | offering should do so in a manner which does not create potential 161 | liability for other Contributors. Therefore, if a Contributor includes 162 | the Program in a commercial product offering, such Contributor 163 | ("Commercial Contributor") hereby agrees to defend and indemnify every 164 | other Contributor ("Indemnified Contributor") against any losses, 165 | damages and costs (collectively "Losses") arising from claims, lawsuits 166 | and other legal actions brought by a third party against the Indemnified 167 | Contributor to the extent caused by the acts or omissions of such 168 | Commercial Contributor in connection with its distribution of the Program 169 | in a commercial product offering. The obligations in this section do not 170 | apply to any claims or Losses relating to any actual or alleged 171 | intellectual property infringement. In order to qualify, an Indemnified 172 | Contributor must: a) promptly notify the Commercial Contributor in 173 | writing of such claim, and b) allow the Commercial Contributor to control, 174 | and cooperate with the Commercial Contributor in, the defense and any 175 | related settlement negotiations. The Indemnified Contributor may 176 | participate in any such claim at its own expense. 177 | 178 | For example, a Contributor might include the Program in a commercial 179 | product offering, Product X. That Contributor is then a Commercial 180 | Contributor. If that Commercial Contributor then makes performance 181 | claims, or offers warranties related to Product X, those performance 182 | claims and warranties are such Commercial Contributor's responsibility 183 | alone. Under this section, the Commercial Contributor would have to 184 | defend claims against the other Contributors related to those performance 185 | claims and warranties, and if a court requires any other Contributor to 186 | pay any damages as a result, the Commercial Contributor must pay 187 | those damages. 188 | 189 | 5. NO WARRANTY 190 | 191 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 192 | PERMITTED BY APPLICABLE LAW, THE PROGRAM IS PROVIDED ON AN "AS IS" 193 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER EXPRESS OR 194 | IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR CONDITIONS OF 195 | TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A PARTICULAR 196 | PURPOSE. Each Recipient is solely responsible for determining the 197 | appropriateness of using and distributing the Program and assumes all 198 | risks associated with its exercise of rights under this Agreement, 199 | including but not limited to the risks and costs of program errors, 200 | compliance with applicable laws, damage to or loss of data, programs 201 | or equipment, and unavailability or interruption of operations. 202 | 203 | 6. DISCLAIMER OF LIABILITY 204 | 205 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, AND TO THE EXTENT 206 | PERMITTED BY APPLICABLE LAW, NEITHER RECIPIENT NOR ANY CONTRIBUTORS 207 | SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, 208 | EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION LOST 209 | PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 210 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 211 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 212 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE 213 | POSSIBILITY OF SUCH DAMAGES. 214 | 215 | 7. GENERAL 216 | 217 | If any provision of this Agreement is invalid or unenforceable under 218 | applicable law, it shall not affect the validity or enforceability of 219 | the remainder of the terms of this Agreement, and without further 220 | action by the parties hereto, such provision shall be reformed to the 221 | minimum extent necessary to make such provision valid and enforceable. 222 | 223 | If Recipient institutes patent litigation against any entity 224 | (including a cross-claim or counterclaim in a lawsuit) alleging that the 225 | Program itself (excluding combinations of the Program with other software 226 | or hardware) infringes such Recipient's patent(s), then such Recipient's 227 | rights granted under Section 2(b) shall terminate as of the date such 228 | litigation is filed. 229 | 230 | All Recipient's rights under this Agreement shall terminate if it 231 | fails to comply with any of the material terms or conditions of this 232 | Agreement and does not cure such failure in a reasonable period of 233 | time after becoming aware of such noncompliance. If all Recipient's 234 | rights under this Agreement terminate, Recipient agrees to cease use 235 | and distribution of the Program as soon as reasonably practicable. 236 | However, Recipient's obligations under this Agreement and any licenses 237 | granted by Recipient relating to the Program shall continue and survive. 238 | 239 | Everyone is permitted to copy and distribute copies of this Agreement, 240 | but in order to avoid inconsistency the Agreement is copyrighted and 241 | may only be modified in the following manner. The Agreement Steward 242 | reserves the right to publish new versions (including revisions) of 243 | this Agreement from time to time. No one other than the Agreement 244 | Steward has the right to modify this Agreement. The Eclipse Foundation 245 | is the initial Agreement Steward. The Eclipse Foundation may assign the 246 | responsibility to serve as the Agreement Steward to a suitable separate 247 | entity. Each new version of the Agreement will be given a distinguishing 248 | version number. The Program (including Contributions) may always be 249 | Distributed subject to the version of the Agreement under which it was 250 | received. In addition, after a new version of the Agreement is published, 251 | Contributor may elect to Distribute the Program (including its 252 | Contributions) under the new version. 253 | 254 | Except as expressly stated in Sections 2(a) and 2(b) above, Recipient 255 | receives no rights or licenses to the intellectual property of any 256 | Contributor under this Agreement, whether expressly, by implication, 257 | estoppel or otherwise. All rights in the Program not expressly granted 258 | under this Agreement are reserved. Nothing in this Agreement is intended 259 | to be enforceable by any entity that is not a Contributor or Recipient. 260 | No third-party beneficiary rights are created under this Agreement. 261 | 262 | Exhibit A - Form of Secondary Licenses Notice 263 | 264 | "This Source Code may also be made available under the following 265 | Secondary Licenses when the conditions for such availability set forth 266 | in the Eclipse Public License, v. 2.0 are satisfied: {name license(s), 267 | version(s), and exceptions or additional permissions here}." 268 | 269 | Simply including a copy of this Agreement, including this Exhibit A 270 | is not sufficient to license the Source Code under Secondary Licenses. 271 | 272 | If it is not possible or desirable to put the notice in a particular 273 | file, then You may include the notice in a location (such as a LICENSE 274 | file in a relevant directory) where a recipient would be likely to 275 | look for such a notice. 276 | 277 | You may add additional accurate notices of copyright ownership. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## 实现接近原生手机QQ的+1功能 2 | 3 | 4 | 5 | 6 | 7 |

8 | 9 | [![Contributors][contributors-shield]][contributors-url] 10 | [![Forks][forks-shield]][forks-url] 11 | [![Stargazers][stars-shield]][stars-url] 12 | [![Issues][issues-shield]][issues-url] 13 | [![MIT License][license-shield]][license-url] 14 | [![LinkedIn][linkedin-shield]][linkedin-url] 15 | 16 |

17 | 18 | 19 | 20 |

21 | 22 | Logo 23 | 24 |

Echo Message

25 |

26 | 查看Demo 27 | · 28 | 报告Bug 29 | · 30 | 提出新特性 31 |

32 |

33 | 34 |

35 | 36 | Reeeee 37 | 38 |

39 |

"喜欢如 40 | 落幕后放映机繁忙空转 41 | 辗转反侧多少夜 42 | 才能够将是非明辨" 43 |

44 | 45 | ## 目录 46 | 47 | - [Echo Message](#projectname) 48 | - [目录](#目录) 49 | - [上手指南](#上手指南) 50 | - [开发前的配置要求](#开发前的配置要求) 51 | - [**插件安装步骤**](#安装步骤) 52 | - [**使用方法**](#使用方法) 53 | - [版权说明](#版权说明) 54 | - [鸣谢](#鸣谢) 55 | 56 | ## 上手指南 57 | 58 | ###### 开发前的配置要求 59 | 60 | 1. 请安装LiteLoader,项目地址为 https://github.com/LiteLoaderQQNT/LiteLoaderQQNT 61 | 62 | 2. 下面是社区开发的LiteLoader快捷安装脚本项目,新手请直接下载下面的即可。 63 | https://github.com/Mzdyl/LiteLoaderQQNT_Install/ 64 | 65 | #### 此处提供两个链接: 66 | 67 | - [LiteLoader QQNT 下载地址](https://github.com/LiteLoaderQQNT/LiteLoaderQQNT/releases) 68 | - [LiteLoader QQNT 安装脚本](https://github.com/Mzdyl/LiteLoaderQQNT_Install/releases) 69 | 70 | #### 对于网络不好的用户,可以使用以下直链进行下载: 71 | 72 | - [LiteLoader QQNT 安装器直链][LL-installer-link] 73 | - [QQ9.9.15.26909_x64 版本直链][oldQQ-download-link] 74 | 75 | ###### 安装步骤 76 | 77 | 1. 下载release中的最新版本 78 | 2. 解压后把整个解压出来的文件夹拖动到Plugins目录下即可。 79 | 3. 重启QQ,LiteLoader会自动加载插件。 80 | 81 | ### 注意,如果使用了上面的install脚本安装liteloader,QQ设置会自带插件商店,在插件商店里可以一键安装本插件。 82 | 83 | # 使用方法 84 | 85 | ### 1.复读你都不会?点+1就行,跟手机QQ操作一样 86 | 87 |

88 | 89 | 聊天界面 90 | 91 |

92 | 93 | ### 2.鼠标移动到任意消息上也会出现+1标记,更好地做复读机 94 | 95 | 96 | ## 版权说明 97 | 98 | 该项目签署了EPL-2.0 license 99 | 授权许可,详情请参阅 [LICENSE](https://github.com/WJZ-P/LiteLoaderQQNT-Encrypt-Chat/blob/main/LICENSE) 100 | 101 | ## 鸣谢 102 | 103 | - [LiteLoader QQNT](https://github.com/LiteLoaderQQNT/LiteLoaderQQNT?tab=readme-ov-file) 104 | 105 | 106 | ## 如果您喜欢本项目,请给我点个⭐吧(๑>◡<๑)! 107 | 108 | ## ⭐ Star 历史 109 | 110 | [![Stargazers over time](https://starchart.cc/WJZ-P/LiteLoaderQQNT-Echo-Message.svg?variant=adaptive)](https://starchart.cc/WJZ-P/LiteLoaderQQNT-Echo-Message) 111 | 112 | 113 | [your-project-path]:WJZ-P/LiteLoaderQQNT-Echo-Message 114 | 115 | [contributors-shield]: https://img.shields.io/github/contributors/WJZ-P/LiteLoaderQQNT-Echo-Message.svg?style=flat-square 116 | 117 | [contributors-url]: https://github.com/WJZ-P/LiteLoaderQQNT-Echo-Message/graphs/contributors 118 | 119 | [forks-shield]: https://img.shields.io/github/forks/WJZ-P/LiteLoaderQQNT-Echo-Message.svg?style=flat-square 120 | 121 | [forks-url]: https://github.com/WJZ-P/LiteLoaderQQNT-Echo-Message/network/members 122 | 123 | [stars-shield]: https://img.shields.io/github/stars/WJZ-P/LiteLoaderQQNT-Echo-Message.svg?style=flat-square 124 | 125 | [stars-url]: https://github.com/WJZ-P/LiteLoaderQQNT-Echo-Message/stargazers 126 | 127 | [issues-shield]: https://img.shields.io/github/issues/WJZ-P/LiteLoaderQQNT-Echo-Message.svg?style=flat-square 128 | 129 | [issues-url]: https://img.shields.io/github/issues/WJZ-P/LiteLoaderQQNT-Echo-Message.svg 130 | 131 | [license-shield]: https://img.shields.io/github/license/WJZ-P/LiteLoaderQQNT-Echo-Message.svg?style=flat-square 132 | 133 | [license-url]: https://github.com/WJZ-P/LiteLoaderQQNT-Echo-Message/blob/main/LICENSE 134 | 135 | [linkedin-shield]: https://img.shields.io/badge/-LinkedIn-black.svg?style=flat-square&logo=linkedin&colorB=555 136 | 137 | [linkedin-url]: https://linkedin.com/in/shaojintian 138 | 139 | [oldQQ-download-link]:https://ats-prod.oss-accelerate.aliyuncs.com/91ff35732557ef7d8415050a85973801 140 | 141 | [LL-installer-link]:https://ats-prod.oss-accelerate.aliyuncs.com/18734247705198dcb594916e8ba1facc 142 | 143 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./manifest_schema.json", 3 | "manifest_version": 4, 4 | "type": "extension", 5 | "name": "Echo Message", 6 | "slug": "plugin_template", 7 | "description": "\"我要+1!\"实现接近原生手机QQ的聊天+1功能(๑>◡<๑)", 8 | "version": "1.1.1", 9 | "icon": "./src/assests/plus_one.svg", 10 | "thumb": "./src/assests/plus_one.svg", 11 | "authors": [ 12 | { 13 | "name": "WJZ_P", 14 | "link": "https://github.com/WJZ-P" 15 | } 16 | ], 17 | "platform": [ 18 | "win32", 19 | "linux", 20 | "darwin" 21 | ], 22 | "injects": { 23 | "renderer": "./src/renderer.js", 24 | "main": "./src/main.js", 25 | "preload": "./src/preload.js" 26 | }, 27 | "repository": { 28 | "repo": "WJZ-P/Echo-Message", 29 | "branch": "main", 30 | "release": { 31 | "tag": "v1.1.1", 32 | "file": "Echo-Message-1.1.1.zip" 33 | } 34 | } 35 | } -------------------------------------------------------------------------------- /manifest_schema.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json-schema.org/draft-07/schema#", 3 | "title": "LiteLoaderQQNT Plugin manifest", 4 | "properties": { 5 | "manifest_version": { 6 | "type": "integer", 7 | "enum": [ 8 | 1, 9 | 2, 10 | 3, 11 | 4 12 | ], 13 | "default": 4, 14 | "title": "Manifest 版本", 15 | "description": "当前版本为 4" 16 | }, 17 | "type": { 18 | "type": "string", 19 | "enum": [ 20 | "extension", 21 | "theme", 22 | "framework" 23 | ], 24 | "title": "插件类型" 25 | }, 26 | "name": { 27 | "type": "string", 28 | "title": "插件名称" 29 | }, 30 | "slug": { 31 | "type": "string", 32 | "title": "插件标识", 33 | "description": "此插件在代码内的唯一标识符" 34 | }, 35 | "description": { 36 | "type": "string", 37 | "title": "插件描述" 38 | }, 39 | "icon": { 40 | "title": "插件图标", 41 | "description": "请填写插件图标的相对路径或 null", 42 | "anyOf": [ 43 | { 44 | "type": "string" 45 | }, 46 | { 47 | "type": "null" 48 | } 49 | ] 50 | }, 51 | "thumb": { 52 | "title": "插件小图标", 53 | "description": "请填写插件小图标的相对路径或 null (展示在设置界面边栏)", 54 | "anyOf": [ 55 | { 56 | "type": "string" 57 | }, 58 | { 59 | "type": "null" 60 | } 61 | ] 62 | }, 63 | "version": { 64 | "type": "string", 65 | "title": "版本号", 66 | "pattern": "^([0-9]+)\\.([0-9]+)\\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*))?(?:\\+[0-9A-Za-z-]+)?$" 67 | }, 68 | "authors": { 69 | "title": "作者们", 70 | "type": "array", 71 | "items": { 72 | "type": "object", 73 | "properties": { 74 | "name": { 75 | "title": "作者名字", 76 | "type": "string" 77 | }, 78 | "link": { 79 | "title": "作者链接", 80 | "type": "string", 81 | "format": "uri" 82 | } 83 | }, 84 | "required": [ 85 | "name", 86 | "link" 87 | ] 88 | } 89 | }, 90 | "dependencies": { 91 | "title": "插件依赖", 92 | "description": "填写依赖的 slug", 93 | "type": "array", 94 | "items": { 95 | "type": "string" 96 | } 97 | }, 98 | "platform": { 99 | "title": "插件支持的系统平台", 100 | "description": "- Windows: win32 \n- Linux: linux \n- MacOS: darwin", 101 | "type": "array", 102 | "items": { 103 | "type": "string", 104 | "enum": [ 105 | "win32", 106 | "linux", 107 | "darwin" 108 | ], 109 | "uniqueItems": true 110 | } 111 | }, 112 | "injects": { 113 | "title": "要注入的脚本", 114 | "type": "object", 115 | "properties": { 116 | "renderer": { 117 | "title": "渲染进程", 118 | "type": "string" 119 | }, 120 | "main": { 121 | "title": "主进程", 122 | "type": "string" 123 | }, 124 | "preload": { 125 | "title": "预加载脚本", 126 | "type": "string" 127 | } 128 | } 129 | }, 130 | "repository": { 131 | "title": "插件仓库信息", 132 | "type": "object", 133 | "properties": { 134 | "repo": { 135 | "title": "仓库短地址", 136 | "description": "GitHub 仓库地址,格式为 {user}/{repo}", 137 | "type": "string" 138 | }, 139 | "branch": { 140 | "title": "分支名称", 141 | "type": "string" 142 | }, 143 | "release": { 144 | "type": "object", 145 | "properties": { 146 | "tag": { 147 | "title": "tag 名称", 148 | "description": "不推荐写 latest", 149 | "type": "string" 150 | }, 151 | "file": { 152 | "title": "release 内的文件名", 153 | "description": "不填会直接下载 tag 的源码", 154 | "type": "string" 155 | } 156 | }, 157 | "required": [ 158 | "tag" 159 | ] 160 | } 161 | }, 162 | "required": [ 163 | "repo", 164 | "branch" 165 | ] 166 | } 167 | }, 168 | "required": [ 169 | "manifest_version", 170 | "name", 171 | "slug", 172 | "description", 173 | "version", 174 | "authors", 175 | "platform" 176 | ] 177 | } 178 | -------------------------------------------------------------------------------- /src/assests/markdown/re.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WJZ-P/LiteLoaderQQNT-Echo-Message/cd5b13b2bd24ae921985d5bbea8eb0c7fa84bf23/src/assests/markdown/re.png -------------------------------------------------------------------------------- /src/assests/markdown/repeat.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WJZ-P/LiteLoaderQQNT-Echo-Message/cd5b13b2bd24ae921985d5bbea8eb0c7fa84bf23/src/assests/markdown/repeat.png -------------------------------------------------------------------------------- /src/assests/plus_one.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/main.js: -------------------------------------------------------------------------------- 1 | // 运行在 Electron 主进程 下的插件入口 2 | const {pluginLog} = require("./utils/backendLogUtils.js"); 3 | 4 | // 创建窗口时触发 5 | exports.onBrowserWindowCreated = (window) => { 6 | try { 7 | // window 为 Electron 的 BrowserWindow 实例 8 | 9 | } catch (e) { 10 | pluginLog(e) 11 | } 12 | } 13 | 14 | 15 | // 用户登录时触发 16 | exports.onLogin = (uid) => { 17 | // uid 为 账号 的 字符串 标识 18 | } -------------------------------------------------------------------------------- /src/preload.js: -------------------------------------------------------------------------------- 1 | // Electron 主进程 与 渲染进程 交互的桥梁 2 | const {contextBridge, ipcRenderer} = require("electron"); 3 | 4 | 5 | // 在window对象下导出只读对象 6 | contextBridge.exposeInMainWorld("echo_message", { 7 | // 框架中 IPC 通信标识格式为 "组织名.项目名.方法名" 8 | // 格式不重要,只需要确保标识唯一即可,定义成什么都行 9 | invokeNative: (eventName, cmdName, registered, webContentId, ...args) => invokeNative(eventName, cmdName, registered, webContentId, ...args), 10 | startRender:() =>ipcRenderer.send("LiteLoader.echo_message.startRender"), 11 | }); 12 | 13 | 14 | /** 15 | * 调用一个qq底层函数,并返回函数返回值。来自 16 | * https://github.com/xtaw/LiteLoaderQQNT-Euphony/blob/master/src/main/preload.js 17 | * 18 | * @param { String } eventName 函数事件名。 19 | * @param { String } cmdName 函数名。 20 | * @param { Boolean } registered 函数是否为一个注册事件函数。 21 | * @param {Number} webContentId 当前窗口的webContentsId,在window对象中有这个属性。 22 | * @param { ...Object } args 函数参数。 23 | * @returns { Promise } 函数返回值。 24 | */ 25 | function invokeNative(eventName, cmdName, registered, webContentId, ...args) { 26 | console.log(`尝试发送IPC消息,webContentsId${webContentId},eventName${eventName},cmdName${cmdName},registered${registered},args${args}`) 27 | return new Promise(resolve => { 28 | const callbackId = crypto.randomUUID(); 29 | const callback = (event, ...args) => { 30 | if (args?.[0]?.callbackId == callbackId) { 31 | ipcRenderer.off(`IPC_DOWN_${webContentId}`, callback); 32 | resolve(args[1]); 33 | } 34 | }; 35 | ipcRenderer.on(`IPC_DOWN_${webContentId}`, callback); 36 | 37 | ipcRenderer.send( 38 | `IPC_UP_${webContentId}`, 39 | { 40 | type: 'request', 41 | callbackId, 42 | eventName: `${eventName}-${webContentId}${registered ? '-register' : ''}` 43 | }, 44 | [cmdName, ...args]); 45 | }); 46 | 47 | 48 | } -------------------------------------------------------------------------------- /src/renderer.js: -------------------------------------------------------------------------------- 1 | import {messageRenderer, patchCss} from "./utils/rendererUtils.js"; 2 | 3 | const pluginName = '[Echo-Message]' 4 | 5 | // 打开设置界面时触发,不需要插件页面 6 | // export const onSettingWindowCreated = (view) => { 7 | // // view 为 Element 对象,修改将同步到插件设置界面 8 | // 9 | // 10 | // } 11 | 12 | //app.__vue_app__.config.globalProperties?.$store?.state?.common_Aio.curAioData 13 | 14 | 15 | function onLoad() { 16 | patchCss()//添加自己自定义的css 17 | if (location.hash === "#/blank") { 18 | navigation.addEventListener("navigatesuccess", onHashUpdate, {once: true}); 19 | } else { 20 | onHashUpdate(); 21 | } 22 | 23 | } 24 | 25 | onLoad()//调用onLoad 26 | 27 | function onHashUpdate() { 28 | const hash = location.hash; 29 | if (hash === '#/blank') { 30 | return 31 | } 32 | 33 | if (!(hash.includes("#/main/message") || hash.includes("#/chat"))) return;//不符合条件直接返回 34 | 35 | const finder = setInterval(() => { 36 | if (document.querySelector(".ml-list.list")) { 37 | clearInterval(finder); 38 | console.log(pluginName, "已检测到聊天区域"); 39 | const targetNode = document.querySelector(".ml-list.list"); 40 | //只检测childList就行了 41 | const config = {attributes: false, childList: true, subtree: false,}; 42 | chatObserver.observe(targetNode, config); 43 | } 44 | }, 100); 45 | } 46 | 47 | async function render() { 48 | try { 49 | const allChats = document.querySelectorAll('.ml-item') 50 | if (allChats) await messageRenderer(allChats) 51 | 52 | } catch (e) { 53 | console.log(e) 54 | } 55 | } 56 | 57 | //下面的方案有bug,MutationObserver有概率不触发,所以选择直接写死循环 58 | 59 | //聊天窗口监听器 60 | const chatObserver = new MutationObserver(mutationsList => { 61 | setTimeout(async () => { 62 | await render() 63 | }, 50) 64 | }) 65 | 66 | 67 | // Vue组件挂载时触发 68 | export const onVueComponentMount = (component) => { 69 | // component 为 Vue Component 对象 70 | } 71 | 72 | 73 | // Vue组件卸载时触发 74 | export const onVueComponentUnmount = (component) => { 75 | // component 为 Vue Component 对象 76 | } -------------------------------------------------------------------------------- /src/utils/backendLogUtils.js: -------------------------------------------------------------------------------- 1 | const pluginName = hexToAnsi('#80d4ff') + "[Echo Message] " + '\x1b[0m' 2 | function hexToAnsi(hex) { 3 | const r = parseInt(hex.slice(1, 3), 16); 4 | const g = parseInt(hex.slice(3, 5), 16); 5 | const b = parseInt(hex.slice(5, 7), 16); 6 | return `\x1b[38;2;${r};${g};${b}m`; 7 | } 8 | function pluginLog(message){ 9 | return console.log(pluginName+message) 10 | } 11 | module.exports={pluginLog} -------------------------------------------------------------------------------- /src/utils/rendererUtils.js: -------------------------------------------------------------------------------- 1 | const pluginName = '[Echo-Message]' 2 | 3 | class ListenerHandler { 4 | constructor(msgContentContainer) { 5 | this.msgContentContainer = msgContentContainer 6 | this.leaveTimeout = undefined 7 | //绑定下面函数this的上下文 8 | this.handleMouseEnter = this.handleMouseEnter.bind(this); 9 | this.handleMouseLeave = this.handleMouseLeave.bind(this); 10 | } 11 | 12 | handleMouseEnter() { 13 | clearTimeout(this.leaveTimeout); 14 | appendPlusOneTag(this.msgContentContainer); // 添加tag 15 | } 16 | 17 | handleMouseLeave() { 18 | this.leaveTimeout = setTimeout(() => { 19 | removePlusOneTag(this.msgContentContainer); 20 | }, 150) // 移除tag 21 | }; 22 | 23 | addCommonPlusOne() { 24 | try { 25 | //给每一个消息都加上tag。实现在hover的时候显示,在不hover的时候取消显示。 26 | if (!this.msgContentContainer?.classList.contains('echo-message'))//说明这条消息还没加上事件监听器 27 | { 28 | this.msgContentContainer.classList.add('echo-message') 29 | //准备添加事件监听器 30 | this.msgContentContainer.addEventListener('mouseenter', this.handleMouseEnter) 31 | this.msgContentContainer.addEventListener('mouseleave', this.handleMouseLeave) 32 | } 33 | } catch (e) { 34 | } 35 | } 36 | } 37 | 38 | export async function messageRenderer(allChats) { 39 | 40 | for (let i = 0; i < allChats.length; i++) { 41 | const msgContentContainer = allChats[i]?.querySelector('.msg-content-container') 42 | const preMsgConContainer = i > 0 ? allChats[i - 1]?.querySelector('.msg-content-container') : null; 43 | if (preMsgConContainer?.querySelector('.em-svg-container')) removePlusOneTag(msgContentContainer)//只保留一个svg 44 | if (msgContentContainer?.classList.contains('em-msg-container')) continue//已经改过的不要改 45 | 46 | 47 | const currentMsgContent = allChats[i]?.querySelector('.message-content'); 48 | const prevMsgContent = i - 1 < 0 ? undefined : allChats[i - 1]?.querySelector('.message-content'); 49 | const nextMsgContent = i + 1 === allChats.length ? undefined : allChats[i + 1]?.querySelector('.message-content'); 50 | //判断有没有越界,越界了就开始下一个循环。 51 | if (!(prevMsgContent || nextMsgContent)) { 52 | (new ListenerHandler(msgContentContainer)).addCommonPlusOne() 53 | continue 54 | } 55 | //判断是否符合+1条件 56 | if (!msgChecker(prevMsgContent, currentMsgContent, nextMsgContent)) { 57 | (new ListenerHandler(msgContentContainer)).addCommonPlusOne() 58 | continue 59 | } 60 | 61 | //没问题!应该对下一条消息加上+1标签。 62 | //console.log(pluginName + '消息检查成功') 63 | appendPlusOneTag(msgContentContainer)//添加tag 64 | } 65 | } 66 | 67 | /** 68 | * 检查当前元素是否和上一个相同,同时和下一个不同 69 | * @param prevMsgContent 70 | * @param currentMsgContent 71 | * @param nextMsgContent 72 | */ 73 | function msgChecker(prevMsgContent, currentMsgContent, nextMsgContent) { 74 | const prevMsgs = msgExtractor(prevMsgContent) 75 | const currentMsgs = msgExtractor(currentMsgContent) 76 | const nextMsgs = msgExtractor(nextMsgContent) 77 | //console.log(JSON.stringify(prevMsgs), JSON.stringify(currentMsgs), JSON.stringify(nextMsgs)) 78 | return JSON.stringify(nextMsgs) === JSON.stringify(currentMsgs) && 79 | JSON.stringify(currentMsgs) !== JSON.stringify(prevMsgs) 80 | } 81 | 82 | /** 83 | * 传入msgContent,返回一个数组,里面是消息文本和图片src地址 84 | * @param msgContent 85 | * @returns {*[]} 86 | */ 87 | function msgExtractor(msgContent) { 88 | if (!msgContent?.querySelectorAll) return [] 89 | return [...(Array.from(msgContent?.querySelectorAll('.text-normal')).map(textElement => textElement?.innerText)), 90 | ...(Array.from(msgContent?.querySelectorAll('.image-content')).map(imgElement => imgElement?.src)), 91 | ...(Array.from(msgContent?.querySelectorAll('.markdown-element')).map(markdownElement => markdownElement.children))] 92 | } 93 | 94 | /** 95 | * 添加+1tag 96 | * @param msgContentContainer 97 | */ 98 | function appendPlusOneTag(msgContentContainer) { 99 | try { 100 | if (msgContentContainer.querySelector('.em-svg-container')) return;//已经有了就不要再加了。 101 | 102 | const svgContainer = document.createElement('div'); 103 | svgContainer.className = 'em-svg-container' 104 | svgContainer.innerHTML = `` 105 | svgContainer.style.display = 'flex'; 106 | svgContainer.style.justifyContent = 'center'; // 水平居中 107 | svgContainer.style.alignItems = 'center'; // 垂直居中 108 | svgContainer.style.cursor = 'pointer'; // 鼠标悬停时光标变为手指 109 | 110 | msgContentContainer.classList.add('em-msg-container')//先修改父元素样式 111 | msgContentContainer.appendChild(svgContainer) 112 | 113 | if (msgContentContainer?.classList.contains('container--others'))//说明是别人发的消息 114 | { 115 | svgContainer.classList.add('em-plus-one-img-right') 116 | svgContainer.addEventListener('mouseenter', () => { 117 | svgContainer.style.transform = "translateX(50%) scale(1.1)"; 118 | svgContainer.style.boxShadow = "0 0 10px rgba(17,183,234,0.5)"; 119 | }) 120 | svgContainer.addEventListener('mouseleave', () => { 121 | svgContainer.style.transform = "translateX(50%) scale(1)"; 122 | svgContainer.style.boxShadow = "none"; // 恢复原来的样式 123 | }) 124 | svgContainer.addEventListener('mousedown', () => { 125 | svgContainer.style.transform = "translateX(50%) scale(0.9)"; // 按下时缩小 126 | svgContainer.style.boxShadow = "0 0 5px rgba(17,183,234,0.5)"; // 按下时阴影 127 | }); 128 | svgContainer.addEventListener('mouseup', () => { 129 | svgContainer.style.transform = "translateX(50%) scale(1)"; // 按下时缩小 130 | svgContainer.style.boxShadow = "0 0 5px rgba(17,183,234,0.5)"; // 按下时阴影 131 | }); 132 | 133 | 134 | } else { 135 | svgContainer.classList.add('em-plus-one-img-left') 136 | svgContainer.addEventListener('mouseenter', () => { 137 | svgContainer.style.transform = "translateX(-50%) scale(1.1)"; 138 | svgContainer.style.boxShadow = "0 0 10px rgba(17,183,234,0.5)"; 139 | }) 140 | svgContainer.addEventListener('mouseleave', () => { 141 | svgContainer.style.transform = "translateX(-50%) scale(1)"; 142 | svgContainer.style.boxShadow = "none"; // 恢复原来的样式 143 | }) 144 | svgContainer.addEventListener('mousedown', () => { 145 | svgContainer.style.transform = "translateX(-50%) scale(0.9)"; // 按下时缩小 146 | svgContainer.style.boxShadow = "0 0 5px rgba(17,183,234,0.5)"; // 按下时阴影 147 | }); 148 | svgContainer.addEventListener('mouseup', () => { 149 | svgContainer.style.transform = "translateX(-50%) scale(1)"; // 按下时缩小 150 | svgContainer.style.boxShadow = "0 0 5px rgba(17,183,234,0.5)"; // 按下时阴影 151 | }); 152 | } 153 | 154 | setTimeout(() => { 155 | svgContainer.style.transform = msgContentContainer?.classList.contains('container--others') ? 156 | "translateX(50%)" : "translateX(-50%)"; 157 | svgContainer.style.opacity = "0.9"; 158 | svgContainer.style.border = "2px solid #66ccff"; 159 | }, 100); 160 | 161 | plusOneListener(svgContainer)//添加事件监听器。 162 | 163 | // console.log(pluginName + '+1tag添加成功') 164 | } catch (e) { 165 | 166 | } 167 | } 168 | 169 | 170 | function removePlusOneTag(msgContentContainer) { 171 | const svgContainer = msgContentContainer?.querySelector('.em-svg-container') 172 | if (!svgContainer) return//找不到就直接退出 173 | 174 | svgContainer.style.opacity = '0' 175 | if (msgContentContainer?.classList.contains('container--others'))//说明是别人发的消息 176 | { 177 | svgContainer.style.transform = "translateX(-100%)" 178 | } else { 179 | svgContainer.style.transform = "translateX(100%)" 180 | } 181 | 182 | setTimeout(() => { 183 | msgContentContainer.removeChild(svgContainer)//移除掉多余的svg 184 | }, 500) 185 | 186 | } 187 | 188 | function plusOneListener(svgContainer) { 189 | svgContainer.addEventListener('click', async () => { 190 | //准备复读并发送消息. 191 | const msgID = svgContainer.closest('.ml-item').id 192 | const curAioData = app.__vue_app__.config.globalProperties.$store.state.common_Aio.curAioData 193 | const peerUid = curAioData.header.uid 194 | const chatType = curAioData.chatType 195 | //console.log('拿到的消息ID为' + msgID) 196 | //发送IPC消息 197 | await window.echo_message.invokeNative("ns-ntApi", "nodeIKernelMsgService/forwardMsgWithComment", false, window.webContentId, 198 | { 199 | "msgIds": [msgID], 200 | "msgAttributeInfos": new Map(), 201 | "srcContact": {"chatType": chatType, "peerUid": peerUid, "guildId": ""}, 202 | "dstContacts": [{"chatType": chatType, "peerUid": peerUid, "guildId": ""}], 203 | "commentElements": [] 204 | }, null) 205 | }) 206 | } 207 | 208 | export function patchCss() { 209 | console.log(pluginName + 'css加载中') 210 | 211 | let style = document.createElement('style') 212 | style.type = "text/css"; 213 | style.id = "echo-message-css"; 214 | 215 | let sHtml = ` 216 | .em-msg-container { 217 | position: relative; 218 | overflow: unset !important; 219 | display: flex; 220 | align-items: center; 221 | justify-content: center; 222 | flex-direction: row; 223 | } 224 | 225 | .em-plus-one-img-right { 226 | position: absolute; 227 | left: calc(100% - 5px); 228 | width: 25px; 229 | height: 25px; 230 | border-radius: 50%; 231 | opacity: 0.2; 232 | color: var(--text-color); 233 | background-color: var(--background-color-05); 234 | backdrop-filter: blur(14px); 235 | box-shadow: var(--box-shadow); 236 | transition: 250ms; 237 | } 238 | 239 | 240 | .em-plus-one-img-left { 241 | position: absolute; 242 | right: calc(100% - 5px); 243 | width: 25px; 244 | height: 25px; 245 | border-radius: 50%; 246 | opacity: 0.2; 247 | color: var(--text-color); 248 | background-color: var(--background-color-05); 249 | backdrop-filter: blur(28px); 250 | box-shadow: var(--box-shadow); 251 | transition: 250ms; 252 | } 253 | 254 | ` 255 | 256 | style.innerHTML = sHtml 257 | document.getElementsByTagName('head')[0].appendChild(style) 258 | console.log(pluginName + 'css加载完成') 259 | } 260 | 261 | const textElement = { 262 | elementType: 1, 263 | elementId: '', 264 | textElement: { 265 | content: '', 266 | atType: 0, 267 | atUid: '', 268 | atTinyId: '', 269 | atNtUid: '' 270 | } 271 | } 272 | --------------------------------------------------------------------------------