项目迁移
106
.github/workflows/build.yml
vendored
Normal file
@@ -0,0 +1,106 @@
|
||||
name: CI/CD
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- 'v*'
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: Code Check
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.14.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Type check backend
|
||||
run: |
|
||||
cd backend
|
||||
pnpm exec tsc --noEmit
|
||||
|
||||
- name: Type check frontend
|
||||
run: |
|
||||
cd front
|
||||
pnpm exec vue-tsc --noEmit
|
||||
|
||||
build:
|
||||
name: Build Windows
|
||||
runs-on: windows-latest
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Setup pnpm
|
||||
uses: pnpm/action-setup@v4
|
||||
with:
|
||||
version: 9
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '24.14.0'
|
||||
cache: 'pnpm'
|
||||
|
||||
- name: Setup Rust
|
||||
uses: dtolnay/rust-toolchain@stable
|
||||
|
||||
- name: Install dependencies
|
||||
run: pnpm install
|
||||
|
||||
- name: Build backend
|
||||
run: pnpm run backend
|
||||
|
||||
- name: Compress with UPX
|
||||
run: pnpm run upx
|
||||
|
||||
- name: Copy backend to frontend
|
||||
run: pnpm run back2front
|
||||
|
||||
- name: Build frontend
|
||||
run: pnpm run tauri
|
||||
|
||||
- name: Move build to root
|
||||
run: pnpm run build2root
|
||||
|
||||
- name: Upload artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: DeEarthX-V3-${{ github.ref_name }}-windows
|
||||
path: |
|
||||
DeEarthX-V3_x64-setup.exe
|
||||
DeEarthX-V3_x64-setup.zip
|
||||
|
||||
- name: Create Release
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: |
|
||||
DeEarthX-V3_x64-setup.exe
|
||||
DeEarthX-V3_x64-setup.zip
|
||||
draft: false
|
||||
prerelease: false
|
||||
generate_release_notes: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
34
.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
*.zip
|
||||
Dex-v3-core.exe
|
||||
core-x86_64-pc-windows-msvc.exe
|
||||
config.json
|
||||
*.exe
|
||||
*.jar
|
||||
instance
|
||||
*.mrpack
|
||||
templates/
|
||||
package-lock.json
|
||||
133
.trae/documents/deearthx_improvement_plan.md
Normal file
@@ -0,0 +1,133 @@
|
||||
# DeEarthX-CE - 改进计划
|
||||
|
||||
## 项目分析
|
||||
|
||||
DeEarthX-CE 是一个与 Minecraft 相关的工具,包含以下组件:
|
||||
- **后端**:TypeScript + Express 构建,提供模组检测、过滤、平台集成等功能
|
||||
- **前端**:Vue 3 + Ant Design Vue + Tauri 构建的桌面应用
|
||||
- **文档**:VitePress 构建的文档网站
|
||||
|
||||
## 改进任务列表
|
||||
|
||||
### [ ] 任务 1:代码质量检查与优化
|
||||
- **Priority**:P1
|
||||
- **Depends On**:None
|
||||
- **Description**:
|
||||
- 检查并清理未使用的依赖
|
||||
- 统一代码风格和命名规范
|
||||
- 优化错误处理机制
|
||||
- 提高代码可读性和可维护性
|
||||
- **Success Criteria**:
|
||||
- 所有依赖都是必要的
|
||||
- 代码风格统一
|
||||
- 错误处理完善
|
||||
- **Test Requirements**:
|
||||
- `programmatic` TR-1.1:运行 `npm run build` 无错误
|
||||
- `programmatic` TR-1.2:运行代码检查工具无严重警告
|
||||
- `human-judgement` TR-1.3:代码结构清晰,注释完善
|
||||
|
||||
### [ ] 任务 2:性能优化
|
||||
- **Priority**:P2
|
||||
- **Depends On**:任务 1
|
||||
- **Description**:
|
||||
- 优化文件操作性能
|
||||
- 优化网络请求和响应
|
||||
- 减少不必要的计算和重复操作
|
||||
- 提高模组处理速度
|
||||
- **Success Criteria**:
|
||||
- 文件操作速度提升
|
||||
- 网络请求响应时间减少
|
||||
- 模组处理效率提高
|
||||
- **Test Requirements**:
|
||||
- `programmatic` TR-2.1:模组处理时间减少 20%
|
||||
- `programmatic` TR-2.2:内存使用降低 15%
|
||||
- `human-judgement` TR-2.3:用户操作响应更流畅
|
||||
|
||||
### [ ] 任务 3:安全性增强
|
||||
- **Priority**:P1
|
||||
- **Depends On**:任务 1
|
||||
- **Description**:
|
||||
- 检查并修复安全漏洞
|
||||
- 加强输入验证
|
||||
- 优化文件操作安全性
|
||||
- 检查依赖的安全状态
|
||||
- **Success Criteria**:
|
||||
- 无安全漏洞
|
||||
- 输入验证完善
|
||||
- 依赖无安全问题
|
||||
- **Test Requirements**:
|
||||
- `programmatic` TR-3.1:运行安全扫描工具无严重漏洞
|
||||
- `programmatic` TR-3.2:所有输入都经过验证
|
||||
- `human-judgement` TR-3.3:安全措施到位
|
||||
|
||||
### [ ] 任务 4:功能增强
|
||||
- **Priority**:P2
|
||||
- **Depends On**:任务 1, 任务 3
|
||||
- **Description**:
|
||||
- 完善用户界面交互
|
||||
- 增加更多模组平台支持
|
||||
- 优化模板管理功能
|
||||
- 增强多语言支持
|
||||
- **Success Criteria**:
|
||||
- 用户界面更友好
|
||||
- 支持更多模组平台
|
||||
- 模板管理更便捷
|
||||
- 多语言支持更完善
|
||||
- **Test Requirements**:
|
||||
- `programmatic` TR-4.1:所有新增功能正常工作
|
||||
- `human-judgement` TR-4.2:用户界面美观易用
|
||||
- `human-judgement` TR-4.3:多语言支持准确
|
||||
|
||||
### [ ] 任务 5:构建和部署优化
|
||||
- **Priority**:P2
|
||||
- **Depends On**:任务 1, 任务 2
|
||||
- **Description**:
|
||||
- 优化构建流程
|
||||
- 减少构建时间
|
||||
- 优化打包大小
|
||||
- 完善部署文档
|
||||
- **Success Criteria**:
|
||||
- 构建流程更高效
|
||||
- 构建时间减少
|
||||
- 打包大小优化
|
||||
- 部署文档完善
|
||||
- **Test Requirements**:
|
||||
- `programmatic` TR-5.1:构建时间减少 25%
|
||||
- `programmatic` TR-5.2:打包大小减少 20%
|
||||
- `human-judgement` TR-5.3:部署文档清晰完整
|
||||
|
||||
### [ ] 任务 6:测试覆盖度提升
|
||||
- **Priority**:P3
|
||||
- **Depends On**:任务 1
|
||||
- **Description**:
|
||||
- 增加单元测试
|
||||
- 增加集成测试
|
||||
- 提高测试覆盖度
|
||||
- 建立测试自动化流程
|
||||
- **Success Criteria**:
|
||||
- 测试覆盖度达到 80% 以上
|
||||
- 关键功能有测试用例
|
||||
- 测试自动化流程建立
|
||||
- **Test Requirements**:
|
||||
- `programmatic` TR-6.1:测试覆盖度达到 80% 以上
|
||||
- `programmatic` TR-6.2:所有测试用例通过
|
||||
- `human-judgement` TR-6.3:测试用例设计合理
|
||||
|
||||
## 实施步骤
|
||||
|
||||
1. 首先进行代码质量检查与优化(任务 1)
|
||||
2. 然后进行安全性增强(任务 3)
|
||||
3. 接着进行性能优化(任务 2)
|
||||
4. 之后进行功能增强(任务 4)
|
||||
5. 然后进行构建和部署优化(任务 5)
|
||||
6. 最后进行测试覆盖度提升(任务 6)
|
||||
|
||||
## 预期成果
|
||||
|
||||
通过以上改进,DeEarthX-CE 项目将:
|
||||
- 代码质量更高,更易维护
|
||||
- 性能更优,响应更快
|
||||
- 安全性更强,更可靠
|
||||
- 功能更完善,用户体验更好
|
||||
- 构建和部署更高效
|
||||
- 测试覆盖更全面,质量更有保障
|
||||
661
LICENSE
Normal file
@@ -0,0 +1,661 @@
|
||||
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||
Version 3, 19 November 2007
|
||||
|
||||
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||
Everyone is permitted to copy and distribute verbatim copies
|
||||
of this license document, but changing it is not allowed.
|
||||
|
||||
Preamble
|
||||
|
||||
The GNU Affero General Public License is a free, copyleft license for
|
||||
software and other kinds of works, specifically designed to ensure
|
||||
cooperation with the community in the case of network server software.
|
||||
|
||||
The licenses for most software and other practical works are designed
|
||||
to take away your freedom to share and change the works. By contrast,
|
||||
our General Public Licenses are intended to guarantee your freedom to
|
||||
share and change all versions of a program--to make sure it remains free
|
||||
software for all its users.
|
||||
|
||||
When we speak of free software, we are referring to freedom, not
|
||||
price. Our General Public Licenses are designed to make sure that you
|
||||
have the freedom to distribute copies of free software (and charge for
|
||||
them if you wish), that you receive source code or can get it if you
|
||||
want it, that you can change the software or use pieces of it in new
|
||||
free programs, and that you know you can do these things.
|
||||
|
||||
Developers that use our General Public Licenses protect your rights
|
||||
with two steps: (1) assert copyright on the software, and (2) offer
|
||||
you this License which gives you legal permission to copy, distribute
|
||||
and/or modify the software.
|
||||
|
||||
A secondary benefit of defending all users' freedom is that
|
||||
improvements made in alternate versions of the program, if they
|
||||
receive widespread use, become available for other developers to
|
||||
incorporate. Many developers of free software are heartened and
|
||||
encouraged by the resulting cooperation. However, in the case of
|
||||
software used on network servers, this result may fail to come about.
|
||||
The GNU General Public License permits making a modified version and
|
||||
letting the public access it on a server without ever releasing its
|
||||
source code to the public.
|
||||
|
||||
The GNU Affero General Public License is designed specifically to
|
||||
ensure that, in such cases, the modified source code becomes available
|
||||
to the community. It requires the operator of a network server to
|
||||
provide the source code of the modified version running there to the
|
||||
users of that server. Therefore, public use of a modified version, on
|
||||
a publicly accessible server, gives the public access to the source
|
||||
code of the modified version.
|
||||
|
||||
An older license, called the Affero General Public License and
|
||||
published by Affero, was designed to accomplish similar goals. This is
|
||||
a different license, not a version of the Affero GPL, but Affero has
|
||||
released a new version of the Affero GPL which permits relicensing under
|
||||
this license.
|
||||
|
||||
The precise terms and conditions for copying, distribution and
|
||||
modification follow.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
|
||||
0. Definitions.
|
||||
|
||||
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||
|
||||
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||
works, such as semiconductor masks.
|
||||
|
||||
"The Program" refers to any copyrightable work licensed under this
|
||||
License. Each licensee is addressed as "you". "Licensees" and
|
||||
"recipients" may be individuals or organizations.
|
||||
|
||||
To "modify" a work means to copy from or adapt all or part of the work
|
||||
in a fashion requiring copyright permission, other than the making of an
|
||||
exact copy. The resulting work is called a "modified version" of the
|
||||
earlier work or a work "based on" the earlier work.
|
||||
|
||||
A "covered work" means either the unmodified Program or a work based
|
||||
on the Program.
|
||||
|
||||
To "propagate" a work means to do anything with it that, without
|
||||
permission, would make you directly or secondarily liable for
|
||||
infringement under applicable copyright law, except executing it on a
|
||||
computer or modifying a private copy. Propagation includes copying,
|
||||
distribution (with or without modification), making available to the
|
||||
public, and in some countries other activities as well.
|
||||
|
||||
To "convey" a work means any kind of propagation that enables other
|
||||
parties to make or receive copies. Mere interaction with a user through
|
||||
a computer network, with no transfer of a copy, is not conveying.
|
||||
|
||||
An interactive user interface displays "Appropriate Legal Notices"
|
||||
to the extent that it includes a convenient and prominently visible
|
||||
feature that (1) displays an appropriate copyright notice, and (2)
|
||||
tells the user that there is no warranty for the work (except to the
|
||||
extent that warranties are provided), that licensees may convey the
|
||||
work under this License, and how to view a copy of this License. If
|
||||
the interface presents a list of user commands or options, such as a
|
||||
menu, a prominent item in the list meets this criterion.
|
||||
|
||||
1. Source Code.
|
||||
|
||||
The "source code" for a work means the preferred form of the work
|
||||
for making modifications to it. "Object code" means any non-source
|
||||
form of a work.
|
||||
|
||||
A "Standard Interface" means an interface that either is an official
|
||||
standard defined by a recognized standards body, or, in the case of
|
||||
interfaces specified for a particular programming language, one that
|
||||
is widely used among developers working in that language.
|
||||
|
||||
The "System Libraries" of an executable work include anything, other
|
||||
than the work as a whole, that (a) is included in the normal form of
|
||||
packaging a Major Component, but which is not part of that Major
|
||||
Component, and (b) serves only to enable use of the work with that
|
||||
Major Component, or to implement a Standard Interface for which an
|
||||
implementation is available to the public in source code form. A
|
||||
"Major Component", in this context, means a major essential component
|
||||
(kernel, window system, and so on) of the specific operating system
|
||||
(if any) on which the executable work runs, or a compiler used to
|
||||
produce the work, or an object code interpreter used to run it.
|
||||
|
||||
The "Corresponding Source" for a work in object code form means all
|
||||
the source code needed to generate, install, and (for an executable
|
||||
work) run the object code and to modify the work, including scripts to
|
||||
control those activities. However, it does not include the work's
|
||||
System Libraries, or general-purpose tools or generally available free
|
||||
programs which are used unmodified in performing those activities but
|
||||
which are not part of the work. For example, Corresponding Source
|
||||
includes interface definition files associated with source files for
|
||||
the work, and the source code for shared libraries and dynamically
|
||||
linked subprograms that the work is specifically designed to require,
|
||||
such as by intimate data communication or control flow between those
|
||||
subprograms and other parts of the work.
|
||||
|
||||
The Corresponding Source need not include anything that users
|
||||
can regenerate automatically from other parts of the Corresponding
|
||||
Source.
|
||||
|
||||
The Corresponding Source for a work in source code form is that
|
||||
same work.
|
||||
|
||||
2. Basic Permissions.
|
||||
|
||||
All rights granted under this License are granted for the term of
|
||||
copyright on the Program, and are irrevocable provided the stated
|
||||
conditions are met. This License explicitly affirms your unlimited
|
||||
permission to run the unmodified Program. The output from running a
|
||||
covered work is covered by this License only if the output, given its
|
||||
content, constitutes a covered work. This License acknowledges your
|
||||
rights of fair use or other equivalent, as provided by copyright law.
|
||||
|
||||
You may make, run and propagate covered works that you do not
|
||||
convey, without conditions so long as your license otherwise remains
|
||||
in force. You may convey covered works to others for the sole purpose
|
||||
of having them make modifications exclusively for you, or provide you
|
||||
with facilities for running those works, provided that you comply with
|
||||
the terms of this License in conveying all material for which you do
|
||||
not control copyright. Those thus making or running the covered works
|
||||
for you must do so exclusively on your behalf, under your direction
|
||||
and control, on terms that prohibit them from making any copies of
|
||||
your copyrighted material outside their relationship with you.
|
||||
|
||||
Conveying under any other circumstances is permitted solely under
|
||||
the conditions stated below. Sublicensing is not allowed; section 10
|
||||
makes it unnecessary.
|
||||
|
||||
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||
|
||||
No covered work shall be deemed part of an effective technological
|
||||
measure under any applicable law fulfilling obligations under article
|
||||
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||
similar laws prohibiting or restricting circumvention of such
|
||||
measures.
|
||||
|
||||
When you convey a covered work, you waive any legal power to forbid
|
||||
circumvention of technological measures to the extent such circumvention
|
||||
is effected by exercising rights under this License with respect to
|
||||
the covered work, and you disclaim any intention to limit operation or
|
||||
modification of the work as a means of enforcing, against the work's
|
||||
users, your or third parties' legal rights to forbid circumvention of
|
||||
technological measures.
|
||||
|
||||
4. Conveying Verbatim Copies.
|
||||
|
||||
You may convey verbatim copies of the Program's source code as you
|
||||
receive it, in any medium, provided that you conspicuously and
|
||||
appropriately publish on each copy an appropriate copyright notice;
|
||||
keep intact all notices stating that this License and any
|
||||
non-permissive terms added in accord with section 7 apply to the code;
|
||||
keep intact all notices of the absence of any warranty; and give all
|
||||
recipients a copy of this License along with the Program.
|
||||
|
||||
You may charge any price or no price for each copy that you convey,
|
||||
and you may offer support or warranty protection for a fee.
|
||||
|
||||
5. Conveying Modified Source Versions.
|
||||
|
||||
You may convey a work based on the Program, or the modifications to
|
||||
produce it from the Program, in the form of source code under the
|
||||
terms of section 4, provided that you also meet all of these conditions:
|
||||
|
||||
a) The work must carry prominent notices stating that you modified
|
||||
it, and giving a relevant date.
|
||||
|
||||
b) The work must carry prominent notices stating that it is
|
||||
released under this License and any conditions added under section
|
||||
7. This requirement modifies the requirement in section 4 to
|
||||
"keep intact all notices".
|
||||
|
||||
c) You must license the entire work, as a whole, under this
|
||||
License to anyone who comes into possession of a copy. This
|
||||
License will therefore apply, along with any applicable section 7
|
||||
additional terms, to the whole of the work, and all its parts,
|
||||
regardless of how they are packaged. This License gives no
|
||||
permission to license the work in any other way, but it does not
|
||||
invalidate such permission if you have separately received it.
|
||||
|
||||
d) If the work has interactive user interfaces, each must display
|
||||
Appropriate Legal Notices; however, if the Program has interactive
|
||||
interfaces that do not display Appropriate Legal Notices, your
|
||||
work need not make them do so.
|
||||
|
||||
A compilation of a covered work with other separate and independent
|
||||
works, which are not by their nature extensions of the covered work,
|
||||
and which are not combined with it such as to form a larger program,
|
||||
in or on a volume of a storage or distribution medium, is called an
|
||||
"aggregate" if the compilation and its resulting copyright are not
|
||||
used to limit the access or legal rights of the compilation's users
|
||||
beyond what the individual works permit. Inclusion of a covered work
|
||||
in an aggregate does not cause this License to apply to the other
|
||||
parts of the aggregate.
|
||||
|
||||
6. Conveying Non-Source Forms.
|
||||
|
||||
You may convey a covered work in object code form under the terms
|
||||
of sections 4 and 5, provided that you also convey the
|
||||
machine-readable Corresponding Source under the terms of this License,
|
||||
in one of these ways:
|
||||
|
||||
a) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by the
|
||||
Corresponding Source fixed on a durable physical medium
|
||||
customarily used for software interchange.
|
||||
|
||||
b) Convey the object code in, or embodied in, a physical product
|
||||
(including a physical distribution medium), accompanied by a
|
||||
written offer, valid for at least three years and valid for as
|
||||
long as you offer spare parts or customer support for that product
|
||||
model, to give anyone who possesses the object code either (1) a
|
||||
copy of the Corresponding Source for all the software in the
|
||||
product that is covered by this License, on a durable physical
|
||||
medium customarily used for software interchange, for a price no
|
||||
more than your reasonable cost of physically performing this
|
||||
conveying of source, or (2) access to copy the
|
||||
Corresponding Source from a network server at no charge.
|
||||
|
||||
c) Convey individual copies of the object code with a copy of the
|
||||
written offer to provide the Corresponding Source. This
|
||||
alternative is allowed only occasionally and noncommercially, and
|
||||
only if you received the object code with such an offer, in accord
|
||||
with subsection 6b.
|
||||
|
||||
d) Convey the object code by offering access from a designated
|
||||
place (gratis or for a charge), and offer equivalent access to the
|
||||
Corresponding Source in the same way through the same place at no
|
||||
further charge. You need not require recipients to copy the
|
||||
Corresponding Source along with the object code. If the place to
|
||||
copy the object code is a network server, the Corresponding Source
|
||||
may be on a different server (operated by you or a third party)
|
||||
that supports equivalent copying facilities, provided you maintain
|
||||
clear directions next to the object code saying where to find the
|
||||
Corresponding Source. Regardless of what server hosts the
|
||||
Corresponding Source, you remain obligated to ensure that it is
|
||||
available for as long as needed to satisfy these requirements.
|
||||
|
||||
e) Convey the object code using peer-to-peer transmission, provided
|
||||
you inform other peers where the object code and Corresponding
|
||||
Source of the work are being offered to the general public at no
|
||||
charge under subsection 6d.
|
||||
|
||||
A separable portion of the object code, whose source code is excluded
|
||||
from the Corresponding Source as a System Library, need not be
|
||||
included in conveying the object code work.
|
||||
|
||||
A "User Product" is either (1) a "consumer product", which means any
|
||||
tangible personal property which is normally used for personal, family,
|
||||
or household purposes, or (2) anything designed or sold for incorporation
|
||||
into a dwelling. In determining whether a product is a consumer product,
|
||||
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||
product received by a particular user, "normally used" refers to a
|
||||
typical or common use of that class of product, regardless of the status
|
||||
of the particular user or of the way in which the particular user
|
||||
actually uses, or expects or is expected to use, the product. A product
|
||||
is a consumer product regardless of whether the product has substantial
|
||||
commercial, industrial or non-consumer uses, unless such uses represent
|
||||
the only significant mode of use of the product.
|
||||
|
||||
"Installation Information" for a User Product means any methods,
|
||||
procedures, authorization keys, or other information required to install
|
||||
and execute modified versions of a covered work in that User Product from
|
||||
a modified version of its Corresponding Source. The information must
|
||||
suffice to ensure that the continued functioning of the modified object
|
||||
code is in no case prevented or interfered with solely because
|
||||
modification has been made.
|
||||
|
||||
If you convey an object code work under this section in, or with, or
|
||||
specifically for use in, a User Product, and the conveying occurs as
|
||||
part of a transaction in which the right of possession and use of the
|
||||
User Product is transferred to the recipient in perpetuity or for a
|
||||
fixed term (regardless of how the transaction is characterized), the
|
||||
Corresponding Source conveyed under this section must be accompanied
|
||||
by the Installation Information. But this requirement does not apply
|
||||
if neither you nor any third party retains the ability to install
|
||||
modified object code on the User Product (for example, the work has
|
||||
been installed in ROM).
|
||||
|
||||
The requirement to provide Installation Information does not include a
|
||||
requirement to continue to provide support service, warranty, or updates
|
||||
for a work that has been modified or installed by the recipient, or for
|
||||
the User Product in which it has been modified or installed. Access to a
|
||||
network may be denied when the modification itself materially and
|
||||
adversely affects the operation of the network or violates the rules and
|
||||
protocols for communication across the network.
|
||||
|
||||
Corresponding Source conveyed, and Installation Information provided,
|
||||
in accord with this section must be in a format that is publicly
|
||||
documented (and with an implementation available to the public in
|
||||
source code form), and must require no special password or key for
|
||||
unpacking, reading or copying.
|
||||
|
||||
7. Additional Terms.
|
||||
|
||||
"Additional permissions" are terms that supplement the terms of this
|
||||
License by making exceptions from one or more of its conditions.
|
||||
Additional permissions that are applicable to the entire Program shall
|
||||
be treated as though they were included in this License, to the extent
|
||||
that they are valid under applicable law. If additional permissions
|
||||
apply only to part of the Program, that part may be used separately
|
||||
under those permissions, but the entire Program remains governed by
|
||||
this License without regard to the additional permissions.
|
||||
|
||||
When you convey a copy of a covered work, you may at your option
|
||||
remove any additional permissions from that copy, or from any part of
|
||||
it. (Additional permissions may be written to require their own
|
||||
removal in certain cases when you modify the work.) You may place
|
||||
additional permissions on material, added by you to a covered work,
|
||||
for which you have or can give appropriate copyright permission.
|
||||
|
||||
Notwithstanding any other provision of this License, for material you
|
||||
add to a covered work, you may (if authorized by the copyright holders of
|
||||
that material) supplement the terms of this License with terms:
|
||||
|
||||
a) Disclaiming warranty or limiting liability differently from the
|
||||
terms of sections 15 and 16 of this License; or
|
||||
|
||||
b) Requiring preservation of specified reasonable legal notices or
|
||||
author attributions in that material or in the Appropriate Legal
|
||||
Notices displayed by works containing it; or
|
||||
|
||||
c) Prohibiting misrepresentation of the origin of that material, or
|
||||
requiring that modified versions of such material be marked in
|
||||
reasonable ways as different from the original version; or
|
||||
|
||||
d) Limiting the use for publicity purposes of names of licensors or
|
||||
authors of the material; or
|
||||
|
||||
e) Declining to grant rights under trademark law for use of some
|
||||
trade names, trademarks, or service marks; or
|
||||
|
||||
f) Requiring indemnification of licensors and authors of that
|
||||
material by anyone who conveys the material (or modified versions of
|
||||
it) with contractual assumptions of liability to the recipient, for
|
||||
any liability that these contractual assumptions directly impose on
|
||||
those licensors and authors.
|
||||
|
||||
All other non-permissive additional terms are considered "further
|
||||
restrictions" within the meaning of section 10. If the Program as you
|
||||
received it, or any part of it, contains a notice stating that it is
|
||||
governed by this License along with a term that is a further
|
||||
restriction, you may remove that term. If a license document contains
|
||||
a further restriction but permits relicensing or conveying under this
|
||||
License, you may add to a covered work material governed by the terms
|
||||
of that license document, provided that the further restriction does
|
||||
not survive such relicensing or conveying.
|
||||
|
||||
If you add terms to a covered work in accord with this section, you
|
||||
must place, in the relevant source files, a statement of the
|
||||
additional terms that apply to those files, or a notice indicating
|
||||
where to find the applicable terms.
|
||||
|
||||
Additional terms, permissive or non-permissive, may be stated in the
|
||||
form of a separately written license, or stated as exceptions;
|
||||
the above requirements apply either way.
|
||||
|
||||
8. Termination.
|
||||
|
||||
You may not propagate or modify a covered work except as expressly
|
||||
provided under this License. Any attempt otherwise to propagate or
|
||||
modify it is void, and will automatically terminate your rights under
|
||||
this License (including any patent licenses granted under the third
|
||||
paragraph of section 11).
|
||||
|
||||
However, if you cease all violation of this License, then your
|
||||
license from a particular copyright holder is reinstated (a)
|
||||
provisionally, unless and until the copyright holder explicitly and
|
||||
finally terminates your license, and (b) permanently, if the copyright
|
||||
holder fails to notify you of the violation by some reasonable means
|
||||
prior to 60 days after the cessation.
|
||||
|
||||
Moreover, your license from a particular copyright holder is
|
||||
reinstated permanently if the copyright holder notifies you of the
|
||||
violation by some reasonable means, this is the first time you have
|
||||
received notice of violation of this License (for any work) from that
|
||||
copyright holder, and you cure the violation prior to 30 days after
|
||||
your receipt of the notice.
|
||||
|
||||
Termination of your rights under this section does not terminate the
|
||||
licenses of parties who have received copies or rights from you under
|
||||
this License. If your rights have been terminated and not permanently
|
||||
reinstated, you do not qualify to receive new licenses for the same
|
||||
material under section 10.
|
||||
|
||||
9. Acceptance Not Required for Having Copies.
|
||||
|
||||
You are not required to accept this License in order to receive or
|
||||
run a copy of the Program. Ancillary propagation of a covered work
|
||||
occurring solely as a consequence of using peer-to-peer transmission
|
||||
to receive a copy likewise does not require acceptance. However,
|
||||
nothing other than this License grants you permission to propagate or
|
||||
modify any covered work. These actions infringe copyright if you do
|
||||
not accept this License. Therefore, by modifying or propagating a
|
||||
covered work, you indicate your acceptance of this License to do so.
|
||||
|
||||
10. Automatic Licensing of Downstream Recipients.
|
||||
|
||||
Each time you convey a covered work, the recipient automatically
|
||||
receives a license from the original licensors, to run, modify and
|
||||
propagate that work, subject to this License. You are not responsible
|
||||
for enforcing compliance by third parties with this License.
|
||||
|
||||
An "entity transaction" is a transaction transferring control of an
|
||||
organization, or substantially all assets of one, or subdividing an
|
||||
organization, or merging organizations. If propagation of a covered
|
||||
work results from an entity transaction, each party to that
|
||||
transaction who receives a copy of the work also receives whatever
|
||||
licenses to the work the party's predecessor in interest had or could
|
||||
give under the previous paragraph, plus a right to possession of the
|
||||
Corresponding Source of the work from the predecessor in interest, if
|
||||
the predecessor has it or can get it with reasonable efforts.
|
||||
|
||||
You may not impose any further restrictions on the exercise of the
|
||||
rights granted or affirmed under this License. For example, you may
|
||||
not impose a license fee, royalty, or other charge for exercise of
|
||||
rights granted under this License, and you may not initiate litigation
|
||||
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||
any patent claim is infringed by making, using, selling, offering for
|
||||
sale, or importing the Program or any portion of it.
|
||||
|
||||
11. Patents.
|
||||
|
||||
A "contributor" is a copyright holder who authorizes use under this
|
||||
License of the Program or a work on which the Program is based. The
|
||||
work thus licensed is called the contributor's "contributor version".
|
||||
|
||||
A contributor's "essential patent claims" are all patent claims
|
||||
owned or controlled by the contributor, whether already acquired or
|
||||
hereafter acquired, that would be infringed by some manner, permitted
|
||||
by this License, of making, using, or selling its contributor version,
|
||||
but do not include claims that would be infringed only as a
|
||||
consequence of further modification of the contributor version. For
|
||||
purposes of this definition, "control" includes the right to grant
|
||||
patent sublicenses in a manner consistent with the requirements of
|
||||
this License.
|
||||
|
||||
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||
patent license under the contributor's essential patent claims, to
|
||||
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||
propagate the contents of its contributor version.
|
||||
|
||||
In the following three paragraphs, a "patent license" is any express
|
||||
agreement or commitment, however denominated, not to enforce a patent
|
||||
(such as an express permission to practice a patent or covenant not to
|
||||
sue for patent infringement). To "grant" such a patent license to a
|
||||
party means to make such an agreement or commitment not to enforce a
|
||||
patent against the party.
|
||||
|
||||
If you convey a covered work, knowingly relying on a patent license,
|
||||
and the Corresponding Source of the work is not available for anyone
|
||||
to copy, free of charge and under the terms of this License, through a
|
||||
publicly available network server or other readily accessible means,
|
||||
then you must either (1) cause the Corresponding Source to be so
|
||||
available, or (2) arrange to deprive yourself of the benefit of the
|
||||
patent license for this particular work, or (3) arrange, in a manner
|
||||
consistent with the requirements of this License, to extend the patent
|
||||
license to downstream recipients. "Knowingly relying" means you have
|
||||
actual knowledge that, but for the patent license, your conveying the
|
||||
covered work in a country, or your recipient's use of the covered work
|
||||
in a country, would infringe one or more identifiable patents in that
|
||||
country that you have reason to believe are valid.
|
||||
|
||||
If, pursuant to or in connection with a single transaction or
|
||||
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||
covered work, and grant a patent license to some of the parties
|
||||
receiving the covered work authorizing them to use, propagate, modify
|
||||
or convey a specific copy of the covered work, then the patent license
|
||||
you grant is automatically extended to all recipients of the covered
|
||||
work and works based on it.
|
||||
|
||||
A patent license is "discriminatory" if it does not include within
|
||||
the scope of its coverage, prohibits the exercise of, or is
|
||||
conditioned on the non-exercise of one or more of the rights that are
|
||||
specifically granted under this License. You may not convey a covered
|
||||
work if you are a party to an arrangement with a third party that is
|
||||
in the business of distributing software, under which you make payment
|
||||
to the third party based on the extent of your activity of conveying
|
||||
the work, and under which the third party grants, to any of the
|
||||
parties who would receive the covered work from you, a discriminatory
|
||||
patent license (a) in connection with copies of the covered work
|
||||
conveyed by you (or copies made from those copies), or (b) primarily
|
||||
for and in connection with specific products or compilations that
|
||||
contain the covered work, unless you entered into that arrangement,
|
||||
or that patent license was granted, prior to 28 March 2007.
|
||||
|
||||
Nothing in this License shall be construed as excluding or limiting
|
||||
any implied license or other defenses to infringement that may
|
||||
otherwise be available to you under applicable patent law.
|
||||
|
||||
12. No Surrender of Others' Freedom.
|
||||
|
||||
If conditions are imposed on you (whether by court order, agreement or
|
||||
otherwise) that contradict the conditions of this License, they do not
|
||||
excuse you from the conditions of this License. If you cannot convey a
|
||||
covered work so as to satisfy simultaneously your obligations under this
|
||||
License and any other pertinent obligations, then as a consequence you may
|
||||
not convey it at all. For example, if you agree to terms that obligate you
|
||||
to collect a royalty for further conveying from those to whom you convey
|
||||
the Program, the only way you could satisfy both those terms and this
|
||||
License would be to refrain entirely from conveying the Program.
|
||||
|
||||
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||
|
||||
Notwithstanding any other provision of this License, if you modify the
|
||||
Program, your modified version must prominently offer all users
|
||||
interacting with it remotely through a computer network (if your version
|
||||
supports such interaction) an opportunity to receive the Corresponding
|
||||
Source of your version by providing access to the Corresponding Source
|
||||
from a network server at no charge, through some standard or customary
|
||||
means of facilitating copying of software. This Corresponding Source
|
||||
shall include the Corresponding Source for any work covered by version 3
|
||||
of the GNU General Public License that is incorporated pursuant to the
|
||||
following paragraph.
|
||||
|
||||
Notwithstanding any other provision of this License, you have
|
||||
permission to link or combine any covered work with a work licensed
|
||||
under version 3 of the GNU General Public License into a single
|
||||
combined work, and to convey the resulting work. The terms of this
|
||||
License will continue to apply to the part which is the covered work,
|
||||
but the work with which it is combined will remain governed by version
|
||||
3 of the GNU General Public License.
|
||||
|
||||
14. Revised Versions of this License.
|
||||
|
||||
The Free Software Foundation may publish revised and/or new versions of
|
||||
the GNU Affero General Public License from time to time. Such new versions
|
||||
will be similar in spirit to the present version, but may differ in detail to
|
||||
address new problems or concerns.
|
||||
|
||||
Each version is given a distinguishing version number. If the
|
||||
Program specifies that a certain numbered version of the GNU Affero General
|
||||
Public License "or any later version" applies to it, you have the
|
||||
option of following the terms and conditions either of that numbered
|
||||
version or of any later version published by the Free Software
|
||||
Foundation. If the Program does not specify a version number of the
|
||||
GNU Affero General Public License, you may choose any version ever published
|
||||
by the Free Software Foundation.
|
||||
|
||||
If the Program specifies that a proxy can decide which future
|
||||
versions of the GNU Affero General Public License can be used, that proxy's
|
||||
public statement of acceptance of a version permanently authorizes you
|
||||
to choose that version for the Program.
|
||||
|
||||
Later license versions may give you additional or different
|
||||
permissions. However, no additional obligations are imposed on any
|
||||
author or copyright holder as a result of your choosing to follow a
|
||||
later version.
|
||||
|
||||
15. Disclaimer of Warranty.
|
||||
|
||||
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||
|
||||
16. Limitation of Liability.
|
||||
|
||||
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||
SUCH DAMAGES.
|
||||
|
||||
17. Interpretation of Sections 15 and 16.
|
||||
|
||||
If the disclaimer of warranty and limitation of liability provided
|
||||
above cannot be given local legal effect according to their terms,
|
||||
reviewing courts shall apply local law that most closely approximates
|
||||
an absolute waiver of all civil liability in connection with the
|
||||
Program, unless a warranty or assumption of liability accompanies a
|
||||
copy of the Program in return for a fee.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
How to Apply These Terms to Your New Programs
|
||||
|
||||
If you develop a new program, and you want it to be of the greatest
|
||||
possible use to the public, the best way to achieve this is to make it
|
||||
free software which everyone can redistribute and change under these terms.
|
||||
|
||||
To do so, attach the following notices to the program. It is safest
|
||||
to attach them to the start of each source file to most effectively
|
||||
state the exclusion of warranty; and each file should have at least
|
||||
the "copyright" line and a pointer to where the full notice is found.
|
||||
|
||||
<one line to give the program's name and a brief idea of what it does.>
|
||||
Copyright (C) <year> <name of author>
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU Affero General Public License as published
|
||||
by the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU Affero General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU Affero General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
Also add information on how to contact you by electronic and paper mail.
|
||||
|
||||
If your software can interact with users remotely through a computer
|
||||
network, you should also make sure that it provides a way for users to
|
||||
get its source. For example, if your program is a web application, its
|
||||
interface could display a "Source" link that leads users to an archive
|
||||
of the code. There are many ways you could offer source, and different
|
||||
solutions will be better for different programs; see section 13 for the
|
||||
specific requirements.
|
||||
|
||||
You should also get your employer (if you work as a programmer) or school,
|
||||
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||
<https://www.gnu.org/licenses/>.
|
||||
87
README.md
Normal file
@@ -0,0 +1,87 @@
|
||||
# DeEarthX V3
|
||||
|
||||
## 项目概述
|
||||
|
||||
DeEarthX V3 是一个 Minecraft 整合包服务端制作工具,帮你快速把客户端整合包转换成可运行的服务端,同时提供模板管理功能。
|
||||
|
||||
QQ群:1090666196
|
||||
|
||||
## 核心功能
|
||||
|
||||
### 整合包支持
|
||||
- CurseForge
|
||||
- Modrinth
|
||||
- MCBBS
|
||||
|
||||
### 模组处理
|
||||
自动区分客户端和服务端模组,保留服务端需要的,剔除客户端专用的(光影、材质包等)。
|
||||
|
||||
### 工作模式
|
||||
- **开服模式**:下载服务端和模组加载器,完整生成服务端
|
||||
- **上传模式**:只做模组筛选,不下载服务端文件
|
||||
|
||||
### 模组加载器
|
||||
- Forge
|
||||
- NeoForge
|
||||
- Fabric
|
||||
|
||||
### 版本支持
|
||||
支持 1.16.5 到最新版本。
|
||||
|
||||
### 模板管理
|
||||
- 创建、编辑、删除本地模板
|
||||
- 导入/导出模板
|
||||
- 模板商店,支持从远程下载模板
|
||||
- 智能下载速度测试,选择最快的下载链接
|
||||
|
||||
## 技术架构
|
||||
|
||||
### 后端
|
||||
TypeScript + Node.js,Express 提供 Web 服务,WebSocket 实时通信,使用 Node.js SEA 打包为独立 exe。
|
||||
|
||||
### 前端
|
||||
Vue 3 + TypeScript,Tauri 2 桌面框架,Ant Design Vue UI 组件,Tailwind CSS 样式。
|
||||
|
||||
## 使用流程
|
||||
|
||||
1. 准备整合包文件
|
||||
2. 选择模式(开服/上传)
|
||||
3. 上传文件
|
||||
4. 等待处理完成
|
||||
5. 下载服务端
|
||||
|
||||
## 模板管理流程
|
||||
|
||||
1. 进入模板管理页面
|
||||
2. 选择本地模板或模板商店
|
||||
3. 本地模板:创建、编辑、删除、导出模板
|
||||
4. 模板商店:浏览并下载模板
|
||||
|
||||
## 项目特色
|
||||
|
||||
- 上传即用,无需配置
|
||||
- 实时进度显示
|
||||
- 内置 BMCLAPI 和 MCIM 镜像源加速下载
|
||||
- 支持多语言
|
||||
- 智能模板管理系统
|
||||
- 模板商店提供丰富的预设模板
|
||||
|
||||
> [!WARNING]
|
||||
> 模组可能过滤不干净,且制作的服务端禁止用于售卖!
|
||||
|
||||
## 安装说明
|
||||
|
||||
直接下载安装包安装即可使用。
|
||||
|
||||
**注意**:建议不要安装在 C 盘,避免权限问题。
|
||||
|
||||
## 系统要求
|
||||
|
||||
- 操作系统:Windows
|
||||
- 开服模式需要 Java 环境
|
||||
- 上传模式不需要 Java
|
||||
|
||||
## 开发团队
|
||||
|
||||
- **Tianpao**:核心开发
|
||||
- **XCC**:功能优化
|
||||
68
b2f.js
Normal file
@@ -0,0 +1,68 @@
|
||||
import fs from "node:fs";
|
||||
import archiver from "archiver";
|
||||
import path from "node:path";
|
||||
|
||||
const args = process.argv.slice(2);
|
||||
|
||||
if (args.length !== 1) {
|
||||
console.error("使用方法: node b2f.js <b2f|b2r>");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
switch (args[0]) {
|
||||
case "b2f": //backend to frontend
|
||||
const sourcePath = "./backend/dist/core.exe";
|
||||
const destPath = "./front/src-tauri/binaries/core-x86_64-pc-windows-msvc.exe";
|
||||
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
console.error(`错误: 源文件不存在: ${sourcePath}`);
|
||||
console.error("请先运行 'npm run backend' 构建后端");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 确保目标目录存在
|
||||
const destDir = path.dirname(destPath);
|
||||
if (!fs.existsSync(destDir)) {
|
||||
fs.mkdirSync(destDir, { recursive: true });
|
||||
console.log(`创建目录: ${destDir}`);
|
||||
}
|
||||
|
||||
// 复制文件
|
||||
fs.copyFileSync(sourcePath, destPath);
|
||||
console.log(`成功复制: ${sourcePath} -> ${destPath}`);
|
||||
break;
|
||||
case "b2r": //build to root
|
||||
const exePath = "./front/src-tauri/target/release/bundle/nsis/DeEarthX-V3_1.0.0_x64-setup.exe";
|
||||
const rootExePath = "./DeEarthX-V3_x64-setup.exe";
|
||||
const zipPath = "./DeEarthX-V3_x64-setup.zip";
|
||||
|
||||
if (!fs.existsSync(exePath)) {
|
||||
console.error(`错误: 源文件不存在: ${exePath}`);
|
||||
console.error("请先运行 'npm run tauri' 构建前端");
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 移动 exe 到根目录
|
||||
fs.renameSync(exePath, rootExePath);
|
||||
console.log(`移动文件: ${exePath} -> ${rootExePath}`);
|
||||
|
||||
// 打包成 zip
|
||||
const output = fs.createWriteStream(zipPath);
|
||||
const archive = archiver("zip", {
|
||||
zlib: { level: 9 } // 最高压缩级别
|
||||
});
|
||||
|
||||
output.on('close', () => {
|
||||
console.log(`打包完成: ${zipPath} (${archive.pointer()} 字节)`);
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
archive.file(rootExePath, { name: path.basename(rootExePath) });
|
||||
await archive.finalize();
|
||||
|
||||
break;
|
||||
default:
|
||||
console.error(`错误: 未知参数 '${args[0]}'`);
|
||||
console.error("有效参数: b2f, b2r");
|
||||
process.exit(1);
|
||||
}
|
||||
14
backend/config1.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"mirror": {
|
||||
"bmclapi": true,
|
||||
"mcimirror": true
|
||||
},
|
||||
"filter": {
|
||||
"hashes": true,
|
||||
"dexpub": true,
|
||||
"mixins": true
|
||||
},
|
||||
"oaf": true,
|
||||
"port": 37019,
|
||||
"host": "localhost"
|
||||
}
|
||||
54
backend/package.json
Normal file
@@ -0,0 +1,54 @@
|
||||
{
|
||||
"name": "dex-v3-core",
|
||||
"version": "1.0.0",
|
||||
"description": "",
|
||||
"license": "MIT",
|
||||
"author": "Tianpao",
|
||||
"type": "module",
|
||||
"main": "dist/bundle.js",
|
||||
"bin": "dist/bundle.js",
|
||||
"scripts": {
|
||||
"test": "set \"DEBUG=true\"&&tsc&&node dist/main.js",
|
||||
"rollup": "rollup -c rollup.config.js",
|
||||
"sea": "node --experimental-sea-config sea-config.json",
|
||||
"sea:build": "node -e \"require('fs').copyFileSync(process.execPath, './dist/core.exe')\" && npx postject ./dist/core.exe NODE_SEA_BLOB ./dist/sea-prep.blob --sentinel-fuse NODE_SEA_FUSE_fce680ab2cc467b6e072b8b5df1996b2",
|
||||
"build": "npm run rollup && npm run sea && npm run sea:build"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@rollup/plugin-commonjs": "^28.0.6",
|
||||
"@rollup/plugin-json": "^6.1.0",
|
||||
"@rollup/plugin-node-resolve": "^16.0.1",
|
||||
"@rollup/plugin-terser": "^0.4.4",
|
||||
"@rollup/plugin-typescript": "^12.1.4",
|
||||
"@types/adm-zip": "^0.5.7",
|
||||
"@types/archiver": "^7.0.0",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/fs-extra": "^11.0.4",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/multer": "^2.0.0",
|
||||
"@types/ws": "^8.18.1",
|
||||
"@types/yauzl": "^2.10.3",
|
||||
"jest": "^30.2.0",
|
||||
"postject": "^1.0.0-alpha.6",
|
||||
"rollup": "^4.50.1",
|
||||
"ts-jest": "^29.4.6",
|
||||
"typescript": "^5.9.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@types/yazl": "^3.3.0",
|
||||
"adm-zip": "^0.5.16",
|
||||
"cors": "^2.8.5",
|
||||
"express": "^5.1.0",
|
||||
"fs-extra": "^11.3.1",
|
||||
"got": "^14.4.8",
|
||||
"multer": "^2.0.2",
|
||||
"p-map": "^7.0.3",
|
||||
"p-retry": "^7.0.0",
|
||||
"picocolors": "^1.1.1",
|
||||
"smol-toml": "^1.6.0",
|
||||
"ws": "^8.18.3",
|
||||
"yauzl": "^3.2.0",
|
||||
"yazl": "^3.3.1"
|
||||
}
|
||||
}
|
||||
45
backend/rollup.config.js
Normal file
@@ -0,0 +1,45 @@
|
||||
import typescript from '@rollup/plugin-typescript'
|
||||
import resolve from '@rollup/plugin-node-resolve';
|
||||
import commonjs from '@rollup/plugin-commonjs'
|
||||
import json from '@rollup/plugin-json';
|
||||
import terser from '@rollup/plugin-terser';
|
||||
|
||||
export default {
|
||||
input: 'src/main.ts',
|
||||
output: {
|
||||
file: 'dist/bundle.js',
|
||||
format: 'cjs',
|
||||
inlineDynamicImports: true,
|
||||
sourcemap: false
|
||||
},
|
||||
plugins: [
|
||||
typescript({
|
||||
tsconfig: './tsconfig.json',
|
||||
module: 'Node16',
|
||||
compilerOptions: {
|
||||
module: 'Node16'
|
||||
}
|
||||
}),
|
||||
resolve({
|
||||
preferBuiltins: true,
|
||||
browser: false,
|
||||
extensions: ['.ts', '.js', '.json'],
|
||||
dedupe: ['tslib']
|
||||
}),
|
||||
commonjs({
|
||||
transformMixedEsModules: true
|
||||
}),
|
||||
json(),
|
||||
terser({
|
||||
compress: true,
|
||||
mangle: false
|
||||
})
|
||||
],
|
||||
onwarn: (warning, warn) => {
|
||||
if (warning.code === 'CIRCULAR_DEPENDENCY') return;
|
||||
if (warning.code === 'THIS_IS_UNDEFINED') return;
|
||||
if (warning.code === 'MODULE_LEVEL_DIRECTIVE') return;
|
||||
if (warning.code === 'UNRESOLVED_IMPORT') return;
|
||||
warn(warning);
|
||||
}
|
||||
};
|
||||
7
backend/sea-config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"main": "./dist/bundle.js",
|
||||
"output": "./dist/sea-prep.blob",
|
||||
"disableExperimentalSEAWarning": true,
|
||||
"useSnapshot": false,
|
||||
"useCodeCache": true
|
||||
}
|
||||
365
backend/src/Dex.ts
Normal file
@@ -0,0 +1,365 @@
|
||||
import fs from "node:fs";
|
||||
import p from "node:path";
|
||||
import websocket, { WebSocketServer } from "ws";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { platform, what_platform } from "./platform/index.js";
|
||||
import { ModFilterService } from "./dearth/index.js";
|
||||
import { dinstall, mlsetup } from "./modloader/index.js";
|
||||
import { Config } from "./utils/config.js";
|
||||
import { execPromise, getAppDir } from "./utils/utils.js";
|
||||
import { MessageWS } from "./utils/ws.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import { yauzl_promise } from "./utils/ziplib.js";
|
||||
import yauzl from "yauzl";
|
||||
import archiver from "archiver";
|
||||
|
||||
export class Dex {
|
||||
wsx!: WebSocketServer;
|
||||
message!: MessageWS;
|
||||
|
||||
constructor(ws: WebSocketServer) {
|
||||
this.wsx = ws;
|
||||
this.wsx.on("connection", (e) => {
|
||||
this.message = new MessageWS(e);
|
||||
});
|
||||
}
|
||||
|
||||
public async Main(buffer: Buffer, dser: boolean, filename?: string, template?: string) {
|
||||
try {
|
||||
const first = Date.now();
|
||||
await this.processModpack(buffer, filename, first, dser, template);
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
logger.error("主流程执行失败", err);
|
||||
this.message.handleError(err);
|
||||
}
|
||||
}
|
||||
|
||||
private async processModpack(buffer: Buffer, filename: string | undefined, startTime: number, isServerMode: boolean, template?: string) {
|
||||
const processedBuffer = await this._processModpack(buffer, filename);
|
||||
const zps = await this._zips(processedBuffer);
|
||||
const { contain, info } = await zps._getinfo();
|
||||
|
||||
if (!contain || !info) {
|
||||
logger.error("整合包信息为空");
|
||||
this.message.handleError(new Error("该整合包似乎不是有效的整合包。"));
|
||||
return;
|
||||
}
|
||||
|
||||
const plat = what_platform(contain);
|
||||
logger.debug("检测到平台", { 平台: plat });
|
||||
logger.debug("整合包信息", info);
|
||||
|
||||
const mpname = info.name;
|
||||
const unpath = p.join(getAppDir(), "instance", mpname);
|
||||
|
||||
await this.parallelTasks(zps, mpname, plat, info, unpath);
|
||||
await this.filterMods(unpath, mpname);
|
||||
await this.installModLoader(plat, info, unpath, isServerMode, template);
|
||||
await this.completeTask(startTime, unpath, mpname, isServerMode);
|
||||
}
|
||||
|
||||
private async parallelTasks(zps: any, mpname: string, plat: string | undefined, info: any, unpath: string) {
|
||||
await Promise.all([
|
||||
zps._unzip(mpname),
|
||||
platform(plat).downloadfile(info, unpath, this.message)
|
||||
]).catch(e => {
|
||||
logger.error("并行任务执行异常", e);
|
||||
});
|
||||
this.message.statusChange();
|
||||
}
|
||||
|
||||
private async filterMods(unpath: string, mpname: string) {
|
||||
const config = Config.getConfig();
|
||||
await new ModFilterService(p.join(unpath, "mods"), p.join(getAppDir(), ".rubbish", mpname), config.filter, this.message).filter();
|
||||
this.message.statusChange();
|
||||
}
|
||||
|
||||
private async installModLoader(plat: string | undefined, info: any, unpath: string, isServerMode: boolean, template?: string) {
|
||||
const mlinfo = await platform(plat).getinfo(info);
|
||||
if (isServerMode) {
|
||||
await mlsetup(
|
||||
mlinfo.loader,
|
||||
mlinfo.minecraft,
|
||||
mlinfo.loader_version,
|
||||
unpath,
|
||||
this.message,
|
||||
template
|
||||
)
|
||||
} else {
|
||||
dinstall(
|
||||
mlinfo.loader,
|
||||
mlinfo.minecraft,
|
||||
mlinfo.loader_version,
|
||||
unpath
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async completeTask(startTime: number, unpath: string, mpname: string, isServerMode: boolean) {
|
||||
const config = Config.getConfig();
|
||||
const latest = Date.now();
|
||||
const duration = latest - startTime;
|
||||
|
||||
if (isServerMode) {
|
||||
this.message.serverInstallComplete(unpath, duration);
|
||||
} else {
|
||||
this.message.finish(startTime, latest);
|
||||
}
|
||||
|
||||
if (!isServerMode && config.autoZip) {
|
||||
await this._createZip(unpath, mpname);
|
||||
}
|
||||
|
||||
if (config.oaf) {
|
||||
await execPromise(`start ${p.join(getAppDir(), "instance")}`);
|
||||
}
|
||||
|
||||
logger.info(`任务完成,耗时 ${duration}ms`);
|
||||
}
|
||||
|
||||
private async _processModpack(buffer: Buffer, filename?: string): Promise<Buffer> {
|
||||
if (!filename || !filename.endsWith('.zip')) {
|
||||
logger.debug("文件名无效或非 ZIP 格式,直接返回原始缓冲区", { 文件名: filename });
|
||||
return buffer;
|
||||
}
|
||||
|
||||
const startTime = Date.now();
|
||||
const bufferSize = buffer.length;
|
||||
logger.info("开始处理整合包", { 文件名: filename, 文件大小: `${(bufferSize / 1024 / 1024).toFixed(2)} MB` });
|
||||
|
||||
try {
|
||||
const zip = await (new Promise<yauzl.ZipFile>((resolve, reject) => {
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true, strictFileNames: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
logger.error("解析 ZIP 文件失败", { 文件名: filename, 错误: err.message });
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
logger.debug("ZIP 文件解析成功", { 文件名: filename });
|
||||
resolve(zipfile);
|
||||
});
|
||||
}));
|
||||
|
||||
logger.info("检测到 PCL 整合包格式,尝试提取 modpack.mrpack 文件");
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
let mrpackBuffer: Buffer | null = null;
|
||||
let hasProcessed = false;
|
||||
let entryCount = 0;
|
||||
|
||||
zip.on('entry', (entry: yauzl.Entry) => {
|
||||
entryCount++;
|
||||
|
||||
if (hasProcessed) {
|
||||
zip.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
if (entry.fileName === 'modpack.mrpack') {
|
||||
logger.info("找到 modpack.mrpack 文件,开始读取", { 文件大小: `${(entry.uncompressedSize / 1024).toFixed(2)} KB` });
|
||||
hasProcessed = true;
|
||||
zip.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
logger.error("打开 modpack.mrpack 读取流失败", { 错误: err.message });
|
||||
zip.close();
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
let bytesRead = 0;
|
||||
|
||||
stream.on('data', (chunk) => {
|
||||
bytesRead += chunk.length;
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
stream.on('end', () => {
|
||||
mrpackBuffer = Buffer.concat(chunks);
|
||||
const duration = Date.now() - startTime;
|
||||
logger.info("modpack.mrpack 提取成功", {
|
||||
原始大小: `${(bufferSize / 1024 / 1024).toFixed(2)} MB`,
|
||||
提取大小: `${(mrpackBuffer.length / 1024).toFixed(2)} KB`,
|
||||
耗时: `${duration}ms`
|
||||
});
|
||||
zip.close();
|
||||
resolve(mrpackBuffer);
|
||||
});
|
||||
|
||||
stream.on('error', (err) => {
|
||||
logger.error("读取 modpack.mrpack 数据失败", { 错误: err.message });
|
||||
zip.close();
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
zip.readEntry();
|
||||
}
|
||||
});
|
||||
|
||||
zip.on('end', () => {
|
||||
if (!hasProcessed) {
|
||||
const duration = Date.now() - startTime;
|
||||
logger.warn("未找到 modpack.mrpack 文件,使用原始缓冲区", {
|
||||
扫描条目数: entryCount,
|
||||
耗时: `${duration}ms`
|
||||
});
|
||||
zip.close();
|
||||
resolve(buffer);
|
||||
}
|
||||
});
|
||||
|
||||
zip.on('error', (err) => {
|
||||
logger.error("ZIP 文件处理异常", { 错误: err.message });
|
||||
zip.close();
|
||||
reject(err);
|
||||
});
|
||||
|
||||
zip.readEntry();
|
||||
});
|
||||
} catch (e) {
|
||||
const err = e as Error;
|
||||
const duration = Date.now() - startTime;
|
||||
logger.error("处理整合包失败,使用原始缓冲区", {
|
||||
文件名: filename,
|
||||
错误: err.message,
|
||||
耗时: `${duration}ms`
|
||||
});
|
||||
return buffer;
|
||||
}
|
||||
}
|
||||
|
||||
private async _zips(buffer: Buffer) {
|
||||
if (buffer.length === 0) {
|
||||
throw new Error("zip 数据为空");
|
||||
}
|
||||
const zip = await yauzl_promise(buffer);
|
||||
let index = 0;
|
||||
const _getinfo = async () => {
|
||||
const importantFiles = ["manifest.json", "modrinth.index.json"];
|
||||
for await (const entry of zip) {
|
||||
if (importantFiles.includes(entry.fileName)) {
|
||||
const content = await entry.ReadEntry;
|
||||
const info = JSON.parse(content.toString());
|
||||
logger.debug("找到关键文件", { fileName: entry.fileName, info });
|
||||
return { contain: entry.fileName, info };
|
||||
}
|
||||
index++;
|
||||
}
|
||||
throw new Error("整合包中未找到清单文件");
|
||||
}
|
||||
if (index === zip.length) {
|
||||
throw new Error("整合包中未找到清单文件");
|
||||
}
|
||||
const _unzip = async (instancename: string) => {
|
||||
logger.info("开始解压流程", { 实例名称: instancename });
|
||||
const instancePath = p.join(getAppDir(), "instance", instancename);
|
||||
let index = 1;
|
||||
for await (const entry of zip) {
|
||||
const isDir = entry.fileName.endsWith("/");
|
||||
logger.info(`进度: ${index}/${zip.length}, 文件: ${entry.fileName}`);
|
||||
|
||||
if (!entry.fileName.startsWith("overrides/")) {
|
||||
logger.info("跳过非 overrides 文件", entry.fileName);
|
||||
this.message.unzip(entry.fileName, zip.length, index);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (entry.fileName === "overrides/") {
|
||||
logger.info("跳过 overrides 目录", entry.fileName);
|
||||
this.message.unzip(entry.fileName, zip.length, index);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (this._ublack(entry.fileName)) {
|
||||
logger.info("跳过黑名单文件", entry.fileName);
|
||||
this.message.unzip(entry.fileName, zip.length, index);
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isDir) {
|
||||
let targetPath = entry.fileName.replace("overrides/", "");
|
||||
await fs.promises.mkdir(p.join(instancePath, targetPath), {
|
||||
recursive: true,
|
||||
});
|
||||
} else {
|
||||
let targetPath = entry.fileName.replace("overrides/", "");
|
||||
|
||||
const dirPath = p.join(instancePath, targetPath.substring(0, targetPath.lastIndexOf("/")));
|
||||
await fs.promises.mkdir(dirPath, { recursive: true });
|
||||
|
||||
const fullPath = p.join(instancePath, targetPath);
|
||||
if (fs.existsSync(fullPath)) {
|
||||
logger.info("文件已存在,跳过解压", targetPath);
|
||||
} else {
|
||||
const stream = await entry.openReadStream;
|
||||
const write = fs.createWriteStream(fullPath);
|
||||
await pipeline(stream, write);
|
||||
}
|
||||
}
|
||||
this.message.unzip(entry.fileName, zip.length, index);
|
||||
index++;
|
||||
}
|
||||
logger.info("解压流程完成", { 实例名称: instancename, 总文件数: zip.length });
|
||||
}
|
||||
return { _getinfo, _unzip };
|
||||
}
|
||||
|
||||
private _ublack(filename: string): boolean {
|
||||
const blacklist = [
|
||||
"overrides/options.txt",
|
||||
"overrides/shaderpacks",
|
||||
"overrides/essential",
|
||||
"overrides/resourcepacks",
|
||||
"overrides/PCL",
|
||||
"overrides/CustomSkinLoader"
|
||||
];
|
||||
|
||||
if (filename === "overrides/" || filename === "overrides") {
|
||||
return true;
|
||||
}
|
||||
|
||||
return blacklist.some(item => {
|
||||
const normalizedItem = item.endsWith("/") ? item : item + "/";
|
||||
const normalizedFilename = filename.endsWith("/") ? filename : filename + "/";
|
||||
return normalizedFilename === normalizedItem || normalizedFilename.startsWith(normalizedItem);
|
||||
});
|
||||
}
|
||||
|
||||
private async _createZip(sourcePath: string, mpname: string): Promise<void> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const outputPath = p.join(getAppDir(), "instance", `${mpname}.zip`);
|
||||
const output = fs.createWriteStream(outputPath);
|
||||
const archive = archiver('zip', {
|
||||
zlib: { level: 9 }
|
||||
});
|
||||
|
||||
output.on('close', () => {
|
||||
logger.info(`打包成功: ${outputPath} (${archive.pointer()} 字节)`);
|
||||
this.message.info(`服务端已打包: ${mpname}.zip`);
|
||||
resolve();
|
||||
});
|
||||
|
||||
archive.on('error', (err: Error) => {
|
||||
logger.error('打包失败', err);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
archive.on('warning', (err: NodeJS.ErrnoException) => {
|
||||
if (err.code === 'ENOENT') {
|
||||
logger.warn('打包警告', err);
|
||||
} else {
|
||||
reject(err);
|
||||
}
|
||||
});
|
||||
|
||||
archive.pipe(output);
|
||||
archive.directory(sourcePath, false);
|
||||
archive.finalize();
|
||||
});
|
||||
}
|
||||
}
|
||||
755
backend/src/core.ts
Normal file
@@ -0,0 +1,755 @@
|
||||
import express, { Application } from "express";
|
||||
import multer from "multer";
|
||||
import cors from "cors"
|
||||
import websocket, { WebSocketServer } from "ws"
|
||||
import { createServer, Server } from "node:http";
|
||||
import { Config, IConfig } from "./utils/config.js";
|
||||
import { Dex } from "./Dex.js";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import { checkJava, JavaCheckResult, detectJavaPaths } from "./utils/utils.js";
|
||||
import { Galaxy } from "./galaxy.js";
|
||||
import fs from "node:fs";
|
||||
|
||||
export class Core {
|
||||
private config: IConfig;
|
||||
private readonly app: Application;
|
||||
private readonly server: Server;
|
||||
public ws!: WebSocketServer;
|
||||
private wsx!: websocket;
|
||||
private readonly upload: multer.Multer;
|
||||
dex: Dex;
|
||||
galaxy: Galaxy;
|
||||
|
||||
constructor(config: IConfig) {
|
||||
this.config = config
|
||||
this.app = express();
|
||||
this.server = createServer(this.app);
|
||||
this.ws = new WebSocketServer({ server: this.server })
|
||||
this.ws.on("connection",(e)=>{
|
||||
this.wsx = e
|
||||
})
|
||||
this.dex = new Dex(this.ws)
|
||||
this.galaxy = new Galaxy()
|
||||
const storage = multer.memoryStorage();
|
||||
this.upload = multer({
|
||||
storage: storage,
|
||||
limits: {
|
||||
fileSize: 2 * 1024 * 1024 * 1024,
|
||||
files: 10
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private async javachecker() {
|
||||
try {
|
||||
const result: JavaCheckResult = await checkJava();
|
||||
|
||||
if (result.exists && result.version) {
|
||||
logger.info(`检测到 Java: ${result.version.fullVersion} (${result.version.vendor})`);
|
||||
|
||||
if (this.wsx) {
|
||||
this.wsx.send(JSON.stringify({
|
||||
type: "info",
|
||||
message: `检测到 Java: ${result.version.fullVersion} (${result.version.vendor})`,
|
||||
data: result.version
|
||||
}));
|
||||
}
|
||||
} else {
|
||||
logger.error("Java 检查失败", result.error);
|
||||
|
||||
if (this.wsx) {
|
||||
this.wsx.send(JSON.stringify({
|
||||
type: "error",
|
||||
message: result.error || "未找到 Java 或版本检查失败",
|
||||
data: result
|
||||
}));
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("Java 检查异常", error as Error);
|
||||
|
||||
if (this.wsx) {
|
||||
this.wsx.send(JSON.stringify({
|
||||
type: "error",
|
||||
message: "Java 检查遇到异常"
|
||||
}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupExpressRoutes() {
|
||||
this.setupMiddleware();
|
||||
this.setupHealthRoutes();
|
||||
this.setupTaskRoutes();
|
||||
this.setupConfigRoutes();
|
||||
this.setupModCheckRoutes();
|
||||
this.setupGalaxyRoutes();
|
||||
this.setupJavaRoutes();
|
||||
this.setupTemplateRoutes();
|
||||
}
|
||||
|
||||
private setupMiddleware() {
|
||||
this.app.use(cors());
|
||||
this.app.use(express.json({ limit: '2gb' }));
|
||||
this.app.use(express.urlencoded({ extended: true, limit: '2gb' }));
|
||||
|
||||
// 全局错误处理中间件
|
||||
this.app.use((err: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
logger.error("全局错误捕获", err);
|
||||
res.status(err.status || 500).json({
|
||||
status: err.status || 500,
|
||||
message: err.message || "服务器内部错误",
|
||||
stack: process.env.NODE_ENV === 'development' ? err.stack : undefined
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setupHealthRoutes() {
|
||||
// 健康检查路由(ping 接口)
|
||||
this.app.get('/', (req, res) => {
|
||||
const pingTime = new Date().toISOString();
|
||||
logger.debug("收到 Ping 请求", { time: pingTime, ip: req.ip });
|
||||
res.json({
|
||||
status: 200,
|
||||
by: "DeEarthX.Core",
|
||||
qqg: "559349662",
|
||||
bilibili: "https://space.bilibili.com/1728953419 ",
|
||||
ping: pingTime
|
||||
});
|
||||
});
|
||||
|
||||
// 版本信息路由
|
||||
this.app.get('/version', (req, res) => {
|
||||
logger.debug("请求版本信息", { ip: req.ip });
|
||||
res.json({
|
||||
status: 200,
|
||||
version: "1.0.0",
|
||||
name: "DeEarthX.Core",
|
||||
buildTime: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private setupTaskRoutes() {
|
||||
// 启动任务路由
|
||||
this.app.post("/start", this.upload.single("file"), (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ status: 400, message: "未上传文件" });
|
||||
}
|
||||
if (!req.query.mode) {
|
||||
return res.status(400).json({ status: 400, message: "缺少 mode 参数" });
|
||||
}
|
||||
|
||||
// 文件类型检查
|
||||
const allowedExtensions = ['.zip', '.mrpack'];
|
||||
const fileExtension = req.file.originalname.toLowerCase().substring(req.file.originalname.lastIndexOf('.'));
|
||||
if (!allowedExtensions.includes(fileExtension)) {
|
||||
return res.status(400).json({ status: 400, message: "只支持 .zip 和 .mrpack 文件" });
|
||||
}
|
||||
|
||||
const isServerMode = req.query.mode === "server";
|
||||
const template = req.query.template as string || "";
|
||||
logger.info("正在启动任务", { 是否服务端模式: isServerMode, 文件名: req.file.originalname, 文件大小: req.file.size, 模板: template || "官方模组加载器" });
|
||||
|
||||
// 非阻塞执行主要任务
|
||||
this.dex.Main(req.file.buffer, isServerMode, req.file.originalname, template).catch(err => {
|
||||
logger.error("任务执行失败", err);
|
||||
});
|
||||
|
||||
res.json({ status: 200, message: "任务已提交,正在处理中" });
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/start 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "服务器内部错误" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupConfigRoutes() {
|
||||
// 获取配置路由
|
||||
this.app.get('/config/get', (req, res) => {
|
||||
try {
|
||||
this.config = Config.getConfig();
|
||||
res.json(this.config);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/config/get 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "获取配置失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 更新配置路由
|
||||
this.app.post('/config/post', (req, res) => {
|
||||
try {
|
||||
Config.writeConfig(req.body);
|
||||
this.config = req.body;
|
||||
Config.clearCache();
|
||||
logger.info("配置已更新");
|
||||
res.json({ status: 200 });
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/config/post 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "更新配置失败" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupModCheckRoutes() {
|
||||
// 模组检查路由 - 通过路径检查
|
||||
this.app.get('/modcheck', async (req, res) => {
|
||||
try {
|
||||
const modsPath = req.query.path as string;
|
||||
if (!modsPath) {
|
||||
return res.status(400).json({ status: 400, message: "缺少 path 参数" });
|
||||
}
|
||||
|
||||
const { ModCheckService } = await import('./dearth/index.js');
|
||||
const checkService = new ModCheckService(modsPath);
|
||||
const results = await checkService.checkMods();
|
||||
|
||||
res.json(results);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/modcheck 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "模组检查失败" });
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
// 模组检查路由 - 通过文件夹路径和整合包名字检查
|
||||
this.app.post('/modcheck/folder', async (req, res) => {
|
||||
try {
|
||||
const { folderPath, bundleName } = req.body;
|
||||
|
||||
if (!folderPath) {
|
||||
logger.warn("请求中缺少文件夹路径");
|
||||
return res.status(400).json({ status: 400, message: "缺少文件夹路径" });
|
||||
}
|
||||
|
||||
if (!bundleName || !bundleName.trim()) {
|
||||
logger.warn("请求中缺少整合包名字");
|
||||
return res.status(400).json({ status: 400, message: "缺少整合包名字" });
|
||||
}
|
||||
|
||||
logger.info("收到模组检查文件夹请求", {
|
||||
folderPath,
|
||||
bundleName: bundleName.trim()
|
||||
});
|
||||
|
||||
const { ModCheckService } = await import('./dearth/index.js');
|
||||
const checkService = new ModCheckService(folderPath);
|
||||
const results = await checkService.checkModsWithBundle(bundleName.trim());
|
||||
|
||||
logger.info("模组检查完成", { resultsCount: results.length });
|
||||
res.json(results);
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/modcheck/folder 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "模组检查失败: " + error.message });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupGalaxyRoutes() {
|
||||
this.app.use("/galaxy", this.galaxy.getRouter());
|
||||
}
|
||||
|
||||
private setupJavaRoutes() {
|
||||
// 检查Java版本
|
||||
this.app.get('/java/check', async (req, res) => {
|
||||
try {
|
||||
const javaPath = req.query.path as string;
|
||||
const result: JavaCheckResult = await checkJava(javaPath);
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
data: result
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/java/check 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "Java检查失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 自动检测Java路径
|
||||
this.app.get('/java/detect', async (req, res) => {
|
||||
try {
|
||||
const paths = await detectJavaPaths();
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
data: paths
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/java/detect 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "Java路径检测失败" });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private setupTemplateRoutes() {
|
||||
// 获取模板列表
|
||||
this.app.get('/templates', async (req, res) => {
|
||||
try {
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
const templates = await templateManager.getTemplates();
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
data: templates
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/templates 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "获取模板列表失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 创建模板
|
||||
this.app.post('/templates', async (req, res) => {
|
||||
try {
|
||||
const { name, version, description, author } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ status: 400, message: "模板名称不能为空" });
|
||||
return;
|
||||
}
|
||||
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
|
||||
const templateId = `template-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
|
||||
await templateManager.createTemplate(templateId, {
|
||||
name,
|
||||
version: version || '1.0.0',
|
||||
description: description || '',
|
||||
author: author || '',
|
||||
created: new Date().toISOString().split("T")[0],
|
||||
type: 'template'
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
message: "模板创建成功",
|
||||
data: { id: templateId }
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/templates POST 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "创建模板失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 删除模板
|
||||
this.app.delete('/templates/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateService = (templateModule as any).TemplateService;
|
||||
const templateService = new TemplateService();
|
||||
|
||||
const success = await templateService.deleteTemplate(id);
|
||||
|
||||
if (success) {
|
||||
res.json({
|
||||
status: 200,
|
||||
message: "模板删除成功"
|
||||
});
|
||||
} else {
|
||||
res.status(404).json({ status: 404, message: "模板不存在" });
|
||||
}
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error(`/templates/${req.params.id} DELETE 路由错误`, error);
|
||||
res.status(500).json({ status: 500, message: "删除模板失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 修改模板信息
|
||||
this.app.put('/templates/:id', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const { name, version, description, author } = req.body;
|
||||
|
||||
if (!name) {
|
||||
res.status(400).json({ status: 400, message: "模板名称不能为空" });
|
||||
return;
|
||||
}
|
||||
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
|
||||
await templateManager.updateTemplate(id, {
|
||||
name,
|
||||
version: version || '1.0.0',
|
||||
description: description || '',
|
||||
author: author || '',
|
||||
type: 'template'
|
||||
});
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
message: "模板更新成功"
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error(`/templates/${req.params.id} PUT 路由错误`, error);
|
||||
res.status(500).json({ status: 500, message: "更新模板失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 打开模板文件夹
|
||||
this.app.get('/templates/:id/path', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const path = await import('path');
|
||||
const { exec } = await import('child_process');
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
|
||||
const templateManager = new TemplateManager();
|
||||
const templatesPath = (templateManager as any).templatesPath;
|
||||
const templatePath = path.resolve(templatesPath, id);
|
||||
|
||||
const platform = process.platform;
|
||||
let command: string;
|
||||
|
||||
if (platform === 'win32') {
|
||||
command = `explorer "${templatePath}"`;
|
||||
} else if (platform === 'darwin') {
|
||||
command = `open "${templatePath}"`;
|
||||
} else {
|
||||
command = `xdg-open "${templatePath}"`;
|
||||
}
|
||||
|
||||
exec(command, (error) => {
|
||||
res.json({
|
||||
status: 200,
|
||||
message: "文件夹已打开"
|
||||
});
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error(`/templates/${req.params.id}/path 路由错误`, error);
|
||||
res.status(500).json({ status: 500, message: "打开文件夹失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 导出模板
|
||||
this.app.get('/templates/:id/export', async (req, res) => {
|
||||
try {
|
||||
const { id } = req.params;
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
|
||||
// 生成临时文件路径
|
||||
const os = await import('os');
|
||||
const path = await import('path');
|
||||
const tempDir = os.tmpdir();
|
||||
const outputPath = path.join(tempDir, `template-${id}.zip`);
|
||||
|
||||
// 导出模板
|
||||
await templateManager.exportTemplate(id, outputPath);
|
||||
|
||||
// 发送文件
|
||||
res.download(outputPath, `template-${id}.zip`, (err) => {
|
||||
// 下载完成后删除临时文件
|
||||
fs.unlink(outputPath, () => {});
|
||||
if (err) {
|
||||
logger.error(`导出模板失败: ${err.message}`);
|
||||
res.status(500).json({ status: 500, message: "导出模板失败" });
|
||||
}
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error(`/templates/${req.params.id}/export 路由错误`, error);
|
||||
res.status(500).json({ status: 500, message: "导出模板失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 导入模板
|
||||
this.app.post('/templates/import', this.upload.single('file'), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) {
|
||||
return res.status(400).json({ status: 400, message: "未上传文件" });
|
||||
}
|
||||
|
||||
// 文件类型检查
|
||||
const fileExtension = req.file.originalname.toLowerCase().substring(req.file.originalname.lastIndexOf('.'));
|
||||
if (fileExtension !== '.zip') {
|
||||
return res.status(400).json({ status: 400, message: "只支持 .zip 文件" });
|
||||
}
|
||||
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
|
||||
// 导入模板
|
||||
const templateId = await templateManager.importTemplate(req.file.buffer);
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
message: "模板导入成功",
|
||||
data: { id: templateId }
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/templates/import 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "导入模板失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// 存储SSE连接
|
||||
const sseConnections = new Map();
|
||||
|
||||
// 存储下载状态
|
||||
const downloadStates = new Map();
|
||||
|
||||
// 从URL安装模板 - POST请求启动下载
|
||||
this.app.post('/templates/install-from-url', async (req, res) => {
|
||||
try {
|
||||
const { url, requestId, resumeFrom = 0 } = req.body;
|
||||
|
||||
if (!url) {
|
||||
return res.status(400).json({ status: 400, message: "缺少 url 参数" });
|
||||
}
|
||||
|
||||
// 下载文件并流式处理
|
||||
const { default: got } = await import('got');
|
||||
const { createWriteStream, readFileSync, statSync, unlinkSync } = await import('fs');
|
||||
const { tmpdir } = await import('os');
|
||||
const { join } = await import('path');
|
||||
|
||||
// 创建临时文件
|
||||
const tempFilePath = join(tmpdir(), `template-${Date.now()}.zip`);
|
||||
const writeStream = createWriteStream(tempFilePath, {
|
||||
flags: resumeFrom > 0 ? 'a' : 'w' // 支持断点续传
|
||||
});
|
||||
|
||||
// 构建请求选项
|
||||
const requestOptions = {
|
||||
headers: {} as Record<string, string>
|
||||
};
|
||||
|
||||
// 如果是续传,设置Range头
|
||||
if (resumeFrom > 0) {
|
||||
requestOptions.headers['Range'] = `bytes=${resumeFrom}-`;
|
||||
}
|
||||
|
||||
// 流式下载(支持分块)
|
||||
const request = await got.stream(url, requestOptions);
|
||||
|
||||
let totalSize = 0;
|
||||
let downloadedSize = resumeFrom;
|
||||
|
||||
// 获取文件大小(如果可用)
|
||||
request.on('response', (response) => {
|
||||
// 检查是否支持分块下载
|
||||
const acceptRanges = response.headers['accept-ranges'];
|
||||
console.log(`服务器支持分块下载: ${acceptRanges}`);
|
||||
|
||||
// 获取文件大小
|
||||
let contentLength = response.headers['content-length'];
|
||||
if (!contentLength) {
|
||||
// 如果没有content-length,尝试从content-range获取
|
||||
const contentRange = response.headers['content-range'];
|
||||
if (contentRange) {
|
||||
const matches = contentRange.match(/bytes \d+-\d+\/(\d+)/);
|
||||
if (matches && matches[1]) {
|
||||
contentLength = matches[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (contentLength) {
|
||||
totalSize = parseInt(contentLength);
|
||||
// 发送初始化信息,包含文件大小
|
||||
if (sseConnections.has(requestId)) {
|
||||
const sseRes = sseConnections.get(requestId);
|
||||
sseRes.write(`data: ${JSON.stringify({
|
||||
type: 'init',
|
||||
totalSize,
|
||||
resumeFrom
|
||||
})}\n\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 监听数据传输,计算进度
|
||||
request.on('data', (chunk) => {
|
||||
downloadedSize += chunk.length;
|
||||
if (totalSize > 0) {
|
||||
const progress = Math.round((downloadedSize / totalSize) * 100);
|
||||
// 向后端日志输出进度
|
||||
console.log(`下载进度: ${progress}%`);
|
||||
// 发送进度信息到SSE连接
|
||||
if (sseConnections.has(requestId)) {
|
||||
const sseRes = sseConnections.get(requestId);
|
||||
sseRes.write(`data: ${JSON.stringify({
|
||||
type: 'progress',
|
||||
progress,
|
||||
downloadedSize,
|
||||
totalSize
|
||||
})}\n\n`);
|
||||
}
|
||||
} else {
|
||||
// 无法计算总大小时,发送假进度
|
||||
const progress = Math.min(90, Math.round((downloadedSize / 1024 / 1024) * 10));
|
||||
if (sseConnections.has(requestId)) {
|
||||
const sseRes = sseConnections.get(requestId);
|
||||
sseRes.write(`data: ${JSON.stringify({
|
||||
type: 'progress',
|
||||
progress,
|
||||
downloadedSize
|
||||
})}\n\n`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// 管道到临时文件
|
||||
await new Promise((resolve, reject) => {
|
||||
request.pipe(writeStream)
|
||||
.on('finish', resolve)
|
||||
.on('error', reject);
|
||||
});
|
||||
|
||||
// 读取临时文件
|
||||
const buffer = readFileSync(tempFilePath);
|
||||
|
||||
// 清理临时文件
|
||||
unlinkSync(tempFilePath);
|
||||
|
||||
// 导入模板
|
||||
const templateModule = await import('./template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
|
||||
const templateId = await templateManager.importTemplate(buffer);
|
||||
|
||||
// 发送完成响应到SSE连接
|
||||
if (sseConnections.has(requestId)) {
|
||||
const sseRes = sseConnections.get(requestId);
|
||||
sseRes.write(`data: ${JSON.stringify({
|
||||
type: 'complete',
|
||||
status: 200,
|
||||
message: "模板安装成功",
|
||||
data: { id: templateId }
|
||||
})}\n\n`);
|
||||
sseRes.end();
|
||||
sseConnections.delete(requestId);
|
||||
}
|
||||
|
||||
// 清理下载状态
|
||||
downloadStates.delete(requestId);
|
||||
|
||||
// 发送POST响应
|
||||
res.json({
|
||||
status: 200,
|
||||
message: "模板安装成功",
|
||||
data: { id: templateId }
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
const { requestId } = req.body;
|
||||
logger.error("/templates/install-from-url 路由错误", error);
|
||||
|
||||
// 发送错误信息到SSE连接
|
||||
if (sseConnections.has(requestId)) {
|
||||
const sseRes = sseConnections.get(requestId);
|
||||
sseRes.write(`data: ${JSON.stringify({
|
||||
type: 'error',
|
||||
status: 500,
|
||||
message: "安装模板失败"
|
||||
})}\n\n`);
|
||||
sseRes.end();
|
||||
sseConnections.delete(requestId);
|
||||
}
|
||||
|
||||
// 清理下载状态
|
||||
downloadStates.delete(requestId);
|
||||
|
||||
res.status(500).json({ status: 500, message: "安装模板失败" });
|
||||
}
|
||||
});
|
||||
|
||||
// SSE连接 - GET请求
|
||||
this.app.get('/templates/install-from-url', (req, res) => {
|
||||
const { requestId } = req.query;
|
||||
|
||||
if (!requestId) {
|
||||
return res.status(400).json({ status: 400, message: "缺少 requestId 参数" });
|
||||
}
|
||||
|
||||
// 设置SSE响应头
|
||||
res.setHeader('Content-Type', 'text/event-stream');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
res.setHeader('Connection', 'keep-alive');
|
||||
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||
|
||||
// 存储连接
|
||||
sseConnections.set(requestId, res);
|
||||
|
||||
// 发送初始信息
|
||||
res.write(`data: ${JSON.stringify({ type: 'init' })}\n\n`);
|
||||
|
||||
// 处理连接关闭
|
||||
req.on('close', () => {
|
||||
sseConnections.delete(requestId);
|
||||
console.log(`SSE连接已关闭: ${requestId}`);
|
||||
});
|
||||
});
|
||||
|
||||
// 获取模板商店数据
|
||||
this.app.get('/templates/store', async (req, res) => {
|
||||
try {
|
||||
const { default: got } = await import('got');
|
||||
|
||||
// 从指定URL获取模板商店数据
|
||||
const response = await got('http://dex.xcclyc.cn/template/template_stor.json');
|
||||
const data = JSON.parse(response.body);
|
||||
|
||||
// 确保返回的数据结构符合前端预期
|
||||
if (!data.templates) {
|
||||
return res.json({
|
||||
status: 200,
|
||||
data: { templates: [] }
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
status: 200,
|
||||
data: data
|
||||
});
|
||||
} catch (err) {
|
||||
const error = err as Error;
|
||||
logger.error("/templates/store 路由错误", error);
|
||||
res.status(500).json({ status: 500, message: "获取模板商店数据失败" });
|
||||
}
|
||||
});
|
||||
}
|
||||
public async start() {
|
||||
|
||||
this.setupExpressRoutes();
|
||||
const port = this.config.port || 37019;
|
||||
const host = this.config.host || 'localhost';
|
||||
this.server.listen(port, host, async () => {
|
||||
logger.info(`服务器正在运行于 http://${host}:${port}`);
|
||||
await this.javachecker();
|
||||
});
|
||||
|
||||
this.server.on('error', (err) => {
|
||||
logger.error("服务器错误", err);
|
||||
});
|
||||
}
|
||||
}
|
||||
571
backend/src/dearth/ModCheckService.ts
Normal file
@@ -0,0 +1,571 @@
|
||||
import { FileExtractor } from "./utils/FileExtractor.js";
|
||||
import { HashFilter } from "./strategies/HashFilter.js";
|
||||
import { MixinFilter } from "./strategies/MixinFilter.js";
|
||||
import { DexpubFilter } from "./strategies/DexpubFilter.js";
|
||||
import { ModrinthFilter } from "./strategies/ModrinthFilter.js";
|
||||
import { IModCheckResult, IModCheckConfig, IFileInfo, ModSide } from "./types.js";
|
||||
import { JarParser } from "../utils/jar-parser.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
import crypto from "node:crypto";
|
||||
|
||||
const DEFAULT_CONFIG: IModCheckConfig = {
|
||||
enableDexpub: true,
|
||||
enableModrinth: true,
|
||||
enableMixin: true,
|
||||
enableHash: true,
|
||||
timeout: 30000,
|
||||
};
|
||||
|
||||
export class ModCheckService {
|
||||
private readonly extractor: FileExtractor;
|
||||
private readonly config: IModCheckConfig;
|
||||
|
||||
constructor(modsDir: string, config?: Partial<IModCheckConfig>) {
|
||||
this.extractor = new FileExtractor(modsDir);
|
||||
this.config = { ...DEFAULT_CONFIG, ...config };
|
||||
}
|
||||
|
||||
async checkMods(): Promise<IModCheckResult[]> {
|
||||
logger.info("开始模组检查流程");
|
||||
const files = await this.extractor.extractFilesInfo();
|
||||
const results: IModCheckResult[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
const result = await this.checkSingleFile(file);
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
logger.info("模组检查流程完成", { 总模组数: results.length });
|
||||
return results;
|
||||
}
|
||||
|
||||
async checkModsWithBundle(bundleName: string): Promise<IModCheckResult[]> {
|
||||
logger.info("开始模组检查流程(带整合包)", { bundleName });
|
||||
const files = await this.extractor.extractFilesInfo();
|
||||
const results: IModCheckResult[] = [];
|
||||
|
||||
const clientMods = await this.identifyClientSideMods(files);
|
||||
|
||||
for (const file of files) {
|
||||
const filename = file.filename;
|
||||
const isClient = clientMods.includes(filename);
|
||||
|
||||
results.push({
|
||||
filename: path.basename(filename),
|
||||
filePath: filename,
|
||||
clientSide: isClient ? 'required' : 'unknown',
|
||||
serverSide: isClient ? 'unsupported' : 'unknown',
|
||||
source: isClient ? 'Multiple' : 'none',
|
||||
checked: isClient,
|
||||
allResults: isClient ? [{
|
||||
source: 'Multiple',
|
||||
clientSide: 'required',
|
||||
serverSide: 'unsupported',
|
||||
checked: true
|
||||
}] : []
|
||||
});
|
||||
}
|
||||
|
||||
if (clientMods.length > 0) {
|
||||
await this.moveClientMods(clientMods, bundleName);
|
||||
logger.info(`已移动 ${clientMods.length} 个客户端模组到 .rubbish/${bundleName}`);
|
||||
}
|
||||
|
||||
logger.info("模组检查流程完成", { 总模组数: results.length, 客户端模组数: clientMods.length });
|
||||
return results;
|
||||
}
|
||||
|
||||
private async identifyClientSideMods(files: IFileInfo[]): Promise<string[]> {
|
||||
const clientMods: string[] = [];
|
||||
const processedFiles = new Set<string>();
|
||||
|
||||
if (this.config.enableDexpub) {
|
||||
logger.info("开始 Galaxy Square (dexpub) 检查客户端模组");
|
||||
const dexpubStrategy = new DexpubFilter();
|
||||
const dexpubMods = await dexpubStrategy.filter(files);
|
||||
const serverModsListSet = new Set(await dexpubStrategy.getServerMods(files));
|
||||
|
||||
dexpubMods.forEach(mod => processedFiles.add(mod));
|
||||
serverModsListSet.forEach(mod => processedFiles.add(mod));
|
||||
clientMods.push(...dexpubMods);
|
||||
}
|
||||
|
||||
if (this.config.enableModrinth) {
|
||||
logger.info("开始 Modrinth API 检查客户端模组");
|
||||
|
||||
let serverModsSet = new Set<string>();
|
||||
if (this.config.enableDexpub) {
|
||||
const dexpubStrategy = new DexpubFilter();
|
||||
serverModsSet = new Set(await dexpubStrategy.getServerMods(files));
|
||||
}
|
||||
|
||||
const unprocessedFiles = files.filter(f => !processedFiles.has(f.filename));
|
||||
const modrinthMods = await new ModrinthFilter().filter(unprocessedFiles);
|
||||
|
||||
modrinthMods.forEach(mod => processedFiles.add(mod));
|
||||
clientMods.push(...modrinthMods);
|
||||
}
|
||||
|
||||
if (this.config.enableMixin) {
|
||||
logger.info("开始 Mixin 检查客户端模组");
|
||||
|
||||
const unprocessedFiles = files.filter(f => !processedFiles.has(f.filename));
|
||||
const mixinMods = await new MixinFilter().filter(unprocessedFiles);
|
||||
|
||||
mixinMods.forEach(mod => processedFiles.add(mod));
|
||||
clientMods.push(...mixinMods);
|
||||
}
|
||||
|
||||
if (this.config.enableHash) {
|
||||
logger.info("开始 Hash 检查客户端模组");
|
||||
|
||||
const unprocessedFiles = files.filter(f => !processedFiles.has(f.filename));
|
||||
const hashMods = await new HashFilter().filter(unprocessedFiles);
|
||||
|
||||
clientMods.push(...hashMods);
|
||||
}
|
||||
|
||||
const uniqueMods = [...new Set(clientMods)];
|
||||
logger.info("识别到客户端模组", { 数量: uniqueMods.length });
|
||||
|
||||
return uniqueMods;
|
||||
}
|
||||
|
||||
private async moveClientMods(clientModFilePaths: string[], bundleName: string): Promise<void> {
|
||||
const rubbishDir = path.join('.rubbish', bundleName);
|
||||
|
||||
try {
|
||||
await fs.promises.mkdir(rubbishDir, { recursive: true });
|
||||
logger.info(`创建目录: ${rubbishDir}`);
|
||||
} catch (error: any) {
|
||||
logger.error(`创建目录失败: ${rubbishDir}`, error);
|
||||
throw error;
|
||||
}
|
||||
|
||||
for (const filePath of clientModFilePaths) {
|
||||
const filename = path.basename(filePath);
|
||||
const destPath = path.join(rubbishDir, filename);
|
||||
|
||||
try {
|
||||
await fs.promises.rename(filePath, destPath);
|
||||
logger.debug(`移动模组: ${filename} -> ${destPath}`);
|
||||
} catch (error: any) {
|
||||
logger.error(`移动模组失败: ${filename}`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async checkUploadedFiles(uploadedFiles: Array<{ originalname: string; buffer: Buffer }>): Promise<IModCheckResult[]> {
|
||||
logger.info("开始检查上传文件", { 文件数量: uploadedFiles.length });
|
||||
const results: IModCheckResult[] = [];
|
||||
|
||||
for (const uploadedFile of uploadedFiles) {
|
||||
try {
|
||||
const fileData = uploadedFile.buffer;
|
||||
const mixins = await JarParser.extractMixins(fileData);
|
||||
const infos = await JarParser.extractModInfo(fileData);
|
||||
|
||||
const fileInfo: IFileInfo = {
|
||||
filename: uploadedFile.originalname,
|
||||
hash: crypto.createHash('sha1').update(fileData).digest('hex'),
|
||||
mixins,
|
||||
infos,
|
||||
fileData,
|
||||
};
|
||||
|
||||
const result = await this.checkSingleFile(fileInfo);
|
||||
results.push(result);
|
||||
} catch (error: any) {
|
||||
logger.error("处理上传文件时出错", { 文件名: uploadedFile.originalname, 错误: error.message });
|
||||
results.push({
|
||||
filename: uploadedFile.originalname,
|
||||
filePath: uploadedFile.originalname,
|
||||
clientSide: "unknown",
|
||||
serverSide: "unknown",
|
||||
source: "none",
|
||||
checked: false,
|
||||
errors: [error.message],
|
||||
allResults: [],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("上传文件模组检查完成", { 总模组数: results.length });
|
||||
return results;
|
||||
}
|
||||
|
||||
async checkSingleMod(filePath: string): Promise<IModCheckResult> {
|
||||
const filename = path.basename(filePath);
|
||||
const hash = await this.calculateHash(filePath);
|
||||
const extractor = new FileExtractor(path.dirname(filePath));
|
||||
const files = await extractor.extractFilesInfo();
|
||||
const fileInfo = files.find(f => f.filename === filename);
|
||||
|
||||
if (!fileInfo) {
|
||||
return {
|
||||
filename,
|
||||
filePath,
|
||||
clientSide: "unknown",
|
||||
serverSide: "unknown",
|
||||
source: "none",
|
||||
checked: false,
|
||||
errors: ["文件未找到或无法提取"],
|
||||
allResults: [],
|
||||
};
|
||||
}
|
||||
|
||||
return this.checkSingleFile(fileInfo);
|
||||
}
|
||||
|
||||
private async checkSingleFile(file: IFileInfo): Promise<IModCheckResult> {
|
||||
const result: IModCheckResult = {
|
||||
filename: file.filename,
|
||||
filePath: file.filename,
|
||||
clientSide: "unknown",
|
||||
serverSide: "unknown",
|
||||
source: "none",
|
||||
checked: false,
|
||||
errors: [],
|
||||
allResults: [],
|
||||
};
|
||||
|
||||
const allResults = await this.collectAllResultsParallel(file);
|
||||
result.allResults = allResults;
|
||||
|
||||
const bestResult = this.mergeResults(allResults);
|
||||
|
||||
result.clientSide = bestResult.clientSide;
|
||||
result.serverSide = bestResult.serverSide;
|
||||
result.source = bestResult.source;
|
||||
result.checked = bestResult.checked;
|
||||
result.errors = bestResult.errors;
|
||||
|
||||
const modInfo = await this.extractModInfoDetails(file);
|
||||
if (modInfo) {
|
||||
result.modId = modInfo.id;
|
||||
result.iconUrl = modInfo.iconUrl;
|
||||
result.description = modInfo.description;
|
||||
result.author = modInfo.author;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private async collectAllResultsParallel(file: IFileInfo): Promise<Array<{
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
source: string;
|
||||
checked: boolean;
|
||||
error?: string;
|
||||
}>> {
|
||||
const checkPromises: Promise<{
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
source: string;
|
||||
checked: boolean;
|
||||
error?: string;
|
||||
}>[] = [];
|
||||
|
||||
if (this.config.enableDexpub) {
|
||||
checkPromises.push(this.runCheckWithTimeout(this.checkDexpub, file, "Dexpub"));
|
||||
}
|
||||
|
||||
if (this.config.enableModrinth) {
|
||||
checkPromises.push(this.runCheckWithTimeout(this.checkModrinth, file, "Modrinth"));
|
||||
}
|
||||
|
||||
if (this.config.enableMixin) {
|
||||
checkPromises.push(this.runCheckWithTimeout(this.checkMixin, file, "Mixin"));
|
||||
}
|
||||
|
||||
if (this.config.enableHash) {
|
||||
checkPromises.push(this.runCheckWithTimeout(this.checkHash, file, "Hash"));
|
||||
}
|
||||
|
||||
return Promise.all(checkPromises);
|
||||
}
|
||||
|
||||
private async runCheckWithTimeout(
|
||||
checkFn: (file: IFileInfo) => Promise<{ clientSide: ModSide; serverSide: ModSide } | null>,
|
||||
file: IFileInfo,
|
||||
source: string
|
||||
): Promise<{
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
source: string;
|
||||
checked: boolean;
|
||||
error?: string;
|
||||
}> {
|
||||
return this.runWithTimeout(
|
||||
checkFn(file),
|
||||
`${source} 检查超时: ${file.filename}`
|
||||
).then(result => {
|
||||
if (result) {
|
||||
return {
|
||||
clientSide: result.clientSide,
|
||||
serverSide: result.serverSide,
|
||||
source,
|
||||
checked: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
clientSide: "unknown" as ModSide,
|
||||
serverSide: "unknown" as ModSide,
|
||||
source,
|
||||
checked: false,
|
||||
};
|
||||
}).catch((error: any) => {
|
||||
logger.warn(`${file.filename} 的 ${source} 检查失败`, { 错误: error.message });
|
||||
return {
|
||||
clientSide: "unknown" as ModSide,
|
||||
serverSide: "unknown" as ModSide,
|
||||
source,
|
||||
checked: false,
|
||||
error: error.message,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private mergeResults(results: Array<{
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
source: string;
|
||||
checked: boolean;
|
||||
error?: string;
|
||||
}>): {
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
source: string;
|
||||
checked: boolean;
|
||||
errors: string[];
|
||||
} {
|
||||
const errors: string[] = [];
|
||||
const successfulResults = results.filter(r => r.checked);
|
||||
|
||||
for (const r of results) {
|
||||
if (r.error) {
|
||||
errors.push(`${r.source}: ${r.error}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (successfulResults.length === 0) {
|
||||
return {
|
||||
clientSide: "unknown",
|
||||
serverSide: "unknown",
|
||||
source: "none",
|
||||
checked: false,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
const priority: { [key: string]: number } = {
|
||||
"Dexpub": 1,
|
||||
"Modrinth": 2,
|
||||
"Mixin": 3,
|
||||
"Hash": 4,
|
||||
};
|
||||
|
||||
successfulResults.sort((a, b) => priority[a.source] - priority[b.source]);
|
||||
const best = successfulResults[0];
|
||||
|
||||
return {
|
||||
clientSide: best.clientSide,
|
||||
serverSide: best.serverSide,
|
||||
source: best.source,
|
||||
checked: true,
|
||||
errors,
|
||||
};
|
||||
}
|
||||
|
||||
private async checkDexpub(file: IFileInfo): Promise<{ clientSide: ModSide; serverSide: ModSide } | null> {
|
||||
const strategy = new DexpubFilter();
|
||||
const files = [file];
|
||||
|
||||
const clientMods = await strategy.filter(files);
|
||||
const serverMods = await strategy.getServerMods(files);
|
||||
const filename = path.basename(file.filename);
|
||||
|
||||
if (clientMods.some(f => path.basename(f) === filename)) {
|
||||
return { clientSide: "required", serverSide: "unsupported" };
|
||||
} else if (serverMods.some(f => path.basename(f) === filename)) {
|
||||
return { clientSide: "unsupported", serverSide: "required" };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async checkModrinth(file: IFileInfo): Promise<{ clientSide: ModSide; serverSide: ModSide } | null> {
|
||||
const strategy = new ModrinthFilter();
|
||||
const files = [file];
|
||||
const clientMods = await strategy.filter(files);
|
||||
const filename = path.basename(file.filename);
|
||||
|
||||
if (clientMods.some(f => path.basename(f) === filename)) {
|
||||
return { clientSide: "required", serverSide: "unsupported" };
|
||||
}
|
||||
|
||||
for (const info of file.infos) {
|
||||
if (info.name === "modrinth.index.json" || info.name === "modrinth.json") {
|
||||
try {
|
||||
const data = JSON.parse(info.data);
|
||||
const clientSide = this.mapClientSide(data.client_side);
|
||||
const serverSide = this.mapServerSide(data.server_side);
|
||||
return { clientSide, serverSide };
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async checkMixin(file: IFileInfo): Promise<{ clientSide: ModSide; serverSide: ModSide } | null> {
|
||||
for (const mixin of file.mixins) {
|
||||
try {
|
||||
const config = JSON.parse(mixin.data);
|
||||
if (!config.mixins?.length && config.client?.length > 0 && !file.filename.includes("lib")) {
|
||||
return { clientSide: "required", serverSide: "unsupported" };
|
||||
}
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async checkHash(file: IFileInfo): Promise<{ clientSide: ModSide; serverSide: ModSide } | null> {
|
||||
const strategy = new HashFilter();
|
||||
const files = [file];
|
||||
const clientMods = await strategy.filter(files);
|
||||
const filename = path.basename(file.filename);
|
||||
|
||||
if (clientMods.some(f => path.basename(f) === filename)) {
|
||||
return { clientSide: "required", serverSide: "unsupported" };
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private mapClientSide(value: string | undefined): ModSide {
|
||||
if (value === "required") return "required";
|
||||
if (value === "optional") return "optional";
|
||||
if (value === "unsupported") return "unsupported";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private mapServerSide(value: string | undefined): ModSide {
|
||||
if (value === "required") return "required";
|
||||
if (value === "optional") return "optional";
|
||||
if (value === "unsupported") return "unsupported";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
private async runWithTimeout<T>(promise: Promise<T>, timeoutMessage: string): Promise<T> {
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
|
||||
const timeoutPromise = new Promise<never>((_, reject) => {
|
||||
timeoutId = setTimeout(() => {
|
||||
reject(new Error(timeoutMessage));
|
||||
}, this.config.timeout);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await Promise.race([promise, timeoutPromise]);
|
||||
clearTimeout(timeoutId!);
|
||||
return result;
|
||||
} catch (error) {
|
||||
clearTimeout(timeoutId!);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async calculateHash(filePath: string): Promise<string> {
|
||||
const fileData = fs.readFileSync(filePath);
|
||||
return crypto.createHash('sha1').update(fileData).digest('hex');
|
||||
}
|
||||
|
||||
private async extractModInfoDetails(file: IFileInfo): Promise<{
|
||||
id?: string;
|
||||
iconUrl?: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
} | null> {
|
||||
for (const info of file.infos) {
|
||||
try {
|
||||
if (info.name.endsWith("mods.toml") || info.name.endsWith("neoforge.mods.toml")) {
|
||||
const { default: toml } = await import("smol-toml");
|
||||
const data = toml.parse(info.data) as any;
|
||||
|
||||
if (data.mods && Array.isArray(data.mods) && data.mods.length > 0) {
|
||||
const mod = data.mods[0] as any;
|
||||
let iconUrl: string | undefined;
|
||||
|
||||
if (mod.logoFile) {
|
||||
iconUrl = await this.extractIconFile(file, mod.logoFile);
|
||||
}
|
||||
|
||||
return {
|
||||
id: mod.modId || mod.modid,
|
||||
iconUrl,
|
||||
description: mod.description,
|
||||
author: mod.authors || mod.author,
|
||||
};
|
||||
}
|
||||
} else if (info.name.endsWith("fabric.mod.json")) {
|
||||
const data = JSON.parse(info.data);
|
||||
|
||||
return {
|
||||
id: data.id,
|
||||
iconUrl: data.icon,
|
||||
description: data.description,
|
||||
author: data.authors?.join(", ") || data.author,
|
||||
};
|
||||
} else if (info.name === "modrinth.index.json" || info.name === "modrinth.json") {
|
||||
const data = JSON.parse(info.data);
|
||||
|
||||
return {
|
||||
id: data.project_id || data.id,
|
||||
description: data.summary || data.description,
|
||||
};
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.debug(`解析 ${info.name} 失败:`, error.message);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private async extractIconFile(file: IFileInfo, iconPath: string): Promise<string | undefined> {
|
||||
try {
|
||||
let jarData: Buffer;
|
||||
|
||||
if (file.fileData) {
|
||||
jarData = file.fileData;
|
||||
} else {
|
||||
jarData = fs.readFileSync(file.filename);
|
||||
}
|
||||
|
||||
const { Azip } = await import("../utils/ziplib.js");
|
||||
const zipEntries = Azip(jarData);
|
||||
|
||||
for (const entry of zipEntries) {
|
||||
if (entry.entryName === iconPath || entry.entryName.endsWith(iconPath)) {
|
||||
const data = await entry.getData();
|
||||
const ext = iconPath.split('.').pop()?.toLowerCase();
|
||||
const mimeType = ext === 'png' ? 'png' : 'jpeg';
|
||||
|
||||
return `data:image/${mimeType};base64,${data.toString('base64')}`;
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.debug(`提取图标文件 ${iconPath} 失败:`, error.message);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
134
backend/src/dearth/ModFilterService.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
import { FileExtractor } from "./utils/FileExtractor.js";
|
||||
import { FileOperator } from "./utils/FileOperator.js";
|
||||
import { HashFilter } from "./strategies/HashFilter.js";
|
||||
import { MixinFilter } from "./strategies/MixinFilter.js";
|
||||
import { DexpubFilter } from "./strategies/DexpubFilter.js";
|
||||
import { ModrinthFilter } from "./strategies/ModrinthFilter.js";
|
||||
import { IFilterConfig } from "./types.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
import { MessageWS } from "../utils/ws.js";
|
||||
import path from "node:path";
|
||||
|
||||
export class ModFilterService {
|
||||
private readonly extractor: FileExtractor;
|
||||
private readonly operator: FileOperator;
|
||||
private readonly config: IFilterConfig;
|
||||
private messageWS?: MessageWS;
|
||||
|
||||
constructor(modsPath: string, movePath: string, config: IFilterConfig, messageWS?: MessageWS) {
|
||||
this.extractor = new FileExtractor(modsPath);
|
||||
this.operator = new FileOperator(movePath);
|
||||
this.config = config;
|
||||
this.messageWS = messageWS;
|
||||
}
|
||||
|
||||
async filter(): Promise<void> {
|
||||
logger.info("开始模组筛选流程");
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const files = await this.extractor.extractFilesInfo();
|
||||
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsStart(files.length);
|
||||
}
|
||||
|
||||
const clientMods = await this.identifyClientSideMods(files);
|
||||
const result = await this.operator.moveClientSideMods(clientMods);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsComplete(clientMods.length, result.success, duration);
|
||||
}
|
||||
|
||||
logger.info("模组筛选流程完成", {
|
||||
识别到的客户端模组: clientMods.length,
|
||||
成功移动: result.success,
|
||||
跳过: result.skipped,
|
||||
失败: result.error
|
||||
});
|
||||
} catch (error) {
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async identifyClientSideMods(files: Array<{ filename: string; hash: string; mixins: any[]; infos: any[] }>): Promise<string[]> {
|
||||
const clientMods: string[] = [];
|
||||
const processedFiles = new Set<string>();
|
||||
|
||||
if (this.config.dexpub) {
|
||||
logger.info("开始 Galaxy Square (dexpub) 检查客户端模组");
|
||||
const dexpubStrategy = new DexpubFilter();
|
||||
const dexpubMods = await dexpubStrategy.filter(files);
|
||||
const serverModsListSet = new Set(await dexpubStrategy.getServerMods(files));
|
||||
|
||||
dexpubMods.forEach(mod => processedFiles.add(mod));
|
||||
serverModsListSet.forEach(mod => processedFiles.add(mod));
|
||||
clientMods.push(...dexpubMods);
|
||||
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsProgress(processedFiles.size, files.length, "Galaxy Square (dexpub) 检查");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.modrinth) {
|
||||
logger.info("开始 Modrinth API 检查客户端模组");
|
||||
|
||||
let serverModsSet = new Set<string>();
|
||||
if (this.config.dexpub) {
|
||||
const dexpubStrategy = new DexpubFilter();
|
||||
serverModsSet = new Set(await dexpubStrategy.getServerMods(files));
|
||||
}
|
||||
|
||||
const unprocessedFiles = files.filter(f => !processedFiles.has(f.filename));
|
||||
const modrinthMods = await new ModrinthFilter().filter(unprocessedFiles);
|
||||
|
||||
modrinthMods.forEach(mod => processedFiles.add(mod));
|
||||
clientMods.push(...modrinthMods);
|
||||
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsProgress(processedFiles.size, files.length, "Modrinth API 检查");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.mixins) {
|
||||
logger.info("开始 Mixin 检查客户端模组");
|
||||
|
||||
const unprocessedFiles = files.filter(f => !processedFiles.has(f.filename));
|
||||
const mixinMods = await new MixinFilter().filter(unprocessedFiles);
|
||||
|
||||
mixinMods.forEach(mod => processedFiles.add(mod));
|
||||
clientMods.push(...mixinMods);
|
||||
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsProgress(processedFiles.size, files.length, "Mixin 检查");
|
||||
}
|
||||
}
|
||||
|
||||
if (this.config.hashes) {
|
||||
logger.info("开始 Hash 检查客户端模组");
|
||||
|
||||
const unprocessedFiles = files.filter(f => !processedFiles.has(f.filename));
|
||||
const hashMods = await new HashFilter().filter(unprocessedFiles);
|
||||
|
||||
clientMods.push(...hashMods);
|
||||
|
||||
if (this.messageWS) {
|
||||
this.messageWS.filterModsProgress(processedFiles.size, files.length, "Hash 检查");
|
||||
}
|
||||
}
|
||||
|
||||
const uniqueMods = [...new Set(clientMods)];
|
||||
logger.info("识别到客户端模组", { 数量: uniqueMods.length, 模组: uniqueMods });
|
||||
|
||||
if (uniqueMods.length > 0) {
|
||||
logger.debug("第一个模组路径", { 原始路径: uniqueMods[0], 绝对路径: path.resolve(uniqueMods[0]), cwd: process.cwd() });
|
||||
}
|
||||
|
||||
return uniqueMods;
|
||||
}
|
||||
}
|
||||
25
backend/src/dearth/index.ts
Normal file
@@ -0,0 +1,25 @@
|
||||
export { ModFilterService } from "./ModFilterService.js";
|
||||
export { FileExtractor } from "./utils/FileExtractor.js";
|
||||
export { FileOperator } from "./utils/FileOperator.js";
|
||||
export { ModCheckService } from "./ModCheckService.js";
|
||||
|
||||
export type {
|
||||
IFileInfo,
|
||||
IInfoFile,
|
||||
IMixinFile,
|
||||
IHashResponse,
|
||||
IProjectInfo,
|
||||
IDexpubCheckResult,
|
||||
IFilterStrategy,
|
||||
IFilterConfig,
|
||||
IModCheckResult,
|
||||
IModCheckConfig,
|
||||
ModSide
|
||||
} from "./types.js";
|
||||
|
||||
export {
|
||||
HashFilter,
|
||||
MixinFilter,
|
||||
DexpubFilter,
|
||||
ModrinthFilter
|
||||
} from "./strategies/index.js";
|
||||
80
backend/src/dearth/strategies/DexpubFilter.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import got, { Got } from "got";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { IFilterStrategy, IFileInfo, IDexpubCheckResult } from "../types.js";
|
||||
|
||||
export class DexpubFilter implements IFilterStrategy {
|
||||
name = "DexpubFilter";
|
||||
private got: Got;
|
||||
|
||||
constructor() {
|
||||
this.got = got.extend({
|
||||
prefixUrl: "https://galaxy.tianpao.top/",
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX",
|
||||
},
|
||||
responseType: "json",
|
||||
});
|
||||
}
|
||||
|
||||
async filter(files: IFileInfo[]): Promise<string[]> {
|
||||
const result = await this.checkDexpubForClientMods(files);
|
||||
logger.info("Galaxy Square 检查完成", { 服务端模组: result.serverMods, 客户端模组: result.clientMods });
|
||||
return result.clientMods;
|
||||
}
|
||||
|
||||
private async checkDexpubForClientMods(files: IFileInfo[]): Promise<IDexpubCheckResult> {
|
||||
const clientMods: string[] = [];
|
||||
const serverMods: string[] = [];
|
||||
const modIds: string[] = [];
|
||||
const map: Map<string, string> = new Map();
|
||||
|
||||
try {
|
||||
for (const file of files) {
|
||||
for (const info of file.infos) {
|
||||
try {
|
||||
const config = JSON.parse(info.data);
|
||||
const keys = Object.keys(config);
|
||||
|
||||
if (keys.includes("id")) {
|
||||
modIds.push(config.id);
|
||||
map.set(config.id, file.filename);
|
||||
} else if (keys.includes("mods")) {
|
||||
modIds.push(config.mods[0].modId);
|
||||
map.set(config.mods[0].modId, file.filename);
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("检查模组信息文件失败,文件名: " + file.filename, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const modIdToIsTypeMod = await this.got.post(`api/mod/check`, {
|
||||
json: {
|
||||
modids: modIds,
|
||||
}
|
||||
}).json<{ [modId: string]: boolean }>();
|
||||
|
||||
const modIdToIsTypeModKeys = Object.keys(modIdToIsTypeMod);
|
||||
|
||||
for (const modId of modIdToIsTypeModKeys) {
|
||||
const mapData = map.get(modId);
|
||||
if (!mapData) continue;
|
||||
|
||||
if (modIdToIsTypeMod[modId]) {
|
||||
clientMods.push(mapData);
|
||||
} else {
|
||||
serverMods.push(mapData);
|
||||
}
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("Dexpub 检查失败", error);
|
||||
}
|
||||
|
||||
return { serverMods, clientMods };
|
||||
}
|
||||
|
||||
async getServerMods(files: IFileInfo[]): Promise<string[]> {
|
||||
const result = await this.checkDexpubForClientMods(files);
|
||||
return result.serverMods;
|
||||
}
|
||||
}
|
||||
53
backend/src/dearth/strategies/HashFilter.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import got from "got";
|
||||
import { Utils } from "../../utils/utils.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { IFilterStrategy, IFileInfo, IHashResponse, IProjectInfo } from "../types.js";
|
||||
|
||||
export class HashFilter implements IFilterStrategy {
|
||||
name = "HashFilter";
|
||||
private utils: Utils;
|
||||
|
||||
constructor() {
|
||||
this.utils = new Utils();
|
||||
}
|
||||
|
||||
async filter(files: IFileInfo[]): Promise<string[]> {
|
||||
const hashToFilename = new Map<string, string>();
|
||||
const hashes = files.map(file => {
|
||||
hashToFilename.set(file.hash, file.filename);
|
||||
return file.hash;
|
||||
});
|
||||
|
||||
logger.debug("Checking mod hashes with Modrinth API", { fileCount: files.length });
|
||||
|
||||
try {
|
||||
const fileInfoResponse = await got.post(`${this.utils.modrinth_url}/v2/version_files`, {
|
||||
headers: { "User-Agent": "DeEarth", "Content-Type": "application/json" },
|
||||
json: { hashes, algorithm: "sha1" }
|
||||
}).json<IHashResponse>();
|
||||
|
||||
const projectIdToFilename = new Map<string, string>();
|
||||
const projectIds = Object.entries(fileInfoResponse)
|
||||
.map(([hash, info]) => {
|
||||
const filename = hashToFilename.get(hash);
|
||||
if (filename) projectIdToFilename.set(info.project_id, filename);
|
||||
return info.project_id;
|
||||
});
|
||||
|
||||
const projectsResponse = await got.get(`${this.utils.modrinth_url}/v2/projects?ids=${JSON.stringify(projectIds)}`, {
|
||||
headers: { "User-Agent": "DeEarth" }
|
||||
}).json<IProjectInfo[]>();
|
||||
|
||||
const clientMods = projectsResponse
|
||||
.filter(p => p.client_side === "required" && p.server_side === "unsupported")
|
||||
.map(p => projectIdToFilename.get(p.id))
|
||||
.filter(Boolean) as string[];
|
||||
|
||||
logger.debug("Hash check completed", { count: clientMods.length });
|
||||
return clientMods;
|
||||
} catch (error: any) {
|
||||
logger.error("Hash check failed", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
||||
27
backend/src/dearth/strategies/MixinFilter.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { IFilterStrategy, IFileInfo } from "../types.js";
|
||||
|
||||
export class MixinFilter implements IFilterStrategy {
|
||||
name = "MixinFilter";
|
||||
|
||||
async filter(files: IFileInfo[]): Promise<string[]> {
|
||||
const clientMods: string[] = [];
|
||||
|
||||
for (const file of files) {
|
||||
for (const mixin of file.mixins) {
|
||||
try {
|
||||
const config = JSON.parse(mixin.data);
|
||||
if (!config.mixins?.length && config.client?.length > 0 && !file.filename.includes("lib")) {
|
||||
clientMods.push(file.filename);
|
||||
break;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.warn("Failed to parse mixin config", { filename: file.filename, mixin: mixin.name, error: error.message });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("Mixins check completed", { count: clientMods.length });
|
||||
return [...new Set(clientMods)];
|
||||
}
|
||||
}
|
||||
107
backend/src/dearth/strategies/ModrinthFilter.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
import { IFilterStrategy, IFileInfo } from "../types.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
|
||||
interface IModrinthProject {
|
||||
client_side: string;
|
||||
server_side: string;
|
||||
project_type: string;
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
export class ModrinthFilter implements IFilterStrategy {
|
||||
name = "ModrinthFilter";
|
||||
private readonly API_BASE = "https://api.modrinth.com/v2";
|
||||
|
||||
private extractProjectId(infos: { name: string; data: string }[]): string | null {
|
||||
for (const info of infos) {
|
||||
if (info.name === "modrinth.index.json" || info.name === "modrinth.json") {
|
||||
try {
|
||||
const data = JSON.parse(info.data);
|
||||
return data.project_id || null;
|
||||
} catch {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private async fetchProjectInfo(projectIds: string[]): Promise<Map<string, IModrinthProject>> {
|
||||
const projectMap = new Map<string, IModrinthProject>();
|
||||
const batchSize = 50;
|
||||
|
||||
for (let i = 0; i < projectIds.length; i += batchSize) {
|
||||
const batch = projectIds.slice(i, i + batchSize);
|
||||
const idsParam = batch.join(",");
|
||||
|
||||
try {
|
||||
const response = await fetch(`${this.API_BASE}/projects?ids=${encodeURIComponent(idsParam)}`, {
|
||||
headers: {
|
||||
'User-Agent': 'DeEarthX-V3/1.0.0'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const projects: Array<any> = await response.json();
|
||||
|
||||
for (const project of projects) {
|
||||
if (project && project.id) {
|
||||
projectMap.set(project.id, {
|
||||
client_side: project.client_side,
|
||||
server_side: project.server_side,
|
||||
project_type: project.project_type,
|
||||
categories: project.categories || []
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error("获取 Modrinth 项目信息失败", { error, batchSize });
|
||||
}
|
||||
}
|
||||
|
||||
return projectMap;
|
||||
}
|
||||
|
||||
private isClientMod(project: IModrinthProject): boolean {
|
||||
const clientSide = project.client_side;
|
||||
const serverSide = project.server_side;
|
||||
|
||||
return (
|
||||
clientSide === "required" ||
|
||||
(clientSide === "optional" && serverSide === "unsupported")
|
||||
);
|
||||
}
|
||||
|
||||
async filter(files: IFileInfo[]): Promise<string[]> {
|
||||
const clientMods: string[] = [];
|
||||
const projectIds: Array<{ filename: string; projectId: string }> = [];
|
||||
|
||||
for (const file of files) {
|
||||
const projectId = this.extractProjectId(file.infos);
|
||||
if (projectId) {
|
||||
projectIds.push({ filename: file.filename, projectId });
|
||||
}
|
||||
}
|
||||
|
||||
if (projectIds.length === 0) {
|
||||
logger.info("未找到 Modrinth 项目 ID");
|
||||
return clientMods;
|
||||
}
|
||||
|
||||
logger.info(`找到 ${projectIds.length} 个 Modrinth 项目`, { 数量: projectIds.length });
|
||||
|
||||
const uniqueProjectIds = [...new Set(projectIds.map(p => p.projectId))];
|
||||
const projectMap = await this.fetchProjectInfo(uniqueProjectIds);
|
||||
|
||||
for (const { filename, projectId } of projectIds) {
|
||||
const project = projectMap.get(projectId);
|
||||
if (project && this.isClientMod(project)) {
|
||||
clientMods.push(filename);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("Modrinth 筛选完成", { 客户端模组数: clientMods.length });
|
||||
return clientMods;
|
||||
}
|
||||
}
|
||||
4
backend/src/dearth/strategies/index.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export { HashFilter } from "./HashFilter.js";
|
||||
export { MixinFilter } from "./MixinFilter.js";
|
||||
export { DexpubFilter } from "./DexpubFilter.js";
|
||||
export { ModrinthFilter } from "./ModrinthFilter.js";
|
||||
126
backend/src/dearth/types.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 模组筛选模块 - 类型定义
|
||||
*/
|
||||
|
||||
/**
|
||||
* Mixin 配置文件信息
|
||||
*/
|
||||
export interface IMixinFile {
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模组信息文件
|
||||
*/
|
||||
export interface IInfoFile {
|
||||
name: string;
|
||||
data: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模组文件信息
|
||||
*/
|
||||
export interface IFileInfo {
|
||||
filename: string;
|
||||
hash: string;
|
||||
mixins: IMixinFile[];
|
||||
infos: IInfoFile[];
|
||||
fileData?: Buffer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Modrinth Hash 响应
|
||||
*/
|
||||
export interface IHashResponse {
|
||||
[hash: string]: { project_id: string };
|
||||
}
|
||||
|
||||
/**
|
||||
* Modrinth 项目信息
|
||||
*/
|
||||
export interface IProjectInfo {
|
||||
id: string;
|
||||
client_side: string;
|
||||
server_side: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dexpub 检查结果
|
||||
*/
|
||||
export interface IDexpubCheckResult {
|
||||
serverMods: string[];
|
||||
clientMods: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选策略接口
|
||||
*/
|
||||
export interface IFilterStrategy {
|
||||
/**
|
||||
* 策略名称
|
||||
*/
|
||||
name: string;
|
||||
|
||||
/**
|
||||
* 筛选客户端模组
|
||||
* @param files 模组文件信息数组
|
||||
* @returns 客户端模组文件名数组
|
||||
*/
|
||||
filter(files: IFileInfo[]): Promise<string[]>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 筛选配置
|
||||
*/
|
||||
export interface IFilterConfig {
|
||||
hashes: boolean;
|
||||
dexpub: boolean;
|
||||
mixins: boolean;
|
||||
modrinth: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模组兼容性类型
|
||||
*/
|
||||
export type ModSide = "required" | "optional" | "unsupported" | "unknown";
|
||||
|
||||
/**
|
||||
* 单个检查方法的结果
|
||||
*/
|
||||
export interface ISingleCheckResult {
|
||||
source: string;
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
checked: boolean;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模组检查结果 - 包含所有检查方法的结果
|
||||
*/
|
||||
export interface IModCheckResult {
|
||||
filename: string;
|
||||
filePath: string;
|
||||
clientSide: ModSide;
|
||||
serverSide: ModSide;
|
||||
source: string;
|
||||
checked: boolean;
|
||||
errors?: string[];
|
||||
allResults: ISingleCheckResult[];
|
||||
modId?: string;
|
||||
iconUrl?: string;
|
||||
description?: string;
|
||||
author?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 模组检查配置
|
||||
*/
|
||||
export interface IModCheckConfig {
|
||||
enableDexpub: boolean;
|
||||
enableModrinth: boolean;
|
||||
enableMixin: boolean;
|
||||
enableHash: boolean;
|
||||
timeout: number;
|
||||
}
|
||||
57
backend/src/dearth/utils/FileExtractor.ts
Normal file
@@ -0,0 +1,57 @@
|
||||
import fs from "node:fs";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { JarParser } from "../../utils/jar-parser.js";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
import { IFileInfo } from "../types.js";
|
||||
|
||||
export class FileExtractor {
|
||||
private readonly modsPath: string;
|
||||
|
||||
constructor(modsPath: string) {
|
||||
this.modsPath = path.isAbsolute(modsPath) ? modsPath : path.resolve(modsPath);
|
||||
}
|
||||
|
||||
async extractFilesInfo(): Promise<IFileInfo[]> {
|
||||
const jarFiles = this.getJarFiles();
|
||||
const files: IFileInfo[] = [];
|
||||
|
||||
logger.info("获取文件信息", { 文件数量: jarFiles.length });
|
||||
|
||||
for (const jarFilename of jarFiles) {
|
||||
const fullPath = path.join(this.modsPath, jarFilename);
|
||||
|
||||
try {
|
||||
let fileData: Buffer | null = null;
|
||||
try {
|
||||
fileData = fs.readFileSync(fullPath);
|
||||
const mixins = await JarParser.extractMixins(fileData);
|
||||
const infos = await JarParser.extractModInfo(fileData);
|
||||
|
||||
files.push({
|
||||
filename: fullPath,
|
||||
hash: crypto.createHash('sha1').update(fileData).digest('hex'),
|
||||
mixins,
|
||||
infos,
|
||||
});
|
||||
|
||||
logger.debug("文件已处理", { 文件名: fullPath, 绝对路径: path.resolve(fullPath), Mixin数量: mixins.length });
|
||||
} finally {
|
||||
fileData = null;
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("处理文件时出错", { 文件名: fullPath, 错误: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug("文件信息收集完成", { 已处理文件: files.length });
|
||||
return files;
|
||||
}
|
||||
|
||||
private getJarFiles(): string[] {
|
||||
if (!fs.existsSync(this.modsPath)) {
|
||||
fs.mkdirSync(this.modsPath, { recursive: true });
|
||||
}
|
||||
return fs.readdirSync(this.modsPath).filter(f => f.endsWith(".jar"));
|
||||
}
|
||||
}
|
||||
61
backend/src/dearth/utils/FileOperator.ts
Normal file
@@ -0,0 +1,61 @@
|
||||
import fs from "node:fs/promises";
|
||||
import fsSync from "node:fs";
|
||||
import path from "node:path";
|
||||
import { logger } from "../../utils/logger.js";
|
||||
|
||||
export class FileOperator {
|
||||
private readonly movePath: string;
|
||||
|
||||
constructor(movePath: string) {
|
||||
this.movePath = movePath;
|
||||
}
|
||||
|
||||
async moveClientSideMods(clientMods: string[]): Promise<{ success: number; error: number; skipped: number }> {
|
||||
if (!clientMods.length) {
|
||||
logger.info("No client-side mods to move");
|
||||
return { success: 0, error: 0, skipped: 0 };
|
||||
}
|
||||
|
||||
const absoluteMovePath = path.isAbsolute(this.movePath) ? this.movePath : path.resolve(this.movePath);
|
||||
logger.debug("Target directory", { path: absoluteMovePath, exists: fsSync.existsSync(absoluteMovePath) });
|
||||
|
||||
if (!fsSync.existsSync(absoluteMovePath)) {
|
||||
logger.debug("Creating target directory", { path: absoluteMovePath });
|
||||
await fs.mkdir(absoluteMovePath, { recursive: true });
|
||||
}
|
||||
|
||||
let successCount = 0, errorCount = 0, skippedCount = 0;
|
||||
|
||||
for (const sourcePath of clientMods) {
|
||||
try {
|
||||
const absoluteSourcePath = path.isAbsolute(sourcePath) ? sourcePath : path.resolve(sourcePath);
|
||||
|
||||
logger.debug("Checking file", { originalPath: sourcePath, resolvedPath: absoluteSourcePath, cwd: process.cwd() });
|
||||
|
||||
try {
|
||||
await fs.access(absoluteSourcePath);
|
||||
} catch (accessError) {
|
||||
logger.warn("File does not exist, skipping", { path: absoluteSourcePath, error: (accessError as Error).message });
|
||||
skippedCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
const filename = path.basename(absoluteSourcePath);
|
||||
const targetPath = path.join(absoluteMovePath, filename);
|
||||
|
||||
logger.info("Moving file", { source: absoluteSourcePath, target: targetPath, filename: filename });
|
||||
|
||||
await fs.copyFile(absoluteSourcePath, targetPath);
|
||||
await fs.unlink(absoluteSourcePath);
|
||||
|
||||
successCount++;
|
||||
} catch (error: any) {
|
||||
logger.error("Failed to move file", { source: sourcePath, error: error.message, code: error.code });
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("File movement completed", { total: clientMods.length, success: successCount, error: errorCount, skipped: skippedCount });
|
||||
return { success: successCount, error: errorCount, skipped: skippedCount };
|
||||
}
|
||||
}
|
||||
90
backend/src/galaxy.ts
Normal file
@@ -0,0 +1,90 @@
|
||||
import express from "express";
|
||||
import toml from "smol-toml";
|
||||
import multer, { Multer } from "multer";
|
||||
import AdmZip from "adm-zip";
|
||||
import { logger } from "./utils/logger.js";
|
||||
import got, { Got } from "got";
|
||||
|
||||
export class Galaxy {
|
||||
private readonly upload: multer.Multer;
|
||||
got: Got;
|
||||
constructor() {
|
||||
this.upload = multer({
|
||||
storage: multer.memoryStorage(),
|
||||
limits: {
|
||||
fileSize: 2 * 1024 * 1024 * 1024,
|
||||
files: 10
|
||||
}
|
||||
})
|
||||
this.got = got.extend({
|
||||
prefixUrl: "https://galaxy.tianpao.top/ ",
|
||||
//prefixUrl: "http://localhost:3000/",
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX",
|
||||
},
|
||||
responseType: "json",
|
||||
});
|
||||
}
|
||||
getRouter() {
|
||||
const router = express.Router();
|
||||
router.use(express.json()); // 解析 JSON 请求体
|
||||
router.post("/upload",this.upload.array("files"), (req, res) => {
|
||||
const files = req.files as Express.Multer.File[];
|
||||
if(!files || files.length === 0){
|
||||
res.status(400).json({ status: 400, message: "未上传文件" });
|
||||
return;
|
||||
}
|
||||
const modids = this.getModids(files);
|
||||
logger.info("已上传模组 ID", { 模组ID: modids });
|
||||
res.json({modids}).end();
|
||||
});
|
||||
router.post("/submit/:type",(req,res)=>{
|
||||
const type = req.params.type;
|
||||
if(type !== "server" && type !== "client"){
|
||||
res.status(400).json({ status: 400, message: "无效的类型参数" });
|
||||
return;
|
||||
}
|
||||
const modid = req.body.modids as string;
|
||||
if(!modid){
|
||||
res.status(400).json({ status: 400, message: "未提供 modid" });
|
||||
return;
|
||||
}
|
||||
this.got.post(`api/mod/submit/${type}`,{
|
||||
json: {
|
||||
modid,
|
||||
}
|
||||
}).then((response)=>{
|
||||
logger.info(`已成功提交 ${type} 端模组 ID`, response.body);
|
||||
res.json(response.body).end();
|
||||
}).catch((error)=>{
|
||||
logger.error(`提交 ${type} 端模组 ID 失败`, error);
|
||||
res.status(500).json({ status: 500, message: "提交模组 ID 失败" });
|
||||
})
|
||||
})
|
||||
return router;
|
||||
}
|
||||
|
||||
getModids(files:Express.Multer.File[]):string[] {
|
||||
let modid:string[] = [];
|
||||
for(const file of files){
|
||||
const zip = new AdmZip(file.buffer);
|
||||
const entries = zip.getEntries();
|
||||
for(const entry of entries){
|
||||
if(entry.entryName.endsWith("mods.toml")){
|
||||
const content = entry.getData().toString("utf8");
|
||||
const config = toml.parse(content) as any;
|
||||
modid.push(config.mods[0].modId as string)
|
||||
}else if(entry.entryName.endsWith("neoforge.mods.toml")){
|
||||
const content = entry.getData().toString("utf8");
|
||||
const config = toml.parse(content) as any;
|
||||
modid.push(config.mods[0].modId as string)
|
||||
}else if(entry.entryName.endsWith("fabric.mod.json")){
|
||||
const content = entry.getData().toString("utf8");
|
||||
const config = JSON.parse(content);
|
||||
modid.push(config.id as string)
|
||||
}
|
||||
}
|
||||
}
|
||||
return modid
|
||||
}
|
||||
}
|
||||
27
backend/src/main.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
import { Config } from "./utils/config.js";
|
||||
import { Core } from "./core.js";
|
||||
|
||||
// 创建核心实例并启动服务
|
||||
const config = Config.getConfig();
|
||||
const core = new Core(config);
|
||||
|
||||
core.start();
|
||||
|
||||
// ==================== 调试/测试代码区域(已注释) ====================
|
||||
|
||||
// 版本比较测试
|
||||
// console.log(version_compare("1.18.1", "1.16.5"))
|
||||
|
||||
// DeEarth 模块测试
|
||||
// await new DeEarth("./mods").Main()
|
||||
|
||||
// Dex 函数定义示例
|
||||
// async function Dex(buffer: Buffer) {
|
||||
// }
|
||||
|
||||
// 模组加载器安装测试
|
||||
// new Forge("1.20.1", "47.3.10").setup() // 安装 Forge 服务端
|
||||
// await new NeoForge("1.21.1", "21.1.1").setup() // 安装 NeoForge 服务端
|
||||
// await new Minecraft("forge", "1.20.1", "0").setup() // 安装 Minecraft + Forge
|
||||
// await new Minecraft("forge", "1.16.5", "0").setup() // 安装 Minecraft + Forge (1.16.5)
|
||||
// await new Fabric("1.20.1", "0.17.2").setup() // 安装 Fabric 服务端
|
||||
123
backend/src/modloader/fabric.ts
Normal file
@@ -0,0 +1,123 @@
|
||||
import got, { Got } from "got";
|
||||
import fs from "node:fs";
|
||||
import { execPromise, fastdownload, verifySHA1, calculateSHA1 } from "../utils/utils.js";
|
||||
import { Config } from "../utils/config.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
interface ILatestLoader {
|
||||
url: string;
|
||||
stable: boolean;
|
||||
}
|
||||
|
||||
interface IServer {
|
||||
libraries: {
|
||||
name: string;
|
||||
}[];
|
||||
}
|
||||
|
||||
export class Fabric {
|
||||
minecraft: string;
|
||||
loaderVersion: string;
|
||||
got: Got;
|
||||
path: string;
|
||||
|
||||
constructor(minecraft: string, loaderVersion: string, path: string) {
|
||||
this.minecraft = minecraft;
|
||||
this.loaderVersion = loaderVersion;
|
||||
this.path = path;
|
||||
this.got = got.extend({
|
||||
prefixUrl: "https://bmclapi2.bangbang93.com/",
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX"
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setup(): Promise<void> {
|
||||
await this.installer();
|
||||
const config = Config.getConfig();
|
||||
if (config.mirror.bmclapi) {
|
||||
await this.libraries();
|
||||
}
|
||||
await this.install();
|
||||
await this.wshell();
|
||||
}
|
||||
|
||||
async install() {
|
||||
const config = Config.getConfig();
|
||||
const javaCmd = config.javaPath || 'java';
|
||||
await execPromise(`${javaCmd} -jar fabric-installer.jar server -dir . -mcversion ${this.minecraft} -loader ${this.loaderVersion}`, {
|
||||
cwd: this.path
|
||||
}).catch(e => console.log(e));
|
||||
}
|
||||
|
||||
private async wshell() {
|
||||
const config = Config.getConfig();
|
||||
const javaCmd = config.javaPath || 'java';
|
||||
const cmd = `${javaCmd} -jar fabric-server-launch.jar`;
|
||||
await fs.promises.writeFile(`${this.path}/run.bat`, `@echo off\n${cmd}`);
|
||||
await fs.promises.writeFile(`${this.path}/run.sh`, `#!/bin/bash\n${cmd}`);
|
||||
}
|
||||
|
||||
async libraries() {
|
||||
const config = Config.getConfig();
|
||||
const res = await this.got.get(`fabric-meta/v2/versions/loader/${this.minecraft}/${this.loaderVersion}/server/json`).json<IServer>();
|
||||
const _downlist: [string, string, string?][] = [];
|
||||
res.libraries.forEach(e => {
|
||||
const path = this.MTP(e.name);
|
||||
_downlist.push([`https://bmclapi2.bangbang93.com/maven/${path}`, `${this.path}/libraries/${path}`]);
|
||||
});
|
||||
|
||||
await fastdownload(_downlist as any);
|
||||
|
||||
if (config.mirror.bmclapi) {
|
||||
logger.info(`验证 ${_downlist.length} 个 Fabric 库文件的完整性...`);
|
||||
let verifiedCount = 0;
|
||||
for (const [, filePath] of _downlist) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
const hash = calculateSHA1(filePath);
|
||||
logger.debug(`${filePath}: SHA1 = ${hash}`);
|
||||
verifiedCount++;
|
||||
}
|
||||
}
|
||||
logger.info(`Fabric 库文件验证完成,共验证 ${verifiedCount}/${_downlist.length} 个文件`);
|
||||
}
|
||||
}
|
||||
|
||||
async installer() {
|
||||
let downurl = "";
|
||||
const res = await this.got.get("fabric-meta/v2/versions/installer").json<ILatestLoader[]>();
|
||||
res.forEach(e => {
|
||||
if (e.stable) {
|
||||
downurl = e.url;
|
||||
return;
|
||||
}
|
||||
});
|
||||
|
||||
const filePath = `${this.path}/fabric-installer.jar`;
|
||||
await fastdownload([downurl, filePath]);
|
||||
|
||||
if (fs.existsSync(filePath)) {
|
||||
const hash = calculateSHA1(filePath);
|
||||
logger.debug(`Fabric installer 下载完成,SHA1: ${hash}`);
|
||||
}
|
||||
}
|
||||
|
||||
private MTP(string: string) {
|
||||
const mjp = string.replace(/^\[|\]$/g, '');
|
||||
const OriginalName = mjp.split("@")[0];
|
||||
const x = OriginalName.split(":");
|
||||
const _mappingType = mjp.split('@')[1];
|
||||
let mappingType = "";
|
||||
if (_mappingType) {
|
||||
mappingType = _mappingType;
|
||||
} else {
|
||||
mappingType = "jar";
|
||||
}
|
||||
if (x[3]) {
|
||||
return `${x[0].replace(/\./g, '/')}/${x[1]}/${x[2]}/${x[1]}-${x[2]}-${x[3]}.${mappingType}`;
|
||||
} else {
|
||||
return `${x[0].replace(/\./g, '/')}/${x[1]}/${x[2]}/${x[1]}-${x[2]}.${mappingType}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
187
backend/src/modloader/forge.ts
Normal file
@@ -0,0 +1,187 @@
|
||||
import got, { Got } from "got";
|
||||
import fs from "node:fs";
|
||||
import fse from "fs-extra";
|
||||
import { execPromise, fastdownload, version_compare, verifySHA1 } from "../utils/utils.js";
|
||||
import { Azip } from "../utils/ziplib.js";
|
||||
import { Config } from "../utils/config.js";
|
||||
import { logger } from "../utils/logger.js";
|
||||
|
||||
interface IForge {
|
||||
data: {
|
||||
MOJMAPS: {
|
||||
server: string;
|
||||
};
|
||||
MAPPINGS: {
|
||||
server: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface IVersion {
|
||||
downloads: {
|
||||
server_mappings: {
|
||||
url: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface IForgeFile {
|
||||
format: string;
|
||||
category: string;
|
||||
hash: string;
|
||||
_id: string;
|
||||
}
|
||||
|
||||
interface IForgeBuild {
|
||||
branch: string;
|
||||
build: number;
|
||||
mcversion: string;
|
||||
modified: string;
|
||||
version: string;
|
||||
_id: string;
|
||||
files: IForgeFile[];
|
||||
}
|
||||
|
||||
export class Forge {
|
||||
minecraft: string;
|
||||
loaderVersion: string;
|
||||
got: Got;
|
||||
path: string;
|
||||
|
||||
constructor(minecraft: string, loaderVersion: string, path: string) {
|
||||
this.minecraft = minecraft;
|
||||
this.loaderVersion = loaderVersion;
|
||||
this.path = path;
|
||||
const config = Config.getConfig();
|
||||
this.got = got.extend({
|
||||
headers: { "User-Agent": "DeEarthX" },
|
||||
hooks: {
|
||||
init: [
|
||||
(options) => {
|
||||
if (config.mirror.bmclapi) {
|
||||
options.prefixUrl = "https://bmclapi2.bangbang93.com";
|
||||
} else {
|
||||
options.prefixUrl = "http://maven.minecraftforge.net/";
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.installer();
|
||||
const config = Config.getConfig();
|
||||
if (config.mirror.bmclapi) {
|
||||
await this.library();
|
||||
}
|
||||
await this.install();
|
||||
if (version_compare(this.minecraft, "1.18") === -1) {
|
||||
await this.wshell();
|
||||
}
|
||||
}
|
||||
|
||||
async library() {
|
||||
const _downlist: [string, string][] = [];
|
||||
const data = await fs.promises.readFile(`${this.path}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`);
|
||||
const zip = Azip(data);
|
||||
|
||||
for await (const entry of zip) {
|
||||
if (entry.entryName === "version.json" || entry.entryName === "install_profile.json") {
|
||||
JSON.parse((entry.getData()).toString()).libraries.forEach(async (e: any) => {
|
||||
const t = e.downloads.artifact.path;
|
||||
_downlist.push([`https://bmclapi2.bangbang93.com/maven/${t}`, `${this.path}/libraries/${t}`]);
|
||||
});
|
||||
}
|
||||
|
||||
if (entry.entryName === "install_profile.json") {
|
||||
const json = JSON.parse((entry.getData()).toString()) as IForge;
|
||||
const vjson = await this.got.get(`version/${this.minecraft}/json`).json<IVersion>();
|
||||
console.log(`${new URL(vjson.downloads.server_mappings.url).pathname}`);
|
||||
const mojpath = this.MTP(json.data.MOJMAPS.server);
|
||||
_downlist.push([`https://bmclapi2.bangbang93.com/${new URL(vjson.downloads.server_mappings.url).pathname.slice(1)}`, `${this.path}/libraries/${mojpath}`]);
|
||||
|
||||
const mappingobj = json.data.MAPPINGS.server;
|
||||
const path = this.MTP(mappingobj.replace(":mappings@txt", "@zip"));
|
||||
_downlist.push([`https://bmclapi2.bangbang93.com/maven/${path}`, `${this.path}/libraries/${path}`]);
|
||||
}
|
||||
}
|
||||
|
||||
const downlist = [...new Set(_downlist)];
|
||||
await fastdownload(downlist);
|
||||
}
|
||||
|
||||
async install() {
|
||||
const config = Config.getConfig();
|
||||
const javaCmd = config.javaPath || 'java';
|
||||
let cmd = `${javaCmd} -jar forge-${this.minecraft}-${this.loaderVersion}-installer.jar --installServer`;
|
||||
if (config.mirror.bmclapi) {
|
||||
cmd += ` --mirror https://bmclapi2.bangbang93.com/maven/`;
|
||||
}
|
||||
await execPromise(cmd, { cwd: this.path }).catch((e) => {
|
||||
logger.error(`Forge 安装失败: ${e}`);
|
||||
throw e;
|
||||
});
|
||||
}
|
||||
|
||||
async installer() {
|
||||
const config = Config.getConfig();
|
||||
let url = `forge/download?mcversion=${this.minecraft}&version=${this.loaderVersion}&category=installer&format=jar`;
|
||||
let expectedHash: string | undefined;
|
||||
|
||||
if (config.mirror?.bmclapi) {
|
||||
try {
|
||||
const forgeInfo = await this.got.get(`forge/minecraft/${this.minecraft}`).json<IForgeBuild[]>();
|
||||
const forgeVersion = forgeInfo.find(f => f.version === this.loaderVersion);
|
||||
if (forgeVersion) {
|
||||
const installerFile = forgeVersion.files.find(f => f.category === 'installer' && f.format === 'jar');
|
||||
if (installerFile) {
|
||||
expectedHash = installerFile.hash;
|
||||
logger.debug(`获取到 Forge installer hash: ${expectedHash}`);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
logger.warn(`获取 Forge hash 信息失败,将跳过 hash 验证`, error);
|
||||
}
|
||||
} else {
|
||||
url = `net/minecraftforge/forge/${this.minecraft}-${this.loaderVersion}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`;
|
||||
}
|
||||
|
||||
const res = (await this.got.get(url)).rawBody;
|
||||
const filePath = `${this.path}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`;
|
||||
await fse.outputFile(filePath, res);
|
||||
|
||||
if (expectedHash) {
|
||||
if (!verifySHA1(filePath, expectedHash)) {
|
||||
logger.warn(`Forge installer hash 验证失败,删除文件并重试`);
|
||||
fs.unlinkSync(filePath);
|
||||
const res2 = (await this.got.get(url)).rawBody;
|
||||
await fse.outputFile(filePath, res2);
|
||||
|
||||
if (!verifySHA1(filePath, expectedHash)) {
|
||||
throw new Error(`Forge installer hash 验证失败,文件可能已损坏`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async wshell() {
|
||||
const config = Config.getConfig();
|
||||
const javaCmd = config.javaPath || 'java';
|
||||
const cmd = `${javaCmd} -jar forge-${this.minecraft}-${this.loaderVersion}.jar`;
|
||||
await fs.promises.writeFile(`${this.path}/run.bat`, `@echo off\n${cmd}`);
|
||||
await fs.promises.writeFile(`${this.path}/run.sh`, `#!/bin/bash\n${cmd}`);
|
||||
}
|
||||
|
||||
private MTP(string: string) {
|
||||
const mjp = string.replace(/^\[|\]$/g, '');
|
||||
const OriginalName = mjp.split("@")[0];
|
||||
const x = OriginalName.split(":");
|
||||
const mappingType = mjp.split('@')[1];
|
||||
if (x[3]) {
|
||||
return `${x[0].replace(/\./g, '/')}/${x[1]}/${x[2]}/${x[1]}-${x[2]}-${x[3]}.${mappingType}`;
|
||||
} else {
|
||||
return `${x[0].replace(/\./g, '/')}/${x[1]}/${x[2]}/${x[1]}-${x[2]}.${mappingType}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
129
backend/src/modloader/index.ts
Normal file
@@ -0,0 +1,129 @@
|
||||
import { Fabric } from "./fabric.js";
|
||||
import { Forge } from "./forge.js";
|
||||
import { Minecraft } from "./minecraft.js";
|
||||
import { NeoForge } from "./neoforge.js";
|
||||
import fs from "node:fs";
|
||||
import { MessageWS } from "../utils/ws.js";
|
||||
import { getAppDir } from "../utils/utils.js";
|
||||
|
||||
interface XModloader {
|
||||
setup(): Promise<void>;
|
||||
installer(): Promise<void>;
|
||||
}
|
||||
|
||||
export function modloader(ml: string, mcv: string, mlv: string, path: string): XModloader {
|
||||
switch (ml) {
|
||||
case "fabric":
|
||||
case "fabric-loader":
|
||||
return new Fabric(mcv, mlv, path);
|
||||
case "forge":
|
||||
return new Forge(mcv, mlv, path);
|
||||
case "neoforge":
|
||||
return new NeoForge(mcv, mlv, path);
|
||||
default:
|
||||
return new Minecraft(ml, mcv, mlv, path);
|
||||
}
|
||||
}
|
||||
|
||||
export async function mlsetup(ml: string, mcv: string, mlv: string, path: string, messageWS?: MessageWS, template?: string): Promise<void> {
|
||||
const totalSteps = template && template !== '0' ? 1 : (template ? 3 : 2);
|
||||
|
||||
try {
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallStart("Server Installation", mcv, ml, mlv);
|
||||
}
|
||||
|
||||
if (template && template !== '0') {
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallStep(`Applying Template: ${template}`, 1, totalSteps);
|
||||
}
|
||||
|
||||
const templateModule = await import('../template/index.js');
|
||||
const TemplateManager = (templateModule as any).TemplateManager;
|
||||
const templateManager = new TemplateManager();
|
||||
const templates = await templateManager.getTemplates();
|
||||
const selectedTemplate = templates.find((t: { id: string; metadata: any }) => t.id === template);
|
||||
|
||||
if (selectedTemplate) {
|
||||
const pathModule = await import('node:path');
|
||||
const templatePath = pathModule.join(getAppDir(), "templates", template);
|
||||
const fs = await import('node:fs/promises');
|
||||
|
||||
try {
|
||||
const dataPath = pathModule.join(templatePath, 'data');
|
||||
const files = await fs.readdir(dataPath, { recursive: true });
|
||||
|
||||
for (const file of files) {
|
||||
const srcPath = pathModule.join(dataPath, file);
|
||||
const stat = await fs.stat(srcPath);
|
||||
|
||||
if (stat.isFile()) {
|
||||
const destPath = pathModule.join(path, file);
|
||||
const destDir = pathModule.dirname(destPath);
|
||||
await fs.mkdir(destDir, { recursive: true });
|
||||
await fs.copyFile(srcPath, destPath);
|
||||
}
|
||||
}
|
||||
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallProgress(`Applied Template: ${template}`, 100);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Failed to apply template ${template}:`, error);
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallError(`Failed to apply template: ${error instanceof Error ? error.message : String(error)}`);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console.warn(`Template ${template} not found`);
|
||||
}
|
||||
} else {
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallStep("Installing Minecraft Server", 1, totalSteps);
|
||||
}
|
||||
|
||||
const minecraft = new Minecraft(ml, mcv, mlv, path);
|
||||
await minecraft.setup();
|
||||
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallProgress("Installing Minecraft Server", 100);
|
||||
messageWS.serverInstallStep(`Installing ${ml} Loader`, 2, totalSteps);
|
||||
}
|
||||
|
||||
await modloader(ml, mcv, mlv, path).setup();
|
||||
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallProgress(`Installing ${ml} Loader`, 100);
|
||||
}
|
||||
|
||||
if (template && template === '0') {
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallStep(`No template selected, using official mod loader`, 3, totalSteps);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (messageWS) {
|
||||
messageWS.serverInstallError(error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export async function dinstall(ml: string, mcv: string, mlv: string, path: string): Promise<void> {
|
||||
await modloader(ml, mcv, mlv, path).installer();
|
||||
|
||||
let cmd = '';
|
||||
if (ml === 'forge' || ml === 'neoforge') {
|
||||
cmd = `java -jar forge-${mcv}-${mlv}-installer.jar --installServer`;
|
||||
} else if (ml === 'fabric' || ml === 'fabric-loader') {
|
||||
await fs.promises.writeFile(`${path}/run.bat`,`@echo off\njava -jar fabric-server-launch.jar\n`)
|
||||
await fs.promises.writeFile(`${path}/run.sh`,`#!/bin/bash\njava -jar fabric-server-launch.jar\n`)
|
||||
cmd = `java -jar fabric-installer.jar server -dir . -mcversion ${mcv} -loader ${mlv} -downloadMinecraft`;
|
||||
}
|
||||
|
||||
if (cmd) {
|
||||
await fs.promises.writeFile(`${path}/install.bat`, `@echo off\n${cmd}\necho Install Successfully,Enter Some Key to Exit!\npause\n`);
|
||||
await fs.promises.writeFile(`${path}/install.sh`, `#!/bin/bash\n${cmd}\n`);
|
||||
}
|
||||
}
|
||||
102
backend/src/modloader/minecraft.ts
Normal file
@@ -0,0 +1,102 @@
|
||||
import fs from "node:fs";
|
||||
import { fastdownload, version_compare } from "../utils/utils.js";
|
||||
import got from "got";
|
||||
import p from "path";
|
||||
import { Azip } from "../utils/ziplib.js";
|
||||
import { Config } from "../utils/config.js";
|
||||
|
||||
interface ILInfo {
|
||||
libraries: {
|
||||
downloads: {
|
||||
artifact: {
|
||||
path: string;
|
||||
};
|
||||
};
|
||||
}[];
|
||||
}
|
||||
|
||||
export class Minecraft {
|
||||
loader: string;
|
||||
minecraft: string;
|
||||
loaderVersion: string;
|
||||
path: string;
|
||||
|
||||
constructor(loader: string, minecraft: string, lv: string, path: string) {
|
||||
this.path = path;
|
||||
this.loader = loader;
|
||||
this.minecraft = minecraft;
|
||||
this.loaderVersion = lv;
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.eula();
|
||||
const config = Config.getConfig();
|
||||
if (!config.mirror.bmclapi) {
|
||||
return;
|
||||
}
|
||||
switch (this.loader) {
|
||||
case "forge":
|
||||
await this.forge_setup();
|
||||
break;
|
||||
case "neoforge":
|
||||
await this.forge_setup();
|
||||
break;
|
||||
case "fabric":
|
||||
await this.fabric_setup();
|
||||
break;
|
||||
case "fabric-loader":
|
||||
await this.fabric_setup();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
async forge_setup() {
|
||||
if (version_compare(this.minecraft, "1.18") === 1) {
|
||||
const mcpath = `${this.path}/libraries/net/minecraft/server/${this.minecraft}/server-${this.minecraft}.jar`;
|
||||
await fastdownload([`https://bmclapi2.bangbang93.com/version/${this.minecraft}/server`, mcpath]);
|
||||
const zip = await Azip(await fs.promises.readFile(mcpath));
|
||||
for await (const entry of zip) {
|
||||
if (entry.entryName.startsWith("META-INF/libraries/") && !entry.entryName.endsWith("/")) {
|
||||
console.log(entry.entryName);
|
||||
const data = entry.getData();
|
||||
const filepath = `${this.path}/libraries/${entry.entryName.replace("META-INF/libraries/", "")}`;
|
||||
const dir = p.dirname(filepath);
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
await fs.promises.writeFile(filepath, data);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const lowv = `${this.path}/minecraft_server.${this.minecraft}.jar`;
|
||||
const dmc = fastdownload([`https://bmclapi2.bangbang93.com/version/${this.minecraft}/server`, lowv]);
|
||||
|
||||
const download = (async () => {
|
||||
console.log("并行");
|
||||
const json = await got.get(`https://bmclapi2.bangbang93.com/version/${this.minecraft}/json`, {
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX"
|
||||
}
|
||||
}).json<ILInfo>();
|
||||
|
||||
await Promise.all(json.libraries.map(async e => {
|
||||
const path = e.downloads.artifact.path;
|
||||
await fastdownload([`https://bmclapi2.bangbang93.com/maven/${path}`, `${this.path}/libraries/${path}`]);
|
||||
}));
|
||||
})();
|
||||
|
||||
await Promise.all([dmc, download]);
|
||||
}
|
||||
}
|
||||
|
||||
async fabric_setup() {
|
||||
const mcpath = `${this.path}/server.jar`;
|
||||
await fastdownload([`https://bmclapi2.bangbang93.com/version/${this.minecraft}/server`, mcpath]);
|
||||
}
|
||||
|
||||
async installer() {
|
||||
}
|
||||
|
||||
async eula() {
|
||||
const context = `#By changing the setting below to TRUE you are indicating your agreement to our EULA (https://aka.ms/MinecraftEULA).\n#Spawn by DeEarthX(QQgroup:559349662) Tianpao:(https://space.bilibili.com/1728953419)\neula=true`;
|
||||
await fs.promises.writeFile(`${this.path}/eula.txt`, context);
|
||||
}
|
||||
}
|
||||
46
backend/src/modloader/neoforge.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
import fse from "fs-extra";
|
||||
import { Forge } from "./forge.js";
|
||||
import { Config } from "../utils/config.js";
|
||||
import { Got, got } from "got";
|
||||
|
||||
export class NeoForge extends Forge {
|
||||
got: Got;
|
||||
|
||||
constructor(minecraft: string, loaderVersion: string, path: string) {
|
||||
super(minecraft, loaderVersion, path);
|
||||
const config = Config.getConfig();
|
||||
this.got = got.extend({
|
||||
headers: { "User-Agent": "DeEarthX" },
|
||||
hooks: {
|
||||
init: [
|
||||
(options) => {
|
||||
if (config.mirror?.bmclapi) {
|
||||
options.prefixUrl = "https://bmclapi2.bangbang93.com/";
|
||||
} else {
|
||||
options.prefixUrl = "https://maven.neoforged.net/releases/";
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async setup() {
|
||||
await this.installer();
|
||||
const config = Config.getConfig();
|
||||
if (config.mirror.bmclapi) {
|
||||
await this.library();
|
||||
}
|
||||
await this.install();
|
||||
}
|
||||
|
||||
async installer() {
|
||||
const config = Config.getConfig();
|
||||
let url = `neoforge/version/${this.loaderVersion}/download/installer.jar`;
|
||||
if (!config.mirror?.bmclapi) {
|
||||
url = `net/neoforged/neoforge/${this.loaderVersion}/neoforge-${this.loaderVersion}-installer.jar`;
|
||||
}
|
||||
const res = (await this.got.get(url)).rawBody;
|
||||
await fse.outputFile(`${this.path}/forge-${this.minecraft}-${this.loaderVersion}-installer.jar`, res);
|
||||
}
|
||||
}
|
||||
73
backend/src/platform/curseforge.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
import got, { Got } from "got";
|
||||
import { join } from "node:path";
|
||||
import { Wfastdownload, Utils } from "../utils/utils.js";
|
||||
import { modpack_info, XPlatform } from "./index.js";
|
||||
import { MessageWS } from "../utils/ws.js";
|
||||
|
||||
export interface CurseForgeManifest {
|
||||
minecraft: {
|
||||
version: string;
|
||||
modLoaders: Array<{ id: string }>;
|
||||
};
|
||||
files: Array<{ projectID: number; fileID: number }>;
|
||||
}
|
||||
|
||||
export class CurseForge implements XPlatform {
|
||||
private utils: Utils;
|
||||
private got: Got;
|
||||
|
||||
constructor() {
|
||||
this.utils = new Utils();
|
||||
this.got = got.extend({
|
||||
prefixUrl: this.utils.curseforge_url,
|
||||
headers: {
|
||||
"User-Agent": "DeEarthX",
|
||||
"x-api-key": "$2a$10$ydk0TLDG/Gc6uPMdz7mad.iisj2TaMDytVcIW4gcVP231VKngLBKy",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
async getinfo(manifest: object): Promise<modpack_info> {
|
||||
let result: modpack_info = Object.create({});
|
||||
const local_manifest = manifest as CurseForgeManifest;
|
||||
if (result && local_manifest)
|
||||
result.minecraft = local_manifest.minecraft.version;
|
||||
const id = local_manifest.minecraft.modLoaders[0].id;
|
||||
const loader_all = id.match(/(.*)-/) as RegExpMatchArray;
|
||||
result.loader = loader_all[1];
|
||||
result.loader_version = id.replace(loader_all[0], "");
|
||||
return result;
|
||||
}
|
||||
|
||||
async downloadfile(manifest: object, path: string, ws: MessageWS): Promise<void> {
|
||||
const local_manifest = manifest as CurseForgeManifest;
|
||||
if (local_manifest.files.length === 0) {
|
||||
return;
|
||||
}
|
||||
const FileID = JSON.stringify({
|
||||
fileIds: local_manifest.files.map(
|
||||
(file: { fileID: number }) => file.fileID
|
||||
),
|
||||
});
|
||||
let tmp: string[][] = [];
|
||||
await this.got
|
||||
.post("v1/mods/files", {
|
||||
body: FileID,
|
||||
})
|
||||
.json()
|
||||
.then((res: any) => {
|
||||
res.data.forEach(
|
||||
(e: { fileName: string; downloadUrl: null | string }) => {
|
||||
if (e.fileName.endsWith(".zip") || e.downloadUrl == null) {
|
||||
return;
|
||||
}
|
||||
const unpath = join(path + "/mods/", e.fileName);
|
||||
const url = e.downloadUrl.replace("https://edge.forgecdn.net", this.utils.curseforge_Durl);
|
||||
tmp.push([url, unpath]);
|
||||
}
|
||||
);
|
||||
});
|
||||
await Wfastdownload(tmp, ws, true, true);
|
||||
}
|
||||
}
|
||||
38
backend/src/platform/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { MessageWS } from "../utils/ws.js";
|
||||
import { CurseForge } from "./curseforge.js";
|
||||
import { Modrinth } from "./modrinth.js";
|
||||
|
||||
export interface XPlatform {
|
||||
getinfo(manifest: object): Promise<modpack_info>;
|
||||
downloadfile(manifest: object, path: string, ws: MessageWS): Promise<void>;
|
||||
}
|
||||
|
||||
export interface modpack_info {
|
||||
minecraft: string;
|
||||
loader: string;
|
||||
loader_version: string;
|
||||
}
|
||||
|
||||
export function platform(plat: string | undefined): XPlatform {
|
||||
let platform: XPlatform = Object.create({});
|
||||
switch (plat) {
|
||||
case "curseforge":
|
||||
platform = new CurseForge();
|
||||
break;
|
||||
case "modrinth":
|
||||
platform = new Modrinth();
|
||||
break;
|
||||
}
|
||||
return platform;
|
||||
}
|
||||
|
||||
export function what_platform(dud_files: string | "manifest.json" | "modrinth.index.json") {
|
||||
switch (dud_files) {
|
||||
case "manifest.json":
|
||||
return "curseforge";
|
||||
case "modrinth.index.json":
|
||||
return "modrinth";
|
||||
default:
|
||||
return undefined
|
||||
}
|
||||
}
|
||||
54
backend/src/platform/modrinth.ts
Normal file
@@ -0,0 +1,54 @@
|
||||
import fs from "node:fs";
|
||||
import { join } from "node:path";
|
||||
import { Wfastdownload, Utils } from "../utils/utils.js";
|
||||
import { modpack_info, XPlatform } from "./index.js";
|
||||
import { MessageWS } from "../utils/ws.js";
|
||||
|
||||
interface ModrinthManifest {
|
||||
files: Array<{ path: string; downloads: string[]; fileSize: number; }>;
|
||||
dependencies: {
|
||||
minecraft: string;
|
||||
forge: string;
|
||||
neoforge: string;
|
||||
"fabric-loader": string;
|
||||
[key: string]: string;
|
||||
};
|
||||
}
|
||||
|
||||
export class Modrinth implements XPlatform {
|
||||
private utils: Utils;
|
||||
|
||||
constructor() {
|
||||
this.utils = new Utils();
|
||||
}
|
||||
|
||||
async getinfo(manifest: object): Promise<modpack_info> {
|
||||
let result: modpack_info = Object.create({});
|
||||
const local_manifest = manifest as ModrinthManifest;
|
||||
const depkey = Object.keys(local_manifest.dependencies);
|
||||
const loader = ["forge", "neoforge", "fabric-loader"];
|
||||
result.minecraft = local_manifest.dependencies.minecraft;
|
||||
for (let i = 0; i < depkey.length; i++) {
|
||||
const key = depkey[i];
|
||||
if (key !== "minecraft" && loader.includes(key)) {
|
||||
result.loader = key;
|
||||
result.loader_version = local_manifest.dependencies[key];
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
async downloadfile(manifest: object, path: string, ws: MessageWS): Promise<void> {
|
||||
const index = manifest as ModrinthManifest;
|
||||
let tmp: [string, string][] = [];
|
||||
for (const e of index.files) {
|
||||
if (e.path.endsWith(".zip")) {
|
||||
continue;
|
||||
}
|
||||
const url = e.downloads[0].replace("https://cdn.modrinth.com", this.utils.modrinth_Durl);
|
||||
const unpath = join(path, e.path);
|
||||
tmp.push([url, unpath]);
|
||||
}
|
||||
await Wfastdownload(tmp, ws, true, true);
|
||||
}
|
||||
}
|
||||
229
backend/src/template/TemplateManager.ts
Normal file
@@ -0,0 +1,229 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { getAppDir } from "../utils/utils.js";
|
||||
import { createWriteStream } from "node:fs";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import yauzl from "yauzl";
|
||||
import yazl from "yazl";
|
||||
|
||||
interface TemplateMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string;
|
||||
created: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class TemplateManager {
|
||||
private readonly templatesPath: string;
|
||||
|
||||
constructor(templatesPath?: string) {
|
||||
this.templatesPath = templatesPath || path.join(getAppDir(), "templates");
|
||||
}
|
||||
|
||||
async ensureDefaultTemplate(): Promise<void> {
|
||||
// 确保templates文件夹存在
|
||||
await fs.mkdir(this.templatesPath, { recursive: true });
|
||||
|
||||
const examplePath = path.join(this.templatesPath, "example");
|
||||
const metadataPath = path.join(examplePath, "metadata.json");
|
||||
const dataPath = path.join(examplePath, "data");
|
||||
|
||||
try {
|
||||
await fs.access(metadataPath);
|
||||
} catch {
|
||||
await this.createTemplate("example", {
|
||||
name: "example",
|
||||
version: "1.0.0",
|
||||
description: "Example template for DeEarthX",
|
||||
author: "DeEarthX",
|
||||
created: new Date().toISOString().split("T")[0],
|
||||
type: "template",
|
||||
});
|
||||
|
||||
await fs.mkdir(dataPath, { recursive: true });
|
||||
|
||||
const readmePath = path.join(dataPath, "README.txt");
|
||||
await fs.writeFile(readmePath, "This is an example template for DeEarthX.\nPlace your server files in this data folder.");
|
||||
}
|
||||
}
|
||||
|
||||
async createTemplate(name: string, metadata: Partial<TemplateMetadata>): Promise<void> {
|
||||
const templatePath = path.join(this.templatesPath, name);
|
||||
|
||||
await fs.mkdir(templatePath, { recursive: true });
|
||||
|
||||
const defaultMetadata: TemplateMetadata = {
|
||||
name,
|
||||
version: "1.0.0",
|
||||
description: "",
|
||||
author: "",
|
||||
created: new Date().toISOString().split("T")[0],
|
||||
type: "template",
|
||||
...metadata,
|
||||
};
|
||||
|
||||
const metadataPath = path.join(templatePath, "metadata.json");
|
||||
await fs.writeFile(metadataPath, JSON.stringify(defaultMetadata, null, 2));
|
||||
|
||||
const dataPath = path.join(templatePath, "data");
|
||||
await fs.mkdir(dataPath, { recursive: true });
|
||||
}
|
||||
|
||||
async getTemplates(): Promise<Array<{ id: string; metadata: TemplateMetadata }>> {
|
||||
try {
|
||||
// 确保templates文件夹存在
|
||||
await fs.mkdir(this.templatesPath, { recursive: true });
|
||||
|
||||
const entries = await fs.readdir(this.templatesPath, { withFileTypes: true });
|
||||
const templates: Array<{ id: string; metadata: TemplateMetadata }> = [];
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const templateId = entry.name;
|
||||
const metadataPath = path.join(this.templatesPath, templateId, "metadata.json");
|
||||
|
||||
try {
|
||||
const metadataContent = await fs.readFile(metadataPath, "utf-8");
|
||||
const metadata: TemplateMetadata = JSON.parse(metadataContent);
|
||||
templates.push({ id: templateId, metadata });
|
||||
} catch (error) {
|
||||
console.warn(`Failed to read metadata for template ${templateId}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return templates;
|
||||
} catch (error) {
|
||||
console.error("Failed to read templates directory:", error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
async updateTemplate(templateId: string, metadata: Partial<TemplateMetadata>): Promise<void> {
|
||||
const templatePath = path.join(this.templatesPath, templateId);
|
||||
const metadataPath = path.join(templatePath, "metadata.json");
|
||||
|
||||
try {
|
||||
await fs.access(metadataPath);
|
||||
} catch {
|
||||
throw new Error(`Template ${templateId} does not exist`);
|
||||
}
|
||||
|
||||
const existingMetadataContent = await fs.readFile(metadataPath, "utf-8");
|
||||
const existingMetadata: TemplateMetadata = JSON.parse(existingMetadataContent);
|
||||
|
||||
const updatedMetadata: TemplateMetadata = {
|
||||
...existingMetadata,
|
||||
...metadata,
|
||||
};
|
||||
|
||||
await fs.writeFile(metadataPath, JSON.stringify(updatedMetadata, null, 2));
|
||||
}
|
||||
|
||||
async exportTemplate(templateId: string, outputPath: string): Promise<void> {
|
||||
const templatePath = path.join(this.templatesPath, templateId);
|
||||
const metadataPath = path.join(templatePath, "metadata.json");
|
||||
|
||||
try {
|
||||
await fs.access(metadataPath);
|
||||
} catch {
|
||||
throw new Error(`Template ${templateId} does not exist`);
|
||||
}
|
||||
|
||||
const zipfile = new yazl.ZipFile();
|
||||
|
||||
// 读取并添加metadata.json
|
||||
const metadataContent = await fs.readFile(metadataPath, "utf-8");
|
||||
zipfile.addBuffer(Buffer.from(metadataContent), "metadata.json");
|
||||
|
||||
// 添加data目录
|
||||
const dataPath = path.join(templatePath, "data");
|
||||
try {
|
||||
await fs.access(dataPath);
|
||||
const dataFiles = await this.getFilesRecursively(dataPath);
|
||||
|
||||
for (const file of dataFiles) {
|
||||
const relativePath = path.relative(templatePath, file);
|
||||
zipfile.addFile(file, relativePath);
|
||||
}
|
||||
} catch {
|
||||
// data目录不存在,跳过
|
||||
}
|
||||
|
||||
// 生成zip文件
|
||||
return new Promise((resolve, reject) => {
|
||||
zipfile.outputStream.pipe(createWriteStream(outputPath))
|
||||
.on("close", () => resolve())
|
||||
.on("error", (err) => reject(err));
|
||||
zipfile.end();
|
||||
});
|
||||
}
|
||||
|
||||
async importTemplate(zipBuffer: Buffer, templateId?: string): Promise<string> {
|
||||
const newTemplateId = templateId || `template-${Date.now()}-${Math.random().toString(36).substring(2, 9)}`;
|
||||
const templatePath = path.join(this.templatesPath, newTemplateId);
|
||||
|
||||
// 确保模板目录存在
|
||||
await fs.mkdir(templatePath, { recursive: true });
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
yauzl.fromBuffer(zipBuffer, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
zipfile.on("entry", async (entry) => {
|
||||
if (entry.fileName.endsWith("/")) {
|
||||
// 目录,跳过
|
||||
zipfile.readEntry();
|
||||
return;
|
||||
}
|
||||
|
||||
const entryPath = path.join(templatePath, entry.fileName);
|
||||
const entryDir = path.dirname(entryPath);
|
||||
|
||||
// 确保目录存在
|
||||
await fs.mkdir(entryDir, { recursive: true });
|
||||
|
||||
// 读取并写入文件
|
||||
zipfile.openReadStream(entry, (err, readStream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const writeStream = createWriteStream(entryPath);
|
||||
pipeline(readStream, writeStream)
|
||||
.then(() => zipfile.readEntry())
|
||||
.catch((err) => reject(err));
|
||||
});
|
||||
});
|
||||
|
||||
zipfile.on("end", () => resolve(newTemplateId));
|
||||
zipfile.on("error", (err) => reject(err));
|
||||
|
||||
zipfile.readEntry();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
private async getFilesRecursively(dir: string): Promise<string[]> {
|
||||
const files: string[] = [];
|
||||
const entries = await fs.readdir(dir, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
const fullPath = path.join(dir, entry.name);
|
||||
if (entry.isDirectory()) {
|
||||
const subFiles = await this.getFilesRecursively(fullPath);
|
||||
files.push(...subFiles);
|
||||
} else {
|
||||
files.push(fullPath);
|
||||
}
|
||||
}
|
||||
|
||||
return files;
|
||||
}
|
||||
}
|
||||
94
backend/src/template/index.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import fs from "node:fs/promises";
|
||||
import path from "node:path";
|
||||
import { TemplateManager } from "./TemplateManager.js";
|
||||
import { getAppDir } from "../utils/utils.js";
|
||||
|
||||
export { TemplateManager };
|
||||
|
||||
interface TemplateMetadata {
|
||||
name: string;
|
||||
version: string;
|
||||
description: string;
|
||||
author: string;
|
||||
created: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export class TemplateService {
|
||||
private readonly templatesPath: string;
|
||||
|
||||
constructor(templatesPath?: string) {
|
||||
this.templatesPath = templatesPath || path.join(getAppDir(), "templates");
|
||||
}
|
||||
|
||||
async getTemplate(name: string): Promise<TemplateMetadata | null> {
|
||||
const metadataPath = path.join(this.templatesPath, name, "metadata.json");
|
||||
|
||||
try {
|
||||
const data = await fs.readFile(metadataPath, "utf-8");
|
||||
return JSON.parse(data) as TemplateMetadata;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async updateTemplate(name: string, metadata: Partial<TemplateMetadata>): Promise<boolean> {
|
||||
const currentMetadata = await this.getTemplate(name);
|
||||
|
||||
if (!currentMetadata) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const updatedMetadata = { ...currentMetadata, ...metadata };
|
||||
const metadataPath = path.join(this.templatesPath, name, "metadata.json");
|
||||
await fs.writeFile(metadataPath, JSON.stringify(updatedMetadata, null, 2));
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
async deleteTemplate(name: string): Promise<boolean> {
|
||||
const templatePath = path.join(this.templatesPath, name);
|
||||
|
||||
try {
|
||||
await fs.rm(templatePath, { recursive: true, force: true });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async listTemplates(): Promise<TemplateMetadata[]> {
|
||||
const templates: TemplateMetadata[] = [];
|
||||
|
||||
try {
|
||||
// 确保templates文件夹存在
|
||||
await fs.mkdir(this.templatesPath, { recursive: true });
|
||||
|
||||
const entries = await fs.readdir(this.templatesPath, { withFileTypes: true });
|
||||
|
||||
for (const entry of entries) {
|
||||
if (entry.isDirectory()) {
|
||||
const metadata = await this.getTemplate(entry.name);
|
||||
if (metadata) {
|
||||
templates.push(metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
|
||||
return templates;
|
||||
}
|
||||
|
||||
async templateExists(name: string): Promise<boolean> {
|
||||
const metadataPath = path.join(this.templatesPath, name, "metadata.json");
|
||||
|
||||
try {
|
||||
await fs.access(metadataPath);
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
backend/src/utils/colors.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
// ANSI 颜色码
|
||||
const COLORS = {
|
||||
reset: "\x1b[0m",
|
||||
bright: "\x1b[1m",
|
||||
dim: "\x1b[2m",
|
||||
|
||||
// 前景色
|
||||
black: "\x1b[30m",
|
||||
red: "\x1b[31m",
|
||||
green: "\x1b[32m",
|
||||
yellow: "\x1b[33m",
|
||||
blue: "\x1b[34m",
|
||||
magenta: "\x1b[35m",
|
||||
cyan: "\x1b[36m",
|
||||
white: "\x1b[37m",
|
||||
|
||||
// 背景色(可选)
|
||||
bgRed: "\x1b[41m",
|
||||
bgGreen: "\x1b[42m",
|
||||
bgYellow: "\x1b[43m",
|
||||
} as const;
|
||||
|
||||
// 日志级别对应的颜色
|
||||
const LEVEL_COLORS: Record<string, string> = {
|
||||
error: COLORS.red,
|
||||
warn: COLORS.yellow,
|
||||
info: COLORS.green,
|
||||
debug: COLORS.cyan,
|
||||
};
|
||||
|
||||
// 是否支持彩色输出(检测终端)
|
||||
const supportsColor = () => {
|
||||
if (process.env.FORCE_COLOR === "0") return false;
|
||||
if (process.env.FORCE_COLOR === "1") return true;
|
||||
return process.stdout.isTTY;
|
||||
};
|
||||
|
||||
// 格式化颜色文本
|
||||
export const colorize = (text: string, color: string): string => {
|
||||
if (!supportsColor()) return text;
|
||||
return `${color}${text}${COLORS.reset}`;
|
||||
};
|
||||
|
||||
// 格式化日志级别标签
|
||||
export const formatLevel = (level: string): string => {
|
||||
const color = LEVEL_COLORS[level.toLowerCase()] || COLORS.white;
|
||||
const label = `[${level.toUpperCase()}]`;
|
||||
return colorize(label, COLORS.bright + color);
|
||||
};
|
||||
|
||||
// 格式化时间戳
|
||||
export const formatTime = (): string => {
|
||||
const now = new Date();
|
||||
const time = now.toISOString().replace("T", " ").slice(0, 19);
|
||||
return colorize(time, COLORS.dim);
|
||||
};
|
||||
|
||||
export { COLORS };
|
||||
169
backend/src/utils/config.ts
Normal file
@@ -0,0 +1,169 @@
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { logger } from './logger.js';
|
||||
|
||||
/**
|
||||
* 应用配置接口
|
||||
*/
|
||||
export interface IConfig {
|
||||
mirror: {
|
||||
bmclapi: boolean;
|
||||
mcimirror: boolean;
|
||||
};
|
||||
filter: {
|
||||
hashes: boolean;
|
||||
dexpub: boolean;
|
||||
mixins: boolean;
|
||||
modrinth: boolean;
|
||||
};
|
||||
oaf: boolean;
|
||||
autoZip: boolean;
|
||||
port?: number;
|
||||
host?: string;
|
||||
javaPath?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认配置
|
||||
*/
|
||||
const DEFAULT_CONFIG: IConfig = {
|
||||
mirror: {
|
||||
bmclapi: true,
|
||||
mcimirror: true,
|
||||
},
|
||||
filter: {
|
||||
hashes: true,
|
||||
dexpub: true,
|
||||
mixins: true,
|
||||
modrinth: false,
|
||||
},
|
||||
oaf: true,
|
||||
autoZip: false,
|
||||
port: 37019,
|
||||
host: 'localhost',
|
||||
javaPath: undefined
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取可执行文件所在目录
|
||||
* 在开发环境返回当前目录,在生产环境返回可执行文件所在目录
|
||||
*/
|
||||
function getAppDir(): string {
|
||||
const execPath = process.execPath;
|
||||
const cwd = process.cwd();
|
||||
|
||||
// 检查是否在开发环境中运行
|
||||
// 如果 execPath 指向 node.exe 且当前目录不是 node 安装目录,说明是开发环境
|
||||
const isDevelopment = execPath.toLowerCase().includes('node.exe') &&
|
||||
!cwd.toLowerCase().includes('program files') &&
|
||||
!cwd.toLowerCase().includes('nodejs');
|
||||
|
||||
if (isDevelopment) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
return path.dirname(execPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置文件路径 - 使用可执行文件所在目录
|
||||
*/
|
||||
const CONFIG_PATH = path.join(getAppDir(), "config.json");
|
||||
|
||||
/**
|
||||
* 从环境变量获取配置
|
||||
* @param key 环境变量键
|
||||
* @param defaultValue 默认值
|
||||
* @returns 环境变量值或默认值
|
||||
*/
|
||||
function getEnv<T>(key: string, defaultValue: T): T {
|
||||
const value = process.env[key];
|
||||
if (value === undefined) {
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'boolean') {
|
||||
return (value.toLowerCase() === 'true') as unknown as T;
|
||||
}
|
||||
|
||||
if (typeof defaultValue === 'number') {
|
||||
const num = parseInt(value, 10);
|
||||
return (isNaN(num) ? defaultValue : num) as unknown as T;
|
||||
}
|
||||
|
||||
return value as unknown as T;
|
||||
}
|
||||
|
||||
/**
|
||||
* 配置管理器
|
||||
*/
|
||||
export class Config {
|
||||
private static cachedConfig: IConfig | null = null;
|
||||
|
||||
/**
|
||||
* 获取配置
|
||||
* @returns 配置对象
|
||||
*/
|
||||
public static getConfig(): IConfig {
|
||||
if (this.cachedConfig) {
|
||||
return this.cachedConfig;
|
||||
}
|
||||
|
||||
let config: IConfig;
|
||||
if (!fs.existsSync(CONFIG_PATH)) {
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(DEFAULT_CONFIG, null, 2));
|
||||
config = DEFAULT_CONFIG;
|
||||
} else {
|
||||
try {
|
||||
const content = fs.readFileSync(CONFIG_PATH, "utf-8");
|
||||
config = JSON.parse(content);
|
||||
} catch (err) {
|
||||
logger.error("Failed to read config file, using defaults", err as Error);
|
||||
config = DEFAULT_CONFIG;
|
||||
}
|
||||
}
|
||||
|
||||
// 从环境变量覆盖配置
|
||||
const envConfig: IConfig = {
|
||||
mirror: {
|
||||
bmclapi: getEnv('DEEARTHX_MIRROR_BMCLAPI', config.mirror.bmclapi),
|
||||
mcimirror: getEnv('DEEARTHX_MIRROR_MCIMIRROR', config.mirror.mcimirror)
|
||||
},
|
||||
filter: {
|
||||
hashes: getEnv('DEEARTHX_FILTER_HASHES', config.filter.hashes),
|
||||
dexpub: getEnv('DEEARTHX_FILTER_DEXPUB', config.filter.dexpub),
|
||||
mixins: getEnv('DEEARTHX_FILTER_MIXINS', config.filter.mixins),
|
||||
modrinth: getEnv('DEEARTHX_FILTER_MODRINTH', config.filter.modrinth)
|
||||
},
|
||||
oaf: getEnv('DEEARTHX_OAF', config.oaf),
|
||||
autoZip: getEnv('DEEARTHX_AUTO_ZIP', config.autoZip),
|
||||
port: getEnv('DEEARTHX_PORT', config.port || DEFAULT_CONFIG.port),
|
||||
host: getEnv('DEEARTHX_HOST', config.host || DEFAULT_CONFIG.host)
|
||||
};
|
||||
|
||||
this.cachedConfig = envConfig;
|
||||
logger.debug("Loaded config", envConfig);
|
||||
return envConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入配置
|
||||
* @param config 配置对象
|
||||
*/
|
||||
public static writeConfig(config: IConfig): void {
|
||||
try {
|
||||
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2));
|
||||
this.cachedConfig = config;
|
||||
logger.info("Config file written successfully");
|
||||
} catch (err) {
|
||||
logger.error("Failed to write config file", err as Error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除配置缓存(强制下次读取时重新从文件加载)
|
||||
*/
|
||||
public static clearCache(): void {
|
||||
this.cachedConfig = null;
|
||||
}
|
||||
}
|
||||
44
backend/src/utils/jar-parser.ts
Normal file
@@ -0,0 +1,44 @@
|
||||
import { IInfoFile, IMixinFile } from "../dearth/types.js";
|
||||
import { Azip } from "./ziplib.js";
|
||||
import toml from "smol-toml";
|
||||
|
||||
export class JarParser {
|
||||
static async extractModInfo(jarData: Buffer): Promise<IInfoFile[]> {
|
||||
const infos: IInfoFile[] = [];
|
||||
const zipEntries = Azip(jarData);
|
||||
|
||||
for (const entry of zipEntries) {
|
||||
try {
|
||||
if (entry.entryName.endsWith("neoforge.mods.toml") || entry.entryName.endsWith("mods.toml")) {
|
||||
const data = await entry.getData();
|
||||
infos.push({ name: entry.entryName, data: JSON.stringify(toml.parse(data.toString())) });
|
||||
} else if (entry.entryName.endsWith("fabric.mod.json")) {
|
||||
const data = await entry.getData();
|
||||
infos.push({ name: entry.entryName, data: data.toString() });
|
||||
}
|
||||
} catch (error: any) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
return infos;
|
||||
}
|
||||
|
||||
static async extractMixins(jarData: Buffer): Promise<IMixinFile[]> {
|
||||
const mixins: IMixinFile[] = [];
|
||||
const zipEntries = Azip(jarData);
|
||||
|
||||
for (const entry of zipEntries) {
|
||||
if (entry.entryName.endsWith(".mixins.json") && !entry.entryName.includes("/")) {
|
||||
try {
|
||||
const data = await entry.getData();
|
||||
mixins.push({ name: entry.entryName, data: data.toString() });
|
||||
} catch (error: any) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return mixins;
|
||||
}
|
||||
}
|
||||
97
backend/src/utils/logger.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { formatLevel, formatTime, colorize, COLORS } from "./colors.js";
|
||||
import * as fs from "fs";
|
||||
import * as path from "path";
|
||||
|
||||
type LogLevel = "debug" | "info" | "warn" | "error";
|
||||
|
||||
interface Logger {
|
||||
debug: (message: string, meta?: any) => void;
|
||||
info: (message: string, meta?: any) => void;
|
||||
warn: (message: string, meta?: any) => void;
|
||||
error: (message: string, meta?: any) => void;
|
||||
}
|
||||
|
||||
function getAppDir(): string {
|
||||
const execPath = process.execPath;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const isDevelopment = execPath.toLowerCase().includes('node.exe') &&
|
||||
!cwd.toLowerCase().includes('program files') &&
|
||||
!cwd.toLowerCase().includes('nodejs');
|
||||
|
||||
if (isDevelopment) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
return path.dirname(execPath);
|
||||
}
|
||||
|
||||
const logsDir = path.join(getAppDir(), "logs");
|
||||
|
||||
const ensureLogsDir = () => {
|
||||
if (!fs.existsSync(logsDir)) {
|
||||
fs.mkdirSync(logsDir, { recursive: true });
|
||||
}
|
||||
};
|
||||
|
||||
const generateLogFileName = () => {
|
||||
const now = new Date();
|
||||
const year = now.getFullYear();
|
||||
const month = String(now.getMonth() + 1).padStart(2, "0");
|
||||
const day = String(now.getDate()).padStart(2, "0");
|
||||
const timestamp = Date.now();
|
||||
return `${year}-${month}-${day}-${timestamp}.log`;
|
||||
};
|
||||
|
||||
const logFilePath = path.join(logsDir, generateLogFileName());
|
||||
|
||||
const writeToFile = (level: LogLevel, message: string, meta?: any) => {
|
||||
const timestamp = formatTime();
|
||||
let metaStr = "";
|
||||
if (meta) {
|
||||
try {
|
||||
const metaContent = typeof meta === "object"
|
||||
? JSON.stringify(meta)
|
||||
: String(meta);
|
||||
metaStr = ` ${metaContent}`;
|
||||
} catch {
|
||||
metaStr = " [元数据解析错误]";
|
||||
}
|
||||
}
|
||||
const logLine = `${timestamp} [${level.toUpperCase()}] ${message}${metaStr}\n`;
|
||||
fs.appendFileSync(logFilePath, logLine, "utf-8");
|
||||
};
|
||||
|
||||
ensureLogsDir();
|
||||
|
||||
const log = (level: LogLevel, message: string, meta?: any) => {
|
||||
const timestamp = formatTime();
|
||||
const levelTag = formatLevel(level);
|
||||
|
||||
writeToFile(level, message, meta);
|
||||
|
||||
let metaStr = "";
|
||||
if (meta) {
|
||||
try {
|
||||
const metaContent = typeof meta === "object"
|
||||
? JSON.stringify(meta)
|
||||
: String(meta);
|
||||
metaStr = ` ${colorize(metaContent, COLORS.dim)}`;
|
||||
} catch {
|
||||
metaStr = ` ${colorize("[元数据解析错误]", COLORS.red)}`;
|
||||
}
|
||||
}
|
||||
|
||||
const msg = level === "error"
|
||||
? colorize(message, COLORS.bright)
|
||||
: message;
|
||||
|
||||
console.log(`${timestamp} ${levelTag} ${msg}${metaStr}`);
|
||||
};
|
||||
|
||||
export const logger: Logger = {
|
||||
debug: (msg, meta) => log("debug", msg, meta),
|
||||
info: (msg, meta) => log("info", msg, meta),
|
||||
warn: (msg, meta) => log("warn", msg, meta),
|
||||
error: (msg, meta) => log("error", msg, meta),
|
||||
};
|
||||
555
backend/src/utils/utils.ts
Normal file
@@ -0,0 +1,555 @@
|
||||
import pMap from "p-map";
|
||||
import { Config } from "./config.js";
|
||||
import got from "got";
|
||||
import pRetry from "p-retry";
|
||||
import fs from "node:fs";
|
||||
import fse from "fs-extra";
|
||||
import { SpawnOptions, exec, spawn } from "node:child_process";
|
||||
import crypto from "node:crypto";
|
||||
import path from "node:path";
|
||||
import { MessageWS } from "./ws.js";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
export function getAppDir(): string {
|
||||
const execPath = process.execPath;
|
||||
const cwd = process.cwd();
|
||||
|
||||
const isDevelopment = execPath.toLowerCase().includes('node.exe') &&
|
||||
!cwd.toLowerCase().includes('program files') &&
|
||||
!cwd.toLowerCase().includes('nodejs');
|
||||
|
||||
if (isDevelopment) {
|
||||
return cwd;
|
||||
}
|
||||
|
||||
return path.dirname(execPath);
|
||||
}
|
||||
|
||||
export interface JavaVersion {
|
||||
major: number;
|
||||
minor: number;
|
||||
patch: number;
|
||||
fullVersion: string;
|
||||
vendor: string;
|
||||
runtimeVersion?: string;
|
||||
}
|
||||
|
||||
export interface JavaCheckResult {
|
||||
exists: boolean;
|
||||
version?: JavaVersion;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export class Utils {
|
||||
public modrinth_url: string;
|
||||
public curseforge_url: string;
|
||||
public curseforge_Durl: string;
|
||||
public modrinth_Durl: string;
|
||||
|
||||
constructor() {
|
||||
const config = Config.getConfig();
|
||||
this.modrinth_url = "https://api.modrinth.com";
|
||||
this.curseforge_url = "https://api.curseforge.com";
|
||||
this.modrinth_Durl = "https://cdn.modrinth.com";
|
||||
this.curseforge_Durl = "https://edge.forgecdn.net";
|
||||
if (config.mirror.mcimirror) {
|
||||
this.modrinth_url = "https://mod.mcimirror.top/modrinth";
|
||||
this.curseforge_url = "https://mod.mcimirror.top/curseforge";
|
||||
this.modrinth_Durl = "https://mod.mcimirror.top";
|
||||
this.curseforge_Durl = "https://mod.mcimirror.top";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function mavenToUrl(
|
||||
coordinate: { split: (arg0: string) => [any, any, any, any] },
|
||||
base = "maven"
|
||||
) {
|
||||
const [g, a, v, ce] = coordinate.split(":");
|
||||
const [c, e = "jar"] = (ce || "").split("@");
|
||||
return `${base.replace(/\/$/, "")}/${g.replace(
|
||||
/\./g,
|
||||
"/"
|
||||
)}/${a}/${v}/${a}-${v}${c ? "-" + c : ""}.${e}`;
|
||||
}
|
||||
|
||||
export function version_compare(v1: string, v2: string) {
|
||||
const v1_arr = v1.split(".");
|
||||
const v2_arr = v2.split(".");
|
||||
for (let i = 0; i < v1_arr.length; i++) {
|
||||
if (v1_arr[i] !== v2_arr[i]) {
|
||||
return v1_arr[i] > v2_arr[i] ? 1 : -1;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
export async function checkJava(javaPath?: string): Promise<JavaCheckResult> {
|
||||
try {
|
||||
const javaCmd = javaPath || "java";
|
||||
const output = await new Promise<string>((resolve, reject) => {
|
||||
exec(`${javaCmd} -version`, (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
logger.error("Java 检查失败", err);
|
||||
reject(new Error("Java not found"));
|
||||
return;
|
||||
}
|
||||
resolve(stderr);
|
||||
});
|
||||
});
|
||||
|
||||
logger.debug(`Java version output: ${output}`);
|
||||
|
||||
const versionRegex = /version "(\d+)(\.(\d+))?(\.(\d+))?/;
|
||||
const vendorRegex = /(Java\(TM\)|OpenJDK).*Runtime Environment.*by (.*)/;
|
||||
|
||||
const versionMatch = output.match(versionRegex);
|
||||
const vendorMatch = output.match(vendorRegex);
|
||||
|
||||
if (!versionMatch) {
|
||||
return {
|
||||
exists: true,
|
||||
error: "解析 Java 版本失败"
|
||||
};
|
||||
}
|
||||
|
||||
const major = parseInt(versionMatch[1], 10);
|
||||
const minor = versionMatch[3] ? parseInt(versionMatch[3], 10) : 0;
|
||||
const patch = versionMatch[5] ? parseInt(versionMatch[5], 10) : 0;
|
||||
|
||||
const versionInfo: JavaVersion = {
|
||||
major,
|
||||
minor,
|
||||
patch,
|
||||
fullVersion: versionMatch[0].replace("version ", ""),
|
||||
vendor: vendorMatch ? vendorMatch[2] : "Unknown"
|
||||
};
|
||||
|
||||
logger.info(`检测到 Java: ${JSON.stringify(versionInfo)}`);
|
||||
|
||||
return {
|
||||
exists: true,
|
||||
version: versionInfo
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Java 检查异常", error as Error);
|
||||
return {
|
||||
exists: false,
|
||||
error: (error as Error).message
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function detectJavaPaths(): Promise<string[]> {
|
||||
const javaPaths: string[] = [];
|
||||
|
||||
const windowsPaths = [
|
||||
"C:\\Program Files\\Java\\",
|
||||
"C:\\Program Files (x86)\\Java\\",
|
||||
"C:\\Program Files\\Eclipse Adoptium\\",
|
||||
"C:\\Program Files\\Eclipse Foundation\\",
|
||||
"C:\\Program Files\\Microsoft\\",
|
||||
"C:\\Program Files\\Amazon Corretto\\",
|
||||
"C:\\Program Files\\BellSoft\\",
|
||||
"C:\\Program Files\\Zulu\\",
|
||||
"C:\\Program Files\\Semeru\\",
|
||||
"C:\\Program Files\\Oracle\\",
|
||||
"C:\\Program Files\\RedHat\\",
|
||||
];
|
||||
|
||||
for (const basePath of windowsPaths) {
|
||||
try {
|
||||
if (fs.existsSync(basePath)) {
|
||||
const versions = fs.readdirSync(basePath);
|
||||
for (const version of versions) {
|
||||
const javaExe = `${basePath}${version}\\bin\\java.exe`;
|
||||
if (fs.existsSync(javaExe)) {
|
||||
javaPaths.push(javaExe);
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const pathOutput = await new Promise<string>((resolve, reject) => {
|
||||
exec("where java", (err, stdout, stderr) => {
|
||||
if (err) {
|
||||
resolve("");
|
||||
return;
|
||||
}
|
||||
resolve(stdout);
|
||||
});
|
||||
});
|
||||
|
||||
const wherePaths = pathOutput.split('\n').filter(p => p.trim() !== '');
|
||||
for (const path of wherePaths) {
|
||||
if (!javaPaths.includes(path.trim())) {
|
||||
javaPaths.push(path.trim());
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
}
|
||||
|
||||
return [...new Set(javaPaths)];
|
||||
}
|
||||
|
||||
function safeLog(level: 'debug' | 'error', message: string): void {
|
||||
try {
|
||||
if (level === 'debug') {
|
||||
logger.debug(message);
|
||||
} else {
|
||||
logger.error(message);
|
||||
}
|
||||
} catch (err) {
|
||||
console.error(`[logger fallback] ${level}: ${message}`, err);
|
||||
}
|
||||
}
|
||||
|
||||
export function execPromise(cmd: string, options?: SpawnOptions): Promise<number> {
|
||||
safeLog('debug', `执行命令: ${cmd}`);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn(cmd, {
|
||||
...options,
|
||||
shell: true,
|
||||
windowsHide: true,
|
||||
stdio: ['ignore', 'pipe', 'pipe']
|
||||
});
|
||||
|
||||
child.stdout?.on('data', (chunk: unknown) => {
|
||||
const text = Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk);
|
||||
safeLog('debug', text.trim());
|
||||
});
|
||||
|
||||
child.stderr?.on('data', (chunk: unknown) => {
|
||||
const text = Buffer.isBuffer(chunk) ? chunk.toString() : String(chunk);
|
||||
safeLog('error', text.trim());
|
||||
});
|
||||
|
||||
child.on('error', (err) => {
|
||||
safeLog('error', `命令执行错误: ${cmd}`);
|
||||
reject(err);
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
safeLog('debug', `命令执行完成,退出码: ${code}`);
|
||||
if (code !== 0) {
|
||||
reject(new Error(`Command failed with exit code ${code}`));
|
||||
return;
|
||||
}
|
||||
resolve(code ?? 0);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function calculateSHA1(filePath: string): string {
|
||||
const hash = crypto.createHash('sha1');
|
||||
const fileBuffer = fs.readFileSync(filePath);
|
||||
hash.update(fileBuffer);
|
||||
return hash.digest('hex').toLowerCase();
|
||||
}
|
||||
|
||||
export function verifySHA1(filePath: string, expectedHash: string): boolean {
|
||||
const actualHash = calculateSHA1(filePath);
|
||||
const expectedHashLower = expectedHash.toLowerCase();
|
||||
const isMatch = actualHash === expectedHashLower;
|
||||
|
||||
if (!isMatch) {
|
||||
logger.error(`文件哈希验证失败: ${filePath}`);
|
||||
logger.error(`期望: ${expectedHashLower}`);
|
||||
logger.error(`实际: ${actualHash}`);
|
||||
} else {
|
||||
logger.debug(`文件哈希验证成功: ${filePath} (sha1: ${actualHash})`);
|
||||
}
|
||||
|
||||
return isMatch;
|
||||
}
|
||||
|
||||
interface DownloadOptions {
|
||||
url: string;
|
||||
filePath: string;
|
||||
expectedHash?: string;
|
||||
forceDownload?: boolean;
|
||||
}
|
||||
|
||||
async function chunkedDownload(url: string, filePath: string, chunkSize = 5 * 1024 * 1024, concurrency = 4): Promise<void> {
|
||||
logger.debug(`开始分块下载 ${url},块大小: ${chunkSize / 1024 / 1024}MB,并发数: ${concurrency}`);
|
||||
|
||||
const isBMCLAPI = url.includes('bmclapi2');
|
||||
|
||||
if (isBMCLAPI) {
|
||||
logger.debug(`检测到 BMCLAPI 下载,使用普通下载: ${url}`);
|
||||
const res = await got.get(url, {
|
||||
responseType: "buffer",
|
||||
headers: { "user-agent": "DeEarthX" },
|
||||
followRedirect: true,
|
||||
});
|
||||
fse.outputFileSync(filePath, res.rawBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const tempDir = `${filePath}.chunks`;
|
||||
await fse.ensureDir(tempDir);
|
||||
|
||||
try {
|
||||
const response = await got.head(url, {
|
||||
headers: { "user-agent": "DeEarthX" },
|
||||
followRedirect: true,
|
||||
timeout: { request: 30000 }
|
||||
});
|
||||
|
||||
const fileSize = parseInt(response.headers['content-length'] || '0', 10);
|
||||
const acceptRanges = response.headers['accept-ranges'];
|
||||
|
||||
if (fileSize <= chunkSize || acceptRanges !== 'bytes') {
|
||||
logger.debug(`文件较小或服务器不支持分块下载,使用普通下载: ${url}`);
|
||||
const res = await got.get(url, {
|
||||
responseType: "buffer",
|
||||
headers: { "user-agent": "DeEarthX" },
|
||||
followRedirect: true,
|
||||
});
|
||||
fse.outputFileSync(filePath, res.rawBody);
|
||||
return;
|
||||
}
|
||||
|
||||
const totalChunks = Math.ceil(fileSize / chunkSize);
|
||||
logger.debug(`文件大小: ${(fileSize / 1024 / 1024).toFixed(2)}MB,分 ${totalChunks} 个块下载`);
|
||||
|
||||
let supportsChunkedDownload = true;
|
||||
let currentConcurrency = Math.min(concurrency, totalChunks);
|
||||
let rate429Count = 0;
|
||||
|
||||
const sleep = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
|
||||
|
||||
const downloadChunk = async (chunkIndex: number): Promise<void> => {
|
||||
const start = chunkIndex * chunkSize;
|
||||
const end = Math.min(start + chunkSize - 1, fileSize - 1);
|
||||
const chunkPath = `${tempDir}/chunk_${chunkIndex}`;
|
||||
|
||||
logger.debug(`下载块 ${chunkIndex + 1}/${totalChunks}: bytes ${start}-${end}`);
|
||||
|
||||
let retryCount = 0;
|
||||
const maxRetries = 5;
|
||||
|
||||
while (retryCount < maxRetries) {
|
||||
try {
|
||||
const res = await got.get(url, {
|
||||
responseType: "buffer",
|
||||
headers: {
|
||||
"user-agent": "DeEarthX",
|
||||
"Range": `bytes=${start}-${end}`
|
||||
},
|
||||
followRedirect: true,
|
||||
timeout: { request: 60000 }
|
||||
});
|
||||
|
||||
if (res.statusCode === 206) {
|
||||
fse.writeFileSync(chunkPath, res.rawBody);
|
||||
return;
|
||||
} else if (res.statusCode === 200) {
|
||||
supportsChunkedDownload = false;
|
||||
throw new Error('服务器不支持 Range 请求');
|
||||
} else if (res.statusCode === 429) {
|
||||
rate429Count++;
|
||||
|
||||
const retryAfter = res.headers['retry-after'];
|
||||
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.min(5000 * Math.pow(2, retryCount), 60000);
|
||||
|
||||
logger.warn(`遇到 429 错误,等待 ${waitTime / 1000} 秒后重试 (${retryCount + 1}/${maxRetries})`);
|
||||
await sleep(waitTime);
|
||||
retryCount++;
|
||||
continue;
|
||||
} else {
|
||||
supportsChunkedDownload = false;
|
||||
throw new Error(`服务器返回状态码 ${res.statusCode},不支持分块下载`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
if (error.response?.statusCode === 429) {
|
||||
rate429Count++;
|
||||
|
||||
const retryAfter = error.response.headers['retry-after'];
|
||||
const waitTime = retryAfter ? parseInt(retryAfter, 10) * 1000 : Math.min(5000 * Math.pow(2, retryCount), 60000);
|
||||
|
||||
logger.warn(`遇到 429 错误,等待 ${waitTime / 1000} 秒后重试 (${retryCount + 1}/${maxRetries})`);
|
||||
await sleep(waitTime);
|
||||
retryCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (error.response?.statusCode) {
|
||||
supportsChunkedDownload = false;
|
||||
logger.warn(`Range 请求失败,状态码: ${error.response.statusCode}`);
|
||||
throw new Error(`服务器返回状态码 ${error.response.statusCode},不支持分块下载`);
|
||||
}
|
||||
|
||||
if (error.message.includes('Range') || error.message.includes('不支持')) {
|
||||
supportsChunkedDownload = false;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`下载块失败,已重试 ${maxRetries} 次`);
|
||||
};
|
||||
|
||||
const chunks = Array.from({ length: totalChunks }, (_, i) => i);
|
||||
|
||||
try {
|
||||
await pMap(chunks, downloadChunk, { concurrency: currentConcurrency });
|
||||
} catch (error: any) {
|
||||
if (!supportsChunkedDownload) {
|
||||
logger.warn(`服务器不支持分块下载,切换到普通下载: ${url}`);
|
||||
await fse.remove(tempDir);
|
||||
const res = await got.get(url, {
|
||||
responseType: "buffer",
|
||||
headers: { "user-agent": "DeEarthX" },
|
||||
followRedirect: true,
|
||||
});
|
||||
fse.outputFileSync(filePath, res.rawBody);
|
||||
return;
|
||||
}
|
||||
|
||||
if (rate429Count > 0) {
|
||||
const newConcurrency = Math.max(1, Math.floor(currentConcurrency / 2));
|
||||
logger.warn(`检测到 ${rate429Count} 次 429 错误,降低并发数从 ${currentConcurrency} 到 ${newConcurrency},重新下载`);
|
||||
|
||||
await fse.remove(tempDir);
|
||||
await fse.ensureDir(tempDir);
|
||||
|
||||
rate429Count = 0;
|
||||
currentConcurrency = newConcurrency;
|
||||
|
||||
await pMap(chunks, downloadChunk, { concurrency: currentConcurrency });
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
if (supportsChunkedDownload) {
|
||||
logger.debug(`所有块下载完成,开始合并文件`);
|
||||
const writeStream = fs.createWriteStream(filePath);
|
||||
|
||||
for (let i = 0; i < totalChunks; i++) {
|
||||
const chunkPath = `${tempDir}/chunk_${i}`;
|
||||
const chunkBuffer = fs.readFileSync(chunkPath);
|
||||
writeStream.write(chunkBuffer);
|
||||
fs.unlinkSync(chunkPath);
|
||||
}
|
||||
|
||||
writeStream.end();
|
||||
await new Promise((resolve) => writeStream.on('finish', resolve));
|
||||
|
||||
logger.debug(`文件合并完成: ${filePath}`);
|
||||
}
|
||||
} finally {
|
||||
await fse.remove(tempDir);
|
||||
}
|
||||
}
|
||||
|
||||
async function downloadFile(url: string, filePath: string, expectedHash?: string, forceDownload = false, useChunked = false) {
|
||||
await pRetry(
|
||||
async () => {
|
||||
if (fs.existsSync(filePath) && !forceDownload) {
|
||||
logger.debug(`文件已存在,跳过: ${filePath}`);
|
||||
if (expectedHash && !verifySHA1(filePath, expectedHash)) {
|
||||
logger.warn(`已存在文件哈希不匹配,将重新下载: ${filePath}`);
|
||||
fs.unlinkSync(filePath);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
logger.debug(`正在下载 ${url} 到 ${filePath}`);
|
||||
try {
|
||||
await fse.ensureDir(path.dirname(filePath));
|
||||
|
||||
if (useChunked) {
|
||||
await chunkedDownload(url, filePath);
|
||||
} else {
|
||||
const res = await got.get(url, {
|
||||
responseType: "buffer",
|
||||
headers: { "user-agent": "DeEarthX" },
|
||||
followRedirect: true,
|
||||
});
|
||||
fse.outputFileSync(filePath, res.rawBody);
|
||||
}
|
||||
|
||||
logger.debug(`下载 ${url} 成功`);
|
||||
|
||||
if (expectedHash && !verifySHA1(filePath, expectedHash)) {
|
||||
throw new Error(`文件哈希验证失败,下载的文件可能已损坏`);
|
||||
}
|
||||
} catch (error) {
|
||||
if (fs.existsSync(filePath)) {
|
||||
fs.unlinkSync(filePath);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{
|
||||
retries: 3,
|
||||
onFailedAttempt: (error) => {
|
||||
logger.warn(`${url} 下载失败,正在重试 (${error.attemptNumber}/3)`);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
interface DownloadItem {
|
||||
url: string;
|
||||
filePath: string;
|
||||
expectedHash?: string;
|
||||
}
|
||||
|
||||
export async function fastdownload(data: [string, string] | string[][], enableHashVerify = true) {
|
||||
let downloadList: Array<[string, string, string?]>;
|
||||
|
||||
if (Array.isArray(data[0])) {
|
||||
downloadList = (data as string[][]).map((item): [string, string, string?] =>
|
||||
item.length >= 3 ? [item[0], item[1], item[2]] : [item[0], item[1]]
|
||||
);
|
||||
} else {
|
||||
const singleItem = data as [string, string];
|
||||
downloadList = [[singleItem[0], singleItem[1]]];
|
||||
}
|
||||
|
||||
logger.info(`开始快速下载 ${downloadList.length} 个文件${enableHashVerify ? '(启用 hash 验证)' : ''}`);
|
||||
|
||||
return await pMap(
|
||||
downloadList,
|
||||
async (item: [string, string, string?]) => {
|
||||
const [url, filePath, expectedHash] = item;
|
||||
try {
|
||||
await downloadFile(url, filePath, enableHashVerify ? expectedHash : undefined);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to download ${url} after 3 attempts`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{ concurrency: 16 }
|
||||
);
|
||||
}
|
||||
|
||||
export async function Wfastdownload(data: string[][], ws: MessageWS, enableHashVerify = true, useChunked = false) {
|
||||
logger.info(`开始 Web 下载 ${data.length} 个文件${enableHashVerify ? '(启用 hash 验证)' : ''}${useChunked ? '(启用分块下载)' : ''}`);
|
||||
const completed = new Set<number>();
|
||||
return await pMap(
|
||||
data,
|
||||
async (item: string[], index: number) => {
|
||||
const [url, filePath, expectedHash] = item;
|
||||
try {
|
||||
await downloadFile(url, filePath, enableHashVerify ? expectedHash : undefined, false, useChunked);
|
||||
if (!completed.has(index)) {
|
||||
completed.add(index);
|
||||
ws.download(data.length, completed.size, filePath);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`${url} 下载失败,已重试 3 次`, error);
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
{ concurrency: 24 }
|
||||
);
|
||||
}
|
||||
209
backend/src/utils/ws.ts
Normal file
@@ -0,0 +1,209 @@
|
||||
import websocket, { WebSocketServer } from "ws";
|
||||
import { logger } from "./logger.js";
|
||||
|
||||
export class MessageWS {
|
||||
private ws!: websocket;
|
||||
|
||||
constructor(ws: websocket) {
|
||||
this.ws = ws;
|
||||
|
||||
// 监听WebSocket错误
|
||||
this.ws.on('error', (err) => {
|
||||
logger.error("WebSocket error", err);
|
||||
});
|
||||
|
||||
// 监听连接关闭
|
||||
this.ws.on('close', (code, reason) => {
|
||||
logger.info("WebSocket connection closed", { code, reason: reason.toString() });
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送完成消息
|
||||
* @param startTime 开始时间
|
||||
* @param endTime 结束时间
|
||||
*/
|
||||
finish(startTime: number, endTime: number) {
|
||||
this.send("finish", endTime - startTime);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送解压进度消息
|
||||
* @param entryName 文件名
|
||||
* @param total 总文件数
|
||||
* @param current 当前文件索引
|
||||
*/
|
||||
unzip(entryName: string, total: number, current: number) {
|
||||
this.send("unzip", { name: entryName, total, current });
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送下载进度消息
|
||||
* @param total 总文件数
|
||||
* @param index 当前文件索引
|
||||
* @param name 文件名
|
||||
*/
|
||||
download(total: number, index: number, name: string) {
|
||||
this.send("downloading", { total, index, name });
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送状态变更消息
|
||||
*/
|
||||
statusChange() {
|
||||
this.send("changed", undefined);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送错误消息
|
||||
* @param error 错误对象
|
||||
*/
|
||||
handleError(error: Error) {
|
||||
this.send("error", error.message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送信息消息
|
||||
* @param message 消息内容
|
||||
*/
|
||||
info(message: string) {
|
||||
this.send("info", message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务端安装开始消息
|
||||
* @param modpackName 整合包名称
|
||||
* @param minecraftVersion Minecraft版本
|
||||
* @param loaderType 加载器类型
|
||||
* @param loaderVersion 加载器版本
|
||||
*/
|
||||
serverInstallStart(modpackName: string, minecraftVersion: string, loaderType: string, loaderVersion: string) {
|
||||
this.send("server_install_start", {
|
||||
modpackName,
|
||||
minecraftVersion,
|
||||
loaderType,
|
||||
loaderVersion
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务端安装步骤消息
|
||||
* @param step 当前步骤名称
|
||||
* @param stepIndex 步骤索引
|
||||
* @param totalSteps 总步骤数
|
||||
* @param message 步骤详情
|
||||
*/
|
||||
serverInstallStep(step: string, stepIndex: number, totalSteps: number, message?: string) {
|
||||
this.send("server_install_step", {
|
||||
step,
|
||||
stepIndex,
|
||||
totalSteps,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务端安装进度消息
|
||||
* @param step 当前步骤
|
||||
* @param progress 进度百分比 (0-100)
|
||||
* @param message 进度详情
|
||||
*/
|
||||
serverInstallProgress(step: string, progress: number, message?: string) {
|
||||
this.send("server_install_progress", {
|
||||
step,
|
||||
progress,
|
||||
message
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务端安装完成消息
|
||||
* @param installPath 安装路径
|
||||
* @param duration 耗时(毫秒)
|
||||
*/
|
||||
serverInstallComplete(installPath: string, duration: number) {
|
||||
this.send("server_install_complete", {
|
||||
installPath,
|
||||
duration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送服务端安装错误消息
|
||||
* @param error 错误信息
|
||||
* @param step 出错的步骤
|
||||
*/
|
||||
serverInstallError(error: string, step?: string) {
|
||||
this.send("server_install_error", {
|
||||
error,
|
||||
step
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送筛选模组开始消息
|
||||
* @param totalMods 总模组数
|
||||
*/
|
||||
filterModsStart(totalMods: number) {
|
||||
this.send("filter_mods_start", {
|
||||
totalMods
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送筛选模组进度消息
|
||||
* @param current 当前处理的模组索引
|
||||
* @param total 总模组数
|
||||
* @param modName 模组名称
|
||||
*/
|
||||
filterModsProgress(current: number, total: number, modName: string) {
|
||||
this.send("filter_mods_progress", {
|
||||
current,
|
||||
total,
|
||||
modName
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送筛选模组完成消息
|
||||
* @param filteredCount 筛选出的客户端模组数
|
||||
* @param movedCount 成功移动的数量
|
||||
* @param duration 耗时(毫秒)
|
||||
*/
|
||||
filterModsComplete(filteredCount: number, movedCount: number, duration: number) {
|
||||
this.send("filter_mods_complete", {
|
||||
filteredCount,
|
||||
movedCount,
|
||||
duration
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送筛选模组错误消息
|
||||
* @param error 错误信息
|
||||
*/
|
||||
filterModsError(error: string) {
|
||||
this.send("filter_mods_error", {
|
||||
error
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用消息发送方法
|
||||
* @param status 消息状态
|
||||
* @param result 消息内容
|
||||
*/
|
||||
private send(status: string, result: any) {
|
||||
try {
|
||||
if (this.ws.readyState === websocket.OPEN) {
|
||||
const message = JSON.stringify({ status, result });
|
||||
logger.debug("Sending WebSocket message", { status, result });
|
||||
this.ws.send(message);
|
||||
} else {
|
||||
logger.warn(`WebSocket not open, cannot send message: ${status}`);
|
||||
}
|
||||
} catch (err) {
|
||||
logger.error("Failed to send WebSocket message", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
backend/src/utils/yauzl.promise.ts1
Normal file
@@ -0,0 +1,89 @@
|
||||
import yauzl from "yauzl";
|
||||
import Stream from "node:stream"
|
||||
|
||||
export interface IentryP extends yauzl.Entry {
|
||||
openReadStream: Promise<Stream.Readable>;
|
||||
ReadEntry: Promise<Buffer>;
|
||||
}
|
||||
|
||||
export async function yauzl_promise(buffer: Buffer): Promise<IentryP[]>{
|
||||
const zip = await (new Promise((resolve,reject)=>{
|
||||
yauzl.fromBuffer(buffer, { lazyEntries: true }, (err, zipfile) => {
|
||||
if (err){
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(zipfile);
|
||||
});
|
||||
}) as Promise<yauzl.ZipFile>);
|
||||
|
||||
return await new Promise((resolve, reject) => {
|
||||
const entries: IentryP[] = [];
|
||||
let entryCount = 0;
|
||||
|
||||
zip.on("entry", (entry: yauzl.Entry) => {
|
||||
// 创建新对象并复制所有entry属性,避免yauzl重用对象导致的引用问题
|
||||
const _entry = Object.assign({}, entry) as IentryP;
|
||||
_entry.openReadStream = _openReadStream(zip, entry);
|
||||
_entry.ReadEntry = _ReadEntry(zip, entry);
|
||||
|
||||
entries.push(_entry);
|
||||
entryCount++;
|
||||
//console.log(entryCount, entry.fileName);
|
||||
// 继续读取下一个条目
|
||||
zip.readEntry();
|
||||
});
|
||||
|
||||
zip.on("end", () => {
|
||||
zip.close();
|
||||
console.log(entryCount, "entries read");
|
||||
if(entryCount === zip.entryCount){
|
||||
console.log("All entries read");
|
||||
resolve(entries);
|
||||
}
|
||||
});
|
||||
|
||||
zip.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
|
||||
// 开始读取第一个条目
|
||||
zip.readEntry();
|
||||
});
|
||||
}
|
||||
|
||||
async function _openReadStream(zip: yauzl.ZipFile, entry: yauzl.Entry): Promise<Stream.Readable>{
|
||||
return new Promise((resolve, reject) => {
|
||||
zip.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(stream);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function _ReadEntry(zip: yauzl.ZipFile, entry: yauzl.Entry): Promise<Buffer>{
|
||||
return new Promise((resolve, reject) => {
|
||||
zip.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk) => {
|
||||
chunks.push(chunk);
|
||||
});
|
||||
|
||||
stream.on("end", () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
|
||||
stream.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
88
backend/src/utils/ziplib.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
import admZip from "adm-zip";
|
||||
import yauzl from "yauzl";
|
||||
import Stream from "node:stream";
|
||||
|
||||
export interface IentryP extends yauzl.Entry {
|
||||
openReadStream: Promise<Stream.Readable>;
|
||||
ReadEntry: Promise<Buffer>;
|
||||
}
|
||||
|
||||
export async function yauzl_promise(buffer: Buffer): Promise<IentryP[]> {
|
||||
const zip = await (new Promise((resolve, reject) => {
|
||||
yauzl.fromBuffer(
|
||||
buffer,
|
||||
/*{lazyEntries:true},*/ (err, zipfile) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(zipfile);
|
||||
}
|
||||
);
|
||||
return;
|
||||
}) as Promise<yauzl.ZipFile>);
|
||||
|
||||
const _ReadEntry = async (
|
||||
zip: yauzl.ZipFile,
|
||||
entry: yauzl.Entry
|
||||
): Promise<Buffer> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
zip.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const chunks: Buffer[] = [];
|
||||
stream.on("data", (chunk) => {
|
||||
if (Buffer.isBuffer(chunk)) {
|
||||
chunks.push(chunk);
|
||||
} else if (typeof chunk === 'string') {
|
||||
chunks.push(Buffer.from(chunk));
|
||||
}
|
||||
});
|
||||
stream.on("end", () => {
|
||||
resolve(Buffer.concat(chunks));
|
||||
});
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
const _openReadStream = async (
|
||||
zip: yauzl.ZipFile,
|
||||
entry: yauzl.Entry
|
||||
): Promise<Stream.Readable> => {
|
||||
return new Promise((resolve, reject) => {
|
||||
zip.openReadStream(entry, (err, stream) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
resolve(stream);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const entries: IentryP[] = [];
|
||||
zip.on("entry", async (entry: yauzl.Entry) => {
|
||||
const entryP = entry as IentryP;
|
||||
//console.log(entry.fileName);
|
||||
entryP.openReadStream = _openReadStream(zip, entry);
|
||||
entryP.ReadEntry = _ReadEntry(zip, entry);
|
||||
entries.push(entryP);
|
||||
if (zip.entryCount === entries.length) {
|
||||
zip.close();
|
||||
resolve(entries);
|
||||
}
|
||||
});
|
||||
zip.on("error", (err) => {
|
||||
reject(err);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function Azip(buffer: Buffer) {
|
||||
const zip = new admZip(buffer);
|
||||
const entries = zip.getEntries();
|
||||
return entries;
|
||||
}
|
||||
110
backend/tsconfig.json
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
/* Visit https://aka.ms/tsconfig to read more about this file */
|
||||
|
||||
/* Projects */
|
||||
// "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
|
||||
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
|
||||
// "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
|
||||
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
|
||||
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
|
||||
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
|
||||
|
||||
/* Language and Environment */
|
||||
"target": "ES2024", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
|
||||
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
|
||||
// "jsx": "preserve", /* Specify what JSX code is generated. */
|
||||
// "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
|
||||
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
|
||||
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
|
||||
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
|
||||
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
|
||||
// "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
|
||||
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
|
||||
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
|
||||
// "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
|
||||
|
||||
/* Modules */
|
||||
"module": "Node16", /* Specify what module code is generated. */
|
||||
"rootDir": "./src", /* Specify the root folder within your source files. */
|
||||
"moduleResolution": "node16", /* Specify how TypeScript looks up a file from a given module specifier. */
|
||||
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
|
||||
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
|
||||
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
|
||||
// "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
|
||||
// "types": [], /* Specify type package names to be included without being referenced in a source file. */
|
||||
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
|
||||
// "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
|
||||
// "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
|
||||
// "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
|
||||
// "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
|
||||
// "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
|
||||
// "noUncheckedSideEffectImports": true, /* Check side effect imports. */
|
||||
// "resolveJsonModule": true, /* Enable importing .json files. */
|
||||
// "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
|
||||
// "noResolve": true, /* Disallow 'import's, 'require's or '<reference>'s from expanding the number of files TypeScript should add to a project. */
|
||||
|
||||
/* JavaScript Support */
|
||||
"allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
|
||||
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
|
||||
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
|
||||
|
||||
/* Emit */
|
||||
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
|
||||
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
|
||||
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
|
||||
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
|
||||
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
|
||||
// "noEmit": true, /* Disable emitting files from a compilation. */
|
||||
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
|
||||
"outDir": "./dist", /* Specify an output folder for all emitted files. */
|
||||
// "removeComments": true, /* Disable emitting comments. */
|
||||
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
|
||||
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
|
||||
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
|
||||
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
|
||||
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
|
||||
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
|
||||
// "newLine": "crlf", /* Set the newline character for emitting files. */
|
||||
// "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
|
||||
// "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
|
||||
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
|
||||
// "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
|
||||
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
|
||||
|
||||
/* Interop Constraints */
|
||||
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
|
||||
// "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
|
||||
// "isolatedDeclarations": true, /* Require sufficient annotation on exports so other tools can trivially generate declaration files. */
|
||||
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
|
||||
"esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
|
||||
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
|
||||
"forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
|
||||
|
||||
/* Type Checking */
|
||||
"strict": true, /* Enable all strict type-checking options. */
|
||||
// "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
|
||||
// "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
|
||||
// "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
|
||||
// "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
|
||||
// "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
|
||||
// "strictBuiltinIteratorReturn": true, /* Built-in iterators are instantiated with a 'TReturn' type of 'undefined' instead of 'any'. */
|
||||
// "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
|
||||
// "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
|
||||
// "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
|
||||
// "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
|
||||
// "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
|
||||
// "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
|
||||
// "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
|
||||
// "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
|
||||
// "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
|
||||
// "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
|
||||
// "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
|
||||
// "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
|
||||
// "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
|
||||
|
||||
/* Completeness */
|
||||
// "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
|
||||
"skipLibCheck": true, /* Skip type checking all .d.ts files. */
|
||||
},"include": ["src/**/*"]
|
||||
}
|
||||
12
front/.env
Normal file
@@ -0,0 +1,12 @@
|
||||
# 前端环境变量配置
|
||||
|
||||
# API服务地址
|
||||
VITE_API_HOST=localhost
|
||||
VITE_API_PORT=37019
|
||||
|
||||
# WebSocket服务地址
|
||||
VITE_WS_HOST=localhost
|
||||
VITE_WS_PORT=37019
|
||||
|
||||
# 构建模式
|
||||
NODE_ENV=development
|
||||
3
front/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# 前端
|
||||
|
||||
Tauri + Vue + TypeScript
|
||||
152
front/build-with-error-handling.js
Normal file
@@ -0,0 +1,152 @@
|
||||
const { execSync } = require('child_process');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
// 打包错误处理函数:提供中文解释和原错误输出
|
||||
function handleBuildError(error, stage) {
|
||||
let chineseExplanation = '';
|
||||
const originalError = error?.toString() || JSON.stringify(error);
|
||||
const stderr = error?.stderr?.toString() || '';
|
||||
const stdout = error?.stdout?.toString() || '';
|
||||
|
||||
if (stderr || stdout) {
|
||||
const errorOutput = (stderr + stdout).toLowerCase();
|
||||
|
||||
if (errorOutput.includes('type') && errorOutput.includes('is not assignable')) {
|
||||
chineseExplanation = '类型错误:类型不匹配,请检查 TypeScript 类型定义';
|
||||
} else if (errorOutput.includes('property') && errorOutput.includes('does not exist')) {
|
||||
chineseExplanation = '类型错误:属性不存在,请检查对象属性名称';
|
||||
} else if (errorOutput.includes('cannot find module') || errorOutput.includes('module not found')) {
|
||||
chineseExplanation = '模块错误:找不到模块,请检查依赖是否正确安装';
|
||||
} else if (errorOutput.includes('enoent') || errorOutput.includes('no such file')) {
|
||||
chineseExplanation = '文件错误:找不到文件或目录,请检查文件路径';
|
||||
} else if (errorOutput.includes('eacces') || errorOutput.includes('permission denied')) {
|
||||
chineseExplanation = '权限错误:权限不足,请以管理员身份运行';
|
||||
} else if (errorOutput.includes('enoent') && errorOutput.includes('cargo')) {
|
||||
chineseExplanation = 'Rust环境错误:未找到Cargo,请确保已安装Rust工具链';
|
||||
} else if (errorOutput.includes('linker') && errorOutput.includes('not found')) {
|
||||
chineseExplanation = '链接器错误:找不到链接器,请安装C++编译工具链';
|
||||
} else if (errorOutput.includes('failed to resolve') || errorOutput.includes('could not resolve')) {
|
||||
chineseExplanation = '依赖解析错误:无法解析依赖,请检查网络连接和依赖配置';
|
||||
} else if (errorOutput.includes('out of memory') || errorOutput.includes('heap')) {
|
||||
chineseExplanation = '内存错误:内存不足,请关闭其他程序或增加系统内存';
|
||||
} else if (errorOutput.includes('disk space') || errorOutput.includes('no space')) {
|
||||
chineseExplanation = '磁盘空间错误:磁盘空间不足,请清理磁盘空间';
|
||||
} else if (errorOutput.includes('certificate') || errorOutput.includes('ssl')) {
|
||||
chineseExplanation = '证书错误:SSL证书问题,请检查系统时间或网络代理设置';
|
||||
} else if (errorOutput.includes('network') || errorOutput.includes('connection')) {
|
||||
chineseExplanation = '网络错误:网络连接失败,请检查网络连接和代理设置';
|
||||
} else if (errorOutput.includes('timeout')) {
|
||||
chineseExplanation = '超时错误:操作超时,请检查网络或增加超时时间';
|
||||
} else if (errorOutput.includes('cargo') && errorOutput.includes('failed')) {
|
||||
chineseExplanation = 'Rust编译错误:Rust代码编译失败,请检查Rust代码';
|
||||
} else if (errorOutput.includes('vite') && errorOutput.includes('error')) {
|
||||
chineseExplanation = 'Vite构建错误:前端构建失败,请检查Vite配置和源代码';
|
||||
} else if (errorOutput.includes('tauri') && errorOutput.includes('error')) {
|
||||
chineseExplanation = 'Tauri打包错误:Tauri打包失败,请检查Tauri配置';
|
||||
} else if (errorOutput.includes('nsis') || errorOutput.includes('wix')) {
|
||||
chineseExplanation = '安装程序错误:安装程序生成失败,请检查Windows安装工具';
|
||||
} else if (errorOutput.includes('icon') || errorOutput.includes('logo')) {
|
||||
chineseExplanation = '图标错误:图标文件问题,请检查图标文件格式和路径';
|
||||
} else if (errorOutput.includes('bundle') || errorOutput.includes('package')) {
|
||||
chineseExplanation = '打包错误:应用程序打包失败,请检查打包配置';
|
||||
} else if (errorOutput.includes('target') && errorOutput.includes('not found')) {
|
||||
chineseExplanation = '目标平台错误:不支持的目标平台,请检查平台配置';
|
||||
} else if (errorOutput.includes('version') || errorOutput.includes('semver')) {
|
||||
chineseExplanation = '版本错误:版本号格式错误,请检查版本号格式';
|
||||
} else {
|
||||
chineseExplanation = '未知错误:打包过程中发生未知错误';
|
||||
}
|
||||
} else {
|
||||
chineseExplanation = '未知错误:无法获取错误详情';
|
||||
}
|
||||
|
||||
const errorDetails = {
|
||||
阶段: stage,
|
||||
中文解释: chineseExplanation,
|
||||
原错误: originalError,
|
||||
完整错误输出: stderr || stdout
|
||||
};
|
||||
|
||||
console.error('\n========================================');
|
||||
console.error('❌ 打包失败');
|
||||
console.error('========================================\n');
|
||||
console.error('📋 错误阶段:', errorDetails.阶段);
|
||||
console.error('🇨🇳 中文解释:', errorDetails.中文解释);
|
||||
console.error('🔍 原错误:', errorDetails.原错误);
|
||||
if (errorDetails.完整错误输出) {
|
||||
console.error('\n📄 完整错误输出:');
|
||||
console.error(errorDetails.完整错误输出);
|
||||
}
|
||||
console.error('\n========================================\n');
|
||||
|
||||
return errorDetails;
|
||||
}
|
||||
|
||||
// 记录构建日志
|
||||
function logBuild(stage, message) {
|
||||
const timestamp = new Date().toLocaleString('zh-CN');
|
||||
const logMessage = `[${timestamp}] [${stage}] ${message}\n`;
|
||||
|
||||
const logFile = path.join(__dirname, 'build-error.log');
|
||||
fs.appendFileSync(logFile, logMessage, 'utf-8');
|
||||
|
||||
console.log(logMessage.trim());
|
||||
}
|
||||
|
||||
// 执行命令并处理错误
|
||||
function executeCommand(command, stage) {
|
||||
try {
|
||||
logBuild(stage, `开始执行: ${command}`);
|
||||
execSync(command, {
|
||||
stdio: 'inherit',
|
||||
shell: true
|
||||
});
|
||||
logBuild(stage, '✅ 执行成功');
|
||||
return true;
|
||||
} catch (error) {
|
||||
handleBuildError(error, stage);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 主构建流程
|
||||
async function build() {
|
||||
console.log('\n========================================');
|
||||
console.log('🚀 开始打包 DeEarthX V3');
|
||||
console.log('========================================\n');
|
||||
|
||||
// 清理旧的日志
|
||||
const logFile = path.join(__dirname, 'build-error.log');
|
||||
if (fs.existsSync(logFile)) {
|
||||
fs.unlinkSync(logFile);
|
||||
}
|
||||
|
||||
// 阶段1: TypeScript 类型检查
|
||||
console.log('📦 阶段 1/4: TypeScript 类型检查');
|
||||
if (!executeCommand('npm run vue-tsc --noEmit', 'TypeScript类型检查')) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 阶段2: Vite 前端构建
|
||||
console.log('\n📦 阶段 2/4: Vite 前端构建');
|
||||
if (!executeCommand('npm run vite build', 'Vite前端构建')) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// 阶段3: Tauri 应用打包
|
||||
console.log('\n📦 阶段 3/4: Tauri 应用打包');
|
||||
if (!executeCommand('npm run tauri build', 'Tauri应用打包')) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
console.log('\n========================================');
|
||||
console.log('✅ 打包完成!');
|
||||
console.error('========================================\n');
|
||||
}
|
||||
|
||||
// 运行构建
|
||||
build().catch(error => {
|
||||
handleBuildError(error, '构建流程');
|
||||
process.exit(1);
|
||||
});
|
||||
15
front/index.html
Normal file
@@ -0,0 +1,15 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link href="./src/tailwind.css" rel="stylesheet">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>DeEarthX V3</title>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</body>
|
||||
</html>
|
||||
257
front/lang/de_de.json
Normal file
@@ -0,0 +1,257 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "Version",
|
||||
"status_loading": "Startet",
|
||||
"status_success": "Normal",
|
||||
"status_error": "Fehler",
|
||||
"backend_status": "Backend-Status",
|
||||
"confirm": "Bestätigen",
|
||||
"cancel": "Abbrechen",
|
||||
"submit": "Absenden",
|
||||
"upload": "Hochladen",
|
||||
"start": "Starten",
|
||||
"save": "Speichern",
|
||||
"delete": "Löschen",
|
||||
"edit": "Bearbeiten",
|
||||
"close": "Schließen",
|
||||
"loading": "Wird geladen...",
|
||||
"error": "Fehler",
|
||||
"success": "Erfolg",
|
||||
"warning": "Warnung",
|
||||
"info": "Information"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Startseite",
|
||||
"deearth": "Filter",
|
||||
"galaxy": "Senden",
|
||||
"template": "Vorlage",
|
||||
"setting": "Einstellungen",
|
||||
"about": "Über"
|
||||
},
|
||||
"home": {
|
||||
"title": "Server-Einrichtung überall und jederzeit!",
|
||||
"mode_title": "Modus-Auswahl",
|
||||
"upload_title": "Dateien per Drag & Drop oder Klick hochladen",
|
||||
"upload_hint": "Bitte verwenden Sie .zip (CurseForge, MCBBS) und .mrpack (Modrinth) Dateien",
|
||||
"mode_server": "Server-Modus",
|
||||
"mode_upload": "Upload-Modus",
|
||||
"preparing": "Wird vorbereitet...",
|
||||
"task_preparing": "Verbinde mit Backend-Dienst...",
|
||||
"task_connecting": "Verbindung zum Backend-Dienst erfolgreich, beginne mit Aufgabenverarbeitung...",
|
||||
"step1_title": "Modpack entpacken",
|
||||
"step1_desc": "Inhalt entpacken und Dateien herunterladen",
|
||||
"step2_title": "Mods filtern",
|
||||
"step2_desc": "Kernfunktion von DeEarthX",
|
||||
"step3_title": "Server herunterladen",
|
||||
"step3_desc": "Mod-Loader-Server installieren",
|
||||
"step4_title": "Abgeschlossen",
|
||||
"step4_desc": "Alles bereit!",
|
||||
"progress_title": "Erstellungsfortschritt",
|
||||
"upload_progress": "Upload-Fortschritt",
|
||||
"unzip_progress": "Entpack-Fortschritt",
|
||||
"download_progress": "Download-Fortschritt",
|
||||
"server_install_progress": "Server-Installation",
|
||||
"server_install_step": "Schritt",
|
||||
"server_install_message": "Nachricht",
|
||||
"server_install_completed": "Installation abgeschlossen!",
|
||||
"server_install_error": "Installation fehlgeschlagen",
|
||||
"server_install_duration": "Dauer",
|
||||
"filter_mods_progress": "Mods filtern",
|
||||
"filter_mods_total": "Gesamtzahl der Mods",
|
||||
"filter_mods_current": "Aktuell wird geprüft",
|
||||
"filter_mods_completed": "Filtern abgeschlossen! {filtered} Client-Mods identifiziert, {moved} verschoben",
|
||||
"filter_mods_error": "Filtern fehlgeschlagen",
|
||||
"start_production": "Erstellung starten, Menü nicht wechseln!",
|
||||
"production_complete": "Server-Erstellung abgeschlossen! Dauer {time} Sekunden!",
|
||||
"please_select_file": "Bitte zuerst Dateien per Drag & Drop oder Auswahl hochladen",
|
||||
"only_zip_mrpack": "Nur .zip und .mrpack Dateien können hochgeladen werden",
|
||||
"file_prepared": "Datei bereit",
|
||||
"preparing_file": "Datei wird vorbereitet...",
|
||||
"ws_connecting": "WebSocket-Verbindung wird hergestellt...",
|
||||
"ws_connected": "WebSocket-Verbindung erfolgreich",
|
||||
"ws_failed": "WebSocket-Verbindung fehlgeschlagen",
|
||||
"request_failed": "Anfrage an Backend-Dienst fehlgeschlagen, bitte prüfen Sie, ob der Backend-Dienst läuft",
|
||||
"backend_error": "DeEarthX.Core hat einen schwerwiegenden Fehler festgestellt!",
|
||||
"backend_error_desc": "Bitte machen Sie einen Screenshot des gesamten Fensters und senden Sie ihn in die Gruppe\nFehlerinformationen: {error}",
|
||||
"java_not_found": "Java nicht in Systemvariablen gefunden! Bitte Java installieren, sonst kann der Server-Modus nicht verwendet werden!",
|
||||
"unknown_step": "Unbekannter Schritt",
|
||||
"parse_error": "Analyse der Servernachricht fehlgeschlagen",
|
||||
"speed": "Geschwindigkeit",
|
||||
"remaining": "Verbleibende Zeit",
|
||||
"java_error_title": "Java nicht gefunden",
|
||||
"java_error_desc": "Java nicht in Systemvariablen gefunden! Bitte Java installieren, sonst kann der Server-Modus nicht verwendet werden!\n\nVorschläge:\n1. Java 17 oder höher herunterladen und installieren\n2. Java-Umgebungsvariablen konfigurieren\n3. Anwendung neu starten",
|
||||
"network_error_title": "Netzwerkfehler",
|
||||
"network_error_desc": "Netzwerkverbindungsproblem\nFehlerinformationen: {error}",
|
||||
"file_error_title": "Dateifehler",
|
||||
"file_error_desc": "Dateioperationsproblem\nFehlerinformationen: {error}",
|
||||
"memory_error_title": "Speicherfehler",
|
||||
"memory_error_desc": "Unzureichender Speicher\nFehlerinformationen: {error}",
|
||||
"unknown_error_title": "Unbekannter Fehler",
|
||||
"unknown_error_desc": "Ein unbekannter Fehler ist aufgetreten, bitte wenden Sie sich an den technischen Support",
|
||||
"ws_error_title": "WebSocket-Verbindung fehlgeschlagen",
|
||||
"ws_error_desc": "Keine WebSocket-Verbindung zum Backend herstellbar",
|
||||
"suggestions": "Vorgeschlagene Lösungen",
|
||||
"suggestion_check_network": "Prüfen, ob die Netzwerkverbindung normal ist",
|
||||
"suggestion_check_firewall": "Firewall-Einstellungen prüfen",
|
||||
"suggestion_retry": "Später erneut versuchen",
|
||||
"suggestion_check_disk_space": "Prüfen, ob genügend Speicherplatz vorhanden ist",
|
||||
"suggestion_check_permission": "Dateiberechtigungseinstellungen prüfen",
|
||||
"suggestion_check_file_format": "Bestätigen, dass das Dateiformat korrekt ist",
|
||||
"suggestion_increase_memory": "Der Anwendung mehr Speicher zuweisen",
|
||||
"suggestion_close_other_apps": "Andere Anwendungen schließen, die Speicher verwenden",
|
||||
"suggestion_restart_application": "Anwendung neu starten",
|
||||
"suggestion_check_backend": "Prüfen, ob der Backend-Dienst normal ausgeführt wird",
|
||||
"suggestion_check_logs": "Protokolldateien für weitere Informationen prüfen",
|
||||
"suggestion_check_port": "Prüfen, ob Port 37019 belegt ist",
|
||||
"suggestion_contact_support": "Technischen Support für Hilfe kontaktieren",
|
||||
"template_select_title": "Server-Vorlage auswählen",
|
||||
"template_select_desc": "Wählen Sie eine Vorlage für Ihren Server. Die Verwendung einer Vorlage überspringt die Server-Installation und kopiert die Vorlagendateien direkt in das Serververzeichnis",
|
||||
"template_official_loader": "Keine",
|
||||
"template_official_loader_desc": "Keine Vorlage verwenden, Standard-Server-Installation durchführen",
|
||||
"template_name": "Vorlagenname",
|
||||
"template_description": "Beschreibung",
|
||||
"template_author": "Autor",
|
||||
"template_version": "Version",
|
||||
"template_selected": "Ausgewählte Vorlage",
|
||||
"template_select_button": "Vorlage auswählen",
|
||||
"template_loading": "Vorlagenliste wird geladen...",
|
||||
"template_load_failed": "Laden der Vorlagenliste fehlgeschlagen",
|
||||
"template_apply_success": "Vorlage erfolgreich angewendet",
|
||||
"template_apply_failed": "Anwenden der Vorlage fehlgeschlagen"
|
||||
},
|
||||
"template": {
|
||||
"title": "Vorlagenverwaltung",
|
||||
"description": "Verwalten Sie Ihre Server-Vorlagen, erstellen, anzeigen und löschen Sie Vorlagen",
|
||||
"create_button": "Vorlage erstellen",
|
||||
"create_title": "Neue Vorlage erstellen",
|
||||
"name": "Vorlagenname",
|
||||
"name_required": "Vorlagenname ist erforderlich",
|
||||
"name_placeholder": "Bitte Vorlagenname eingeben",
|
||||
"version": "Version",
|
||||
"version_placeholder": "Bitte Versionsnummer eingeben",
|
||||
"description": "Beschreibung",
|
||||
"description_placeholder": "Bitte Vorlagenbeschreibung eingeben",
|
||||
"author": "Autor",
|
||||
"author_placeholder": "Bitte Autorenname eingeben",
|
||||
"delete_button": "Löschen",
|
||||
"delete_title": "Vorlage löschen",
|
||||
"delete_confirm": "Möchten Sie die Vorlage \"{name}\" wirklich löschen?",
|
||||
"delete_warning": "Dieser Vorgang löscht die Vorlage und alle ihre Dateien dauerhaft und kann nicht rückgängig gemacht werden.",
|
||||
"delete_success": "Vorlage erfolgreich gelöscht",
|
||||
"delete_failed": "Löschen der Vorlage fehlgeschlagen",
|
||||
"create_success": "Vorlage erfolgreich erstellt",
|
||||
"create_failed": "Erstellen der Vorlage fehlgeschlagen",
|
||||
"open_folder": "Ordner öffnen",
|
||||
"open_folder_success": "Vorlagenordner geöffnet",
|
||||
"open_folder_failed": "Ordner öffnen fehlgeschlagen",
|
||||
"edit_button": "Bearbeiten",
|
||||
"edit_title": "Vorlage bearbeiten",
|
||||
"update_success": "Vorlage erfolgreich aktualisiert",
|
||||
"update_failed": "Aktualisieren der Vorlage fehlgeschlagen",
|
||||
"empty": "Noch keine Vorlagen",
|
||||
"empty_hint": "Klicken Sie auf die Schaltfläche oben, um Ihre erste Vorlage zu erstellen"
|
||||
},
|
||||
"setting": {
|
||||
"title": "DeEarthX Einstellungen",
|
||||
"subtitle": "Machen Sie DeEarthX V3 noch besser für Sie!",
|
||||
"category_filter": "Mod-Filter-Einstellungen",
|
||||
"category_mirror": "Download-Quellen-Einstellungen",
|
||||
"category_system": "Systemverwaltungseinstellungen",
|
||||
"filter_hashes_name": "Hash-Filterung",
|
||||
"filter_hashes_desc": "Unnötige Client-Mods filtern (Hash-Filter-Methode)",
|
||||
"filter_dexpub_name": "Galaxy Square-Filterung",
|
||||
"filter_dexpub_desc": "Client-Dateien filtern, die auf der Galaxy Square-Plattform erfasst wurden",
|
||||
"filter_modrinth_name": "Modrinth-API-Filterung",
|
||||
"filter_modrinth_desc": "Client/Server-Kompatibilität von Mods über Modrinth-API prüfen",
|
||||
"filter_mixins_name": "Mixin-Filterung",
|
||||
"filter_mixins_desc": "Client Mixin-bezogene Dateien filtern",
|
||||
"mirror_mcimirror_name": "MCIM-Spiegelquelle",
|
||||
"mirror_mcimirror_desc": "MCIM-Spiegelquelle zum Beschleunigen von Downloads verwenden",
|
||||
"mirror_bmclapi_name": "BMCLAPI-Spiegelquelle",
|
||||
"mirror_bmclapi_desc": "BMCLAPI-Spiegelquelle zum Beschleunigen von Downloads verwenden",
|
||||
"system_oaf_name": "Verzeichnis nach Vorgang öffnen",
|
||||
"system_oaf_desc": "Verzeichnis automatisch nach Server-Erstellung öffnen",
|
||||
"system_autozip_name": "Automatisch als zip packen",
|
||||
"system_autozip_desc": "Automatisch als zip packen nach Server-Erstellung (nicht packen im Server-Modus)",
|
||||
"switch_on": "Ein",
|
||||
"switch_off": "Aus",
|
||||
"language_title": "Spracheinstellungen",
|
||||
"language_desc": "Oberflächensprache auswählen",
|
||||
"language_chinese": "Vereinfachtes Chinesisch",
|
||||
"language_english": "English",
|
||||
"language_japanese": "日本語",
|
||||
"language_french": "Français",
|
||||
"language_german": "Deutsch",
|
||||
"language_spanish": "Español",
|
||||
"config_saved": "Konfiguration gespeichert",
|
||||
"config_load_failed": "Laden der Konfiguration fehlgeschlagen",
|
||||
"config_save_failed": "Speichern der Konfiguration fehlgeschlagen",
|
||||
"export_config": "Konfiguration exportieren",
|
||||
"import_config": "Konfiguration importieren",
|
||||
"config_exported": "Konfiguration erfolgreich exportiert",
|
||||
"config_export_failed": "Export der Konfiguration fehlgeschlagen",
|
||||
"config_imported": "Konfiguration erfolgreich importiert",
|
||||
"config_import_failed": "Import der Konfiguration fehlgeschlagen",
|
||||
"config_invalid_format": "Ungültiges Konfigurationsdateiformat"
|
||||
},
|
||||
"about": {
|
||||
"title": "Über DeEarthX",
|
||||
"subtitle": "Professionelles Tool zur Erstellung von Minecraft-Modpack-Servern",
|
||||
"about_software": "Über die Software",
|
||||
"current_version": "Aktuelle Version:",
|
||||
"build_time": "Build-Zeit:",
|
||||
"author": "Autor:",
|
||||
"development_team": "Entwicklungsteam",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "Autor",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2. Entwickler (Verbesserungen und Optimierungen)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "BMCLAPI-Spiegel",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "MCIM-Spiegel",
|
||||
"sponsor": "Sponsoren",
|
||||
"sponsor_elfidc": "亿讯云",
|
||||
"sponsor_type_gold": "Gold-Sponsor",
|
||||
"version_file_read_failed": "Lesen der Versionsdatei fehlgeschlagen"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "Lassen Sie alle Mods hier glänzen",
|
||||
"mod_submit_title": "Mod-Einreichung",
|
||||
"mod_type_label": "Mod-Typ",
|
||||
"mod_type_client": "Client-Mod",
|
||||
"mod_type_server": "Server-Mod",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "Geben Sie ein Modid ein (mehrere durch Kommas getrennt) oder laden Sie eine Datei hoch, um automatisch zu erhalten",
|
||||
"modid_count": "{count} Modids aktuell hinzugefügt",
|
||||
"upload_file_label": "Datei hochladen",
|
||||
"upload_file_hint": "Klicken oder Dateien hierher ziehen zum Hochladen",
|
||||
"upload_file_support": "Unterstützt .jar-Dateien, Mehrfachauswahl möglich",
|
||||
"file_selected": "{count} Dateien ausgewählt",
|
||||
"start_upload": "Upload starten",
|
||||
"uploading": "Wird hochgeladen...",
|
||||
"submit": "{type}-Mods senden",
|
||||
"submitting": "Wird gesendet...",
|
||||
"submit_confirm_title": "Senden bestätigen",
|
||||
"submit_confirm_content": "Möchten Sie wirklich {count} {type}-Mods senden?",
|
||||
"please_select_file": "Bitte zuerst Dateien auswählen",
|
||||
"upload_success": "{count} Dateien erfolgreich hochgeladen",
|
||||
"data_format_error": "Fehler im zurückgegebenen Datenformat",
|
||||
"upload_failed": "Upload fehlgeschlagen",
|
||||
"upload_error": "Upload-Fehler, bitte erneut versuchen",
|
||||
"submit_success": "{type}-Mods erfolgreich gesendet",
|
||||
"submit_failed": "Senden fehlgeschlagen",
|
||||
"submit_error": "Sendefehler, bitte erneut versuchen"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core läuft bereits",
|
||||
"backend_started": "DeEarthX.Core erfolgreich gestartet",
|
||||
"backend_port_occupied": "Port 37019 wird von einer anderen Anwendung belegt!",
|
||||
"backend_start_failed": "DeEarthX.Core konnte nicht gestartet werden, prüfen Sie, ob Port 37019 belegt ist! ({count} Wiederholungsversuche)",
|
||||
"backend_restart": "DeEarthX.Core wird neu gestartet!",
|
||||
"retry_start": "Start fehlgeschlagen, Wiederholung ({current}/{max})...",
|
||||
"config_load_error": "Laden der Konfiguration fehlgeschlagen"
|
||||
}
|
||||
}
|
||||
271
front/lang/en_us.json
Normal file
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "Version",
|
||||
"status_loading": "Starting",
|
||||
"status_success": "Normal",
|
||||
"status_error": "Error",
|
||||
"backend_status": "Backend Status",
|
||||
"confirm": "Confirm",
|
||||
"cancel": "Cancel",
|
||||
"submit": "Submit",
|
||||
"upload": "Upload",
|
||||
"start": "Start",
|
||||
"save": "Save",
|
||||
"delete": "Delete",
|
||||
"edit": "Edit",
|
||||
"close": "Close",
|
||||
"loading": "Loading...",
|
||||
"error": "Error",
|
||||
"success": "Success",
|
||||
"warning": "Warning",
|
||||
"info": "Info"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Home",
|
||||
"deearth": "Filter",
|
||||
"galaxy": "Submit",
|
||||
"template": "Template",
|
||||
"setting": "Settings",
|
||||
"about": "About"
|
||||
},
|
||||
"home": {
|
||||
"title": "Make server hosting available anytime, anywhere!",
|
||||
"mode_title": "Mode Selection",
|
||||
"upload_title": "Drag and drop files here or click to upload",
|
||||
"upload_hint": "Supports .zip (CurseForge, MCBBS) and .mrpack (Modrinth) files",
|
||||
"mode_server": "Server Mode",
|
||||
"mode_upload": "Upload Mode",
|
||||
"preparing": "Preparing...",
|
||||
"task_preparing": "Connecting to backend service...",
|
||||
"task_connecting": "Backend service connected successfully, starting to process task...",
|
||||
"step1_title": "Extract Modpack",
|
||||
"step1_desc": "Extract content and download files",
|
||||
"step2_title": "Filter Mods",
|
||||
"step2_desc": "Core feature of DeEarthX",
|
||||
"step3_title": "Download Server",
|
||||
"step3_desc": "Install mod loader server",
|
||||
"step4_title": "Complete",
|
||||
"step4_desc": "Everything is ready!",
|
||||
"progress_title": "Production Progress",
|
||||
"upload_progress": "Upload Progress",
|
||||
"unzip_progress": "Extraction Progress",
|
||||
"download_progress": "Download Progress",
|
||||
"server_install_progress": "Server Installation",
|
||||
"server_install_step": "Step",
|
||||
"server_install_message": "Message",
|
||||
"server_install_completed": "Installation completed!",
|
||||
"server_install_error": "Installation failed",
|
||||
"server_install_duration": "Duration",
|
||||
"filter_mods_progress": "Filtering Mods",
|
||||
"filter_mods_total": "Total Mods",
|
||||
"filter_mods_current": "Currently Checking",
|
||||
"filter_mods_completed": "Filtering completed! Identified {filtered} client mods, moved {moved}",
|
||||
"filter_mods_error": "Filtering failed",
|
||||
"start_production": "Starting production, please do not switch menus!",
|
||||
"production_complete": "Server production completed! Total time: {time} seconds!",
|
||||
"please_select_file": "Please drag and drop or select a file first",
|
||||
"only_zip_mrpack": "Only .zip and .mrpack files are supported",
|
||||
"file_prepared": "File preparation completed",
|
||||
"preparing_file": "Preparing file...",
|
||||
"ws_connecting": "Establishing WebSocket connection...",
|
||||
"ws_connected": "WebSocket connected successfully",
|
||||
"ws_failed": "WebSocket connection failed",
|
||||
"request_failed": "Failed to request backend service, please check if backend service is running",
|
||||
"backend_error": "DeEarthX.Core encountered a fatal error!",
|
||||
"backend_error_desc": "Please take a screenshot of the entire window and send it to the group\nError: {error}",
|
||||
"java_not_found": "Java not found in system variables! Please install Java, otherwise server mode will be unavailable!",
|
||||
"unknown_step": "Unknown step",
|
||||
"parse_error": "Failed to parse server message",
|
||||
"speed": "Speed",
|
||||
"remaining": "Remaining",
|
||||
"java_error_title": "Java Not Found",
|
||||
"java_error_desc": "Java not found in system variables! Please install Java, otherwise server mode will be unavailable!\n\nSuggestions:\n1. Download and install Java 17 or higher\n2. Configure Java environment variables\n3. Restart the application",
|
||||
"network_error_title": "Network Error",
|
||||
"network_error_desc": "Network connection issue\nError: {error}",
|
||||
"file_error_title": "File Error",
|
||||
"file_error_desc": "File operation issue\nError: {error}",
|
||||
"memory_error_title": "Memory Error",
|
||||
"memory_error_desc": "Insufficient memory\nError: {error}",
|
||||
"unknown_error_title": "Unknown Error",
|
||||
"unknown_error_desc": "An unknown error occurred, please contact technical support",
|
||||
"ws_error_title": "WebSocket Connection Failed",
|
||||
"ws_error_desc": "Unable to establish WebSocket connection with backend",
|
||||
"suggestions": "Suggested Solutions",
|
||||
"suggestion_check_network": "Check if network connection is normal",
|
||||
"suggestion_check_firewall": "Check firewall settings",
|
||||
"suggestion_retry": "Try again later",
|
||||
"suggestion_check_disk_space": "Check if disk space is sufficient",
|
||||
"suggestion_check_permission": "Check file permission settings",
|
||||
"suggestion_check_file_format": "Confirm file format is correct",
|
||||
"suggestion_increase_memory": "Increase memory allocated to the application",
|
||||
"suggestion_close_other_apps": "Close other applications that use memory",
|
||||
"suggestion_restart_application": "Restart the application",
|
||||
"suggestion_check_backend": "Check if backend service is running normally",
|
||||
"suggestion_check_logs": "Check log files for more information",
|
||||
"suggestion_check_port": "Check if port 37019 is occupied",
|
||||
"suggestion_contact_support": "Contact technical support for help",
|
||||
"template_select_title": "Select Server Template",
|
||||
"template_select_desc": "Choose a template for your server. Using a template will skip server installation and directly copy template files to the server directory",
|
||||
"template_official_loader": "None",
|
||||
"template_official_loader_desc": "Do not use any template, perform standard server installation",
|
||||
"template_name": "Template Name",
|
||||
"template_description": "Description",
|
||||
"template_author": "Author",
|
||||
"template_version": "Version",
|
||||
"template_selected": "Selected Template",
|
||||
"template_select_button": "Select Template",
|
||||
"template_loading": "Loading template list...",
|
||||
"template_load_failed": "Failed to load template list",
|
||||
"template_apply_success": "Template applied successfully",
|
||||
"template_apply_failed": "Failed to apply template",
|
||||
"template_import_title": "Import Template",
|
||||
"template_import_hint": "Drag or click to upload template zip file",
|
||||
"template_import_success": "Template imported successfully",
|
||||
"template_import_failed": "Failed to import template",
|
||||
"template_export_button": "Export",
|
||||
"template_export_success": "Template exported successfully",
|
||||
"template_export_failed": "Failed to export template",
|
||||
"template_export_progress": "Exporting template...",
|
||||
"template_import_progress": "Importing template...",
|
||||
"template_download_progress": "Downloading template..."
|
||||
},
|
||||
"template": {
|
||||
"title": "Template Management",
|
||||
"description": "Manage your server templates, create, view and delete templates",
|
||||
"create_button": "Create Template",
|
||||
"create_title": "Create New Template",
|
||||
"name": "Template Name",
|
||||
"name_required": "Template name is required",
|
||||
"name_placeholder": "Please enter template name",
|
||||
"version": "Version",
|
||||
"version_placeholder": "Please enter version number",
|
||||
"description": "Description",
|
||||
"description_placeholder": "Please enter template description",
|
||||
"author": "Author",
|
||||
"author_placeholder": "Please enter author name",
|
||||
"delete_button": "Delete",
|
||||
"delete_title": "Delete Template",
|
||||
"delete_confirm": "Are you sure you want to delete template \"{name}\"?",
|
||||
"delete_warning": "This operation will permanently delete this template and all its files, cannot be recovered.",
|
||||
"delete_success": "Template deleted successfully",
|
||||
"delete_failed": "Failed to delete template",
|
||||
"create_success": "Template created successfully",
|
||||
"create_failed": "Failed to create template",
|
||||
"open_folder": "Open Folder",
|
||||
"open_folder_success": "Template folder opened",
|
||||
"open_folder_failed": "Failed to open folder",
|
||||
"edit_button": "Edit",
|
||||
"edit_title": "Edit Template",
|
||||
"update_success": "Template updated successfully",
|
||||
"update_failed": "Failed to update template",
|
||||
"empty": "No templates yet",
|
||||
"empty_hint": "Click the button above to create your first template",
|
||||
"local_templates": "Local Templates",
|
||||
"template_store": "Template Store",
|
||||
"store_empty": "Template store is empty",
|
||||
"store_empty_hint": "No templates available at the moment",
|
||||
"install_button": "Install",
|
||||
"install_success": "Template installed successfully",
|
||||
"install_failed": "Failed to install template",
|
||||
"store_load_failed": "Failed to load template store"
|
||||
},
|
||||
"setting": {
|
||||
"title": "DeEarthX Settings",
|
||||
"subtitle": "Make your DeEarthX V3 suit you better!",
|
||||
"category_filter": "Mod Filter Settings",
|
||||
"category_mirror": "Download Source Settings",
|
||||
"category_system": "System Management Settings",
|
||||
"filter_hashes_name": "Hash Filter",
|
||||
"filter_hashes_desc": "Filter unnecessary client mods (hash filtering method)",
|
||||
"filter_dexpub_name": "Galaxy Square Filter",
|
||||
"filter_dexpub_desc": "Filter client files recorded in Galaxy Square platform",
|
||||
"filter_modrinth_name": "Modrinth API Filter",
|
||||
"filter_modrinth_desc": "Check mod client/server compatibility through Modrinth API",
|
||||
"filter_mixins_name": "Mixin Filter",
|
||||
"filter_mixins_desc": "Filter Client Mixin related files",
|
||||
"mirror_mcimirror_name": "MCIM Mirror",
|
||||
"mirror_mcimirror_desc": "Use MCIM mirror to accelerate downloads",
|
||||
"mirror_bmclapi_name": "BMCLAPI Mirror",
|
||||
"mirror_bmclapi_desc": "Use BMCLAPI mirror to accelerate downloads",
|
||||
"system_oaf_name": "Open directory after operation",
|
||||
"system_oaf_desc": "Automatically open directory after server production is completed",
|
||||
"system_autozip_name": "Auto package to zip",
|
||||
"system_autozip_desc": "Automatically package to zip after server production is completed (not packaged in server mode)",
|
||||
"switch_on": "On",
|
||||
"switch_off": "Off",
|
||||
"language_title": "Language Settings",
|
||||
"language_desc": "Select interface display language",
|
||||
"language_chinese": "简体中文",
|
||||
"language_english": "English",
|
||||
"config_saved": "Configuration saved",
|
||||
"config_load_failed": "Failed to load configuration",
|
||||
"config_save_failed": "Failed to save configuration",
|
||||
"export_config": "Export Config",
|
||||
"import_config": "Import Config",
|
||||
"config_exported": "Configuration exported successfully",
|
||||
"config_export_failed": "Failed to export configuration",
|
||||
"config_imported": "Configuration imported successfully",
|
||||
"config_import_failed": "Failed to import configuration",
|
||||
"config_invalid_format": "Invalid configuration file format"
|
||||
},
|
||||
"about": {
|
||||
"title": "About DeEarthX",
|
||||
"subtitle": "Professional Minecraft modpack server production tool",
|
||||
"about_software": "About Software",
|
||||
"current_version": "Current version: ",
|
||||
"build_time": "Build time: ",
|
||||
"author": "Author: ",
|
||||
"development_team": "Development Team",
|
||||
"author_tianpao": "Tianpao",
|
||||
"contribution_author": "Author",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "Developer 2 (Improvement and Optimization)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "BMCLAPI Mirror",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "MCIM Mirror",
|
||||
"sponsor": "Sponsors",
|
||||
"sponsor_elfidc": "ElfIDC",
|
||||
"sponsor_type_gold": "Gold Sponsor",
|
||||
"version_file_read_failed": "Failed to read version file"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "Let all mods shine here",
|
||||
"mod_submit_title": "Mod Submission",
|
||||
"mod_type_label": "Mod Type",
|
||||
"mod_type_client": "Client Mod",
|
||||
"mod_type_server": "Server Mod",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "Please enter Modid (separated by commas) or upload files to automatically obtain",
|
||||
"modid_count": "Currently added {count} Modids",
|
||||
"upload_file_label": "Upload File",
|
||||
"upload_file_hint": "Click or drag files to this area to upload",
|
||||
"upload_file_support": "Supports .jar format files, multiple selection allowed",
|
||||
"file_selected": "{count} files selected",
|
||||
"start_upload": "Start Upload",
|
||||
"uploading": "Uploading...",
|
||||
"submit": "Submit {type} Mod",
|
||||
"submitting": "Submitting...",
|
||||
"submit_confirm_title": "Confirm Submission",
|
||||
"submit_confirm_content": "Are you sure you want to submit {count} {type} mods?",
|
||||
"please_select_file": "Please select files first",
|
||||
"upload_success": "Successfully uploaded {count} files",
|
||||
"data_format_error": "Returned data format error",
|
||||
"upload_failed": "Upload failed",
|
||||
"upload_error": "Upload error, please try again",
|
||||
"submit_success": "{type} mod submitted successfully",
|
||||
"submit_failed": "Submission failed",
|
||||
"submit_error": "Submission error, please try again"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core is already running",
|
||||
"backend_started": "DeEarthX.Core started successfully",
|
||||
"backend_port_occupied": "Port 37019 is occupied by another application!",
|
||||
"backend_start_failed": "DeEarthX.Core failed to start, please check if port 37019 is occupied! (Retried {count} times)",
|
||||
"backend_restart": "DeEarthX.Core restarting!",
|
||||
"retry_start": "Start failed, retrying ({current}/{max})...",
|
||||
"config_load_error": "Failed to load configuration"
|
||||
}
|
||||
}
|
||||
257
front/lang/es_es.json
Normal file
@@ -0,0 +1,257 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "Versión",
|
||||
"status_loading": "Iniciando",
|
||||
"status_success": "Normal",
|
||||
"status_error": "Error",
|
||||
"backend_status": "Estado del backend",
|
||||
"confirm": "Confirmar",
|
||||
"cancel": "Cancelar",
|
||||
"submit": "Enviar",
|
||||
"upload": "Subir",
|
||||
"start": "Iniciar",
|
||||
"save": "Guardar",
|
||||
"delete": "Eliminar",
|
||||
"edit": "Editar",
|
||||
"close": "Cerrar",
|
||||
"loading": "Cargando...",
|
||||
"error": "Error",
|
||||
"success": "Éxito",
|
||||
"warning": "Advertencia",
|
||||
"info": "Información"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Inicio",
|
||||
"deearth": "Filtro",
|
||||
"galaxy": "Enviar",
|
||||
"template": "Plantilla",
|
||||
"setting": "Configuración",
|
||||
"about": "Acerca de"
|
||||
},
|
||||
"home": {
|
||||
"title": "¡Crea servidores donde quieras, cuando quieras!",
|
||||
"mode_title": "Selección de modo",
|
||||
"upload_title": "Arrastra o haz clic para subir archivos",
|
||||
"upload_hint": "Por favor usa archivos .zip (CurseForge, MCBBS) y .mrpack (Modrinth)",
|
||||
"mode_server": "Modo servidor",
|
||||
"mode_upload": "Modo subida",
|
||||
"preparing": "Preparando...",
|
||||
"task_preparing": "Conectando al servicio backend...",
|
||||
"task_connecting": "Conexión exitosa al servicio backend, comenzando procesamiento de tareas...",
|
||||
"step1_title": "Extraer pack",
|
||||
"step1_desc": "Extraer contenido y descargar archivos",
|
||||
"step2_title": "Filtrar mods",
|
||||
"step2_desc": "Función principal de DeEarthX",
|
||||
"step3_title": "Descargar servidor",
|
||||
"step3_desc": "Instalar servidor de cargador de mods",
|
||||
"step4_title": "Completado",
|
||||
"step4_desc": "¡Todo listo!",
|
||||
"progress_title": "Progreso de creación",
|
||||
"upload_progress": "Progreso de subida",
|
||||
"unzip_progress": "Progreso de extracción",
|
||||
"download_progress": "Progreso de descarga",
|
||||
"server_install_progress": "Instalación del servidor",
|
||||
"server_install_step": "Paso",
|
||||
"server_install_message": "Mensaje",
|
||||
"server_install_completed": "¡Instalación completada!",
|
||||
"server_install_error": "Error de instalación",
|
||||
"server_install_duration": "Duración",
|
||||
"filter_mods_progress": "Filtrado de mods",
|
||||
"filter_mods_total": "Total de mods",
|
||||
"filter_mods_current": "Verificando actualmente",
|
||||
"filter_mods_completed": "¡Filtrado completado! Se identificaron {filtered} mods de cliente, se movieron {moved}",
|
||||
"filter_mods_error": "Filtrado fallido",
|
||||
"start_production": "Iniciar creación, ¡no cambies de menú!",
|
||||
"production_complete": "¡Creación del servidor completada! Duración {time} segundos!",
|
||||
"please_select_file": "Por favor arrastra o selecciona un archivo primero",
|
||||
"only_zip_mrpack": "Solo se pueden subir archivos .zip y .mrpack",
|
||||
"file_prepared": "Archivo preparado",
|
||||
"preparing_file": "Preparando archivo...",
|
||||
"ws_connecting": "Estableciendo conexión WebSocket...",
|
||||
"ws_connected": "Conexión WebSocket exitosa",
|
||||
"ws_failed": "Fallo en la conexión WebSocket",
|
||||
"request_failed": "Fallo en la solicitud al servicio backend, verifica que el servicio backend esté en ejecución",
|
||||
"backend_error": "¡DeEarthX.Core encontró un error fatal!",
|
||||
"backend_error_desc": "Por favor toma una captura de pantalla completa de la ventana y envíala al grupo\nInformación de error: {error}",
|
||||
"java_not_found": "¡Java no encontrado en las variables del sistema! Por favor instala Java, de lo contrario el modo servidor no se podrá usar!",
|
||||
"unknown_step": "Paso desconocido",
|
||||
"parse_error": "Fallo al analizar mensaje del servidor",
|
||||
"speed": "Velocidad",
|
||||
"remaining": "Tiempo restante",
|
||||
"java_error_title": "Java no encontrado",
|
||||
"java_error_desc": "¡Java no encontrado en las variables del sistema! Por favor instala Java, de lo contrario el modo servidor no se podrá usar!\n\nSugerencias:\n1. Descargar e instalar Java 17 o superior\n2. Configurar variables de entorno Java\n3. Reiniciar la aplicación",
|
||||
"network_error_title": "Error de red",
|
||||
"network_error_desc": "Problema de conexión de red\nInformación de error: {error}",
|
||||
"file_error_title": "Error de archivo",
|
||||
"file_error_desc": "Problema de operación de archivo\nInformación de error: {error}",
|
||||
"memory_error_title": "Error de memoria",
|
||||
"memory_error_desc": "Memoria insuficiente\nInformación de error: {error}",
|
||||
"unknown_error_title": "Error desconocido",
|
||||
"unknown_error_desc": "Ocurrió un error desconocido, por favor contacta al soporte técnico",
|
||||
"ws_error_title": "Fallo en la conexión WebSocket",
|
||||
"ws_error_desc": "No se puede establecer conexión WebSocket con el backend",
|
||||
"suggestions": "Soluciones sugeridas",
|
||||
"suggestion_check_network": "Verificar si la conexión de red es normal",
|
||||
"suggestion_check_firewall": "Verificar configuración de firewall",
|
||||
"suggestion_retry": "Intentar de nuevo más tarde",
|
||||
"suggestion_check_disk_space": "Verificar si hay suficiente espacio en disco",
|
||||
"suggestion_check_permission": "Verificar configuración de permisos de archivos",
|
||||
"suggestion_check_file_format": "Confirmar que el formato de archivo es correcto",
|
||||
"suggestion_increase_memory": "Aumentar memoria asignada a la aplicación",
|
||||
"suggestion_close_other_apps": "Cerrar otras aplicaciones que usen memoria",
|
||||
"suggestion_restart_application": "Reiniciar la aplicación",
|
||||
"suggestion_check_backend": "Verificar si el servicio backend está funcionando normalmente",
|
||||
"suggestion_check_logs": "Verificar archivos de registro para más información",
|
||||
"suggestion_check_port": "Verificar si el puerto 37019 está ocupado",
|
||||
"suggestion_contact_support": "Contactar soporte técnico para obtener ayuda",
|
||||
"template_select_title": "Seleccionar plantilla de servidor",
|
||||
"template_select_desc": "Elija una plantilla para su servidor. El uso de una plantilla omitirá la instalación del servidor y copiará directamente los archivos de la plantilla al directorio del servidor",
|
||||
"template_official_loader": "Ninguno",
|
||||
"template_official_loader_desc": "No usar ninguna plantilla, realizar instalación estándar del servidor",
|
||||
"template_name": "Nombre de plantilla",
|
||||
"template_description": "Descripción",
|
||||
"template_author": "Autor",
|
||||
"template_version": "Versión",
|
||||
"template_selected": "Plantilla seleccionada",
|
||||
"template_select_button": "Seleccionar plantilla",
|
||||
"template_loading": "Cargando lista de plantillas...",
|
||||
"template_load_failed": "Error al cargar la lista de plantillas",
|
||||
"template_apply_success": "Plantilla aplicada con éxito",
|
||||
"template_apply_failed": "Error al aplicar la plantilla"
|
||||
},
|
||||
"template": {
|
||||
"title": "Gestión de plantillas",
|
||||
"description": "Gestiona tus plantillas de servidor, crea, visualiza y elimina plantillas",
|
||||
"create_button": "Crear plantilla",
|
||||
"create_title": "Crear nueva plantilla",
|
||||
"name": "Nombre de plantilla",
|
||||
"name_required": "El nombre de la plantilla es obligatorio",
|
||||
"name_placeholder": "Por favor, introduce el nombre de la plantilla",
|
||||
"version": "Versión",
|
||||
"version_placeholder": "Por favor, introduce el número de versión",
|
||||
"description": "Descripción",
|
||||
"description_placeholder": "Por favor, introduce la descripción de la plantilla",
|
||||
"author": "Autor",
|
||||
"author_placeholder": "Por favor, introduce el nombre del autor",
|
||||
"delete_button": "Eliminar",
|
||||
"delete_title": "Eliminar plantilla",
|
||||
"delete_confirm": "¿Estás seguro de que quieres eliminar la plantilla \"{name}\"?",
|
||||
"delete_warning": "Esta operación eliminará permanentemente esta plantilla y todos sus archivos, no se puede recuperar.",
|
||||
"delete_success": "Plantilla eliminada con éxito",
|
||||
"delete_failed": "Error al eliminar la plantilla",
|
||||
"create_success": "Plantilla creada con éxito",
|
||||
"create_failed": "Error al crear la plantilla",
|
||||
"open_folder": "Abrir carpeta",
|
||||
"open_folder_success": "Carpeta de plantilla abierta",
|
||||
"open_folder_failed": "Error al abrir la carpeta",
|
||||
"edit_button": "Editar",
|
||||
"edit_title": "Editar plantilla",
|
||||
"update_success": "Plantilla actualizada con éxito",
|
||||
"update_failed": "Error al actualizar la plantilla",
|
||||
"empty": "Aún no hay plantillas",
|
||||
"empty_hint": "Haz clic en el botón de arriba para crear tu primera plantilla"
|
||||
},
|
||||
"setting": {
|
||||
"title": "Configuración DeEarthX",
|
||||
"subtitle": "¡Haz que DeEarthX V3 sea más adecuado para ti!",
|
||||
"category_filter": "Configuración de filtrado de mods",
|
||||
"category_mirror": "Configuración de fuente de descarga",
|
||||
"category_system": "Configuración de gestión del sistema",
|
||||
"filter_hashes_name": "Filtrado por hash",
|
||||
"filter_hashes_desc": "Filtrar mods de cliente innecesarios (método de filtrado por hash)",
|
||||
"filter_dexpub_name": "Filtrado Galaxy Square",
|
||||
"filter_dexpub_desc": "Filtrar archivos de cliente registrados en la plataforma Galaxy Square",
|
||||
"filter_modrinth_name": "Filtrado API Modrinth",
|
||||
"filter_modrinth_desc": "Verificar compatibilidad cliente/servidor de mods a través de la API Modrinth",
|
||||
"filter_mixins_name": "Filtrado Mixin",
|
||||
"filter_mixins_desc": "Filtrar archivos relacionados con Client Mixins",
|
||||
"mirror_mcimirror_name": "Fuente espejo MCIM",
|
||||
"mirror_mcimirror_desc": "Usar fuente espejo MCIM para acelerar descargas",
|
||||
"mirror_bmclapi_name": "Fuente espejo BMCLAPI",
|
||||
"mirror_bmclapi_desc": "Usar fuente espejo BMCLAPI para acelerar descargas",
|
||||
"system_oaf_name": "Abrir directorio después de la operación",
|
||||
"system_oaf_desc": "Abrir automáticamente el directorio después de completar la creación del servidor",
|
||||
"system_autozip_name": "Empaquetar automáticamente en zip",
|
||||
"system_autozip_desc": "Empaquetar automáticamente en zip después de completar la creación del servidor (no empaquetar en modo servidor)",
|
||||
"switch_on": "Activado",
|
||||
"switch_off": "Desactivado",
|
||||
"language_title": "Configuración de idioma",
|
||||
"language_desc": "Elegir idioma de visualización de la interfaz",
|
||||
"language_chinese": "Chino simplificado",
|
||||
"language_english": "English",
|
||||
"language_japanese": "日本語",
|
||||
"language_french": "Français",
|
||||
"language_german": "Deutsch",
|
||||
"language_spanish": "Español",
|
||||
"config_saved": "Configuración guardada",
|
||||
"config_load_failed": "Fallo al cargar configuración",
|
||||
"config_save_failed": "Fallo al guardar configuración",
|
||||
"export_config": "Exportar configuración",
|
||||
"import_config": "Importar configuración",
|
||||
"config_exported": "Configuración exportada exitosamente",
|
||||
"config_export_failed": "Fallo al exportar configuración",
|
||||
"config_imported": "Configuración importada exitosamente",
|
||||
"config_import_failed": "Fallo al importar configuración",
|
||||
"config_invalid_format": "Formato de archivo de configuración inválido"
|
||||
},
|
||||
"about": {
|
||||
"title": "Acerca de DeEarthX",
|
||||
"subtitle": "Herramienta profesional para crear servidores de packs de mods de Minecraft",
|
||||
"about_software": "Acerca del software",
|
||||
"current_version": "Versión actual:",
|
||||
"build_time": "Tiempo de construcción:",
|
||||
"author": "Autor:",
|
||||
"development_team": "Equipo de desarrollo",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "Autor",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2º desarrollador (mejoras y optimizaciones)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "Espejo BMCLAPI",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "Espejo MCIM",
|
||||
"sponsor": "Patrocinadores",
|
||||
"sponsor_elfidc": "亿讯云",
|
||||
"sponsor_type_gold": "Patrocinador oro",
|
||||
"version_file_read_failed": "Fallo al leer archivo de versión"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "Haz que todos los mods brillen aquí",
|
||||
"mod_submit_title": "Envío de mod",
|
||||
"mod_type_label": "Tipo de mod",
|
||||
"mod_type_client": "Mod de cliente",
|
||||
"mod_type_server": "Mod de servidor",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "Ingresa un Modid (separados por comas) o sube un archivo para obtener automáticamente",
|
||||
"modid_count": "{count} Modids actualmente agregados",
|
||||
"upload_file_label": "Subir archivo",
|
||||
"upload_file_hint": "Haz clic o arrastra archivos aquí para subir",
|
||||
"upload_file_support": "Soporta archivos .jar, selección múltiple disponible",
|
||||
"file_selected": "{count} archivos seleccionados",
|
||||
"start_upload": "Iniciar subida",
|
||||
"uploading": "Subiendo...",
|
||||
"submit": "Enviar mods {type}",
|
||||
"submitting": "Enviando...",
|
||||
"submit_confirm_title": "Confirmar envío",
|
||||
"submit_confirm_content": "¿Estás seguro de que quieres enviar {count} mods {type}?",
|
||||
"please_select_file": "Por favor selecciona un archivo primero",
|
||||
"upload_success": "{count} archivos subidos exitosamente",
|
||||
"data_format_error": "Error en formato de datos devueltos",
|
||||
"upload_failed": "Fallo al subir",
|
||||
"upload_error": "Error al subir, por favor intenta de nuevo",
|
||||
"submit_success": "Envío exitoso de mods {type}",
|
||||
"submit_failed": "Fallo al enviar",
|
||||
"submit_error": "Error al enviar, por favor intenta de nuevo"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core ya se está ejecutando",
|
||||
"backend_started": "DeEarthX.Core iniciado exitosamente",
|
||||
"backend_port_occupied": "¡El puerto 37019 está ocupado por otra aplicación!",
|
||||
"backend_start_failed": "Fallo al iniciar DeEarthX.Core, verifica si el puerto 37019 está ocupado (reintentado {count} veces)",
|
||||
"backend_restart": "¡Reiniciando DeEarthX.Core!",
|
||||
"retry_start": "Fallo al iniciar, reintentando ({current}/{max})...",
|
||||
"config_load_error": "Fallo al cargar configuración"
|
||||
}
|
||||
}
|
||||
257
front/lang/fr_fr.json
Normal file
@@ -0,0 +1,257 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "Version",
|
||||
"status_loading": "Démarrage",
|
||||
"status_success": "Normal",
|
||||
"status_error": "Erreur",
|
||||
"backend_status": "État du backend",
|
||||
"confirm": "Confirmer",
|
||||
"cancel": "Annuler",
|
||||
"submit": "Soumettre",
|
||||
"upload": "Télécharger",
|
||||
"start": "Démarrer",
|
||||
"save": "Enregistrer",
|
||||
"delete": "Supprimer",
|
||||
"edit": "Modifier",
|
||||
"close": "Fermer",
|
||||
"loading": "Chargement...",
|
||||
"error": "Erreur",
|
||||
"success": "Succès",
|
||||
"warning": "Avertissement",
|
||||
"info": "Information"
|
||||
},
|
||||
"menu": {
|
||||
"home": "Accueil",
|
||||
"deearth": "Filtre",
|
||||
"galaxy": "Soumettre",
|
||||
"template": "Modèle",
|
||||
"setting": "Paramètres",
|
||||
"about": "À propos"
|
||||
},
|
||||
"home": {
|
||||
"title": "Créez des serveurs n'importe où, n'importe quand !",
|
||||
"mode_title": "Sélection du mode",
|
||||
"upload_title": "Glissez-déposez ou cliquez pour télécharger un fichier",
|
||||
"upload_hint": "Veuillez utiliser des fichiers .zip (CurseForge, MCBBS) et .mrpack (Modrinth)",
|
||||
"mode_server": "Mode serveur",
|
||||
"mode_upload": "Mode téléchargement",
|
||||
"preparing": "Préparation...",
|
||||
"task_preparing": "Connexion au service backend...",
|
||||
"task_connecting": "Connexion au service backend réussie, début du traitement de la tâche...",
|
||||
"step1_title": "Extraire le pack",
|
||||
"step1_desc": "Extraire le contenu et télécharger les fichiers",
|
||||
"step2_title": "Filtrer les mods",
|
||||
"step2_desc": "Fonctionnalité principale de DeEarthX",
|
||||
"step3_title": "Télécharger le serveur",
|
||||
"step3_desc": "Installer le serveur de chargeur de mods",
|
||||
"step4_title": "Terminé",
|
||||
"step4_desc": "Tout est prêt !",
|
||||
"progress_title": "Progression de la création",
|
||||
"upload_progress": "Progression du téléchargement",
|
||||
"unzip_progress": "Progression de l'extraction",
|
||||
"download_progress": "Progression du téléchargement",
|
||||
"server_install_progress": "Installation du serveur",
|
||||
"server_install_step": "Étape",
|
||||
"server_install_message": "Message",
|
||||
"server_install_completed": "Installation terminée !",
|
||||
"server_install_error": "Échec de l'installation",
|
||||
"server_install_duration": "Durée",
|
||||
"filter_mods_progress": "Filtrage des mods",
|
||||
"filter_mods_total": "Total des mods",
|
||||
"filter_mods_current": "Vérification en cours",
|
||||
"filter_mods_completed": "Filtrage terminé ! {filtered} mods clients identifiés, {moved} déplacés",
|
||||
"filter_mods_error": "Échec du filtrage",
|
||||
"start_production": "Démarrer la création, ne changez pas de menu !",
|
||||
"production_complete": "Création du serveur terminée ! Durée {time} secondes !",
|
||||
"please_select_file": "Veuillez d'abord glisser-déposer ou sélectionner un fichier",
|
||||
"only_zip_mrpack": "Seuls les fichiers .zip et .mrpack peuvent être téléchargés",
|
||||
"file_prepared": "Fichier prêt",
|
||||
"preparing_file": "Préparation du fichier...",
|
||||
"ws_connecting": "Établissement de la connexion WebSocket...",
|
||||
"ws_connected": "Connexion WebSocket réussie",
|
||||
"ws_failed": "Échec de la connexion WebSocket",
|
||||
"request_failed": "Échec de la requête vers le service backend, vérifiez que le service backend est en cours d'exécution",
|
||||
"backend_error": "DeEarthX.Core a rencontré une erreur fatale !",
|
||||
"backend_error_desc": "Veuillez prendre une capture d'écran complète de la fenêtre et l'envoyer dans le groupe\nInformations d'erreur : {error}",
|
||||
"java_not_found": "Java introuvable dans les variables système ! Veuillez installer Java, sinon le mode serveur ne pourra pas être utilisé !",
|
||||
"unknown_step": "Étape inconnue",
|
||||
"parse_error": "Échec de l'analyse du message du serveur",
|
||||
"speed": "Vitesse",
|
||||
"remaining": "Temps restant",
|
||||
"java_error_title": "Java introuvable",
|
||||
"java_error_desc": "Java introuvable dans les variables système ! Veuillez installer Java, sinon le mode serveur ne pourra pas être utilisé !\n\nSuggestions :\n1. Télécharger et installer Java 17 ou supérieur\n2. Configurer les variables d'environnement Java\n3. Redémarrer l'application",
|
||||
"network_error_title": "Erreur réseau",
|
||||
"network_error_desc": "Problème de connexion réseau\nInformations d'erreur : {error}",
|
||||
"file_error_title": "Erreur de fichier",
|
||||
"file_error_desc": "Problème d'opération de fichier\nInformations d'erreur : {error}",
|
||||
"memory_error_title": "Erreur de mémoire",
|
||||
"memory_error_desc": "Mémoire insuffisante\nInformations d'erreur : {error}",
|
||||
"unknown_error_title": "Erreur inconnue",
|
||||
"unknown_error_desc": "Une erreur inconnue s'est produite, veuillez contacter le support technique",
|
||||
"ws_error_title": "Échec de la connexion WebSocket",
|
||||
"ws_error_desc": "Impossible d'établir une connexion WebSocket avec le backend",
|
||||
"suggestions": "Solutions suggérées",
|
||||
"suggestion_check_network": "Vérifier si la connexion réseau est normale",
|
||||
"suggestion_check_firewall": "Vérifier les paramètres du pare-feu",
|
||||
"suggestion_retry": "Réessayer plus tard",
|
||||
"suggestion_check_disk_space": "Vérifier si l'espace disque est suffisant",
|
||||
"suggestion_check_permission": "Vérifier les paramètres de permissions de fichiers",
|
||||
"suggestion_check_file_format": "Confirmer que le format de fichier est correct",
|
||||
"suggestion_increase_memory": "Augmenter la mémoire allouée à l'application",
|
||||
"suggestion_close_other_apps": "Fermer d'autres applications utilisant de la mémoire",
|
||||
"suggestion_restart_application": "Redémarrer l'application",
|
||||
"suggestion_check_backend": "Vérifier si le service backend fonctionne normalement",
|
||||
"suggestion_check_logs": "Vérifier les fichiers journaux pour plus d'informations",
|
||||
"suggestion_check_port": "Vérifier si le port 37019 est occupé",
|
||||
"suggestion_contact_support": "Contacter le support technique pour obtenir de l'aide",
|
||||
"template_select_title": "Sélectionner le modèle de serveur",
|
||||
"template_select_desc": "Choisissez un modèle pour votre serveur. L'utilisation d'un modèle sautera l'installation du serveur et copiera directement les fichiers du modèle dans le répertoire du serveur",
|
||||
"template_official_loader": "Aucun",
|
||||
"template_official_loader_desc": "Ne pas utiliser de modèle, effectuer l'installation standard du serveur",
|
||||
"template_name": "Nom du modèle",
|
||||
"template_description": "Description",
|
||||
"template_author": "Auteur",
|
||||
"template_version": "Version",
|
||||
"template_selected": "Modèle sélectionné",
|
||||
"template_select_button": "Sélectionner le modèle",
|
||||
"template_loading": "Chargement de la liste des modèles...",
|
||||
"template_load_failed": "Échec du chargement de la liste des modèles",
|
||||
"template_apply_success": "Modèle appliqué avec succès",
|
||||
"template_apply_failed": "Échec de l'application du modèle"
|
||||
},
|
||||
"template": {
|
||||
"title": "Gestion des modèles",
|
||||
"description": "Gérez vos modèles de serveur, créez, affichez et supprimez des modèles",
|
||||
"create_button": "Créer un modèle",
|
||||
"create_title": "Créer un nouveau modèle",
|
||||
"name": "Nom du modèle",
|
||||
"name_required": "Le nom du modèle est requis",
|
||||
"name_placeholder": "Veuillez entrer le nom du modèle",
|
||||
"version": "Version",
|
||||
"version_placeholder": "Veuillez entrer le numéro de version",
|
||||
"description": "Description",
|
||||
"description_placeholder": "Veuillez entrer la description du modèle",
|
||||
"author": "Auteur",
|
||||
"author_placeholder": "Veuillez entrer le nom de l'auteur",
|
||||
"delete_button": "Supprimer",
|
||||
"delete_title": "Supprimer le modèle",
|
||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer le modèle \"{name}\" ?",
|
||||
"delete_warning": "Cette opération supprimera définitivement ce modèle et tous ses fichiers, impossible à récupérer.",
|
||||
"delete_success": "Modèle supprimé avec succès",
|
||||
"delete_failed": "Échec de la suppression du modèle",
|
||||
"create_success": "Modèle créé avec succès",
|
||||
"create_failed": "Échec de la création du modèle",
|
||||
"open_folder": "Ouvrir le dossier",
|
||||
"open_folder_success": "Dossier du modèle ouvert",
|
||||
"open_folder_failed": "Échec de l'ouverture du dossier",
|
||||
"edit_button": "Modifier",
|
||||
"edit_title": "Modifier le modèle",
|
||||
"update_success": "Modèle mis à jour avec succès",
|
||||
"update_failed": "Échec de la mise à jour du modèle",
|
||||
"empty": "Aucun modèle pour le moment",
|
||||
"empty_hint": "Cliquez sur le bouton ci-dessus pour créer votre premier modèle"
|
||||
},
|
||||
"setting": {
|
||||
"title": "Paramètres DeEarthX",
|
||||
"subtitle": "Rendez DeEarthX V3 plus adapté à vos besoins !",
|
||||
"category_filter": "Paramètres de filtrage des mods",
|
||||
"category_mirror": "Paramètres de la source de téléchargement",
|
||||
"category_system": "Paramètres de gestion système",
|
||||
"filter_hashes_name": "Filtrage par hachage",
|
||||
"filter_hashes_desc": "Filtrer les mods client inutiles (méthode de filtrage par hachage)",
|
||||
"filter_dexpub_name": "Filtrage Galaxy Square",
|
||||
"filter_dexpub_desc": "Filtrer les fichiers client enregistrés sur la plateforme Galaxy Square",
|
||||
"filter_modrinth_name": "Filtrage API Modrinth",
|
||||
"filter_modrinth_desc": "Vérifier la compatibilité client/serveur des mods via l'API Modrinth",
|
||||
"filter_mixins_name": "Filtrage Mixin",
|
||||
"filter_mixins_desc": "Filtrer les fichiers liés aux Client Mixins",
|
||||
"mirror_mcimirror_name": "Source miroir MCIM",
|
||||
"mirror_mcimirror_desc": "Utiliser la source miroir MCIM pour accélérer le téléchargement",
|
||||
"mirror_bmclapi_name": "Source miroir BMCLAPI",
|
||||
"mirror_bmclapi_desc": "Utiliser la source miroir BMCLAPI pour accélérer le téléchargement",
|
||||
"system_oaf_name": "Ouvrir le dossier après l'opération",
|
||||
"system_oaf_desc": "Ouvrir automatiquement le dossier après la création du serveur",
|
||||
"system_autozip_name": "Créer automatiquement un zip",
|
||||
"system_autozip_desc": "Créer automatiquement un zip après la création du serveur (pas de zip en mode serveur)",
|
||||
"switch_on": "Activé",
|
||||
"switch_off": "Désactivé",
|
||||
"language_title": "Paramètres de langue",
|
||||
"language_desc": "Choisir la langue d'affichage de l'interface",
|
||||
"language_chinese": "Chinois simplifié",
|
||||
"language_english": "English",
|
||||
"language_japanese": "日本語",
|
||||
"language_french": "Français",
|
||||
"language_german": "Deutsch",
|
||||
"language_spanish": "Español",
|
||||
"config_saved": "Configuration enregistrée",
|
||||
"config_load_failed": "Échec du chargement de la configuration",
|
||||
"config_save_failed": "Échec de l'enregistrement de la configuration",
|
||||
"export_config": "Exporter la configuration",
|
||||
"import_config": "Importer la configuration",
|
||||
"config_exported": "Configuration exportée avec succès",
|
||||
"config_export_failed": "Échec de l'exportation de la configuration",
|
||||
"config_imported": "Configuration importée avec succès",
|
||||
"config_import_failed": "Échec de l'importation de la configuration",
|
||||
"config_invalid_format": "Format de fichier de configuration invalide"
|
||||
},
|
||||
"about": {
|
||||
"title": "À propos de DeEarthX",
|
||||
"subtitle": "Outil professionnel de création de serveur de pack de mods Minecraft",
|
||||
"about_software": "À propos du logiciel",
|
||||
"current_version": "Version actuelle :",
|
||||
"build_time": "Heure de construction :",
|
||||
"author": "Auteur :",
|
||||
"development_team": "Équipe de développement",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "Auteur",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2ème développeur (améliorations et optimisations)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "Miroir BMCLAPI",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "Miroir MCIM",
|
||||
"sponsor": "Sponsors",
|
||||
"sponsor_elfidc": "亿讯云",
|
||||
"sponsor_type_gold": "Sponsor or",
|
||||
"version_file_read_failed": "Échec de la lecture du fichier de version"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "Faire briller tous les mods ici",
|
||||
"mod_submit_title": "Soumission de mod",
|
||||
"mod_type_label": "Type de mod",
|
||||
"mod_type_client": "Mod client",
|
||||
"mod_type_server": "Mod serveur",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "Entrez un Modid (séparés par des virgules) ou téléchargez un fichier pour obtenir automatiquement",
|
||||
"modid_count": "{count} Modids actuellement ajoutés",
|
||||
"upload_file_label": "Télécharger un fichier",
|
||||
"upload_file_hint": "Cliquez ou faites glisser un fichier ici pour le télécharger",
|
||||
"upload_file_support": "Supporte les fichiers .jar, sélection multiple possible",
|
||||
"file_selected": "{count} fichiers sélectionnés",
|
||||
"start_upload": "Commencer le téléchargement",
|
||||
"uploading": "Téléchargement en cours...",
|
||||
"submit": "Soumettre mods {type}",
|
||||
"submitting": "Soumission en cours...",
|
||||
"submit_confirm_title": "Confirmer la soumission",
|
||||
"submit_confirm_content": "Êtes-vous sûr de vouloir soumettre {count} mods {type} ?",
|
||||
"please_select_file": "Veuillez d'abord sélectionner un fichier",
|
||||
"upload_success": "{count} fichiers téléchargés avec succès",
|
||||
"data_format_error": "Erreur de format des données renvoyées",
|
||||
"upload_failed": "Échec du téléchargement",
|
||||
"upload_error": "Erreur de téléchargement, veuillez réessayer",
|
||||
"submit_success": "Soumission réussie des mods {type}",
|
||||
"submit_failed": "Échec de la soumission",
|
||||
"submit_error": "Erreur de soumission, veuillez réessayer"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core est déjà en cours d'exécution",
|
||||
"backend_started": "DeEarthX.Core démarré avec succès",
|
||||
"backend_port_occupied": "Le port 37019 est occupé par une autre application !",
|
||||
"backend_start_failed": "Échec du démarrage de DeEarthX.Core, vérifiez si le port 37019 est occupé ! (Réessayé {count} fois)",
|
||||
"backend_restart": "Redémarrage de DeEarthX.Core !",
|
||||
"retry_start": "Échec du démarrage, nouvelle tentative ({current}/{max})...",
|
||||
"config_load_error": "Échec du chargement de la configuration"
|
||||
}
|
||||
}
|
||||
265
front/lang/ja_jp.json
Normal file
@@ -0,0 +1,265 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "バージョン",
|
||||
"status_loading": "起動中",
|
||||
"status_success": "正常",
|
||||
"status_error": "エラー",
|
||||
"backend_status": "バックエンド状態",
|
||||
"confirm": "確認",
|
||||
"cancel": "キャンセル",
|
||||
"submit": "送信",
|
||||
"upload": "アップロード",
|
||||
"start": "開始",
|
||||
"save": "保存",
|
||||
"delete": "削除",
|
||||
"edit": "編集",
|
||||
"close": "閉じる",
|
||||
"loading": "読み込み中...",
|
||||
"error": "エラー",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "情報"
|
||||
},
|
||||
"menu": {
|
||||
"home": "ホーム",
|
||||
"deearth": "フィルター",
|
||||
"galaxy": "送信",
|
||||
"template": "テンプレート",
|
||||
"setting": "設定",
|
||||
"about": "について"
|
||||
},
|
||||
"home": {
|
||||
"title": "サーバー設定をいつでもどこで!",
|
||||
"mode_title": "モード選択",
|
||||
"upload_title": "ドラッグまたはクリックしてファイルをアップロード",
|
||||
"upload_hint": ".zip(CurseForge、MCBBS)と.mrpack(Modrinth)ファイルを使用してください",
|
||||
"mode_server": "サーバー設定モード",
|
||||
"mode_upload": "アップロードモード",
|
||||
"preparing": "準備中...",
|
||||
"task_preparing": "バックエンドサービスに接続中...",
|
||||
"task_connecting": "バックエンドサービスへの接続に成功、タスク処理を開始...",
|
||||
"step1_title": "統合パックを展開",
|
||||
"step1_desc": "内容を展開してファイルをダウンロード",
|
||||
"step2_title": "Modをフィルタリング",
|
||||
"step2_desc": "DeEarthXの核心機能",
|
||||
"step3_title": "サーバーをダウンロード",
|
||||
"step3_desc": "Modローダーサーバーをインストール",
|
||||
"step4_title": "完了",
|
||||
"step4_desc": "準備完了!",
|
||||
"progress_title": "制作進度",
|
||||
"upload_progress": "アップロード進度",
|
||||
"unzip_progress": "展開進度",
|
||||
"download_progress": "ダウンロード進捗",
|
||||
"server_install_progress": "サーバーインストール",
|
||||
"server_install_step": "ステップ",
|
||||
"server_install_message": "メッセージ",
|
||||
"server_install_completed": "インストール完了!",
|
||||
"server_install_error": "インストール失敗",
|
||||
"server_install_duration": "所要時間",
|
||||
"filter_mods_progress": "Modフィルタリング",
|
||||
"filter_mods_total": "総Mod数",
|
||||
"filter_mods_current": "現在チェック中",
|
||||
"filter_mods_completed": "フィルタリング完了!{filtered}個のクライアントModを識別、{moved}個を移動",
|
||||
"filter_mods_error": "フィルタリング失敗",
|
||||
"start_production": "制作を開始、メニューを切り替えないでください!",
|
||||
"production_complete": "サーバー制作完了!所要時間{time}秒!",
|
||||
"please_select_file": "ファイルをドラッグまたは選択してください",
|
||||
"only_zip_mrpack": ".zipと.mrpackファイルのみアップロード可能",
|
||||
"file_prepared": "ファイルの準備完了",
|
||||
"preparing_file": "ファイルを準備中...",
|
||||
"ws_connecting": "WebSocket接続を確立中...",
|
||||
"ws_connected": "WebSocket接続成功",
|
||||
"ws_failed": "WebSocket接続失敗",
|
||||
"request_failed": "バックエンドサービスへのリクエストが失敗しました。バックエンドサービスが実行されているか確認してください",
|
||||
"backend_error": "DeEarthX.Coreで致命的なエラーが発生しました!",
|
||||
"backend_error_desc": "ウィンドウ全体のスクリーンショットをグループに送ってください\nエラー情報:{error}",
|
||||
"java_not_found": "システム変数にJavaが見つかりません!Javaをインストールしてください。そうしないとサーバー設定モードは使用できません!",
|
||||
"unknown_step": "不明なステップ",
|
||||
"parse_error": "サーバーメッセージの解析に失敗しました",
|
||||
"speed": "速度",
|
||||
"remaining": "残り時間",
|
||||
"java_error_title": "Javaが見つかりません",
|
||||
"java_error_desc": "システム変数にJavaが見つかりません!Javaをインストールしてください。そうしないとサーバー設定モードは使用できません!\n\n提案:\n1. Java 17以上をダウンロードしてインストール\n2. Java環境変数を設定\n3. アプリケーションを再起動",
|
||||
"network_error_title": "ネットワークエラー",
|
||||
"network_error_desc": "ネットワーク接続に問題があります\nエラー情報:{error}",
|
||||
"file_error_title": "ファイルエラー",
|
||||
"file_error_desc": "ファイル操作に問題があります\nエラー情報:{error}",
|
||||
"memory_error_title": "メモリエラー",
|
||||
"memory_error_desc": "メモリ不足\nエラー情報:{error}",
|
||||
"unknown_error_title": "不明なエラー",
|
||||
"unknown_error_desc": "不明なエラーが発生しました。技術サポートに連絡してください",
|
||||
"ws_error_title": "WebSocket接続失敗",
|
||||
"ws_error_desc": "バックエンドとのWebSocket接続を確立できません",
|
||||
"suggestions": "提案された解決策",
|
||||
"suggestion_check_network": "ネットワーク接続が正常か確認",
|
||||
"suggestion_check_firewall": "ファイアウォール設定を確認",
|
||||
"suggestion_retry": "後でもう一度試す",
|
||||
"suggestion_check_disk_space": "ディスク容量が十分か確認",
|
||||
"suggestion_check_permission": "ファイル権限設定を確認",
|
||||
"suggestion_check_file_format": "ファイル形式が正しいか確認",
|
||||
"suggestion_increase_memory": "アプリケーションに割り当てるメモリを増やす",
|
||||
"suggestion_close_other_apps": "メモリを使用する他のアプリケーションを閉じる",
|
||||
"suggestion_restart_application": "アプリケーションを再起動",
|
||||
"suggestion_check_backend": "バックエンドサービスが正常に動作しているか確認",
|
||||
"suggestion_check_logs": "詳細情報のためにログファイルを確認",
|
||||
"suggestion_check_port": "ポート37019が使用されているか確認",
|
||||
"suggestion_contact_support": "技術サポートに連絡してヘルプを受ける",
|
||||
"template_select_title": "サーバーテンプレートを選択",
|
||||
"template_select_desc": "サーバー用のテンプレートを選択してください。テンプレートを使用するとサーバーインストールがスキップされ、テンプレートファイルがサーバーディレクトリに直接コピーされます",
|
||||
"template_official_loader": "なし",
|
||||
"template_official_loader_desc": "テンプレートを使用せず、標準のサーバーインストールを実行",
|
||||
"template_name": "テンプレート名",
|
||||
"template_description": "説明",
|
||||
"template_author": "作者",
|
||||
"template_version": "バージョン",
|
||||
"template_selected": "選択されたテンプレート",
|
||||
"template_select_button": "テンプレートを選択",
|
||||
"template_loading": "テンプレートリストを読み込み中...",
|
||||
"template_load_failed": "テンプレートリストの読み込みに失敗",
|
||||
"template_apply_success": "テンプレートの適用に成功",
|
||||
"template_apply_failed": "テンプレートの適用に失敗"
|
||||
},
|
||||
"template": {
|
||||
"title": "テンプレート管理",
|
||||
"description": "サーバーテンプレートを管理し、作成、表示、削除します",
|
||||
"create_button": "テンプレートを作成",
|
||||
"create_title": "新しいテンプレートを作成",
|
||||
"name": "テンプレート名",
|
||||
"name_required": "テンプレート名は必須です",
|
||||
"name_placeholder": "テンプレート名を入力してください",
|
||||
"version": "バージョン",
|
||||
"version_placeholder": "バージョン番号を入力してください",
|
||||
"description": "説明",
|
||||
"description_placeholder": "テンプレートの説明を入力してください",
|
||||
"author": "作者",
|
||||
"author_placeholder": "作者名を入力してください",
|
||||
"delete_button": "削除",
|
||||
"delete_title": "テンプレートを削除",
|
||||
"delete_confirm": "テンプレート \"{name}\" を削除してもよろしいですか?",
|
||||
"delete_warning": "この操作はテンプレートとそのすべてのファイルを永久に削除し、復元できません。",
|
||||
"delete_success": "テンプレートを削除しました",
|
||||
"delete_failed": "テンプレートの削除に失敗しました",
|
||||
"create_success": "テンプレートを作成しました",
|
||||
"create_failed": "テンプレートの作成に失敗しました",
|
||||
"open_folder": "フォルダーを開く",
|
||||
"open_folder_success": "テンプレートフォルダーを開きました",
|
||||
"open_folder_failed": "フォルダーを開くことに失敗しました",
|
||||
"edit_button": "編集",
|
||||
"edit_title": "テンプレートを編集",
|
||||
"update_success": "テンプレートを更新しました",
|
||||
"update_failed": "テンプレートの更新に失敗しました",
|
||||
"empty": "まだテンプレートがありません",
|
||||
"empty_hint": "上のボタンをクリックして最初のテンプレートを作成してください",
|
||||
"local_templates": "ローカルテンプレート",
|
||||
"template_store": "テンプレートストア",
|
||||
"store_empty": "テンプレートストアは空です",
|
||||
"store_empty_hint": "現在利用可能なテンプレートはありません",
|
||||
"install_button": "インストール",
|
||||
"install_success": "テンプレートのインストールに成功しました",
|
||||
"install_failed": "テンプレートのインストールに失敗しました",
|
||||
"store_load_failed": "テンプレートストアの読み込みに失敗しました"
|
||||
},
|
||||
"setting": {
|
||||
"title": "DeEarthX 設定",
|
||||
"subtitle": "DeEarthX V3をあなたに合わせて!",
|
||||
"category_filter": "Modフィルタリング設定",
|
||||
"category_mirror": "ダウンロードソース設定",
|
||||
"category_system": "システム管理設定",
|
||||
"filter_hashes_name": "ハッシュフィルタリング",
|
||||
"filter_hashes_desc": "不要なクライアントModをフィルタリング(ハッシュフィルタリング法)",
|
||||
"filter_dexpub_name": "Galaxy Squareフィルタリング",
|
||||
"filter_dexpub_desc": "Galaxy Squareプラットフォームに記録されたクライアントファイルをフィルタリング",
|
||||
"filter_modrinth_name": "Modrinth APIフィルタリング",
|
||||
"filter_modrinth_desc": "Modrinth APIでModのクライアント/サーバー互換性を確認",
|
||||
"filter_mixins_name": "Mixinフィルタリング",
|
||||
"filter_mixins_desc": "Client Mixin関連ファイルをフィルタリング",
|
||||
"mirror_mcimirror_name": "MCIMミラーソース",
|
||||
"mirror_mcimirror_desc": "MCIMミラーソースを使用してダウンロードを加速",
|
||||
"mirror_bmclapi_name": "BMCLAPIミラーソース",
|
||||
"mirror_bmclapi_desc": "BMCLAPIミラーソースを使用してダウンロードを加速",
|
||||
"system_oaf_name": "操作完了後にディレクトリを開く",
|
||||
"system_oaf_desc": "サーバー制作完了後に自動的にディレクトリを開く",
|
||||
"system_autozip_name": "自動的にzipにパッケージ",
|
||||
"system_autozip_desc": "サーバー制作完了後に自動的にzipにパッケージ(サーバー設定モードではパッケージしない)",
|
||||
"switch_on": "オン",
|
||||
"switch_off": "オフ",
|
||||
"language_title": "言語設定",
|
||||
"language_desc": "インターフェース表示言語を選択",
|
||||
"language_chinese": "簡体字中国語",
|
||||
"language_english": "English",
|
||||
"language_japanese": "日本語",
|
||||
"language_french": "Français",
|
||||
"language_german": "Deutsch",
|
||||
"language_spanish": "Español",
|
||||
"config_saved": "設定が保存されました",
|
||||
"config_load_failed": "設定の読み込みに失敗しました",
|
||||
"config_save_failed": "設定の保存に失敗しました",
|
||||
"export_config": "設定をエクスポート",
|
||||
"import_config": "設定をインポート",
|
||||
"config_exported": "設定のエクスポートに成功しました",
|
||||
"config_export_failed": "設定のエクスポートに失敗しました",
|
||||
"config_imported": "設定のインポートに成功しました",
|
||||
"config_import_failed": "設定のインポートに失敗しました",
|
||||
"config_invalid_format": "設定ファイルの形式が無効です"
|
||||
},
|
||||
"about": {
|
||||
"title": "DeEarthXについて",
|
||||
"subtitle": "プロフェッショナルなMinecraft統合パックサーバー制作ツール",
|
||||
"about_software": "ソフトウェアについて",
|
||||
"current_version": "現在のバージョン:",
|
||||
"build_time": "ビルド時間:",
|
||||
"author": "作者:",
|
||||
"development_team": "開発チーム",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "作者",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2番目の開発者(改善と最適化)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "BMCLAPIミラー",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "MCIMミラー",
|
||||
"sponsor": "スポンサー",
|
||||
"sponsor_elfidc": "亿讯云",
|
||||
"sponsor_type_gold": "ゴールドスポンサー",
|
||||
"version_file_read_failed": "バージョンファイルの読み込みに失敗しました"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "すべてのModをここで輝かせる",
|
||||
"mod_submit_title": "Mod送信",
|
||||
"mod_type_label": "Modタイプ",
|
||||
"mod_type_client": "クライアントMod",
|
||||
"mod_type_server": "サーバーMod",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "Modidを入力してください(複数はカンマで区切る)またはファイルをアップロードして自動取得",
|
||||
"modid_count": "現在{count}個のModidが追加されています",
|
||||
"upload_file_label": "ファイルをアップロード",
|
||||
"upload_file_hint": "クリックまたはドラッグしてファイルをアップロード",
|
||||
"upload_file_support": ".jar形式ファイルをサポート、複数選択可能",
|
||||
"file_selected": "{count}個のファイルが選択されています",
|
||||
"start_upload": "アップロード開始",
|
||||
"uploading": "アップロード中...",
|
||||
"submit": "{type}Modを送信",
|
||||
"submitting": "送信中...",
|
||||
"submit_confirm_title": "送信確認",
|
||||
"submit_confirm_content": "{count}個の{type}Modを送信してもよろしいですか?",
|
||||
"please_select_file": "先にファイルを選択してください",
|
||||
"upload_success": "{count}個のファイルのアップロードに成功しました",
|
||||
"data_format_error": "返されたデータ形式が間違っています",
|
||||
"upload_failed": "アップロードに失敗しました",
|
||||
"upload_error": "アップロードエラー、再試行してください",
|
||||
"submit_success": "{type}Modの送信に成功しました",
|
||||
"submit_failed": "送信に失敗しました",
|
||||
"submit_error": "送信エラー、再試行してください"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Coreは実行中です",
|
||||
"backend_started": "DeEarthX.Coreの起動に成功しました",
|
||||
"backend_port_occupied": "ポート37019が他のアプリケーションによって使用されています!",
|
||||
"backend_start_failed": "DeEarthX.Coreの起動に失敗しました。ポート37019が使用されているか確認してください!({count}回再試行しました)",
|
||||
"backend_restart": "DeEarthX.Coreを再起動します!",
|
||||
"retry_start": "起動に失敗しました、再試行中 ({current}/{max})...",
|
||||
"config_load_error": "設定の読み込みに失敗しました"
|
||||
}
|
||||
}
|
||||
271
front/lang/zh_cn.json
Normal file
@@ -0,0 +1,271 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "版本",
|
||||
"status_loading": "启动中",
|
||||
"status_success": "正常",
|
||||
"status_error": "错误",
|
||||
"backend_status": "后端状态",
|
||||
"confirm": "确定",
|
||||
"cancel": "取消",
|
||||
"submit": "提交",
|
||||
"upload": "上传",
|
||||
"start": "开始",
|
||||
"save": "保存",
|
||||
"delete": "删除",
|
||||
"edit": "编辑",
|
||||
"close": "关闭",
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "提示"
|
||||
},
|
||||
"menu": {
|
||||
"home": "主页",
|
||||
"deearth": "筛选",
|
||||
"galaxy": "提交",
|
||||
"template": "模板",
|
||||
"setting": "设置",
|
||||
"about": "关于"
|
||||
},
|
||||
"home": {
|
||||
"title": "让开服变得随时随地!",
|
||||
"mode_title": "模式选择",
|
||||
"upload_title": "拖拽或点击上传文件",
|
||||
"upload_hint": "支持 .zip(CurseForge、MCBBS)和 .mrpack(Modrinth)格式文件",
|
||||
"mode_server": "开服模式",
|
||||
"mode_upload": "上传模式",
|
||||
"preparing": "准备中...",
|
||||
"task_preparing": "正在连接后端服务...",
|
||||
"task_connecting": "后端服务连接成功,开始处理任务...",
|
||||
"step1_title": "解压整合包",
|
||||
"step1_desc": "解压内容并下载文件",
|
||||
"step2_title": "筛选模组",
|
||||
"step2_desc": "DeEarthX 的核心功能",
|
||||
"step3_title": "下载服务端",
|
||||
"step3_desc": "安装模组加载器服务端",
|
||||
"step4_title": "完成",
|
||||
"step4_desc": "一切就绪!",
|
||||
"progress_title": "制作进度",
|
||||
"upload_progress": "上传进度",
|
||||
"unzip_progress": "解压进度",
|
||||
"download_progress": "下载进度",
|
||||
"server_install_progress": "服务端安装",
|
||||
"server_install_step": "步骤",
|
||||
"server_install_message": "消息",
|
||||
"server_install_completed": "安装完成!",
|
||||
"server_install_error": "安装失败",
|
||||
"server_install_duration": "耗时",
|
||||
"filter_mods_progress": "筛选模组",
|
||||
"filter_mods_total": "总模组数",
|
||||
"filter_mods_current": "当前检查",
|
||||
"filter_mods_completed": "筛选完成!识别到 {filtered} 个客户端模组,移动 {moved} 个",
|
||||
"filter_mods_error": "筛选失败",
|
||||
"start_production": "开始制作,请勿切换菜单!",
|
||||
"production_complete": "服务端制作完成!共用时 {time} 秒!",
|
||||
"please_select_file": "请先拖拽或选择文件",
|
||||
"only_zip_mrpack": "仅支持 .zip 和 .mrpack 文件",
|
||||
"file_prepared": "文件准备完成",
|
||||
"preparing_file": "正在准备文件...",
|
||||
"ws_connecting": "正在建立 WebSocket 连接...",
|
||||
"ws_connected": "WebSocket 连接成功",
|
||||
"ws_failed": "WebSocket 连接失败",
|
||||
"request_failed": "请求后端服务失败,请检查后端服务是否运行",
|
||||
"backend_error": "DeEarthX.Core 遇到致命错误!",
|
||||
"backend_error_desc": "请将整个窗口截图发在群里\n错误信息:{error}",
|
||||
"java_not_found": "未在系统变量中找到 Java!请安装 Java,否则开服模式将无法使用!",
|
||||
"unknown_step": "未知步骤",
|
||||
"parse_error": "解析服务器消息失败",
|
||||
"speed": "速度",
|
||||
"remaining": "剩余时间",
|
||||
"java_error_title": "Java 未找到",
|
||||
"java_error_desc": "未在系统变量中找到 Java!请安装 Java,否则开服模式将无法使用!\n\n建议:\n1. 下载并安装 Java 17 或更高版本\n2. 配置 Java 环境变量\n3. 重启应用程序",
|
||||
"network_error_title": "网络错误",
|
||||
"network_error_desc": "网络连接出现问题\n错误信息:{error}",
|
||||
"file_error_title": "文件错误",
|
||||
"file_error_desc": "文件操作出现问题\n错误信息:{error}",
|
||||
"memory_error_title": "内存错误",
|
||||
"memory_error_desc": "内存不足\n错误信息:{error}",
|
||||
"unknown_error_title": "未知错误",
|
||||
"unknown_error_desc": "发生未知错误,请联系技术支持",
|
||||
"ws_error_title": "WebSocket 连接失败",
|
||||
"ws_error_desc": "无法建立与后端的 WebSocket 连接",
|
||||
"suggestions": "建议解决方案",
|
||||
"suggestion_check_network": "检查网络连接是否正常",
|
||||
"suggestion_check_firewall": "检查防火墙设置",
|
||||
"suggestion_retry": "稍后重试",
|
||||
"suggestion_check_disk_space": "检查磁盘空间是否充足",
|
||||
"suggestion_check_permission": "检查文件权限设置",
|
||||
"suggestion_check_file_format": "确认文件格式正确",
|
||||
"suggestion_increase_memory": "增加分配给应用程序的内存",
|
||||
"suggestion_close_other_apps": "关闭其他占用内存的应用程序",
|
||||
"suggestion_restart_application": "重启应用程序",
|
||||
"suggestion_check_backend": "检查后端服务是否正常运行",
|
||||
"suggestion_check_logs": "查看日志文件获取更多信息",
|
||||
"suggestion_check_port": "检查端口 37019 是否被占用",
|
||||
"suggestion_contact_support": "联系技术支持获取帮助",
|
||||
"template_select_title": "选择服务端模板",
|
||||
"template_select_desc": "为您的服务器选择一个模板,使用模板将跳过服务端安装,直接复制模板文件到服务器目录",
|
||||
"template_official_loader": "标准安装",
|
||||
"template_official_loader_desc": "不使用任何模板,进行标准的服务端安装流程",
|
||||
"template_name": "模板名称",
|
||||
"template_description": "描述",
|
||||
"template_author": "作者",
|
||||
"template_version": "版本",
|
||||
"template_selected": "已选择模板",
|
||||
"template_select_button": "选择模板",
|
||||
"template_loading": "加载模板列表中...",
|
||||
"template_load_failed": "加载模板列表失败",
|
||||
"template_apply_success": "模板应用成功",
|
||||
"template_apply_failed": "模板应用失败",
|
||||
"template_import_title": "导入模板",
|
||||
"template_import_hint": "拖拽或点击上传模板 zip 文件",
|
||||
"template_import_success": "模板导入成功",
|
||||
"template_import_failed": "模板导入失败",
|
||||
"template_export_button": "导出",
|
||||
"template_export_success": "模板导出成功",
|
||||
"template_export_failed": "模板导出失败",
|
||||
"template_export_progress": "正在导出模板...",
|
||||
"template_import_progress": "正在导入模板...",
|
||||
"template_download_progress": "正在下载模板..."
|
||||
},
|
||||
"template": {
|
||||
"title": "模板管理",
|
||||
"description": "管理您的服务端模板,创建、查看和删除模板",
|
||||
"create_button": "创建模板",
|
||||
"create_title": "创建新模板",
|
||||
"name": "模板名称",
|
||||
"name_required": "模板名称不能为空",
|
||||
"name_placeholder": "请输入模板名称",
|
||||
"version": "版本",
|
||||
"version_placeholder": "请输入版本号",
|
||||
"description": "描述",
|
||||
"description_placeholder": "请输入模板描述",
|
||||
"author": "作者",
|
||||
"author_placeholder": "请输入作者名称",
|
||||
"delete_button": "删除",
|
||||
"delete_title": "删除模板",
|
||||
"delete_confirm": "确定要删除模板 \"{name}\" 吗?",
|
||||
"delete_warning": "此操作将永久删除该模板及其所有文件,无法恢复。",
|
||||
"delete_success": "模板删除成功",
|
||||
"delete_failed": "模板删除失败",
|
||||
"create_success": "模板创建成功",
|
||||
"create_failed": "模板创建失败",
|
||||
"open_folder": "打开文件夹",
|
||||
"open_folder_success": "已打开模板文件夹",
|
||||
"open_folder_failed": "打开文件夹失败",
|
||||
"edit_button": "编辑",
|
||||
"edit_title": "编辑模板",
|
||||
"update_success": "模板更新成功",
|
||||
"update_failed": "模板更新失败",
|
||||
"empty": "还没有模板哦~",
|
||||
"empty_hint": "点击上方按钮创建你的第一个模板",
|
||||
"local_templates": "本地模板",
|
||||
"template_store": "模板商店",
|
||||
"store_empty": "模板商店为空",
|
||||
"store_empty_hint": "暂时没有可用的模板",
|
||||
"install_button": "安装",
|
||||
"install_success": "模板安装成功",
|
||||
"install_failed": "模板安装失败",
|
||||
"store_load_failed": "加载模板商店失败"
|
||||
},
|
||||
"setting": {
|
||||
"title": "DeEarthX 设置",
|
||||
"subtitle": "让 DeEarthX V3 更适合你!",
|
||||
"category_filter": "模组筛选设置",
|
||||
"category_mirror": "下载源设置",
|
||||
"category_system": "系统管理设置",
|
||||
"filter_hashes_name": "哈希过滤",
|
||||
"filter_hashes_desc": "过滤不必要的客户端模组(哈希过滤法)",
|
||||
"filter_dexpub_name": "Galaxy Square 过滤",
|
||||
"filter_dexpub_desc": "过滤 Galaxy Square 平台中记录的客户端文件",
|
||||
"filter_modrinth_name": "Modrinth API 过滤",
|
||||
"filter_modrinth_desc": "通过 Modrinth API 检查模组的客户端/服务端兼容性",
|
||||
"filter_mixins_name": "Mixin 过滤",
|
||||
"filter_mixins_desc": "过滤 Client Mixin 相关文件",
|
||||
"mirror_mcimirror_name": "MCIM 镜像源",
|
||||
"mirror_mcimirror_desc": "使用 MCIM 镜像源加速下载",
|
||||
"mirror_bmclapi_name": "BMCLAPI 镜像源",
|
||||
"mirror_bmclapi_desc": "使用 BMCLAPI 镜像源加速下载",
|
||||
"system_oaf_name": "操作完成后打开目录",
|
||||
"system_oaf_desc": "服务端制作完成后自动打开目录",
|
||||
"system_autozip_name": "自动打包成 zip",
|
||||
"system_autozip_desc": "服务端制作完成后自动打包成 zip(开服模式不打包)",
|
||||
"switch_on": "开",
|
||||
"switch_off": "关",
|
||||
"language_title": "语言设置",
|
||||
"language_desc": "选择界面显示语言",
|
||||
"language_chinese": "简体中文",
|
||||
"language_english": "English",
|
||||
"config_saved": "配置已保存",
|
||||
"config_load_failed": "加载配置失败",
|
||||
"config_save_failed": "保存配置失败",
|
||||
"export_config": "导出配置",
|
||||
"import_config": "导入配置",
|
||||
"config_exported": "配置导出成功",
|
||||
"config_export_failed": "配置导出失败",
|
||||
"config_imported": "配置导入成功",
|
||||
"config_import_failed": "配置导入失败",
|
||||
"config_invalid_format": "配置文件格式无效"
|
||||
},
|
||||
"about": {
|
||||
"title": "关于 DeEarthX",
|
||||
"subtitle": "专业的 Minecraft 整合包服务端制作工具",
|
||||
"about_software": "关于软件",
|
||||
"current_version": "当前版本:",
|
||||
"build_time": "构建时间:",
|
||||
"author": "作者:",
|
||||
"development_team": "开发团队",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "作者",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2号开发(改良与优化)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "BMCLAPI 镜像",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "MCIM 镜像",
|
||||
"sponsor": "赞助商",
|
||||
"sponsor_elfidc": "亿讯云",
|
||||
"sponsor_type_gold": "金牌赞助",
|
||||
"version_file_read_failed": "版本文件读取失败"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "让所有模组都在这里发光",
|
||||
"mod_submit_title": "模组提交",
|
||||
"mod_type_label": "模组类型",
|
||||
"mod_type_client": "客户端模组",
|
||||
"mod_type_server": "服务端模组",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "请输入 Modid(多个用逗号分隔)或上传文件自动获取",
|
||||
"modid_count": "当前已添加 {count} 个 Modid",
|
||||
"upload_file_label": "上传文件",
|
||||
"upload_file_hint": "点击或拖拽文件到此区域上传",
|
||||
"upload_file_support": "支持 .jar 格式文件,可多选",
|
||||
"file_selected": "已选择 {count} 个文件",
|
||||
"start_upload": "开始上传",
|
||||
"uploading": "上传中...",
|
||||
"submit": "提交 {type} 模组",
|
||||
"submitting": "提交中...",
|
||||
"submit_confirm_title": "确认提交",
|
||||
"submit_confirm_content": "确定要提交 {count} 个 {type} 模组吗?",
|
||||
"please_select_file": "请先选择文件",
|
||||
"upload_success": "成功上传 {count} 个文件",
|
||||
"data_format_error": "返回数据格式错误",
|
||||
"upload_failed": "上传失败",
|
||||
"upload_error": "上传出错,请重试",
|
||||
"submit_success": "{type} 模组提交成功",
|
||||
"submit_failed": "提交失败",
|
||||
"submit_error": "提交出错,请重试"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core 已在运行",
|
||||
"backend_started": "DeEarthX.Core 启动成功",
|
||||
"backend_port_occupied": "37019 端口被其他应用占用!",
|
||||
"backend_start_failed": "DeEarthX.Core 启动失败,请检查 37019 端口是否被占用!(已重试 {count} 次)",
|
||||
"backend_restart": "DeEarthX.Core 重新启动!",
|
||||
"retry_start": "启动失败,正在重试 ({current}/{max})...",
|
||||
"config_load_error": "加载配置失败"
|
||||
}
|
||||
}
|
||||
253
front/lang/zh_hk.json
Normal file
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "版本",
|
||||
"status_loading": "啟動中",
|
||||
"status_success": "正常",
|
||||
"status_error": "錯誤",
|
||||
"backend_status": "後端狀態",
|
||||
"confirm": "確定",
|
||||
"cancel": "取消",
|
||||
"submit": "提交",
|
||||
"upload": "上傳",
|
||||
"start": "開始",
|
||||
"save": "儲存",
|
||||
"delete": "刪除",
|
||||
"edit": "編輯",
|
||||
"close": "關閉",
|
||||
"loading": "載入中...",
|
||||
"error": "錯誤",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "提示"
|
||||
},
|
||||
"menu": {
|
||||
"home": "主頁",
|
||||
"deearth": "篩選",
|
||||
"galaxy": "提交",
|
||||
"template": "模板",
|
||||
"setting": "設置",
|
||||
"about": "關於"
|
||||
},
|
||||
"home": {
|
||||
"title": "讓開服變得隨時隨地!",
|
||||
"mode_title": "模式選擇",
|
||||
"upload_title": "拖曳或點擊上傳檔案",
|
||||
"upload_hint": "支援 .zip(CurseForge、MCBBS)和 .mrpack(Modrinth)格式檔案",
|
||||
"mode_server": "開服模式",
|
||||
"mode_upload": "上傳模式",
|
||||
"preparing": "準備中...",
|
||||
"task_preparing": "正在連接後端服務...",
|
||||
"task_connecting": "後端服務連接成功,開始處理任務...",
|
||||
"step1_title": "解壓整合包",
|
||||
"step1_desc": "解壓內容並下載檔案",
|
||||
"step2_title": "篩選模組",
|
||||
"step2_desc": "DeEarthX 的核心功能",
|
||||
"step3_title": "下載伺服器",
|
||||
"step3_desc": "安裝模組載入器伺服器",
|
||||
"step4_title": "完成",
|
||||
"step4_desc": "一切就緒!",
|
||||
"progress_title": "製作進度",
|
||||
"upload_progress": "上傳進度",
|
||||
"unzip_progress": "解壓進度",
|
||||
"download_progress": "下載進度",
|
||||
"server_install_progress": "伺服器安裝",
|
||||
"server_install_step": "步驟",
|
||||
"server_install_message": "訊息",
|
||||
"server_install_completed": "安裝完成!",
|
||||
"server_install_error": "安裝失敗",
|
||||
"server_install_duration": "耗時",
|
||||
"filter_mods_progress": "篩選模組",
|
||||
"filter_mods_total": "總模組數",
|
||||
"filter_mods_current": "目前檢查",
|
||||
"filter_mods_completed": "篩選完成!識別到 {filtered} 個客戶端模組,移動 {moved} 個",
|
||||
"filter_mods_error": "篩選失敗",
|
||||
"start_production": "開始製作,請勿切換選單!",
|
||||
"production_complete": "伺服器製作完成!共用時 {time} 秒!",
|
||||
"please_select_file": "請先拖曳或選擇檔案",
|
||||
"only_zip_mrpack": "僅支援 .zip 和 .mrpack 檔案",
|
||||
"file_prepared": "檔案準備完成",
|
||||
"preparing_file": "正在準備檔案...",
|
||||
"ws_connecting": "正在建立 WebSocket 連接...",
|
||||
"ws_connected": "WebSocket 連接成功",
|
||||
"ws_failed": "WebSocket 連接失敗",
|
||||
"request_failed": "請求後端服務失敗,請檢查後端服務是否運行",
|
||||
"backend_error": "DeEarthX.Core 遇到致命錯誤!",
|
||||
"backend_error_desc": "請將整個視窗截圖發在群裡\n錯誤訊息:{error}",
|
||||
"java_not_found": "未在系統變數中找到 Java!請安裝 Java,否則開服模式將無法使用!",
|
||||
"unknown_step": "未知步驟",
|
||||
"parse_error": "解析伺服器訊息失敗",
|
||||
"speed": "速度",
|
||||
"remaining": "剩餘時間",
|
||||
"java_error_title": "Java 未找到",
|
||||
"java_error_desc": "未在系統變數中找到 Java!請安裝 Java,否則開服模式將無法使用!\n\n建議:\n1. 下載並安裝 Java 17 或更高版本\n2. 配置 Java 環境變數\n3. 重新啟動應用程式",
|
||||
"network_error_title": "網路錯誤",
|
||||
"network_error_desc": "網路連接出現問題\n錯誤訊息:{error}",
|
||||
"file_error_title": "檔案錯誤",
|
||||
"file_error_desc": "檔案操作出現問題\n錯誤訊息:{error}",
|
||||
"memory_error_title": "記憶體錯誤",
|
||||
"memory_error_desc": "記憶體不足\n錯誤訊息:{error}",
|
||||
"unknown_error_title": "未知錯誤",
|
||||
"unknown_error_desc": "發生未知錯誤,請聯繫技術支援",
|
||||
"ws_error_title": "WebSocket 連接失敗",
|
||||
"ws_error_desc": "無法建立與後端的 WebSocket 連接",
|
||||
"suggestions": "建議解決方案",
|
||||
"suggestion_check_network": "檢查網路連接是否正常",
|
||||
"suggestion_check_firewall": "檢查防火牆設定",
|
||||
"suggestion_retry": "稍後重試",
|
||||
"suggestion_check_disk_space": "檢查磁碟空間是否充足",
|
||||
"suggestion_check_permission": "檢查檔案權限設定",
|
||||
"suggestion_check_file_format": "確認檔案格式正確",
|
||||
"suggestion_increase_memory": "增加分配給應用程式的記憶體",
|
||||
"suggestion_close_other_apps": "關閉其他佔用記憶體的應用程式",
|
||||
"suggestion_restart_application": "重新啟動應用程式",
|
||||
"suggestion_check_backend": "檢查後端服務是否正常運行",
|
||||
"suggestion_check_logs": "查看日誌檔案獲取更多資訊",
|
||||
"suggestion_check_port": "檢查連接埠 37019 是否被佔用",
|
||||
"suggestion_contact_support": "聯繫技術支援獲取幫助",
|
||||
"template_select_title": "選擇服務器模板",
|
||||
"template_select_desc": "為您的服務器選擇一個模板,使用模板將跳過服務器安裝,直接複製模板檔案到服務器目錄",
|
||||
"template_official_loader": "無",
|
||||
"template_official_loader_desc": "不使用任何模板,進行標準的服務器安裝流程",
|
||||
"template_name": "模板名稱",
|
||||
"template_description": "描述",
|
||||
"template_author": "作者",
|
||||
"template_version": "版本",
|
||||
"template_selected": "已選擇模板",
|
||||
"template_select_button": "選擇模板",
|
||||
"template_loading": "載入模板列表中...",
|
||||
"template_load_failed": "載入模板列表失敗",
|
||||
"template_apply_success": "模板應用成功",
|
||||
"template_apply_failed": "模板應用失敗"
|
||||
},
|
||||
"template": {
|
||||
"title": "模板管理",
|
||||
"description": "管理您的服務器模板,創建、查看和刪除模板",
|
||||
"create_button": "創建模板",
|
||||
"create_title": "創建新模板",
|
||||
"name": "模板名稱",
|
||||
"name_required": "模板名稱不能為空",
|
||||
"name_placeholder": "請輸入模板名稱",
|
||||
"version": "版本",
|
||||
"version_placeholder": "請輸入版本號",
|
||||
"description": "描述",
|
||||
"description_placeholder": "請輸入模板描述",
|
||||
"author": "作者",
|
||||
"author_placeholder": "請輸入作者名稱",
|
||||
"delete_button": "刪除",
|
||||
"delete_title": "刪除模板",
|
||||
"delete_confirm": "確定要刪除模板 \"{name}\" 嗎?",
|
||||
"delete_warning": "此操作將永久刪除該模板及其所有文件,無法恢復。",
|
||||
"delete_success": "模板刪除成功",
|
||||
"delete_failed": "模板刪除失敗",
|
||||
"create_success": "模板創建成功",
|
||||
"create_failed": "模板創建失敗",
|
||||
"open_folder": "打開文件夾",
|
||||
"open_folder_success": "已打開模板文件夾",
|
||||
"open_folder_failed": "打開文件夾失敗",
|
||||
"edit_button": "編輯",
|
||||
"edit_title": "編輯模板",
|
||||
"update_success": "模板更新成功",
|
||||
"update_failed": "模板更新失敗",
|
||||
"empty": "還沒有模板",
|
||||
"empty_hint": "點擊上方按鈕創建你的第一個模板"
|
||||
},
|
||||
"setting": {
|
||||
"title": "DeEarthX 設定",
|
||||
"subtitle": "讓 DeEarthX V3 更適合你!",
|
||||
"category_filter": "模組篩選設定",
|
||||
"category_mirror": "下載來源設定",
|
||||
"category_system": "系統管理設定",
|
||||
"filter_hashes_name": "哈希過濾",
|
||||
"filter_hashes_desc": "過濾不必要的客戶端模組(哈希過濾法)",
|
||||
"filter_dexpub_name": "Galaxy Square 過濾",
|
||||
"filter_dexpub_desc": "過濾 Galaxy Square 平台中記錄的客戶端檔案",
|
||||
"filter_modrinth_name": "Modrinth API 過濾",
|
||||
"filter_modrinth_desc": "透過 Modrinth API 檢查模組的客戶端/伺服器相容性",
|
||||
"filter_mixins_name": "Mixin 過濾",
|
||||
"filter_mixins_desc": "過濾 Client Mixin 相關檔案",
|
||||
"mirror_mcimirror_name": "MCIM 鏡像來源",
|
||||
"mirror_mcimirror_desc": "使用 MCIM 鏡像來源加速下載",
|
||||
"mirror_bmclapi_name": "BMCLAPI 鏡像來源",
|
||||
"mirror_bmclapi_desc": "使用 BMCLAPI 鏡像來源加速下載",
|
||||
"system_oaf_name": "操作完成後開啟目錄",
|
||||
"system_oaf_desc": "伺服器製作完成後自動開啟目錄",
|
||||
"system_autozip_name": "自動打包成 zip",
|
||||
"system_autozip_desc": "伺服器製作完成後自動打包成 zip(開服模式不打包)",
|
||||
"switch_on": "開",
|
||||
"switch_off": "關",
|
||||
"language_title": "語言設定",
|
||||
"language_desc": "選擇介面顯示語言",
|
||||
"language_chinese": "簡體中文",
|
||||
"language_english": "English",
|
||||
"config_saved": "設定已儲存",
|
||||
"config_load_failed": "載入設定失敗",
|
||||
"config_save_failed": "儲存設定失敗",
|
||||
"export_config": "匯出設定",
|
||||
"import_config": "匯入設定",
|
||||
"config_exported": "設定匯出成功",
|
||||
"config_export_failed": "設定匯出失敗",
|
||||
"config_imported": "設定匯入成功",
|
||||
"config_import_failed": "設定匯入失敗",
|
||||
"config_invalid_format": "設定檔案格式無效"
|
||||
},
|
||||
"about": {
|
||||
"title": "關於 DeEarthX",
|
||||
"subtitle": "專業的 Minecraft 整合包伺服器製作工具",
|
||||
"about_software": "關於軟體",
|
||||
"current_version": "目前版本:",
|
||||
"build_time": "建置時間:",
|
||||
"author": "作者:",
|
||||
"development_team": "開發團隊",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "作者",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2號開發(改良與優化)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "BMCLAPI 鏡像",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "MCIM 鏡像",
|
||||
"sponsor": "贊助商",
|
||||
"sponsor_elfidc": "億訊雲",
|
||||
"sponsor_type_gold": "金牌贊助",
|
||||
"version_file_read_failed": "版本檔案讀取失敗"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "讓所有模組都在這裡發光",
|
||||
"mod_submit_title": "模組提交",
|
||||
"mod_type_label": "模組類型",
|
||||
"mod_type_client": "客戶端模組",
|
||||
"mod_type_server": "伺服器模組",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "請輸入 Modid(多個用逗號分隔)或上傳檔案自動獲取",
|
||||
"modid_count": "目前已新增 {count} 個 Modid",
|
||||
"upload_file_label": "上傳檔案",
|
||||
"upload_file_hint": "點擊或拖曳檔案到此區域上傳",
|
||||
"upload_file_support": "支援 .jar 格式檔案,可多選",
|
||||
"file_selected": "已選擇 {count} 個檔案",
|
||||
"start_upload": "開始上傳",
|
||||
"uploading": "上傳中...",
|
||||
"submit": "提交 {type} 模組",
|
||||
"submitting": "提交中...",
|
||||
"submit_confirm_title": "確認提交",
|
||||
"submit_confirm_content": "確定要提交 {count} 個 {type} 模組嗎?",
|
||||
"please_select_file": "請先選擇檔案",
|
||||
"upload_success": "成功上傳 {count} 個檔案",
|
||||
"data_format_error": "傳回資料格式錯誤",
|
||||
"upload_failed": "上傳失敗",
|
||||
"upload_error": "上傳出錯,請重試",
|
||||
"submit_success": "{type} 模組提交成功",
|
||||
"submit_failed": "提交失敗",
|
||||
"submit_error": "提交出錯,請重試"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core 已在運行",
|
||||
"backend_started": "DeEarthX.Core 啟動成功",
|
||||
"backend_port_occupied": "37019 連接埠被其他應用程式佔用!",
|
||||
"backend_start_failed": "DeEarthX.Core 啟動失敗,請檢查 37019 連接埠是否被佔用!(已重試 {count} 次)",
|
||||
"backend_restart": "DeEarthX.Core 重新啟動!",
|
||||
"retry_start": "啟動失敗,正在重試 ({current}/{max})...",
|
||||
"config_load_error": "載入設定失敗"
|
||||
}
|
||||
}
|
||||
253
front/lang/zh_tw.json
Normal file
@@ -0,0 +1,253 @@
|
||||
{
|
||||
"common": {
|
||||
"app_name": "DeEarthX",
|
||||
"version": "版本",
|
||||
"status_loading": "啟動中",
|
||||
"status_success": "正常",
|
||||
"status_error": "錯誤",
|
||||
"backend_status": "後端狀態",
|
||||
"confirm": "確定",
|
||||
"cancel": "取消",
|
||||
"submit": "提交",
|
||||
"upload": "上傳",
|
||||
"start": "開始",
|
||||
"save": "儲存",
|
||||
"delete": "刪除",
|
||||
"edit": "編輯",
|
||||
"close": "關閉",
|
||||
"loading": "載入中...",
|
||||
"error": "錯誤",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "提示"
|
||||
},
|
||||
"menu": {
|
||||
"home": "主頁",
|
||||
"deearth": "篩選",
|
||||
"galaxy": "提交",
|
||||
"template": "模板",
|
||||
"setting": "設置",
|
||||
"about": "關於"
|
||||
},
|
||||
"home": {
|
||||
"title": "讓開伺服器變得隨時隨地!",
|
||||
"mode_title": "模式選擇",
|
||||
"upload_title": "拖曳或點擊上傳檔案",
|
||||
"upload_hint": "支援 .zip(CurseForge、MCBBS)和 .mrpack(Modrinth)格式檔案",
|
||||
"mode_server": "開伺服器模式",
|
||||
"mode_upload": "上傳模式",
|
||||
"preparing": "準備中...",
|
||||
"task_preparing": "正在連線後端服務...",
|
||||
"task_connecting": "後端服務連線成功,開始處理任務...",
|
||||
"step1_title": "解壓縮整合包",
|
||||
"step1_desc": "解壓縮內容並下載檔案",
|
||||
"step2_title": "篩選模組",
|
||||
"step2_desc": "DeEarthX 的核心功能",
|
||||
"step3_title": "下載伺服器",
|
||||
"step3_desc": "安裝模組載入器伺服器",
|
||||
"step4_title": "完成",
|
||||
"step4_desc": "一切就緒!",
|
||||
"progress_title": "製作進度",
|
||||
"upload_progress": "上傳進度",
|
||||
"unzip_progress": "解壓縮進度",
|
||||
"download_progress": "下載進度",
|
||||
"server_install_progress": "伺服器安裝",
|
||||
"server_install_step": "步驟",
|
||||
"server_install_message": "訊息",
|
||||
"server_install_completed": "安裝完成!",
|
||||
"server_install_error": "安裝失敗",
|
||||
"server_install_duration": "耗時",
|
||||
"filter_mods_progress": "篩選模組",
|
||||
"filter_mods_total": "總模組數",
|
||||
"filter_mods_current": "目前檢查",
|
||||
"filter_mods_completed": "篩選完成!識別到 {filtered} 個客戶端模組,移動 {moved} 個",
|
||||
"filter_mods_error": "篩選失敗",
|
||||
"start_production": "開始製作,請勿切換選單!",
|
||||
"production_complete": "伺服器製作完成!共用時 {time} 秒!",
|
||||
"please_select_file": "請先拖曳或選擇檔案",
|
||||
"only_zip_mrpack": "僅支援 .zip 和 .mrpack 檔案",
|
||||
"file_prepared": "檔案準備完成",
|
||||
"preparing_file": "正在準備檔案...",
|
||||
"ws_connecting": "正在建立 WebSocket 連線...",
|
||||
"ws_connected": "WebSocket 連線成功",
|
||||
"ws_failed": "WebSocket 連線失敗",
|
||||
"request_failed": "請求後端服務失敗,請檢查後端服務是否執行",
|
||||
"backend_error": "DeEarthX.Core 遇到致命錯誤!",
|
||||
"backend_error_desc": "請將整個視窗截圖發在群裡\n錯誤訊息:{error}",
|
||||
"java_not_found": "未在系統變數中找到 Java!請安裝 Java,否則開伺服器模式將無法使用!",
|
||||
"unknown_step": "未知步驟",
|
||||
"parse_error": "解析伺服器訊息失敗",
|
||||
"speed": "速度",
|
||||
"remaining": "剩餘時間",
|
||||
"java_error_title": "Java 未找到",
|
||||
"java_error_desc": "未在系統變數中找到 Java!請安裝 Java,否則開伺服器模式將無法使用!\n\n建議:\n1. 下載並安裝 Java 17 或更高版本\n2. 設定 Java 環境變數\n3. 重新啟動應用程式",
|
||||
"network_error_title": "網路錯誤",
|
||||
"network_error_desc": "網路連線出現問題\n錯誤訊息:{error}",
|
||||
"file_error_title": "檔案錯誤",
|
||||
"file_error_desc": "檔案操作出現問題\n錯誤訊息:{error}",
|
||||
"memory_error_title": "記憶體錯誤",
|
||||
"memory_error_desc": "記憶體不足\n錯誤訊息:{error}",
|
||||
"unknown_error_title": "未知錯誤",
|
||||
"unknown_error_desc": "發生未知錯誤,請聯絡技術支援",
|
||||
"ws_error_title": "WebSocket 連線失敗",
|
||||
"ws_error_desc": "無法建立與後端的 WebSocket 連線",
|
||||
"suggestions": "建議解決方案",
|
||||
"suggestion_check_network": "檢查網路連線是否正常",
|
||||
"suggestion_check_firewall": "檢查防火牆設定",
|
||||
"suggestion_retry": "稍後重試",
|
||||
"suggestion_check_disk_space": "檢查磁碟空間是否充足",
|
||||
"suggestion_check_permission": "檢查檔案權限設定",
|
||||
"suggestion_check_file_format": "確認檔案格式正確",
|
||||
"suggestion_increase_memory": "增加分配給應用程式的記憶體",
|
||||
"suggestion_close_other_apps": "關閉其他佔用記憶體的應用程式",
|
||||
"suggestion_restart_application": "重新啟動應用程式",
|
||||
"suggestion_check_backend": "檢查後端服務是否正常執行",
|
||||
"suggestion_check_logs": "查看日誌檔案獲取更多資訊",
|
||||
"suggestion_check_port": "檢查連接埠 37019 是否被佔用",
|
||||
"suggestion_contact_support": "聯絡技術支援獲取協助",
|
||||
"template_select_title": "選擇伺服器模板",
|
||||
"template_select_desc": "為您的伺服器選擇一個模板,使用模板將跳過伺服器安裝,直接複製模板檔案到伺服器目錄",
|
||||
"template_official_loader": "無",
|
||||
"template_official_loader_desc": "不使用任何模板,進行標準的伺服器安裝流程",
|
||||
"template_name": "模板名稱",
|
||||
"template_description": "描述",
|
||||
"template_author": "作者",
|
||||
"template_version": "版本",
|
||||
"template_selected": "已選擇模板",
|
||||
"template_select_button": "選擇模板",
|
||||
"template_loading": "載入模板列表中...",
|
||||
"template_load_failed": "載入模板列表失敗",
|
||||
"template_apply_success": "模板應用成功",
|
||||
"template_apply_failed": "模板應用失敗"
|
||||
},
|
||||
"template": {
|
||||
"title": "模板管理",
|
||||
"description": "管理您的伺服器模板,建立、查看和刪除模板",
|
||||
"create_button": "建立模板",
|
||||
"create_title": "建立新模板",
|
||||
"name": "模板名稱",
|
||||
"name_required": "模板名稱不能為空",
|
||||
"name_placeholder": "請輸入模板名稱",
|
||||
"version": "版本",
|
||||
"version_placeholder": "請輸入版本號",
|
||||
"description": "描述",
|
||||
"description_placeholder": "請輸入模板描述",
|
||||
"author": "作者",
|
||||
"author_placeholder": "請輸入作者名稱",
|
||||
"delete_button": "刪除",
|
||||
"delete_title": "刪除模板",
|
||||
"delete_confirm": "確定要刪除模板 \"{name}\" 嗎?",
|
||||
"delete_warning": "此操作將永久刪除該模板及其所有檔案,無法恢復。",
|
||||
"delete_success": "模板刪除成功",
|
||||
"delete_failed": "模板刪除失敗",
|
||||
"create_success": "模板建立成功",
|
||||
"create_failed": "模板建立失敗",
|
||||
"open_folder": "打開文件夾",
|
||||
"open_folder_success": "已打開模板文件夾",
|
||||
"open_folder_failed": "打開文件夾失敗",
|
||||
"edit_button": "編輯",
|
||||
"edit_title": "編輯模板",
|
||||
"update_success": "模板更新成功",
|
||||
"update_failed": "模板更新失敗",
|
||||
"empty": "還沒有模板",
|
||||
"empty_hint": "點擊上方按鈕建立你的第一個模板"
|
||||
},
|
||||
"setting": {
|
||||
"title": "DeEarthX 設定",
|
||||
"subtitle": "讓 DeEarthX V3 更適合你!",
|
||||
"category_filter": "模組篩選設定",
|
||||
"category_mirror": "下載來源設定",
|
||||
"category_system": "系統管理設定",
|
||||
"filter_hashes_name": "雜湊過濾",
|
||||
"filter_hashes_desc": "過濾不必要的客戶端模組(雜湊過濾法)",
|
||||
"filter_dexpub_name": "Galaxy Square 過濾",
|
||||
"filter_dexpub_desc": "過濾 Galaxy Square 平台中記錄的客戶端檔案",
|
||||
"filter_modrinth_name": "Modrinth API 過濾",
|
||||
"filter_modrinth_desc": "透過 Modrinth API 檢查模組的客戶端/伺服器相容性",
|
||||
"filter_mixins_name": "Mixin 過濾",
|
||||
"filter_mixins_desc": "過濾 Client Mixin 相關檔案",
|
||||
"mirror_mcimirror_name": "MCIM 鏡像來源",
|
||||
"mirror_mcimirror_desc": "使用 MCIM 鏡像來源加速下載",
|
||||
"mirror_bmclapi_name": "BMCLAPI 鏡像來源",
|
||||
"mirror_bmclapi_desc": "使用 BMCLAPI 鏡像來源加速下載",
|
||||
"system_oaf_name": "操作完成後開啟目錄",
|
||||
"system_oaf_desc": "伺服器製作完成後自動開啟目錄",
|
||||
"system_autozip_name": "自動打包成 zip",
|
||||
"system_autozip_desc": "伺服器製作完成後自動打包成 zip(開伺服器模式不打包)",
|
||||
"switch_on": "開",
|
||||
"switch_off": "關",
|
||||
"language_title": "語言設定",
|
||||
"language_desc": "選擇介面顯示語言",
|
||||
"language_chinese": "簡體中文",
|
||||
"language_english": "English",
|
||||
"config_saved": "設定已儲存",
|
||||
"config_load_failed": "載入設定失敗",
|
||||
"config_save_failed": "儲存設定失敗",
|
||||
"export_config": "匯出設定",
|
||||
"import_config": "匯入設定",
|
||||
"config_exported": "設定匯出成功",
|
||||
"config_export_failed": "設定匯出失敗",
|
||||
"config_imported": "設定匯入成功",
|
||||
"config_import_failed": "設定匯入失敗",
|
||||
"config_invalid_format": "設定檔案格式無效"
|
||||
},
|
||||
"about": {
|
||||
"title": "關於 DeEarthX",
|
||||
"subtitle": "專業的 Minecraft 整合包伺服器製作工具",
|
||||
"about_software": "關於軟體",
|
||||
"current_version": "目前版本:",
|
||||
"build_time": "建置時間:",
|
||||
"author": "作者:",
|
||||
"development_team": "開發團隊",
|
||||
"author_tianpao": "天跑",
|
||||
"contribution_author": "作者",
|
||||
"dev2_xcc": "XCC",
|
||||
"contribution_dev2": "2號開發(改良與優化)",
|
||||
"contributor_bangbang93": "bangbang93",
|
||||
"contribution_bangbang93": "BMCLAPI 鏡像",
|
||||
"contributor_z0z0r4": "z0z0r4",
|
||||
"contribution_z0z0r4": "MCIM 鏡像",
|
||||
"sponsor": "贊助商",
|
||||
"sponsor_elfidc": "億訊雲",
|
||||
"sponsor_type_gold": "金牌贊助",
|
||||
"version_file_read_failed": "版本檔案讀取失敗"
|
||||
},
|
||||
"galaxy": {
|
||||
"title": "Galaxy Square",
|
||||
"subtitle": "讓所有模組都在這裡發光",
|
||||
"mod_submit_title": "模組提交",
|
||||
"mod_type_label": "模組類型",
|
||||
"mod_type_client": "客戶端模組",
|
||||
"mod_type_server": "伺服器模組",
|
||||
"modid_label": "Modid",
|
||||
"modid_placeholder": "請輸入 Modid(多個用逗號分隔)或上傳檔案自動獲取",
|
||||
"modid_count": "目前已新增 {count} 個 Modid",
|
||||
"upload_file_label": "上傳檔案",
|
||||
"upload_file_hint": "點擊或拖曳檔案到此區域上傳",
|
||||
"upload_file_support": "支援 .jar 格式檔案,可多選",
|
||||
"file_selected": "已選擇 {count} 個檔案",
|
||||
"start_upload": "開始上傳",
|
||||
"uploading": "上傳中...",
|
||||
"submit": "提交 {type} 模組",
|
||||
"submitting": "提交中...",
|
||||
"submit_confirm_title": "確認提交",
|
||||
"submit_confirm_content": "確定要提交 {count} 個 {type} 模組嗎?",
|
||||
"please_select_file": "請先選擇檔案",
|
||||
"upload_success": "成功上傳 {count} 個檔案",
|
||||
"data_format_error": "傳回資料格式錯誤",
|
||||
"upload_failed": "上傳失敗",
|
||||
"upload_error": "上傳出錯,請重試",
|
||||
"submit_success": "{type} 模組提交成功",
|
||||
"submit_failed": "提交失敗",
|
||||
"submit_error": "提交出錯,請重試"
|
||||
},
|
||||
"message": {
|
||||
"backend_running": "DeEarthX.Core 已在執行",
|
||||
"backend_started": "DeEarthX.Core 啟動成功",
|
||||
"backend_port_occupied": "37019 連接埠被其他應用程式佔用!",
|
||||
"backend_start_failed": "DeEarthX.Core 啟動失敗,請檢查 37019 連接埠是否被佔用!(已重試 {count} 次)",
|
||||
"backend_restart": "DeEarthX.Core 重新啟動!",
|
||||
"retry_start": "啟動失敗,正在重試 ({current}/{max})...",
|
||||
"config_load_error": "載入設定失敗"
|
||||
}
|
||||
}
|
||||
39
front/package.json
Normal file
@@ -0,0 +1,39 @@
|
||||
{
|
||||
"name": "dex-v3-ui",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vue-tsc --noEmit && vite build",
|
||||
"build:with-error-handling": "node build-with-error-handling.js",
|
||||
"preview": "vite preview",
|
||||
"tauri": "tauri",
|
||||
"tauri-dev": "tauri dev --no-watch",
|
||||
"tauri-build": "tauri build"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons-vue": "^7.0.1",
|
||||
"@tailwindcss/vite": "^4.2.1",
|
||||
"@tauri-apps/api": "^2.10.1",
|
||||
"@tauri-apps/plugin-dialog": "~2",
|
||||
"@tauri-apps/plugin-notification": "^2.3.3",
|
||||
"@tauri-apps/plugin-opener": "^2.5.3",
|
||||
"@tauri-apps/plugin-shell": "^2.3.5",
|
||||
"@tauri-apps/plugin-store": "^2.4.2",
|
||||
"ant-design-vue": "^4.2.6",
|
||||
"axios": "^1.13.6",
|
||||
"tailwindcss": "^4.2.1",
|
||||
"vue": "^3.5.29",
|
||||
"vue-i18n": "^11.2.8",
|
||||
"vue-router": "^4.6.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tauri-apps/cli": "^2.10.1",
|
||||
"@tauri-apps/plugin-shell": "^2.3.1",
|
||||
"@vitejs/plugin-vue": "^5.2.4",
|
||||
"typescript": "~5.6.3",
|
||||
"vite": "^6.4.1",
|
||||
"vue-tsc": "^2.2.12"
|
||||
}
|
||||
}
|
||||
BIN
front/public/bb93.jpg
Normal file
|
After Width: | Height: | Size: 71 KiB |
BIN
front/public/dex.png
Normal file
|
After Width: | Height: | Size: 4.9 MiB |
1
front/public/elfidc.svg
Normal file
|
After Width: | Height: | Size: 30 KiB |
6
front/public/tauri.svg
Normal file
@@ -0,0 +1,6 @@
|
||||
<svg width="206" height="231" viewBox="0 0 206 231" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M143.143 84C143.143 96.1503 133.293 106 121.143 106C108.992 106 99.1426 96.1503 99.1426 84C99.1426 71.8497 108.992 62 121.143 62C133.293 62 143.143 71.8497 143.143 84Z" fill="#FFC131"/>
|
||||
<ellipse cx="84.1426" cy="147" rx="22" ry="22" transform="rotate(180 84.1426 147)" fill="#24C8DB"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M166.738 154.548C157.86 160.286 148.023 164.269 137.757 166.341C139.858 160.282 141 153.774 141 147C141 144.543 140.85 142.121 140.558 139.743C144.975 138.204 149.215 136.139 153.183 133.575C162.73 127.404 170.292 118.608 174.961 108.244C179.63 97.8797 181.207 86.3876 179.502 75.1487C177.798 63.9098 172.884 53.4021 165.352 44.8883C157.82 36.3744 147.99 30.2165 137.042 27.1546C126.095 24.0926 114.496 24.2568 103.64 27.6274C92.7839 30.998 83.1319 37.4317 75.8437 46.1553C74.9102 47.2727 74.0206 48.4216 73.176 49.5993C61.9292 50.8488 51.0363 54.0318 40.9629 58.9556C44.2417 48.4586 49.5653 38.6591 56.679 30.1442C67.0505 17.7298 80.7861 8.57426 96.2354 3.77762C111.685 -1.01901 128.19 -1.25267 143.769 3.10474C159.348 7.46215 173.337 16.2252 184.056 28.3411C194.775 40.457 201.767 55.4101 204.193 71.404C206.619 87.3978 204.374 103.752 197.73 118.501C191.086 133.25 180.324 145.767 166.738 154.548ZM41.9631 74.275L62.5557 76.8042C63.0459 72.813 63.9401 68.9018 65.2138 65.1274C57.0465 67.0016 49.2088 70.087 41.9631 74.275Z" fill="#FFC131"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M38.4045 76.4519C47.3493 70.6709 57.2677 66.6712 67.6171 64.6132C65.2774 70.9669 64 77.8343 64 85.0001C64 87.1434 64.1143 89.26 64.3371 91.3442C60.0093 92.8732 55.8533 94.9092 51.9599 97.4256C42.4128 103.596 34.8505 112.392 30.1816 122.756C25.5126 133.12 23.9357 144.612 25.6403 155.851C27.3449 167.09 32.2584 177.598 39.7906 186.112C47.3227 194.626 57.153 200.784 68.1003 203.846C79.0476 206.907 90.6462 206.743 101.502 203.373C112.359 200.002 122.011 193.568 129.299 184.845C130.237 183.722 131.131 182.567 131.979 181.383C143.235 180.114 154.132 176.91 164.205 171.962C160.929 182.49 155.596 192.319 148.464 200.856C138.092 213.27 124.357 222.426 108.907 227.222C93.458 232.019 76.9524 232.253 61.3736 227.895C45.7948 223.538 31.8055 214.775 21.0867 202.659C10.3679 190.543 3.37557 175.59 0.949823 159.596C-1.47592 143.602 0.768139 127.248 7.41237 112.499C14.0566 97.7497 24.8183 85.2327 38.4045 76.4519ZM163.062 156.711L163.062 156.711C162.954 156.773 162.846 156.835 162.738 156.897C162.846 156.835 162.954 156.773 163.062 156.711Z" fill="#24C8DB"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.5 KiB |
BIN
front/public/tianpao.jpg
Normal file
|
After Width: | Height: | Size: 32 KiB |
5
front/public/version.json
Normal file
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "3.0.35",
|
||||
"buildTime": "2026-03-10",
|
||||
"author": "DeEarthX Team"
|
||||
}
|
||||
1
front/public/vite.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
BIN
front/public/xcc.jpg
Normal file
|
After Width: | Height: | Size: 9.6 KiB |
BIN
front/public/z0z0r4.jpg
Normal file
|
After Width: | Height: | Size: 80 KiB |
7
front/src-tauri/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
||||
# Generated by Cargo
|
||||
# will have compiled files and executables
|
||||
/target/
|
||||
|
||||
# Generated by Tauri
|
||||
# will have schema files for capabilities auto-completion
|
||||
/gen/schemas
|
||||
3
front/src-tauri/.taurignore
Normal file
@@ -0,0 +1,3 @@
|
||||
capabilities/
|
||||
target/debug/
|
||||
target/release/
|
||||
5
front/src-tauri/2
Normal file
@@ -0,0 +1,5 @@
|
||||
|
||||
added 1 package in 2s
|
||||
|
||||
16 packages are looking for funding
|
||||
run `npm fund` for details
|
||||
5483
front/src-tauri/Cargo.lock
generated
Normal file
30
front/src-tauri/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "dex-v3-ui"
|
||||
version = "1.0.0"
|
||||
description = "DeEarthX V3 - Minecraft整合包服务端制作工具"
|
||||
authors = ["Tianpao"]
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[lib]
|
||||
# The `_lib` suffix may seem redundant but it is necessary
|
||||
# to make the lib name unique and wouldn't conflict with the bin name.
|
||||
# This seems to be only an issue on Windows, see https://github.com/rust-lang/cargo/issues/8519
|
||||
name = "dex_v3_ui_lib"
|
||||
crate-type = ["staticlib", "cdylib", "rlib"]
|
||||
|
||||
[build-dependencies]
|
||||
tauri-build = { version = "2", features = [] }
|
||||
|
||||
[dependencies]
|
||||
tauri = { version = "2", features = [] }
|
||||
tauri-plugin-opener = "2"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
open = "5.3.2"
|
||||
tauri-plugin-store = "2"
|
||||
tauri-plugin-shell = "2"
|
||||
tauri-plugin-notification = "2"
|
||||
tauri-plugin-dialog = "2"
|
||||
|
||||
3
front/src-tauri/build.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
fn main() {
|
||||
tauri_build::build()
|
||||
}
|
||||
30
front/src-tauri/capabilities/default.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"$schema": "../gen/schemas/desktop-schema.json",
|
||||
"identifier": "default",
|
||||
"description": "Capability for the main window",
|
||||
"windows": [
|
||||
"main"
|
||||
],
|
||||
"permissions": [
|
||||
"core:default",
|
||||
"opener:default",
|
||||
"store:default",
|
||||
"shell:default",
|
||||
"shell:allow-execute",
|
||||
"shell:allow-spawn",
|
||||
"shell:allow-kill",
|
||||
"shell:allow-open",
|
||||
{
|
||||
"identifier": "shell:allow-spawn",
|
||||
"allow": [
|
||||
{
|
||||
"name": "core",
|
||||
"cmd": "core.exe",
|
||||
"args": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"notification:default",
|
||||
"dialog:default"
|
||||
]
|
||||
}
|
||||
BIN
front/src-tauri/icons/128x128.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
front/src-tauri/icons/128x128.png1
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
front/src-tauri/icons/128x128@2x.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
front/src-tauri/icons/128x128@2x.png1
Normal file
|
After Width: | Height: | Size: 6.8 KiB |
BIN
front/src-tauri/icons/32x32.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
front/src-tauri/icons/32x32.png1
Normal file
|
After Width: | Height: | Size: 974 B |
BIN
front/src-tauri/icons/Square107x107Logo.png
Normal file
|
After Width: | Height: | Size: 2.8 KiB |
BIN
front/src-tauri/icons/Square142x142Logo.png
Normal file
|
After Width: | Height: | Size: 3.8 KiB |
BIN
front/src-tauri/icons/Square150x150Logo.png
Normal file
|
After Width: | Height: | Size: 3.9 KiB |
BIN
front/src-tauri/icons/Square284x284Logo.png
Normal file
|
After Width: | Height: | Size: 7.6 KiB |
BIN
front/src-tauri/icons/Square30x30Logo.png
Normal file
|
After Width: | Height: | Size: 903 B |
BIN
front/src-tauri/icons/Square310x310Logo.png
Normal file
|
After Width: | Height: | Size: 8.4 KiB |
BIN
front/src-tauri/icons/Square44x44Logo.png
Normal file
|
After Width: | Height: | Size: 1.3 KiB |
BIN
front/src-tauri/icons/Square71x71Logo.png
Normal file
|
After Width: | Height: | Size: 2.0 KiB |
BIN
front/src-tauri/icons/Square89x89Logo.png
Normal file
|
After Width: | Height: | Size: 2.4 KiB |
BIN
front/src-tauri/icons/StoreLogo.png
Normal file
|
After Width: | Height: | Size: 1.5 KiB |
BIN
front/src-tauri/icons/icon.icns
Normal file
BIN
front/src-tauri/icons/icon.icns1
Normal file
BIN
front/src-tauri/icons/icon.ico
Normal file
|
After Width: | Height: | Size: 66 KiB |
BIN
front/src-tauri/icons/icon.ico1
Normal file
|
After Width: | Height: | Size: 85 KiB |
BIN
front/src-tauri/icons/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
21
front/src-tauri/src/lib.rs
Normal file
@@ -0,0 +1,21 @@
|
||||
// Learn more about Tauri commands at https://tauri.app/develop/calling-rust/
|
||||
#[allow(unused_imports)]
|
||||
use tauri_plugin_opener::OpenerExt;
|
||||
|
||||
#[tauri::command]
|
||||
fn greet(name: &str) -> String {
|
||||
format!("Hello, {}! You've been greeted from Rust!", name)
|
||||
}
|
||||
|
||||
#[cfg_attr(mobile, tauri::mobile_entry_point)]
|
||||
pub fn run() {
|
||||
tauri::Builder::default()
|
||||
.plugin(tauri_plugin_dialog::init())
|
||||
.plugin(tauri_plugin_notification::init())
|
||||
.plugin(tauri_plugin_shell::init())
|
||||
.plugin(tauri_plugin_store::Builder::new().build())
|
||||
.plugin(tauri_plugin_opener::init())
|
||||
.invoke_handler(tauri::generate_handler![greet])
|
||||
.run(tauri::generate_context!())
|
||||
.expect("error while running tauri application");
|
||||
}
|
||||
6
front/src-tauri/src/main.rs
Normal file
@@ -0,0 +1,6 @@
|
||||
// Prevents additional console window on Windows in release, DO NOT REMOVE!!
|
||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||
|
||||
fn main() {
|
||||
dex_v3_ui_lib::run()
|
||||
}
|
||||
55
front/src-tauri/tauri.conf.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "DeEarthX-V3",
|
||||
"version": "1.0.0",
|
||||
"identifier": "top.tianpao.dex-v3-ui",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
"devUrl": "http://localhost:9888",
|
||||
"beforeBuildCommand": "npm run build",
|
||||
"frontendDist": "../dist"
|
||||
},
|
||||
"app": {
|
||||
"windows": [
|
||||
{
|
||||
"title": "DeEarthX V3",
|
||||
"width": 1024,
|
||||
"height": 600,
|
||||
"minWidth": 800,
|
||||
"minHeight": 500,
|
||||
"dragDropEnabled": false,
|
||||
"fullscreen": false,
|
||||
"resizable": true,
|
||||
"center": true
|
||||
}
|
||||
],
|
||||
"security": {
|
||||
"csp": null
|
||||
}
|
||||
},
|
||||
"bundle": {
|
||||
"active": true,
|
||||
"targets": "all",
|
||||
"icon": [
|
||||
"icons/32x32.png",
|
||||
"icons/128x128.png",
|
||||
"icons/128x128@2x.png",
|
||||
"icons/icon.icns",
|
||||
"icons/icon.ico"
|
||||
],
|
||||
"externalBin": ["binaries/core"],
|
||||
"windows": {
|
||||
"webviewInstallMode": {
|
||||
"type": "downloadBootstrapper"
|
||||
},
|
||||
"nsis": {
|
||||
"languages": ["SimpChinese","English"],
|
||||
"displayLanguageSelector": true,
|
||||
"installMode": "perMachine"
|
||||
},
|
||||
"wix": {
|
||||
"language": "zh-CN"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
403
front/src/App.vue
Normal file
@@ -0,0 +1,403 @@
|
||||
<script lang="ts" setup>
|
||||
import { h, provide, ref, onMounted, computed } from 'vue';
|
||||
import { MenuProps, message } from 'ant-design-vue';
|
||||
import { SettingOutlined, UploadOutlined, UserOutlined, WindowsOutlined, LoadingOutlined, CheckCircleOutlined, CloseCircleOutlined, FileSearchOutlined, FolderOutlined } from '@ant-design/icons-vue';
|
||||
import { useRouter, useRoute } from 'vue-router';
|
||||
import { Command } from '@tauri-apps/plugin-shell';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
let killCoreProcess: (() => void) | null = null;
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
// 版本号相关
|
||||
const version = ref<string>('V3');
|
||||
|
||||
// 加载版本号
|
||||
async function loadVersion() {
|
||||
try {
|
||||
console.log('开始加载版本号...');
|
||||
const response = await fetch('/version.json');
|
||||
console.log('version.json 响应状态:', response.status);
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP error! status: ${response.status}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
console.log('版本号数据:', data);
|
||||
version.value = `V${data.version}`;
|
||||
console.log('设置版本号为:', version.value);
|
||||
} catch (error) {
|
||||
console.error('加载版本号失败:', error);
|
||||
version.value = 'V3';
|
||||
}
|
||||
}
|
||||
|
||||
// 后端连接状态相关
|
||||
const backendStatus = ref<'loading' | 'success' | 'error'>('loading');
|
||||
const backendErrorInfo = ref<string>('');
|
||||
const retryCount = ref<number>(0);
|
||||
const maxRetries = 5;
|
||||
|
||||
// 检测端口是否被正确的后端占用
|
||||
async function checkPortOccupied(): Promise<'correct_backend' | 'wrong_app' | 'free'> {
|
||||
try {
|
||||
const response = await fetch("http://localhost:37019/config/get", {
|
||||
method: "GET",
|
||||
signal: AbortSignal.timeout(1000)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const config = await response.json();
|
||||
// 检查是否包含 DeEarthX 后端的特征字段(mirror、filter 等)
|
||||
if (config.mirror !== undefined || config.filter !== undefined) {
|
||||
// 端口被正确的后端占用
|
||||
return 'correct_backend';
|
||||
} else {
|
||||
// 端口被其他应用占用
|
||||
return 'wrong_app';
|
||||
}
|
||||
} else {
|
||||
return 'free';
|
||||
}
|
||||
} catch (error) {
|
||||
// 连接失败,端口可能是空闲的
|
||||
return 'free';
|
||||
}
|
||||
}
|
||||
|
||||
// 启动后端核心服务
|
||||
async function runCoreProcess() {
|
||||
// 先检测端口状态
|
||||
const portStatus = await checkPortOccupied();
|
||||
|
||||
if (portStatus === 'correct_backend') {
|
||||
// 端口已经被正确的后端占用,直接使用
|
||||
backendStatus.value = 'success';
|
||||
backendErrorInfo.value = '';
|
||||
message.success(t('message.backend_running'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (portStatus === 'wrong_app') {
|
||||
// 端口被其他应用占用
|
||||
backendStatus.value = 'error';
|
||||
backendErrorInfo.value = t('message.backend_port_occupied');
|
||||
message.error(t('message.backend_port_occupied'));
|
||||
return;
|
||||
}
|
||||
|
||||
// 端口空闲,尝试启动后端
|
||||
backendStatus.value = 'loading';
|
||||
|
||||
Command.create("core").spawn()
|
||||
.then((e) => {
|
||||
console.log("DeEarthX V3 Core");
|
||||
killCoreProcess = e.kill;
|
||||
|
||||
// 等待后端启动并检查状态
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
const response = await fetch("http://localhost:37019/", { method: "GET" });
|
||||
if (response.ok) {
|
||||
backendStatus.value = 'success';
|
||||
backendErrorInfo.value = '';
|
||||
message.success(t('message.backend_started'));
|
||||
} else {
|
||||
backendStatus.value = 'error';
|
||||
backendErrorInfo.value = t('common.status_error');
|
||||
router.push('/error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("后端连接失败:", error);
|
||||
backendStatus.value = 'error';
|
||||
backendErrorInfo.value = t('common.status_error');
|
||||
router.push('/error');
|
||||
}
|
||||
}, 3000); // 等待3秒让后端启动
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
retryCount.value++;
|
||||
|
||||
if (retryCount.value <= maxRetries) {
|
||||
message.info(t('message.retry_start', { current: retryCount.value, max: maxRetries }));
|
||||
setTimeout(() => {
|
||||
runCoreProcess();
|
||||
}, 2000);
|
||||
} else {
|
||||
backendStatus.value = 'error';
|
||||
backendErrorInfo.value = t('message.backend_start_failed', { count: maxRetries });
|
||||
message.error(t('message.backend_start_failed', { count: maxRetries }));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 组件挂载时启动后端
|
||||
onMounted(async () => {
|
||||
loadVersion();
|
||||
runCoreProcess();
|
||||
});
|
||||
|
||||
provide("killCoreProcess", () => {
|
||||
if (killCoreProcess && typeof killCoreProcess === 'function') {
|
||||
killCoreProcess();
|
||||
killCoreProcess = null;
|
||||
message.info(t('message.backend_restart'));
|
||||
runCoreProcess();
|
||||
}
|
||||
});
|
||||
|
||||
// 导航菜单配置
|
||||
const selectedKeys = ref<(string | number)[]>(['main']);
|
||||
|
||||
// 监听路由变化,更新选中菜单
|
||||
router.beforeEach((to, _from, next) => {
|
||||
const routeToKey: Record<string, string> = {
|
||||
'/': 'main',
|
||||
'/setting': 'setting',
|
||||
'/about': 'about',
|
||||
'/error': 'main',
|
||||
'/galaxy': 'galaxy',
|
||||
'/deearth': 'deearth',
|
||||
'/template': 'template'
|
||||
};
|
||||
selectedKeys.value[0] = routeToKey[to.path] || 'main';
|
||||
next();
|
||||
});
|
||||
|
||||
// 菜单项配置(使用计算属性使其响应语言变化)
|
||||
const menuItems = computed<MenuProps['items']>(() => {
|
||||
return [
|
||||
{
|
||||
key: 'main',
|
||||
icon: h(WindowsOutlined),
|
||||
label: t('menu.home'),
|
||||
title: t('menu.home'),
|
||||
},
|
||||
{
|
||||
key: 'deearth',
|
||||
icon: h(FileSearchOutlined),
|
||||
label: t('menu.deearth'),
|
||||
title: t('menu.deearth'),
|
||||
},
|
||||
{
|
||||
key: 'galaxy',
|
||||
icon: h(UploadOutlined),
|
||||
label: t('menu.galaxy'),
|
||||
title: t('menu.galaxy'),
|
||||
},
|
||||
{
|
||||
key: 'template',
|
||||
icon: h(FolderOutlined),
|
||||
label: t('menu.template'),
|
||||
title: t('menu.template'),
|
||||
},
|
||||
{
|
||||
key: 'setting',
|
||||
icon: h(SettingOutlined),
|
||||
label: t('menu.setting'),
|
||||
title: t('menu.setting'),
|
||||
},
|
||||
{
|
||||
key: 'about',
|
||||
icon: h(UserOutlined),
|
||||
label: t('menu.about'),
|
||||
title: t('menu.about'),
|
||||
}
|
||||
];
|
||||
});
|
||||
|
||||
// 菜单点击事件处理
|
||||
const handleMenuClick: MenuProps['onClick'] = (e) => {
|
||||
selectedKeys.value[0] = e.key;
|
||||
const routeMap: Record<string, string> = {
|
||||
main: '/',
|
||||
deearth: '/deearth',
|
||||
setting: '/setting',
|
||||
about: '/about',
|
||||
galaxy: '/galaxy',
|
||||
template: '/template'
|
||||
};
|
||||
const route = routeMap[e.key] || '/';
|
||||
router.push(route);
|
||||
};
|
||||
|
||||
// 主题配置
|
||||
const theme = ref({
|
||||
token: {
|
||||
colorPrimary: '#67eac3',
|
||||
borderRadius: 8,
|
||||
},
|
||||
components: {
|
||||
Menu: {
|
||||
itemActiveBg: '#e8fff5',
|
||||
itemSelectedBg: '#e8fff5',
|
||||
itemSelectedColor: '#10b981',
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<a-config-provider :theme="theme">
|
||||
<div class="tw:h-screen tw:w-screen tw:flex tw:flex-col tw:overflow-hidden">
|
||||
<!-- 顶部导航栏 -->
|
||||
<a-page-header
|
||||
class="tw:h-14 tw:px-6 tw:flex tw:items-center tw:bg-white tw:shadow-sm tw:z-10 tw:transition-all tw:duration-300"
|
||||
style="border: none;"
|
||||
|
||||
>
|
||||
<!-- <template #extra>
|
||||
<a-button @click="openAuthorBilibili">作者B站</a-button>
|
||||
</template> -->
|
||||
<!-- 后端状态图标 -->
|
||||
<template #title>
|
||||
<div class="tw:flex tw:items-center tw:gap-3">
|
||||
<span>
|
||||
<span style="color: #000000; font-weight: 500;">{{ t('common.app_name') }}</span>
|
||||
<span style="color: #888888; font-size: 12px; margin-left: 5px;">{{ version }}</span>
|
||||
</span>
|
||||
<span
|
||||
class="tw:flex tw:items-center tw:gap-2"
|
||||
:title="backendErrorInfo || t('message.backend_running')"
|
||||
>
|
||||
<LoadingOutlined v-if="backendStatus === 'loading'" style="color: #1890ff; font-size: 18px;" />
|
||||
<CheckCircleOutlined v-else-if="backendStatus === 'success'" style="color: #52c41a; font-size: 18px;" />
|
||||
<CloseCircleOutlined v-else style="color: #ff4d4f; font-size: 18px;" />
|
||||
<span class="tw:text-xs tw:ml-1"
|
||||
:style="{
|
||||
color: backendStatus === 'loading' ? '#1890ff' :
|
||||
backendStatus === 'success' ? '#52c41a' : '#ff4d4f'
|
||||
}">
|
||||
{{ backendStatus === 'loading' ? t('common.status_loading') :
|
||||
backendStatus === 'success' ? t('common.status_success') : t('common.status_error') }}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
</a-page-header>
|
||||
|
||||
<!-- 主体内容区域 -->
|
||||
<div class="tw:flex tw:flex-1 tw:overflow-hidden">
|
||||
<!-- 侧边菜单 -->
|
||||
<a-menu
|
||||
id="menu"
|
||||
class="tw:shadow-lg tw:z-20"
|
||||
style="width: 180px; flex-shrink: 0;"
|
||||
:selectedKeys="selectedKeys"
|
||||
mode="inline"
|
||||
:items="menuItems"
|
||||
@click="handleMenuClick"
|
||||
/>
|
||||
|
||||
<!-- 内容区域 - 带过渡动画 -->
|
||||
<div class="tw:flex-1 tw:overflow-hidden tw:relative tw:bg-gradient-to-br tw:from-slate-50 tw:via-blue-50 tw:to-indigo-50">
|
||||
<router-view v-slot="{ Component }">
|
||||
<transition
|
||||
name="fade-slide"
|
||||
mode="out-in"
|
||||
appear
|
||||
>
|
||||
<component :is="Component" :key="route.path" class="tw:w-full tw:h-full tw:absolute tw:top-0 tw:left-0" />
|
||||
</transition>
|
||||
</router-view>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a-config-provider>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 禁止选择文本的样式 */
|
||||
h1,
|
||||
li,
|
||||
p,
|
||||
span {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
-ms-user-select: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* 禁止拖拽图片 */
|
||||
img {
|
||||
-webkit-user-drag: none;
|
||||
-moz-user-drag: none;
|
||||
-ms-user-drag: none;
|
||||
}
|
||||
|
||||
/* 页面切换过渡动画 - 淡入淡出 + 滑动 */
|
||||
.fade-slide-enter-active {
|
||||
animation: fadeSlideIn 0.4s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.fade-slide-leave-active {
|
||||
animation: fadeSlideOut 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
@keyframes fadeSlideIn {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(20px);
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes fadeSlideOut {
|
||||
0% {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-20px);
|
||||
}
|
||||
}
|
||||
|
||||
/* 菜单项悬停效果优化 */
|
||||
#menu .ant-menu-item {
|
||||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
#menu .ant-menu-item:hover {
|
||||
transform: translateX(4px);
|
||||
background: #f0fdf9;
|
||||
}
|
||||
|
||||
#menu .ant-menu-item-selected {
|
||||
background: linear-gradient(135deg, #d1fae5 0%, #e8fff5 100%);
|
||||
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.15);
|
||||
}
|
||||
|
||||
#menu .ant-menu-item-selected .anticon {
|
||||
color: #10b981;
|
||||
}
|
||||
|
||||
/* 滚动条美化 */
|
||||
::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: #f1f5f9;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: linear-gradient(180deg, #94a3b8 0%, #64748b 100%);
|
||||
border-radius: 4px;
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: linear-gradient(180deg, #64748b 0%, #475569 100%);
|
||||
}
|
||||
</style>
|
||||
1
front/src/assets/vue.svg
Normal file
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="37.07" height="36" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 198"><path fill="#41B883" d="M204.8 0H256L128 220.8L0 0h97.92L128 51.2L157.44 0h47.36Z"></path><path fill="#41B883" d="m0 0l128 220.8L256 0h-51.2L128 132.48L50.56 0H0Z"></path><path fill="#35495E" d="M50.56 0L128 133.12L204.8 0h-47.36L128 51.2L97.92 0H50.56Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 496 B |
44
front/src/components/ModeSelector.vue
Normal file
@@ -0,0 +1,44 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed } from 'vue';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import type { SelectProps } from 'ant-design-vue/es/vc-select';
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
interface Props {
|
||||
modelValue: string;
|
||||
javaAvailable: boolean;
|
||||
}
|
||||
|
||||
const props = defineProps<Props>();
|
||||
const emit = defineEmits<{
|
||||
'update:modelValue': [value: string];
|
||||
'select': [value: string];
|
||||
}>();
|
||||
|
||||
const modeOptions = computed<SelectProps['options']>(() => {
|
||||
return [
|
||||
{ label: t('home.mode_server'), value: 'server', disabled: !props.javaAvailable },
|
||||
{ label: t('home.mode_upload'), value: 'upload', disabled: false }
|
||||
];
|
||||
});
|
||||
|
||||
function handleSelect(value: string) {
|
||||
emit('update:modelValue', value);
|
||||
emit('select', value);
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<h2 class="tw:text-sm tw:font-semibold tw:text-gray-700 tw:mb-3">
|
||||
{{ t('home.mode_title') }}
|
||||
</h2>
|
||||
<a-select
|
||||
:value="modelValue"
|
||||
:options="modeOptions"
|
||||
@select="handleSelect"
|
||||
class="tw:w-full"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||