diff --git a/integrations/minecraft/.env b/integrations/minecraft/.env index 963c684de..8201d5c04 100644 --- a/integrations/minecraft/.env +++ b/integrations/minecraft/.env @@ -29,6 +29,12 @@ BOT_PORT='' # BOT_AUTH='microsoft' BOT_VERSION='' +# AIRI desktop server channel +# Copy the Auth Token from AIRI desktop under Settings > Connection. +AIRI_WS_BASEURL='ws://localhost:6121/ws' +AIRI_WS_TOKEN='' +AIRI_CLIENT_NAME='minecraft-bot' + # ============================================================================== # Debug and Development Tools # ============================================================================== diff --git a/integrations/minecraft/README.md b/integrations/minecraft/README.md index c88bc948b..ece48269a 100644 --- a/integrations/minecraft/README.md +++ b/integrations/minecraft/README.md @@ -30,9 +30,21 @@ Treat this service as a local-development and trusted-server tool only. cp integrations/minecraft/.env integrations/minecraft/.env.local ``` -3. Edit `integrations/minecraft/.env.local`. +3. Open **Settings > Connection** in the AIRI desktop app. -4. Start the service: +4. Copy the **Auth Token**. + +5. Edit `integrations/minecraft/.env.local` and set the token: + + ```dotenv + AIRI_WS_TOKEN='paste-the-desktop-auth-token-here' + ``` + + The desktop app generates this token by default. Keep the token private. + + The default `AIRI_WS_BASEURL` is `ws://localhost:6121/ws`. Change itwhen the desktop server uses another address. + +6. Start the service: ```bash pnpm -F @proj-airi/minecraft-bot dev @@ -44,7 +56,7 @@ Treat this service as a local-development and trusted-server tool only. pnpm dev ``` -5. The bot should automatically connect to both AIRI and the Minecraft server. +7. The bot should automatically connect to both AIRI and the Minecraft server. ## Cognitive Architecture diff --git a/integrations/minecraft/package.json b/integrations/minecraft/package.json index 5b4eb858c..1e9a8c16f 100644 --- a/integrations/minecraft/package.json +++ b/integrations/minecraft/package.json @@ -16,6 +16,7 @@ "@modelcontextprotocol/sdk": "catalog:", "@moeru/std": "catalog:", "@proj-airi/server-sdk": "workspace:*", + "@proj-airi/server-shared": "workspace:*", "@xsai/generate-text": "catalog:", "@xsai/shared-chat": "catalog:", "alien-signals": "catalog:", diff --git a/integrations/minecraft/src/airi/start-background-client.ts b/integrations/minecraft/src/airi/start-background-client.ts index 6dccd39c7..d49c9d235 100644 --- a/integrations/minecraft/src/airi/start-background-client.ts +++ b/integrations/minecraft/src/airi/start-background-client.ts @@ -1,4 +1,5 @@ import { errorMessageFrom } from '@moeru/std' +import { parseServerErrorMessage } from '@proj-airi/server-shared' interface AiriClientLike { connect: () => Promise @@ -10,49 +11,75 @@ interface LoggerLike { withFields: (fields: Record) => LoggerLike } +type ReportedFailureKind = 'authentication-retrying' | 'authentication-terminal' | 'connection' + export function startAiriClientConnection(client: AiriClientLike, deps: { logger: LoggerLike url: string }) { - let unavailableReported = false + let connectionInterrupted = false + let reportedFailureKind: ReportedFailureKind | undefined - const reportUnavailable = (error: unknown) => { - if (unavailableReported) + const reportError = (error: unknown) => { + const errorMessage = errorMessageFrom(error) ?? 'Unknown error' + const serverError = parseServerErrorMessage(errorMessage) + let failureKind: ReportedFailureKind = 'connection' + if (serverError.authentication) { + failureKind = serverError.terminal ? 'authentication-terminal' : 'authentication-retrying' + } + + if (reportedFailureKind === failureKind) return - unavailableReported = true + connectionInterrupted = true + reportedFailureKind = failureKind + + if (serverError.authentication) { + deps.logger.withFields({ + url: deps.url, + error: errorMessage, + retrying: !serverError.terminal, + }).warn( + serverError.terminal + ? 'AIRI server authentication failed. Check AIRI_WS_TOKEN before restarting the service' + : 'AIRI server authentication failed. Set AIRI_WS_TOKEN to the desktop server Auth Token. The client will retry in background', + ) + return + } + deps.logger.withFields({ url: deps.url, - error: errorMessageFrom(error) ?? 'Unknown error', - }).warn('AIRI server is unavailable; continuing startup without AIRI and retrying in background') + error: errorMessage, + retrying: true, + }).warn('AIRI server is unavailable. The service will continue startup and retry in background') } const reportDisconnected = () => { + connectionInterrupted = true + reportedFailureKind = 'connection' deps.logger.withFields({ url: deps.url, - }).warn('AIRI server connection closed; retrying in background') + retrying: true, + }).warn('AIRI server connection closed. The client will retry in background') } - void client.connect() - .then(() => { - deps.logger.withFields({ - url: deps.url, - }).log( - unavailableReported - ? 'Connected to AIRI server after background retry' - : 'Connected to AIRI server', - ) - unavailableReported = false - }) - .catch((error) => { - deps.logger.withFields({ - url: deps.url, - error: errorMessageFrom(error) ?? 'Unknown error', - }).warn('AIRI client stopped retrying') - }) + const reportConnected = () => { + deps.logger.withFields({ + url: deps.url, + }).log( + connectionInterrupted + ? 'Connected to AIRI server after background retry' + : 'Connected to AIRI server', + ) + connectionInterrupted = false + reportedFailureKind = undefined + } + + void client.connect().catch(reportError) return { - reportUnavailable, + reportConnected, reportDisconnected, + reportError, } } diff --git a/integrations/minecraft/src/main.ts b/integrations/minecraft/src/main.ts index 468ea2819..6dcc39c27 100644 --- a/integrations/minecraft/src/main.ts +++ b/integrations/minecraft/src/main.ts @@ -86,8 +86,9 @@ async function main() { // the connection recovers immediately once the loop frees up. // Root cause (the periodic event-loop stall itself) is tracked separately. heartbeat: { readTimeout: 120_000, pingInterval: 20_000 }, - onError: error => airiConnectionLifecycle?.reportUnavailable(error), + onError: error => airiConnectionLifecycle?.reportError(error), onClose: () => airiConnectionLifecycle?.reportDisconnected(), + onReady: () => airiConnectionLifecycle?.reportConnected(), }) airiConnectionLifecycle = startAiriClientConnection(airiClient, { logger, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 220278de1..9383b8b8f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3100,6 +3100,9 @@ importers: '@proj-airi/server-sdk': specifier: workspace:* version: link:../../packages/server-sdk + '@proj-airi/server-shared': + specifier: workspace:* + version: link:../../packages/server-shared '@xsai/generate-text': specifier: 'catalog:' version: 0.5.0-beta.2(patch_hash=306bfb723913596b334140f0d6fa48063f336e3b44024efc1d72bf60d54b15e6)