Thiết lập monorepo
Nhiệm vụ 1: Tạo một monorepo
Phần tiêu đề “Nhiệm vụ 1: Tạo một monorepo”Để tạo một monorepo mới, từ thư mục mong muốn của bạn, chạy lệnh sau:
npx create-nx-workspace@22.0.2 dungeon-adventure --pm=pnpm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@22.0.2 dungeon-adventure --pm=yarn --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@22.0.2 dungeon-adventure --pm=npm --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsnpx create-nx-workspace@22.0.2 dungeon-adventure --pm=bun --preset=@aws/nx-plugin --iacProvider=CDK --ci=skip --aiAgentsĐiều này sẽ thiết lập một NX monorepo trong thư mục dungeon-adventure. Khi bạn mở thư mục trong VSCode, bạn sẽ thấy cấu trúc tệp này:
Thư mục.nx/
- …
Thư mục.vscode/
- …
Thư mụcnode_modules/
- …
Thư mụcpackages/ đây là nơi các dự án con của bạn sẽ nằm
- …
- .gitignore
- .npmrc
- .prettierignore
- .prettierrc
- nx.json cấu hình Nx CLI và các giá trị mặc định của monorepo
- package.json tất cả các phụ thuộc node được định nghĩa ở đây
- pnpm-lock.yaml hoặc bun.lock, yarn.lock, package-lock.json tùy thuộc vào trình quản lý gói
- pnpm-workspace.yaml nếu sử dụng pnpm
- README.md
- tsconfig.base.json tất cả các dự án con dựa trên node đều mở rộng từ tệp này
- tsconfig.json
- aws-nx-plugin.config.mts cấu hình cho Nx Plugin cho AWS
Bây giờ chúng ta có thể bắt đầu tạo các dự án con khác nhau bằng cách sử dụng @aws/nx-plugin.
Nhiệm vụ 2: Tạo một Game API
Phần tiêu đề “Nhiệm vụ 2: Tạo một Game API”Đầu tiên, hãy tạo Game API của chúng ta. Để làm điều này, tạo một tRPC API có tên GameApi bằng các bước sau:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#trpc-api - Fill in the required parameters
- name: GameApi
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactiveyarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactivenpx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactivebunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#trpc-api --name=GameApi --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện trong cây tệp của mình.
Các tệp được cập nhật bởi ts#trpc-api
Dưới đây là danh sách tất cả các tệp đã được tạo bởi trình tạo ts#trpc-api. Chúng ta sẽ xem xét một số tệp chính được đánh dấu trong cây tệp:
Thư mụcpackages/
Thư mụccommon/
Thư mụcconstructs/
Thư mụcsrc/
Thư mụcapp/ các cdk construct cụ thể cho ứng dụng
Thư mụcapis/
- game-api.ts cdk construct để tạo tRPC API của bạn
- index.ts
- …
- index.ts
Thư mụccore/ các cdk construct chung
Thư mụcapi/
- rest-api.ts cdk construct cơ sở cho API Gateway Rest API
- trpc-utils.ts tiện ích cho các cdk construct của trpc API
- utils.ts tiện ích cho các API construct
- index.ts
- runtime-config.ts
- index.ts
- project.json
- …
Thư mụctypes/ các kiểu dữ liệu được chia sẻ
Thư mụcsrc/
- index.ts
- runtime-config.ts định nghĩa interface được sử dụng bởi cả CDK và website
- project.json
- …
Thư mụcgame-api/ tRPC API
Thư mụcsrc/
Thư mụcclient/ vanilla client thường được sử dụng cho các cuộc gọi máy móc với máy móc bằng ts
- index.ts
- sigv4.ts
Thư mụcmiddleware/ công cụ đo lường powertools
- error.ts
- index.ts
- logger.ts
- metrics.ts
- tracer.ts
Thư mụcschema/ định nghĩa đầu vào và đầu ra cho API của bạn
- echo.ts
Thư mụcprocedures/ triển khai cụ thể cho các thủ tục/tuyến đường API của bạn
- echo.ts
- index.ts
- init.ts thiết lập ngữ cảnh và middleware
- local-server.ts được sử dụng khi chạy máy chủ tRPC cục bộ
- router.ts điểm vào cho trình xử lý lambda của bạn, định nghĩa tất cả các thủ tục
- project.json
- …
- eslint.config.mjs
- vitest.workspace.ts
Hãy xem các tệp chính này:
import { awsLambdaRequestHandler, CreateAWSLambdaContextOptions,} from '@trpc/server/adapters/aws-lambda';import { echo } from './procedures/echo.js';import { t } from './init.js';import { APIGatewayProxyEvent } from 'aws-lambda';
export const router = t.router;
export const appRouter = router({ echo,});
export const handler = awsLambdaRequestHandler({ router: appRouter, createContext: ( ctx: CreateAWSLambdaContextOptions<APIGatewayProxyEvent>, ) => ctx, responseMeta: () => ({ headers: { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Methods': '*', }, }),});
export type AppRouter = typeof appRouter;Router định nghĩa điểm vào cho tRPC API của bạn và là nơi bạn sẽ khai báo tất cả các phương thức API của mình. Như bạn có thể thấy ở trên, chúng ta có một phương thức gọi là echo với triển khai của nó trong tệp ./procedures/echo.ts.
import { publicProcedure } from '../init.js';import { EchoInputSchema, EchoOutputSchema,} from '../schema/echo.js';
export const echo = publicProcedure .input(EchoInputSchema) .output(EchoOutputSchema) .query((opts) => ({ result: opts.input.message }));Tệp này là triển khai của phương thức echo và như bạn có thể thấy được định kiểu mạnh bằng cách khai báo các cấu trúc dữ liệu đầu vào và đầu ra của nó.
import { z } from 'zod';
export const EchoInputSchema = z.object({ message: z.string(),});
export type IEchoInput = z.TypeOf<typeof EchoInputSchema>;
export const EchoOutputSchema = z.object({ result: z.string(),});
export type IEchoOutput = z.TypeOf<typeof EchoOutputSchema>;Tất cả các định nghĩa schema tRPC được định nghĩa bằng Zod và được xuất dưới dạng các kiểu typescript thông qua cú pháp z.TypeOf.
import { Construct } from 'constructs';import * as url from 'url';import { Code, Runtime, Function, FunctionProps, Tracing,} from 'aws-cdk-lib/aws-lambda';import { AuthorizationType, Cors, LambdaIntegration,} from 'aws-cdk-lib/aws-apigateway';import { Duration, Stack } from 'aws-cdk-lib';import { PolicyDocument, PolicyStatement, Effect, AccountPrincipal, AnyPrincipal,} from 'aws-cdk-lib/aws-iam';import { IntegrationBuilder, RestApiIntegration,} from '../../core/api/utils.js';import { RestApi } from '../../core/api/rest-api.js';import { Procedures, routerToOperations } from '../../core/api/trpc-utils.js';import { AppRouter, appRouter } from ':dungeon-adventure/game-api';
// Kiểu union chuỗi cho tất cả tên hoạt động APItype Operations = Procedures<AppRouter>;
/** * Thuộc tính để tạo một construct GameApi * * @template TIntegrations - Ánh xạ tên hoạt động với các tích hợp của chúng */export interface GameApiProps< TIntegrations extends Record<Operations, RestApiIntegration>,> { /** * Ánh xạ tên hoạt động với các tích hợp API Gateway của chúng */ integrations: TIntegrations;}
/** * Một CDK construct tạo và cấu hình AWS API Gateway REST API * dành riêng cho GameApi. * @template TIntegrations - Ánh xạ tên hoạt động với các tích hợp của chúng */export class GameApi< TIntegrations extends Record<Operations, RestApiIntegration>,> extends RestApi<Operations, TIntegrations> { /** * Tạo các tích hợp mặc định cho tất cả các hoạt động, triển khai mỗi hoạt động như * một hàm lambda riêng lẻ. * * @param scope - Phạm vi construct CDK * @returns Một IntegrationBuilder với các tích hợp lambda mặc định */ public static defaultIntegrations = (scope: Construct) => { return IntegrationBuilder.rest({ operations: routerToOperations(appRouter), defaultIntegrationOptions: { runtime: Runtime.NODEJS_LATEST, handler: 'index.handler', code: Code.fromAsset( url.fileURLToPath( new URL( '../../../../../../dist/packages/game-api/bundle', import.meta.url, ), ), ), timeout: Duration.seconds(30), tracing: Tracing.ACTIVE, environment: { AWS_CONNECTION_REUSE_ENABLED: '1', }, } satisfies FunctionProps, buildDefaultIntegration: (op, props: FunctionProps) => { const handler = new Function(scope, `GameApi${op}Handler`, props); return { handler, integration: new LambdaIntegration(handler) }; }, }); };
constructor( scope: Construct, id: string, props: GameApiProps<TIntegrations>, ) { super(scope, id, { apiName: 'GameApi', defaultMethodOptions: { authorizationType: AuthorizationType.IAM, }, defaultCorsPreflightOptions: { allowOrigins: Cors.ALL_ORIGINS, allowMethods: Cors.ALL_METHODS, }, policy: new PolicyDocument({ statements: [ // Ở đây chúng ta cấp bất kỳ thông tin xác thực AWS nào từ tài khoản mà dự án được triển khai để gọi api. // Truy cập chi tiết máy móc với máy móc có thể được định nghĩa ở đây bằng cách sử dụng các principal cụ thể hơn (ví dụ: vai trò hoặc // người dùng) và tài nguyên (ví dụ: đường dẫn api nào có thể được gọi bởi principal nào) nếu cần. new PolicyStatement({ effect: Effect.ALLOW, principals: [new AccountPrincipal(Stack.of(scope).account)], actions: ['execute-api:Invoke'], resources: ['execute-api:/*'], }), // Mở OPTIONS để cho phép trình duyệt thực hiện các yêu cầu preflight không xác thực new PolicyStatement({ effect: Effect.ALLOW, principals: [new AnyPrincipal()], actions: ['execute-api:Invoke'], resources: ['execute-api:/*/OPTIONS/*'], }), ], }), operations: routerToOperations(appRouter), ...props, }); }}Đây là CDK construct định nghĩa GameApi của chúng ta. Nó cung cấp một phương thức defaultIntegrations tự động tạo một hàm Lambda cho mỗi thủ tục trong tRPC API của chúng ta, trỏ đến triển khai API đã được đóng gói. Điều này có nghĩa là tại thời điểm cdk synth, việc đóng gói không xảy ra (trái ngược với việc sử dụng NodeJsFunction) vì chúng ta đã đóng gói nó như một phần của mục tiêu build của dự án backend.
Nhiệm vụ 3: Tạo các agent Story
Phần tiêu đề “Nhiệm vụ 3: Tạo các agent Story”Bây giờ hãy tạo các Story Agent của chúng ta.
Story agent: Dự án Python
Phần tiêu đề “Story agent: Dự án Python”Để tạo một dự án Python:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - py#project - Fill in the required parameters
- name: story
- Click
Generate
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactiveyarn nx g @aws/nx-plugin:py#project --name=story --no-interactivenpx nx g @aws/nx-plugin:py#project --name=story --no-interactivebunx nx g @aws/nx-plugin:py#project --name=story --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#project --name=story --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện trong cây tệp của mình.
Các tệp được cập nhật bởi py#project
py#project tạo các tệp sau:
Thư mục.venv/ môi trường ảo duy nhất cho monorepo
- …
Thư mụcpackages/
Thư mụcstory/
Thư mụcdungeon_adventure_story/ module python
- hello.py tệp python ví dụ (chúng ta sẽ bỏ qua điều này)
Thư mụctests/
- …
- .python-version
- pyproject.toml
- project.json
- .python-version phiên bản python uv được cố định
- pyproject.toml
- uv.lock
Điều này đã cấu hình một dự án Python và UV Workspace với môi trường ảo được chia sẻ.
Story agent: Strands Agent
Phần tiêu đề “Story agent: Strands Agent”Để thêm một Strands agent vào dự án với trình tạo py#strands-agent:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - py#strands-agent - Fill in the required parameters
- project: story
- Click
Generate
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactiveyarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactivenpx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactivebunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runyarn nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runnpx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runbunx nx g @aws/nx-plugin:py#strands-agent --project=story --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện trong cây tệp của mình.
Các tệp được cập nhật bởi py#strands-agent
Trình tạo py#strands-agent tạo các tệp sau:
Thư mụcpackages/
Thư mụcstory/
Thư mụcdungeon_adventure_story/ module python
Thư mụcagent/
- main.py điểm vào cho agent của bạn trong Bedrock AgentCore Runtime
- agent.py định nghĩa một agent và công cụ ví dụ
- agentcore_mcp_client.py tiện ích để tạo client để tương tác với các máy chủ MCP
- Dockerfile định nghĩa docker image để triển khai lên AgentCore Runtime
Thư mụccommon/constructs/
Thư mụcsrc
Thư mụccore/agent-core/
- runtime.ts construct chung để triển khai lên AgentCore Runtime
Thư mụcapp/agents/story-agent/
- story-agent.ts construct để triển khai Story agent của bạn lên AgentCore Runtime
Hãy xem xét một số tệp chi tiết:
from contextlib import contextmanager
from strands import Agent, toolfrom strands_tools import current_time
# Định nghĩa một công cụ tùy chỉnh@tooldef add(a: int, b: int) -> int: return a + b
@contextmanagerdef get_agent(session_id: str): yield Agent( system_prompt="""You are an addition wizard.Use the 'add' tool for addition tasks.Refer to tools as your 'spellbook'.""", tools=[add, current_time], )Điều này tạo một Strands agent ví dụ và định nghĩa một công cụ cộng.
from bedrock_agentcore.runtime import BedrockAgentCoreApp
from .agent import get_agent
app = BedrockAgentCoreApp()
@app.entrypointasync def invoke(payload, context): """Handler cho việc gọi agent""" prompt = payload.get( "prompt", "No prompt found in input, please guide the user " "to create a json payload with prompt key" )
with get_agent(session_id=context.session_id) as agent: stream = agent.stream_async(prompt) async for event in stream: print(event) yield (event)
if __name__ == "__main__": app.run()Đây là điểm vào cho agent, được cấu hình bằng Amazon Bedrock AgentCore SDK. Nó sử dụng hỗ trợ streaming của Strands và truyền các sự kiện trở lại client khi chúng xảy ra.
import { Lazy, Names } from 'aws-cdk-lib';import { DockerImageAsset, Platform } from 'aws-cdk-lib/aws-ecr-assets';import { Construct } from 'constructs';import { execSync } from 'child_process';import * as path from 'path';import * as url from 'url';import { AgentCoreRuntime, AgentCoreRuntimeProps,} from '../../../core/agent-core/runtime.js';
export type StoryAgentProps = Omit< AgentCoreRuntimeProps, 'runtimeName' | 'serverProtocol' | 'containerUri'>;
export class StoryAgent extends Construct { public readonly dockerImage: DockerImageAsset; public readonly agentCoreRuntime: AgentCoreRuntime;
constructor(scope: Construct, id: string, props?: StoryAgentProps) { super(scope, id);
this.dockerImage = new DockerImageAsset(this, 'DockerImage', { platform: Platform.LINUX_ARM64, directory: path.dirname(url.fileURLToPath(new URL(import.meta.url))), extraHash: execSync( `docker inspect dungeon-adventure-story-agent:latest --format '{{.Id}}'`, { encoding: 'utf-8' }, ).trim(), });
this.agentCoreRuntime = new AgentCoreRuntime(this, 'StoryAgent', { runtimeName: Lazy.string({ produce: () => Names.uniqueResourceName(this.agentCoreRuntime, { maxLength: 40 }), }), serverProtocol: 'HTTP', containerUri: this.dockerImage.imageUri, ...props, }); }}Điều này cấu hình một CDK DockerImageAsset tải image Docker agent của bạn lên ECR và lưu trữ nó bằng AgentCore Runtime.
Bạn có thể nhận thấy một Dockerfile bổ sung, tham chiếu đến Docker image từ dự án story, cho phép chúng ta đặt cùng vị trí Dockerfile và mã nguồn agent.
Nhiệm vụ 4: Thiết lập công cụ inventory
Phần tiêu đề “Nhiệm vụ 4: Thiết lập công cụ inventory”Inventory: Dự án TypeScript
Phần tiêu đề “Inventory: Dự án TypeScript”Hãy tạo một máy chủ MCP để cung cấp công cụ cho Story Agent của chúng ta để quản lý kho đồ của người chơi.
Đầu tiên, chúng ta tạo một dự án TypeScript:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#project - Fill in the required parameters
- name: inventory
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#project --name=inventory --no-interactive --dry-runĐiều này sẽ tạo một dự án TypeScript trống.
Các tệp được cập nhật bởi ts#project
Trình tạo ts#project tạo các tệp này.
Thư mụcpackages/
Thư mụcinventory/
Thư mụcsrc/
- index.ts điểm vào với hàm ví dụ
- project.json cấu hình dự án
- eslint.config.mjs cấu hình lint
- vite.config.ts cấu hình test
- tsconfig.json cấu hình typescript cơ sở cho dự án
- tsconfig.lib.json cấu hình typescript cho dự án được nhắm mục tiêu để biên dịch và đóng gói
- tsconfig.spec.json cấu hình typescript cho các bài test
- tsconfig.base.json được cập nhật để cấu hình một bí danh cho các dự án khác tham chiếu đến dự án này
Inventory: Máy chủ MCP
Phần tiêu đề “Inventory: Máy chủ MCP”Tiếp theo, chúng ta sẽ thêm một máy chủ MCP vào dự án TypeScript của mình:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#mcp-server - Fill in the required parameters
- project: inventory
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivenpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactivebunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#mcp-server --project=inventory --no-interactive --dry-runĐiều này sẽ thêm một máy chủ MCP.
Các tệp được cập nhật bởi ts#mcp-server
Trình tạo ts#mcp-server tạo các tệp này.
Thư mụcpackages/
Thư mụcinventory/
Thư mụcsrc/mcp-server/
- server.ts tạo máy chủ MCP
Thư mụctools/
- add.ts công cụ ví dụ
Thư mụcresources/
- sample-guidance.ts tài nguyên ví dụ
- stdio.ts điểm vào cho MCP với STDIO transport
- http.ts điểm vào cho MCP với Streamable HTTP transport
- Dockerfile xây dựng image cho AgentCore Runtime
- rolldown.config.ts cấu hình để đóng gói máy chủ MCP để triển khai lên AgentCore
Thư mụccommon/constructs/
Thư mụcsrc
Thư mụcapp/mcp-servers/inventory-mcp-server/
- inventory-mcp-server.ts construct để triển khai máy chủ MCP inventory của bạn lên AgentCore Runtime
Nhiệm vụ 5: Tạo Giao diện Người dùng (UI)
Phần tiêu đề “Nhiệm vụ 5: Tạo Giao diện Người dùng (UI)”Trong nhiệm vụ này, chúng ta sẽ tạo UI cho phép bạn tương tác với trò chơi.
Game UI: Website
Phần tiêu đề “Game UI: Website”Để tạo UI, tạo một website có tên GameUI bằng các bước sau:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#react-website - Fill in the required parameters
- name: GameUI
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactivenpx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactivebunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website --name=GameUI --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện trong cây tệp của mình.
Các tệp được cập nhật bởi ts#react-website
ts#react-website tạo các tệp này. Hãy xem xét một số tệp chính được đánh dấu trong cây tệp:
Thư mụcpackages/
Thư mụccommon/
Thư mụcconstructs/
Thư mụcsrc/
Thư mụcapp/ các cdk construct cụ thể cho ứng dụng
Thư mụcstatic-websites/
- game-ui.ts cdk construct để tạo Game UI của bạn
Thư mụccore/
- static-website.ts construct website tĩnh chung
Thư mụcgame-ui/
Thư mụcpublic/
- …
Thư mụcsrc/
Thư mụccomponents/
Thư mụcAppLayout/
- index.ts bố cục trang tổng thể: header, footer, sidebar, v.v.
- navitems.ts các mục điều hướng sidebar
Thư mụchooks/
- useAppLayout.tsx cho phép bạn đặt động các thứ như thông báo, kiểu trang, v.v.
Thư mụcroutes/ các tuyến đường dựa trên tệp của @tanstack/react-router
- index.tsx trang gốc ’/’ chuyển hướng đến ‘/welcome’
- __root.tsx tất cả các trang sử dụng component này làm cơ sở
Thư mụcwelcome/
- index.tsx
- config.ts
- main.tsx điểm vào React
- routeTree.gen.ts tệp này được cập nhật tự động bởi @tanstack/react-router
- styles.css
- index.html
- project.json
- vite.config.ts
- …
import * as url from 'url';import { Construct } from 'constructs';import { StaticWebsite } from '../../core/index.js';
export class GameUI extends StaticWebsite { constructor(scope: Construct, id: string) { super(scope, id, { websiteName: 'GameUI', websiteFilePath: url.fileURLToPath( new URL( '../../../../../../dist/packages/game-ui/bundle', import.meta.url, ), ), }); }}Đây là CDK construct định nghĩa GameUI của chúng ta. Nó đã cấu hình đường dẫn tệp đến gói được tạo cho UI dựa trên Vite của chúng ta. Điều này có nghĩa là tại thời điểm build, việc đóng gói xảy ra trong mục tiêu build của dự án game-ui và đầu ra được sử dụng ở đây.
import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';
import '@cloudscape-design/global-styles/index.css';
const router = createRouter({ routeTree });
// Đăng ký instance router để đảm bảo kiểu an toàndeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}
const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RouterProvider router={router} /> </I18nProvider> </React.StrictMode>, );Đây là điểm vào nơi React được gắn kết. Như được hiển thị, ban đầu nó chỉ cấu hình một @tanstack/react-router trong cấu hình file-based-routing. Miễn là máy chủ phát triển của bạn đang chạy, bạn có thể tạo các tệp trong thư mục routes và @tanstack/react-router sẽ tạo thiết lập tệp boilerplate cho bạn, cùng với việc cập nhật tệp routeTree.gen.ts. Tệp này duy trì tất cả các tuyến đường một cách an toàn về kiểu, có nghĩa là khi bạn sử dụng <Link>, tùy chọn to sẽ chỉ hiển thị các tuyến đường hợp lệ.
Để biết thêm thông tin, hãy tham khảo tài liệu @tanstack/react-router.
import { ContentLayout, Header, SpaceBetween, Container,} from '@cloudscape-design/components';import { createFileRoute } from '@tanstack/react-router';
export const Route = createFileRoute('/welcome/')({ component: RouteComponent,});
function RouteComponent() { return ( <ContentLayout header={<Header>Welcome</Header>}> <SpaceBetween size="l"> <Container>Welcome to your new Cloudscape website!</Container> </SpaceBetween> </ContentLayout> );}Một component sẽ được render khi điều hướng đến tuyến đường /welcome. @tanstack/react-router sẽ quản lý Route cho bạn bất cứ khi nào bạn tạo/di chuyển tệp này (miễn là máy chủ dev đang chạy).
Game UI: Auth
Phần tiêu đề “Game UI: Auth”Hãy cấu hình Game UI của chúng ta để yêu cầu truy cập được xác thực qua Amazon Cognito bằng các bước sau:
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#react-website#auth - Fill in the required parameters
- cognitoDomain: game-ui
- project: @dungeon-adventure/game-ui
- allowSignup: true
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveyarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivenpx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactivebunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#react-website#auth --cognitoDomain=game-ui --project=@dungeon-adventure/game-ui --allowSignup=true --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện/thay đổi trong cây tệp của mình.
Các tệp được cập nhật bởi ts#react-website#auth
Trình tạo ts#react-website#auth cập nhật/tạo các tệp này. Hãy xem xét một số tệp chính được đánh dấu trong cây tệp:
Thư mụcpackages/
Thư mụccommon/
Thư mụcconstructs/
Thư mụcsrc/
Thư mụccore/
- user-identity.ts cdk construct để tạo user/identity pools
Thư mụctypes/
Thư mụcsrc/
- runtime-config.ts được cập nhật để thêm cognitoProps
Thư mụcgame-ui/
Thư mụcsrc/
Thư mụccomponents/
Thư mụcAppLayout/
- index.tsx thêm người dùng đã đăng nhập/đăng xuất vào header
Thư mụcCognitoAuth/
- index.ts quản lý đăng nhập vào Cognito
Thư mụcRuntimeConfig/
- index.tsx lấy
runtime-config.jsonvà cung cấp cho các children qua context
- index.tsx lấy
Thư mụchooks/
- useRuntimeConfig.tsx
- main.tsx Được cập nhật để thêm Cognito
import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Đăng ký instance router để đảm bảo kiểu an toàndeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <RouterProvider router={router} /> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );Các component RuntimeConfigProvider và CognitoAuth đã được thêm vào tệp main.tsx thông qua một biến đổi AST. Điều này cho phép component CognitoAuth xác thực với Amazon Cognito bằng cách lấy runtime-config.json chứa cấu hình kết nối cognito cần thiết để thực hiện các cuộc gọi backend đến đích chính xác.
Game UI: Kết nối với Game API
Phần tiêu đề “Game UI: Kết nối với Game API”Hãy cấu hình Game UI của chúng ta để kết nối với Game API đã tạo trước đó.
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - api-connection - Fill in the required parameters
- sourceProject: @dungeon-adventure/game-ui
- targetProject: @dungeon-adventure/game-api
- Click
Generate
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveyarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivenpx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactivebunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runyarn nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runnpx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runbunx nx g @aws/nx-plugin:api-connection --sourceProject=@dungeon-adventure/game-ui --targetProject=@dungeon-adventure/game-api --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện/thay đổi trong cây tệp của mình.
Các tệp được cập nhật bởi api-connection từ UI -> tRPC
Trình tạo api-connection tạo/cập nhật các tệp này. Hãy xem xét một số tệp chính được đánh dấu trong cây tệp:
Thư mụcpackages/
Thư mụcgame-ui/
Thư mụcsrc/
Thư mụccomponents/
- GameApiClientProvider.tsx thiết lập client GameAPI
Thư mụchooks/
- useGameApi.tsx hooks để gọi GameApi
- main.tsx chèn các provider client trpc
- package.json
import { GameApiTRCPContext } from '../components/GameApiClientProvider';
export const useGameApi = GameApiTRCPContext.useTRPC;Hook này sử dụng tích hợp React Query mới nhất của tRPC cho phép người dùng tương tác trực tiếp với @tanstack/react-query mà không có bất kỳ lớp trừu tượng bổ sung nào. Để xem ví dụ về cách gọi tRPC API, hãy tham khảo hướng dẫn sử dụng tRPC hook.
import GameApiClientProvider from './components/GameApiClientProvider';import QueryClientProvider from './components/QueryClientProvider';import CognitoAuth from './components/CognitoAuth';import RuntimeConfigProvider from './components/RuntimeConfig';import React from 'react';import { createRoot } from 'react-dom/client';import { I18nProvider } from '@cloudscape-design/components/i18n';import messages from '@cloudscape-design/components/i18n/messages/all.en';import { RouterProvider, createRouter } from '@tanstack/react-router';import { routeTree } from './routeTree.gen';import '@cloudscape-design/global-styles/index.css';const router = createRouter({ routeTree });// Đăng ký instance router để đảm bảo kiểu an toàndeclare module '@tanstack/react-router' { interface Register { router: typeof router; }}const root = document.getElementById('root');root && createRoot(root).render( <React.StrictMode> <I18nProvider locale="en" messages={[messages]}> <RuntimeConfigProvider> <CognitoAuth> <QueryClientProvider> <GameApiClientProvider> <RouterProvider router={router} /> </GameApiClientProvider> </QueryClientProvider> </CognitoAuth> </RuntimeConfigProvider> </I18nProvider> </React.StrictMode>, );Tệp main.tsx đã được cập nhật thông qua một biến đổi AST để chèn các provider tRPC.
Game UI: Cơ sở hạ tầng
Phần tiêu đề “Game UI: Cơ sở hạ tầng”Hãy tạo dự án con cuối cùng cho cơ sở hạ tầng CDK.
- Install the Nx Console VSCode Plugin if you haven't already
- Open the Nx Console in VSCode
- Click
Generate (UI)in the "Common Nx Commands" section - Search for
@aws/nx-plugin - ts#infra - Fill in the required parameters
- name: infra
- Click
Generate
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivenpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactivebunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactiveYou can also perform a dry-run to see what files would be changed
pnpm nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runyarn nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runnpx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runbunx nx g @aws/nx-plugin:ts#infra --name=infra --no-interactive --dry-runBạn sẽ thấy một số tệp mới xuất hiện/thay đổi trong cây tệp của mình.
Các tệp được cập nhật bởi ts#infra
Trình tạo ts#infra tạo/cập nhật các tệp này. Hãy xem xét một số tệp chính được đánh dấu trong cây tệp:
Thư mụcpackages/
Thư mụccommon/
Thư mụcconstructs/
Thư mụcsrc/
Thư mụccore/
- checkov.ts
- index.ts
Thư mụcinfra
Thư mụcsrc/
Thư mụcstages/
- application-stage.ts các cdk stack được định nghĩa ở đây
Thư mụcstacks/
- application-stack.ts các tài nguyên cdk được định nghĩa ở đây
- index.ts
- main.ts điểm vào định nghĩa tất cả các stage
- cdk.json
- project.json
- …
- package.json
- tsconfig.json thêm references
- tsconfig.base.json thêm alias
import { ApplicationStage } from './stacks/application-stage.js';import { App } from ':dungeon-adventure/common-constructs';
const app = new App();
// Sử dụng cái này để triển khai môi trường sandbox của riêng bạn (giả định thông tin xác thực CLI của bạn)new ApplicationStage(app, 'dungeon-adventure-infra-sandbox', { env: { account: process.env.CDK_DEFAULT_ACCOUNT, region: process.env.CDK_DEFAULT_REGION, },});
app.synth();Đây là điểm vào cho ứng dụng CDK của bạn.
import * as cdk from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props);
// Mã định nghĩa stack của bạn ở đây }}Hãy khởi tạo các CDK construct của chúng ta để xây dựng trò chơi phiêu lưu hầm ngục.
Nhiệm vụ 6: Cập nhật cơ sở hạ tầng của chúng ta
Phần tiêu đề “Nhiệm vụ 6: Cập nhật cơ sở hạ tầng của chúng ta”Hãy cập nhật packages/infra/src/stacks/application-stack.ts để khởi tạo một số construct đã tạo của chúng ta:
import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}import { Stack, StackProps } from 'aws-cdk-lib';import { GameApi, GameUI, InventoryMcpServer, RuntimeConfig, StoryAgent, UserIdentity,} from ':dungeon-adventure/common-constructs';import { Stack, StackProps, CfnOutput } from 'aws-cdk-lib';import { Construct } from 'constructs';
export class ApplicationStack extends Stack { constructor(scope: Construct, id: string, props?: StackProps) { super(scope, id, props);
// The code that defines your stack goes here const userIdentity = new UserIdentity(this, 'UserIdentity');
const gameApi = new GameApi(this, 'GameApi', { integrations: GameApi.defaultIntegrations(this).build(), });
const { userPool, userPoolClient } = userIdentity;
const mcpServer = new InventoryMcpServer(this, 'InventoryMcpServer');
// Use Cognito for user authentication with the agent const storyAgent = new StoryAgent(this, 'StoryAgent', { authorizerConfiguration: { customJwtAuthorizer: { discoveryUrl: `https://cognito-idp.${Stack.of(userPool).region}.amazonaws.com/${userPool.userPoolId}/.well-known/openid-configuration`, allowedAudience: [userPoolClient.userPoolClientId], }, }, environment: { INVENTORY_MCP_ARN: mcpServer.agentCoreRuntime.arn, }, }); // Add the Story Agent ARN to runtime-config.json so it can be referenced by the website RuntimeConfig.ensure(this).config.agentArn = storyAgent.agentCoreRuntime.arn;
new CfnOutput(this, 'StoryAgentArn', { value: storyAgent.agentCoreRuntime.arn, }); new CfnOutput(this, 'InventoryMcpArn', { value: mcpServer.agentCoreRuntime.arn, });
// Grant the agent permissions to invoke our mcp server mcpServer.agentCoreRuntime.grantInvoke(storyAgent.agentCoreRuntime);
// Grant the authenticated role access to invoke the api gameApi.grantInvokeAccess(userIdentity.identityPool.authenticatedRole);
// Ensure this is instantiated last so our runtime-config.json can be automatically configured new GameUI(this, 'GameUI'); }}Nhiệm vụ 7: Build mã
Phần tiêu đề “Nhiệm vụ 7: Build mã”Các lệnh Nx
Mục tiêu đơn lẻ vs nhiều mục tiêu
Phần tiêu đề “Mục tiêu đơn lẻ vs nhiều mục tiêu”Lệnh run-many sẽ chạy một mục tiêu trên nhiều dự án con được liệt kê (--all sẽ nhắm mục tiêu tất cả). Điều này đảm bảo các phụ thuộc được thực thi theo đúng thứ tự.
Bạn cũng có thể kích hoạt một build (hoặc bất kỳ tác vụ nào khác) cho một mục tiêu dự án đơn lẻ bằng cách chạy mục tiêu trực tiếp trên dự án. Ví dụ: để build dự án @dungeon-adventure/infra, chạy lệnh sau:
pnpm nx run @dungeon-adventure/infra:buildyarn nx run @dungeon-adventure/infra:buildnpx nx run @dungeon-adventure/infra:buildbunx nx run @dungeon-adventure/infra:buildBạn cũng có thể bỏ qua scope và sử dụng cú pháp viết tắt của Nx nếu muốn:
pnpm nx build infrayarn nx build infranpx nx build infrabunx nx build infraTrực quan hóa các phụ thuộc của bạn
Phần tiêu đề “Trực quan hóa các phụ thuộc của bạn”Để trực quan hóa các phụ thuộc của bạn, chạy:
pnpm nx graphyarn nx graphnpx nx graphbunx nx graph
Caching
Phần tiêu đề “Caching”Nx dựa vào caching để bạn có thể tái sử dụng các artifact từ các build trước đó nhằm tăng tốc độ phát triển. Có một số cấu hình cần thiết để điều này hoạt động chính xác và có thể có những trường hợp bạn muốn thực hiện build mà không sử dụng cache. Để làm điều đó, chỉ cần thêm đối số --skip-nx-cache vào lệnh của bạn. Ví dụ:
pnpm nx run @dungeon-adventure/infra:build --skip-nx-cacheyarn nx run @dungeon-adventure/infra:build --skip-nx-cachenpx nx run @dungeon-adventure/infra:build --skip-nx-cachebunx nx run @dungeon-adventure/infra:build --skip-nx-cacheNếu vì bất kỳ lý do gì bạn muốn xóa cache của mình (được lưu trữ trong thư mục .nx), bạn có thể chạy lệnh sau:
pnpm nx resetyarn nx resetnpx nx resetbunx nx resetSử dụng dòng lệnh, chạy lệnh sau để sửa các lỗi lint trước:
pnpm nx run-many --target lint --configuration=fix --allyarn nx run-many --target lint --configuration=fix --allnpx nx run-many --target lint --configuration=fix --allbunx nx run-many --target lint --configuration=fix --allSau đó, chạy lệnh sau để thực hiện build đầy đủ:
pnpm nx run-many --target build --allyarn nx run-many --target build --allnpx nx run-many --target build --allbunx nx run-many --target build --allBạn sẽ được nhắc với thông báo sau:
NX The workspace is out of sync
[@nx/js:typescript-sync]: Some TypeScript configuration files are missing project references to the projects they depend on or contain outdated project references.
This will result in an error in CI.
? Would you like to sync the identified changes to get your workspace up to date? …Yes, sync the changes and run the tasksNo, run the tasks without syncing the changesThông báo này cho biết rằng NX đã phát hiện một số tệp có thể được cập nhật tự động cho bạn. Trong trường hợp này, nó đề cập đến các tệp tsconfig.json không có tham chiếu Typescript được thiết lập trên các dự án tham chiếu.
Chọn tùy chọn Yes, sync the changes and run the tasks để tiếp tục. Bạn sẽ nhận thấy tất cả các lỗi import liên quan đến IDE của bạn được tự động giải quyết vì trình tạo sync sẽ tự động thêm các tham chiếu typescript còn thiếu!
Tất cả các artifact đã build hiện có sẵn trong thư mục dist/ nằm ở gốc của monorepo. Đây là một thực hành tiêu chuẩn khi sử dụng các dự án được tạo bởi @aws/nx-plugin vì nó không làm ô nhiễm cây tệp của bạn với các tệp được tạo. Trong trường hợp bạn muốn dọn dẹp các tệp của mình, hãy xóa thư mục dist/ mà không phải lo lắng về các artifact build bị rải rác khắp cây tệp.
Chúc mừng! Bạn đã tạo tất cả các dự án con cần thiết để bắt đầu triển khai cốt lõi của trò chơi AI Dungeon Adventure của chúng ta. 🎉🎉🎉