diff --git a/.env.template b/.env.template new file mode 100644 index 0000000000000000000000000000000000000000..348c7515fac2d9033281aa9d07874038e1cbbc7f --- /dev/null +++ b/.env.template @@ -0,0 +1,15 @@ +# 这个文件是一个模板文件,实际使用时请将其重命名为 .env 并填写相应的值 + +# PORT= +# VITE_BASE_PROXY_URL= + +# Apple 公证相关环境变量 +# 请选择一种方式取消下面三行的注释并填写相应信息 +# - API 密钥方式(新式) +# APPLE_API_KEY= +# APPLE_API_KEY_ID= +# APPLE_API_ISSUER= +# - Apple ID 方式(传统) +# APPLE_ID= +# APPLE_APP_SPECIFIC_PASSWORD= +# APPLE_TEAM_ID= diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000000000000000000000000000000000..6fc5235c6cfa390c8157e2a9dbc842281d2c5b53 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,57 @@ +# 设置默认行为,所有文件的行尾自动转换为LF +* text=auto eol=lf + +# 源代码文件 +*.js text eol=lf +*.jsx text eol=lf +*.ts text eol=lf +*.tsx text eol=lf +*.vue text eol=lf +*.html text eol=lf +*.css text eol=lf +*.scss text eol=lf +*.less text eol=lf +*.json text eol=lf +*.md text eol=lf +*.yaml text eol=lf +*.yml text eol=lf + +# 配置文件 +*.spec text eol=lf +*.conf text eol=lf +*.config text eol=lf +Dockerfile text eol=lf +.env* text eol=lf +.gitignore text eol=lf +.eslintrc* text eol=lf +.prettierrc* text eol=lf +tsconfig.json text eol=lf +vite.config.ts text eol=lf +package.json text eol=lf + +# SVG 是 XML 格式的文本文件 +*.svg text eol=lf + +# 二进制文件(不进行任何行尾转换) +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.woff binary +*.woff2 binary +*.eot binary +*.ttf binary +*.otf binary +*.pdf binary +*.zip binary +*.tgz binary +*.icns binary +*.rpm binary +*.pak binary +*.so binary +*.dll binary +*.exe binary + +# 确保shell脚本在Linux/macOS上可执行 +*.sh text eol=lf diff --git a/.gitignore b/.gitignore index 7fdc56522ff9d8dae6814047c94e207696a685c5..d14ad82ebb1b7378e05b1272c12b0d10c403e5ed 100755 --- a/.gitignore +++ b/.gitignore @@ -8,12 +8,14 @@ pnpm-debug.log* lerna-debug.log* node_modules -.DS_Store +release dist dist-ssr coverage *.local +.DS_Store + /cypress/videos/ /cypress/screenshots/ @@ -29,6 +31,7 @@ coverage .env .env.development *-lock.* +pnpm-lock.yaml *.tsbuildinfo @@ -39,3 +42,4 @@ coverage .gitee/ .npmrc +.bash_profile diff --git a/LICENSE/LICENSE b/LICENSE similarity index 97% rename from LICENSE/LICENSE rename to LICENSE index f6c26977bbb993b180afd759658dcf5ea6619cd0..f63f5a9cf3498818a73068495709cceed67efd6a 100644 --- a/LICENSE/LICENSE +++ b/LICENSE @@ -1,194 +1,194 @@ -木兰宽松许可证,第2版 - -木兰宽松许可证,第2版 - -2020年1月 http://license.coscl.org.cn/MulanPSL2 - -您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: - -0. 定义 - -“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 - -“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 - -“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 - -“法人实体” 是指提交贡献的机构及其“关联实体”。 - -“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是 -指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 - -1. 授予版权许可 - -每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可 -以复制、使用、修改、分发其“贡献”,不论修改与否。 - -2. 授予专利许可 - -每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定 -撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡 -献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软 -件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“ -关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或 -其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权 -行动之日终止。 - -3. 无商标许可 - -“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定 -的声明义务而必须使用除外。 - -4. 分发限制 - -您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“ -本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 - -5. 免责声明与责任限制 - -“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对 -任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于 -何种法律理论,即使其曾被建议有此种损失的可能性。 - -6. 语言 - -“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文 -版为准。 - -条款结束 - -如何将木兰宽松许可证,第2版,应用到您的软件 - -如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: - -1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; - -2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; - -3, 请将如下声明文本放入每个源文件的头部注释中。 - -Copyright (c) [Year] [name of copyright holder] -[Software Name] is licensed under Mulan PSL v2. -You can use this software according to the terms and conditions of the Mulan -PSL v2. -You may obtain a copy of Mulan PSL v2 at: - http://license.coscl.org.cn/MulanPSL2 -THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -See the Mulan PSL v2 for more details. - -Mulan Permissive Software License,Version 2 - -Mulan Permissive Software License,Version 2 (Mulan PSL v2) - -January 2020 http://license.coscl.org.cn/MulanPSL2 - -Your reproduction, use, modification and distribution of the Software shall -be subject to Mulan PSL v2 (this License) with the following terms and -conditions: - -0. Definition - -Software means the program and related documents which are licensed under -this License and comprise all Contribution(s). - -Contribution means the copyrightable work licensed by a particular -Contributor under this License. - -Contributor means the Individual or Legal Entity who licenses its -copyrightable work under this License. - -Legal Entity means the entity making a Contribution and all its -Affiliates. - -Affiliates means entities that control, are controlled by, or are under -common control with the acting entity under this License, ‘control’ means -direct or indirect ownership of at least fifty percent (50%) of the voting -power, capital or other securities of controlled or commonly controlled -entity. - -1. Grant of Copyright License - -Subject to the terms and conditions of this License, each Contributor hereby -grants to you a perpetual, worldwide, royalty-free, non-exclusive, -irrevocable copyright license to reproduce, use, modify, or distribute its -Contribution, with modification or not. - -2. Grant of Patent License - -Subject to the terms and conditions of this License, each Contributor hereby -grants to you a perpetual, worldwide, royalty-free, non-exclusive, -irrevocable (except for revocation under this Section) patent license to -make, have made, use, offer for sale, sell, import or otherwise transfer its -Contribution, where such patent license is only limited to the patent claims -owned or controlled by such Contributor now or in future which will be -necessarily infringed by its Contribution alone, or by combination of the -Contribution with the Software to which the Contribution was contributed. -The patent license shall not apply to any modification of the Contribution, -and any other combination which includes the Contribution. If you or your -Affiliates directly or indirectly institute patent litigation (including a -cross claim or counterclaim in a litigation) or other patent enforcement -activities against any individual or entity by alleging that the Software or -any Contribution in it infringes patents, then any patent license granted to -you under this License for the Software shall terminate as of the date such -litigation or activity is filed or taken. - -3. No Trademark License - -No trademark license is granted to use the trade names, trademarks, service -marks, or product names of Contributor, except as required to fulfill notice -requirements in section 4. - -4. Distribution Restriction - -You may distribute the Software in any medium with or without modification, -whether in source or executable forms, provided that you provide recipients -with a copy of this License and retain copyright, patent, trademark and -disclaimer statements in the Software. - -5. Disclaimer of Warranty and Limitation of Liability - -THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY -KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR -COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT -LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING -FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO -MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF -THE POSSIBILITY OF SUCH DAMAGES. - -6. Language - -THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION -AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF -DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION -SHALL PREVAIL. - -END OF THE TERMS AND CONDITIONS - -How to Apply the Mulan Permissive Software License,Version 2 -(Mulan PSL v2) to Your Software - -To apply the Mulan PSL v2 to your work, for easy identification by -recipients, you are suggested to complete following three steps: - -i. Fill in the blanks in following statement, including insert your software -name, the year of the first publication of your software, and your name -identified as the copyright owner; - -ii. Create a file named "LICENSE" which contains the whole context of this -License in the first directory of your software package; - -iii. Attach the statement to the appropriate annotated syntax at the -beginning of each source file. - -Copyright (c) [Year] [name of copyright holder] -[Software Name] is licensed under Mulan PSL v2. -You can use this software according to the terms and conditions of the Mulan -PSL v2. -You may obtain a copy of Mulan PSL v2 at: - http://license.coscl.org.cn/MulanPSL2 -THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY -KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO -NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. -See the Mulan PSL v2 for more details. +木兰宽松许可证,第2版 + +木兰宽松许可证,第2版 + +2020年1月 http://license.coscl.org.cn/MulanPSL2 + +您对“软件”的复制、使用、修改及分发受木兰宽松许可证,第2版(“本许可证”)的如下条款的约束: + +0. 定义 + +“软件” 是指由“贡献”构成的许可在“本许可证”下的程序和相关文档的集合。 + +“贡献” 是指由任一“贡献者”许可在“本许可证”下的受版权法保护的作品。 + +“贡献者” 是指将受版权法保护的作品许可在“本许可证”下的自然人或“法人实体”。 + +“法人实体” 是指提交贡献的机构及其“关联实体”。 + +“关联实体” 是指,对“本许可证”下的行为方而言,控制、受控制或与其共同受控制的机构,此处的控制是 +指有受控方或共同受控方至少50%直接或间接的投票权、资金或其他有价证券。 + +1. 授予版权许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的版权许可,您可 +以复制、使用、修改、分发其“贡献”,不论修改与否。 + +2. 授予专利许可 + +每个“贡献者”根据“本许可证”授予您永久性的、全球性的、免费的、非独占的、不可撤销的(根据本条规定 +撤销除外)专利许可,供您制造、委托制造、使用、许诺销售、销售、进口其“贡献”或以其他方式转移其“贡 +献”。前述专利许可仅限于“贡献者”现在或将来拥有或控制的其“贡献”本身或其“贡献”与许可“贡献”时的“软 +件”结合而将必然会侵犯的专利权利要求,不包括对“贡献”的修改或包含“贡献”的其他结合。如果您或您的“ +关联实体”直接或间接地,就“软件”或其中的“贡献”对任何人发起专利侵权诉讼(包括反诉或交叉诉讼)或 +其他专利维权行动,指控其侵犯专利权,则“本许可证”授予您对“软件”的专利许可自您提起诉讼或发起维权 +行动之日终止。 + +3. 无商标许可 + +“本许可证”不提供对“贡献者”的商品名称、商标、服务标志或产品名称的商标许可,但您为满足第4条规定 +的声明义务而必须使用除外。 + +4. 分发限制 + +您可以在任何媒介中将“软件”以源程序形式或可执行形式重新分发,不论修改与否,但您必须向接收者提供“ +本许可证”的副本,并保留“软件”中的版权、商标、专利及免责声明。 + +5. 免责声明与责任限制 + +“软件”及其中的“贡献”在提供时不带任何明示或默示的担保。在任何情况下,“贡献者”或版权所有者不对 +任何人因使用“软件”或其中的“贡献”而引发的任何直接或间接损失承担责任,不论因何种原因导致或者基于 +何种法律理论,即使其曾被建议有此种损失的可能性。 + +6. 语言 + +“本许可证”以中英文双语表述,中英文版本具有同等法律效力。如果中英文版本存在任何冲突不一致,以中文 +版为准。 + +条款结束 + +如何将木兰宽松许可证,第2版,应用到您的软件 + +如果您希望将木兰宽松许可证,第2版,应用到您的新软件,为了方便接收者查阅,建议您完成如下三步: + +1, 请您补充如下声明中的空白,包括软件名、软件的首次发表年份以及您作为版权人的名字; + +2, 请您在软件包的一级目录下创建以“LICENSE”为名的文件,将整个许可证文本放入该文件中; + +3, 请将如下声明文本放入每个源文件的头部注释中。 + +Copyright (c) [Year] [name of copyright holder] +[Software Name] is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan +PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. + +Mulan Permissive Software License,Version 2 + +Mulan Permissive Software License,Version 2 (Mulan PSL v2) + +January 2020 http://license.coscl.org.cn/MulanPSL2 + +Your reproduction, use, modification and distribution of the Software shall +be subject to Mulan PSL v2 (this License) with the following terms and +conditions: + +0. Definition + +Software means the program and related documents which are licensed under +this License and comprise all Contribution(s). + +Contribution means the copyrightable work licensed by a particular +Contributor under this License. + +Contributor means the Individual or Legal Entity who licenses its +copyrightable work under this License. + +Legal Entity means the entity making a Contribution and all its +Affiliates. + +Affiliates means entities that control, are controlled by, or are under +common control with the acting entity under this License, ‘control’ means +direct or indirect ownership of at least fifty percent (50%) of the voting +power, capital or other securities of controlled or commonly controlled +entity. + +1. Grant of Copyright License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable copyright license to reproduce, use, modify, or distribute its +Contribution, with modification or not. + +2. Grant of Patent License + +Subject to the terms and conditions of this License, each Contributor hereby +grants to you a perpetual, worldwide, royalty-free, non-exclusive, +irrevocable (except for revocation under this Section) patent license to +make, have made, use, offer for sale, sell, import or otherwise transfer its +Contribution, where such patent license is only limited to the patent claims +owned or controlled by such Contributor now or in future which will be +necessarily infringed by its Contribution alone, or by combination of the +Contribution with the Software to which the Contribution was contributed. +The patent license shall not apply to any modification of the Contribution, +and any other combination which includes the Contribution. If you or your +Affiliates directly or indirectly institute patent litigation (including a +cross claim or counterclaim in a litigation) or other patent enforcement +activities against any individual or entity by alleging that the Software or +any Contribution in it infringes patents, then any patent license granted to +you under this License for the Software shall terminate as of the date such +litigation or activity is filed or taken. + +3. No Trademark License + +No trademark license is granted to use the trade names, trademarks, service +marks, or product names of Contributor, except as required to fulfill notice +requirements in section 4. + +4. Distribution Restriction + +You may distribute the Software in any medium with or without modification, +whether in source or executable forms, provided that you provide recipients +with a copy of this License and retain copyright, patent, trademark and +disclaimer statements in the Software. + +5. Disclaimer of Warranty and Limitation of Liability + +THE SOFTWARE AND CONTRIBUTION IN IT ARE PROVIDED WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED. IN NO EVENT SHALL ANY CONTRIBUTOR OR +COPYRIGHT HOLDER BE LIABLE TO YOU FOR ANY DAMAGES, INCLUDING, BUT NOT +LIMITED TO ANY DIRECT, OR INDIRECT, SPECIAL OR CONSEQUENTIAL DAMAGES ARISING +FROM YOUR USE OR INABILITY TO USE THE SOFTWARE OR THE CONTRIBUTION IN IT, NO +MATTER HOW IT’S CAUSED OR BASED ON WHICH LEGAL THEORY, EVEN IF ADVISED OF +THE POSSIBILITY OF SUCH DAMAGES. + +6. Language + +THIS LICENSE IS WRITTEN IN BOTH CHINESE AND ENGLISH, AND THE CHINESE VERSION +AND ENGLISH VERSION SHALL HAVE THE SAME LEGAL EFFECT. IN THE CASE OF +DIVERGENCE BETWEEN THE CHINESE AND ENGLISH VERSIONS, THE CHINESE VERSION +SHALL PREVAIL. + +END OF THE TERMS AND CONDITIONS + +How to Apply the Mulan Permissive Software License,Version 2 +(Mulan PSL v2) to Your Software + +To apply the Mulan PSL v2 to your work, for easy identification by +recipients, you are suggested to complete following three steps: + +i. Fill in the blanks in following statement, including insert your software +name, the year of the first publication of your software, and your name +identified as the copyright owner; + +ii. Create a file named "LICENSE" which contains the whole context of this +License in the first directory of your software package; + +iii. Attach the statement to the appropriate annotated syntax at the +beginning of each source file. + +Copyright (c) [Year] [name of copyright holder] +[Software Name] is licensed under Mulan PSL v2. +You can use this software according to the terms and conditions of the Mulan +PSL v2. +You may obtain a copy of Mulan PSL v2 at: + http://license.coscl.org.cn/MulanPSL2 +THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY +KIND, EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO +NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE. +See the Mulan PSL v2 for more details. diff --git a/README.en.md b/README.en.md index 3b0f1549a55e263213e894324e733b6c31913243..3d4d756321eaf4cd571c3dc2591bbfd68ca66a31 100644 --- a/README.en.md +++ b/README.en.md @@ -1,7 +1,7 @@ # euler-copilot-web #### Description -EulerCopilot前端 +openEuler Intelligence前端 #### Software Architecture Software architecture description diff --git a/README.md b/README.md index 4fbb2c071eefe1e32fa428819d72e7929225963d..143183324e3b8162f11ef1f88b2bf4669cf86755 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # euler-copilot-web #### 介绍 -EulerCopilot前端 +openEuler Intelligence前端 #### 软件架构 软件架构说明 diff --git a/build/icon.icns b/build/icon.icns new file mode 100644 index 0000000000000000000000000000000000000000..dfcf9da716cca19ad2a8db3a7ca4a3375e32a5c7 Binary files /dev/null and b/build/icon.icns differ diff --git a/build/icon.ico b/build/icon.ico new file mode 100644 index 0000000000000000000000000000000000000000..4c1cf0ba07ca32eee38aded9ea8cf33a0863600b Binary files /dev/null and b/build/icon.ico differ diff --git a/build/icon.png b/build/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..196b687e8fd96aa6157366df9f30162202e45917 Binary files /dev/null and b/build/icon.png differ diff --git a/build/icons/1024x1024.png b/build/icons/1024x1024.png new file mode 100644 index 0000000000000000000000000000000000000000..74deaacca56101116b6f77d92397ec420e132537 Binary files /dev/null and b/build/icons/1024x1024.png differ diff --git a/build/icons/128x128.png b/build/icons/128x128.png new file mode 100644 index 0000000000000000000000000000000000000000..9d83273e5594a13b2783f19b947d6972423bbfa5 Binary files /dev/null and b/build/icons/128x128.png differ diff --git a/build/icons/128x128@2x.png b/build/icons/128x128@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..3280ba838001b6a934cb7f941e0d66311fd80335 Binary files /dev/null and b/build/icons/128x128@2x.png differ diff --git a/build/icons/16x16.png b/build/icons/16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..8ee1647808813ddc8c0ccaee3e5ef1464e615b5b Binary files /dev/null and b/build/icons/16x16.png differ diff --git a/build/icons/16x16@2x.png b/build/icons/16x16@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..64674df204834c7861ad28f593211608fe5db79a Binary files /dev/null and b/build/icons/16x16@2x.png differ diff --git a/build/icons/24x24.png b/build/icons/24x24.png new file mode 100644 index 0000000000000000000000000000000000000000..7b2ad6b7b1194606cb89178f518d5b4f7dc2a2e4 Binary files /dev/null and b/build/icons/24x24.png differ diff --git a/build/icons/24x24@2x.png b/build/icons/24x24@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..e460d0994f2cab93e20a4209b35e88e4e04013a9 Binary files /dev/null and b/build/icons/24x24@2x.png differ diff --git a/build/icons/256x256.png b/build/icons/256x256.png new file mode 100644 index 0000000000000000000000000000000000000000..cf38e1811ef4dcf8880c06b71168ad4bc82ba69a Binary files /dev/null and b/build/icons/256x256.png differ diff --git a/build/icons/256x256@2x.png b/build/icons/256x256@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..9f214983e05cea93bbe0b79177f9659c5d0c3b43 Binary files /dev/null and b/build/icons/256x256@2x.png differ diff --git a/build/icons/32x32.png b/build/icons/32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..cadebdd3ee0f7f3c76c75e5885929867a4295c02 Binary files /dev/null and b/build/icons/32x32.png differ diff --git a/build/icons/32x32@2x.png b/build/icons/32x32@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..fdf4f897f52506e82308f0f6a32328a8b411097c Binary files /dev/null and b/build/icons/32x32@2x.png differ diff --git a/build/icons/48x48.png b/build/icons/48x48.png new file mode 100644 index 0000000000000000000000000000000000000000..0a0ea4e0962859b8d18a79644232834711078619 Binary files /dev/null and b/build/icons/48x48.png differ diff --git a/build/icons/48x48@2x.png b/build/icons/48x48@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..38eba57df733c7f4ac351cb79f9540c3c8b2ae78 Binary files /dev/null and b/build/icons/48x48@2x.png differ diff --git a/build/icons/512x512.png b/build/icons/512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..fa1f1fb4070e422e586473516e15bea0beabe35f Binary files /dev/null and b/build/icons/512x512.png differ diff --git a/build/icons/512x512@2x.png b/build/icons/512x512@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..38b463ca89bec60bdbfff2a7b02a51b97270ca24 Binary files /dev/null and b/build/icons/512x512@2x.png differ diff --git a/build/icons/64x64.png b/build/icons/64x64.png new file mode 100644 index 0000000000000000000000000000000000000000..73bda39eeebdffa9c91bdf2fd6301c2dbad45765 Binary files /dev/null and b/build/icons/64x64.png differ diff --git a/build/icons/64x64@2x.png b/build/icons/64x64@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..d3afc74390c9f080065befbec2bc3a6502879443 Binary files /dev/null and b/build/icons/64x64@2x.png differ diff --git a/build/linux/euler-copilot-desktop.desktop b/build/linux/euler-copilot-desktop.desktop new file mode 100644 index 0000000000000000000000000000000000000000..1549c470f2f1b4a1ff9975e99ba4d929c5156377 --- /dev/null +++ b/build/linux/euler-copilot-desktop.desktop @@ -0,0 +1,17 @@ +[Desktop Entry] +Name=openEuler Intelligence +Name[zh_CN]=openEuler 智能化解决方案 +Exec=/opt/Intelligence/euler-copilot-desktop %U +Terminal=false +Type=Application +Icon=euler-copilot-desktop +StartupWMClass=openEuler Intelligence +Comment=openEuler Intelligence Desktop Client +Comment[zh]=openEuler 智能化解决方案桌面客户端 +Comment[zh_CN]=openEuler 智能化解决方案桌面客户端 +Comment[zh_CN.UTF-8]=openEuler 智能化解决方案桌面客户端 +Comment[zh_HK]=openEuler 智能化解決方案桌面應用程式 +Comment[zh_HK.UTF-8]=openEuler 智能化解決方案桌面應用程式 +Comment[zh_TW]=openEuler 智能化解決方案桌面軟體 +Comment[zh_TW.UTF-8]=openEuler 智能化解決方案桌面軟體 +Categories=Development;Utility;Network; diff --git a/build/linux/euler-copilot-web.spec b/build/linux/euler-copilot-web.spec new file mode 100644 index 0000000000000000000000000000000000000000..3b90548c260eb0272f1d9c167586b69896b3777b --- /dev/null +++ b/build/linux/euler-copilot-web.spec @@ -0,0 +1,326 @@ +AutoReq: no +%undefine __find_requires +# Be sure buildpolicy set to do nothing +%define __spec_install_post %{nil} +# Something that need for rpm-4.1 +%define _missing_doc_files_terminate_build 0 +# Disable debug package generation +%define debug_package %{nil} + +%ifarch aarch64 +%define _electron_arch arm64 +%define _electron_build_dir linux-arm64-unpacked +%else +%define _electron_arch x64 +%define _electron_build_dir linux-unpacked +%endif + +BuildArch: aarch64 x86_64 +Name: euler-copilot-web +Version: 0.9.6 +Release: 5%{?dist} +License: MulanPSL-2.0 +Summary: openEuler 智能化解决方案 Web 前端 +Source0: %{name}-%{version}.tar.gz +Source1: offline_node_modules-%{_electron_arch}.tar.zst.part0 +Source2: offline_node_modules-%{_electron_arch}.tar.zst.part1 +Source3: offline_node_modules-%{_electron_arch}.tar.zst.part2 +Source4: offline_node_modules-%{_electron_arch}.tar.zst.part3 + +URL: https://gitee.com/openeuler/euler-copilot-web +Vendor: openEuler +Packager: openEuler + +BuildRequires: curl +BuildRequires: zstd + +Requires: nginx + +%description +openEuler 智能化解决方案 Web 前端 + +%package -n euler-copilot-desktop +# Electron 客户端 +Group: Applications/Utilities +Summary: openEuler 智能化解决方案桌面客户端 +Requires: which +Requires: at-spi2-core +Requires: gtk3 +Requires: libXScrnSaver +Requires: libnotify +Requires: nss +Requires: xdg-utils +Requires: (libXtst or libXtst6) +Requires: (libuuid or libuuid1) +Requires(post): /bin/sh +Requires(postun): /bin/sh + +%description -n euler-copilot-desktop +openEuler 智能化解决方案桌面客户端 + + +%prep +%setup -q + + +%build +# Extract Node.js version using grep+sed for compatibility +NODE_VER=$(grep '"node":' package.json | grep -Eo '[0-9]+\.[0-9]+\.[0-9]+' | head -1) +NODE_LINK="https://mirrors.huaweicloud.com/nodejs/v${NODE_VER}/node-v${NODE_VER}-linux-%{_electron_arch}.tar.xz" +# Download and install Node.js into a subdirectory +NODE_HOME="$PWD/.node-v${NODE_VER}" +mkdir -p "$NODE_HOME" +curl -sSL "$NODE_LINK" | tar -xJ -C "$NODE_HOME" --strip-components=1 +# Set NODE_HOME and update PATH, then test Node.js installation +export NODE_HOME +export PATH="$NODE_HOME/bin:$PATH" +node -v + +# Setup npm mirror +npm config set registry https://mirrors.huaweicloud.com/repository/npm/ + +# Setup mirrors for Electron +export ELECTRON_MIRROR="https://mirrors.huaweicloud.com/electron/" + +# Install pnpm globally +npm install -g pnpm +pnpm -v + +# Download Electron binaries to cache directory +ELECTRON_VER=$(grep -Po '(?<="electron": ")[^"]+' package.json) +PACKAGE_NAME="electron-v$ELECTRON_VER-linux-%{_electron_arch}.zip" +# Electron cache directory +CACHE_DIR="$HOME/.cache/electron" +if [ ! -d "$CACHE_DIR" ]; then + mkdir -p "$CACHE_DIR" +fi +# Only download if not already present +if [ ! -f "$CACHE_DIR/$PACKAGE_NAME" ]; then + curl -sSL "https://mirrors.huaweicloud.com/electron/$ELECTRON_VER/$PACKAGE_NAME" \ + -o "$CACHE_DIR/$PACKAGE_NAME" +fi + +# 合并并解压离线 node_modules +cat %{_sourcedir}/offline_node_modules-%{_electron_arch}.tar.zst.part0 \ + %{_sourcedir}/offline_node_modules-%{_electron_arch}.tar.zst.part1 \ + %{_sourcedir}/offline_node_modules-%{_electron_arch}.tar.zst.part2 \ + %{_sourcedir}/offline_node_modules-%{_electron_arch}.tar.zst.part3 \ + > offline_node_modules-%{_electron_arch}.tar.zst + +if [ -f offline_node_modules-%{_electron_arch}.tar.zst ]; then + zstd -d offline_node_modules-%{_electron_arch}.tar.zst -c | tar -xf - +fi + +# Install pnpm packages +# pnpm install --offline +# Build Electron app +pnpm run package:linux + +# Build Web app +pnpm run build + + +%install + +# Web 主包安装 +mkdir -p %{buildroot}/usr/share/euler-copilot-web +cp -a %{_builddir}/%{name}-%{version}/dist/. %{buildroot}/usr/share/euler-copilot-web/ +chmod -R a+rX %{buildroot}/usr/share/euler-copilot-web + +# nginx 配置安装 +mkdir -p %{buildroot}/etc/nginx/conf.d +cp -a %{_builddir}/%{name}-%{version}/build/linux/nginx.conf.local.tmpl %{buildroot}/etc/nginx/conf.d/euler-copilot-web.conf + +# Electron 客户端安装 +mkdir -p %{buildroot}/opt/Intelligence +mkdir -p %{buildroot}/usr/share/applications +# 创建图标目录 +mkdir -p %{buildroot}/usr/share/icons/hicolor/16x16/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/24x24/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/32x32/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/48x48/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/64x64/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/128x128/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/256x256@2/apps +mkdir -p %{buildroot}/usr/share/icons/hicolor/512x512/apps + +# 复制构件到目标目录 +cp -a %{_builddir}/%{name}-%{version}/release/openeuler-intelligence-%{version}/%{_electron_build_dir}/* %{buildroot}/opt/Intelligence/ +# 拷贝桌面入口文件和图标 +cp -a %{_builddir}/%{name}-%{version}/build/linux/euler-copilot-desktop.desktop %{buildroot}/usr/share/applications/ +cp -a %{_builddir}/%{name}-%{version}/build/icons/16x16.png %{buildroot}/usr/share/icons/hicolor/16x16/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/24x24.png %{buildroot}/usr/share/icons/hicolor/24x24/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/32x32.png %{buildroot}/usr/share/icons/hicolor/32x32/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/48x48.png %{buildroot}/usr/share/icons/hicolor/48x48/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/64x64.png %{buildroot}/usr/share/icons/hicolor/64x64/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/128x128.png %{buildroot}/usr/share/icons/hicolor/128x128/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/256x256.png %{buildroot}/usr/share/icons/hicolor/256x256/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/256x256@2x.png %{buildroot}/usr/share/icons/hicolor/256x256@2/apps/euler-copilot-desktop.png +cp -a %{_builddir}/%{name}-%{version}/build/icons/512x512.png %{buildroot}/usr/share/icons/hicolor/512x512/apps/euler-copilot-desktop.png + + +%files +# Web 主包安装内容 +%dir /usr/share/euler-copilot-web +%dir /usr/share/euler-copilot-web/assets +%attr(0644, root, root) /usr/share/euler-copilot-web/*.* +%attr(0644, root, root) /usr/share/euler-copilot-web/assets/* +%config(noreplace) /etc/nginx/conf.d/euler-copilot-web.conf + + +%files -n euler-copilot-desktop +# 应用安装目录及其所有内容 +%dir /opt/Intelligence +%attr(0755, root, root) /opt/Intelligence/** +# 桌面与图标 +%attr(0644, root, root) /usr/share/applications/euler-copilot-desktop.desktop +%attr(0644, root, root) /usr/share/icons/hicolor/16x16/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/24x24/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/32x32/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/48x48/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/64x64/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/128x128/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/256x256/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/256x256@2/apps/euler-copilot-desktop.png +%attr(0644, root, root) /usr/share/icons/hicolor/512x512/apps/euler-copilot-desktop.png + + +%post +#!/bin/bash +echo "========================================================================" +echo "openEuler Intelligence 前端服务安装完成!" +echo "" +echo "已安装 nginx 配置文件: /etc/nginx/conf.d/euler-copilot-web.conf" +echo "Web 文件安装目录: /usr/share/euler-copilot-web" +echo "" +echo "服务配置信息:" +echo " - Web 服务端口: 8080" +echo " - 访问路径:" +echo " * 主页: http://your-server:8080/" +echo " * 登录页: http://your-server:8080/login" +echo " - API 代理: http://your-server:8080/api/ -> http://127.0.0.1:8002/api/" +echo " - 静态资源: /assets/ (30天缓存)" +echo "" +echo "安全特性: XSS防护、内容类型保护、HSTS、CSP策略已启用" +echo "" +echo "请检查配置文件并手动启动或重启 nginx 服务:" +echo " # systemctl start nginx (首次启动)" +echo " # systemctl restart nginx (重启服务)" +echo " # systemctl enable nginx (开机自启)" +echo "" +echo "注意: 请确保后端 API 服务在 127.0.0.1:8002 端口运行" +echo "========================================================================" + + +%postun +#!/bin/bash +echo "========================================================================" +echo "openEuler Intelligence 前端服务已卸载!" +echo "" +echo "已移除的文件:" +echo " - Web 文件目录: /usr/share/euler-copilot-web" +echo " - nginx 配置文件: /etc/nginx/conf.d/euler-copilot-web.conf" +echo "" +echo "服务影响:" +echo " - Web 服务 (端口 8080) 已停止" +echo " - 如需继续使用 nginx,请手动重启服务:" +echo " # systemctl restart nginx" +echo "" +echo "注意: 若过去已手动安装 nginx,nginx 服务本身未被卸载,仅移除了相关配置" +echo "========================================================================" + + +%post -n euler-copilot-desktop -p /bin/sh +#!/bin/bash + +if type update-alternatives 2>/dev/null >&1; then + # Remove previous link if it doesn't use update-alternatives + if [ -L '/usr/bin/euler-copilot-desktop' -a -e '/usr/bin/euler-copilot-desktop' -a "`readlink '/usr/bin/euler-copilot-desktop'`" != '/etc/alternatives/euler-copilot-desktop' ]; then + rm -f '/usr/bin/euler-copilot-desktop' + fi + update-alternatives --install '/usr/bin/euler-copilot-desktop' 'euler-copilot-desktop' '/opt/Intelligence/euler-copilot-desktop' 100 || ln -sf '/opt/Intelligence/euler-copilot-desktop' '/usr/bin/euler-copilot-desktop' +else + ln -sf '/opt/Intelligence/euler-copilot-desktop' '/usr/bin/euler-copilot-desktop' +fi + +# Check if user namespaces are supported by the kernel and working with a quick test: +if ! { [[ -L /proc/self/ns/user ]] && unshare --user true; }; then + # Use SUID chrome-sandbox only on systems without user namespaces: + chmod 4755 '/opt/Intelligence/chrome-sandbox' || true +else + chmod 0755 '/opt/Intelligence/chrome-sandbox' || true +fi + +if hash update-mime-database 2>/dev/null; then + update-mime-database /usr/share/mime || true +fi + +if hash update-desktop-database 2>/dev/null; then + update-desktop-database /usr/share/applications || true +fi + +# Install apparmor profile. (Ubuntu 24+) +# First check if the version of AppArmor running on the device supports our profile. +# This is in order to keep backwards compatibility with Ubuntu 22.04 which does not support abi/4.0. +# In that case, we just skip installing the profile since the app runs fine without it on 22.04. +# +# Those apparmor_parser flags are akin to performing a dry run of loading a profile. +# https://wiki.debian.org/AppArmor/HowToUse#Dumping_profiles +# +# Unfortunately, at the moment AppArmor doesn't have a good story for backwards compatibility. +# https://askubuntu.com/questions/1517272/writing-a-backwards-compatible-apparmor-profile +if apparmor_status --enabled > /dev/null 2>&1; then + APPARMOR_PROFILE_SOURCE='/opt/Intelligence/resources/apparmor-profile' + APPARMOR_PROFILE_TARGET='/etc/apparmor.d/euler-copilot-desktop' + if apparmor_parser --skip-kernel-load --debug "$APPARMOR_PROFILE_SOURCE" > /dev/null 2>&1; then + cp -f "$APPARMOR_PROFILE_SOURCE" "$APPARMOR_PROFILE_TARGET" + + # Updating the current AppArmor profile is not possible and probably not meaningful in a chroot'ed environment. + # Use cases are for example environments where images for clients are maintained. + # There, AppArmor might correctly be installed, but live updating makes no sense. + if ! { [ -x '/usr/bin/ischroot' ] && /usr/bin/ischroot; } && hash apparmor_parser 2>/dev/null; then + # Extra flags taken from dh_apparmor: + # > By using '-W -T' we ensure that any abstraction updates are also pulled in. + # https://wiki.debian.org/AppArmor/Contribute/FirstTimeProfileImport + apparmor_parser --replace --write-cache --skip-read-cache "$APPARMOR_PROFILE_TARGET" + fi + else + echo "Skipping the installation of the AppArmor profile as this version of AppArmor does not seem to support the bundled profile" + fi +fi + + +%postun -n euler-copilot-desktop -p /bin/sh +#!/bin/bash + +# Delete the link to the binary +if type update-alternatives >/dev/null 2>&1; then + update-alternatives --remove 'euler-copilot-desktop' '/usr/bin/euler-copilot-desktop' +else + rm -f '/usr/bin/euler-copilot-desktop' +fi + +APPARMOR_PROFILE_DEST='/etc/apparmor.d/euler-copilot-desktop' + +# Remove apparmor profile. +if [ -f "$APPARMOR_PROFILE_DEST" ]; then + rm -f "$APPARMOR_PROFILE_DEST" +fi + + +%changelog +* Wed Jun 11 2025 openEuler - 0.9.6-5 +- 修复桌面端 YAML 编辑器无法粘贴文本的问题 + +* Wed Jun 11 2025 openEuler - 0.9.6-4 +- 本地部署 & MCP 支持 + +* Wed Jun 04 2025 openEuler - 0.9.6-3 +- 修复 GNOME Dock 图标显示问题 + +* Wed Jun 04 2025 openEuler - 0.9.6-2 +- 增加安装后提示信息 + +* Thu Apr 17 2025 openEuler - 0.9.6-1 +- Initial release diff --git a/build/linux/nginx.conf.local.tmpl b/build/linux/nginx.conf.local.tmpl new file mode 100644 index 0000000000000000000000000000000000000000..b10a8d2c058fdc127ba387f33094221454acf34a --- /dev/null +++ b/build/linux/nginx.conf.local.tmpl @@ -0,0 +1,143 @@ +server { + listen 8080 default_server; + server_name _; + charset utf-8; + + client_body_buffer_size 5120M; + client_max_body_size 5120M; + + add_header X-XSS-Protection "1; mode=block"; + add_header X-Content-Type-Options nosniff; + add_header Referrer-Policy "no-referrer"; + add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + add_header Cache-Control "no-cache"; + add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: base64;"; + + resolver 8.8.8.8 8.8.4.4 valid=60s; + resolver_timeout 5s; + + if ($request_method !~ ^(GET|HEAD|POST|PUT|DELETE)$) { + return 444; + } + + location ~ /\. { + deny all; + return 404; + } + + location / { + root /usr/share/euler-copilot-web; + try_files $uri $uri/ /index.html; + if (!-e $request_filename) { + return 404; + } + } + + location /copilot { + alias /usr/share/euler-copilot-web; + index index.html; + try_files $uri $uri/ /index.html; + } + + location /login { + root /usr/share/euler-copilot-web; + try_files $uri $uri/ /index.html; + } + + location /api/health_check { + deny all; + return 404; + } + + location /api/ { + proxy_set_header X-Forwarded-For $http_x_real_ip; + add_header Cache-Control "no-cache,no-store,must-revalidate"; + add_header X-Accel-Buffering no; + proxy_buffering off; + proxy_intercept_errors on; + + error_page 404 /404.html; + proxy_read_timeout 500s; + proxy_connect_timeout 500s; + + proxy_pass http://127.0.0.1:8002/api/; + } + + location ~ ^/witchaind(.*)$ { + # 提取路径后缀 + set $path_suffix $1; + + # 转发请求到本地服务,保留路径结构和查询参数 + proxy_pass http://127.0.0.1:9888/witchaind$path_suffix$is_args$args; + + # HTTP/1.1 支持 + proxy_http_version 1.1; + + # 确保传递Content-Type头部 + proxy_set_header Content-Type $content_type; + + # 添加Content-Length头部,解决POST请求体丢失问题 + proxy_set_header Content-Length $content_length; + + # 代理请求头设置 + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # 正确设置Upgrade和Connection头部 + set $connection_upgrade $http_upgrade; + if ($http_upgrade) { + set $connection_upgrade "upgrade"; + } + proxy_set_header Connection $connection_upgrade; + + # 增加请求体大小限制 + client_max_body_size 20m; + + # 优化代理缓冲设置 + proxy_buffering off; # 对于POST请求可能更好 + + # 请求和响应超时设置 + proxy_connect_timeout 60s; + proxy_send_timeout 60s; + proxy_read_timeout 60s; + + # 跨域设置 + add_header Access-Control-Allow-Origin *; + add_header Access-Control-Allow-Methods 'GET, POST, OPTIONS'; + add_header Access-Control-Allow-Headers 'DNT,X-Mx-ReqToken,Keep-Alive,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Authorization'; + + # 处理 OPTIONS 请求 + if ($request_method = 'OPTIONS') { + add_header Access-Control-Max-Age 1728000; + add_header Content-Type 'text/plain charset=UTF-8'; + add_header Content-Length 0; + return 204; + } + } + + error_page 401 402 403 405 406 407 413 414 /error.html; + error_page 404 /404.html; + error_page 500 501 502 503 504 505 /error.html; + + location = /404 { + return 404; + } + + location = /404.html { + root /usr/share/euler-copilot-web; + internal; + } + + location = /error.html { + root /usr/share/euler-copilot-web; + internal; + } + + location /assets/ { + alias /usr/share/euler-copilot-web/assets/; + expires 30d; + add_header Cache-Control public; + } +} diff --git a/build/scripts/build_rpm.sh b/build/scripts/build_rpm.sh new file mode 100755 index 0000000000000000000000000000000000000000..f7dffafe247afd6a34fed33b127f1872f433fc80 --- /dev/null +++ b/build/scripts/build_rpm.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +# Exit on error and unset vars +set -euo pipefail + +# 检查是否以 root 身份运行 +if [ "$(id -u)" -ne 0 ]; then + echo "错误: 此脚本必须以 root 身份运行" >&2 + exit 1 +fi + +# 脚本所在目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# 项目根目录 +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +# 构建输出目录 +RELEASE_DIR="${PROJECT_ROOT}/release" +mkdir -p "${RELEASE_DIR}" +# 清理上次构建残留 +rm -rf "${RELEASE_DIR}/rpmbuild" +rm -f "${RELEASE_DIR}"/*.tar.gz + +# spec 文件路径 +SPEC="${PROJECT_ROOT}/build/linux/euler-copilot-web.spec" + +# 从 spec 文件获取 Name 和 Version +name=$(grep -E '^Name:' "${SPEC}" | head -1 | awk '{print $2}') +version=$(grep -E '^Version:' "${SPEC}" | head -1 | awk '{print $2}') +tarball="${name}-${version}.tar.gz" +tarball_path="${RELEASE_DIR}/${tarball}" + +# 1. 生成源码包到 release 目录 +if [ ! -f "${tarball_path}" ]; then + echo "生成源码包 ${tarball_path}..." + bash "${SCRIPT_DIR}/package_repository.sh" +fi + +# 1.5 检测架构并准备离线 node 依赖 +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + ARCH_SUFFIX="x64" +elif [[ "$ARCH" == "aarch64" ]]; then + ARCH_SUFFIX="arm64" +else + echo "不支持的架构: $ARCH" >&2 + exit 2 +fi +# 检查 node_modules 分块文件是否存在,不存在则生成 +NEED_GEN=0 +for i in 0 1 2 3; do + if [ ! -f "${RELEASE_DIR}/offline_node_modules-${ARCH_SUFFIX}.tar.zst.part${i}" ]; then + NEED_GEN=1 + fi +done +if [ "$NEED_GEN" -eq 1 ]; then + echo "生成离线依赖..." + bash "${SCRIPT_DIR}/prepare_node_modules_offline.sh" +fi + +# 2. 初始化 rpmbuild 目录到 release 目录 +RPMBUILD_DIR="${RELEASE_DIR}/rpmbuild" +mkdir -p "${RPMBUILD_DIR}"/{BUILD,RPMS,SOURCES,SPECS,SRPMS} + +# 3. 准备 SPEC 和 SOURCES +cp "${SPEC}" "${RPMBUILD_DIR}/SPECS/" +cp "${tarball_path}" "${RPMBUILD_DIR}/SOURCES/" + +# 3.5 复制离线依赖包分块到 SOURCES +for i in 0 1 2 3; do + cp "${RELEASE_DIR}/offline_node_modules-${ARCH_SUFFIX}.tar.zst.part${i}" "${RPMBUILD_DIR}/SOURCES/" +done + +# 4. 执行 rpmbuild +echo "开始构建 RPM 包..." +rpmbuild --define "_topdir ${RPMBUILD_DIR}" -ba "${RPMBUILD_DIR}/SPECS/$(basename "${SPEC}")" + +echo "RPM 包构建完成,输出在 ${RPMBUILD_DIR}/RPMS 和 ${RPMBUILD_DIR}/SRPMS" + +# 移动构建好的 rpm 包到 release 目录 +find "${RPMBUILD_DIR}/RPMS" -type f -name '*.rpm' -exec cp -f {} "${RELEASE_DIR}/" \; +echo "所有 RPM 包已移动到 ${RELEASE_DIR}" diff --git a/build/scripts/notarize.js b/build/scripts/notarize.js new file mode 100644 index 0000000000000000000000000000000000000000..f6186ddaa0d54e780e353af8ed1e341fb4c4945e --- /dev/null +++ b/build/scripts/notarize.js @@ -0,0 +1,23 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +// +// notarize.js +const path = require('path'); + +// 加载.env文件中的环境变量 +require('dotenv').config({ path: path.resolve(process.cwd(), '.env') }); + +// 保留此空函数以确保 electron-builder 触发 notarize 逻辑 +exports.default = async function notarizing(context) { + const { electronPlatformName } = context; + if (electronPlatformName !== 'darwin') { + return; + } +}; diff --git a/build/scripts/package_repository.sh b/build/scripts/package_repository.sh new file mode 100755 index 0000000000000000000000000000000000000000..08840ac0946ed4770dda60c31b848b38d0db682f --- /dev/null +++ b/build/scripts/package_repository.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -euo pipefail + +# 脚本所在目录,用于定位项目目录 +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +# 项目根目录 +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)" +# 构建输出目录 +RELEASE_DIR="${PROJECT_ROOT}/release" +mkdir -p "${RELEASE_DIR}" + +# spec 文件路径 +SPEC="${PROJECT_ROOT}/build/linux/euler-copilot-web.spec" + +# 从 spec 文件中读取 Name 和 Version +name=$(grep -E '^Name:' "${SPEC}" | head -1 | awk '{print $2}') +version=$(grep -E '^Version:' "${SPEC}" | head -1 | awk '{print $2}') + +# 输出文件名为 name-version.tar.gz +output="${name}-${version}.tar.gz" + +# 打包当前 HEAD,忽略 .gitignore,且不包含 .git 目录 +# 输出到项目根 release 目录 +git archive --format=tar --prefix="${name}-${version}/" HEAD | gzip >"${RELEASE_DIR}/${output}" + +echo "打包完成:${RELEASE_DIR}/${output}" diff --git a/build/scripts/prepare_node_modules_offline.sh b/build/scripts/prepare_node_modules_offline.sh new file mode 100755 index 0000000000000000000000000000000000000000..4f3d96b6219d684dc3c75e53d25c14e01f59407c --- /dev/null +++ b/build/scripts/prepare_node_modules_offline.sh @@ -0,0 +1,55 @@ +#!/bin/bash +# build/scripts/prepare_node_modules_offline.sh +# 用于在有网环境下准备 node_modules 和 pnpm 缓存,实现离线安装 +# 用法:在有网环境下运行一次,生成 release/offline_node_modules.tar.gz 和 release/offline_pnpm_store.tar.gz + +set -e + +WORKDIR=$(cd "$(dirname "$0")/../.." && pwd) +cd "$WORKDIR" + +RELEASE_DIR="$WORKDIR/release" +mkdir -p "$RELEASE_DIR" + +# 0. 确保已安装 pnpm +if ! command -v pnpm >/dev/null 2>&1; then + echo "未检测到 pnpm,正在全局安装..." + npm install -g pnpm +fi + +# 1. 检测架构 +ARCH=$(uname -m) +if [[ "$ARCH" == "x86_64" ]]; then + ARCH_SUFFIX="x64" +elif [[ "$ARCH" == "aarch64" ]]; then + ARCH_SUFFIX="arm64" +else + echo "不支持的架构: $ARCH" >&2 + exit 2 +fi + +# 2. 在 RELEASE_DIR 下创建快照缓存目录 +CACHE_DIR="$RELEASE_DIR/offline_build_cache_$ARCH_SUFFIX" +rm -rf "$CACHE_DIR" +mkdir -p "$CACHE_DIR" + +# 3. 拷贝当前代码仓内容到缓存目录(排除 .git、release、node_modules、pnpm-lock.yaml) +rsync -a --exclude='.git' --exclude='release' --exclude='node_modules' --exclude='pnpm-lock.yaml' ./ "$CACHE_DIR/" +cd "$CACHE_DIR" + +# 3. 安装依赖 +pnpm install + +# 4. 打包 node_modules 及 pnpm-lock.yaml,并分割为4个分块 +if [ -d node_modules ]; then + if [ -f pnpm-lock.yaml ]; then + tar -cf - node_modules pnpm-lock.yaml | zstd -19 -T0 -o "$RELEASE_DIR/offline_node_modules-$ARCH_SUFFIX.tar.zst" + else + tar -cf - node_modules | zstd -19 -T0 -o "$RELEASE_DIR/offline_node_modules-$ARCH_SUFFIX.tar.zst" + fi + # 分割为4个分块 + split -d -n 4 -a 1 "$RELEASE_DIR/offline_node_modules-$ARCH_SUFFIX.tar.zst" "$RELEASE_DIR/offline_node_modules-$ARCH_SUFFIX.tar.zst.part" + rm -f "$RELEASE_DIR/offline_node_modules-$ARCH_SUFFIX.tar.zst" +fi + +echo "已生成 offline_node_modules-$ARCH_SUFFIX.tar.zst.part[0-3],可用于离线环境。" diff --git a/build/tray.png b/build/tray.png new file mode 100644 index 0000000000000000000000000000000000000000..da28f09a671cc05f4a3964179f85f95f36b5806d Binary files /dev/null and b/build/tray.png differ diff --git a/build/trayTemplate.png b/build/trayTemplate.png new file mode 100644 index 0000000000000000000000000000000000000000..32242e0d1d6b09ad3ae2a20864e77ce4519130a8 Binary files /dev/null and b/build/trayTemplate.png differ diff --git a/build/win/nsis-installer.nsh b/build/win/nsis-installer.nsh new file mode 100644 index 0000000000000000000000000000000000000000..750916ed00da1353814776bab4c94ee72aff3b42 --- /dev/null +++ b/build/win/nsis-installer.nsh @@ -0,0 +1,21 @@ +# Custom NSIS script + +# 预初始化时,根据机器位数决定默认安装路径 + +!include "x64.nsh" +!include "LogicLib.nsh" + +Var INSTALL_PATH + +!macro preInit + ${If} ${RunningX64} + SetRegView 64 + StrCpy $INSTALL_PATH "C:\Program Files\eulercopilot" + ${Else} + SetRegView 32 + StrCpy $INSTALL_PATH "C:\Program Files (x86)\eulercopilot" + ${EndIf} + + WriteRegExpandStr HKLM "${INSTALL_REGISTRY_KEY}" InstallLocation "$INSTALL_PATH" + WriteRegExpandStr HKCU "${INSTALL_REGISTRY_KEY}" InstallLocation "$INSTALL_PATH" +!macroend diff --git a/deploy/prod/Dockerfile b/deploy/prod/Dockerfile index a01ebb90a62710c90e3ec406fdda8013ca01fcaf..43f3cbf54277892caf6dc335d5f14640ae57c3e1 100644 --- a/deploy/prod/Dockerfile +++ b/deploy/prod/Dockerfile @@ -1,7 +1,8 @@ -FROM node:18.20.6-alpine +FROM node:22.14.0-alpine WORKDIR /opt/eulerCopilot-web COPY . . +ENV ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/" RUN npm install pnpm -g --registry=https://registry.npmmirror.com && \ pnpm install --registry=https://registry.npmmirror.com && \ pnpm run build diff --git a/deploy/prod/nginx.conf.tmpl b/deploy/prod/nginx.conf.tmpl index d14f201498cf11fcf6b828d0982ae644f2c0bb9a..138aacdeb405561d0e11434c8e9c6f015f664da0 100644 --- a/deploy/prod/nginx.conf.tmpl +++ b/deploy/prod/nginx.conf.tmpl @@ -67,12 +67,9 @@ http { add_header X-Content-Type-Options nosniff; add_header Referrer-Policy "no-referrer"; add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; always"; + add_header Cache-Control "no-cache"; add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data: base64;"; - add_header Cache-Control "no-cache,no-store,must-revalidate"; - add_header X-Accel-Buffering no; - add_header Pragma no-cache; - add_header Expires 0; - + limit_conn limitperip 50; ${SSL_SETTINGS} @@ -121,15 +118,11 @@ http { location /api/ { proxy_set_header X-Forwarded-For $http_x_real_ip; - add_header X-XSS-Protection "1; mode=block"; - add_header X-Content-Type-Options nosniff; - add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'"; add_header Cache-Control "no-cache,no-store,must-revalidate"; add_header X-Accel-Buffering no; - add_header Pragma no-cache; - add_header Expires 0; proxy_buffering off; proxy_intercept_errors on; + error_page 404 /404.html; limit_req zone=ratelimit burst=15 nodelay; proxy_read_timeout 500s; diff --git a/docs/development/DESKTOP_CONFIG.md b/docs/development/DESKTOP_CONFIG.md new file mode 100644 index 0000000000000000000000000000000000000000..16d53c221a91d30a72b357866d9711aa06c7f1f5 --- /dev/null +++ b/docs/development/DESKTOP_CONFIG.md @@ -0,0 +1,583 @@ +# 桌面配置管理系统开发文档 + +## 系统概述 + +桌面配置管理系统是 openEuler Intelligence 桌面应用的核心组件,负责管理应用配置、首次启动欢迎流程以及提供配置相关的 API 接口。该系统采用现代化的架构设计,提供类型安全、可扩展且健壮的配置管理解决方案。 + +### 核心特性 + +- **类型安全**: 基于 TypeScript 的完整类型定义 +- **健壮性**: 完备的错误处理和恢复机制 +- **可扩展性**: 灵活的配置结构,易于添加新配置项 +- **安全性**: 配置验证和备份恢复机制 +- **用户友好**: 首次启动引导和直观的配置界面 +- **模块化**: 清晰的 API 职责分离,避免代码重复 +- **快速响应**: 服务器验证1.5秒超时,提供快速用户反馈 + +## 系统架构 + +### 核心组件 + +#### 配置管理器 (`ConfigManager`) + +- **位置**: `electron/main/common/config.ts` +- **模式**: 单例模式 +- **职责**: 配置文件的 CRUD 操作、验证、备份和恢复 + +#### 欢迎窗口管理器 (`WelcomeWindow`) + +- **位置**: `electron/main/window/welcome.ts` +- **职责**: 首次启动流程、欢迎界面窗口管理 + +#### IPC 通信层 + +- **位置**: `electron/main/common/ipc.ts` +- **职责**: 主进程与渲染进程间的配置管理通信 + +#### 预加载脚本 + +- **主预加载**: `electron/preload/index.ts` - 提供主程序完整功能API +- **欢迎预加载**: `electron/preload/welcome.ts` - 提供欢迎界面专用API +- **共享模块**: `electron/preload/shared.ts` - 提供跨窗口共享的通用功能 +- **职责**: 安全的 API 桥接 + +### API 职责分离 + +#### 主程序 API (`eulercopilot`) + +- 完整的配置管理功能(增删改查) +- 窗口控制(最大化、最小化、关闭) +- 主题管理 +- 系统信息访问 + +#### 欢迎界面 API (`eulercopilotWelcome`) + +- 受限的配置管理(仅代理设置和服务器验证) +- 欢迎流程控制(显示、完成、取消) +- 基础系统信息 +- 实用工具函数 + +#### 共享功能 (`shared`) + +- 安全的IPC通信封装 +- 通用工具函数 +- 服务器验证(1.5秒快速响应) +- 代理URL设置 + +### IPC 接口 + +#### 配置管理接口 + +```typescript +'copilot:get-config' - 获取完整配置(主程序专用) +'copilot:update-config' - 更新配置(主程序专用) +'copilot:reset-config' - 重置为默认配置(主程序专用) +'copilot:set-proxy-url' - 设置代理 URL(共享功能) +'copilot:get-proxy-url' - 获取代理 URL(主程序专用) +'copilot:validate-server' - 验证服务器连接(共享功能,1.5秒超时) +``` + +#### 欢迎界面接口 + +```typescript +'copilot:show-welcome' - 显示欢迎界面 +'copilot:complete-welcome' - 完成欢迎流程 +``` + +#### 窗口控制接口 + +```typescript +'copilot:window-control' - 窗口控制(minimize/maximize/close) +'copilot:window-is-maximized' - 检查窗口最大化状态 +``` + +### 使用示例 + +#### 在渲染进程中使用配置 + +```typescript +// 主程序中使用完整API +const config = await window.eulercopilot.config.get(); +await window.eulercopilot.config.update({ base_url: 'https://new-server.com' }); + +// 欢迎界面中使用受限API +await window.eulercopilotWelcome.config.setProxyUrl('https://proxy.com'); +const result = await window.eulercopilotWelcome.config.validateServer('https://server.com'); +await window.eulercopilotWelcome.welcome.complete(); +``` + +#### 在主进程中使用配置 + +```typescript +import { getConfigManager } from './common/config'; + +const configManager = getConfigManager(); + +// 读取配置 +const config = configManager.readConfig(); + +// 更新配置 +configManager.updateConfig({ base_url: 'new-url' }); + +// 检查配置是否存在(首次启动判断) +if (!configManager.isConfigExists()) { + // 处理首次启动逻辑,显示欢迎界面 + showWelcomeWindow(); +} +``` + +### 配置文件格式 + +默认配置文件 (`desktop-config.json`) 格式: + +```json +{ + "base_url": "https://www.eulercopilot.local" +} +``` + +配置文件位置:`{userData}/Config/desktop-config.json` + +### 实现状态 + +✅ **已完成的功能**: + +- 配置管理系统(完整的CRUD操作) +- 首次启动检测和欢迎窗口自动显示 +- 三层预加载脚本架构(主程序、欢迎界面、共享模块) +- IPC通信层和API职责分离 +- 服务器验证(1.5秒快速响应) +- 配置文件备份和恢复机制 +- 开发环境构建和监控系统 + +🚧 **需要完善的功能**: + +1. **欢迎界面 UI**: 当前为基础HTML模板,需要创建完整的配置界面组件 +2. **国际化**: 为欢迎界面添加多语言支持 +3. **用户指南**: 添加更详细的配置帮助信息 + +📊 **开发环境验证结果**: + +- ✅ Vite开发服务器正常启动(端口自动检测:3000/3001/3002/...) +- ✅ 预加载脚本编译成功(主预载和欢迎预载) +- ✅ 首次启动逻辑正常工作 +- ✅ 欢迎窗口成功显示 +- ⚠️ Chrome DevTools自动填充警告(不影响功能) + +## API 参考 + +### ConfigManager 类 + +#### 接口定义 + +```typescript +export interface DesktopConfig { + base_url: string; + [key: string]: unknown; +} + +export class ConfigManager { + public static getInstance(): ConfigManager; + public isConfigExists(): boolean; + public initializeConfig(): void; + public readConfig(): DesktopConfig; + public writeConfig(config: DesktopConfig): void; + public updateConfig(updates: Partial): DesktopConfig; + public getConfigValue(key: keyof DesktopConfig): T | undefined; + public setConfigValue(key: keyof DesktopConfig, value: unknown): void; + public resetConfig(): void; +} +``` + +#### 核心方法详解 + +##### getInstance() + +- **作用**: 获取 ConfigManager 单例实例 +- **返回**: ConfigManager 实例 +- **示例**: `const manager = ConfigManager.getInstance()` + +##### isConfigExists() + +- **作用**: 检查配置文件是否存在 +- **返回**: boolean +- **示例**: `if (!manager.isConfigExists()) { /* 首次启动逻辑 */ }` + +##### readConfig() + +- **作用**: 读取完整配置,自动处理错误恢复 +- **返回**: DesktopConfig 对象 +- **特性**: + - 自动初始化不存在的配置文件 + - 验证配置有效性 + - 备份恢复机制 + - 默认配置合并 + +##### writeConfig(config) + +- **作用**: 写入完整配置 +- **参数**: config - DesktopConfig 对象 +- **特性**: + - 配置验证 + - 自动备份旧配置 + - 原子性写入 + +##### updateConfig(updates) + +- **作用**: 部分更新配置 +- **参数**: updates - Partial<DesktopConfig> 对象 +- **返回**: 更新后的完整配置 +- **示例**: `manager.updateConfig({ base_url: 'new-url' })` + +### IPC API 参考 + +#### 主要接口 + +##### 配置管理 + +```typescript +// 获取配置 +ipcRenderer.invoke('copilot:get-config'): Promise + +// 更新配置 +ipcRenderer.invoke('copilot:update-config', updates: Partial): Promise + +// 重置配置 +ipcRenderer.invoke('copilot:reset-config'): Promise + +// 设置代理 URL(便捷方法) +ipcRenderer.invoke('copilot:set-proxy-url', url: string): Promise + +// 获取代理 URL(便捷方法) +ipcRenderer.invoke('copilot:get-proxy-url'): Promise + +// 验证服务器连接 +ipcRenderer.invoke('copilot:validate-server', url: string): Promise<{ + isValid: boolean; + error?: string; + status?: number; + responseTime?: number; +}> +``` + +##### 欢迎流程 + +```typescript +// 显示欢迎窗口 +ipcRenderer.invoke('copilot:show-welcome'): Promise + +// 完成欢迎流程 +ipcRenderer.invoke('copilot:complete-welcome'): Promise +``` + +### 前端 API 使用 + +#### 通过 electronAPI 使用 + +```typescript +// 类型定义 +interface DesktopAppAPI { + config: { + get(): Promise; + update(updates: Partial): Promise; + reset(): Promise; + setProxyUrl(url: string): Promise; + getProxyUrl(): Promise; + }; + welcome: { + show(): Promise; + complete(): Promise; + }; +} + +// 使用示例 +const config = await window.eulercopilot.config.get(); +await window.eulercopilot.config.update({ base_url: 'https://new-server.com' }); +await window.eulercopilot.welcome.complete(); +``` + +## 配置文件规范 + +### 文件位置 + +- **配置目录**: `{userData}/Config/` +- **主配置文件**: `desktop-config.json` +- **备份文件**: `desktop-config.backup.json` + +其中 `{userData}` 为系统用户数据目录: + +- **Windows**: `%APPDATA%/{AppName}` +- **macOS**: `~/Library/Application Support/{AppName}` +- **Linux**: `~/.config/{AppName}` + +### 配置结构 + +```typescript +interface DesktopConfig { + base_url: string; // 后端服务器地址 + [key: string]: unknown; // 扩展字段支持 +} +``` + +### 默认配置 + +```json +{ + "base_url": "https://www.eulercopilot.local" +} +``` + +### 配置验证规则 + +1. **base_url 验证** + - 必须为非空字符串 + - 必须为有效的 URL 格式 + - 支持 HTTP 和 HTTPS 协议 + +2. **扩展性支持** + - 支持任意额外字段 + - 保持向后兼容性 + +## 欢迎流程设计 + +### 流程概述 + +1. **启动检查**: 应用启动时检查配置文件是否存在 +2. **首次启动**: 配置文件不存在时显示欢迎界面 +3. **配置设置**: 用户在欢迎界面中配置必要参数 +4. **配置保存**: 完成配置并保存到文件 +5. **继续启动**: 自动关闭欢迎界面,继续应用启动流程 + +### 欢迎窗口特性 + +```typescript +// 窗口配置 +{ + width: 720, + height: 560, + minWidth: 720, + minHeight: 560, + center: true, + resizable: false, + maximizable: false, + minimizable: false, + modal: true, + alwaysOnTop: true, + title: '欢迎使用' +} +``` + +### 欢迎界面当前实现 + +当前欢迎界面 (`electron/welcome/index.html`) 为基础HTML模板: + +```html + + + + + 欢迎使用 openEuler Intelligence + + +
+

欢迎使用 openEuler Intelligence

+
+ + +``` + +**后续开发建议**: + +1. 使用 Vue.js 或 React 创建交互式配置界面 +2. 添加服务器地址配置表单 +3. 集成服务器连接验证功能 +4. 添加配置向导和帮助文档 +5. 实现主题和样式系统 + +```typescript +interface WelcomeAPI { + config: { + get(): Promise; + update(updates: Partial): Promise; + reset(): Promise; + validateServer(url: string): Promise; + }; + welcome: { + complete(): Promise; + close(): Promise; + }; + utils: { + openExternal(url: string): Promise; + showMessageBox(options: MessageBoxOptions): Promise; + }; +} +``` + +## 开发指南 + +### 项目结构 + +```text +electron/ +├── main/ +│ ├── common/ +│ │ ├── config.ts # 配置管理器 +│ │ ├── ipc.ts # IPC 处理器 +│ │ └── cache-conf.ts # 基础配置路径和缓存路径定义 +│ └── window/ +│ └── welcome.ts # 欢迎窗口管理 +├── preload/ +| ├── shared.ts # 共享组件 +│ ├── index.ts # 主界面预加载脚本 +│ ├── welcome.ts # 欢迎界面预加载脚本 +│ └── types.ts # 类型定义 +└── welcome/ + └── index.html # 欢迎界面 HTML +``` + +### 开发环境启动和调试 + +#### 启动开发环境 + +```bash +# 进入项目目录 +cd /path/to/euler-copilot/web + +# 安装依赖 +pnpm install + +# 启动开发模式 +pnpm run dev:desktop +``` + +#### 开发环境架构 + +开发模式使用 `concurrently` 并行运行三个服务: + +- **R (Render)**: Vite开发服务器 - 负责前端渲染进程 +- **P (Preload)**: 预加载脚本构建 - 监听并重新构建预加载脚本 +- **M (Main)**: 主进程构建 - 监听并重新构建Electron主进程 + +#### 端口自动检测 + +- 默认尝试端口:3000 +- 如果被占用,自动尝试:3001, 3002, ... +- 实际端口会在终端输出中显示 + +#### 调试信息 + +启动成功时会看到以下关键日志: + +```text +[R] VITE ready in XXXms +[R] ➜ Local: http://localhost:XXXX/ +[P] main preload built successfully +[P] welcome preload built successfully +[M] Configuration file not found, showing welcome window +[M] First time startup, showing welcome window +``` + +#### 常见问题处理 + +1. **端口冲突**: 系统会自动选择可用端口 +2. **DevTools警告**: Chrome自动填充相关警告可忽略 +3. **首次启动**: 删除 `{userData}/Config/desktop-config.json` 可重置为首次启动状态 + +### 添加新配置项 + +1. **更新接口定义** + + ```typescript + // electron/main/common/config.ts + export interface DesktopConfig { + base_url: string; + new_option: string; // 新增配置项 + [key: string]: unknown; + } + ``` + +2. **更新默认配置** + + ```typescript + export const DEFAULT_CONFIG: DesktopConfig = { + base_url: 'https://www.eulercopilot.local', + new_option: 'default_value', // 新增默认值 + }; + ``` + +3. **更新验证逻辑** + + ```typescript + private validateConfig(config: any): config is DesktopConfig { + // 添加新字段验证逻辑 + if (typeof config.new_option !== 'string') { + return false; + } + // ... 其他验证 + } + ``` + +### 自定义 IPC 处理器 + +```typescript +// electron/main/common/ipc.ts +function registerCustomListeners(): void { + ipcMain.handle('copilot:custom-action', async (event, params) => { + try { + // 自定义处理逻辑 + const result = await performCustomAction(params); + return { success: true, data: result }; + } catch (error) { + console.error('Custom action failed:', error); + return { success: false, error: error.message }; + } + }); +} +``` + +## 总结 + +### 当前系统状态 + +openEuler Intelligence 桌面配置管理系统已经具备了完整的核心功能架构: + +**✅ 已实现的核心功能:** + +- **健壮的配置管理**: 完整的 CRUD 操作、备份恢复、配置验证 +- **智能首次启动**: 自动检测配置文件,无配置时显示欢迎界面 +- **模块化预加载架构**: 三层设计(主程序、欢迎界面、共享模块) +- **清晰的API职责分离**: 避免功能重复,提供专用接口 +- **快速服务器验证**: 1.5秒超时机制,提供即时用户反馈 +- **完善的开发环境**: 热重载、并行构建、自动监听 + +**🚧 需要进一步开发的功能:** + +- **欢迎界面UI**: 从基础 HTML 升级为完整的配置界面 +- **国际化支持**: 多语言界面和错误信息 +- **高级配置选项**: 主题、代理、安全设置等 + +### 开发验证结果 + +通过运行 `pnpm run dev:desktop` 验证: + +- ✅ **构建系统**: Vite + TypeScript + Electron 协同工作正常 +- ✅ **首次启动逻辑**: 正确检测配置文件缺失并显示欢迎窗口 +- ✅ **预加载脚本**: 主程序和欢迎界面预加载都成功编译 +- ✅ **IPC通信**: 进程间通信正常,API调用响应良好 +- ✅ **监听重载**: 文件变更时自动重新构建 + +### 技术特性总结 + +- **类型安全**: 基于 TypeScript 的完整类型定义和编译时检查 +- **容错能力**: 完备的错误处理、配置验证和自动恢复机制 +- **开发效率**: 热重载、并行构建、实时监听提升开发体验 +- **架构清晰**: 单例模式、职责分离、模块化设计 +- **性能优化**: 快速响应时间、异步操作、资源管理 + +该系统为 openEuler Intelligence 桌面应用提供了稳定可靠的配置管理基础,代码架构成熟,具备良好的可扩展性和维护性。 + +--- + +*文档最后更新: 2025年6月6日* +*版本: 0.9.6* +*开发环境验证: ✅ 通过* diff --git a/electron/main/common/cache-conf.ts b/electron/main/common/cache-conf.ts new file mode 100644 index 0000000000000000000000000000000000000000..d5ce45ff943cf56df37cfe901220071e984607bb --- /dev/null +++ b/electron/main/common/cache-conf.ts @@ -0,0 +1,35 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import path from 'path'; +import fs from 'node:fs'; +import { getUserDataPath } from '../node/userDataPath'; +import { productObj } from './product'; + +interface ICacheConf { + theme: 'system' | 'light' | 'dark'; + userLocale: string; +} + +export const userDataPath = getUserDataPath(productObj.name); +export const cachePath = getCachePath(); +export const commonCacheConfPath = path.join( + cachePath, + 'eulercopilot-common-storage.json', +); + +export function getCachePath(): string { + return path.join(userDataPath, 'CachedData'); +} + +export function updateConf(conf: Partial) { + const oldConf = fs.readFileSync(commonCacheConfPath, 'utf-8'); + const updateConf = { ...JSON.parse(oldConf), ...conf }; + fs.writeFileSync(commonCacheConfPath, JSON.stringify(updateConf)); +} diff --git a/electron/main/common/config.ts b/electron/main/common/config.ts new file mode 100644 index 0000000000000000000000000000000000000000..506cd7a19dbe265460c1010ce51e158fd82be9aa --- /dev/null +++ b/electron/main/common/config.ts @@ -0,0 +1,266 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import * as fs from 'node:fs'; +import * as path from 'node:path'; +import { userDataPath } from './cache-conf'; + +/** + * 桌面应用配置接口 + */ +export interface DesktopConfig { + base_url: string; + // 可扩展其他配置项 + [key: string]: unknown; +} + +/** + * 默认配置 + */ +export const DEFAULT_CONFIG: DesktopConfig = { + base_url: 'https://www.eulercopilot.local', +}; + +/** + * 配置文件路径 + */ +export const CONFIG_DIR = path.join(userDataPath, 'Config'); +export const CONFIG_FILE_PATH = path.join(CONFIG_DIR, 'desktop-config.json'); +export const CONFIG_BACKUP_PATH = path.join( + CONFIG_DIR, + 'desktop-config.backup.json', +); + +/** + * 配置管理类 + */ +export class ConfigManager { + private static instance: ConfigManager; + private config: DesktopConfig | null = null; + + private constructor() {} + + /** + * 获取单例实例 + */ + public static getInstance(): ConfigManager { + if (!ConfigManager.instance) { + ConfigManager.instance = new ConfigManager(); + } + return ConfigManager.instance; + } + + /** + * 检查配置文件是否存在 + */ + public isConfigExists(): boolean { + return fs.existsSync(CONFIG_FILE_PATH); + } + + /** + * 确保配置目录存在 + */ + private ensureConfigDir(): void { + if (!fs.existsSync(CONFIG_DIR)) { + fs.mkdirSync(CONFIG_DIR, { recursive: true }); + } + } + + /** + * 配置验证函数 + */ + private validateConfig(config: any): config is DesktopConfig { + if (!config || typeof config !== 'object') { + return false; + } + + // 检查必需的 base_url 字段 + if (typeof config.base_url !== 'string' || !config.base_url.trim()) { + return false; + } + + // 检查 URL 格式 + try { + new URL(config.base_url); + } catch { + return false; + } + + return true; + } + + /** + * 创建备份配置文件 + */ + private createBackup(config: DesktopConfig): void { + try { + fs.writeFileSync( + CONFIG_BACKUP_PATH, + JSON.stringify(config, null, 2), + 'utf8', + ); + } catch (error) { + console.warn('Failed to create config backup:', error); + } + } + + /** + * 从备份恢复配置 + */ + private restoreFromBackup(): DesktopConfig | null { + try { + if (fs.existsSync(CONFIG_BACKUP_PATH)) { + const backupData = fs.readFileSync(CONFIG_BACKUP_PATH, 'utf8'); + const backupConfig = JSON.parse(backupData); + + if (this.validateConfig(backupConfig)) { + console.log('Restored config from backup'); + return backupConfig; + } + } + } catch (error) { + console.warn('Failed to restore from backup:', error); + } + + return null; + } + + /** + * 初始化配置文件(使用默认配置) + */ + public initializeConfig(): void { + try { + this.ensureConfigDir(); + if (!this.isConfigExists()) { + this.writeConfig(DEFAULT_CONFIG); + } + } catch (error) { + console.error('Failed to initialize config:', error); + throw error; + } + } + + /** + * 读取配置文件 + */ + public readConfig(): DesktopConfig { + if (this.config) { + return { ...this.config }; + } + + try { + if (!this.isConfigExists()) { + this.initializeConfig(); + return { ...DEFAULT_CONFIG }; + } + + const configContent = fs.readFileSync(CONFIG_FILE_PATH, 'utf-8'); + const parsedConfig = JSON.parse(configContent); + + // 验证配置文件的有效性 + if (!this.validateConfig(parsedConfig)) { + console.warn('Invalid config file detected, attempting recovery...'); + + // 尝试从备份恢复 + const backupConfig = this.restoreFromBackup(); + if (backupConfig) { + this.config = backupConfig; + // 重写配置文件 + this.writeConfig(backupConfig); + return { ...backupConfig }; + } + + // 备份恢复失败,使用默认配置 + console.warn('Backup recovery failed, using default config'); + this.config = { ...DEFAULT_CONFIG }; + this.writeConfig(this.config); + return { ...this.config }; + } + + // 合并默认配置,确保必需字段存在 + this.config = { ...DEFAULT_CONFIG, ...parsedConfig }; + return { ...this.config }; + } catch (error) { + console.error('Failed to read config:', error); + // 返回默认配置 + this.config = { ...DEFAULT_CONFIG }; + return { ...this.config }; + } + } + + /** + * 写入配置文件 + */ + public writeConfig(config: DesktopConfig): void { + try { + // 验证配置 + if (!this.validateConfig(config)) { + throw new Error('Invalid config provided'); + } + + this.ensureConfigDir(); + + // 在写入新配置前,如果存在旧配置,先创建备份 + if (this.isConfigExists() && this.config) { + this.createBackup(this.config); + } + + // 写入新配置 + fs.writeFileSync( + CONFIG_FILE_PATH, + JSON.stringify(config, null, 2), + 'utf-8', + ); + this.config = { ...config }; + } catch (error) { + console.error('Failed to write config:', error); + throw error; + } + } + + /** + * 更新配置(部分更新) + */ + public updateConfig(updates: Partial): DesktopConfig { + const currentConfig = this.readConfig(); + const newConfig = { ...currentConfig, ...updates }; + this.writeConfig(newConfig); + return newConfig; + } + + /** + * 获取配置项 + */ + public getConfigValue(key: keyof DesktopConfig): T | undefined { + const config = this.readConfig(); + return config[key] as T; + } + + /** + * 设置配置项 + */ + public setConfigValue(key: keyof DesktopConfig, value: unknown): void { + this.updateConfig({ [key]: value }); + } + + /** + * 重置为默认配置 + */ + public resetConfig(): void { + this.writeConfig(DEFAULT_CONFIG); + } +} + +/** + * 获取配置管理器实例的便捷函数 + */ +export function getConfigManager(): ConfigManager { + return ConfigManager.getInstance(); +} diff --git a/electron/main/common/fs-utils.ts b/electron/main/common/fs-utils.ts new file mode 100644 index 0000000000000000000000000000000000000000..5269ce00053747212dd67e9c97e778326c3964fb --- /dev/null +++ b/electron/main/common/fs-utils.ts @@ -0,0 +1,52 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import fs from 'node:fs'; + +/** + * 创建目录(如果不存在) + * @param dir 目录路径 + * @returns 成功创建的目录路径,如果创建失败则返回undefined + */ +export async function mkdirpIgnoreError( + dir: string | undefined, +): Promise { + if (typeof dir === 'string') { + try { + if (fs.existsSync(dir)) { + return dir; + } + await fs.promises.mkdir(dir, { recursive: true }); + + return dir; + } catch { + // ignore + } + } + + return undefined; +} + +/** + * 获取用户定义的配置 + * @param dir 配置文件路径 + * @returns 配置对象 + */ +export function getUserDefinedConf(dir: string): Record { + try { + if (!fs.existsSync(dir)) { + fs.writeFileSync(dir, JSON.stringify({})); + } + + return JSON.parse(fs.readFileSync(dir, 'utf-8')); + } catch { + // Ignore error + return {}; + } +} diff --git a/electron/main/common/ipc.ts b/electron/main/common/ipc.ts new file mode 100644 index 0000000000000000000000000000000000000000..90fb15d2dc8574d0c36c47176e29da08243a8c3c --- /dev/null +++ b/electron/main/common/ipc.ts @@ -0,0 +1,277 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { ipcMain, BrowserWindow } from 'electron'; +import { toggleTheme, setSystemTheme } from './theme'; +import { getConfigManager, DesktopConfig } from './config'; +import { completeWelcomeFlow, showWelcomeWindow } from '../window/welcome'; +import { DeploymentIPCHandler } from '../deploy/main/DeploymentIPCHandler'; +import https from 'https'; +import http from 'http'; + +// 全局部署服务IPC处理程序实例 +let deploymentIPCHandler: DeploymentIPCHandler | null = null; + +/** + * 注册所有IPC监听器 + */ +export function registerIpcListeners(): void { + registerThemeListeners(); + registerWindowControlListeners(); + registerConfigListeners(); + registerWelcomeListeners(); + registerDeploymentListeners(); +} + +/** + * 注册主题相关的IPC监听器 + */ +function registerThemeListeners(): void { + // 切换主题 + ipcMain.handle('copilot:toggle', () => { + return toggleTheme(); + }); + + // 设置系统主题 + ipcMain.handle('copilot:system', () => { + setSystemTheme(); + }); +} + +/** + * 注册窗口控制相关的IPC监听器 + */ +function registerWindowControlListeners(): void { + // 添加窗口控制命令处理程序 + ipcMain.handle('copilot:window-control', (event, command) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) return; + + switch (command) { + case 'minimize': + win.minimize(); + break; + case 'maximize': + if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } + break; + case 'close': + win.close(); + break; + } + }); + + // 添加获取窗口最大化状态的处理程序 + ipcMain.handle('copilot:window-is-maximized', (event) => { + const win = BrowserWindow.fromWebContents(event.sender); + if (win) { + return win.isMaximized(); + } + return false; + }); +} + +/** + * 注册配置管理相关的IPC监听器 + */ +function registerConfigListeners(): void { + const configManager = getConfigManager(); + + // 获取配置 + ipcMain.handle('copilot:get-config', () => { + try { + return configManager.readConfig(); + } catch (error) { + console.error('Failed to get config:', error); + return null; + } + }); + + // 更新配置 + ipcMain.handle( + 'copilot:update-config', + (event, updates: Partial) => { + try { + return configManager.updateConfig(updates); + } catch (error) { + console.error('Failed to update config:', error); + return null; + } + }, + ); + + // 重置配置 + ipcMain.handle('copilot:reset-config', () => { + try { + configManager.resetConfig(); + return configManager.readConfig(); + } catch (error) { + console.error('Failed to reset config:', error); + return null; + } + }); + + // 获取代理URL + ipcMain.handle('copilot:get-proxy-url', () => { + try { + return configManager.getConfigValue('base_url') || ''; + } catch (error) { + console.error('Failed to get proxy URL:', error); + return ''; + } + }); + + // 设置代理URL + ipcMain.handle('copilot:set-proxy-url', (event, url: string) => { + try { + configManager.setConfigValue('base_url', url); + return true; + } catch (error) { + console.error('Failed to set proxy URL:', error); + return false; + } + }); + + // 验证服务器连接 + ipcMain.handle('copilot:validate-server', async (event, url: string) => { + try { + return await validateServerConnection(url); + } catch (error) { + console.error('Failed to validate server:', error); + return { + isValid: false, + error: + error instanceof Error ? error.message : '验证服务器时发生未知错误', + }; + } + }); +} + +/** + * 注册欢迎界面相关的IPC监听器 + */ +function registerWelcomeListeners(): void { + // 显示欢迎界面 + ipcMain.handle('copilot:show-welcome', () => { + try { + showWelcomeWindow(); + return true; + } catch (error) { + console.error('Failed to show welcome window:', error); + return false; + } + }); + + // 完成欢迎流程 + ipcMain.handle('copilot:complete-welcome', async () => { + try { + await completeWelcomeFlow(); + return true; + } catch (error) { + console.error('Failed to complete welcome flow:', error); + return false; + } + }); +} + +/** + * 验证服务器连接性 + */ +async function validateServerConnection(url: string): Promise<{ + isValid: boolean; + error?: string; + status?: number; + responseTime?: number; +}> { + try { + const startTime = Date.now(); + const parsedUrl = new URL(url); + const isHttps = parsedUrl.protocol === 'https:'; + const requestModule = isHttps ? https : http; + + return new Promise((resolve) => { + const timeout = 1500; // 1.5秒超时 + + const options = { + hostname: parsedUrl.hostname, + port: parsedUrl.port || (isHttps ? 443 : 80), + path: '/', + method: 'HEAD', + timeout, + // 对于HTTPS,忽略证书错误(开发环境) + rejectUnauthorized: false, + }; + + const req = requestModule.request(options, (res) => { + const responseTime = Date.now() - startTime; + req.destroy(); + + resolve({ + isValid: res.statusCode ? res.statusCode < 500 : false, + status: res.statusCode, + responseTime, + }); + }); + + req.on('error', (error: Error) => { + resolve({ + isValid: false, + error: error.message, + }); + }); + + req.on('timeout', () => { + req.destroy(); + resolve({ + isValid: false, + error: '连接超时', + }); + }); + + req.end(); + }); + } catch (error) { + return { + isValid: false, + error: error instanceof Error ? error.message : '无效的URL格式', + }; + } +} + +/** + * 注册部署服务相关的IPC监听器 + */ +function registerDeploymentListeners(): void { + // 初始化部署服务IPC处理程序 + if (!deploymentIPCHandler) { + deploymentIPCHandler = new DeploymentIPCHandler(); + } +} + +/** + * 设置部署服务的主窗口引用 + */ +export function setDeploymentMainWindow(window: BrowserWindow): void { + if (deploymentIPCHandler) { + deploymentIPCHandler.setMainWindow(window); + } +} + +/** + * 清理部署服务IPC处理程序 + */ +export function cleanupDeploymentHandlers(): void { + if (deploymentIPCHandler) { + deploymentIPCHandler.cleanup(); + deploymentIPCHandler = null; + } +} diff --git a/electron/main/common/locale.ts b/electron/main/common/locale.ts new file mode 100644 index 0000000000000000000000000000000000000000..a2d6daa99b739b11a014a09b2e91c464c8aa50f3 --- /dev/null +++ b/electron/main/common/locale.ts @@ -0,0 +1,87 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { app } from 'electron'; +import type { INLSConfiguration } from './nls'; +import { updateConf } from './cache-conf'; + +/** + * 处理中文语言环境 + * @param appLocale 应用语言环境 + * @returns 处理后的语言标识 + */ +export function processZhLocale(appLocale: string): string { + if (appLocale.startsWith('zh')) { + const region = appLocale.split('-')[1]; + + // On Windows and macOS, Chinese languages returned by + // app.getPreferredSystemLanguages() start with zh-hans + // for Simplified Chinese or zh-hant for Traditional Chinese, + // so we can easily determine whether to use Simplified or Traditional. + // However, on Linux, Chinese languages returned by that same API + // are of the form zh-XY, where XY is a country code. + // For China (CN), Singapore (SG), and Malaysia (MY) + // country codes, assume they use Simplified Chinese. + // For other cases, assume they use Traditional. + if (['hans', 'cn', 'sg', 'my'].includes(region)) { + return 'zh_cn'; + } + + return 'zh_tw'; + } + + return appLocale; +} + +/** + * 获取系统语言环境 + */ +export const getOsLocale = (): string => { + return processZhLocale( + (app.getPreferredSystemLanguages()?.[0] ?? 'en').toLowerCase(), + ); +}; + +/** + * 国际化配置解析 + * @param commonCacheConf 缓存配置 + * @param osLocale 系统语言环境 + * @returns INLSConfiguration + */ +export async function resolveNlsConfiguration( + commonCacheConf: { userLocale?: string }, + osLocale: string, +): Promise { + if (commonCacheConf.userLocale) { + return { + userLocale: commonCacheConf.userLocale, + osLocale, + resolvedLanguage: commonCacheConf.userLocale, + }; + } + + let userLocale = app.getLocale(); + + if (!userLocale) { + updateConf({ userLocale: 'en' }); + return { + userLocale: 'en', + osLocale, + resolvedLanguage: 'en', + }; + } + + userLocale = processZhLocale(userLocale.toLowerCase()); + updateConf({ userLocale: 'en' }); + return { + userLocale, + osLocale, + resolvedLanguage: osLocale, + }; +} diff --git a/electron/main/common/menu.ts b/electron/main/common/menu.ts new file mode 100644 index 0000000000000000000000000000000000000000..0a50b0cbab99a5b7e77fb74f28d5a38c4cfde8a3 --- /dev/null +++ b/electron/main/common/menu.ts @@ -0,0 +1,159 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { app, Menu, BrowserWindow } from 'electron'; +import { CHAT_SHORTCUT_KEY } from './shortcuts'; +import { createChatWindow } from '../window'; + +/** + * 构建原生应用菜单,支持中英文 + * @param nlsConfig 国际化配置 + * @returns 构建的菜单 + */ +export function buildAppMenu(nlsConfig: { resolvedLanguage: string }): Menu { + const isZh = nlsConfig.resolvedLanguage.startsWith('zh'); + const isMac = process.platform === 'darwin'; + const template = [ + // macOS App 菜单 + ...(isMac + ? [ + { + role: 'appMenu', + label: app.name, + submenu: [ + { + role: 'about', + label: isZh ? `关于 ${app.name}` : `About ${app.name}`, + }, + { type: 'separator' }, + { role: 'services', label: isZh ? '服务' : 'Services' }, + { type: 'separator' }, + { + role: 'hide', + label: isZh ? `隐藏 ${app.name}` : `Hide ${app.name}`, + }, + { role: 'hideOthers', label: isZh ? '隐藏其他' : 'Hide Others' }, + { role: 'unhide', label: isZh ? '显示全部' : 'Show All' }, + { type: 'separator' }, + { + role: 'quit', + label: isZh ? `退出 ${app.name}` : `Quit ${app.name}`, + }, + ], + }, + ] + : []), + // File 菜单 + { + role: 'fileMenu', + label: isZh ? '文件' : 'File', + submenu: [{ role: 'close', label: isZh ? '关闭窗口' : 'Close Window' }], + }, + // Edit 菜单 + { + role: 'editMenu', + label: isZh ? '编辑' : 'Edit', + submenu: [ + { role: 'undo', label: isZh ? '撤销' : 'Undo' }, + { role: 'redo', label: isZh ? '重做' : 'Redo' }, + { type: 'separator' }, + { role: 'cut', label: isZh ? '剪切' : 'Cut' }, + { role: 'copy', label: isZh ? '复制' : 'Copy' }, + { role: 'paste', label: isZh ? '粘贴' : 'Paste' }, + ...(isMac + ? [ + { + role: 'pasteAndMatchStyle', + label: isZh ? '粘贴并匹配样式' : 'Paste and Match Style', + }, + { role: 'delete', label: isZh ? '删除' : 'Delete' }, + { role: 'selectAll', label: isZh ? '全选' : 'Select All' }, + ] + : [ + { role: 'delete', label: isZh ? '删除' : 'Delete' }, + { type: 'separator' }, + { role: 'selectAll', label: isZh ? '全选' : 'Select All' }, + ]), + ], + }, + // View 菜单 + { + role: 'viewMenu', + label: isZh ? '显示' : 'View', + submenu: [ + { role: 'reload', label: isZh ? '重新加载' : 'Reload' }, + { + role: 'forcereload', + label: isZh ? '强制重新加载' : 'Force Reload', + }, + { + role: 'toggleDevTools', + label: isZh ? '切换开发者工具' : 'Toggle Developer Tools', + }, + { type: 'separator' }, + { role: 'resetZoom', label: isZh ? '重置缩放' : 'Reset Zoom' }, + { role: 'zoomIn', label: isZh ? '放大' : 'Zoom In' }, + { role: 'zoomOut', label: isZh ? '缩小' : 'Zoom Out' }, + { type: 'separator' }, + { + role: 'togglefullscreen', + label: isZh ? '切换全屏' : 'Toggle Fullscreen', + }, + ], + }, + // Window 菜单 + { + role: 'windowMenu', + label: isZh ? '窗口' : 'Window', + submenu: [ + { role: 'minimize', label: isZh ? '最小化' : 'Minimize' }, + { role: 'zoom', label: isZh ? '缩放' : 'Zoom' }, + { type: 'separator' }, + { + label: isZh ? '打开快捷问答' : 'Open Quick Chat', + accelerator: CHAT_SHORTCUT_KEY, + click: () => { + const chat = BrowserWindow.getAllWindows().find((win) => + win.webContents.getURL().includes('chat'), + ); + if (chat) { + if (chat.isMinimized()) chat.restore(); + chat.show(); + chat.focus(); + } else { + createChatWindow().show(); + } + }, + }, + { type: 'separator' }, + ...(isMac + ? [ + { + role: 'front', + label: isZh ? '前置全部窗口' : 'Bring All to Front', + }, + ] + : []), + ], + }, + // Help 菜单 + { + role: 'help', + label: isZh ? '帮助' : 'Help', + submenu: [ + { label: isZh ? '文档' : 'Documentation', click: () => {} }, + { label: isZh ? '社区' : 'Community Discussions', click: () => {} }, + { label: isZh ? '搜索问题' : 'Search Issues', click: () => {} }, + ], + }, + ]; + return Menu.buildFromTemplate( + template as Electron.MenuItemConstructorOptions[], + ); +} diff --git a/electron/main/common/nls.ts b/electron/main/common/nls.ts new file mode 100644 index 0000000000000000000000000000000000000000..e16be02f0d6001cd3b52dc428d4a27b5de07cc16 --- /dev/null +++ b/electron/main/common/nls.ts @@ -0,0 +1,26 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +export interface INLSConfiguration { + /** + * Locale as defined in `argv.json` or `app.getLocale()`. + */ + readonly userLocale: string; + + /** + * Locale as defined by the OS (e.g. `app.getPreferredSystemLanguages()`). + */ + readonly osLocale: string; + + /** + * The actual language of the UI that ends up being used considering `userLocale` + * and `osLocale`. + */ + readonly resolvedLanguage: string; +} diff --git a/electron/main/common/platform.ts b/electron/main/common/platform.ts new file mode 100644 index 0000000000000000000000000000000000000000..1d499b4250a8fd91ab390ee32c5c14dc5757f703 --- /dev/null +++ b/electron/main/common/platform.ts @@ -0,0 +1,92 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import * as nls from './nls'; + +export const LANGUAGE_DEFAULT = 'zh_cn'; + +let _isWindows = false; +let _isMacintosh = false; +let _isLinux = false; +let _isElectron = false; +let _locale: string | undefined = undefined; +let _language: string = LANGUAGE_DEFAULT; + +export interface IProcessEnvironment { + [key: string]: string | undefined; +} + +/** + * This interface is intentionally not identical to node.js + * process because it also works in sandboxed environments + * where the process object is implemented differently. We + * define the properties here that we need for `platform` + * to work and nothing else. + */ +export interface INodeProcess { + platform: string; + arch: string; + env: IProcessEnvironment; + versions?: { + node?: string; + electron?: string; + chrome?: string; + }; + type?: string; + cwd: () => string; +} + +let nodeProcess: INodeProcess | undefined = process; + +const isElectronProcess = typeof nodeProcess?.versions?.electron === 'string'; + +if (typeof nodeProcess === 'object') { + _isWindows = nodeProcess.platform === 'win32'; + _isMacintosh = nodeProcess.platform === 'darwin'; + _isLinux = nodeProcess.platform === 'linux'; + _isElectron = isElectronProcess; + _locale = LANGUAGE_DEFAULT; + _language = LANGUAGE_DEFAULT; + const rawNlsConfig = nodeProcess.env['EULERCOPILOT_NLS_CONFIG']; + if (rawNlsConfig) { + try { + const nlsConfig: nls.INLSConfiguration = JSON.parse(rawNlsConfig); + _locale = nlsConfig.userLocale; + _language = nlsConfig.resolvedLanguage || LANGUAGE_DEFAULT; + } catch (e) {} + } +} else { + console.error('Unable to resolve platform.'); +} + +export const enum Platform { + Web, + Mac, + Linux, + Windows, +} +export type PlatformName = 'Web' | 'Windows' | 'Mac' | 'Linux'; + +let _platform: Platform = Platform.Web; +if (_isMacintosh) { + _platform = Platform.Mac; +} else if (_isWindows) { + _platform = Platform.Windows; +} else if (_isLinux) { + _platform = Platform.Linux; +} + +export const isWindows = _isWindows; +export const isMacintosh = _isMacintosh; +export const isLinux = _isLinux; +export const isElectron = _isElectron; +export const platform = _platform; + +export const locale = _locale; +export const language = _language; diff --git a/electron/main/common/product.ts b/electron/main/common/product.ts new file mode 100644 index 0000000000000000000000000000000000000000..c414ac62c6c019abc1e0826c063359ca49b89584 --- /dev/null +++ b/electron/main/common/product.ts @@ -0,0 +1,18 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { build } from '../../../package.json'; + +export interface IProductConfiguration { + readonly name: string; +} + +export const productObj: IProductConfiguration = { + name: build.productName, +}; diff --git a/electron/main/common/shortcuts.ts b/electron/main/common/shortcuts.ts new file mode 100644 index 0000000000000000000000000000000000000000..f11889adbf90695cfa9f467304b46fbd75c38696 --- /dev/null +++ b/electron/main/common/shortcuts.ts @@ -0,0 +1,86 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { app, globalShortcut, BrowserWindow, dialog } from 'electron'; +import { createChatWindow } from '../window'; + +// 定义全局快捷键 +export const CHAT_SHORTCUT_KEY = + process.platform === 'darwin' ? 'Cmd+Option+O' : 'Ctrl+Alt+O'; + +// 快捷键是否已注册 +let isShortcutRegistered = false; + +/** + * 在macOS上,检查是否已经获得辅助功能权限 + */ +export function checkAccessibilityPermission(): boolean { + if (process.platform !== 'darwin') return true; + + try { + return app.isAccessibilitySupportEnabled(); + } catch (err) { + console.error('Failed to check accessibility permission:', err); + return false; + } +} + +/** + * 注册全局快捷键 + * @returns 是否注册成功 + */ +export function registerGlobalShortcut(): boolean { + // 如果已经注册了快捷键,先取消注册 + if (isShortcutRegistered) { + globalShortcut.unregister(CHAT_SHORTCUT_KEY); + } + + // 注册新的快捷键 + const success = globalShortcut.register(CHAT_SHORTCUT_KEY, () => { + const chatWindow = BrowserWindow.getAllWindows().find((win) => + win.webContents.getURL().includes('chat'), + ); + + if (chatWindow) { + if (chatWindow.isMinimized()) chatWindow.restore(); + chatWindow.show(); + chatWindow.focus(); + } else { + // 如果没有找到聊天窗口,则创建一个新的 + const newChatWindow = createChatWindow(); + newChatWindow.show(); + newChatWindow.focus(); + } + }); + + isShortcutRegistered = success; + + if (!success) { + console.error('Failed to register global shortcut'); + // 在macOS上,提示用户需要授予辅助功能权限 + if (process.platform === 'darwin' && !checkAccessibilityPermission()) { + dialog.showMessageBox({ + type: 'info', + title: '需要辅助功能权限', + message: `要使用快捷键 ${CHAT_SHORTCUT_KEY} 功能,请在系统偏好设置中,授予应用辅助功能权限。`, + buttons: ['好的'], + }); + } + } + + return success; +} + +/** + * 注销所有全局快捷键 + */ +export function unregisterAllShortcuts(): void { + globalShortcut.unregisterAll(); + isShortcutRegistered = false; +} diff --git a/electron/main/common/theme.ts b/electron/main/common/theme.ts new file mode 100644 index 0000000000000000000000000000000000000000..006e1ca564ebbf882b6823aaa2c3f8166c840ec2 --- /dev/null +++ b/electron/main/common/theme.ts @@ -0,0 +1,67 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { nativeTheme } from 'electron'; +import { updateConf } from './cache-conf'; + +export type ThemeType = 'system' | 'light' | 'dark'; + +/** + * 解析主题配置 + * @param commonCacheConf 缓存的配置信息 + * @returns 主题配置信息 + */ +export async function resolveThemeConfiguration(commonCacheConf: { + theme?: ThemeType; +}): Promise<{ theme: ThemeType }> { + if (commonCacheConf.theme) { + return { + theme: commonCacheConf.theme, + }; + } + + const isDarkMode = nativeTheme.shouldUseDarkColors; + const theme = isDarkMode ? 'dark' : 'light'; + + updateConf({ theme }); + + return { theme }; +} + +/** + * 设置应用主题 + * @param theme 主题类型 + */ +export function setApplicationTheme(theme: ThemeType): void { + nativeTheme.themeSource = theme; + process.env['EULERCOPILOT_THEME'] = theme; +} + +/** + * 切换明暗主题 + * @returns 是否为暗色主题 + */ +export function toggleTheme(): boolean { + if (nativeTheme.shouldUseDarkColors) { + nativeTheme.themeSource = 'light'; + updateConf({ theme: 'light' }); + } else { + nativeTheme.themeSource = 'dark'; + updateConf({ theme: 'dark' }); + } + return nativeTheme.shouldUseDarkColors; +} + +/** + * 设置为系统主题 + */ +export function setSystemTheme(): void { + nativeTheme.themeSource = 'system'; + updateConf({ theme: 'system' }); +} diff --git a/electron/main/deploy/core/DeploymentService.ts b/electron/main/deploy/core/DeploymentService.ts new file mode 100644 index 0000000000000000000000000000000000000000..6ed4840b6b1295841e545b734a94e48d28115de8 --- /dev/null +++ b/electron/main/deploy/core/DeploymentService.ts @@ -0,0 +1,2201 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import * as path from 'path'; +import * as fs from 'fs'; +import { exec, spawn } from 'child_process'; +import { getCachePath } from '../../common/cache-conf'; +import type { + DeploymentParams, + DeploymentStatus, +} from '../types/deployment.types'; +import { + EnvironmentChecker, + type EnvironmentCheckResult, +} from './EnvironmentChecker'; +import { ValuesYamlManager } from './ValuesYamlManager'; + +/** + * 支持中断的异步执行函数 + */ +const execAsyncWithAbort = ( + command: string, + options: any = {}, + abortSignal?: AbortSignal, +): Promise<{ stdout: string; stderr: string }> => { + return new Promise((resolve, reject) => { + const childProcess = exec(command, options, (error, stdout, stderr) => { + if (error) { + reject(error); + } else { + resolve({ + stdout: typeof stdout === 'string' ? stdout : stdout.toString(), + stderr: typeof stderr === 'string' ? stderr : stderr.toString(), + }); + } + }); + + // 如果提供了中断信号,监听中断事件 + if (abortSignal) { + abortSignal.addEventListener('abort', () => { + childProcess.kill('SIGTERM'); + reject(new Error('部署进程已被用户停止')); + }); + } + }); +}; + +/** + * 部署服务核心类 + */ +export class DeploymentService { + private cachePath: string; + private deploymentPath: string; + private environmentChecker: EnvironmentChecker; + private valuesYamlManager: ValuesYamlManager; + private environmentCheckResult?: EnvironmentCheckResult; + private currentStatus: DeploymentStatus = { + status: 'idle', + message: '', + currentStep: 'idle', + }; + private statusCallback?: (status: DeploymentStatus) => void; + private abortController?: AbortController; + private sudoSessionActive: boolean = false; + private sudoHelperProcess?: any; + private sudoHelperMonitorInterval?: NodeJS.Timeout; + private activeCommandStartTime?: number; // 记录当前活跃命令的开始时间 + private isCommandExecuting: boolean = false; // 标记是否有命令正在执行 + + constructor() { + this.cachePath = getCachePath(); + // 创建专门的部署工作目录 + this.deploymentPath = path.join( + this.cachePath, + 'deployment', + 'euler-copilot-framework', + ); + this.environmentChecker = new EnvironmentChecker(); + this.valuesYamlManager = new ValuesYamlManager(); + } + + /** + * 设置状态回调函数 + */ + setStatusCallback(callback: (status: DeploymentStatus) => void) { + this.statusCallback = callback; + } + + /** + * 更新部署状态 + */ + private updateStatus(status: Partial) { + // 验证输入状态 + if (!status || typeof status !== 'object') { + if (process.env.NODE_ENV === 'development') { + console.warn('DeploymentService: 尝试更新无效状态:', status); + } + return; + } + + this.currentStatus = { ...this.currentStatus, ...status }; + + // 确保 currentStep 总是存在 + if (!this.currentStatus.currentStep) { + this.currentStatus.currentStep = 'unknown'; + } + + // 调试信息:仅在开发环境下记录状态更新 + if (process.env.NODE_ENV === 'development') { + console.log('🔄 DeploymentService: 状态更新', { + status: this.currentStatus.status, + currentStep: this.currentStatus.currentStep, + message: this.currentStatus.message, + hasCallback: !!this.statusCallback, + }); + } + + if (this.statusCallback) { + try { + this.statusCallback(this.currentStatus); + if (process.env.NODE_ENV === 'development') { + console.log('✅ DeploymentService: 状态回调已调用'); + } + } catch (error) { + console.error('❌ DeploymentService: 状态回调执行失败:', error); + } + } else { + if (process.env.NODE_ENV === 'development') { + console.warn('⚠️ DeploymentService: 没有设置状态回调函数'); + } + } + } + + /** + * 获取当前状态 + */ + getStatus(): DeploymentStatus { + return { ...this.currentStatus }; + } + + /** + * 开始部署流程 + */ + async startDeployment(params: DeploymentParams): Promise { + try { + // 创建新的 AbortController 用于控制部署流程 + this.abortController = new AbortController(); + + // 第一阶段:准备安装环境 + this.updateStatus({ + status: 'preparing', + message: '准备安装环境...', + currentStep: 'preparing-environment', + }); + + // 1. 检查环境 + await this.checkEnvironment(); + + // 2. 克隆仓库 + await this.cloneRepository(); + + // 3. 在Linux系统上,一次性获取sudo权限并设置环境 + await this.initializeSudoSession(); + + // 4. 配置 values.yaml + await this.configureValues(params); + + // 5. 执行部署脚本中的工具安装部分(如果需要) + await this.installTools(); + + // 6. 验证K8s集群状态 + await this.verifyK8sCluster(); + + // 更新准备环境完成状态 + this.updateStatus({ + message: '准备安装环境完成', + currentStep: 'environment-ready', + }); + + // 第二到第四阶段:按顺序安装各个服务 + await this.executeDeploymentScripts(); + + this.updateStatus({ + status: 'success', + message: '部署完成!', + currentStep: 'completed', + }); + } catch (error) { + // 如果是因为手动停止导致的错误,使用停止状态 + if (this.abortController?.signal.aborted) { + this.updateStatus({ + status: 'idle', + message: '部署已停止', + currentStep: 'stopped', + }); + } else { + // 如果错误还没有被处理(设置status为error),在这里处理 + if (this.currentStatus.status !== 'error') { + const friendlyMessage = this.getUserFriendlyErrorMessage( + error, + '部署过程', + ); + this.updateStatus({ + status: 'error', + message: friendlyMessage, + currentStep: 'failed', + }); + } + throw error; + } + } finally { + // 清理资源 + this.abortController = undefined; + this.sudoSessionActive = false; // 重置sudo会话状态 + this.cleanupSudoHelper(); // 清理sudo助手进程 + } + } + + /** + * 检查环境 + */ + private async checkEnvironment(): Promise { + try { + this.updateStatus({ + status: 'preparing', + message: '检查系统环境...', + currentStep: 'preparing-environment', + }); + + // 检查 root 权限(仅限 Linux) + try { + await this.checkRootPermission(); + } catch (error) { + throw new Error( + `权限检查失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + let checkResult; + try { + checkResult = await this.environmentChecker.checkAll(); + } catch (error) { + throw new Error( + `系统环境检查失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 存储检查结果,用于后续决定是否需要执行 2-install-tools + // 基础工具的安装将在 initializeSudoSession 中处理 + this.environmentCheckResult = checkResult; + + // 检查是否有严重错误 + if (!checkResult.success) { + throw new Error(`环境检查未通过: ${checkResult.errors.join(', ')}`); + } + + this.updateStatus({ + message: '环境检查通过', + currentStep: 'preparing-environment', + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + this.updateStatus({ + status: 'error', + message: `环境检查阶段失败: ${errorMessage}`, + currentStep: 'preparing-environment', + }); + throw error; + } + } + + /** + * 克隆远程仓库 + */ + private async cloneRepository(): Promise { + try { + this.updateStatus({ + status: 'cloning', + message: '克隆部署仓库...', + currentStep: 'preparing-environment', + }); + + // 确保部署目录的父目录存在 + const deploymentParentDir = path.dirname(this.deploymentPath); + try { + if (!fs.existsSync(deploymentParentDir)) { + fs.mkdirSync(deploymentParentDir, { recursive: true }); + } + } catch (error) { + throw new Error( + `创建部署目录失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 检查是否已经克 clone 过 + const gitDir = path.join(this.deploymentPath, '.git'); + if (fs.existsSync(gitDir)) { + try { + // 已存在,执行 git pull 更新 + await execAsyncWithAbort( + 'git pull origin master', + { cwd: this.deploymentPath }, + this.abortController?.signal, + ); + this.updateStatus({ + message: '更新部署仓库完成', + currentStep: 'preparing-environment', + }); + } catch (error) { + throw new Error( + `更新仓库失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } else { + try { + // 不存在,克隆仓库 + const repoUrl = + 'https://gitee.com/openeuler/euler-copilot-framework.git'; + await execAsyncWithAbort( + `git clone ${repoUrl} ${path.basename(this.deploymentPath)}`, + { + cwd: deploymentParentDir, + }, + this.abortController?.signal, + ); + this.updateStatus({ + message: '克隆部署仓库完成', + currentStep: 'preparing-environment', + }); + } catch (error) { + throw new Error( + `克隆仓库失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + this.updateStatus({ + status: 'error', + message: `仓库操作阶段失败: ${errorMessage}`, + currentStep: 'preparing-environment', + }); + throw error; + } + } + + /** + * 配置 values.yaml 文件 + */ + private async configureValues(params: DeploymentParams): Promise { + try { + this.updateStatus({ + status: 'configuring', + message: '配置部署参数...', + currentStep: 'preparing-environment', + }); + + const valuesPath = path.join( + this.deploymentPath, + 'deploy/chart/euler_copilot/values.yaml', + ); + + // 检查 values.yaml 文件是否存在 + if (!fs.existsSync(valuesPath)) { + throw new Error(`配置文件不存在: ${valuesPath}`); + } + + try { + await this.valuesYamlManager.updateModelsConfig(valuesPath, params); + } catch (error) { + throw new Error( + `更新配置文件失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + this.updateStatus({ + message: '配置部署参数完成', + currentStep: 'preparing-environment', + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + this.updateStatus({ + status: 'error', + message: `配置阶段失败: ${errorMessage}`, + currentStep: 'preparing-environment', + }); + throw error; + } + } + + /** + * 安装工具(准备环境的一部分) + */ + private async installTools(): Promise { + try { + // 检查是否需要安装 K8s 工具 + if (!this.environmentCheckResult?.needsK8sToolsInstall) { + this.updateStatus({ + message: 'K8s 工具已存在,跳过工具安装', + currentStep: 'preparing-environment', + }); + return; + } + + this.updateStatus({ + status: 'preparing', + message: '安装 K8s 工具 (kubectl, helm, k3s)...', + currentStep: 'preparing-environment', + }); + + const scriptsPath = path.join(this.deploymentPath, 'deploy/scripts'); + const toolsScriptPath = path.join( + scriptsPath, + '2-install-tools/install_tools.sh', + ); + + // 检查脚本文件是否存在 + if (!fs.existsSync(toolsScriptPath)) { + throw new Error(`工具安装脚本不存在: ${toolsScriptPath}`); + } + + try { + // 直接使用已建立的sudo会话执行脚本 + const envVars = { + KUBECONFIG: '/etc/rancher/k3s/k3s.yaml', + }; + + // 构建环境变量字符串 + const envString = Object.entries(envVars) + .map(([key, value]) => `${key}="${value}"`) + .join(' '); + + // 执行脚本 + await this.executeSudoCommand( + `${envString} bash "${toolsScriptPath}"`, + 600000, // 10分钟超时,k3s安装可能需要较长时间 + ); + } catch (error) { + // 检查是否是超时错误 + if (error instanceof Error && error.message.includes('timeout')) { + throw new Error('K8s 工具安装超时,可能网络较慢或下载失败'); + } + throw new Error( + `K8s 工具安装执行失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + this.updateStatus({ + message: 'K8s 工具安装完成', + currentStep: 'preparing-environment', + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + this.updateStatus({ + status: 'error', + message: `工具安装阶段失败: ${errorMessage}`, + currentStep: 'preparing-environment', + }); + throw error; + } + } + + /** + * 验证K8s集群状态(确保k3s正常运行) + */ + private async verifyK8sCluster(): Promise { + // 只在 Linux 系统上需要验证k3s + if (process.platform !== 'linux') { + return; + } + + try { + this.updateStatus({ + status: 'preparing', + message: '验证 K8s 集群状态...', + currentStep: 'preparing-environment', + }); + + // 1. 检查k3s服务状态 + try { + await this.checkK3sService(); + } catch (error) { + throw new Error( + `k3s 服务检查失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 2. 等待k3s服务完全启动 + try { + await this.waitForK3sReady(); + } catch (error) { + throw new Error( + `k3s 服务启动验证失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 3. 验证kubectl连接 + try { + await this.verifyKubectlConnection(); + } catch (error) { + throw new Error( + `kubectl 连接验证失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + this.updateStatus({ + message: 'K8s 集群验证通过', + currentStep: 'preparing-environment', + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + this.updateStatus({ + status: 'error', + message: `K8s 集群验证阶段失败: ${errorMessage}`, + currentStep: 'preparing-environment', + }); + throw error; + } + } + + /** + * 检查k3s服务状态 + */ + private async checkK3sService(): Promise { + try { + const { stdout } = await execAsyncWithAbort( + 'systemctl is-active k3s', + {}, + this.abortController?.signal, + ); + + if (stdout.trim() !== 'active') { + // 尝试启动k3s服务 + await this.executeSudoCommand('systemctl start k3s', 30000); + + // 再次检查状态 + const { stdout: newStatus } = await execAsyncWithAbort( + 'systemctl is-active k3s', + {}, + this.abortController?.signal, + ); + + if (newStatus.trim() !== 'active') { + throw new Error('k3s 服务启动失败'); + } + } + } catch (error) { + throw new Error( + `k3s 服务检查失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 等待k3s服务完全启动(最多等待60秒) + */ + private async waitForK3sReady(): Promise { + const maxWaitTime = 60000; // 60秒 + const checkInterval = 5000; // 5秒检查一次 + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitTime) { + try { + // 检查k3s.yaml文件是否存在且可读 + const { stdout } = await this.executeSudoCommand( + 'ls -la /etc/rancher/k3s/k3s.yaml', + 10000, + ); + + if (stdout.includes('k3s.yaml')) { + // 文件存在,等待几秒确保内容完整 + await new Promise((resolve) => setTimeout(resolve, 3000)); + return; + } + } catch { + // 文件还不存在,继续等待 + } + + // 等待一段时间后重试 + await new Promise((resolve) => setTimeout(resolve, checkInterval)); + } + + throw new Error('k3s 配置文件生成超时,服务可能启动失败'); + } + + /** + * 验证kubectl连接 + */ + private async verifyKubectlConnection(): Promise { + try { + // 设置KUBECONFIG环境变量并测试连接 + const kubeconfigPath = '/etc/rancher/k3s/k3s.yaml'; + + // 使用sudo权限执行kubectl命令,因为k3s.yaml文件只有root用户可以读取 + const { stdout } = await this.executeSudoCommand( + `KUBECONFIG=${kubeconfigPath} kubectl cluster-info`, + 15000, + { KUBECONFIG: kubeconfigPath }, + ); + + if (!stdout.includes('is running at')) { + throw new Error('kubectl 无法连接到 k3s 集群'); + } + + // 验证节点状态 + const { stdout: nodeStatus } = await this.executeSudoCommand( + `KUBECONFIG=${kubeconfigPath} kubectl get nodes`, + 15000, + { KUBECONFIG: kubeconfigPath }, + ); + + if (!nodeStatus.includes('Ready')) { + throw new Error('k3s 节点状态异常'); + } + } catch (error) { + throw new Error( + `kubectl 连接验证失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 执行部署脚本 + */ + private async executeDeploymentScripts(): Promise { + const scriptsPath = path.join(this.deploymentPath, 'deploy/scripts'); + + // 按照 timeLine.vue 中的步骤定义,执行指定的脚本(排除工具安装,因为已在准备环境阶段执行) + const scripts = [ + { + name: '6-install-databases', + path: '6-install-databases/install_databases.sh', + displayName: '数据库服务', + step: 'install-databases', + envVars: {}, + }, + { + name: '7-install-authhub', + path: '7-install-authhub/install_authhub.sh', + displayName: 'AuthHub 服务', + step: 'install-authhub', + envVars: { + // 通过环境变量或输入重定向避免交互 + AUTHHUB_DOMAIN: 'authhub.eulercopilot.local', + }, + useInputRedirection: true, // 标记需要输入重定向 + }, + { + name: '8-install-EulerCopilot', + path: '8-install-EulerCopilot/install_eulercopilot.sh', + displayName: 'Intelligence 服务', + step: 'install-intelligence', + envVars: { + // install_eulercopilot.sh 已支持这些环境变量 + EULERCOPILOT_DOMAIN: 'www.eulercopilot.local', + AUTHHUB_DOMAIN: 'authhub.eulercopilot.local', + // 设置非交互模式标志 + CI: 'true', + DEBIAN_FRONTEND: 'noninteractive', + }, + }, + ]; + + for (let i = 0; i < scripts.length; i++) { + const script = scripts[i]; + + try { + this.updateStatus({ + status: 'deploying', + message: `正在安装 ${script.displayName}...`, + currentStep: script.step, + }); + + const scriptPath = path.join(scriptsPath, script.path); + + // 检查脚本文件是否存在 + if (!fs.existsSync(scriptPath)) { + throw new Error(`脚本文件不存在: ${scriptPath}`); + } + + // 构建需要权限的命令 + const envVars = { + ...script.envVars, + // 确保 KUBECONFIG 环境变量正确设置 + KUBECONFIG: '/etc/rancher/k3s/k3s.yaml', + }; + + // 过滤掉 undefined 值,确保所有值都是字符串 + const cleanEnvVars = Object.fromEntries( + Object.entries(envVars).filter(([, value]) => value !== undefined), + ) as Record; + + try { + // 使用已建立的sudo会话执行脚本,避免重复输入密码 + let command = `bash "${scriptPath}"`; + + if ( + script.useInputRedirection && + script.useInputRedirection === true + ) { + // 对于需要输入重定向的脚本,预设输入内容 + const inputData = 'authhub.eulercopilot.local'; + command = `echo "${inputData}" | ${command}`; + } + + // 增加详细日志 + if (process.env.NODE_ENV === 'development') { + console.log(`执行脚本: ${script.displayName}`); + console.log(`脚本路径: ${scriptPath}`); + console.log(`执行命令: ${command}`); + console.log(`环境变量:`, cleanEnvVars); + console.log(`超时时间: ${600000}ms (10分钟)`); + } + + await this.executeSudoCommand( + command, + 600000, // 10分钟超时,某些服务安装可能需要较长时间 + cleanEnvVars, + ); + } catch (error) { + // 检查是否是超时错误 + if (error instanceof Error && error.message.includes('timeout')) { + throw new Error( + `${script.displayName} 安装超时,可能网络较慢或下载失败`, + ); + } + // 检查是否是权限错误 + if ( + error instanceof Error && + (error.message.includes('permission denied') || + error.message.includes('Access denied')) + ) { + throw new Error( + `${script.displayName} 安装权限不足,请确保有管理员权限`, + ); + } + // 检查是否是网络错误 + if ( + error instanceof Error && + (error.message.includes('network') || + error.message.includes('connection') || + error.message.includes('resolve')) + ) { + throw new Error( + `${script.displayName} 安装网络错误,请检查网络连接`, + ); + } + throw new Error( + `${script.displayName} 安装失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + + // 更新完成状态 + this.updateStatus({ + message: `${script.displayName} 安装完成`, + currentStep: script.step, + }); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + this.updateStatus({ + status: 'error', + message: `${script.displayName} 安装失败: ${errorMessage}`, + currentStep: script.step, + }); + throw error; + } + } + } + + /** + * 检查并确保有 root 权限或 sudo 权限(仅限 Linux 系统) + */ + private async checkRootPermission(): Promise { + // 只在 Linux 系统上检查权限 + if (process.platform !== 'linux') { + return; + } + + try { + // 检查当前用户 ID,0 表示 root + const { stdout } = await execAsyncWithAbort( + 'id -u', + {}, + this.abortController?.signal, + ); + const uid = parseInt(stdout.trim(), 10); + + // 如果是 root 用户,直接通过 + if (uid === 0) { + return; + } + + // 如果不是 root 用户,检查是否有 sudo 权限 + try { + // 检查用户是否在管理员组中(sudo、wheel、admin) + const { stdout: groupsOutput } = await execAsyncWithAbort( + 'groups', + {}, + this.abortController?.signal, + ); + const userGroups = groupsOutput.trim().split(/\s+/); + + // 检查常见的管理员组 + const adminGroups = ['sudo', 'wheel', 'admin']; + const hasAdminGroup = adminGroups.some((group) => + userGroups.includes(group), + ); + + if (hasAdminGroup) { + // 用户在管理员组中,具有 sudo 权限 + // 在实际执行时,buildRootCommand 会使用适当的图形化 sudo 工具 + return; + } + + // 如果不在管理员组中,尝试检查是否有无密码 sudo 权限 + try { + await execAsyncWithAbort( + 'sudo -n true', + { timeout: 3000 }, + this.abortController?.signal, + ); + // 如果成功,说明用户有无密码 sudo 权限 + return; + } catch { + // 用户既不在管理员组中,也没有无密码 sudo 权限 + throw new Error( + '部署脚本需要管理员权限才能执行。请确保当前用户具有 sudo 权限。', + ); + } + } catch (error) { + if ( + error instanceof Error && + error.message.includes('部署脚本需要管理员权限') + ) { + throw error; + } + // 无法检查组信息,假设用户可能有权限,在实际执行时再处理 + // 这样避免过于严格的权限检查阻止部署 + return; + } + } catch (error) { + if ( + error instanceof Error && + (error.message.includes('部署脚本需要 root 权限') || + error.message.includes('用户具有管理员权限')) + ) { + throw error; + } + throw new Error('无法检查用户权限'); + } + } + + /** + * 初始化sudo会话,一次性获取权限并安装缺失工具、设置脚本权限 + */ + private async initializeSudoSession(): Promise { + // 只在 Linux 系统上需要sudo会话 + if (process.platform !== 'linux') { + return; + } + + // 检查是否为root用户,如果是则不需要sudo + if (process.getuid && process.getuid() === 0) { + this.sudoSessionActive = true; + return; + } + + try { + this.updateStatus({ + status: 'preparing', + message: '获取管理员权限并初始化环境...', + currentStep: 'preparing-environment', + }); + + // 启动sudo助手进程 + await this.startSudoHelper(); + + // 检查是否需要安装基础工具 + const missingTools = this.environmentCheckResult?.missingBasicTools || []; + + // 构建一次性执行的命令列表 + const commands: string[] = []; + + if (missingTools.length > 0) { + // 添加基础工具安装命令 + commands.push(`dnf install -y ${missingTools.join(' ')}`); + } + + // 添加脚本权限设置命令(如果部署目录存在) + const scriptsPath = path.join(this.deploymentPath, 'deploy/scripts'); + if (fs.existsSync(scriptsPath)) { + commands.push( + `find "${scriptsPath}" -name "*.sh" -type f -exec chmod +x {} +`, + ); + } + + if (commands.length > 0) { + // 使用sudo助手执行命令 + const combinedCommand = commands.join(' && '); + await this.executeSudoCommand(combinedCommand, 300000); // 5分钟超时 + + let message = '管理员权限获取成功'; + if (missingTools.length > 0) { + message += `,已安装工具: ${missingTools.join(', ')}`; + } + if (fs.existsSync(scriptsPath)) { + message += ',脚本权限已设置'; + } + + this.updateStatus({ + message, + currentStep: 'preparing-environment', + }); + } else { + // 即使没有要执行的命令,也要验证sudo助手是否正常工作 + await this.executeSudoCommand('echo "权限验证成功"', 30000); + + this.updateStatus({ + message: '管理员权限获取成功', + currentStep: 'preparing-environment', + }); + } + + this.sudoSessionActive = true; + + // 启动进程监控 + this.startSudoHelperMonitor(); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + + // 检查是否是用户取消操作 + if ( + errorMessage.includes('cancelled') || + errorMessage.includes('aborted') + ) { + throw new Error('用户取消了权限授权操作'); + } + + // 检查是否是权限被拒绝 + if ( + errorMessage.includes('authentication') || + errorMessage.includes('permission') + ) { + throw new Error( + '管理员权限验证失败,请确保密码正确或用户具有管理员权限', + ); + } + + this.updateStatus({ + status: 'error', + message: `权限获取阶段失败: ${errorMessage}`, + currentStep: 'preparing-environment', + }); + + throw new Error(`获取管理员权限失败: ${errorMessage}`); + } + } + + /** + * 启动sudo助手进程,只需要一次密码输入 + */ + private async startSudoHelper(): Promise { + if (process.platform !== 'linux') { + return; + } + + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return; + } + + try { + // 创建临时目录 + const tempDir = path.join(this.cachePath, 'temp-sudo'); + if (!fs.existsSync(tempDir)) { + fs.mkdirSync(tempDir, { recursive: true }); + } + + // 创建sudo助手脚本 + const helperScriptPath = path.join(tempDir, 'sudo-helper.sh'); + const helperScriptContent = `#!/bin/bash +# Sudo助手脚本,保持长期运行的sudo会话 + +# 不使用 set -e,因为我们需要手动处理错误以保持进程运行 +# set -o pipefail 也可能导致意外退出,所以也不使用 + +# 设置信号处理,确保优雅退出 +trap 'echo "HELPER_SIGNAL_RECEIVED_$$" >&2; exit 0' SIGTERM SIGINT + +# 输出调试信息 +echo "HELPER_STARTED_$$" >&2 + +# 设置读取超时和错误处理 +export TIMEOUT=3 + +# 全局变量来跟踪当前是否有长时间运行的命令 +RUNNING_COMMAND_PID="" + +# 创建命名管道用于健康检查通信 +HEALTH_PIPE="/tmp/health_check_$$" +mkfifo "$HEALTH_PIPE" 2>/dev/null || true + +# 后台健康检查处理函数 +health_check_handler() { + while true; do + if [ -p "$HEALTH_PIPE" ]; then + if read -t 1 health_cmd < "$HEALTH_PIPE" 2>/dev/null; then + if [[ "$health_cmd" == echo*HEALTH_CHECK* ]]; then + eval "$health_cmd" 2>/dev/null || true + echo "COMMAND_DONE_$$" + exec 1>&1 2>&2 + fi + fi + fi + sleep 0.1 + done +} + +# 启动后台健康检查处理器 +health_check_handler & +HEALTH_HANDLER_PID=$! + +# 主循环:读取命令并执行 +while true; do + # 检查是否有输入可读,使用更短的超时避免阻塞 + if ! IFS= read -r -t 2 command 2>/dev/null; then + # 读取超时,检查进程是否还应该继续运行 + # 发送一个心跳信号表明进程仍然活跃(降低频率) + if [ $((RANDOM % 60)) -eq 0 ]; then + echo "HELPER_HEARTBEAT_$$" >&2 2>/dev/null || true + fi + continue + fi + + # 输出调试信息 + echo "RECEIVED_COMMAND: $command" >&2 + + # 检查退出命令 + if [ "$command" = "EXIT" ]; then + echo "HELPER_EXITING_$$" >&2 + # 清理后台健康检查进程 + kill $HEALTH_HANDLER_PID 2>/dev/null || true + rm -f "$HEALTH_PIPE" 2>/dev/null || true + break + fi + + # 检查命令是否为空 + if [ -z "$command" ]; then + echo "EMPTY_COMMAND_$$" >&2 + echo "COMMAND_DONE_$$" + continue + fi + + # 检查健康检查命令 + if [[ "$command" == echo*HEALTH_CHECK* ]]; then + # 将健康检查命令发送到后台处理器 + if [ -p "$HEALTH_PIPE" ]; then + echo "$command" > "$HEALTH_PIPE" & + else + # 如果管道不可用,直接处理 + eval "$command" 2>/dev/null || true + echo "COMMAND_DONE_$$" + exec 1>&1 2>&2 + fi + continue + fi + + # 执行命令并捕获退出码,使用子shell避免影响主进程 + # 添加超时保护,避免长时间运行的命令阻塞助手进程 + ( + # 在子shell中执行命令,设置超时保护(30分钟) + timeout 1800 bash -c "$command" 2>&1 || exit $? + ) & + RUNNING_COMMAND_PID=$! + + # 等待命令完成 + wait $RUNNING_COMMAND_PID + cmd_exit_code=$? + RUNNING_COMMAND_PID="" + + # 处理timeout命令的特殊退出码 + if [ $cmd_exit_code -eq 124 ]; then + echo "COMMAND_ERROR_TIMEOUT_$$" >&2 + echo "COMMAND_ERROR_124_$$" + elif [ $cmd_exit_code -eq 0 ]; then + echo "COMMAND_SUCCESS_$$" + else + echo "COMMAND_ERROR_\${cmd_exit_code}_$$" + fi + + echo "COMMAND_DONE_$$" + + # 强制刷新输出缓冲区 + exec 1>&1 + exec 2>&2 +done + +# 清理 +kill $HEALTH_HANDLER_PID 2>/dev/null || true +rm -f "$HEALTH_PIPE" 2>/dev/null || true + +echo "HELPER_TERMINATED_$$" >&2 +exit 0 +`; + + // 写入助手脚本 + fs.writeFileSync(helperScriptPath, helperScriptContent, { mode: 0o755 }); + + // 使用pkexec启动助手进程,只需要输入一次密码 + const sudoCommand = this.getSudoCommand(); + + if (!sudoCommand.includes('pkexec')) { + throw new Error('当前系统不支持图形化权限验证工具'); + } + + // 启动长期运行的sudo助手进程 + const command = `${sudoCommand}bash "${helperScriptPath}"`; + + this.sudoHelperProcess = spawn('bash', ['-c', command], { + stdio: ['pipe', 'pipe', 'pipe'], + // 设置进程选项以提高稳定性 + env: { + ...process.env, + PATH: '/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin', + }, + }); + + // 等待进程启动并准备就绪 + await new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('sudo助手进程启动超时,请检查权限验证是否完成')); + }, 120000); // 增加到120秒超时,给用户更充足时间输入密码 + + let isResolved = false; + let helperStarted = false; + + this.sudoHelperProcess.stdout?.on('data', (data: Buffer) => { + const output = data.toString(); + + // 检查助手是否已启动 + if (output.includes('HELPER_STARTED_') && !helperStarted) { + helperStarted = true; + if (process.env.NODE_ENV === 'development') { + console.log('Sudo助手进程已启动'); + } + } + + // 检查是否是我们的命令完成标记,如果是说明进程已经启动并可以接收命令 + if ( + (output.includes('COMMAND_DONE_') || + output.includes('COMMAND_SUCCESS_')) && + !isResolved + ) { + clearTimeout(timeout); + isResolved = true; + resolve(void 0); + } + }); + + this.sudoHelperProcess.stderr?.on('data', (data: Buffer) => { + const errorOutput = data.toString(); + + // 检查助手启动信息 + if (errorOutput.includes('HELPER_STARTED_') && !helperStarted) { + helperStarted = true; + if (process.env.NODE_ENV === 'development') { + console.log('Sudo助手进程已启动 (从stderr)'); + } + } + + if (process.env.NODE_ENV === 'development') { + console.log('Sudo Helper Stderr:', errorOutput.trim()); + } + }); + + this.sudoHelperProcess.on('error', (error: Error) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + reject(new Error(`sudo助手进程启动失败: ${error.message}`)); + } + }); + + this.sudoHelperProcess.on('exit', (code: number) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + + if (code === 126) { + reject( + new Error( + 'sudo助手进程启动失败: 权限被拒绝,请确保用户具有管理员权限', + ), + ); + } else if (code === 127) { + reject( + new Error('sudo助手进程启动失败: 找不到命令,请检查系统配置'), + ); + } else { + reject(new Error(`sudo助手进程启动时退出,代码: ${code}`)); + } + } + }); + + // 等待一小段时间确保进程完全启动,然后发送测试命令 + setTimeout(() => { + if (this.sudoHelperProcess && !this.sudoHelperProcess.killed) { + this.sudoHelperProcess.stdin?.write('echo "Helper Ready"\n'); + } + }, 2000); + }); + + // 清理临时脚本文件 + try { + fs.unlinkSync(helperScriptPath); + } catch { + // 忽略清理错误 + } + } catch (error) { + // 清理可能启动的进程 + if (this.sudoHelperProcess) { + try { + this.sudoHelperProcess.kill(); + } catch { + // 忽略清理错误 + } + this.sudoHelperProcess = undefined; + } + + throw new Error( + `启动sudo助手进程失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 尝试重新启动sudo助手进程 + */ + private async restartSudoHelper(): Promise { + if (process.platform !== 'linux') { + return; + } + + if (process.env.NODE_ENV === 'development') { + console.log('尝试重新启动sudo助手进程...'); + } + + // 清理现有进程 + this.cleanupSudoHelper(); + + // 等待一小段时间确保清理完成 + await new Promise((resolve) => setTimeout(resolve, 1000)); + + // 重置状态 + this.sudoSessionActive = false; + + try { + // 重新启动助手进程,增加重试机制 + let attempts = 0; + const maxAttempts = 3; + + while (attempts < maxAttempts) { + try { + await this.startSudoHelper(); + this.sudoSessionActive = true; + + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重新启动成功'); + } + return; + } catch (error) { + attempts++; + if (process.env.NODE_ENV === 'development') { + console.log( + `sudo助手进程启动尝试 ${attempts}/${maxAttempts} 失败:`, + error, + ); + } + + if (attempts < maxAttempts) { + // 等待一段时间后重试 + await new Promise((resolve) => + setTimeout(resolve, 2000 * attempts), + ); + } else { + throw error; + } + } + } + } catch (error) { + this.sudoSessionActive = false; + throw new Error( + `重启sudo助手进程失败(尝试了${3}次): ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 启动sudo助手进程监控 + */ + private startSudoHelperMonitor(): void { + if (process.platform !== 'linux' || this.sudoHelperMonitorInterval) { + return; + } + + // 每60秒检查一次sudo助手进程状态(降低检查频率减少系统压力) + this.sudoHelperMonitorInterval = setInterval(async () => { + if (this.sudoSessionActive && this.sudoHelperProcess) { + try { + // 检查进程是否仍然活跃 + if ( + this.sudoHelperProcess.killed || + this.sudoHelperProcess.exitCode !== null + ) { + if (process.env.NODE_ENV === 'development') { + console.log('检测到sudo助手进程已退出,准备重启...'); + } + + // 尝试重启进程 + try { + await this.restartSudoHelper(); + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重启成功'); + } + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error('sudo助手进程重启失败:', error); + } + // 重启失败,停止监控并标记会话为非活跃状态 + this.stopSudoHelperMonitor(); + this.sudoSessionActive = false; + + // 可以考虑发送错误状态通知给UI + this.updateStatus({ + status: 'error', + message: 'sudo助手进程重启失败,部署可能中断', + currentStep: 'error', + }); + } + } else { + // 进程仍在运行,检查是否有命令正在执行 + if (this.isCommandExecuting) { + // 有命令正在执行,检查是否超过了合理的执行时间(20分钟) + const now = Date.now(); + const executionTime = this.activeCommandStartTime + ? now - this.activeCommandStartTime + : 0; + const maxExecutionTime = 20 * 60 * 1000; // 20分钟 + + if (executionTime > maxExecutionTime) { + if (process.env.NODE_ENV === 'development') { + console.log( + `检测到命令执行时间过长(${Math.round(executionTime / 1000)}秒),可能存在问题`, + ); + } + // 命令执行时间过长,可能出现问题,但不立即重启,只记录警告 + // 让命令继续执行,但下次检查时如果还是这样就考虑重启 + } else { + if (process.env.NODE_ENV === 'development') { + console.log( + `跳过健康检查,有命令正在执行(执行时间: ${Math.round(executionTime / 1000)}秒)`, + ); + } + // 跳过健康检查,命令正在正常执行 + return; + } + } else { + // 没有命令正在执行,进行健康检查 + try { + await this.checkSudoHelperHealth(); + // 健康检查通过,重置错误计数 + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程健康检查通过'); + } + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程健康检查失败,准备重启:', error); + } + + try { + await this.restartSudoHelper(); + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重启成功'); + } + } catch (restartError) { + if (process.env.NODE_ENV === 'development') { + console.error('sudo助手进程重启失败:', restartError); + } + // 重启失败,停止监控 + this.stopSudoHelperMonitor(); + this.sudoSessionActive = false; + + // 发送错误状态通知 + this.updateStatus({ + status: 'error', + message: 'sudo助手进程无法恢复,部署中断', + currentStep: 'error', + }); + } + } + } + } + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.error('sudo助手进程监控出错:', error); + } + // 发生意外错误,也应该尝试恢复 + try { + await this.restartSudoHelper(); + } catch (restartError) { + this.stopSudoHelperMonitor(); + this.sudoSessionActive = false; + } + } + } + }, 60000); // 改为60秒检查一次 + } + + /** + * 停止sudo助手进程监控 + */ + private stopSudoHelperMonitor(): void { + if (this.sudoHelperMonitorInterval) { + clearInterval(this.sudoHelperMonitorInterval); + this.sudoHelperMonitorInterval = undefined; + } + } + + /** + * 清理sudo助手进程 + */ + private cleanupSudoHelper(): void { + // 停止进程监控 + this.stopSudoHelperMonitor(); + + // 重置命令执行状态 + this.isCommandExecuting = false; + this.activeCommandStartTime = undefined; + + if (this.sudoHelperProcess) { + try { + // 发送退出命令 + if (!this.sudoHelperProcess.killed && this.sudoHelperProcess.stdin) { + this.sudoHelperProcess.stdin.write('EXIT\n'); + } + + // 等待一小段时间让进程正常退出 + setTimeout(() => { + if (this.sudoHelperProcess && !this.sudoHelperProcess.killed) { + try { + this.sudoHelperProcess.kill('SIGTERM'); + + // 如果SIGTERM不起作用,几秒后使用SIGKILL + setTimeout(() => { + if (this.sudoHelperProcess && !this.sudoHelperProcess.killed) { + try { + this.sudoHelperProcess.kill('SIGKILL'); + } catch { + // 忽略SIGKILL错误 + } + } + }, 3000); + } catch { + // 忽略SIGTERM错误 + } + } + }, 1000); + } catch { + // 如果正常退出失败,强制终止进程 + try { + if (this.sudoHelperProcess && !this.sudoHelperProcess.killed) { + this.sudoHelperProcess.kill('SIGKILL'); + } + } catch { + // 忽略强制终止的错误 + } + } + + // 移除所有事件监听器 + try { + this.sudoHelperProcess.removeAllListeners(); + } catch { + // 忽略移除监听器的错误 + } + + this.sudoHelperProcess = undefined; + } + + // 清理临时目录 + const tempDir = path.join(this.cachePath, 'temp-sudo'); + if (fs.existsSync(tempDir)) { + try { + const files = fs.readdirSync(tempDir); + files.forEach((file) => { + try { + fs.unlinkSync(path.join(tempDir, file)); + } catch { + // 忽略文件删除错误 + } + }); + fs.rmdirSync(tempDir); + } catch { + // 忽略目录清理错误 + } + } + } + + /** + * 获取合适的sudo命令前缀 + */ + private getSudoCommand(): string { + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return ''; + } + + // 在Linux系统上使用图形化sudo工具 + if (process.platform === 'linux') { + // 构建完整的环境变量,确保 PATH 包含常用的系统路径 + const currentPath = process.env.PATH || ''; + const additionalPaths = [ + '/usr/local/bin', + '/usr/bin', + '/bin', + '/usr/sbin', + '/sbin', + ]; + + // 确保所有常用路径都在 PATH 中 + const pathArray = currentPath.split(':'); + additionalPaths.forEach((path) => { + if (!pathArray.includes(path)) { + pathArray.push(path); + } + }); + const fullPath = pathArray.join(':'); + + // 优先使用 pkexec(现代 Linux 桌面环境的标准) + // 传递必要的环境变量,包括完整的 PATH + return `pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY PATH="${fullPath}" `; + } + + return ''; + } + + /** + * 构建需要 root 权限的命令(优化版本,减少密码输入) + */ + private buildRootCommand( + scriptPath: string, + useInputRedirection?: boolean, + inputData?: string, + envVars?: Record, + ): string { + // 获取sudo命令前缀 + const sudoCommand = this.getSudoCommand(); + + // 构建环境变量字符串 + let envString = ''; + if (envVars && Object.keys(envVars).length > 0) { + const envPairs = Object.entries(envVars) + .map(([key, value]) => `${key}="${value}"`) + .join(' '); + envString = envPairs + ' '; + } + + // 直接执行脚本,不需要 chmod(权限已在克隆仓库后设置) + let command = ''; + if (useInputRedirection && inputData) { + command = `${sudoCommand}bash -c '${envString}echo "${inputData}" | bash "${scriptPath}"'`; + } else { + command = `${sudoCommand}bash -c '${envString}bash "${scriptPath}"'`; + } + + return command; + } + + /** + * 获取用户友好的错误消息 + */ + private getUserFriendlyErrorMessage(error: unknown, context: string): string { + const errorMessage = error instanceof Error ? error.message : String(error); + + // 网络相关错误 + if ( + errorMessage.includes('network') || + errorMessage.includes('connection') || + errorMessage.includes('resolve') || + errorMessage.includes('timeout') || + errorMessage.includes('ENOTFOUND') || + errorMessage.includes('ECONNREFUSED') + ) { + return `${context}:网络连接失败,请检查网络连接和防火墙设置`; + } + + // 权限相关错误 + if ( + errorMessage.includes('permission') || + errorMessage.includes('Access denied') || + errorMessage.includes('authentication') || + errorMessage.includes('EACCES') + ) { + return `${context}:权限不足,请确保具有管理员权限`; + } + + // 文件不存在错误 + if ( + errorMessage.includes('ENOENT') || + errorMessage.includes('No such file') || + errorMessage.includes('not found') + ) { + return `${context}:所需文件或命令不存在,请检查安装是否完整`; + } + + // 磁盘空间不足 + if ( + errorMessage.includes('ENOSPC') || + errorMessage.includes('No space left') + ) { + return `${context}:磁盘空间不足,请清理磁盘空间后重试`; + } + + // 用户取消操作 + if ( + errorMessage.includes('cancelled') || + errorMessage.includes('aborted') || + errorMessage.includes('用户停止') + ) { + return `${context}:操作被用户取消`; + } + + // Kubernetes相关错误 + if ( + errorMessage.includes('kubectl') || + errorMessage.includes('k3s') || + errorMessage.includes('cluster') || + errorMessage.includes('kubeconfig') + ) { + return `${context}:Kubernetes集群配置错误,请检查k3s服务状态`; + } + + // 端口占用错误 + if ( + errorMessage.includes('port') && + errorMessage.includes('already in use') + ) { + return `${context}:端口被占用,请检查相关服务是否已在运行`; + } + + // 默认返回原始错误消息,但添加上下文 + return `${context}:${errorMessage}`; + } + + /** + * 停止部署 + */ + async stopDeployment(): Promise { + try { + // 如果有正在进行的部署流程,中断它 + if (this.abortController && !this.abortController.signal.aborted) { + if (process.env.NODE_ENV === 'development') { + console.log('正在停止部署流程...'); + } + + // 发送中断信号 + this.abortController.abort(); + if (process.env.NODE_ENV === 'development') { + console.log('已发送中断信号给所有正在运行的进程'); + } + + // 等待一小段时间确保进程能够响应中断信号 + await new Promise((resolve) => setTimeout(resolve, 1000)); + if (process.env.NODE_ENV === 'development') { + console.log('等待进程响应中断信号完成'); + } + + if (process.env.NODE_ENV === 'development') { + console.log('部署流程已成功停止'); + } + } else { + if (process.env.NODE_ENV === 'development') { + console.log('没有正在进行的部署流程,直接更新为停止状态'); + } + } + + // 统一更新为停止状态,不使用前端无法识别的 'stopping' 状态 + this.updateStatus({ + status: 'idle', + message: '部署已停止', + currentStep: 'stopped', + }); + } catch (error) { + console.error('停止部署时出错:', error); + + // 即使停止过程出错,也要更新状态 + this.updateStatus({ + status: 'idle', + message: '部署已停止', + currentStep: 'stopped', + }); + } finally { + // 清理资源 + if (process.env.NODE_ENV === 'development') { + console.log('清理部署相关资源'); + } + this.abortController = undefined; + this.sudoSessionActive = false; // 重置sudo会话状态 + this.cleanupSudoHelper(); // 清理sudo助手进程 + } + } + + /** + * 添加 hosts 条目,将域名指向本地 + */ + async addHostsEntries(domains: string[]): Promise { + try { + // 只在 Linux 和 macOS 系统上执行 + if (process.platform !== 'linux' && process.platform !== 'darwin') { + throw new Error('当前系统不支持自动配置 hosts 文件'); + } + + const hostsPath = '/etc/hosts'; + + // 检查是否已经存在这些条目 + let hostsContent = ''; + try { + hostsContent = fs.readFileSync(hostsPath, 'utf8'); + } catch (error) { + throw new Error(`无法读取 hosts 文件: ${error}`); + } + + // 过滤出需要添加的域名(避免重复添加) + // 使用正则表达式检测域名是否已存在,处理多个空格/tab的情况 + const domainsToAdd = domains.filter((domain) => { + // 匹配 127.0.0.1 + 一个或多个空白字符 + 域名 + 行尾或空白字符或注释 + const domainRegex = new RegExp( + `^127\\.0\\.0\\.1\\s+${domain.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}(\\s|$|#)`, + 'm', + ); + return !domainRegex.test(hostsContent); + }); + + if (domainsToAdd.length === 0) { + // 所有域名都已存在,无需添加 + return; + } + + // 检查是否已经存在 openEuler Intelligence 注释标签 + const commentExists = hostsContent.includes( + '# openEuler Intelligence Local Deployment', + ); + + // 构建要添加的内容 + const entriesToAdd = domainsToAdd + .map((domain) => `127.0.0.1 ${domain}`) + .join('\n'); + + let newContent: string; + if (commentExists) { + // 如果注释已存在,在注释后面插入新的域名条目 + const commentRegex = /(# openEuler Intelligence Local Deployment\n)/; + newContent = hostsContent.replace(commentRegex, `$1${entriesToAdd}\n`); + } else { + // 如果注释不存在,添加完整的注释块和域名条目 + newContent = + hostsContent.trim() + + '\n\n# openEuler Intelligence Local Deployment\n' + + entriesToAdd + + '\n'; + } + + // 使用管理员权限写入 hosts 文件 + // 创建临时文件写入内容,然后移动到 hosts 文件位置 + const tempFile = '/tmp/hosts_new'; + + // 先将内容写入临时文件,避免直接在命令行中处理复杂的字符串转义 + try { + fs.writeFileSync(tempFile, newContent); + } catch (error) { + throw new Error(`无法创建临时文件: ${error}`); + } + + // 移动临时文件到 hosts 文件位置 + await this.executeSudoCommand(`mv ${tempFile} ${hostsPath}`, 30000); + + console.log(`已添加以下域名到 hosts 文件: ${domainsToAdd.join(', ')}`); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + throw new Error(`配置 hosts 文件失败: ${errorMessage}`); + } + } + + /** + * 清理部署文件 + */ + async cleanup(): Promise { + // 清理sudo助手进程 + this.cleanupSudoHelper(); + + // 清理部署文件 + if (fs.existsSync(this.deploymentPath)) { + fs.rmSync(this.deploymentPath, { recursive: true, force: true }); + } + + this.updateStatus({ + status: 'idle', + message: '清理完成', + currentStep: 'idle', + }); + } + + /** + * 检查sudo助手进程健康状态 + */ + private async checkSudoHelperHealth(): Promise { + if (!this.sudoHelperProcess || this.sudoHelperProcess.killed) { + throw new Error('sudo助手进程未运行'); + } + + // 首先检查进程基本状态 + if (this.sudoHelperProcess.exitCode !== null) { + throw new Error( + `sudo助手进程已退出,退出码: ${this.sudoHelperProcess.exitCode}`, + ); + } + + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + cleanup(); + reject(new Error('sudo助手进程健康检查超时')); + }, 10000); // 增加到10秒超时,给系统更多时间响应 + + let isResolved = false; + const healthCheckId = Date.now(); + + const dataHandler = (data: Buffer) => { + const output = data.toString(); + if ( + output.includes(`HEALTH_CHECK_${healthCheckId}_DONE`) && + !isResolved + ) { + clearTimeout(timeout); + isResolved = true; + cleanup(); + resolve(); + } + }; + + const errorHandler = (error: Error) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + cleanup(); + reject(error); + } + }; + + const exitHandler = (code: number | null) => { + if (!isResolved) { + clearTimeout(timeout); + isResolved = true; + cleanup(); + reject(new Error(`sudo助手进程在健康检查期间退出,代码: ${code}`)); + } + }; + + const cleanup = () => { + this.sudoHelperProcess?.stdout?.off('data', dataHandler); + this.sudoHelperProcess?.off('error', errorHandler); + this.sudoHelperProcess?.off('exit', exitHandler); + }; + + this.sudoHelperProcess.stdout?.on('data', dataHandler); + this.sudoHelperProcess.on('error', errorHandler); + this.sudoHelperProcess.on('exit', exitHandler); + + try { + // 发送健康检查命令 + this.sudoHelperProcess.stdin?.write( + `echo "HEALTH_CHECK_${healthCheckId}_DONE"\n`, + ); + } catch (writeError) { + cleanup(); + reject( + new Error( + `健康检查命令发送失败: ${writeError instanceof Error ? writeError.message : String(writeError)}`, + ), + ); + } + }); + } + + /** + * 使用sudo助手进程执行命令,无需重复密码输入 + */ + private async executeSudoCommand( + command: string, + timeout: number = 60000, + envVars?: Record, + ): Promise<{ stdout: string; stderr: string }> { + if (process.platform !== 'linux') { + // 非Linux系统直接执行 + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 检查sudo助手进程是否仍然活跃 + if (!this.sudoHelperProcess || this.sudoHelperProcess.killed) { + throw new Error('sudo助手进程未启动或已终止,请重新初始化sudo会话'); + } + + // 检查sudo助手进程健康状态,如果不健康则尝试重启 + try { + await this.checkSudoHelperHealth(); + } catch (error) { + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程健康检查失败,尝试重启:', error); + } + + try { + await this.restartSudoHelper(); + } catch (restartError) { + // 重启失败,在 Linux 系统上使用后备方案 + if (process.env.NODE_ENV === 'development') { + console.log('sudo助手进程重启失败,尝试使用后备方案:', restartError); + } + + // 只在 Linux 系统上尝试后备方案 + if (process.platform === 'linux') { + try { + return await this.executeSudoCommandFallback( + command, + timeout, + envVars, + ); + } catch (fallbackError) { + throw new Error( + `sudo助手进程重启失败且后备方案也失败: ${restartError instanceof Error ? restartError.message : String(restartError)}. 后备方案错误: ${fallbackError instanceof Error ? fallbackError.message : String(fallbackError)}`, + ); + } + } else { + // 非 Linux 系统直接抛出重启错误 + throw new Error( + `sudo助手进程重启失败: ${restartError instanceof Error ? restartError.message : String(restartError)}`, + ); + } + } + } + + return new Promise((resolve, reject) => { + // 标记命令开始执行 + this.isCommandExecuting = true; + this.activeCommandStartTime = Date.now(); + + const timeoutId = setTimeout(() => { + // 命令执行完成,清除状态 + this.isCommandExecuting = false; + this.activeCommandStartTime = undefined; + reject(new Error(`命令执行超时: ${command}`)); + }, timeout); + + let stdout = ''; + let stderr = ''; + let isResolved = false; + + const dataHandler = (data: Buffer) => { + const output = data.toString(); + stdout += output; + + // 检查命令是否成功完成 + if ( + output.includes(`COMMAND_SUCCESS_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 移除状态标记 + stdout = stdout.replace( + new RegExp( + `COMMAND_(SUCCESS|DONE)_${this.sudoHelperProcess?.pid}\\s*`, + 'g', + ), + '', + ); + resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + } + // 检查命令是否失败 + else if ( + output.includes(`COMMAND_ERROR_`) && + output.includes(`_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 提取错误码 + const errorMatch = output.match( + new RegExp(`COMMAND_ERROR_(\\d+)_${this.sudoHelperProcess?.pid}`), + ); + const errorCode = errorMatch ? errorMatch[1] : 'unknown'; + + // 移除状态标记 + stdout = stdout.replace( + new RegExp( + `COMMAND_(ERROR_\\d+|DONE)_${this.sudoHelperProcess?.pid}\\s*`, + 'g', + ), + '', + ); + + reject( + new Error( + `命令执行失败 (退出码: ${errorCode}): ${stderr.trim() || stdout.trim()}`, + ), + ); + } + // 向后兼容:检查旧的完成标记 + else if ( + output.includes(`COMMAND_DONE_${this.sudoHelperProcess?.pid}`) && + !isResolved + ) { + clearTimeout(timeoutId); + isResolved = true; + // 移除完成标记 + stdout = stdout.replace( + new RegExp(`COMMAND_DONE_${this.sudoHelperProcess?.pid}\\s*`, 'g'), + '', + ); + resolve({ stdout: stdout.trim(), stderr: stderr.trim() }); + } + }; + + const errorHandler = (data: Buffer) => { + const errorOutput = data.toString(); + stderr += errorOutput; + + // 检查是否有调试信息 + if (process.env.NODE_ENV === 'development') { + if ( + errorOutput.includes('HELPER_STARTED_') || + errorOutput.includes('RECEIVED_COMMAND:') || + errorOutput.includes('HELPER_EXITING_') || + errorOutput.includes('HELPER_TERMINATED_') + ) { + console.log('Sudo Helper Debug:', errorOutput.trim()); + } + } + }; + + const processErrorHandler = (error: Error) => { + if (!isResolved) { + clearTimeout(timeoutId); + isResolved = true; + reject(new Error(`sudo助手进程错误: ${error.message}`)); + } + }; + + const processExitHandler = (code: number) => { + if (!isResolved) { + clearTimeout(timeoutId); + isResolved = true; + + // 提供更详细的退出信息 + const stderrInfo = stderr.trim() ? `\nStderr: ${stderr.trim()}` : ''; + const stdoutInfo = stdout.trim() ? `\nStdout: ${stdout.trim()}` : ''; + + reject( + new Error( + `sudo助手进程异常退出,代码: ${code}${stderrInfo}${stdoutInfo}\n` + + `这可能是由于:\n` + + `1. 脚本执行时间过长导致进程超时\n` + + `2. 系统资源不足\n` + + `3. 权限问题或认证超时\n` + + `4. 脚本内部错误导致bash退出`, + ), + ); + } + }; + + // 绑定事件监听器 + this.sudoHelperProcess.stdout?.on('data', dataHandler); + this.sudoHelperProcess.stderr?.on('data', errorHandler); + this.sudoHelperProcess.on('error', processErrorHandler); + this.sudoHelperProcess.on('exit', processExitHandler); + + // 构建环境变量字符串 + let envString = ''; + if (envVars && Object.keys(envVars).length > 0) { + const envPairs = Object.entries(envVars) + .map(([key, value]) => `export ${key}="${value}"`) + .join('; '); + envString = envPairs + '; '; + } + + // 发送命令到助手进程 + const fullCommand = `${envString}${command}`; + this.sudoHelperProcess.stdin?.write(`${fullCommand}\n`); + + // 设置清理函数 + const cleanup = () => { + // 清除命令执行状态 + this.isCommandExecuting = false; + this.activeCommandStartTime = undefined; + + // 清理事件监听器 + this.sudoHelperProcess.stdout?.off('data', dataHandler); + this.sudoHelperProcess.stderr?.off('data', errorHandler); + this.sudoHelperProcess.off('error', processErrorHandler); + this.sudoHelperProcess.off('exit', processExitHandler); + }; + + // 确保在resolve或reject时清理事件监听器和执行状态 + const originalResolve = resolve; + const originalReject = reject; + + resolve = (value: any) => { + cleanup(); + originalResolve(value); + }; + + reject = (reason: any) => { + cleanup(); + originalReject(reason); + }; + }); + } + + /** + * 后备方案:当sudo助手进程不可用时,使用传统的sudo方式执行命令 + * 只适用于 Linux 系统 + */ + private async executeSudoCommandFallback( + command: string, + timeout: number = 60000, + envVars?: Record, + ): Promise<{ stdout: string; stderr: string }> { + // 此方法只应在 Linux 系统上调用 + if (process.platform !== 'linux') { + throw new Error('executeSudoCommandFallback 只能在 Linux 系统上使用'); + } + + // 检查是否为root用户 + if (process.getuid && process.getuid() === 0) { + return await execAsyncWithAbort( + command, + { timeout, env: { ...process.env, ...envVars } }, + this.abortController?.signal, + ); + } + + // 使用传统的sudo方式 + const sudoCommand = this.getSudoCommand(); + + // 构建环境变量字符串 + let envString = ''; + if (envVars && Object.keys(envVars).length > 0) { + const envPairs = Object.entries(envVars) + .map(([key, value]) => `${key}="${value}"`) + .join(' '); + envString = envPairs + ' '; + } + + const fullCommand = `${sudoCommand}bash -c '${envString}${command}'`; + + if (process.env.NODE_ENV === 'development') { + console.log('使用后备方案执行sudo命令:', fullCommand); + } + + return await execAsyncWithAbort( + fullCommand, + { timeout }, + this.abortController?.signal, + ); + } +} diff --git a/electron/main/deploy/core/EnvironmentChecker.ts b/electron/main/deploy/core/EnvironmentChecker.ts new file mode 100644 index 0000000000000000000000000000000000000000..70eaf9af459c56b3ffbffcfe001b6f06af6433f3 --- /dev/null +++ b/electron/main/deploy/core/EnvironmentChecker.ts @@ -0,0 +1,298 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import * as os from 'os'; +import * as fs from 'fs'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); + +export interface EnvironmentCheckResult { + success: boolean; + errors: string[]; + warnings: string[]; + missingBasicTools: string[]; + missingK8sTools: string[]; + needsBasicToolsInstall: boolean; + needsK8sToolsInstall: boolean; +} + +/** + * 环境检查器 - 迁移自 reference-scripts/scripts/1-check-env/check_env.sh + * 但根据需求调整了检查条件 + */ +export class EnvironmentChecker { + /** + * 执行所有环境检查 + * 迁移自 reference-scripts/scripts/1-check-env,但跳过了 OS 版本检查和离线模式 + */ + async checkAll(): Promise { + const result: EnvironmentCheckResult = { + success: true, + errors: [], + warnings: [], + missingBasicTools: [], + missingK8sTools: [], + needsBasicToolsInstall: false, + needsK8sToolsInstall: false, + }; + + // 检查主机名 + try { + await this.checkHostname(); + } catch (error) { + result.warnings.push(`主机名检查: ${error}`); + } + + // 检查DNS + try { + await this.checkDns(); + } catch (error) { + result.warnings.push(`DNS检查: ${error}`); + } + + // 检查内存 (调整为4GB) + try { + await this.checkRam(); + } catch (error) { + result.errors.push(`内存检查: ${error}`); + result.success = false; + } + + // 检查磁盘空间 (调整为4GB) + try { + await this.checkDiskSpace(); + } catch (error) { + result.errors.push(`磁盘空间检查: ${error}`); + result.success = false; + } + + // 检查网络连接 (只考虑联网部署) + try { + await this.checkNetwork(); + } catch (error) { + result.errors.push(`网络检查: ${error}`); + result.success = false; + } + + // 检查必需的系统工具 + try { + const toolsCheckResult = await this.checkRequiredTools(); + result.missingBasicTools = toolsCheckResult.missingBasicTools; + result.missingK8sTools = toolsCheckResult.missingK8sTools; + result.needsBasicToolsInstall = toolsCheckResult.needsBasicToolsInstall; + result.needsK8sToolsInstall = toolsCheckResult.needsK8sToolsInstall; + + if (toolsCheckResult.missingBasicTools.length > 0) { + result.warnings.push( + `缺少基础工具: ${toolsCheckResult.missingBasicTools.join(', ')},将通过 DNF 自动安装`, + ); + } + if (toolsCheckResult.missingK8sTools.length > 0) { + result.warnings.push( + `缺少 K8s 工具: ${toolsCheckResult.missingK8sTools.join(', ')},将通过 install-tools 脚本安装`, + ); + } + } catch (error) { + result.errors.push(`系统工具检查: ${error}`); + result.success = false; + } + + return result; + } + + /** + * 检查主机名 + */ + private async checkHostname(): Promise { + const hostname = os.hostname(); + if (!hostname || hostname.trim() === '') { + throw new Error('未设置主机名'); + } + } + + /** + * 检查DNS配置 + */ + private async checkDns(): Promise { + try { + const resolvConf = fs.readFileSync('/etc/resolv.conf', 'utf8'); + if (!resolvConf.includes('nameserver')) { + throw new Error('DNS未配置'); + } + } catch (error: any) { + if (error.code === 'ENOENT') { + throw new Error('resolv.conf文件不存在'); + } + throw error; + } + } + + /** + * 检查内存 (最低4GB) + */ + private async checkRam(): Promise { + const totalMem = os.totalmem(); + const totalMemMB = Math.floor(totalMem / (1024 * 1024)); + const requiredMB = 4 * 1024; // 4GB + + if (totalMemMB < requiredMB) { + throw new Error(`内存不足,当前: ${totalMemMB}MB,需要: ${requiredMB}MB`); + } + } + + /** + * 检查磁盘空间 (最低4GB可用空间) + */ + private async checkDiskSpace(): Promise { + try { + const { stdout } = await execAsync('df -h /'); + const lines = stdout.trim().split('\n'); + if (lines.length < 2) { + throw new Error('无法获取磁盘信息'); + } + + // 解析df输出,获取可用空间 + const diskInfo = lines[1].split(/\s+/); + const availableStr = diskInfo[3]; // 第4列是可用空间 + + // 转换为MB进行比较 + const availableMB = this.parseDiskSize(availableStr); + const requiredMB = 4 * 1024; // 4GB + + if (availableMB < requiredMB) { + throw new Error( + `磁盘空间不足,可用: ${Math.floor(availableMB)}MB,需要: ${requiredMB}MB`, + ); + } + } catch (error) { + if (error instanceof Error) { + throw error; + } + throw new Error('磁盘空间检查失败'); + } + } + + /** + * 解析磁盘大小字符串 (如 "1.5G", "500M") 并转换为MB + */ + private parseDiskSize(sizeStr: string): number { + const match = sizeStr.match(/^(\d+(?:\.\d+)?)(.)$/); + if (!match) { + return 0; + } + + const value = parseFloat(match[1]); + const unit = match[2].toUpperCase(); + + switch (unit) { + case 'K': + return value / 1024; + case 'M': + return value; + case 'G': + return value * 1024; + case 'T': + return value * 1024 * 1024; + default: + return value / (1024 * 1024); // 假设为字节 + } + } + + /** + * 检查网络连接 + */ + private async checkNetwork(): Promise { + try { + // 使用curl检查网络连接 + await execAsync( + 'curl -s --connect-timeout 5 https://www.baidu.com > /dev/null', + { + timeout: 10000, + }, + ); + } catch { + throw new Error('无法访问外部网络,请检查网络连接'); + } + } + + /** + * 检查必需的系统工具 + */ + private async checkRequiredTools(): Promise<{ + missingBasicTools: string[]; + missingK8sTools: string[]; + needsBasicToolsInstall: boolean; + needsK8sToolsInstall: boolean; + }> { + // 基础工具:通过 DNF 安装 + const basicTools = ['git', 'curl', 'docker']; + // K8s 工具:通过 2-install-tools 脚本安装 + const k8sTools = ['kubectl', 'helm', 'k3s']; + + const missingBasicTools: string[] = []; + const missingK8sTools: string[] = []; + + // 检查基础工具 + for (const tool of basicTools) { + try { + await execAsync(`which ${tool}`); + } catch { + missingBasicTools.push(tool); + } + } + + // 检查 K8s 工具 + for (const tool of k8sTools) { + try { + await execAsync(`which ${tool}`); + } catch { + missingK8sTools.push(tool); + } + } + + return { + missingBasicTools, + missingK8sTools, + needsBasicToolsInstall: missingBasicTools.length > 0, + needsK8sToolsInstall: missingK8sTools.length > 0, + }; + } + + /** + * 通过 DNF 安装基础工具 + */ + async installBasicTools(missingTools: string[]): Promise { + if (missingTools.length === 0) { + return; + } + + // 构建 DNF 安装命令 + let installCommand = `dnf install -y ${missingTools.join(' ')}`; + + // 在 Linux 系统上,如果不是 root 用户,使用图形化 sudo 工具 + const needsSudo = + process.platform === 'linux' && process.getuid && process.getuid() !== 0; + + if (needsSudo) { + // 使用 pkexec 或 sudo 获取权限 + installCommand = `pkexec env DISPLAY=$DISPLAY XAUTHORITY=$XAUTHORITY ${installCommand}`; + } + + try { + await execAsync(installCommand, { timeout: 300000 }); // 5分钟超时 + } catch (error) { + throw new Error( + `安装基础工具失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } +} diff --git a/electron/main/deploy/core/LocalDeployHandler.ts b/electron/main/deploy/core/LocalDeployHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..a956d54fb80486931a18133790855f3b2a81ae76 --- /dev/null +++ b/electron/main/deploy/core/LocalDeployHandler.ts @@ -0,0 +1,149 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { DeploymentService } from './DeploymentService'; +import type { + DeploymentParams, + DeploymentFormData, +} from '../types/deployment.types'; + +/** + * 本地部署处理器 - 连接前端表单和部署服务 + */ +export class LocalDeployHandler { + private deploymentService: DeploymentService; + + constructor() { + this.deploymentService = new DeploymentService(); + } + + /** + * 处理前端表单提交的部署请求 + * @param formData 来自 localDeploy.vue 的表单数据 + */ + async handleDeployment(formData: DeploymentFormData): Promise { + // 将前端表单数据转换为部署参数 + const deploymentParams: DeploymentParams = { + mainModel: { + endpoint: this.formatEndpoint(formData.ruleForm.url), + key: formData.ruleForm.apiKey, + name: formData.ruleForm.modelName, + ctxLength: 8192, + maxTokens: 2048, + }, + embeddingModel: { + type: 'openai', // 根据需求,只考虑 openai 的情况 + endpoint: this.formatEndpoint(formData.embeddingRuleForm.url), + key: formData.embeddingRuleForm.apiKey, + name: formData.embeddingRuleForm.modelName, + }, + }; + + // 验证参数 + this.validateDeploymentParams(deploymentParams); + + // 开始部署 + await this.deploymentService.startDeployment(deploymentParams); + } + + /** + * 格式化 API 端点 URL + * @param url 原始 URL + * @returns 格式化后的 URL + */ + private formatEndpoint(url: string): string { + if (!url) { + throw new Error('URL 不能为空'); + } + + // 移除尾部斜杠 + url = url.replace(/\/+$/, ''); + + // 如果没有协议,默认添加 https:// + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + return url; + } + + /** + * 验证部署参数 + * @param params 部署参数 + */ + private validateDeploymentParams(params: DeploymentParams): void { + const { mainModel, embeddingModel } = params; + + // 验证主模型参数 + if (!mainModel.endpoint) { + throw new Error('主模型 URL 不能为空'); + } + if (!mainModel.name) { + throw new Error('主模型名称不能为空'); + } + if (!mainModel.key) { + throw new Error('主模型 API Key 不能为空'); + } + + // 验证 embedding 模型参数 + if (!embeddingModel.endpoint) { + throw new Error('Embedding 模型 URL 不能为空'); + } + if (!embeddingModel.name) { + throw new Error('Embedding 模型名称不能为空'); + } + if (!embeddingModel.key) { + throw new Error('Embedding 模型 API Key 不能为空'); + } + + // 验证 URL 格式 + try { + new URL(mainModel.endpoint); + new URL(embeddingModel.endpoint); + } catch { + throw new Error('URL 格式不正确'); + } + } + + /** + * 设置状态回调 + */ + setStatusCallback(callback: (status: any) => void) { + this.deploymentService.setStatusCallback(callback); + } + + /** + * 获取当前状态 + */ + getStatus() { + return this.deploymentService.getStatus(); + } + + /** + * 停止部署 + */ + async stopDeployment(): Promise { + await this.deploymentService.stopDeployment(); + } + + /** + * 清理部署文件 + */ + async cleanup(): Promise { + await this.deploymentService.cleanup(); + } + + /** + * 添加 hosts 条目 + */ + async addHostsEntries(domains: string[]): Promise { + await this.deploymentService.addHostsEntries(domains); + } +} diff --git a/electron/main/deploy/core/ValuesYamlManager.ts b/electron/main/deploy/core/ValuesYamlManager.ts new file mode 100644 index 0000000000000000000000000000000000000000..d1190705cb0170589c7b4c3c0ac3128e29d07f6f --- /dev/null +++ b/electron/main/deploy/core/ValuesYamlManager.ts @@ -0,0 +1,108 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import * as fs from 'fs'; +import * as yaml from 'js-yaml'; +import type { DeploymentParams } from '../types/deployment.types'; + +/** + * Values.yaml 文件管理器 + */ +export class ValuesYamlManager { + /** + * 更新 values.yaml 中的 models 配置 + */ + async updateModelsConfig( + valuesPath: string, + params: DeploymentParams, + ): Promise { + try { + // 读取现有的 values.yaml 文件 + const valuesContent = fs.readFileSync(valuesPath, 'utf8'); + const valuesData = yaml.load(valuesContent) as any; + + // 确保 models 节点存在 + if (!valuesData.models) { + valuesData.models = {}; + } + + // 配置 answer 模型(主模型) + valuesData.models.answer = { + endpoint: params.mainModel.endpoint, + key: params.mainModel.key, + name: params.mainModel.name, + ctxLength: params.mainModel.ctxLength || 8192, + maxTokens: params.mainModel.maxTokens || 2048, + }; + + // 配置 functionCall 模型(使用相同的主模型) + valuesData.models.functionCall = { + backend: 'function_call', // 根据需求文档,这里应该是 "function_call" 而不是 "openai" + endpoint: params.mainModel.endpoint, + key: params.mainModel.key, + name: params.mainModel.name, + ctxLength: params.mainModel.ctxLength || 8192, + maxTokens: params.mainModel.maxTokens || 2048, + }; + + // 配置 embedding 模型 (只考虑 openai 类型) + valuesData.models.embedding = { + type: 'openai', // 只考虑 openai 的情况 + endpoint: params.embeddingModel.endpoint, + key: params.embeddingModel.key, + name: params.embeddingModel.name, + }; + + // 写回文件 + const updatedYaml = yaml.dump(valuesData, { + indent: 2, + lineWidth: -1, // 禁用行宽限制 + noRefs: true, + sortKeys: false, + }); + + fs.writeFileSync(valuesPath, updatedYaml, 'utf8'); + } catch (error) { + throw new Error( + `更新 values.yaml 失败: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } + + /** + * 验证 values.yaml 文件格式 + */ + validateValuesFile(valuesPath: string): boolean { + try { + const content = fs.readFileSync(valuesPath, 'utf8'); + yaml.load(content); + return true; + } catch (error) { + console.error('Values.yaml 验证失败:', error); + return false; + } + } + + /** + * 备份 values.yaml 文件 + */ + backupValuesFile(valuesPath: string): string { + const backupPath = `${valuesPath}.backup.${Date.now()}`; + fs.copyFileSync(valuesPath, backupPath); + return backupPath; + } + + /** + * 恢复 values.yaml 文件 + */ + restoreValuesFile(valuesPath: string, backupPath: string): void { + fs.copyFileSync(backupPath, valuesPath); + } +} diff --git a/electron/main/deploy/index.ts b/electron/main/deploy/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa7820e83a967b81c8d5d6c76fdeae3665da5010 --- /dev/null +++ b/electron/main/deploy/index.ts @@ -0,0 +1,30 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { DeploymentService } from './core/DeploymentService'; +import { LocalDeployHandler } from './core/LocalDeployHandler'; +import type { + DeploymentParams, + DeploymentStatus, + DeploymentFormData, + ModelFormData, +} from './types/deployment.types'; + +export { DeploymentService, LocalDeployHandler }; +export type { + DeploymentParams, + DeploymentStatus, + DeploymentFormData, + ModelFormData as RuleForm, +}; + +// 导出单例实例 +export const deploymentService = new DeploymentService(); +export const localDeployHandler = new LocalDeployHandler(); diff --git a/electron/main/deploy/main/DeploymentIPCHandler.ts b/electron/main/deploy/main/DeploymentIPCHandler.ts new file mode 100644 index 0000000000000000000000000000000000000000..c58f8cce6797a153961ddfcff78ff989edc87619 --- /dev/null +++ b/electron/main/deploy/main/DeploymentIPCHandler.ts @@ -0,0 +1,151 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { ipcMain, BrowserWindow } from 'electron'; +import { deploymentService } from '../index'; +import { LocalDeployHandler } from '../core/LocalDeployHandler'; +import type { DeploymentFormData } from '../types/deployment.types'; + +/** + * 部署服务 IPC 处理程序 + */ +export class DeploymentIPCHandler { + private localDeployHandler: LocalDeployHandler; + private mainWindow: BrowserWindow | undefined; + + constructor() { + this.localDeployHandler = new LocalDeployHandler(); + this.mainWindow = undefined; + this.setupHandlers(); + } + + /** + * 设置主窗口引用 + */ + setMainWindow(window: BrowserWindow) { + this.mainWindow = window; + + // 设置状态变化回调 + this.localDeployHandler.setStatusCallback((status) => { + // 调试信息:仅在开发环境下记录IPC层状态更新 + if (process.env.NODE_ENV === 'development') { + console.log('🔄 IPC Handler: 收到状态更新', { + status: status?.status, + currentStep: status?.currentStep, + hasMainWindow: !!this.mainWindow, + isDestroyed: this.mainWindow?.isDestroyed(), + }); + } + + // 验证状态对象是否有效 + if ( + status && + typeof status === 'object' && + this.mainWindow && + !this.mainWindow.isDestroyed() + ) { + try { + this.mainWindow.webContents.send('deployment:statusChanged', status); + if (process.env.NODE_ENV === 'development') { + console.log('✅ IPC Handler: 状态已发送到渲染进程'); + } + } catch (error) { + console.error('❌ IPC Handler: 发送状态到渲染进程失败:', error); + } + } else if (!status) { + if (process.env.NODE_ENV === 'development') { + console.warn('⚠️ IPC Handler: 收到无效的状态更新:', status); + } + } else if (!this.mainWindow || this.mainWindow.isDestroyed()) { + if (process.env.NODE_ENV === 'development') { + console.warn('⚠️ IPC Handler: 主窗口不可用,无法发送状态更新'); + } + } + }); + } + + /** + * 设置 IPC 处理程序 + */ + setupHandlers() { + // 处理前端表单提交的部署请求 + ipcMain.handle( + 'deployment:startFromForm', + async (_event, formData: DeploymentFormData) => { + try { + await this.localDeployHandler.handleDeployment(formData); + } catch (error) { + console.error('部署失败:', error); + throw error; + } + }, + ); + + // 开始部署(原有接口保留兼容性) + ipcMain.handle('deployment:start', async (_event, params) => { + try { + await deploymentService.startDeployment(params); + } catch (error) { + console.error('部署失败:', error); + throw error; + } + }); + + // 停止部署 + ipcMain.handle('deployment:stop', async () => { + try { + await this.localDeployHandler.stopDeployment(); + } catch (error) { + console.error('停止部署失败:', error); + throw error; + } + }); + + // 获取部署状态 + ipcMain.handle('deployment:getStatus', () => { + return this.localDeployHandler.getStatus(); + }); + + // 清理部署文件 + ipcMain.handle('deployment:cleanup', async () => { + try { + await this.localDeployHandler.cleanup(); + } catch (error) { + console.error('清理失败:', error); + throw error; + } + }); + + // 添加 hosts 条目 + ipcMain.handle( + 'deployment:addHostsEntries', + async (event, domains: string[]) => { + try { + await this.localDeployHandler.addHostsEntries(domains); + } catch (error) { + console.error('添加 hosts 条目失败:', error); + throw error; + } + }, + ); + } + + /** + * 清理处理程序 + */ + cleanup() { + ipcMain.removeHandler('deployment:startFromForm'); + ipcMain.removeHandler('deployment:start'); + ipcMain.removeHandler('deployment:stop'); + ipcMain.removeHandler('deployment:getStatus'); + ipcMain.removeHandler('deployment:cleanup'); + ipcMain.removeHandler('deployment:addHostsEntries'); + } +} diff --git a/electron/main/deploy/preload/deploymentPreload.ts b/electron/main/deploy/preload/deploymentPreload.ts new file mode 100644 index 0000000000000000000000000000000000000000..15b7a2fc63364f389cbc12c6963f7d52b7a7c2a7 --- /dev/null +++ b/electron/main/deploy/preload/deploymentPreload.ts @@ -0,0 +1,80 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { contextBridge, ipcRenderer } from 'electron'; +import type { + DeploymentParams, + DeploymentStatus, + DeploymentFormData, +} from '../types/deployment.types'; + +// 暴露给渲染进程的部署服务 API +const deploymentAPI = { + /** + * 从前端表单开始部署 + */ + startDeploymentFromForm: (formData: DeploymentFormData): Promise => { + return ipcRenderer.invoke('deployment:startFromForm', formData); + }, + + /** + * 开始部署(原有接口) + */ + startDeployment: (params: DeploymentParams): Promise => { + return ipcRenderer.invoke('deployment:start', params); + }, + + /** + * 停止部署 + */ + stopDeployment: (): Promise => { + return ipcRenderer.invoke('deployment:stop'); + }, + + /** + * 获取部署状态 + */ + getStatus: (): Promise => { + return ipcRenderer.invoke('deployment:getStatus'); + }, + + /** + * 监听部署状态变化 + */ + onStatusChange: (callback: (status: DeploymentStatus) => void): void => { + ipcRenderer.on('deployment:statusChanged', (_event, status) => { + callback(status); + }); + }, + + /** + * 移除状态变化监听器 + */ + removeStatusListener: (): void => { + ipcRenderer.removeAllListeners('deployment:statusChanged'); + }, + + /** + * 清理部署文件 + */ + cleanup: (): Promise => { + return ipcRenderer.invoke('deployment:cleanup'); + }, +}; + +// 注册到全局对象 +contextBridge.exposeInMainWorld('deploymentService', deploymentAPI); + +// 类型声明 +declare global { + interface Window { + deploymentService: typeof deploymentAPI; + } +} diff --git a/electron/main/deploy/types/deployment.types.ts b/electron/main/deploy/types/deployment.types.ts new file mode 100644 index 0000000000000000000000000000000000000000..a293a7d4dfe27802254b05b188a76afb58089738 --- /dev/null +++ b/electron/main/deploy/types/deployment.types.ts @@ -0,0 +1,55 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +export interface ModelConfig { + endpoint: string; + key: string; + name: string; + ctxLength?: number; + maxTokens?: number; +} + +export interface EmbeddingConfig { + type: 'openai' | 'mindie'; + endpoint: string; + key: string; + name: string; +} + +// 与 localDeploy.vue 中的 RuleForm 保持一致 +export interface ModelFormData { + url: string; + modelName: string; + apiKey: string; +} + +export interface DeploymentFormData { + ruleForm: ModelFormData; + embeddingRuleForm: ModelFormData; +} + +export interface DeploymentParams { + mainModel: ModelConfig; + embeddingModel: EmbeddingConfig; +} + +export interface DeploymentStatus { + status: + | 'idle' + | 'preparing' + | 'cloning' + | 'configuring' + | 'deploying' + | 'success' + | 'error'; + message: string; + currentStep?: string; + estimatedTime?: number; +} diff --git a/electron/main/index.ts b/electron/main/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..b44c289611d9a206ec519ab8aba82a2a613d6954 --- /dev/null +++ b/electron/main/index.ts @@ -0,0 +1,216 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { app, session, ipcMain, Menu, BrowserWindow } from 'electron'; +import { + createDefaultWindow, + createChatWindow, + createTray, + checkAndShowWelcomeIfNeeded, +} from './window'; +import { cachePath, commonCacheConfPath } from './common/cache-conf'; +import { mkdirpIgnoreError, getUserDefinedConf } from './common/fs-utils'; +import { getOsLocale, resolveNlsConfiguration } from './common/locale'; +import { resolveThemeConfiguration, setApplicationTheme } from './common/theme'; +import { productObj } from './common/product'; +import { + registerGlobalShortcut, + checkAccessibilityPermission, + unregisterAllShortcuts, +} from './common/shortcuts'; +import { buildAppMenu } from './common/menu'; +import { registerIpcListeners, cleanupDeploymentHandlers } from './common/ipc'; + +// 允许本地部署时使用无效证书,仅在 Electron 主进程下生效 +if (process.versions.electron) { + process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; +} + +// 确保应用名称使用productName而不是package.json中的name +app.name = productObj.name; + +// 添加表示应用是否正在退出的标志位 +let isQuitting = false; + +// 获取系统语言环境 +const osLocale = getOsLocale(); + +// 应用初始化 +app.once('ready', () => { + onReady(); +}); + +// 处理证书错误事件,允许所有证书错误 +app.on( + 'certificate-error', + (event, webContents, url, error, certificate, callback) => { + event.preventDefault(); + callback(true); + }, +); + +// 针对所有 session 处理证书错误 +app.on('ready', () => { + session.defaultSession.setCertificateVerifyProc((request, callback) => { + callback(0); // 0 表示通过所有证书校验 + }); +}); + +// 处理应用退出前的事件,设置退出标志 +app.on('before-quit', () => { + isQuitting = true; +}); + +app.on('will-quit', () => { + unregisterAllShortcuts(); +}); + +app.on('window-all-closed', () => { + ipcMain.removeAllListeners(); + cleanupDeploymentHandlers(); + if (process.platform !== 'darwin') { + app.quit(); + } +}); + +// 在macOS上,当应用图标被点击时重置退出标志和显示主窗口 +app.on('activate', () => { + isQuitting = false; +}); + +/** + * 应用准备好时的处理函数 + */ +async function onReady() { + try { + // 首先注册IPC监听器,确保欢迎窗口也能使用相关功能 + registerIpcListeners(); + + // 检查配置文件是否存在,如果不存在则显示欢迎界面 + const shouldShowWelcome = checkAndShowWelcomeIfNeeded(); + + if (shouldShowWelcome) { + console.log('First time startup, showing welcome window'); + // 如果是首次启动,显示欢迎界面后等待用户完成配置 + // 用户完成配置时会触发 continueAppStartup 函数 + return; + } + + // 继续正常的应用启动流程 + await continueAppStartup(); + } catch (error) { + console.error('Application startup error:', error); + } +} + +/** + * 继续应用启动(在配置完成后调用) + */ +export async function continueAppStartup() { + try { + // 初始化缓存目录和配置 + await mkdirpIgnoreError(cachePath); + const commonCacheConf = await getUserDefinedConf(commonCacheConfPath); + + // 解析国际化和主题配置 + const [nlsConfig, themeConfig] = await Promise.all([ + resolveNlsConfiguration(commonCacheConf, osLocale), + resolveThemeConfiguration(commonCacheConf), + ]); + + // 设置主题和环境变量 + setApplicationTheme(themeConfig.theme || 'light'); + process.env['EULERCOPILOT_NLS_CONFIG'] = JSON.stringify(nlsConfig); + process.env['EULERCOPILOT_CACHE_PATH'] = cachePath || ''; + + // 启动应用 + await startup(); + + // 设置原生应用菜单 + Menu.setApplicationMenu(buildAppMenu(nlsConfig)); + + // 注册全局快捷键 + registerGlobalShortcut(); + + // 在macOS上,监听辅助功能权限变化,重新注册快捷键 + if (process.platform === 'darwin') { + app.accessibilitySupportEnabled = checkAccessibilityPermission(); + + app.on( + 'accessibility-support-changed', + (event, accessibilitySupportEnabled) => { + console.log( + 'Accessibility support changed:', + accessibilitySupportEnabled, + ); + if (accessibilitySupportEnabled) { + registerGlobalShortcut(); + } + }, + ); + } + } catch (error) { + console.error('Continue app startup error:', error); + } +} + +/** + * 启动应用 + */ +async function startup() { + // 创建系统托盘 + createTray(); + + // 创建应用窗口 + let win = createDefaultWindow(); + let chatWindow = createChatWindow(); + + // 处理窗口激活 + app.on('activate', () => { + isQuitting = false; + if (BrowserWindow.getAllWindows().length === 0) { + // 如果没有窗口,则创建新窗口 + win = createDefaultWindow(); + chatWindow = createChatWindow(); + } else { + // 如果窗口存在但被隐藏,则显示主窗口 + if (win && !win.isDestroyed()) { + win.show(); + } + } + }); + + // 设置主窗口的关闭行为 + win.on('close', (event) => { + // 如果应用正在退出(例如通过Cmd+Q触发),则允许窗口正常关闭 + if (isQuitting) { + return; + } + // 否则阻止关闭,只是隐藏窗口 + event.preventDefault(); + win.hide(); + }); + + // 设置聊天窗口的关闭行为 + chatWindow.on('close', (event) => { + // 如果应用正在退出(例如通过Cmd+Q触发),则允许窗口正常关闭 + if (isQuitting) { + return; + } + // 触发窗口隐藏事件 + chatWindow.webContents.send('clean:storage'); + // 清除 localStorage 中的 conversationId + win.webContents.executeJavaScript(` + localStorage.removeItem('conversationId'); + true; + `); + event.preventDefault(); + chatWindow.hide(); + }); +} diff --git a/electron/main/node/userDataPath.ts b/electron/main/node/userDataPath.ts new file mode 100644 index 0000000000000000000000000000000000000000..806e9ecb89a0ed24049d6fe104dc9bbcd4c096d9 --- /dev/null +++ b/electron/main/node/userDataPath.ts @@ -0,0 +1,53 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import * as os from 'os'; +import * as path from 'node:path'; + +const cwd = process.cwd(); + +export function getUserDataPath(productName: string): string { + const userDataPath = doGetUserDataPath(productName); + const pathsToResolve = [userDataPath]; + + if (!path.isAbsolute(userDataPath)) { + pathsToResolve.unshift(cwd); + } + + return path.resolve(...pathsToResolve); +} + +function doGetUserDataPath(productName: string): string { + let appDataPath = process.env['VSCODE_APPDATA']; + switch (process.platform) { + case 'win32': + appDataPath = process.env['APPDATA']; + if (!appDataPath) { + const userProfile = process.env['USERPROFILE']; + if (typeof userProfile !== 'string') { + throw new Error( + 'Windows: Unexpected undefined %USERPROFILE% environment variable', + ); + } + appDataPath = path.join(userProfile, 'AppData', 'Roaming'); + } + break; + case 'darwin': + appDataPath = path.join(os.homedir(), 'Library', 'Application Support'); + break; + case 'linux': + appDataPath = + process.env['XDG_CONFIG_HOME'] || path.join(os.homedir(), '.config'); + break; + default: + throw new Error('Platform not supported'); + } + + return path.join(appDataPath, productName); +} diff --git a/electron/main/window/create.ts b/electron/main/window/create.ts new file mode 100644 index 0000000000000000000000000000000000000000..61a8534f6c89d2f3bdc960442834499a3696ebf3 --- /dev/null +++ b/electron/main/window/create.ts @@ -0,0 +1,337 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import path from 'node:path'; +import * as electron from 'electron'; +import { BrowserWindow, app, ipcMain, Menu } from 'electron'; +import { options as allWindow } from './options'; +import { updateConf } from '../common/cache-conf'; +import { isLinux } from '../common/platform'; + +// 存储所有创建的窗口实例,用于全局访问 +const windowInstances: Map = new Map(); + +export function createWindow( + options: Electron.BrowserWindowConstructorOptions, + hash: string, + id?: string, +): BrowserWindow { + const win = new BrowserWindow({ + ...options, + webPreferences: { + webSecurity: false, // 禁用安全策略(不推荐) + nodeIntegration: true, + contextIsolation: false, + preload: path.join(__dirname, '../preload/index.js'), + }, + }); + + // 存储窗口实例以便全局访问 + if (id) { + windowInstances.set(id, win); + } + + if (app.isPackaged) { + win.loadFile(path.join(__dirname, `../index.html`), { hash }); + } else { + win.loadURL(`http://localhost:${process.env.PORT}/#${hash}`); + } + + // 设置窗口控制事件处理 + setupWindowControls(win); + + // 设置右键上下文菜单 + setupContextMenu(win); + + // 设置窗口打开处理程序 + setupWindowOpenHandler(win); + + return win; +} + +/** + * 为窗口设置控制事件(适用于所有平台) + */ +function setupWindowControls(win: BrowserWindow) { + // 监听窗口最大化/还原事件 + win.on('maximize', () => { + if (win.webContents) { + win.webContents.send('window-maximized-change', true); + } + }); + + win.on('unmaximize', () => { + if (win.webContents) { + win.webContents.send('window-maximized-change', false); + } + }); +} + +/** + * 设置右键上下文菜单,支持中英文 + */ +function setupContextMenu(win: BrowserWindow) { + win.webContents.on('context-menu', (_event, params) => { + const nlsEnv = process.env.EULERCOPILOT_NLS_CONFIG; + let resolved = 'en'; + try { + const cfg = JSON.parse(nlsEnv || '{}'); + resolved = cfg.resolvedLanguage || 'en'; + } catch { + resolved = 'en'; + } + const isZh = resolved.startsWith('zh'); + const template: Electron.MenuItemConstructorOptions[] = [ + { + role: 'copy', + label: isZh ? '复制' : 'Copy', + enabled: params.selectionText.length > 0, + }, + { + role: 'paste', + label: isZh ? '粘贴' : 'Paste', + enabled: params.editFlags.canPaste, + }, + { type: 'separator' }, + { role: 'selectAll', label: isZh ? '全选' : 'Select All' }, + ]; + Menu.buildFromTemplate(template).popup({ window: win }); + }); +} + +function setupWindowOpenHandler(win: BrowserWindow) { + win.webContents.setWindowOpenHandler((details: Electron.HandlerDetails) => { + if (details.url) { + const features = details.features || ''; + const width = parseInt(features.split('width=')[1] || '800', 10); + const height = parseInt(features.split('height=')[1] || '600', 10); + const x = parseInt(features.split('left=')[1] || '0', 10); + const y = parseInt(features.split('top=')[1] || '0', 10); + + return { + action: 'allow', + overrideBrowserWindowOptions: { + width, + height, + autoHideMenuBar: true, + x, + y, + resizable: true, + webPreferences: { + preload: path.join(__dirname, '../preload/index.js'), + webSecurity: false, + nodeIntegration: true, + nodeIntegrationInWorker: true, + }, + }, + }; + } + return { action: 'deny' }; + }); + + // 为新窗口设置右键菜单 + win.webContents.on('did-create-window', (childWindow) => { + setupContextMenu(childWindow); + }); +} + +let defaultWindow: BrowserWindow | null = null; +let chatWindow: BrowserWindow | null = null; + +/** + * 获取窗口标题栏的样式配置 + * @param theme 主题类型 'dark'|'light' + * @returns Electron.TitleBarOverlay 配置 + */ +function getDefaultTitleBarOverlay( + theme: string = 'light', +): Electron.TitleBarOverlay { + return { + color: theme === 'dark' ? '#1f2329' : '#ffffff', + symbolColor: theme === 'dark' ? 'white' : 'black', + height: 48, + }; +} + +/** + * 创建默认窗口 + * 仅在第一次调用时创建,后续调用返回已创建的窗口实例 + */ +export function createDefaultWindow(): BrowserWindow { + if (defaultWindow) return defaultWindow; + + const hash = allWindow.mainWindow.hash; + const defaultWindowOptions = allWindow.mainWindow.window; + const theme = process.env.EULERCOPILOT_THEME || 'light'; + + // 仅在非Linux平台设置titleBarOverlay + if (!isLinux) { + defaultWindowOptions.titleBarOverlay = getDefaultTitleBarOverlay(theme); + } + + defaultWindow = createWindow(defaultWindowOptions, hash, 'mainWindow'); + + // 开发模式下可以打开开发者工具 + if (process.env.NODE_ENV === 'development') { + defaultWindow.webContents.openDevTools({ mode: 'detach' }); + } + + // 设置窗口标题并显示窗口 + defaultWindow.webContents.on('did-finish-load', () => { + defaultWindow?.setTitle('openEuler 智能化解决方案'); + if (defaultWindow && !defaultWindow.isDestroyed()) { + defaultWindow.show(); + } + }); + + return defaultWindow; +} + +/** + * 计算聊天窗口默认位置 + */ +function getDefaultChatWindowPosition( + windowWidth: number, + windowHeight: number, +) { + const rightOffset = 24; // 右侧间距 + // 获取主显示器的工作区尺寸 + const primaryDisplay = electron.screen.getPrimaryDisplay(); + const { width: screenWidth, height: screenHeight } = + primaryDisplay.workAreaSize; + + // x坐标:屏幕宽度 - 窗口宽度 - 右侧间距 + const x = screenWidth - windowWidth - rightOffset; + + // y坐标:上下居中 + const y = Math.round((screenHeight - windowHeight) / 2); + + return { x, y }; +} + +/** + * 创建聊天窗口 + * 仅在第一次调用时创建,后续调用返回已创建的窗口实例 + */ +export function createChatWindow(): BrowserWindow { + if (chatWindow) return chatWindow; + const hash = allWindow.chatWindow.hash; + const chatWindowOptions = { ...allWindow.chatWindow.window }; + const theme = process.env.EULERCOPILOT_THEME || 'light'; + + // 计算窗口位置 + const { x, y } = getDefaultChatWindowPosition( + chatWindowOptions.width || 680, + chatWindowOptions.height || 960, + ); + + // 设置窗口位置 + chatWindowOptions.x = x; + chatWindowOptions.y = y; + + // 仅在非Linux平台设置titleBarOverlay + if (!isLinux) { + chatWindowOptions.titleBarOverlay = getDefaultTitleBarOverlay(theme); + } + + chatWindow = createWindow(chatWindowOptions, hash, 'chatWindow'); + + // 设置窗口标题 + chatWindow.webContents.on('did-finish-load', () => { + chatWindow?.setTitle('快捷问答'); + }); + + return chatWindow; +} + +// 全局设置IPC事件处理 +ipcMain.on('window-control', (e, command) => { + console.log('Received window control command:', command); + + // 确保命令来自正确的窗口 + const webContents = e.sender; + const win = BrowserWindow.fromWebContents(webContents); + + if (!win) { + console.error('Cannot find window for the command'); + return; + } + + switch (command) { + case 'minimize': + console.log('Minimizing window'); + win.minimize(); + break; + case 'maximize': + if (win.isMaximized()) { + console.log('Unmaximizing window'); + win.unmaximize(); + } else { + console.log('Maximizing window'); + win.maximize(); + } + break; + case 'close': + console.log('Closing window'); + win.close(); + break; + default: + console.error('Unknown window command:', command); + } +}); + +// 添加查询窗口最大化状态的处理程序 +ipcMain.handle('window-is-maximized', (e) => { + const win = BrowserWindow.fromWebContents(e.sender); + if (win) { + return win.isMaximized(); + } + return false; +}); + +ipcMain.handle('copilot:theme', (e, args) => { + electron.nativeTheme.themeSource = args.theme; + + // 仅在非Linux平台上更新titleBarOverlay + if (!isLinux) { + if (chatWindow) { + chatWindow.setTitleBarOverlay({ + color: args.backgroundColor, + symbolColor: args.theme === 'dark' ? 'white' : 'black', + }); + } + + if (defaultWindow) { + defaultWindow.setTitleBarOverlay({ + color: args.backgroundColor, + symbolColor: args.theme === 'dark' ? 'white' : 'black', + }); + } + } + + // 通知渲染进程主题已更改,以更新Linux自定义标题栏 + if (isLinux) { + if (chatWindow && chatWindow.webContents) { + chatWindow.webContents.send('theme-updated', args); + } + if (defaultWindow && defaultWindow.webContents) { + defaultWindow.webContents.send('theme-updated', args); + } + } + + updateConf({ + theme: args.theme, + }); +}); + +ipcMain.handle('copilot:lang', (e, args) => { + updateConf({ + userLocale: args.lang, + }); +}); diff --git a/electron/main/window/index.ts b/electron/main/window/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..cb1dea897d020182ff395cc8693a65d44ab4ce0c --- /dev/null +++ b/electron/main/window/index.ts @@ -0,0 +1,33 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { createDefaultWindow, createChatWindow } from './create'; +import { createTray } from './tray'; // 导入createTray函数 +import { + createWelcomeWindow, + showWelcomeWindow, + hideWelcomeWindow, + closeWelcomeWindow, + checkAndShowWelcomeIfNeeded, + completeWelcomeFlow, +} from './welcome'; + +// 重新导出以便在index.ts中使用 +export { + createDefaultWindow, + createChatWindow, + createTray, + createWelcomeWindow, + showWelcomeWindow, + hideWelcomeWindow, + closeWelcomeWindow, + checkAndShowWelcomeIfNeeded, + completeWelcomeFlow, +}; diff --git a/electron/main/window/options.ts b/electron/main/window/options.ts new file mode 100644 index 0000000000000000000000000000000000000000..78423d34724bb78f478dd350561ebcb3913221d0 --- /dev/null +++ b/electron/main/window/options.ts @@ -0,0 +1,83 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import { isLinux } from '../common/platform'; + +export interface allWindowType { + [propName: string]: { + id: string; + window: Electron.BrowserWindowConstructorOptions; + hash: string; + }; +} + +// Linux平台专用窗口配置,添加圆角支持 +const getLinuxSpecificOptions = + (): Partial => { + if (!isLinux) return {}; + + return { + transparent: true, + backgroundColor: '#00000000', + }; + }; + +// 调整Linux平台窗口尺寸,为16px阴影留出空间 +const adjustWindowSize = ( + options: Electron.BrowserWindowConstructorOptions, +): Electron.BrowserWindowConstructorOptions => { + if (!isLinux) return options; + + // 阴影在各个方向增加16px,所以宽高各需要增加32px + const shadowOffset = 32; // 16px * 2 + const result = { ...options }; + + if (result.width) result.width += shadowOffset; + if (result.height) result.height += shadowOffset; + if (result.minWidth) result.minWidth += shadowOffset; + if (result.minHeight) result.minHeight += shadowOffset; + + return result; +}; + +export const options: allWindowType = { + mainWindow: { + id: 'mainWindow', + window: adjustWindowSize({ + width: 1440, + height: 810, + minWidth: 1440, + minHeight: 810, + titleBarStyle: 'hidden', + resizable: true, + show: false, + alwaysOnTop: false, + useContentSize: true, + ...getLinuxSpecificOptions(), + }), + hash: '/', + }, + chatWindow: { + id: 'chatWindow', + window: adjustWindowSize({ + width: 680, + height: 960, + minWidth: 680, + minHeight: 810, + resizable: true, + show: false, + skipTaskbar: true, + alwaysOnTop: true, + useContentSize: true, + titleBarStyle: 'hidden', + ...getLinuxSpecificOptions(), + }), + hash: '/chat', + }, +}; diff --git a/electron/main/window/tray.ts b/electron/main/window/tray.ts new file mode 100644 index 0000000000000000000000000000000000000000..7eaa058501dd613f2677b8dbcb05997b8c60630c --- /dev/null +++ b/electron/main/window/tray.ts @@ -0,0 +1,103 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. +import path from 'node:path'; +import { app, Tray, Menu, BrowserWindow, nativeImage } from 'electron'; +import type { MenuItemConstructorOptions } from 'electron'; +import { createDefaultWindow, createChatWindow } from './create'; + +// 保存对主窗口和聊天窗口的引用,方便在托盘菜单中使用 +let defaultWindow: BrowserWindow | null = null; +let chatWindow: BrowserWindow | null = null; + +export function createTray(): Tray { + let appTray: Tray | null = null; + + if (appTray) return appTray; + + // 获取窗口引用 + defaultWindow = + BrowserWindow.getAllWindows().find((win) => + win.webContents.getURL().includes('main'), + ) || null; + + chatWindow = + BrowserWindow.getAllWindows().find((win) => + win.webContents.getURL().includes('chat'), + ) || null; + + // 如果没有找到窗口,尝试创建它们 + if (!defaultWindow) { + defaultWindow = createDefaultWindow(); + } + + if (!chatWindow) { + chatWindow = createChatWindow(); + } + + const trayMenus: MenuItemConstructorOptions[] = [ + { + label: '显示主窗口', + click: () => { + if (defaultWindow) { + defaultWindow.show(); + defaultWindow.focus(); + } else { + defaultWindow = createDefaultWindow(); + defaultWindow.show(); + } + }, + }, + { + label: '启动快捷问答', + click: () => { + if (chatWindow) { + chatWindow.show(); + chatWindow.focus(); + } else { + chatWindow = createChatWindow(); + chatWindow.show(); + } + }, + }, + { type: 'separator' }, + { + label: '退出', + click: () => { + app.exit(); + }, + }, + ]; + const iconPath = + process.platform === 'darwin' + ? path.join(__dirname, '../trayTemplate.png') + : path.join(__dirname, '../tray.png'); + appTray = new Tray(iconPath); + // 根据平台处理图标 + if (process.platform === 'win32') { + // Windows平台直接设置图标 + appTray.setImage(iconPath); + } else if (process.platform === 'darwin') { + // macOS 平台需要调整尺寸并设置为模板图像 + const image = nativeImage.createFromPath(iconPath); + const resizedImage = image.resize({ width: 18, height: 18 }); + resizedImage.setTemplateImage(true); + appTray.setImage(resizedImage); + } else if (process.platform === 'linux') { + // Linux 平台需要调整尺寸 + const image = nativeImage.createFromPath(iconPath); + const resizedImage = image.resize({ width: 18, height: 18 }); + appTray.setImage(resizedImage); + } + const contextMenu = Menu.buildFromTemplate(trayMenus); + appTray.setToolTip('openEuler Intelligence'); + + appTray.setContextMenu(contextMenu); + return appTray; +} diff --git a/electron/main/window/welcome.ts b/electron/main/window/welcome.ts new file mode 100644 index 0000000000000000000000000000000000000000..6e133a11f98472cbf8e8caaf312add9f5a257fd0 --- /dev/null +++ b/electron/main/window/welcome.ts @@ -0,0 +1,165 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { BrowserWindow } from 'electron'; +import * as path from 'node:path'; +import { getConfigManager } from '../common/config'; +import { setDeploymentMainWindow } from '../common/ipc'; + +/** + * 欢迎窗口引用 + */ +let welcomeWindow: BrowserWindow | null = null; + +/** + * 创建欢迎窗口 + */ +export function createWelcomeWindow(): BrowserWindow { + // 如果欢迎窗口已存在且未被销毁,则返回现有窗口 + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.focus(); + return welcomeWindow; + } + + // 创建欢迎窗口 + welcomeWindow = new BrowserWindow({ + width: 720, + height: 560, + minWidth: 720, + minHeight: 560, + center: true, + resizable: false, + maximizable: false, + minimizable: false, + alwaysOnTop: false, + frame: false, + title: '欢迎使用', + webPreferences: { + nodeIntegration: false, + contextIsolation: true, + preload: path.join(__dirname, '../preload/welcome.js'), // 欢迎界面专用预加载脚本 + }, + show: false, + }); + + // 加载欢迎界面的 HTML 文件 + welcomeWindow.loadFile(path.join(__dirname, '../welcome/welcome.html')); + + // 开发模式下可以打开开发者工具 + if (process.env.NODE_ENV === 'development') { + welcomeWindow.webContents.openDevTools({ mode: 'detach' }); + } + + // 窗口准备显示时显示窗口 + welcomeWindow.once('ready-to-show', () => { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.show(); + // 设置部署服务的主窗口引用 + setDeploymentMainWindow(welcomeWindow); + } + }); + + // 窗口关闭时清理引用 + welcomeWindow.on('closed', () => { + welcomeWindow = null; + }); + + // 阻止窗口导航到外部链接 + welcomeWindow.webContents.on('will-navigate', (event, navigationUrl) => { + const parsedUrl = new URL(navigationUrl); + if (parsedUrl.origin !== 'data:') { + event.preventDefault(); + } + }); + + // 阻止新窗口创建 + welcomeWindow.webContents.setWindowOpenHandler(() => { + return { action: 'deny' }; + }); + + return welcomeWindow; +} + +/** + * 显示欢迎窗口 + */ +export function showWelcomeWindow(): BrowserWindow { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.show(); + welcomeWindow.focus(); + return welcomeWindow; + } + return createWelcomeWindow(); +} + +/** + * 隐藏欢迎窗口 + */ +export function hideWelcomeWindow(): void { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.hide(); + } +} + +/** + * 关闭欢迎窗口 + */ +export function closeWelcomeWindow(): void { + if (welcomeWindow && !welcomeWindow.isDestroyed()) { + welcomeWindow.close(); + } +} + +/** + * 检查是否需要显示欢迎界面 + * 当配置文件不存在时,显示欢迎界面 + */ +export function checkAndShowWelcomeIfNeeded(): boolean { + const configManager = getConfigManager(); + + if (!configManager.isConfigExists()) { + console.log('Configuration file not found, showing welcome window'); + showWelcomeWindow(); + return true; + } + + return false; +} + +/** + * 完成欢迎流程 + * 初始化配置并关闭欢迎窗口,然后继续应用启动 + */ +export async function completeWelcomeFlow(): Promise { + const configManager = getConfigManager(); + + try { + // 确保配置文件已初始化 + configManager.initializeConfig(); + + // 关闭欢迎窗口 + closeWelcomeWindow(); + + console.log('Welcome flow completed, configuration initialized'); + + // 动态导入主模块以避免循环依赖 + const { continueAppStartup } = await import('../index'); + await continueAppStartup(); + } catch (error) { + console.error('Failed to complete welcome flow:', error); + } +} + +/** + * 获取当前欢迎窗口实例 + */ +export function getWelcomeWindow(): BrowserWindow | null { + return welcomeWindow; +} diff --git a/electron/preload/index.ts b/electron/preload/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..be190682ee4a2c14a4e7952be88f162aa97dc661 --- /dev/null +++ b/electron/preload/index.ts @@ -0,0 +1,147 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { contextBridge } from 'electron'; +import { + safeIPC, + sharedConfigAPI, + windowAPI, + systemAPI, + themeAPI, + utilsAPI, +} from './shared'; +import type { DesktopConfig } from './types'; + +/** + * 主窗口 preload 脚本 + * 提供主应用所需的所有 API + */ + +const globals = { + // 原始 IPC 接口(兼容性) + ipcRenderer: safeIPC, + + // 配置管理(主程序完整功能) + config: { + /** + * 获取当前配置 + */ + get: async (): Promise => { + try { + return await safeIPC.invoke('copilot:get-config'); + } catch (error) { + console.error('Failed to get config:', error); + return null; + } + }, + + /** + * 更新配置 + */ + update: async ( + updates: Partial, + ): Promise => { + try { + return await safeIPC.invoke('copilot:update-config', updates); + } catch (error) { + console.error('Failed to update config:', error); + return null; + } + }, + + /** + * 重置配置为默认值 + */ + reset: async (): Promise => { + try { + return await safeIPC.invoke('copilot:reset-config'); + } catch (error) { + console.error('Failed to reset config:', error); + return null; + } + }, + + /** + * 设置代理URL + */ + setProxyUrl: sharedConfigAPI.setProxyUrl, + + /** + * 获取代理URL + */ + getProxyUrl: async (): Promise => { + try { + return await safeIPC.invoke('copilot:get-proxy-url'); + } catch (error) { + console.error('Failed to get proxy URL:', error); + return ''; + } + }, + + /** + * 验证服务器连接(统一接口) + */ + validateServer: sharedConfigAPI.validateServer, + }, + + // 窗口控制 + window: { + // 统一的窗口控制接口 + control: async ( + command: 'minimize' | 'maximize' | 'close', + ): Promise => { + switch (command) { + case 'minimize': + return await windowAPI.minimize(); + case 'maximize': + return await windowAPI.maximize(); + case 'close': + return await windowAPI.close(); + default: + throw new Error(`Unknown window command: ${command}`); + } + }, + + // 单独的方法(向后兼容) + ...windowAPI, + }, + + // 主题管理 + theme: themeAPI, + + // 系统信息 + process: systemAPI, + + // 实用工具 + utils: utilsAPI, + + // 快捷聊天 + chat: { + /** + * 清理数据 + */ + onCleanStorage(callback): void { + safeIPC.on('clean:storage', (_event, value) => callback(value)); + }, + }, +}; + +// 暴露 API 到渲染进程 +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('eulercopilot', globals); + } catch (error) { + console.error('Failed to expose main API:', error); + } +} else { + (window as any).eulercopilot = globals; +} + +console.log('Main preload script loaded'); diff --git a/electron/preload/shared.ts b/electron/preload/shared.ts new file mode 100644 index 0000000000000000000000000000000000000000..df9c2537eab93cc7a347fa58294c11d0a0e33551 --- /dev/null +++ b/electron/preload/shared.ts @@ -0,0 +1,283 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { ipcRenderer } from 'electron'; +import type { ServerValidationResult } from './types'; + +/** + * 共享的 preload 功能模块 + * 避免在多个 preload 脚本中重复代码 + */ + +/** + * IPC 通道验证 + */ +export function validateIPC(channel: string): true | never { + if ( + !channel || + (!channel.startsWith('copilot:') && !channel.startsWith('deployment:')) + ) { + // 允许窗口状态变化事件通过验证 + if ( + channel === 'window-maximized-change' || + channel === 'window-is-maximized' || + channel === 'clean:storage' + ) { + return true; + } + throw new Error(`Unsupported event IPC channel '${channel}'`); + } + return true; +} + +/** + * 安全的 IPC 调用包装器 + */ +export const safeIPC = { + invoke: async (channel: string, ...args: any[]): Promise => { + validateIPC(channel); + return await ipcRenderer.invoke(channel, ...args); + }, + + on: (channel: string, listener: (...args: any[]) => void): void => { + validateIPC(channel); + ipcRenderer.on(channel, (event, ...args) => { + listener(...args); + }); + }, + + once: (channel: string, listener: (...args: any[]) => void): void => { + validateIPC(channel); + ipcRenderer.once(channel, (event, ...args) => listener(...args)); + }, + + removeListener: ( + channel: string, + listener: (...args: any[]) => void, + ): void => { + validateIPC(channel); + ipcRenderer.removeListener(channel, listener); + }, + + removeAllListeners: (channel: string): void => { + validateIPC(channel); + ipcRenderer.removeAllListeners(channel); + }, +}; + +/** + * 配置管理 API(代理设置和服务器验证,供所有窗口使用) + */ +export const sharedConfigAPI = { + /** + * 设置代理URL + */ + setProxyUrl: async (url: string): Promise => { + try { + return await safeIPC.invoke('copilot:set-proxy-url', url); + } catch (error) { + console.error('Failed to set proxy URL:', error); + return false; + } + }, + + /** + * 验证服务器连接(统一接口) + */ + validateServer: async (url: string): Promise => { + try { + return await safeIPC.invoke('copilot:validate-server', url); + } catch (error) { + console.error('Failed to validate server:', error); + return { + isValid: false, + error: + error instanceof Error ? error.message : '验证服务器时发生未知错误', + }; + } + }, +}; + +/** + * 窗口控制 API(共享) + */ +export const windowAPI = { + /** + * 关闭窗口 + */ + close: async (): Promise => { + try { + await safeIPC.invoke('copilot:window-control', 'close'); + } catch (error) { + console.error('Failed to close window:', error); + } + }, + + /** + * 最小化窗口 + */ + minimize: async (): Promise => { + try { + await safeIPC.invoke('copilot:window-control', 'minimize'); + } catch (error) { + console.error('Failed to minimize window:', error); + } + }, + + /** + * 最大化窗口 + */ + maximize: async (): Promise => { + try { + await safeIPC.invoke('copilot:window-control', 'maximize'); + } catch (error) { + console.error('Failed to maximize window:', error); + } + }, + + /** + * 检查窗口是否最大化 + */ + isMaximized: async (): Promise => { + try { + return await safeIPC.invoke('copilot:window-is-maximized'); + } catch (error) { + console.error('Failed to check window maximized state:', error); + return false; + } + }, + + /** + * 监听窗口最大化状态变化 + */ + onMaximizedChange: (callback: (isMaximized: boolean) => void): void => { + safeIPC.on('window-maximized-change', callback); + }, + + /** + * 移除窗口最大化状态变化监听 + */ + offMaximizedChange: (): void => { + safeIPC.removeAllListeners('window-maximized-change'); + }, +}; + +/** + * 系统信息 API(共享) + */ +export const systemAPI = { + /** + * 获取平台信息 + */ + get platform(): string { + return process.platform; + }, + + /** + * 获取架构信息 + */ + get arch(): string { + return process.arch; + }, + + /** + * 获取版本信息 + */ + get versions(): NodeJS.ProcessVersions { + return process.versions; + }, + + /** + * 获取环境变量(安全的副本) + */ + get env(): Record { + return { ...process.env }; + }, +}; + +/** + * 实用工具 API(共享) + */ +export const utilsAPI = { + /** + * 检查 URL 格式是否有效 + */ + isValidUrl: (url: string): boolean => { + try { + new URL(url); + return true; + } catch { + return false; + } + }, + + /** + * 格式化 URL(确保有协议) + */ + formatUrl: (url: string): string => { + if (!url) return ''; + + // 移除尾部斜杠 + url = url.replace(/\/+$/, ''); + + // 如果没有协议,默认添加 https:// + if (!/^https?:\/\//i.test(url)) { + url = 'https://' + url; + } + + return url; + }, + + /** + * 延迟执行 + */ + delay: (ms: number): Promise => { + return new Promise((resolve) => setTimeout(resolve, ms)); + }, +}; + +/** + * 主题 API(共享) + */ +export const themeAPI = { + /** + * 切换主题 + */ + toggle: async (): Promise => { + try { + return await safeIPC.invoke('copilot:toggle'); + } catch (error) { + console.error('Failed to toggle theme:', error); + return null; + } + }, + + /** + * 设置系统主题 + */ + setSystem: async (): Promise => { + try { + return await safeIPC.invoke('copilot:system'); + } catch (error) { + console.error('Failed to set system theme:', error); + return null; + } + }, +}; + +// 确保此文件被识别为 ES 模块 +export default { + safeIPC, + sharedConfigAPI, + windowAPI, + systemAPI, + utilsAPI, + themeAPI, +}; diff --git a/electron/preload/types.d.ts b/electron/preload/types.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e314e8fb9a6aecc7a219c1ae87247189d5829c3 --- /dev/null +++ b/electron/preload/types.d.ts @@ -0,0 +1,144 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +/** + * Electron Preload API 类型定义 + */ + +export interface DesktopConfig { + base_url: string; + [key: string]: any; +} + +export interface ServerValidationResult { + isValid: boolean; + error?: string; + status?: number; + responseTime?: number; +} + +export interface DesktopAppAPI { + // IPC 渲染器(原始接口) + ipcRenderer: { + invoke(channel: string, ...args: any[]): Promise; + on(channel: string, listener: (...args: any[]) => void): void; + once(channel: string, listener: (...args: any[]) => void): void; + removeListener(channel: string, listener: (...args: any[]) => void): void; + removeAllListeners(channel: string): void; + }; + + // 配置管理 + config: { + get(): Promise; + update(updates: Partial): Promise; + reset(): Promise; + setProxyUrl(url: string): Promise; + getProxyUrl(): Promise; + validateServer(url: string): Promise; + }; + + // 窗口控制 + window: { + control(command: 'minimize' | 'maximize' | 'close'): Promise; + close(): Promise; + minimize(): Promise; + maximize(): Promise; + isMaximized(): Promise; + onMaximizedChange(callback: (isMaximized: boolean) => void): void; + offMaximizedChange(): void; + }; + + // 主题 + theme: { + toggle(): Promise; + setSystem(): Promise; + }; + + // 进程信息 + process: { + platform: string; + arch: string; + versions: NodeJS.ProcessVersions; + env: Record; + }; + + // 实用工具 + utils: { + isValidUrl(url: string): boolean; + formatUrl(url: string): string; + delay(ms: number): Promise; + }; + + // 快捷聊天 + chat: { + onCleanStorage(fun: () => void): void; + }; +} + +export interface DesktopAppWelcomeAPI { + // 配置管理(代理设置和服务器验证,欢迎界面功能) + config: { + setProxyUrl(url: string): Promise; + validateServer(url: string): Promise; + }; + + // 欢迎流程 + welcome: { + show(): Promise; + complete(): Promise; + cancel(): Promise; + }; + + // 部署服务 + deployment: { + startDeploymentFromForm(formData: { + ruleForm: { + url: string; + modelName: string; + apiKey: string; + }; + embeddingRuleForm: { + url: string; + modelName: string; + apiKey: string; + }; + }): Promise; + stopDeployment(): Promise; + getStatus(): Promise; + onStatusChange(callback: (status: any) => void): void; + removeStatusListener(): void; + cleanup(): Promise; + addHostsEntries(domains: string[]): Promise; + }; + + // 系统信息 + system: { + platform: string; + arch: string; + versions: NodeJS.ProcessVersions; + env: Record; + }; + + // 实用工具 + utils: { + isValidUrl(url: string): boolean; + formatUrl(url: string): string; + delay(ms: number): Promise; + }; +} + +declare global { + interface Window { + eulercopilot: DesktopAppAPI; + eulercopilotWelcome: DesktopAppWelcomeAPI; + } +} + +export {}; diff --git a/electron/preload/welcome.ts b/electron/preload/welcome.ts new file mode 100644 index 0000000000000000000000000000000000000000..af3e2a90303942cca3c8746ae96bdc75f8c35353 --- /dev/null +++ b/electron/preload/welcome.ts @@ -0,0 +1,168 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +import { contextBridge } from 'electron'; +import { sharedConfigAPI, systemAPI, utilsAPI, safeIPC } from './shared'; + +// 导入部署服务API +let statusChangeCallback: ((status: any) => void) | null = null; +let isListenerSetup = false; + +const deploymentAPI = { + /** + * 从前端表单开始部署 + */ + startDeploymentFromForm: (formData: { + ruleForm: { + url: string; + modelName: string; + apiKey: string; + }; + embeddingRuleForm: { + url: string; + modelName: string; + apiKey: string; + }; + }): Promise => { + return safeIPC.invoke('deployment:startFromForm', formData); + }, + + /** + * 停止部署 + */ + stopDeployment: (): Promise => { + return safeIPC.invoke('deployment:stop'); + }, + + /** + * 获取部署状态 + */ + getStatus: (): Promise => { + return safeIPC.invoke('deployment:getStatus'); + }, + + /** + * 监听部署状态变化 + */ + onStatusChange: (callback: (status: any) => void): void => { + // 存储回调函数 + statusChangeCallback = callback; + + // 只设置一次 IPC 监听器 + if (!isListenerSetup) { + isListenerSetup = true; + safeIPC.on('deployment:statusChanged', (status) => { + if (statusChangeCallback) { + statusChangeCallback(status); + } + }); + } + }, + + /** + * 移除状态变化监听器 + */ + removeStatusListener: (): void => { + statusChangeCallback = null; + safeIPC.removeAllListeners('deployment:statusChanged'); + isListenerSetup = false; + }, + + /** + * 清理部署文件 + */ + cleanup: (): Promise => { + return safeIPC.invoke('deployment:cleanup'); + }, + + /** + * 添加 hosts 条目 + */ + addHostsEntries: (domains: string[]): Promise => { + return safeIPC.invoke('deployment:addHostsEntries', domains); + }, +}; + +/** + * 欢迎界面专用的 preload 脚本 + * 提供配置管理和欢迎流程相关的 API,使用统一的后端验证接口 + */ + +/** + * 欢迎流程 API(专用于欢迎界面) + */ +const welcomeFlowAPI = { + /** + * 显示欢迎界面 + */ + show: async (): Promise => { + try { + return await safeIPC.invoke('copilot:show-welcome'); + } catch (error) { + console.error('Failed to show welcome:', error); + return false; + } + }, + + /** + * 完成欢迎设置流程 + */ + complete: async (): Promise => { + try { + return await safeIPC.invoke('copilot:complete-welcome'); + } catch (error) { + console.error('Failed to complete welcome flow:', error); + return false; + } + }, + + /** + * 取消欢迎流程(关闭窗口) + */ + cancel: async (): Promise => { + try { + await safeIPC.invoke('copilot:window-control', 'close'); + } catch (error) { + console.error('Failed to cancel welcome flow:', error); + } + }, +}; + +/** + * 欢迎界面 API + * 只提供代理设置、服务器验证和欢迎流程功能 + */ +const welcomeAPI = { + // 配置管理(仅代理设置和服务器验证) + config: sharedConfigAPI, + + // 欢迎流程管理(专用实现) + welcome: welcomeFlowAPI, + + // 部署服务(新增) + deployment: deploymentAPI, + + // 系统信息(使用共享模块) + system: systemAPI, + + // 实用工具(使用共享模块) + utils: utilsAPI, +}; + +// 暴露 API 到渲染进程 +if (process.contextIsolated) { + try { + contextBridge.exposeInMainWorld('eulercopilotWelcome', welcomeAPI); + } catch (error) { + console.error('Failed to expose welcome API:', error); + } +} else { + (window as any).eulercopilotWelcome = welcomeAPI; +} diff --git a/electron/welcome/assets/images/logo-euler-copilot.png b/electron/welcome/assets/images/logo-euler-copilot.png new file mode 100644 index 0000000000000000000000000000000000000000..c9051151e00ffdfec60fa643af8b5cda91cae4d2 Binary files /dev/null and b/electron/welcome/assets/images/logo-euler-copilot.png differ diff --git a/electron/welcome/assets/images/welcome_bg.webp b/electron/welcome/assets/images/welcome_bg.webp new file mode 100644 index 0000000000000000000000000000000000000000..97fe582342c404720ed14bc6c678e5d16ce8baaf Binary files /dev/null and b/electron/welcome/assets/images/welcome_bg.webp differ diff --git a/electron/welcome/assets/svgs/close.svg b/electron/welcome/assets/svgs/close.svg new file mode 100644 index 0000000000000000000000000000000000000000..72cfe62bed616d1ad497073041938d97e33b2bdd --- /dev/null +++ b/electron/welcome/assets/svgs/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron/welcome/assets/svgs/copy_icon.svg b/electron/welcome/assets/svgs/copy_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..6750ed5c6fa94e7032ccd08fae13792cf842c399 --- /dev/null +++ b/electron/welcome/assets/svgs/copy_icon.svg @@ -0,0 +1,16 @@ + + + Created with Pixso. + + + + + + + + + + + + + diff --git a/electron/welcome/assets/svgs/error.svg b/electron/welcome/assets/svgs/error.svg new file mode 100644 index 0000000000000000000000000000000000000000..eff026d9a371ecd397542f213880a651cb1559cf --- /dev/null +++ b/electron/welcome/assets/svgs/error.svg @@ -0,0 +1,8 @@ + + + Created with Pixso. + + + + + diff --git a/electron/welcome/assets/svgs/left_arrow.svg b/electron/welcome/assets/svgs/left_arrow.svg new file mode 100644 index 0000000000000000000000000000000000000000..d776032013b05da0c71aa2487fb50e957e292f38 --- /dev/null +++ b/electron/welcome/assets/svgs/left_arrow.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron/welcome/assets/svgs/local_deploy.svg b/electron/welcome/assets/svgs/local_deploy.svg new file mode 100644 index 0000000000000000000000000000000000000000..79f478da6a7d3a6beb00316f09969baab2bcc7ea --- /dev/null +++ b/electron/welcome/assets/svgs/local_deploy.svg @@ -0,0 +1,26 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/electron/welcome/assets/svgs/logo .svg b/electron/welcome/assets/svgs/logo .svg new file mode 100644 index 0000000000000000000000000000000000000000..b97dee1ef763c74ae59e4cf62541132f042da109 --- /dev/null +++ b/electron/welcome/assets/svgs/logo .svg @@ -0,0 +1,60 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/electron/welcome/assets/svgs/online_service.svg b/electron/welcome/assets/svgs/online_service.svg new file mode 100644 index 0000000000000000000000000000000000000000..0c815738869cc00cf2d037482242dc629cdec7ee --- /dev/null +++ b/electron/welcome/assets/svgs/online_service.svg @@ -0,0 +1,13 @@ + + + Created with Pixso. + + + + + + + + + + diff --git a/electron/welcome/assets/svgs/success.svg b/electron/welcome/assets/svgs/success.svg new file mode 100644 index 0000000000000000000000000000000000000000..676e309048dde792cb3988203f3a4e3bda88aa0b --- /dev/null +++ b/electron/welcome/assets/svgs/success.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron/welcome/assets/svgs/upload-loading.svg b/electron/welcome/assets/svgs/upload-loading.svg new file mode 100644 index 0000000000000000000000000000000000000000..328f23c83d377e6c387e1dbaf63967e34e6b1855 --- /dev/null +++ b/electron/welcome/assets/svgs/upload-loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/electron/welcome/deployServiceConnector.js b/electron/welcome/deployServiceConnector.js new file mode 100644 index 0000000000000000000000000000000000000000..31cf8f6fb34cb1ae6ae342a8915ee4dd1ff6b493 --- /dev/null +++ b/electron/welcome/deployServiceConnector.js @@ -0,0 +1,45 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +/** + * 本地部署服务连接器 + * 由于不能修改 localDeploy.vue,这个文件作为桥梁连接前端表单和部署服务 + * 注意:此文件已被 localDeploy.vue 中的直接调用取代,保留用于兼容性 + */ + +// 等待 DOM 加载完成 +document.addEventListener('DOMContentLoaded', function () { + // 检查是否在 Electron 环境中 + if ( + typeof window.eulercopilotWelcome === 'undefined' || + typeof window.eulercopilotWelcome.deployment === 'undefined' + ) { + return; + } +}); + +// 保留一些实用函数用于调试 +window.deploymentUtils = { + getFormData: function () { + return null; + }, + + getDeploymentStatus: async function () { + if (window.eulercopilotWelcome && window.eulercopilotWelcome.deployment) { + try { + return await window.eulercopilotWelcome.deployment.getStatus(); + } catch (error) { + console.error('获取部署状态失败:', error); + return null; + } + } + return null; + }, +}; diff --git a/electron/welcome/index.vue b/electron/welcome/index.vue new file mode 100644 index 0000000000000000000000000000000000000000..f22d588a966189c34387e7819b611e04bbf6fc77 --- /dev/null +++ b/electron/welcome/index.vue @@ -0,0 +1,210 @@ + + + + + diff --git a/electron/welcome/lang/en.ts b/electron/welcome/lang/en.ts new file mode 100644 index 0000000000000000000000000000000000000000..d79422a9f8b20e43ec56bb93291053f910356d2d --- /dev/null +++ b/electron/welcome/lang/en.ts @@ -0,0 +1,40 @@ +export default { + welcome: { + welcomeText: 'Welcome to use', + localDeploy: 'Back-end local deployment', + onlineService: 'Back-end online services', + back: 'Back', + confirm: 'Ok', + pleaseInput: 'Please Input', + validUrl: 'Please enter a valid URL', + validationFailure: 'Validation failure', + connectionFailed: 'Connection failed', + }, + localDeploy: { + model: 'Large model', + embeddingModel: 'Embedding Model', + url: 'URL', + modelName: 'Model Name', + apiKey: 'API Key', + copyTip: 'Reuse the same link for large models', + installation: 'Installation', + prepareEnv: 'Preparing installation environment', + dataBase: 'Database services', + authHub: 'AuthHub services', + intelligence: 'Intelligence services', + stopInstall: 'Stop installation', + complete: 'Complete', + retry: 'Retry', + validationFailed: 'Validation Failed', + authError: 'Authentication failed, please check if the API Key is correct', + functionCallNotSupported: + 'Function Call not supported, please use a model that supports this feature', + connectionError: 'Connection failed, please check if the URL is correct', + modelError: 'Model validation failed', + llmValidationFailed: 'LLM validation failed', + embeddingValidationFailed: 'Embedding model validation failed', + }, + onlineService: { + serviceUrl: 'Backend Service Links', + }, +}; diff --git a/electron/welcome/lang/index.ts b/electron/welcome/lang/index.ts new file mode 100644 index 0000000000000000000000000000000000000000..2c1acbd4894b86151d3a0ca5b4c36a025e6e7a90 --- /dev/null +++ b/electron/welcome/lang/index.ts @@ -0,0 +1,176 @@ +import { createI18n } from 'vue-i18n'; +// 语言包 +import zh from './zh'; +import en from './en'; + +/** + * 开发模式日志输出 + * 只在开发环境下输出日志 + */ +function devLog(...args: any[]) { + if (import.meta.env.DEV || process.env.NODE_ENV === 'development') { + console.log(...args); + } +} + +/** + * 开发模式警告输出 + * 只在开发环境下输出警告 + */ +function devWarn(...args: any[]) { + if (import.meta.env.DEV || process.env.NODE_ENV === 'development') { + console.warn(...args); + } +} + +/** + * 检测系统语言 + * 支持多种操作系统的语言检测 + * @returns {'zh' | 'en'} 语言代码 'zh' 或 'en' + */ +function detectSystemLanguage(): 'zh' | 'en' { + let systemLanguage: 'zh' | 'en' = 'zh'; // 默认中文 + + try { + // 优先级1: 检查 Electron 环境中的系统信息 + if (typeof window !== 'undefined' && window.eulercopilotWelcome?.system) { + const { platform, env } = window.eulercopilotWelcome.system; + + devLog(`检测到系统平台: ${platform}`); + + // 根据不同操作系统检测语言 + if (platform === 'win32') { + // Windows 系统语言检测 + const winLang = env.LANG || env.LC_ALL || env.LANGUAGE || ''; + if ( + winLang.toLowerCase().includes('zh') || + winLang.toLowerCase().includes('chinese') || + winLang.toLowerCase().includes('chs') || + winLang.toLowerCase().includes('cn') + ) { + systemLanguage = 'zh'; + } else if (winLang && !winLang.toLowerCase().includes('zh')) { + systemLanguage = 'en'; + } + } else if (platform === 'darwin') { + // macOS 系统语言检测 + const macLang = + env.LANG || env.LC_ALL || env.LC_MESSAGES || env.LANGUAGE || ''; + if ( + macLang.toLowerCase().includes('zh') || + macLang.toLowerCase().includes('chinese') || + macLang.toLowerCase().includes('cn') + ) { + systemLanguage = 'zh'; + } else if (macLang && !macLang.toLowerCase().includes('zh')) { + systemLanguage = 'en'; + } + } else if (platform === 'linux') { + // Linux 系统语言检测 + const linuxLang = + env.LANG || env.LC_ALL || env.LC_MESSAGES || env.LANGUAGE || ''; + if ( + linuxLang.toLowerCase().includes('zh') || + linuxLang.toLowerCase().includes('chinese') || + linuxLang.toLowerCase().includes('cn') + ) { + systemLanguage = 'zh'; + } else if (linuxLang && !linuxLang.toLowerCase().includes('zh')) { + systemLanguage = 'en'; + } + } + + devLog(`根据环境变量检测到语言: ${systemLanguage}`); + return systemLanguage; // 如果 Electron 环境可用,直接返回结果 + } + + // 优先级2: 浏览器语言检测 (作为后备方案) + if (typeof navigator !== 'undefined' && navigator.language) { + const browserLang = navigator.language.toLowerCase(); + + devLog(`浏览器语言: ${browserLang}`); + + // 检测是否为中文相关语言 + if ( + browserLang.startsWith('zh') || + browserLang.includes('china') || + browserLang.includes('chinese') || + browserLang === 'zh-cn' || + browserLang === 'zh-tw' || + browserLang === 'zh-hk' + ) { + systemLanguage = 'zh'; + } + // 检测是否为英文相关语言 + else if ( + browserLang.startsWith('en') || + browserLang.includes('english') || + browserLang.includes('us') || + browserLang.includes('gb') + ) { + systemLanguage = 'en'; + } + // 其他语言默认使用英文 + else { + systemLanguage = 'en'; + } + } + + devLog(`最终检测到的系统语言: ${systemLanguage}`); + } catch (error) { + devWarn('检测系统语言失败,使用默认中文:', error); + systemLanguage = 'zh'; + } + + return systemLanguage; +} + +// 检测系统语言,默认中文 +const locale = detectSystemLanguage(); + +const i18n_welcome = createI18n({ + legacy: false, // 设置为 false,启用 composition API 模式 + locale, + messages: { + zh, + en, + }, +}); + +/** + * 在 Electron 环境准备好后重新检测语言 + * 这个函数会在应用初始化后调用,确保能获取到正确的系统语言 + */ +export function redetectLanguageOnReady() { + // 等待一小段时间让 Electron preload 脚本完成初始化 + setTimeout(() => { + const newLocale = detectSystemLanguage(); + if (newLocale !== i18n_welcome.global.locale.value) { + devLog( + `重新检测到语言变化,从 ${i18n_welcome.global.locale.value} 切换到 ${newLocale}`, + ); + i18n_welcome.global.locale.value = newLocale; + } + }, 100); +} + +/** + * 切换语言 + * @param {string} newLocale 新的语言代码 'zh' 或 'en' + */ +export function changeLanguage(newLocale: 'zh' | 'en') { + if (i18n_welcome.global.locale) { + i18n_welcome.global.locale.value = newLocale; + devLog(`语言已切换到: ${newLocale}`); + } +} + +/** + * 获取当前语言 + * @returns {'zh' | 'en'} 当前语言代码 + */ +export function getCurrentLanguage(): 'zh' | 'en' { + return i18n_welcome.global.locale.value as 'zh' | 'en'; +} + +export default i18n_welcome; diff --git a/electron/welcome/lang/zh.ts b/electron/welcome/lang/zh.ts new file mode 100644 index 0000000000000000000000000000000000000000..6bf89e1af890edfd6c2e0a7295c9351b9b937144 --- /dev/null +++ b/electron/welcome/lang/zh.ts @@ -0,0 +1,39 @@ +export default { + welcome: { + welcomeText: '欢迎使用', + localDeploy: '后端本地部署', + onlineService: '后端在线服务', + back: '返回', + confirm: '确定', + pleaseInput: '请输入', + validUrl: '请输入有效的 URL', + validationFailure: '检验失败', + connectionFailed: '连接失败', + }, + localDeploy: { + model: '大模型', + embeddingModel: 'Embedding 模型', + url: 'URL', + modelName: '模型名称', + apiKey: 'API Key', + copyTip: '复用大模型相同链接', + installation: '安装中', + prepareEnv: '准备安装环境', + dataBase: '数据库服务', + authHub: 'AuthHub 服务', + intelligence: 'Intelligence 服务', + stopInstall: '停止安装', + complete: '完成', + retry: '重试', + validationFailed: '校验失败', + authError: '鉴权失败,请检查 API Key 是否正确', + functionCallNotSupported: '不支持 Function Call,请使用支持此功能的模型', + connectionError: '连接失败,请检查 URL 是否正确', + modelError: '模型验证失败', + llmValidationFailed: '大模型校验失败', + embeddingValidationFailed: 'Embedding 模型校验失败', + }, + onlineService: { + serviceUrl: '后端服务链接', + }, +}; diff --git a/electron/welcome/localDeploy.vue b/electron/welcome/localDeploy.vue new file mode 100644 index 0000000000000000000000000000000000000000..2a55e26edb0981c34cd1259e34ce379bbd3ff123 --- /dev/null +++ b/electron/welcome/localDeploy.vue @@ -0,0 +1,706 @@ + + + + diff --git a/electron/welcome/main.ts b/electron/welcome/main.ts new file mode 100644 index 0000000000000000000000000000000000000000..fada465af83715504cbec262dfb14c0ad7100f3f --- /dev/null +++ b/electron/welcome/main.ts @@ -0,0 +1,27 @@ +// Welcome 页面 Vue 应用入口 +import type { App } from 'vue'; +import { createApp } from 'vue'; +import WelcomeComponent from './index.vue'; +import ElementPlus from 'element-plus'; +import 'element-plus/dist/index.css'; +import i18n_welcome, { redetectLanguageOnReady } from './lang'; + +/** + * 开发模式日志输出 + * 只在开发环境下输出日志 + */ +function devLog(...args: any[]) { + if (import.meta.env.DEV || process.env.NODE_ENV === 'development') { + console.log(...args); + } +} + +// 创建 Vue 应用并挂载 +const app: App = createApp(WelcomeComponent); +app.use(ElementPlus).use(i18n_welcome); +app.mount('#app'); + +// 在应用挂载后重新检测语言 +redetectLanguageOnReady(); + +devLog('Welcome Vue app initialized'); diff --git a/electron/welcome/onlineService.vue b/electron/welcome/onlineService.vue new file mode 100644 index 0000000000000000000000000000000000000000..dbc838e84341746420973f70e685b5b8e8c51394 --- /dev/null +++ b/electron/welcome/onlineService.vue @@ -0,0 +1,207 @@ + + + + diff --git a/electron/welcome/timeLine.vue b/electron/welcome/timeLine.vue new file mode 100644 index 0000000000000000000000000000000000000000..8f8626fbb65dfc4f75850b612f3c44b5e19236e7 --- /dev/null +++ b/electron/welcome/timeLine.vue @@ -0,0 +1,500 @@ + + + + + diff --git a/electron/welcome/types.d.ts b/electron/welcome/types.d.ts new file mode 100644 index 0000000000000000000000000000000000000000..1ffe94c6ea46e486c418935f23b6c7a8742cf6d0 --- /dev/null +++ b/electron/welcome/types.d.ts @@ -0,0 +1,50 @@ +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. +// licensed under the Mulan PSL v2. +// You can use this software according to the terms and conditions of the Mulan PSL v2. +// You may obtain a copy of Mulan PSL v2 at: +// http://license.coscl.org.cn/MulanPSL2 +// THIS SOFTWARE IS PROVIDED ON AN 'AS IS' BASIS, WITHOUT WARRANTIES OF ANY KIND, EITHER EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR +// PURPOSE. +// See the Mulan PSL v2 for more details. + +// 为欢迎页面定义类型 + +interface Window { + eulercopilotWelcome?: { + config: { + setProxyUrl(url: string): Promise; + validateServer(url: string): Promise<{ + isValid: boolean; + error?: string; + status?: number; + responseTime?: number; + }>; + }; + welcome: { + show(): Promise; + complete(): Promise; + cancel(): Promise; + }; + deployment: { + startDeploymentFromForm(formData: any): Promise; + stopDeployment(): Promise; + getStatus(): Promise; + onStatusChange(callback: (status: any) => void): void; + removeStatusListener(): void; + cleanup(): Promise; + addHostsEntries(domains: string[]): Promise; + }; + system: { + platform: string; + arch: string; + versions: NodeJS.ProcessVersions; + env: Record; + }; + utils: { + isValidUrl(url: string): boolean; + formatUrl(url: string): string; + delay(ms: number): Promise; + }; + }; +} diff --git a/electron/welcome/welcome.html b/electron/welcome/welcome.html new file mode 100644 index 0000000000000000000000000000000000000000..e2423571b8cd677b9b62ea00d514c4bf951b580b --- /dev/null +++ b/electron/welcome/welcome.html @@ -0,0 +1,41 @@ + + + + + + 欢迎使用 openEuler Intelligence + + + + +
+ + + + + + + + diff --git a/env.d.ts b/env.d.ts index ad8d6d5a42450984ab946b880909b2956d9d1a92..43df7b98287cdcdfc7b3a48603a071a08312bde7 100644 --- a/env.d.ts +++ b/env.d.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -10,6 +10,15 @@ /// declare interface Window { onHtmlEventDispatch: any; + eulercopilot: any; + // 添加electronProcess属性定义 + electronProcess?: { + platform: 'win32' | 'darwin' | 'linux'; + versions: { + electron: string; + }; + env?: Record; + }; } declare interface ImportMetaEnv { diff --git a/eslint.config.js b/eslint.config.js index 26ecd885b0f9f1eb2fec2eb741b576c7dfa2858f..bf9b67a143b439dadc66d596534152881b8d50b2 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -21,4 +21,10 @@ export default [ files: ['**/*.vue'], languageOptions: { parserOptions: { parser: tseslint.parser } }, }, + { + rules: { + '@typescript-eslint/no-explicit-any': 'off', + 'vue/multi-word-component-names': 'off', + }, + }, ]; diff --git a/index.html b/index.html index ef5e0952a35bc3c4176f97e304e68c1de6514421..9514e77528f36106a4f09a98ae331326872c2a4c 100644 --- a/index.html +++ b/index.html @@ -4,7 +4,7 @@ - EulerCopilot-问答机器人 + openEuler Intelligence diff --git a/src/apis/appCenter/appCenterService.ts b/src/apis/appCenter/appCenterService.ts index 9ec8328b2c16aa7d3e272635b07cd7b3cd58e073..025e7899e39561faa46629f785fa4dfb8ede9d72 100644 --- a/src/apis/appCenter/appCenterService.ts +++ b/src/apis/appCenter/appCenterService.ts @@ -1,7 +1,12 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. import { post, get, del, put } from 'src/apis/server'; import type { FcResponse } from 'src/apis/server'; -import { QueryAppListParamsType, CreateOrUpdateAppParamsType } from './type'; +import { + QueryAppListParamsType, + CreateOrUpdateAppParamsType, + AppDetail, +} from './type'; + /** * 获取应用列表 * @param params @@ -9,7 +14,28 @@ import { QueryAppListParamsType, CreateOrUpdateAppParamsType } from './type'; */ export const queryAppList = ( params: QueryAppListParamsType, -): Promise<[any, FcResponse | undefined]> => { +): Promise< + [ + any, + ( + | FcResponse<{ + applications: { + appId: string; + author: string; + description: string; + favorited: boolean; + appType: 'flow' | 'agent'; + icon: string; + name: string; + published: boolean; + }[]; + currentPage: number; + totalApps: number; + }> + | undefined + ), + ] +> => { return get('/api/app', params); }; @@ -18,10 +44,8 @@ export const queryAppList = ( * @param params * @returns */ -export const createOrUpdateApp = ( - params: CreateOrUpdateAppParamsType, -): Promise<[any, FcResponse | undefined]> => { - return post('/api/app', params); +export const createOrUpdateApp = (params: CreateOrUpdateAppParamsType) => { + return post<{ appId: string }>('/api/app', params); }; /** @@ -29,10 +53,8 @@ export const createOrUpdateApp = ( * @param params * @returns */ -export const querySingleAppData = (params: { - id: string; -}): Promise<[any, FcResponse | undefined]> => { - return get(`/api/app/${params.id}`); +export const querySingleAppData = (params: { id: string }) => { + return get(`/api/app/${params.id}`); }; /** @@ -52,9 +74,9 @@ export const deleteSingleAppData = (params: { * @returns */ export const releaseSingleAppData = (params: { - id: string; + appId: string; }): Promise<[any, FcResponse | undefined]> => { - return post(`/api/app/${params.id}`, params); + return post(`/api/app/${params.appId}`); }; /** @@ -75,7 +97,13 @@ export const changeSingleAppCollect = (params: { * @returns */ export const getPartAppConfgUser = (): Promise< - [any, FcResponse | undefined] + [ + any, + ( + | FcResponse<{ userInfoList: { userName: string; userSub: string }[] }> + | undefined + ), + ] > => { return get('/api/user'); }; diff --git a/src/apis/appCenter/index.ts b/src/apis/appCenter/index.ts index 95d1ce017d353e0157bca4a5084afd72ec08da1a..1300da1b095c7a7d3df60e6a9a30f7ff9e7aee89 100644 --- a/src/apis/appCenter/index.ts +++ b/src/apis/appCenter/index.ts @@ -1,2 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. export * from './appCenterService'; +export * from './prompt'; +export * from './knowledge'; diff --git a/src/apis/appCenter/knowledge.ts b/src/apis/appCenter/knowledge.ts new file mode 100644 index 0000000000000000000000000000000000000000..ab59da53221cb04cb30cf26386f03da358cb55e7 --- /dev/null +++ b/src/apis/appCenter/knowledge.ts @@ -0,0 +1,29 @@ +import { get, post, del } from '../server'; + +export interface KnowledgeBase { + kbId: string; + name: string; + isUsed: boolean; + description: string; +} + +const KNOWLEDGE_URL = '/api/knowledge'; + +/** + * 查询Prompt列表 + * @param keyword + * @returns + */ +const getKnowledgeList = (keyword?: string) => { + return get<{ + team_kb_list: { + teamId: string; + teamName: string; + kb_list: KnowledgeBase[]; + }[]; + }>(KNOWLEDGE_URL, { kbName: keyword }); +}; + +export const kbApi = { + getKnowledgeList, +}; diff --git a/src/apis/appCenter/prompt.ts b/src/apis/appCenter/prompt.ts new file mode 100644 index 0000000000000000000000000000000000000000..8b115639ac056e71991ba7bf4076807d5d821232 --- /dev/null +++ b/src/apis/appCenter/prompt.ts @@ -0,0 +1,55 @@ +import { get, post, del } from '../server'; + +export interface Prompt { + promptId: string; + name: string; + description: string; + prompt?: string; +} + +const PROMPT_URL = '/api/prompt'; + +/** + * 查询Prompt列表 + * @param keyword + * @returns + */ +const getPrompts = (keyword?: string) => { + return get<{ + prompts: Prompt[]; + totalPrompts: number; + }>(PROMPT_URL, { keyword }); +}; + +/** + * 创建或更新Prompt + * @param params + * @returns + */ +const createOrUpdatePrompts = (params: { + name: string; + description: string; + prompt: string; + promptId?: string; +}) => { + return post<{ + promptId: string; + }>(PROMPT_URL, params); +}; + +/** + * 删除Prompt + * @param promptId + * @returns + */ +const deletePrompt = (promptId: string) => { + return del<{ + promptId: string; + }>(`${PROMPT_URL}/${promptId}`); +}; + +export const promptApi = { + getPrompts, + createOrUpdatePrompts, + deletePrompt, +}; diff --git a/src/apis/appCenter/type.ts b/src/apis/appCenter/type.ts index ae30e0e98fe59bce2e0b1a64189e4f8feadd0cdd..95d12c7b40f36ae7a6dfac9d2775056e33e934a1 100644 --- a/src/apis/appCenter/type.ts +++ b/src/apis/appCenter/type.ts @@ -31,9 +31,8 @@ export interface QueryAppListParamsType { */ export enum SearchType { All = 'all', - Author = 'author', - Description = 'description', - Name = 'name', + Agent = 'agent', + Flow = 'flow', } /** @@ -43,7 +42,11 @@ export interface CreateOrUpdateAppParamsType { /** * 应用ID */ - appId?: string; + appId?: string | null; + /** + * 应用类型 + */ + appType: 'flow' | 'agent'; /** * 应用简介 */ @@ -72,6 +75,10 @@ export interface CreateOrUpdateAppParamsType { * 推荐问题(列表,最多3项) */ recommendedQuestions?: string[]; + /** + * Mcpservice,MCP服务 + */ + mcpService?: string[]; /** * 工作流(列表,每个元素为工作流ID) */ @@ -79,6 +86,140 @@ export interface CreateOrUpdateAppParamsType { [property: string]: any; } +/** + * MCPServiceMetadata,MCPService的元数据 + */ +export interface MCPServiceMetadataInput { + /** + * Author,创建者的用户名 + */ + author: string; + /** + * MCP服务配置 + */ + config: MCPServiceConfig; + /** + * Description,元数据描述 + */ + description: string; + /** + * Hashes,资源(App、Service等)下所有文件的hash值 + */ + hashes?: { [key: string]: string } | null; + /** + * Icon,图标 + */ + icon?: string; + /** + * Id,元数据ID + */ + id: string; + /** + * Name,元数据名称 + */ + name: string; + /** + * Tools,MCP服务Tools列表 + */ + tools: MCPServiceToolsdataInput[]; + type?: MetadataType; + [property: string]: any; +} + +/** + * MCPServiceToolsdata,MCP Service中tool信息 + */ +export interface MCPServiceToolsdataInput { + /** + * Description,Tool功能描述 + */ + description: string; + /** + * Input Args,Tool参数列表 + */ + input_args: MCPServiceToolsArgs[]; + /** + * Name,Tool名称 + */ + name: string; + /** + * Output Args,Tool参数列表 + */ + output_args: MCPServiceToolsArgs[]; + [property: string]: any; +} + +/** + * MetadataType,元数据类型 + */ +export enum MetadataType { + Agent = 'agent', + Flow = 'flow', + McpService = 'mcp_service', + Model = 'model', + Prompt = 'prompt', + Service = 'service', +} + +/** + * MCPServiceToolsArgs,MCP Service中tool参数信息 + */ +export interface MCPServiceToolsArgs { + /** + * Description,Tool参数描述 + */ + description: string; + /** + * Name,Tool参数名称 + */ + name: string; + /** + * Tool参数类型 + */ + type: MCPServiceToolsArgsType; + [property: string]: any; +} + +/** + * 传输协议(Stdio/SSE/Streamable) + * + * MCPTransmitProto,MCP传输方式 + */ +export enum MCPTransmitProto { + SSE = 'sse', + Stdio = 'stdio', + Streamable = 'stream', +} + +/** + * MCP服务配置 + * + * MCPServiceConfig,MCPService的API配置 + */ +export interface MCPServiceConfig { + /** + * Config,对应MCP的配置 + */ + config: { [key: string]: any }; + /** + * 传输协议(Stdio/SSE/Streamable) + */ + transmitProto?: MCPTransmitProto; + [property: string]: any; +} + +/** + * Tool参数类型 + * + * MCPServiceToolsArgsType,MCPService tool参数数据类型 + */ +export enum MCPServiceToolsArgsType { + Boolean = 'boolean', + Double = 'double', + Integer = 'integer', + String = 'string', +} + /** * AppLink, 应用链接数据结构 */ @@ -109,7 +250,43 @@ export interface AppPermissionData { } export enum Visibility { - Private = 'private', - Protected = 'protected', - Public = 'public', + private = 'private', + protected = 'protected', + public = 'public', +} + +export interface AppDetail { + type: 'flow' | 'agent'; + icon?: string; + name: string; + description: string; + dialogRounds?: number; + permission?: { + visibility: keyof typeof Visibility; + authorizedUsers: string[]; + }; + links?: AppLink[]; + recommendedQuestions?: string[]; + workflows?: { + id: string; + name: string; + description: string; + debug: boolean; + }; + mcpService?: string[]; + model?: { + provider: string; + icon?: string; + url: string; + model: string; + apiKey: string; + maxTokens: number; + id: string; + author: string; + hashes?: any; + }; + prompt?: string; + knowledge?: string; + appId: string; + published: boolean; } diff --git a/src/apis/index.ts b/src/apis/index.ts index 859159858c830cb67fa914f44b013681c1771c3f..9791c928b8a2eb230a416d6b7a2b9f1efc048077 100644 --- a/src/apis/index.ts +++ b/src/apis/index.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -15,9 +15,12 @@ import { knowledgeApi, appApi, apiApi, + modelApi, + mcpApi, + llmApi, } from './paths'; import { workFlowApi } from './workFlow'; -import { appCenterApi } from './appCenter'; +import { appCenterApi, promptApi, kbApi } from './appCenter'; export const api = { ...accountApi, @@ -29,4 +32,9 @@ export const api = { ...workFlowApi, ...appApi, ...apiApi, + ...modelApi, + ...mcpApi, + ...llmApi, + ...promptApi, + ...kbApi, }; diff --git a/src/apis/paths/account.ts b/src/apis/paths/account.ts index 6ff6b4c230bfba8c2004ba9390adbb4e4583a534..93411cdfd4d012974ef357b27183497015f9d8b4 100644 --- a/src/apis/paths/account.ts +++ b/src/apis/paths/account.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -23,6 +23,7 @@ export const authorizeUser = (): Promise< username: string; organization: string; revision_number: string | null; + is_admin: boolean; }> | undefined ), diff --git a/src/apis/paths/api.ts b/src/apis/paths/api.ts index 8538d5dcf1975fed1ddb6797bdae9121c46bc0da..95dd5fae5c99954fe28296b65d4b0706f9325b7c 100644 --- a/src/apis/paths/api.ts +++ b/src/apis/paths/api.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. import { post, get, del, put } from 'src/apis/server'; import type { FcResponse } from 'src/apis/server'; import { QueryApiListParamsType, CreateOrUpdateApiParamsType } from './type'; @@ -10,7 +10,26 @@ import { QueryApiListParamsType, CreateOrUpdateApiParamsType } from './type'; // 导出一个函数queryApiList,用于查询API列表 export const queryApiList = ( params: QueryApiListParamsType, -): Promise<[any, FcResponse | undefined]> => { +): Promise< + [ + any, + ( + | FcResponse<{ + totalCount: number; + currentPage: number; + services: { + serviceId: string; + description: string; + favorited: boolean; + icon: string; + author: string; + name: string; + }[]; + }> + | undefined + ), + ] +> => { // 调用get函数,传入/api/service路径和params参数,返回一个Promise对象 return get('/api/service', params); }; @@ -34,7 +53,9 @@ export const createOrUpdateApi = ( export const querySingleApiData = (params: { serviceId: string; edit?: boolean; -}): Promise<[any, FcResponse | undefined]> => { +}): Promise< + [any, FcResponse<{ data: any; name: string; apis: string }> | undefined] +> => { return get(`/api/service/${params.serviceId}`, { edit: params.edit }); }; diff --git a/src/apis/paths/apikey.ts b/src/apis/paths/apikey.ts index 3848437900e7ba0429072e64cb88e7495d548d82..0e7498ccfd113f45c52df839702b308c54ab3516 100644 --- a/src/apis/paths/apikey.ts +++ b/src/apis/paths/apikey.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -14,9 +14,17 @@ import type { FcResponse } from 'src/apis/server'; * 验证用户信息 * @returns */ -export const getApiKey = (): Promise<[any, FcResponse<{ - api_key: string; -}> | undefined]> => { +export const getApiKey = (): Promise< + [ + any, + ( + | FcResponse<{ + api_key_exists: string; + }> + | undefined + ), + ] +> => { return get('/api/auth/key'); }; @@ -24,11 +32,10 @@ export const getApiKey = (): Promise<[any, FcResponse<{ * USER登录 * @returns */ -export const changeApiKey = (params: { - action: string; - query?: string; -}): Promise<[any, FcResponse | undefined]> => { - return post('/api/auth/key', params, params); +export const changeApiKey = (params: { action: string; query?: string }) => { + return post<{ + api_key: string; + }>('/api/auth/key', params, params); }; export const apiKeyApi = { diff --git a/src/apis/paths/app.ts b/src/apis/paths/app.ts index 3a87663be2ee3a4f15d8da921df5acb2f1bd7ada..51995ec6dd6e4ff4837cea60d22b13cb397fce6a 100644 --- a/src/apis/paths/app.ts +++ b/src/apis/paths/app.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: diff --git a/src/apis/paths/conversation.ts b/src/apis/paths/conversation.ts index 08a052f29ad1e973398990f887a95322e3828aac..8c3057bff8a692ffa90b3a1044a9ebfc1b7d7dd0 100644 --- a/src/apis/paths/conversation.ts +++ b/src/apis/paths/conversation.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -18,13 +18,7 @@ const BASE_URL = '/api/conversation'; * @returns */ export const stopGeneration = (): Promise< - [ - any, - ( - | FcResponse - | undefined - ), - ] + [any, FcResponse | undefined] > => { return post(`/api/stop`); }; @@ -43,7 +37,17 @@ export const getSessionRecord = (): Promise< * 创建一个会话 * @returns */ -export const createSession = (): Promise< +export const createSession = ({ + appId, + debug = false, + llm_id = '', + kb_ids = [], +}: { + appId: string; + debug?: boolean; + llm_id?: string; + kb_ids?: string[]; +}): Promise< [ any, ( @@ -54,7 +58,7 @@ export const createSession = (): Promise< ), ] > => { - return post(BASE_URL); + return post(BASE_URL, { appId, debug }, { llm_id, kb_ids }); }; /** @@ -76,7 +80,9 @@ export const createSessionDebug = (params: any): Promise<[any, any]> => { */ export const updateSession = (params: { conversationId: string; - title: string; + modelId?: string; + kbIds?: string[]; + title?: string; }): Promise< [ any, @@ -94,6 +100,8 @@ export const updateSession = (params: { BASE_URL, { title: params.title, + modelId: params.modelId, + kbIds: params.kbIds, }, { conversationId: params.conversationId, @@ -128,26 +136,44 @@ export const getHistoryConversation = ( }; /** - * 评论对话 + * 点踩 * @param params * @returns */ export const commentConversation = (params: { + type: string; qaRecordId: string; - isLike: number; + comment: string; dislikeReason?: string; reasonLink?: string; reasonDescription?: string; + groupId: string | undefined; }): Promise<[any, FcResponse> | undefined]> => { - const { qaRecordId, isLike, dislikeReason, reasonLink, reasonDescription } = - params; - return post(`/api/comment`, { - record_id: qaRecordId, - is_like: isLike, - dislike_reason: dislikeReason, - reason_link: reasonLink, - reason_description: reasonDescription, - }); + const { + qaRecordId, + comment, + dislikeReason, + reasonLink, + reasonDescription, + groupId, + type, + } = params; + if (type === 'disliked') { + return post(`/api/comment`, { + record_id: qaRecordId, + comment: comment, + group_id: groupId, + dislike_reason: dislikeReason, + reason_link: reasonLink, + reason_description: reasonDescription, + }); + } else { + return post(`/api/comment`, { + record_id: qaRecordId, + group_id: groupId, + comment: comment, + }); + } }; export const getRecognitionMode = (): Promise< diff --git a/src/apis/paths/external.ts b/src/apis/paths/external.ts index d46588efafc0d15f9a2aa62dd08fae334c3e30be..7bd1730a4333ee1235662d0e0cc379a58629158b 100644 --- a/src/apis/paths/external.ts +++ b/src/apis/paths/external.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: diff --git a/src/apis/paths/index.ts b/src/apis/paths/index.ts index 17d6d4b602720cb8515517f3cf0fd2e1ef654a94..ddf41fa545611e17388959db525a4f5b42c43619 100644 --- a/src/apis/paths/index.ts +++ b/src/apis/paths/index.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -14,3 +14,6 @@ export * from './apikey'; export * from './knowledge'; export * from './app'; export * from './api'; +export * from './model'; +export * from './mcp'; +export * from './llm'; diff --git a/src/apis/paths/knowledge.ts b/src/apis/paths/knowledge.ts index 8016773ede2a22bcf8dadc39e4fcfb7978d8633d..14d93209821af4af590bfc3472212f0a0fde0ae7 100644 --- a/src/apis/paths/knowledge.ts +++ b/src/apis/paths/knowledge.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -7,19 +7,32 @@ // IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT, MERCHANTABILITY OR FIT FOR A PARTICULAR // PURPOSE. // See the Mulan PSL v2 for more details. -import { get, post } from 'src/apis/server'; +import { get, post, put } from 'src/apis/server'; import type { FcResponse } from 'src/apis/server'; /** - * USER登录 + * updateKnowledgeList * @returns */ -export const updateKnowledgeList = (params: { - kb_id: string; +export const updateKnowledgeList = ({ + kb_ids, + conversationId, +}: { + kb_ids: string[]; + conversationId: string; }): Promise<[any, FcResponse<{}> | undefined]> => { - return post('/api/knowledge', params); + let kbIds = kb_ids; + return put('/api/knowledge', { kbIds }, { conversationId }); +}; + +export const getConvKnowledgeList = (params: { + conversationId: string; + kbName?: string; +}): Promise<[any, FcResponse<{}> | undefined]> => { + return get('/api/knowledge', params); }; export const knowledgeApi = { updateKnowledgeList, + getConvKnowledgeList, }; diff --git a/src/apis/paths/llm.ts b/src/apis/paths/llm.ts new file mode 100644 index 0000000000000000000000000000000000000000..aa20aa4ecd3310b91a262f346d833f5af5e06acd --- /dev/null +++ b/src/apis/paths/llm.ts @@ -0,0 +1,39 @@ +import { get, post, del, put } from '../server'; +import { AddedModalList } from './type'; +/** + * 获取用户的模型列表 + * @returns + */ +const getLLMList = () => { + return get< + { + id: string; + icon: string; + openaiBaseUrl: string; + openaiApiKey: string; + modelName: string; + maxTokens: number; + }[] + >('/api/llm'); +}; + +/** + * 获取用户的模型列表 + * @returns + */ +const updateLLMList = ({ conversationId, llmId }) => { + return put('/api/llm/conv', {}, { conversationId, llmId: llmId.llmId }); +}; + +/** + * 获取已添加模型列表 + */ +const getAddedModels = () => { + return get('/api/llm'); +}; + +export const llmApi = { + getAddedModels, + getLLMList, + updateLLMList, +}; diff --git a/src/apis/paths/mcp.ts b/src/apis/paths/mcp.ts new file mode 100644 index 0000000000000000000000000000000000000000..7d7297350f49bb3e37fe7411380a57fd7a8f1011 --- /dev/null +++ b/src/apis/paths/mcp.ts @@ -0,0 +1,92 @@ +import { get, post, del } from '../server'; + +const MCP_BASE_URL = '/api/mcp'; +/** + * 获取mcp服务列表 + * @returns + */ +const getMcpList = (params: { + searchType?: string; + keyword?: string; + page?: number; + pageSize?: number; +}) => { + return get<{ + currentPage: number; + totalCount: 0; + services: [ + { + mcpserviceId: string; + name: string; + description: string; + icon: string; + author: string; + isActive: boolean; + status: 'installing' | 'ready' | 'failed'; + }, + ]; + totalModels: number; + }>(MCP_BASE_URL, params); +}; + +const getMcpServiceDetail = (id: string, edit?: boolean) => { + return get<{ + serviceId: string; + icon: string; + name: string; + overview: string; + description: string; + data: string; + mcpType: 'stdio' | 'sse' | 'stream'; + tools: { + id: string; + name: string; + description: string; + mcp_id: string; + input_schema: { + properties: { + [key: string]: { + description: string; + type: string; + }; + }; + }; + output_schema: { + properties: { + [key: string]: { + description: string; + type: string; + }; + }; + }; + }[]; + }>(`${MCP_BASE_URL}/${id}`, { edit }); +}; + +const createOrUpdateMcpService = (params: { + serviceId?: string; + icon: string; + name: string; + overview: string; + description: string; + config: string; + mcpType: 'stdio' | 'sse' | 'stream'; +}) => { + return post(`${MCP_BASE_URL}`, params); +}; + +const deleteMcpService = (id: string) => { + return del<{ serviceId: string }>(`${MCP_BASE_URL}/${id}`); +}; + +const activeMcpService = (id: string, active: boolean) => { + return post<{ serviceId: string }>(`${MCP_BASE_URL}/${id}`, { active }); +}; + +export const mcpApi = { + getMcpList, + getMcpServiceDetail, + createOrUpdateMcpService, + deleteMcpService, + activeMcpService, +}; diff --git a/src/apis/paths/model.ts b/src/apis/paths/model.ts new file mode 100644 index 0000000000000000000000000000000000000000..b30e7331724183d9b76f82a057dbe42f5e743b1f --- /dev/null +++ b/src/apis/paths/model.ts @@ -0,0 +1,115 @@ +import { get, post, del, put } from '../server'; +import { AddedModalList } from './type'; +enum Provider { + OLLAMA = 'Ollama', + VLLM = 'VLLM', + QWEN = 'Tongyi-Qianwen', + XUNFEI = 'XunFei Spark', + BAICHUAN = 'BaiChuan', + BAIDU = 'BaiduYiyan', + MODELSCOPE = 'ModelScope', +} + +/** + * 获取用户的模型列表 + * @returns + */ +const getUserModelList = () => { + return get< + { + llmId: string; + icon: string; + openaiBaseUrl: string; + openaiApiKey: string; + modelName: string; + maxTokens: number; + isEditable?: boolean; + }[] + >('/api/llm'); +}; + +const getModelById = (modelId: string) => { + return get<{ + modelId: string; + icon: string; + model: string; + url: string; + provider: string; + maxTokens: number; + apiKey: string; + }>(`/api/model/${modelId}`); +}; + +/** + * 获取模型提供商列表 + * @returns + */ +const getModelProviderList = () => { + return get< + { + provider: string; + icon: string; + url: string; + description: string; + }[] + >('/api/llm/provider'); +}; + +const getAllModels = (searchKey: string) => { + return get<{ + models: { modelId: string; modelName: string }[]; + }>('/api/model/model', { + keyword: searchKey, + }); +}; + +/** + * 获取已添加模型列表 + */ +const getAddedModels = () => { + return get('/api/llm'); +}; + +/** + * 添加模型 + * @param params + * @returns + */ +const createOrUpdateModel = (params: { + llmId?: string; + icon: string; + openaiBaseUrl?: string; + openaiApiKey: string; + modelName: string; + maxTokens: number; +}) => { + return put('/api/llm', params, { llmId: params.llmId }); +}; + +/** + * 删除模型 + * @param modelId + * @returns + */ +const deleteModel = (modelId: string) => { + return del(`/api/llm`, undefined, { llmId: modelId }); +}; + +const updateModelAndKnowLedgeList = (params: { + conversationId: string; + modelId: string; + kbIds: string[]; +}) => { + return post('/api/conversation', params); +}; + +export const modelApi = { + updateModelAndKnowLedgeList, + getAddedModels, + getUserModelList, + getModelProviderList, + createOrUpdateModel, + getAllModels, + deleteModel, + getModelById, +}; diff --git a/src/apis/paths/type.ts b/src/apis/paths/type.ts index 54e012e930d4216e6f6e7aad6a1dc37480badfda..d610be29dfbe9ff96ffe175f54a8727bb5f05bbc 100644 --- a/src/apis/paths/type.ts +++ b/src/apis/paths/type.ts @@ -56,7 +56,7 @@ export interface ConversationRecord { flow: Flow; content: Content; metadata: Metadata; - is_like?: boolean; + comment: string; created_at: string; } @@ -71,10 +71,10 @@ export interface ConversationRecordList { * "flow_description": "查询机器192.168.10.1的CVE信息", //推荐项关联的工作流描述,若不关联则为空 * "question": "查询机器192.168.10.1的CVE信息", //推荐问题的内容 */ -export interface Suggest { - appId: string; +export interface Suggestion { + flowName: string; flowId: string; - flow_description: string; + flowDescription: string; question: string; } @@ -86,6 +86,10 @@ export interface ConversationListItem { createdTime: string; docCount: number; title: string; + llm: { + icon: string; + modelName: string; + }; } /* @@ -203,3 +207,31 @@ export interface serviceApiData { */ [property: string]: any; } +/** + * addedModalList, 获取可选modal列表 + */ +export interface AddedModalList { + llmId: string; + icon?: string; + openaiBaseUrl?: string; + openaiApiKey: string; + modelName: string; + maxTokens: string; +} +/** + * teamKnowledgeList, 获取teamKnowledgeList列表 + */ +export interface teamKnowledgeList { + teamId: string; + teamName: string; + knowledgeList: KnowledgeList[]; +} +/** + * KnowledgeList, 获取knowledgeList列表 + */ +export interface KnowledgeList { + kbId: string; + kbName: string; + description: string; + isUsed: boolean; +} diff --git a/src/apis/server.ts b/src/apis/server.ts index e185043e8c7cbb93390bb2b920c89cf6661ebd1d..71dbdb6e4107d81c9522b29832d43d379ed959f2 100644 --- a/src/apis/server.ts +++ b/src/apis/server.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -17,6 +17,8 @@ import type { AxiosHeaders, } from 'axios'; import { ElMessage } from 'element-plus'; +import { successMsg } from 'src/components/Message'; +import { getBaseProxyUrl } from 'src/utils/tools'; export interface FcResponse { error: string; @@ -34,8 +36,16 @@ export interface IAnyObj { export type Fn = (data: FcResponse) => unknown; +const baseURL: string = './'; +if (import.meta.env.MODE === 'electron-production') { + getBaseProxyUrl().then((url) => { + server.defaults.baseURL = url; + }); +} + // 创建 axios 实例 export const server = axios.create({ + baseURL, // API 请求的默认前缀 timeout: 60 * 1000, // 请求超时时间 }); @@ -70,7 +80,7 @@ server.interceptors.response.use( return Promise.resolve(response); }, async (error: AxiosError) => { - if (error.status !== 401 && error.status !== 403) { + if (error.status !== 401 && error.status !== 403 && error.status !== 409) { ElMessage({ showClose: true, message: @@ -80,6 +90,11 @@ server.interceptors.response.use( duration: 3000, }); } + if (error.status === 409) { + // 处理错误码为409的情况 + successMsg('已是最新对话'); + return Promise.reject(error as any); + } return await handleStatusError(error); }, ); diff --git a/src/apis/tools.ts b/src/apis/tools.ts index ad08b016a804ea3cbf9beacb3dd8ee7a403cca9e..2a2c9c5fb895b38d06083ca1aa4d655b26497c24 100644 --- a/src/apis/tools.ts +++ b/src/apis/tools.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. // licensed under the Mulan PSL v2. // You can use this software according to the terms and conditions of the Mulan PSL v2. // You may obtain a copy of Mulan PSL v2 at: @@ -8,12 +8,8 @@ // PURPOSE. // See the Mulan PSL v2 for more details. import { ElNotification, ElMessageBox } from 'element-plus'; -import { - CALLBACK_URL, - LOGOUT_CALLBACK_URL, -} from 'src/views/dialogue/constants'; +import { LOGOUT_CALLBACK_URL } from 'src/views/dialogue/constants'; import { useAccountStore } from 'src/store'; -import { qiankunWindow } from 'vite-plugin-qiankun/dist/helper'; import type { AxiosError, @@ -22,14 +18,11 @@ import type { } from 'axios'; import { storeToRefs } from 'pinia'; import i18n from 'src/i18n'; -import { errorMsg } from 'src/components/Message'; -function getCookie(name: string) { +export function getCookie(name: string) { const matches = document.cookie.match( new RegExp( - '(?:^|; )' + - name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, '\\$1') + - '=([^;]*)', + '(?:^|; )' + name.replace(/([.$?*|{}()[]\\\/\+^])/g, '\\$1') + '=([^;]*)', ), ); return matches ? decodeURIComponent(matches[1]) : undefined; @@ -46,45 +39,76 @@ export const handleChangeRequestHeader = ( if (cookieValue) { config.headers['X-CSRF-Token'] = cookieValue; } + // TODO 请求携带token 字段待定 + const token = localStorage.getItem('ECSESSION'); + if (token) { + config.headers.Authorization = `Bearer ${token}`; + } return config; }; +async function toAuthorization() { + const store = useAccountStore(); + const url = await store.getAuthUrl('login'); + if (!url) return; + const w = 1000; + const h = 750; + const left = (screen.width - w) / 2; + const top = (screen.height - h) / 2; + + const authWindow = window.open( + url, + 'loginWindow', + `width=${w},height=${h},resizable=yes,scrollbars=yes,top=${top},left=${left}`, + ); + + const postMessageListener = async (event: MessageEvent) => { + // 期望 event.data = { type: 'auth_success', sessionId: 'xxxx' } + const { sessionId, type } = event.data || {}; + if (type === 'auth_success' && sessionId) { + window.removeEventListener('message', postMessageListener); + localStorage.setItem('ECSESSION', sessionId); + authWindow?.close(); + window.location.reload(); + } + }; + + if (authWindow) { + const loop = setInterval(() => { + if (authWindow && authWindow.closed) { + clearInterval(loop); + window.location.reload(); + } + }, 500); + } + + window.addEventListener('message', postMessageListener); +} + +let isAuthorizing = false; export const handleAuthorize = async (errStatus: number): Promise => { const type = import.meta.env.VITE_USER_TYPE; const store = useAccountStore(); const { userinfo } = storeToRefs(store); userinfo.value.organization = type; if (errStatus === 401 || errStatus === 403) { - if (qiankunWindow.__POWERED_BY_QIANKUN__) { - const url = await store.getAuthUrl('login'); - if (url) { - const redirectUrl = qiankunWindow.__POWERED_BY_QIANKUN__ - ? `${url}&redirect_index=${location.href}` - : url; - if (redirectUrl) window.location.href = redirectUrl; - } - } else { - ElMessageBox.confirm( - i18n.global.t('Login.unauthorized'), - i18n.global.t('history.confirmation_message1'), - { - confirmButtonText: i18n.global.t('Login.login'), - showClose: false, - showCancelButton: false, - autofocus: false, - closeOnClickModal: false, - closeOnPressEscape: false, - }, - ).then(async () => { - const url = await store.getAuthUrl('login'); - if (url) { - const redirectUrl = qiankunWindow.__POWERED_BY_QIANKUN__ - ? `${url}&redirect_index=${location.href}` - : url; - if (redirectUrl) window.location.href = redirectUrl; - } - }); - } + if (isAuthorizing) return; + isAuthorizing = true; + + ElMessageBox.confirm( + i18n.global.t('Login.unauthorized'), + i18n.global.t('history.confirmation_message1'), + { + confirmButtonText: i18n.global.t('Login.login'), + showClose: false, + showCancelButton: false, + autofocus: false, + closeOnClickModal: false, + closeOnPressEscape: false, + }, + ).then(async () => { + toAuthorization(); + }); } if (errStatus === 460) { window.open(LOGOUT_CALLBACK_URL, '_self'); @@ -181,22 +205,6 @@ export const handleStatusError = async ( handleAuthorize(status); return; } - const originalRequest = error.config; - if (originalRequest && originalRequest.url === '/api/auth/refresh_token') { - // 长token过期,需要重新登录 - handleAuthorize(status); - return Promise.reject(error.response); - } - if (originalRequest && originalRequest.url === '/api/auth/user') { - handleAuthorize(status); - return; - } - //引入新的cookie后,会根据用户的请求重置token有效期,先删除重发逻辑,后期修改 - // const suc = await refreshToken(originalRequest); - // if(!suc){ - // return Promise.reject(error.response); - // } - // return server(originalRequest); } return Promise.reject(error.response); }; diff --git a/src/apis/workFlow/index.ts b/src/apis/workFlow/index.ts index fba2fe856dd1d995c5960174fd10a8865ea7a46b..516f17bd2760432fc4a2a39d72bf14a3802d0402 100644 --- a/src/apis/workFlow/index.ts +++ b/src/apis/workFlow/index.ts @@ -1,2 +1,2 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. export * from './workFlowService'; diff --git a/src/apis/workFlow/workFlowService.ts b/src/apis/workFlow/workFlowService.ts index d6b6a39fdcd5aa989d926834a050b611c2ea6a47..3f094d9ab2ad15d94c5f30d2fe2e196d24abc8aa 100644 --- a/src/apis/workFlow/workFlowService.ts +++ b/src/apis/workFlow/workFlowService.ts @@ -1,4 +1,4 @@ -// Copyright (c) Huawei Technologies Co., Ltd. 2023-2024. All rights reserved. +// Copyright (c) Huawei Technologies Co., Ltd. 2023-2025. All rights reserved. import { post, get, del, put } from 'src/apis/server'; import type { FcResponse } from 'src/apis/server'; import { diff --git a/src/assets/base.css b/src/assets/base.css index 18e8cf0ccedaaf60f7653be727f45287668a196e..4f1519d3a0071de1c2d777874845dd0aeb3f72ed 100644 --- a/src/assets/base.css +++ b/src/assets/base.css @@ -1,6 +1,6 @@ @font-face { font-family: 'HarmonyOS_Sans_SC_Regular'; - src: url('./fonts/HarmonyOS_Sans_SC_Regular.ttf'); + src: url('./fonts/HarmonyOS_Sans_SC_Regular.woff2'); font-weight: normal; font-style: normal; } @@ -33,8 +33,8 @@ html { } body { - min-width: 1366px; - min-height: 100vh; + width: 100vw; + height: 100vh; transition: color 0.5s, background-color 0.5s; @@ -55,3 +55,15 @@ body { text-rendering: optimizeLegibility; background-color: #f5f5f6; } +.successBg { + background-color: rgba(36, 171, 54, 0.2); + color: #24ab36; +} +.errorBg { + background-color: #fbdede; + color: #e32020; +} +.debugSuccess { + background-color: var(--o-color-success) !important; + color: var(--o-bg-color-base) !important; +} diff --git a/src/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf b/src/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf deleted file mode 100644 index 6d8eab7f749328780ec0843ac557cfbf36e5e1e6..0000000000000000000000000000000000000000 Binary files a/src/assets/fonts/HarmonyOS_Sans_SC_Medium.ttf and /dev/null differ diff --git a/src/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf b/src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2 old mode 100755 new mode 100644 similarity index 30% rename from src/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf rename to src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2 index aff150a137831c29d4d3cba994ac1b8e3a9940fa..7316e76af047fa9bdd6920c341b343a6e705bb40 Binary files a/src/assets/fonts/HarmonyOS_Sans_SC_Regular.ttf and b/src/assets/fonts/HarmonyOS_Sans_SC_Regular.woff2 differ diff --git a/src/assets/images/logo-euler-copilot.png b/src/assets/images/logo-euler-copilot.png index ce0b766638e21c6eb4fb3d909adc6f2aaf3cac6f..c9051151e00ffdfec60fa643af8b5cda91cae4d2 100644 Binary files a/src/assets/images/logo-euler-copilot.png and b/src/assets/images/logo-euler-copilot.png differ diff --git a/src/assets/images/welcome_bg.webp b/src/assets/images/welcome_bg.webp new file mode 100644 index 0000000000000000000000000000000000000000..2ead216667ac020839249d296d7986854e4d19bd Binary files /dev/null and b/src/assets/images/welcome_bg.webp differ diff --git a/src/assets/styles/codePreview.scss b/src/assets/styles/codePreview.scss index 609ff33a7e8162700264ae895ca436c5674836dd..c7fbe2dbc9b38b814e3b92b53602960892d8638d 100644 --- a/src/assets/styles/codePreview.scss +++ b/src/assets/styles/codePreview.scss @@ -1,8 +1,66 @@ #markdown-preview { + line-height: 24px; .hljs { color: var(--o-text-color-secondary); + font-size: 14px; + font-family: SFMono-Regular, monospace, 'Courier New', Courier, Consolas, + 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'DejaVu Sans Mono', + 'Bitstream Vera Sans Mono', 'Liberation Mono', Monaco, 'Source Code Pro', + 'Fira Mono', 'IBM Plex Mono'; } - //markdown text size + + /* 为highlight.js所有语法元素添加等宽字体 */ + .hljs-keyword, + .hljs-selector-tag, + .hljs-title, + .hljs-section, + .hljs-doctag, + .hljs-name, + .hljs-strong, + .hljs-comment, + .hljs-string, + .hljs-built_in, + .hljs-literal, + .hljs-type, + .hljs-addition, + .hljs-tag, + .hljs-quote, + .hljs-selector-id, + .hljs-selector-class, + .hljs-meta, + .hljs-subst, + .hljs-symbol, + .hljs-variable, + .hljs-template-variable, + .hljs-link, + .hljs-deletion, + .hljs-bullet, + .hljs-regexp, + .hljs-number, + .hljs-emphasis, + .hljs-attr, + .hljs-attribute, + .hljs-function { + font-size: 14px; + font-family: SFMono-Regular, monospace, 'Courier New', Courier, Consolas, + 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'DejaVu Sans Mono', + 'Bitstream Vera Sans Mono', 'Liberation Mono', Monaco, 'Source Code Pro', + 'Fira Mono', 'IBM Plex Mono'; + } + + /* 支持行内代码渲染 */ + .inline-code { + background-color: var(--o-bash-bg); + color: var(--o-text-color-secondary); + padding: 0.2em 0.4em; + border-radius: 6px; + font-family: SFMono-Regular, monospace, 'Courier New', Courier, Consolas, + 'Andale Mono WT', 'Andale Mono', 'Lucida Console', 'DejaVu Sans Mono', + 'Bitstream Vera Sans Mono', 'Liberation Mono', Monaco, 'Source Code Pro', + 'Fira Mono', 'IBM Plex Mono'; + font-size: 0.9em; + } + p { font-size: 16px; font-weight: 400; @@ -80,6 +138,7 @@ white-space: pre-wrap; /* 保持缩进同时允许换行 */ margin: 8px 0px; .code-toolbar { + user-select: none; background-color: var(--o-bash-bg); color: var(--o-text-color-primarys); display: flex; diff --git a/src/assets/styles/element/index.scss b/src/assets/styles/element/index.scss index 02d71a9c2be4d014865878912f88f2fdebaf9018..20a37cb467d87b2bd81fcccaa4cc9c9e5d19aea0 100644 --- a/src/assets/styles/element/index.scss +++ b/src/assets/styles/element/index.scss @@ -1,5 +1,16 @@ :root { --el-color-primary: #0077ff; + .el-button { + --o-button-color_label: #6395fd; + --o-button-color_label_hover: #7aa5ff; + } + .el-message-box__container { + display: flex; + align-items: center; + .el-message-box__message { + padding: 0; + } + } } .euler-copilot-message-box { @@ -17,25 +28,21 @@ } .el-button:not(.is-disabled) { - // background-color: white !important; color: var(--o-button-color) !important; border-color: var(--o-button-border-color) !important; } .el-button:not(.is-disabled):focus { - // background-color: white !important; color: var(--o-button-color) !important; border-color: var(--o-button-border-color) !important; } .el-button:not(.is-disabled):active { - // background-color: white !important; color: #6395fd !important; border-color: #6395fd !important; } .el-button:not(.is-disabled):hover { - // background-color: white !important; color: #7aa5ff !important; border-color: #7aa5ff !important; } diff --git a/src/assets/styles/main.scss b/src/assets/styles/main.scss index 30b5ed186e0b3915fc3ea5fc1fba8f2015905b1a..ac1620b6b74e490c5c5c9d4fe08aa836c7ef63ae 100644 --- a/src/assets/styles/main.scss +++ b/src/assets/styles/main.scss @@ -2,7 +2,92 @@ @import './message.scss'; @import './theme.scss'; -// 全局重写tooltip样式 -.el-popper { - max-width: 376px !important; -} \ No newline at end of file +// Linux平台窗口圆角和阴影样式 +html.platform-linux { + width: 100vw; + height: 100vh; + padding: 16px; + // 添加过渡动画 + transition: padding 0.25s cubic-bezier(0.4, 0, 0.2, 1); + + body { + width: calc(100vw - 32px) !important; + height: calc(100vh - 32px) !important; + background: transparent; + border-radius: 12px; + overflow: hidden; + // 添加16px的窗口阴影,向外扩展 + box-shadow: 0 0 16px rgba(0, 0, 0, 0.3); + // 添加过渡动画 + transition: + border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } + + #app { + border-radius: 12px; + overflow: hidden; + // 添加过渡动画 + transition: border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } + + .el-overlay { + height: calc(100% - 32px) !important; + width: calc(100% - 32px) !important; + top: 16px !important; + left: 16px !important; + border-radius: 12px; + } + + // 窗口最大化状态下取消圆角和阴影 + &.window-maximized { + padding: 0 !important; + // 保持过渡动画一致性 + transition: padding 0.25s cubic-bezier(0.4, 0, 0.2, 1); + body, + #app { + width: 100vw !important; + height: 100vh !important; + border-radius: 0 !important; + // 最大化时移除阴影 + box-shadow: none !important; + // 保持过渡动画一致性 + transition: + border-radius 0.25s cubic-bezier(0.4, 0, 0.2, 1), + box-shadow 0.25s cubic-bezier(0.4, 0, 0.2, 1); + } + + .el-overlay { + height: 100% !important; + width: 100% !important; + top: 0 !important; + left: 0 !important; + border-radius: 0 !important; + } + } +} +/* 滚动条轨道样式 */ +::-webkit-scrollbar-track { + background-image: linear-gradient(180deg, #e7f0fd 1%, #daeafc 40%) !important; + display: none !important; +} + +::-webkit-scrollbar { + width: 4px !important; + height: 4px !important; + color: var(--o-scrollbar-thumb) !important; +} + +/* 滚动条的滑块 */ +::-webkit-scrollbar-thumb { + background-color: var(--o-scrollbar-thumb) !important; + border-radius: 3px !important; +} +::-webkit-scrollbar-corner { + background: transparent !important; +} + +::-webkit-scrollbar-thumb:hover { + background-color: var(--o-scrollbar-thumb) !important; + /* 鼠标悬停时的滚动条按钮颜色 */ +} diff --git a/src/assets/styles/theme.scss b/src/assets/styles/theme.scss index d5509c32c2194d3fbcbecbef7bce6256ff77ed29..24823ca5e5038baa135ab0e217f8b1fc0cbcf755 100644 --- a/src/assets/styles/theme.scss +++ b/src/assets/styles/theme.scss @@ -1,6 +1,14 @@ body[theme='dark'] { + --o-content-bg: linear-gradient( + to right, + rgba(109, 117, 250, 0.5), + rgba(90, 179, 255, 0.5) + ); + --o-main-bg-color: #131415; --o-bg-image: url('../../assets/svgs/dark_background.svg'); --no-work-flow: url('../../assets/images/dark_no_flow.png'); + --o-question-color: #d3dce9; + --o-text-color-record-number: rgb(211, 220, 233); --o-button-color: #576372; --o-button-border-color: #576372; --o-button-disable-color: #626d7c; @@ -18,21 +26,31 @@ body[theme='dark'] { --flow-bg-color: #343a43; --o-bash-box-shadow: 0 4px 16px 0 rgba(255, 255, 255, 0.1); // 工作流开始节点结束背景渐变色 - --flow-startEnd-bg: linear-gradient(rgba(132, 149, 253, 0.3), - rgba(104, 113, 129, 0.15), - rgba(52, 58, 67, 0)); - --flow-system-bg: linear-gradient(rgba(113, 225, 229, 0.3), - rgba(97, 119, 121, 0.15), - rgba(52, 58, 67, 0)); - --flow-apos-apollo-bg: linear-gradient(rgba(137, 212, 255, 0.3), - rgba(103, 111, 130, 0.15), - rgba(52, 58, 67, 0)); - --flow-euler-copilot-tune-bg: linear-gradient(rgba(156, 237, 203, 0.3), - rgba(101, 125, 110, 0.15), - rgba(52, 58, 67, 0)); - --flow-other-node-bg: linear-gradient(rgba(252, 154, 186, 0.3), - rgba(117, 99, 110, 0.15), - rgba(52, 58, 67, 0)); + --flow-startEnd-bg: linear-gradient( + rgba(132, 149, 253, 0.3), + rgba(104, 113, 129, 0.15), + rgba(52, 58, 67, 0) + ); + --flow-system-bg: linear-gradient( + rgba(113, 225, 229, 0.3), + rgba(97, 119, 121, 0.15), + rgba(52, 58, 67, 0) + ); + --flow-apos-apollo-bg: linear-gradient( + rgba(137, 212, 255, 0.3), + rgba(103, 111, 130, 0.15), + rgba(52, 58, 67, 0) + ); + --flow-euler-copilot-tune-bg: linear-gradient( + rgba(156, 237, 203, 0.3), + rgba(101, 125, 110, 0.15), + rgba(52, 58, 67, 0) + ); + --flow-other-node-bg: linear-gradient( + rgba(252, 154, 186, 0.3), + rgba(117, 99, 110, 0.15), + rgba(52, 58, 67, 0) + ); --flow-node-default-over-color: #25303e; --flow-node-boder-default-over: #314265; --flow-node-success-over-color: #1f312a; @@ -44,10 +62,12 @@ body[theme='dark'] { --o-think-header-text: #e4e8ee; --el-drawer-bg-color: #000000; --el-bg-color: #1f2329; - --question-bg: linear-gradient(0deg, - rgb(47, 57, 66), - rgb(32, 35, 37) 33.232%, - rgb(41, 43, 55) 85.699%); + --question-bg: linear-gradient( + 0deg, + rgb(47, 57, 66), + rgb(32, 35, 37) 33.232%, + rgb(41, 43, 55) 85.699% + ); --el-collapse-header-bg: rgb(42, 47, 55); --el-collapse-border: rgb(62, 69, 81, 0.5); --el-collapse-content-bg: rgb(42, 47, 55, 0.5); @@ -58,7 +78,7 @@ body[theme='dark'] { rgba(28, 57, 81, 0.929) 98.202% ); --o-api-description: rgb(211, 220, 233); - --el-drawer-padding-primary: 24px; + --el-drawer-padding-primary: 24px !important; // 这里是不同种类的debug图标 --flow-debug-default: url('../../assets/svgs/dark_debug.svg'); --flow-debug-hover: url('../../assets/svgs/dark_debug_hover.svg'); @@ -68,13 +88,21 @@ body[theme='dark'] { --expand-fold-default: url('../../assets/svgs/dark_expand_fold.svg'); --expand-fold-hover: url('../../assets/svgs/dark_expand_fold_hover.svg'); --expand-fold-active: url('../../assets/svgs/dark_expand_fold_active.svg'); - --o-scrollbar-thumb: #536372; + --o-scrollbar-thumb: #576372; --o-apiBox-bg: rgb(253, 254, 255); } body[theme='light'] { - --o-bg-image: url('../../assets/svgs/light_background.svg'); + --o-content-bg: linear-gradient( + to right, + rgba(109, 117, 250, 0.2), + rgba(90, 179, 255, 0.2) + ); + --o-main-bg-color: #f1f8ff; + --o-bg-image: url('../../assets/svgs/light_background.webp'); --no-work-flow: url('../../assets/images/light_no_flow.png'); + --o-question-color: #4e5865; + --o-text-color-record-number: rgb(78, 88, 101); --o-button-color: #c3cedf; --o-button-border-color: #d3dce9; --o-button-disable-color: #bfc7d7; @@ -92,21 +120,31 @@ body[theme='light'] { --flow-bg-color: #fdfeff; --o-bash-box-shadow: 0 4px 16px 0 rgba(0, 0, 0, 0.1); // 工作流开始节点结束背景渐变色 - --flow-startEnd-bg: linear-gradient(rgba(133, 148, 253, 0.3), - rgba(210, 231, 252, 0.15), - rgba(255, 255, 255, 0)); - --flow-system-bg: linear-gradient(rgba(156, 237, 203, 0.3), - rgba(240, 253, 244, 0.15), - rgba(255, 255, 255, 0)); - --flow-apos-apollo-bg: linear-gradient(rgba(137, 212, 255, 0.3), - rgba(230, 237, 255, 0.15), - rgba(255, 255, 255, 0)); - --flow-euler-copilot-tune-bg: linear-gradient(rgba(156, 237, 203, 0.3), - rgba(240, 253, 248, 0.15), - rgba(255, 255, 255, 0)); - --flow-other-node-bg: linear-gradient(rgba(252, 154, 186, 0.3), - rgba(253, 240, 248, 0.15), - rgba(255, 255, 255, 0)); + --flow-startEnd-bg: linear-gradient( + rgba(133, 148, 253, 0.3), + rgba(210, 231, 252, 0.15), + rgba(255, 255, 255, 0) + ); + --flow-system-bg: linear-gradient( + rgba(156, 237, 203, 0.3), + rgba(240, 253, 244, 0.15), + rgba(255, 255, 255, 0) + ); + --flow-apos-apollo-bg: linear-gradient( + rgba(137, 212, 255, 0.3), + rgba(230, 237, 255, 0.15), + rgba(255, 255, 255, 0) + ); + --flow-euler-copilot-tune-bg: linear-gradient( + rgba(156, 237, 203, 0.3), + rgba(240, 253, 248, 0.15), + rgba(255, 255, 255, 0) + ); + --flow-other-node-bg: linear-gradient( + rgba(252, 154, 186, 0.3), + rgba(253, 240, 248, 0.15), + rgba(255, 255, 255, 0) + ); --flow-node-boder-default-over: #c7d6f5; --flow-node-success-over-color: #e6f6e9; --flow-node-error-over-color: #f8e7e7; @@ -118,7 +156,18 @@ body[theme='light'] { --el-collapse-header-bg: rgb(244, 246, 250); --el-collapse-border: rgb(223, 229, 239); --el-collapse-content-bg: rgb(244, 246, 250, 0.5); - --question-bg: linear-gradient(177.93deg, rgba(255, 255, 255, 0) -40.031%, rgba(255, 255, 255, 0.35) 1.263%, rgba(255, 255, 255)36.178%), linear-gradient(270deg, rgb(227, 242, 255, 0.5), rgb(195, 227, 255, 0.5) 33.232%, rgb(197, 203, 249, 0.5) 85.699%); + --question-bg: linear-gradient( + 177.93deg, + rgba(255, 255, 255, 0) -40.031%, + rgba(255, 255, 255, 0.35) 1.263%, + rgba(255, 255, 255) 36.178% + ), + linear-gradient( + 270deg, + rgb(227, 242, 255, 0.5), + rgb(195, 227, 255, 0.5) 33.232%, + rgb(197, 203, 249, 0.5) 85.699% + ); --question-shadow: rgba(221, 225, 240, 0.5); --applist-hover: #f3f4f6; --flow-running-bg: linear-gradient( @@ -138,16 +187,15 @@ body[theme='light'] { --expand-fold-active: url('../../assets/svgs/light_expand_fold_active.svg'); --o-scrollbar-thumb: #c3cedf; --o-apiBox-bg: rgb(253, 254, 255); + --el-drawer-padding-primary: 24px !important; } body { - // 这里替换下拉框的选中颜色,无论亮暗都是一致 .el-select-dropdown__item.is-selected { background-color: #6395fd !important; font-weight: normal !important; } - --el-drawer-padding-primary: 24px; // 悬浮优先级高于选中 .el-select-dropdown__item:hover { background-color: #7aa5ff !important; @@ -156,4 +204,5 @@ body { border-top: none !important; cursor: pointer; } -} \ No newline at end of file + --o-border-radius-small: 4px; +} diff --git a/src/assets/svgs/agent.svg b/src/assets/svgs/agent.svg new file mode 100644 index 0000000000000000000000000000000000000000..cadfefb107b35750948f75830b4853fe29c1f161 --- /dev/null +++ b/src/assets/svgs/agent.svg @@ -0,0 +1,176 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/copy_icon.svg b/src/assets/svgs/copy_icon.svg new file mode 100644 index 0000000000000000000000000000000000000000..6750ed5c6fa94e7032ccd08fae13792cf842c399 --- /dev/null +++ b/src/assets/svgs/copy_icon.svg @@ -0,0 +1,16 @@ + + + Created with Pixso. + + + + + + + + + + + + + diff --git a/src/assets/svgs/dark_knowledge.svg b/src/assets/svgs/dark_knowledge.svg new file mode 100644 index 0000000000000000000000000000000000000000..d9f27930514aa389ad54be68f749c9c664d006f3 --- /dev/null +++ b/src/assets/svgs/dark_knowledge.svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/src/assets/svgs/dark_knowledge_hover.svg b/src/assets/svgs/dark_knowledge_hover.svg new file mode 100644 index 0000000000000000000000000000000000000000..83dd93605530fdf1e049dbb8b5aeb850f0343713 --- /dev/null +++ b/src/assets/svgs/dark_knowledge_hover.svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/src/assets/svgs/dark_knowledge_select.svg b/src/assets/svgs/dark_knowledge_select.svg new file mode 100644 index 0000000000000000000000000000000000000000..a1633a4e9a789e3c55e2eac56f0b38d83710fd3a --- /dev/null +++ b/src/assets/svgs/dark_knowledge_select.svg @@ -0,0 +1,10 @@ + + + Created with Pixso. + + + + + + + diff --git a/src/assets/svgs/defaultIcon.svg b/src/assets/svgs/defaultIcon.svg deleted file mode 100644 index d858b33eaeb06fd4d4d9ef799869cb0d1ef920fe..0000000000000000000000000000000000000000 --- a/src/assets/svgs/defaultIcon.svg +++ /dev/null @@ -1,17 +0,0 @@ - - - Created with Pixso. - - - - - - - - - - - - - - diff --git a/src/assets/svgs/defaultIcon.webp b/src/assets/svgs/defaultIcon.webp new file mode 100644 index 0000000000000000000000000000000000000000..5e776bab1aeccc22985b759caea3d3d065ee1e20 Binary files /dev/null and b/src/assets/svgs/defaultIcon.webp differ diff --git a/src/assets/svgs/light_background.svg b/src/assets/svgs/light_background.svg deleted file mode 100644 index 2de92cb9392decb1a6b50ef4314c0f78cffbdf6a..0000000000000000000000000000000000000000 --- a/src/assets/svgs/light_background.svg +++ /dev/null @@ -1,21359 +0,0 @@ - - - Created with Pixso. - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/assets/svgs/light_background.webp b/src/assets/svgs/light_background.webp new file mode 100644 index 0000000000000000000000000000000000000000..6270db00eb51957b6221d1e950ff3a47bb5470c4 Binary files /dev/null and b/src/assets/svgs/light_background.webp differ diff --git a/src/assets/svgs/local_deploy.svg b/src/assets/svgs/local_deploy.svg new file mode 100644 index 0000000000000000000000000000000000000000..79f478da6a7d3a6beb00316f09969baab2bcc7ea --- /dev/null +++ b/src/assets/svgs/local_deploy.svg @@ -0,0 +1,26 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/logo .svg b/src/assets/svgs/logo .svg new file mode 100644 index 0000000000000000000000000000000000000000..b97dee1ef763c74ae59e4cf62541132f042da109 --- /dev/null +++ b/src/assets/svgs/logo .svg @@ -0,0 +1,60 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/online_service.svg b/src/assets/svgs/online_service.svg new file mode 100644 index 0000000000000000000000000000000000000000..0c815738869cc00cf2d037482242dc629cdec7ee --- /dev/null +++ b/src/assets/svgs/online_service.svg @@ -0,0 +1,13 @@ + + + Created with Pixso. + + + + + + + + + + diff --git a/src/assets/svgs/plugin_center.svg b/src/assets/svgs/plugin_center.svg new file mode 100644 index 0000000000000000000000000000000000000000..2f53d49842396a115425242dba2f18ea245a6f72 --- /dev/null +++ b/src/assets/svgs/plugin_center.svg @@ -0,0 +1,13 @@ + + + Created with Pixso. + + + + + + + + + + diff --git a/src/assets/svgs/plugin_center_active.svg b/src/assets/svgs/plugin_center_active.svg new file mode 100644 index 0000000000000000000000000000000000000000..f29328eb4acc539f74bf9f2bc6e208f82258af48 --- /dev/null +++ b/src/assets/svgs/plugin_center_active.svg @@ -0,0 +1,18 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/setting.svg b/src/assets/svgs/setting.svg new file mode 100644 index 0000000000000000000000000000000000000000..ad33ba4bc2985abab7aa63f4e6669799727db49c --- /dev/null +++ b/src/assets/svgs/setting.svg @@ -0,0 +1,13 @@ + + + Created with Pixso. + + + + + + + + + + diff --git a/src/assets/svgs/setting_active.svg b/src/assets/svgs/setting_active.svg new file mode 100644 index 0000000000000000000000000000000000000000..d2579bcffad2dbd7a0e3e57327164db0341a63f5 --- /dev/null +++ b/src/assets/svgs/setting_active.svg @@ -0,0 +1,25 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/assets/svgs/settings.svg b/src/assets/svgs/settings.svg new file mode 100644 index 0000000000000000000000000000000000000000..546c9b53ea7773443c890fab88eb3d4fe2803a05 --- /dev/null +++ b/src/assets/svgs/settings.svg @@ -0,0 +1,13 @@ + + + Created with Pixso. + + + + + + + + + + \ No newline at end of file diff --git a/src/assets/svgs/workflow.svg b/src/assets/svgs/workflow.svg new file mode 100644 index 0000000000000000000000000000000000000000..361d7a190c5d20a24d2b44c3ad00d7734df70560 --- /dev/null +++ b/src/assets/svgs/workflow.svg @@ -0,0 +1,155 @@ + + + Created with Pixso. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/EulerDialog.vue b/src/components/EulerDialog.vue index 39d473654fcfc66238a0d51b5e9ae50e26307bdf..c574a4725dc57c83cf67e5a0da43c073b88d2086 100644 --- a/src/components/EulerDialog.vue +++ b/src/components/EulerDialog.vue @@ -36,7 +36,7 @@ + + + + diff --git a/src/components/Upload/index.vue b/src/components/Upload/index.vue index 40a39052088a453ccef9b8863e220465723c8982..57ac497778fc4b5b1cb86f4133ff55664a86fe42 100644 --- a/src/components/Upload/index.vue +++ b/src/components/Upload/index.vue @@ -7,19 +7,16 @@ import { IconVisible, IconDelete, IconCaretRight, + IconChevronDown, } from '@computing/opendesign-icons'; -import type { - UploadFile, - ElUploadProgressEvent, - ElFile, -} from 'element-plus/es/components/upload/src/upload.type'; -import { Codemirror } from 'vue-codemirror'; +import type { ElFile } from 'element-plus/es/components/upload/src/upload.type'; import { api } from 'src/apis'; import { errorMsg, successMsg } from 'src/components/Message'; import { yaml } from '@codemirror/lang-yaml'; import { oneDark } from '@codemirror/theme-one-dark'; -import { useChangeThemeStore } from 'src/store/conversation'; +import { useChangeThemeStore } from '@/store'; import CustomLoading from 'src/views/customLoading/index.vue'; +import MonacoEditor from 'src/components/monaco/MonacoEditor.vue'; const loading = ref(false); const themeStore = useChangeThemeStore(); @@ -34,7 +31,9 @@ const handleCreateapi = async () => { if (res.code === 200) { getServiceJson.value = res?.result?.apis; getServiceName.value = res?.result?.name; - activeServiceNameList.value = getServiceJson.value.map((item) => item.name); + activeServiceNameList.value = getServiceJson.value.map( + (item) => item.name, + ); uploadtype.value = 'get'; successMsg('创建成功'); } else { @@ -67,7 +66,7 @@ const props = defineProps({ type: String, default: '', }, - getServiceYaml: { + ServiceYaml: { type: String, default: '', }, @@ -181,25 +180,24 @@ const doPreview = (e: Event) => { const getServiceYamlFun = async (id: string) => { await api.querySingleApiData({ serviceId: id, edit: true }).then((res) => { if (res) { - getServiceYaml.value = jsYaml.dump(res?.result.data); - getServiceName.value = res?.result.name; + getServiceYaml.value = jsYaml.dump(res[1].result.data); + getServiceName.value = res[1].result.data.info.title; } }); }; const handleChange = (payload) => { yamlToJsonContent.value = jsYaml.load(payload); - setTimeout(() => { - payload.view.scrollDOM.scrollTop = 0; - }, 100); }; watch( () => props, () => { getServiceJson.value = props.getServiceJson; - getServiceYaml.value = props.getServiceYaml; + getServiceYaml.value = props.ServiceYaml; getServiceName.value = props.getServiceName; if (getServiceJson.value?.length) { - activeServiceNameList.value = getServiceJson.value.map((item) => item.name); + activeServiceNameList.value = getServiceJson.value.map( + (item) => item.name, + ); } if (props.type === 'edit' && props) { getServiceYamlFun(props.serviceId); @@ -293,7 +291,7 @@ onMounted(() => {
{{ getServiceName }} - { @ready="handleReady" @update="updateFunc" @change="handleChange" + /> --> +
- {{ getServiceName }} + {{ getServiceName }} { :name="item.name" >
@@ -366,6 +371,14 @@ onMounted(() => { diff --git a/src/components/commonFooter/CommonFooter.vue b/src/components/commonFooter/CommonFooter.vue index a02803d46760a9573594a2377db3aa29998b95ca..b077064c58431156780f9631a629bc76c224b41f 100644 --- a/src/components/commonFooter/CommonFooter.vue +++ b/src/components/commonFooter/CommonFooter.vue @@ -120,30 +120,6 @@ const readPolicy = async () => { color: var(--o-text-color-primary); font-weight: 700 !important; } - - ::-webkit-scrollbar-track { - background-image: linear-gradient( - 180deg, - #e7f0fd 1%, - #daeafc 40% - ) !important; - display: none; - } - - ::-webkit-scrollbar { - width: 3px; - height: 3px; - display: none; - } - - ::-webkit-scrollbar-thumb { - background-color: #d3dce9 !important; - border-radius: 3px; - } - - .el-scrollbar__thumb { - width: 1px; - } } :deep(.dialog .el-dialog__header) { margin-right: 0px; diff --git a/src/components/commonFooter/PrivacyText.vue b/src/components/commonFooter/PrivacyText.vue index b8772d9a0ca3c2ffe49431c4cddef9ff0b4a0d00..29ddb6bc0b1204ed67533743691edb07197d6295 100644 --- a/src/components/commonFooter/PrivacyText.vue +++ b/src/components/commonFooter/PrivacyText.vue @@ -1,6 +1,6 @@ + - - + diff --git a/src/components/dialoguePanel/DialoguePanel.scss b/src/components/dialoguePanel/DialoguePanel.scss new file mode 100644 index 0000000000000000000000000000000000000000..fbe8fdbce4d0423f6ac6fd0b4e2db67e6cd5e5d8 --- /dev/null +++ b/src/components/dialoguePanel/DialoguePanel.scss @@ -0,0 +1,562 @@ +.button-group { + text-align: center; + .confirm-button { + margin-top: 32px; + width: 64px; + height: 24px; + border-radius: 1; + font-size: 12px; + } +} +.overflowTable { + overflow-x: scroll; +} + +.test { + display: inline-block; + margin-right: 8px; + font-size: 14px; + background-image: linear-gradient(to right, #6d75fa, #5ab3ff); + background-clip: text; + color: transparent; + line-height: 32px; +} +.answer_img_mask { + position: fixed; + top: 0; + left: 0; + width: 100vw; + height: 100vh; + background-color: rgba(0, 0, 0, 0.5); + display: flex; + justify-content: center; + align-items: center; + z-index: 9999; + + img { + max-width: 100%; + max-height: 100%; + } + + span { + position: absolute; + top: 10px; + right: 10px; + font-size: 20px; + } +} + +.el-popper[role='tooltip'] { + max-width: 500px; +} + +.el-popper { + border: none; + + .against-popover-title { + color: var(--o-text-color-primary); + font-size: 16px; + font-weight: 700; + line-height: 24px; + } + + .against-item .el-checkbox .el-checkbox__label { + font-size: 12px; + color: var(--o-text-color-secondary); + line-height: 16px; + } + + .against-button button:first-child { + border: 1px solid var(--o-border-color-lighter); + width: 64px; + height: 24px; + color: var(--o-text-color-secondary); + font-size: 12px; + line-height: 16px; + + &:hover { + color: #7aa5ff; + background-color: transparent; + border: 1px solid #7aa5ff; + } + } + + .against-popover .against-button button:first-child:hover { + background-color: transparent; + } + + .against-button button:last-child { + background-color: var(--o-color-primary); + border: none; + color: var(--o-color-white); + + &:hover { + background-color: #7aa5ff; + color: #fff; + } + } + + .is-disabled, + .is-disabled:hover { + background-color: #b3cbff !important; + color: #e1eaff; + } + + .against-popover .error-input__link, + .against-popover .error-input__desc { + background-color: var(--o-bg-color-light); + } +} + +.against-popover { + .radio { + width: 88px; + margin-bottom: 4px; + } + + .radio_item, + .el-radio-button__inner { + min-width: 88px; + width: 100%; + height: 100%; + border: none; + background-color: var(--o-bg-color-light); + color: var(--o-text-color-primary); + white-space: pre-wrap; + overflow-wrap: break-word; + word-break: auto-phrase; + } + + .el-radio-button__original-radio:checked + .el-radio-button__inner { + border: none; + background-color: transparent; + color: #6395fd; + background-image: linear-gradient( + to right, + rgba(109, 117, 250, 0.2), + rgba(90, 179, 255, 0.2) + ); + } +} + +.svg:hover { + filter: invert(50%) sepia(66%) saturate(446%) hue-rotate(182deg) + brightness(100%) contrast(103%); +} + +.against-button { + height: 16px; + width: auto; + margin-left: 12px; + color: var(--o-text-color-secondary); + + svg { + width: 16px; + height: 16px; + } +} + +.el-tooltip { + float: left; //解决整体右浮动.提示语位置偏差 +} + +::deep .el-popper .el-popper.is-customized { + float: right; //解决整体右浮动.提示语位置偏差 + background-color: pink; +} + +.el-popper.is-customized { + padding: 6px 12px; + background: #f4f6fa; + box-shadow: 0 4px 8px 0 rgba($color: #000000, $alpha: 0.2); +} + +.el-popper.is-customized .el-popper__arrow::before { + background: #f4f6fa; + right: 0; +} + +.search-suggestions { + display: flex; + line-height: 24px; + margin-top: 16px; + + &_value { + display: flex; + flex-wrap: wrap; + } + .tip { + color: var(--o-text-color-secondary); + font-size: 12px; + height: 32px; + line-height: 32px; + align-self: center; + font-weight: 100; + flex-shrink: 0; + } + .value { + display: flex; + color: var(--o-text-color-secondary); + background-color: var(--o-bg-color-base); + border-radius: 8px; + padding: 8px 16px; + margin: 0 0 8px 8px; + font-size: 12px; + &:hover { + background-image: linear-gradient(to right, #6d75fa, #5ab3ff); + color: var(--o-text-color-fourth); + } + p { + align-content: center; + align-items: center; + line-height: 16px; + } + } +} +.dialogue-panel { + width: 1000px; + &__user { + position: relative; + margin-bottom: 24px; + + &-time { + display: flex; + justify-content: center; + color: #8d98aa; + font-size: 12px; + margin-top: 16px; + } + + p { + padding: 12px 16px; + font-size: 16px; + line-height: 24px; + } + } + + &__content { + display: flex; + align-items: flex-start; + margin-top: 10px; + overflow-wrap: break-word; + word-break: break-all; + + img { + width: 48px; + height: 48px; + position: absolute; + left: -10px; + } + + .content { + //min-height: 48px; + border-radius: 8px; + border-top-left-radius: 0px; + padding: 12px; + // display: flex; + // align-items: center; + color: var(--o-text-color-primary); + margin-left: 45px; + background-image: var(--o-content-bg); + .messaege { + top: 10px; + margin-top: 24px; + display: block; + width: 100%; + } + } + } + + &__robot { + position: relative; + padding-left: 45px; + border-radius: 8px; + + .loading { + display: flex; + // min-height: 72px; + // padding: 24px; + background-color: var(--o-bg-color-base); + border-radius: 8px; + border-top-left-radius: 0px; + + @keyframes rotate-img { + from { + transform: rotate(0); + } + + to { + transform: rotate(360deg); + } + } + + &::before { + content: ''; + position: absolute; + top: 0px; + width: 48px; + height: 48px; + left: -10px; + background-image: url('src/assets/svgs/robot.svg'); + } + + &-icon { + animation: rotate-img 1s infinite linear; + } + + &-text { + font-size: 16px; + line-height: 24px; + padding-left: 12px; + color: var(--o-text-color-primary); + } + } + + &-slot { + .dialog-panel__robot-time { + display: flex; + justify-content: center; + color: #8d98aa; + font-size: 12px; + margin-bottom: 10px; + margin-top: 16px; + } + + &::before { + content: ''; + position: absolute; + left: -10px; + top: 30px; + width: 48px; + height: 48px; + background-image: url('src/assets/svgs/robot.svg'); + } + } + + &-content { + background-color: var(--o-bg-color-base); + padding: 24px 24px 16px 24px; + border-top-right-radius: 8px; + overflow-wrap: break-word; + text-align: justify; + line-height: 24px; + color: var(--o-text-color-primary); + + &::before { + content: ''; + position: absolute; + left: -10px; + top: 0px; + width: 48px; + height: 48px; + background-image: url('src/assets/svgs/robot.svg'); + } + } + + &-bottom { + background-color: var(--o-bg-color-base); + padding: 0px 24px; + border-radius: 0 0 8px 8px; + + .action-buttons { + border-top: 1px dashed var(--o-border-color-light); + padding: 16px 0 20px 0px; + display: flex; + align-items: center; + + .pagenation { + display: flex; + + &-item { + margin-right: 8px; + border-radius: 4px; + font-size: 12px; + line-height: 16px; + color: var(--o-question-color) !important; + letter-spacing: 0px; + } + + img { + width: 16px; + height: 16px; + } + + &-arror { + margin: 0; + cursor: pointer; + } + .ml-8 { + margin-left: 4px; + } + .mr-8 { + margin-right: 4px; + } + + .pagenation-cur { + font-size: 12px; + line-height: 16px; + color: var(--o-text-color-tertiary) !important; + } + + .pagenation-total { + font-size: 12px; + line-height: 16px; + color: var(--o-text-color-primary) !important; + } + + letter-spacing: 2px; + } + + .regenerate-button { + display: flex; + align-items: center; + margin-left: 8px; + color: var(--o-text-color-secondary); + font-size: 12px; + line-height: 16px; + cursor: pointer; + user-select: none; + + img { + margin-right: 4px; + } + + .paused-answer { + color: #c4c2c2; + cursor: text; + } + } + + .button-group { + margin-left: auto; + display: flex; + align-items: center; + font-size: 12px; + + .button-icon { + width: 24px; + height: 24px; + margin-left: 4px; + } + + .copy { + width: 24px; + height: 24px; + } + + .copy:hover { + filter: invert(50%) sepia(66%) saturate(446%) hue-rotate(182deg) + brightness(100%) contrast(103%) contrast(99%); + } + + .button-icon:hover { + filter: invert(50%) sepia(66%) saturate(446%) hue-rotate(182deg) + brightness(100%) contrast(103%) contrast(99%); + } + + img { + vertical-align: bottom; + cursor: pointer; + user-select: none; + width: 16px; + height: 16px; + } + } + } + } + } + + &__stop { + display: flex; + justify-content: center; + align-items: center; + width: 128px; + height: 40px; + border-radius: 8px; + border: 1px solid var(--o-border-color-extralight); + margin-top: 38px; + margin-left: auto; + margin-right: auto; + cursor: pointer; + + img { + width: 16px; + height: 16px; + margin-right: 8px; + } + + &-answer { + font-size: 16px; + color: var(--o-text-color-primary); + line-height: 24px; + } + } + + :deep(.el-loading-spinner .circular) { + width: 20px; + height: 20px; + } +} +// 工作流调试抽屉样式 +.workFlowDebugStyle { + width: auto; + padding-right: 24px; + .loading { + display: none; + } + .dialogue-panel__content { + gap: 16px; + .userArea { + min-width: 48px; + height: 48px; + img { + left: 0px; + } + } + .content { + margin-left: 0px; + min-height: 48px; + .message { + white-space: pre-line; + } + } + } + .dialogue-panel__user-time { + height: 20px; + line-height: 20px; + } + .dialogue-panel__robot { + gap: 16px; + padding-left: 64px; + .dialogue-panel__robot-content { + border-radius: 8px; + // 工作流调试时控制显示 + .dialogue-thought { + // ai思考无需显示 + display: none; + } + // 调试抽屉中echarts无需显示 + .answer_img { + display: none; + } + .loading-echarts { + display: none; + } + &::before { + left: 0; + } + } + } + // 调试抽屉中不需要显示底部反对等功能图标 + .dialogue-panel__robot-bottom { + display: none; + } +} +.centerTimeStyle { + color: var(--o-question-color) !important; + width: 136px; + line-height: 20px; + padding: 0 8px; + background-color: var(--o-time-text); + border-radius: 12px; +} diff --git a/src/components/dialoguePanel/DialoguePanel.vue b/src/components/dialoguePanel/DialoguePanel.vue index 2203b49cedf07e20d56f39e8899d3ae26564db1c..d7a59c30f64b2fcbb07b464f50527b7f14f93b5f 100644 --- a/src/components/dialoguePanel/DialoguePanel.vue +++ b/src/components/dialoguePanel/DialoguePanel.vue @@ -1,13 +1,9 @@ @@ -664,7 +688,7 @@ const searchAppName = (appId) => { > { v-if="themeStore.theme === 'dark'" class="button-icon" src="@/assets/svgs/dark_report.svg" - @click="handleSupport('report')" + @click="handleLike('report')" /> {
  • -

    - #{{ searchAppName(item.appId) }} -

    +

    #{{ item.flowName }}

    {{ item.question }}
  • @@ -715,638 +737,6 @@ const searchAppName = (appId) => {
- - - diff --git a/src/components/dialoguePanel/DialogueThought.vue b/src/components/dialoguePanel/DialogueThought.vue index 6caff4b4ef5fb237369979990b72993eb150b1a9..3dedee1e8f29ed9a451ca1385e95d5afaf17a488 100644 --- a/src/components/dialoguePanel/DialogueThought.vue +++ b/src/components/dialoguePanel/DialogueThought.vue @@ -24,7 +24,7 @@ + + + + diff --git a/src/components/monaco/yaml.worker.js b/src/components/monaco/yaml.worker.js new file mode 100644 index 0000000000000000000000000000000000000000..33c5d1a2bde165cb11aa87e7821bd02e79c57bfd --- /dev/null +++ b/src/components/monaco/yaml.worker.js @@ -0,0 +1 @@ +import 'monaco-yaml/yaml.worker.js'; diff --git a/src/components/sessionCard/SessionDropDown.vue b/src/components/sessionCard/SessionDropDown.vue index ea722641a51d2e3f227adb7ec1b8f3250131e7d3..1f56b7175f3c670b09d0dd442e2aefc8554f9f64 100644 --- a/src/components/sessionCard/SessionDropDown.vue +++ b/src/components/sessionCard/SessionDropDown.vue @@ -22,7 +22,7 @@ + + diff --git a/src/views/api/components/McpServiceDetail.vue b/src/views/api/components/McpServiceDetail.vue new file mode 100644 index 0000000000000000000000000000000000000000..e16268cb90e3ac269e2f9a60bd0161bee27f6e89 --- /dev/null +++ b/src/views/api/components/McpServiceDetail.vue @@ -0,0 +1,414 @@ + + + diff --git a/src/views/api/components/MonacoEditor.vue b/src/views/api/components/MonacoEditor.vue new file mode 100644 index 0000000000000000000000000000000000000000..8490f3ba8ec1682f708fb218e57807acdb773d57 --- /dev/null +++ b/src/views/api/components/MonacoEditor.vue @@ -0,0 +1,119 @@ + + + diff --git a/src/views/api/components/PluginCard.vue b/src/views/api/components/PluginCard.vue new file mode 100644 index 0000000000000000000000000000000000000000..eb16dbf9c35d4db8737d8060994651fe0e1c9826 --- /dev/null +++ b/src/views/api/components/PluginCard.vue @@ -0,0 +1,86 @@ + + + diff --git a/src/views/api/index.vue b/src/views/api/index.vue index 0ccfb6b0ba2124307a2bdff529d1c9a7aedb2246..7003fe33253135e35db763e177dcb3deead4a670 100644 --- a/src/views/api/index.vue +++ b/src/views/api/index.vue @@ -1,177 +1,284 @@ + + diff --git a/src/views/api/style.scss b/src/views/api/style.scss index 12a467348f040bab4d3c463751b8d12183028a48..403e381a8e2d746c3bada28f49aef72844b305ad 100644 --- a/src/views/api/style.scss +++ b/src/views/api/style.scss @@ -30,14 +30,13 @@ } .createApi { - margin-left: 16px; + margin-left: 8px; border-radius: 4px; } } .apiCenterType { display: flex; - width: 400px; height: 32px; align-items: center; justify-content: space-between; @@ -45,7 +44,7 @@ border-radius: 4px; background-color: var(--o-border-color-base); .apiCenterBtn { - width: 130px; + width: 197px; height: 28px; display: flex; font-size: 12px; @@ -57,20 +56,19 @@ } } .apiCenterCardContainer { - margin-top: 12px; - padding-top: 4px; - max-width: 1336px; - max-height: calc(100vh - 370px); - min-height: calc(100vh - 370px); - min-width: 1336px; - overflow-y: auto; + width: 1336px; .apiCenterCardBox { + height: calc(100vh - 370px); + overflow-y: auto; width: fit-content; display: flex; + align-content: flex-start; flex-wrap: wrap; box-sizing: border-box; gap: 16px; + padding-top: 16px; .apiCenterCardSingle { + height: fit-content; box-sizing: border-box; border: 1px solid transparent; cursor: pointer; @@ -79,82 +77,64 @@ justify-content: space-between; border-radius: 8px; width: 320px; - height: 136px; padding: 16px; background-color: var(--o-bg-color-base); - .apiCenterCardTop { + .apiCenterCardContentCollect { + width: 24px; + height: 24px; + background-color: var(--o-bg-color-light); + border-radius: 8px; display: flex; - .apiCenterCardIcon { - margin-right: 18px; - display: flex; - align-items: center; - .menu-icon { - width: 48px; - height: 48px; + justify-content: center; + align-items: center; + svg { + width: 16px; + height: 16px; + path { + fill: var(--o-text-color-tertiary); } } - .apiCenterCardContent { - cursor: pointer; - flex: 1; - display: flex; - flex-direction: column; - gap: 4px; - .apiCenterCardContentTop { - display: flex; - justify-content: space-between; - .apiCenterCardContentTitle { - color: var(--o-text-color-primary); - font-size: 16px; - line-height: 24px; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; - width: 80%; - } - .apiCenterCardContentCollect { - width: 24px; - height: 24px; - background-color: var(--o-bg-color-light); - border-radius: 8px; - display: flex; - justify-content: center; - align-items: center; - svg { - width: 16px; - height: 16px; - path { - fill: var(--o-text-color-tertiary); - } - } - .apiFavorite { - path { - fill: rgb(99, 149, 253); - } - } - } - } - .apiCenterCardContentDes { - .vue-text{ - cursor: pointer !important; - color: var(--o-api-description); - } - font-size: 14px; - line-height: 22px; + .apiFavorite { + path { + fill: rgb(99, 149, 253); } } } .apiCenterCardBottom { margin-top: 16px; display: flex; - justify-content: space-between; - .apiCenterCardUser { + flex-direction: column; + color: #8d98aa; + + .apiCenterCardId { font-size: 12px; line-height: 16px; + display: flex; + align-items: center; + justify-content: space-between; } - .apiCenterCardOps { - .el-button span { - color: #6395fd; + .apiCenterCardFooter { + display: flex; + justify-content: space-between; + align-items: center; + margin-top: 4px; + + .apiCenterCardUser { font-size: 12px; + line-height: 16px; + } + .apiCenterCardOps { + display: flex; + align-items: center; + gap: 8px; + .deleteAndEdit { + display: flex; + align-items: center; + } + .el-button span { + color: #6395fd; + font-size: 12px; + } } } } @@ -171,6 +151,8 @@ } } .appCenterNoData { + top: calc(50% - 80px); + position: relative; display: flex; flex-direction: column; gap: 8px; @@ -189,11 +171,6 @@ } } } - ::-webkit-scrollbar { - width: 4px; - height: 8px; - background: transparent; - } @media screen and (width <= 1400px) { .apiCenterCardContainer { width: 1000px; @@ -208,7 +185,7 @@ } } .apiCenterBtnActive { - background-color: var(--o-color-primary) !important; + background-color: #6395fd; border-color: var(--o-color-primary) !important; color: var(--o-color-white) !important; } diff --git a/src/views/app/components/SelectAppTypeDialog.vue b/src/views/app/components/SelectAppTypeDialog.vue new file mode 100644 index 0000000000000000000000000000000000000000..9ef66130ae22798f891b6c2c5c6ae150c5a16401 --- /dev/null +++ b/src/views/app/components/SelectAppTypeDialog.vue @@ -0,0 +1,129 @@ + + + diff --git a/src/views/app/index.vue b/src/views/app/index.vue index c30543282cdc0d0a9bc78b4c3456c8cb776ee135..eb9647c35096e83170482ee28aaa030360243f61 100644 --- a/src/views/app/index.vue +++ b/src/views/app/index.vue @@ -17,43 +17,42 @@ :suffix-icon="IconCaretDown" > - - - + + - + {{ $t('app.app_create') }}
-
-
- {{ $t('app.all_app') }} -
-
- {{ $t('app.my_created') }} -
-
- {{ $t('app.my_favorite') }} -
-
+ + + + + +
- +
+
+ + {{ + appItem.appType === 'flow' + ? $t('app.flow') + : $t('app.agent') + }} + +
@@ -132,11 +147,15 @@ @change="handleChangePage" />
+
- + + diff --git a/src/views/app/style.scss b/src/views/app/style.scss index 01c084838dbda414ad486f8c5c3a1d020db8ae31..2e26280cdec845183e8fe8f4af38ae358354551c 100644 --- a/src/views/app/style.scss +++ b/src/views/app/style.scss @@ -35,7 +35,7 @@ } .createApp { - margin-left: 16px; + margin-left: 8px; border-radius: 4px; } } @@ -62,18 +62,18 @@ } } .appCenterCardContainer { - margin-top: 12px; padding-top: 4px; width: 1340px; - max-height: calc(100vh - 370px); - min-height: calc(100vh - 370px); - overflow-y: auto; .appCenterCardBox { + padding-top: 16px; width: fit-content; display: flex; flex-wrap: wrap; + align-content: flex-start; box-sizing: border-box; gap: 16px; + height: calc(100vh - 370px); + overflow-y: auto; .appCenterCardSingle { box-sizing: border-box; border: 1px solid transparent; @@ -83,20 +83,20 @@ justify-content: space-between; border-radius: 8px; width: 320px; - height: 136px; + height: 160px; padding: 16px; background-color: var(--o-bg-color-base); .appCenterCardTop { cursor: pointer; display: flex; + gap: 16px; max-height: 66px; .appCenterCardIcon { - width: 80px; - height: 50px; display: flex; - align-items: center; + align-items: start; .menu-icon { - width: 40px; + width: 48px; + height: 48px; } } .appCenterCardContent { @@ -117,7 +117,9 @@ color: var(--o-text-color-primary); font-size: 16px; line-height: 24px; + font-weight: 700; } + .appCenterCardContentCollect { width: 24px; height: 24px; @@ -140,8 +142,24 @@ } } } + .appType { + .appTypeName { + font-size: 12px; + line-height: 16px; + padding: 0 4px; + border-radius: 2px; + } + .appTypeName__flow { + color: rgb(109, 71, 245); + background-color: rgb(109, 71, 245, 0.15); + } + .appTypeName__agent { + color: rgb(249, 118, 17); + background-color: rgb(249, 118, 17, 0.15); + } + } .appCenterCardContentDes { - .vue-text{ + .vue-text { cursor: pointer !important; color: var(--o-api-description); } @@ -161,7 +179,6 @@ } .appCenterCardOps { .el-button span { - color: #6395fd; font-size: 12px; } } @@ -179,6 +196,8 @@ } } .appCenterNoData { + top: calc(50% - 80px); + position: relative; display: flex; flex-direction: column; gap: 8px; @@ -197,11 +216,6 @@ } } } - ::-webkit-scrollbar { - width: 4px; - height: 8px; - background: transparent; - } @media screen and (width <= 1400px) { .appCenterCardContainer { width: 1004px; diff --git a/src/views/chat/Operations.vue b/src/views/chat/Operations.vue new file mode 100644 index 0000000000000000000000000000000000000000..da5e883ac2205ec92d43a37325d2bac0dce86286 --- /dev/null +++ b/src/views/chat/Operations.vue @@ -0,0 +1,60 @@ + + + + + diff --git a/src/views/chat/Sender.vue b/src/views/chat/Sender.vue new file mode 100644 index 0000000000000000000000000000000000000000..2735e4345036946b9f1091f57f91c498b2320183 --- /dev/null +++ b/src/views/chat/Sender.vue @@ -0,0 +1,132 @@ + + +