1. 概览
在健康和健身应用领域,为用户提供丰富而引人入胜的体验至关重要。对于瑜伽应用,这意味着不仅要提供简单的姿势文字说明,还要提供全面的信息、多媒体内容和智能搜索功能。在这篇博文中,我们将探讨如何使用 Google Cloud 的 Firestore 构建强大的瑜伽姿势数据库,如何利用其 Vector Search 扩展程序进行情境匹配,以及如何集成 Gemini 2.0 Flash(实验版)的强大功能来处理多模态内容。
为什么选择 Firestore?
Firestore 是 Google Cloud 的无服务器 NoSQL 文档数据库,非常适合用于构建可扩缩的动态应用。以下是它非常适合我们的瑜伽应用的原因:
- 可伸缩性和性能:Firestore 可自动扩缩以处理数百万用户和海量数据集,确保您的应用即使在不断增长的情况下也能保持响应速度。
- 实时更新:内置的实时同步功能可确保所有关联的客户端上的数据保持一致,非常适合实时课程或协作练习等功能。
- 灵活的数据模型:Firestore 基于文档的结构可让您存储各种数据类型,包括文本、图片,甚至嵌入内容,非常适合表示复杂的瑜伽姿势信息。
- 强大的查询功能:Firestore 支持复杂的查询,包括等值查询、不等值查询,以及现在通过新扩展程序支持的向量相似度搜索。
- 离线支持:Firestore 会在本地缓存数据,让您的应用即使在用户离线时也能正常运行。
使用 Firestore Vector Search 扩展程序增强搜索功能
在处理瑜伽姿势等复杂概念时,传统的基于关键字的搜索可能会受到限制。用户可能想搜索“打开髋部”或“改善平衡”的姿势,但不知道具体姿势名称。这时,向量搜索就派上用场了。
借助与 Firestore 集成的 Vector Search,您可以:
- 生成嵌入:使用 Vertex AI 中提供的模型或自定义模型,将文本说明(未来可能还包括图片和音频)转换为可捕捉其语义含义的数值向量表示形式(嵌入)。
- 存储嵌入:直接将这些嵌入存储在 Firestore 文档中。
- 执行相似度搜索:查询数据库以查找与给定查询向量在语义上相似的文档,从而实现上下文匹配。
集成 Gemini 2.0 Flash(实验版)
Gemini 2.0 Flash 是 Google 最先进的多模态 AI 模型。虽然仍处于实验阶段,但它为丰富我们的瑜伽应用提供了令人兴奋的可能性:
- 文本生成:使用 Gemini 2.0 Flash 生成瑜伽体式的详细说明,包括益处、调整和禁忌症。
- 图片生成(模拟):虽然直接使用 Gemini 生成图片的功能尚未公开提供,但我已使用 Google 的 Imagen 模拟了此功能,生成了直观展示姿势的图片。
- 音频生成(模仿):同样,我们可以使用文字转语音 (TTS) 服务为每个姿势创建音频指令,引导用户完成练习。
我设想提出集成建议,以增强应用,使其能够使用模型的以下功能:
- Multimodal Live API:借助这一新的 API,您可以创建支持工具使用的实时视觉和音频流式传输应用。
- 速度和性能:与 Gemini 1.5 Flash 相比,Gemini 2.0 Flash 的首个令牌生成时间 (TTFT) 大幅缩短。
- 改进了智能体体验:Gemini 2.0 改进了多模态理解、编码、复杂指令遵从和函数调用功能。这些改进相辅相成,可支持更好的智能体体验。
如需了解详情,请参阅这篇关于 Gemini 1.5 Flash 的文档页面。
使用 Google 搜索建立依据
为了提高可信度并提供更多资源,我们可以集成 Google 搜索来为应用提供的信息建立依据。这意味着:
- 情境搜索:当管理员用户输入姿势的详细信息时,我们可以使用姿势名称执行 Google 搜索。
- 网址提取:我们可以从搜索结果中提取相关网址(例如文章、视频或信誉良好的瑜伽网站),并在应用内显示这些网址。
构建内容
在本实验中,您将:
- 创建 Firestore 集合并加载瑜伽文档
- 了解如何使用 Firestore 创建 CRUD 应用
- 使用 Gemini 2.0 Flash 生成瑜伽姿势说明
- 启用与 Firestore 集成的 Firebase 向量搜索功能
- 根据瑜伽说明生成嵌入
- 针对用户搜索文本执行相似度搜索
要求
2. 准备工作
创建项目
- 在 Google Cloud Console 的项目选择器页面上,选择或创建一个 Google Cloud 项目。
- 确保您的 Cloud 项目已启用结算功能。了解如何检查项目是否已启用结算功能。
- 您将使用 Cloud Shell,这是一个在 Google Cloud 中运行的命令行环境,它预加载了 bq。点击 Google Cloud 控制台顶部的“激活 Cloud Shell”。

- 连接到 Cloud Shell 后,您可以使用以下命令检查自己是否已通过身份验证,以及项目是否已设置为您的项目 ID:
gcloud auth list
- 在 Cloud Shell 中运行以下命令,以确认 gcloud 命令了解您的项目。
gcloud config list project
- 如果项目未设置,请使用以下命令进行设置:
gcloud config set project <YOUR_PROJECT_ID>
- 点击此链接,按照说明启用所需的 API,直到您可以点击“启用”按钮为止。
如果遗漏了任何 API,您始终可以在实施过程中启用它。
如需了解 gcloud 命令和用法,请参阅文档。
3. 数据库设置
该文档提供了有关如何设置 Firestore 实例的更完整的步骤。概括来讲,我将按以下步骤开始操作:
前往 Firestore 查看器,然后在“选择数据库服务”界面中,选择“原生模式下的 Firestore”
- 为 Firestore 选择一个位置(请务必选择 us-central1,并在整个 Codelab 中选择区域 / 位置时遵循此规则)
- 点击“创建数据库”(如果是首次创建,请将其保留为“(default)”数据库)
您在创建 Firestore 项目时,也会在 Cloud API 管理器中启用 API
- 重要提示:请选择安全规则的测试(而非生产)版本,以便访问数据
- 设置完成后,您应该会看到原生模式下的 Firestore 数据库、集合和文档视图,如下图所示:

- 虽然现在还不用执行此步骤,但为了记录,您可以点击“开始收集”并创建一个新集合。将集合 ID 设置为“姿势”。点击保存按钮。

生产应用方面的专业提示:
- 确定数据模型并确定哪些人应能够访问不同类型的文档后,您可以在 Firebase 界面中创建、修改和监控安全规则。您可以通过以下链接访问安全规则:https://console.firebase.google.com/u/0/project/<<your_project_id>>/firestore/rules
- 在从开发阶段部署 / 推出项目之前,请务必修改、监控和测试安全规则,因为这通常是导致应用运行方式不同的幕后原因 :)
在本演示中,我们将以 TEST 模式使用它。
4. Firestore REST API
- REST API 在以下使用情形中会很有帮助:a. 从资源受限的环境(无法运行完整客户端库)访问 Firestore。自动执行数据库管理,或检索详细的数据库元数据
- 虽然 Firestore 最简单的用法是使用某个原生客户端库,但在有些情况下,直接调用 REST API 会很有用。
- 在本博文中,您将看到 Firestore REST API 的用法和演示,而不是原生客户端库
- Firestore REST API 接受使用 Firebase Authentication ID 令牌或 Google Identity OAuth 2.0 令牌进行身份验证。如需详细了解身份验证和授权主题,请参阅相关文档。
- 所有 REST API 端点都存在于基准网址 https://firestore.googleapis.com/v1/ 下面。
Spring Boot 和 Firestore API
此 Spring Boot 框架中的解决方案旨在演示一个客户端应用,该应用使用 Firestore API 来收集和修改瑜伽姿势和呼吸细节,并提供用户互动体验。
如需详细了解瑜伽姿势应用中 Firestore CRUD 解决方案部分的分步说明,您可以访问此博客链接。
为了专注于当前解决方案并随时随地学习 CRUD 部分,请从 Cloud Shell 终端克隆以下代码库中专注于此博客的整个解决方案,并获取代码库的副本。
git clone https://github.com/AbiramiSukumaran/firestore-poserecommender
请注意:
- 克隆此代码库后,您只需对项目 ID、API 等进行一些更改。无需进行任何其他更改,即可让应用正常运行。在接下来的部分中,我们将介绍应用的各个组成部分。以下是变更列表:
- 在
src/main/java/com/example/demo/GenerateImageSample.java文件中,将“<<YOUR_PROJECT_ID>>”替换为您的项目 ID - 在
src/main/java/com/example/demo/GenerateEmbeddings.java文件中,将“<<YOUR_PROJECT_ID>>”替换为您的项目 ID - 在
src/main/java/com/example/demo/PoseController.java中,将所有出现的“<<YOUR_PROJECT_ID>>"”和数据库名称(在本例中为"(default)",)替换为配置中的相应值:, - 在
src/main/java/com/example/demo/PoseController.java中,将“[YOUR_API_KEY]”替换为 Gemini 2.0 Flash 的 API 密钥。您可以从 AI Studio 获取此密钥。 - 如果您想在本地进行测试,请在 Cloud Shell 终端中从项目文件夹运行以下命令:
mvn package
mvn spring-boot:run
现在,您可以点击 Cloud Shell 终端中的“网页预览”选项,查看正在运行的应用。我们尚未准备好执行测试和试用应用。
- 可选:如果您想在 Cloud Run 中部署应用,则必须从 Cloud Shell 编辑器中从头开始引导全新的 Java Cloud Run 应用,并将 仓库中的 src 文件和模板文件添加到新项目的相应文件夹中(因为当前 GitHub 仓库项目默认情况下未针对 Cloud Run 部署配置进行设置)。在这种情况下,应遵循以下步骤(而不是克隆现有代码库):
- 前往 Cloud Shell 编辑器(确保编辑器已打开,而不是终端),然后点击状态栏左侧的 Google Cloud 项目名称图标(下方屏幕截图中被遮盖的部分)

- 从选项列表中选择“新建应用” ->“Cloud Run 应用” ->“Java: Cloud Run”,并将其命名为“firestore-poserecommender”

- 现在,您应该会看到 Java Cloud Run 应用的完整堆栈模板,该模板已预先配置好,可随时使用
- 移除现有的 Controller 类,并将以下文件复制到项目结构中的相应文件夹中:
firestore-poserecommender/src/main/java/com/example/demo/
- FirestoreSampleApplication.java
- GenerateEmbeddings.java
- GenerateImageSample.java
- Pose.java
- PoseController.java
- ServletInitializer.java
firestore-poserecommender/src/main/resources/static/ - Index.html
firestore-poserecommender/src/main/resources/templates/
- contextsearch.html
- createpose.html
- errmessage.html
- pose.html
- ryoq.html
- searchpose.html
- showmessage.html
firestore-poserecommender/
- Dockerfile
- 您需要在相应文件中进行更改,将项目 ID 和 API 密钥替换为各自的值。(上述第 1 步的 a、b、c 和 d)。
5. 数据注入
应用的数据位于此文件 data.json 中:https://github.com/AbiramiSukumaran/firestore-poserecommender/blob/main/data.json
如果您想从一些预定义的数据开始,可以复制 JSON 并将所有出现的“<<YOUR_PROJECT_ID>>”替换为您的值
- 前往 Firestore Studio
- 确保您已创建名为“poses”的集合
- 手动添加上述 repo 文件中的文档,一次添加一个
您也可以通过运行以下步骤,从我们为您创建的预定义集中一次性导入数据:
- 前往 Cloud Shell 终端,确保已设置活跃的 Google Cloud 项目,并确保您已获得授权。使用以下 gsutil 命令在项目中创建存储分区。将以下命令中的 <PROJECT_ID> 变量替换为您的 Google Cloud 项目 ID:
gsutil mb -l us gs://<PROJECT_ID>-yoga-poses-bucket
- 现在,存储分区已创建完毕,我们需要将准备好的数据库导出内容复制到此存储分区中,然后才能将其导入到 Firebase 数据库中。使用以下命令:
gsutil cp -r gs://demo-bq-gemini-public/yoga_poses gs://<PROJECT_ID>-yoga-poses-bucket
现在,我们已经准备好要导入的数据,接下来可以进入最后一步,将数据导入到我们创建的 Firebase 数据库(默认)中。
- 立即前往 Firestore 控制台,然后点击左侧导航菜单中的导入/导出。
选择“导入”,然后选择您刚刚创建的 Cloud Storage 路径,并导航到可以选择“yoga_poses.overall_export_metadata”文件的位置:

- 点击“导入”。
导入过程需要几秒钟,完成后,您可以访问 https://console.cloud.google.com/firestore/databases 来验证您的 Firestore 数据库和集合,选择 default 数据库和 poses 集合,如下所示:
- 另一种方法是,在部署后,您还可以通过应用使用“创建新姿势”操作手动创建记录。
6. Vector Search
启用 Firestore 向量搜索扩展程序
使用此扩展程序,通过新的向量搜索功能自动嵌入和查询 Firestore 文档!系统会将您转到 Firebase Extensions Hub。
安装 Vector Search 扩展程序时,您需要指定集合和文档字段名称。添加或更新包含此字段的文档会触发此扩展程序,从而计算文档的向量嵌入。此向量嵌入会写回同一文档,并且该文档会在向量存储中编入索引,以便进行查询。
下面我们来看看具体步骤:
安装扩展程序:
点击“在 Firebase 控制台中安装”,从 Firebase Extensions Marketplace 安装“Vector Search with Firestore”扩展程序。
重要提示:
首次前往此扩展程序页面时,您需要选择与 Firebase 控制台中列出的 Google Cloud 控制台中的项目相同的项目。

如果您的项目未列出,请继续在 Firebase 中添加该项目(从列表中选择现有的 Google Cloud 项目)。
配置扩展程序:
指定集合(“姿势”)、包含要嵌入的文本的字段(“姿势”)以及其他参数(例如嵌入维度)。
如果此步骤中列出了需要启用的 API,配置页面将允许您启用这些 API,请按照相应步骤操作。
如果您在启用 API 后,页面长时间没有响应,只需刷新页面,您应该就能看到已启用的 API。

在后续步骤中,您可以选择使用自己喜欢的 LLM 来生成嵌入。选择“Vertex AI”。

接下来的几项设置与您的集合以及要嵌入的字段有关:
LLM:Vertex AI
集合路径:姿势
默认查询限制:3
距离衡量指标:余弦
输入字段名称:姿势
输出字段名称:嵌入
状态字段名称:status
嵌入现有文档:是
更新现有嵌入:是
Cloud Functions 位置:us-central1
“Enable Events”(启用事件):未选中

完成所有这些设置后,点击“安装扩展程序”按钮。此过程将需要 3-5 分钟时间。
生成嵌入:
当您在“姿势”集合中添加或更新文档时,扩展程序将通过 API 端点使用预训练模型或您选择的模型自动生成嵌入。在本例中,我们在扩展程序配置中选择了 Vertex AI。
索引创建
它将强制要求在应用中使用嵌入时,在嵌入字段上创建索引。
Firestore 会自动为基本查询创建索引;不过,您也可以通过运行没有索引的查询,让 Firestore 生成索引语法,然后它会在应用端的错误消息中为您提供指向所生成索引的链接。以下是创建向量索引的步骤列表:
- 前往 Cloud Shell 终端
- 运行以下命令:
gcloud firestore indexes composite create --collection-group="poses" --query-scope=COLLECTION --database="(default)" --field-config vector-config='{"dimension":"768", "flat": "{}"}',field-path="embedding"
如需了解详情,请点击此处。
创建向量索引后,您就可以使用向量嵌入执行最近邻搜索了。
重要提示:
从现在开始,您无需对来源进行任何更改。只需跟着操作,即可了解应用在做什么。
执行向量搜索
我们来看看新建的应用如何使用 Vector Search。存储嵌入后,您可以使用 Firestore Java SDK 的 VectorQuery 类执行向量搜索并获取最近邻结果:
CollectionReference coll = firestore.collection("poses");
VectorQuery vectorQuery = coll.findNearest(
"embedding",
userSearchTextEmbedding,
/* limit */ 3,
VectorQuery.DistanceMeasure.EUCLIDEAN,
VectorQueryOptions.newBuilder().setDistanceResultField("vector_distance")
.setDistanceThreshold(2.0)
.build());
ApiFuture<VectorQuerySnapshot> future = vectorQuery.get();
VectorQuerySnapshot vectorQuerySnapshot = future.get();
List<Pose> posesList = new ArrayList<Pose>();
// Get the ID of the closest document (assuming results are sorted by distance)
String closestDocumentId = vectorQuerySnapshot.getDocuments().get(0).getId();
此代码段会将用户搜索文本的嵌入与 Firestore 中文档的嵌入进行比较,并提取在上下文中最为接近的嵌入。
7. Gemini 2.0 Flash
集成 Gemini 2.0 Flash(用于生成说明)
我们来看看您新建的应用如何处理 Gemini 2.0 Flash 集成以生成说明。
现在,假设某位管理员用户 / 瑜伽教练想要在 Gemini 2.0 Flash 的帮助下输入姿势的详细信息,然后执行搜索以查看最接近的匹配项。这样一来,系统会提取匹配姿势的详细信息以及支持结果的多模态对象。
String apiUrl = "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-exp:generateContent?key=[YOUR_API_KEY]";
Map<String, Object> requestBody = new HashMap<>();
List<Map<String, Object>> contents = new ArrayList<>();
List<Map<String, Object>> tools = new ArrayList<>();
Map<String, Object> content = new HashMap<>();
List<Map<String, Object>> parts = new ArrayList<>();
Map<String, Object> part = new HashMap<>();
part.put("text", prompt);
parts.add(part);
content.put("parts", parts);
contents.add(content);
requestBody.put("contents", contents);
/**Setting up Grounding*/
Map<String, Object> googleSearchTool = new HashMap<>();
googleSearchTool.put("googleSearch", new HashMap<>());
tools.add(googleSearchTool);
requestBody.put("tools", tools);
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
HttpEntity<Map<String, Object>> requestEntity = new HttpEntity<>(requestBody, headers);
ResponseEntity<String> response = restTemplate.exchange(apiUrl, HttpMethod.POST, requestEntity, String.class);
System.out.println("Generated response: " + response);
String responseBody = response.getBody();
JSONObject jsonObject = new JSONObject(responseBody);
JSONArray candidates = jsonObject.getJSONArray("candidates");
JSONObject candidate = candidates.getJSONObject(0);
JSONObject contentResponse = candidate.getJSONObject("content");
JSONArray partsResponse = contentResponse.getJSONArray("parts");
JSONObject partResponse = partsResponse.getJSONObject(0);
String generatedText = partResponse.getString("text");
System.out.println("Generated Text: " + generatedText);
a. 模仿图片和音频生成
Gemini 2.0 Flash Experimental 能够生成多模态结果,但我尚未注册其抢先体验计划,因此我分别使用 Imagen 和 TTS API 模拟了图片和音频输出。想象一下,只需通过一次 API 调用 Gemini 2.0 Flash 即可生成所有这些内容,是不是很棒!
try (PredictionServiceClient predictionServiceClient =
PredictionServiceClient.create(predictionServiceSettings)) {
final EndpointName endpointName =
EndpointName.ofProjectLocationPublisherModelName(
projectId, location, "google", "imagen-3.0-generate-001");
Map<String, Object> instancesMap = new HashMap<>();
instancesMap.put("prompt", prompt);
Value instances = mapToValue(instancesMap);
Map<String, Object> paramsMap = new HashMap<>();
paramsMap.put("sampleCount", 1);
paramsMap.put("aspectRatio", "1:1");
paramsMap.put("safetyFilterLevel", "block_few");
paramsMap.put("personGeneration", "allow_adult");
Value parameters = mapToValue(paramsMap);
PredictResponse predictResponse =
predictionServiceClient.predict(
endpointName, Collections.singletonList(instances), parameters);
for (Value prediction : predictResponse.getPredictionsList()) {
Map<String, Value> fieldsMap = prediction.getStructValue().getFieldsMap();
if (fieldsMap.containsKey("bytesBase64Encoded")) {
bytesBase64Encoded = fieldsMap.get("bytesBase64Encoded").getStringValue();
}
}
return bytesBase64Encoded;
}
try {
// Create a Text-to-Speech client
try (TextToSpeechClient textToSpeechClient = TextToSpeechClient.create()) {
// Set the text input to be synthesized
SynthesisInput input = SynthesisInput.newBuilder().setText(postureString).build();
// Build the voice request, select the language code ("en-US") and the ssml
// voice gender
// ("neutral")
VoiceSelectionParams voice =
VoiceSelectionParams.newBuilder()
.setLanguageCode("en-US")
.setSsmlGender(SsmlVoiceGender.NEUTRAL)
.build();
// Select the type of audio file you want returned
AudioConfig audioConfig =
AudioConfig.newBuilder().setAudioEncoding(AudioEncoding.MP3).build();
// Perform the text-to-speech request on the text input with the selected voice
// parameters and audio file type
SynthesizeSpeechResponse response =
textToSpeechClient.synthesizeSpeech(input, voice, audioConfig);
// Get the audio contents from the response
ByteString audioContents = response.getAudioContent();
// Convert to Base64 string
String base64Audio = Base64.getEncoder().encodeToString(audioContents.toByteArray());
// Add the Base64 encoded audio to the Pose object
return base64Audio;
}
} catch (Exception e) {
e.printStackTrace(); // Handle exceptions appropriately. For a real app, log and provide user feedback.
return "Error in Audio Generation";
}
}
b. 使用 Google 搜索建立依据:
如果您查看第 6 步中的 Gemini 调用代码,您会注意到以下代码段,该代码段用于为 LLM 回答启用 Google 搜索接地:
/**Setting up Grounding*/
Map<String, Object> googleSearchTool = new HashMap<>();
googleSearchTool.put("googleSearch", new HashMap<>());
tools.add(googleSearchTool);
requestBody.put("tools", tools);
这是为了确保我们:
- 让模型基于实际搜索结果
- 提取搜索中提及的相关网址
8. 运行应用
我们来通过一个简单的 Thymeleaf Web 界面看看新建的 Java Spring Boot 应用的所有功能:
- Firestore CRUD 操作(创建、读取、更新、删除)
- 关键字搜索
- 基于生成式 AI 的情境创建
- 内容相关搜索(向量搜索)
- 与搜索内容相关的多模态输出
- 运行您自己的查询(采用 structuredQuery 格式的查询)
示例:{"structuredQuery":{"select":{"fields":[{"fieldPath":"name"}]},"from":[{"collectionId":"fitness_poses"}]}}
到目前为止,我们讨论的所有功能都属于您刚刚从以下代码库创建的应用:https://github.com/AbiramiSukumaran/firestore-poserecommender
如需构建、运行和部署该应用,请在 Cloud Shell 终端中运行以下命令:
mvn package
mvn spring-boot:run
您应该会看到结果,并能够试用应用的功能。如需查看输出内容的演示,请观看以下视频:
使用 Firestore、Vector Search 和 Gemini 2.0 Flash 构建姿势推荐器
可选步骤:
如需将其部署到 Cloud Run(假设您已使用 Dockerfile 引导启动了一个全新的应用,并根据需要复制了文件),请从项目目录内的 Cloud Shell 终端运行以下命令:
gcloud run deploy --source .
提供应用名称、区域代码(选择 us-central1 对应的代码),然后根据提示选择未通过身份验证的调用“Y”。部署成功后,您应该会在终端中收到应用端点。
9. 清理
为避免系统因本博文中使用的资源向您的 Google Cloud 账号收取费用,请按照以下步骤操作:
- 在 Google Cloud 控制台中,前往管理资源页面。
- 在项目列表中,选择要删除的项目,然后点击删除。
- 在对话框中输入项目 ID,然后点击关停以删除项目。
10. 恭喜
恭喜!您已成功利用 Firestore 创建了一个强大而智能的瑜伽姿势管理应用。通过结合使用 Firestore、Vector Search 扩展程序和 Gemini 2.0 Flash 的功能(包括模拟图片和音频生成),我们创建了一款真正引人入胜且信息丰富的瑜伽应用,可用于实现 CRUD 操作、执行基于关键字的搜索、场景化向量搜索和生成多媒体内容。
此方法不仅限于瑜伽应用。随着 Gemini 等 AI 模型的不断发展,我们能够打造的沉浸式个性化用户体验只会越来越丰富。请务必及时了解 Google Cloud 和 Firebase 的最新动态和文档,以便充分发挥这些技术的潜力。
如果我要扩展此应用,我会尝试使用 Gemini 2.0 Flash 完成以下两项任务:
- 通过为用例创建实时视觉和音频流,使用 Multimodal Live API。
- 启用思考模式,让 Gemini 根据实时数据生成回答背后的想法,从而打造更逼真的体验。
欢迎尝试并发送拉取请求:>D!!!