From 0cd095b32354cc7aa5a4eba95aa60fde76c98477 Mon Sep 17 00:00:00 2001 From: LemonNeko Date: Tue, 7 Apr 2026 19:04:45 +0800 Subject: [PATCH] feat(stage-pocket): scan qr code to connect to tamagotchi --- .../android/app/capacitor.build.gradle | 1 + .../android/capacitor.settings.gradle | 3 + .../stage-pocket/android/variables.gradle.kts | 2 +- .../xcshareddata/swiftpm/Package.resolved | 11 +- apps/stage-pocket/ios/App/App/Info.plist | 2 + .../ios/App/CapApp-SPM/Package.resolved | 9 + .../ios/App/CapApp-SPM/Package.swift | 2 + apps/stage-pocket/package.json | 1 + apps/stage-pocket/src/App.vue | 2 +- .../src/modules/server-channel-qr-probe.ts | 39 ++++ .../src/pages/settings/connection/index.vue | 26 +++ .../connection/server-channel-qr-scanner.vue | 66 +++++++ apps/stage-pocket/vite.config.ts | 8 +- apps/stage-tamagotchi/package.json | 1 + .../services/airi/channel-server/index.ts | 52 +++++ .../pages/settings/connection/index.vue | 4 + .../connection/server-channel-qr-card.vue | 180 ++++++++++++++++++ apps/stage-tamagotchi/src/shared/eventa.ts | 2 + packages/i18n/src/locales/en/settings.yaml | 93 ++++++--- .../i18n/src/locales/zh-Hans/settings.yaml | 176 +++++++++-------- packages/stage-shared/package.json | 2 + .../stage-shared/src/server-channel-qr.ts | 44 +++++ .../stores/mods/api/channel-server.test.ts | 30 +++ .../src/stores/mods/api/channel-server.ts | 8 +- pnpm-lock.yaml | 35 ++++ pnpm-workspace.yaml | 2 + 26 files changed, 696 insertions(+), 105 deletions(-) create mode 100644 apps/stage-pocket/src/modules/server-channel-qr-probe.ts create mode 100644 apps/stage-pocket/src/pages/settings/connection/index.vue create mode 100644 apps/stage-pocket/src/pages/settings/connection/server-channel-qr-scanner.vue create mode 100644 apps/stage-tamagotchi/src/renderer/pages/settings/connection/server-channel-qr-card.vue create mode 100644 packages/stage-shared/src/server-channel-qr.ts diff --git a/apps/stage-pocket/android/app/capacitor.build.gradle b/apps/stage-pocket/android/app/capacitor.build.gradle index fff1ea578..bf00752f3 100644 --- a/apps/stage-pocket/android/app/capacitor.build.gradle +++ b/apps/stage-pocket/android/app/capacitor.build.gradle @@ -9,6 +9,7 @@ android { apply from: "../capacitor-cordova-android-plugins/cordova.variables.gradle" dependencies { + implementation project(':capacitor-barcode-scanner') implementation project(':capacitor-local-notifications') implementation project(':capacitor-native-settings') diff --git a/apps/stage-pocket/android/capacitor.settings.gradle b/apps/stage-pocket/android/capacitor.settings.gradle index 274f7227d..93bc355b8 100644 --- a/apps/stage-pocket/android/capacitor.settings.gradle +++ b/apps/stage-pocket/android/capacitor.settings.gradle @@ -2,6 +2,9 @@ include ':capacitor-android' project(':capacitor-android').projectDir = new File('../../../node_modules/.pnpm/@capacitor+android@8.2.0_@capacitor+core@8.2.0/node_modules/@capacitor/android/capacitor') +include ':capacitor-barcode-scanner' +project(':capacitor-barcode-scanner').projectDir = new File('../../../node_modules/.pnpm/@capacitor+barcode-scanner@3.0.2_@capacitor+core@8.2.0/node_modules/@capacitor/barcode-scanner/android') + include ':capacitor-local-notifications' project(':capacitor-local-notifications').projectDir = new File('../../../node_modules/.pnpm/@capacitor+local-notifications@8.0.2_@capacitor+core@8.2.0/node_modules/@capacitor/local-notifications/android') diff --git a/apps/stage-pocket/android/variables.gradle.kts b/apps/stage-pocket/android/variables.gradle.kts index 9ff301cb9..1a36fb69d 100644 --- a/apps/stage-pocket/android/variables.gradle.kts +++ b/apps/stage-pocket/android/variables.gradle.kts @@ -1,4 +1,4 @@ -val minSdkVersion by extra(24) +val minSdkVersion by extra(26) val compileSdkVersion by extra(36) val targetSdkVersion by extra(36) val androidxActivityVersion by extra("1.11.0") diff --git a/apps/stage-pocket/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved b/apps/stage-pocket/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved index b88a6510a..5b4bd2d48 100644 --- a/apps/stage-pocket/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved +++ b/apps/stage-pocket/ios/App/App.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved @@ -1,5 +1,5 @@ { - "originHash" : "ba02834b61084af7060acde9aa9e378f3dfb98b9ff948bb94ea14d8b846a36f1", + "originHash" : "8765687701ea10de7b4fa5981471aaf26157c825c91c99a7f0e25bbdf71f12e6", "pins" : [ { "identity" : "capacitor-swift-pm", @@ -9,6 +9,15 @@ "revision" : "0e862e6ff13852a710c8a484180ca4d6a2cc9761", "version" : "8.2.0" } + }, + { + "identity" : "osbarcodelib-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OutSystems/OSBarcodeLib-iOS.git", + "state" : { + "revision" : "1ae7a716331be720f9f1075ef033276014c341ec", + "version" : "2.1.1" + } } ], "version" : 3 diff --git a/apps/stage-pocket/ios/App/App/Info.plist b/apps/stage-pocket/ios/App/App/Info.plist index 9843f6f3e..ed4385713 100644 --- a/apps/stage-pocket/ios/App/App/Info.plist +++ b/apps/stage-pocket/ios/App/App/Info.plist @@ -26,6 +26,8 @@ NSLocalNetworkUsageDescription AIRI needs access to your local network to connect to your Tamagotchi host. + NSCameraUsageDescription + AIRI uses the camera to scan Tamagotchi connection QR codes. UILaunchStoryboardName LaunchScreen UIMainStoryboardFile diff --git a/apps/stage-pocket/ios/App/CapApp-SPM/Package.resolved b/apps/stage-pocket/ios/App/CapApp-SPM/Package.resolved index 08497ddc4..9ecef2e99 100644 --- a/apps/stage-pocket/ios/App/CapApp-SPM/Package.resolved +++ b/apps/stage-pocket/ios/App/CapApp-SPM/Package.resolved @@ -8,6 +8,15 @@ "revision" : "0e862e6ff13852a710c8a484180ca4d6a2cc9761", "version" : "8.2.0" } + }, + { + "identity" : "osbarcodelib-ios", + "kind" : "remoteSourceControl", + "location" : "https://github.com/OutSystems/OSBarcodeLib-iOS.git", + "state" : { + "revision" : "1ae7a716331be720f9f1075ef033276014c341ec", + "version" : "2.1.1" + } } ], "version" : 2 diff --git a/apps/stage-pocket/ios/App/CapApp-SPM/Package.swift b/apps/stage-pocket/ios/App/CapApp-SPM/Package.swift index 641a7a90c..9b6d2fc48 100644 --- a/apps/stage-pocket/ios/App/CapApp-SPM/Package.swift +++ b/apps/stage-pocket/ios/App/CapApp-SPM/Package.swift @@ -12,6 +12,7 @@ let package = Package( ], dependencies: [ .package(url: "https://github.com/ionic-team/capacitor-swift-pm.git", exact: "8.2.0"), + .package(name: "CapacitorBarcodeScanner", path: "../../../../../node_modules/.pnpm/@capacitor+barcode-scanner@3.0.2_@capacitor+core@8.2.0/node_modules/@capacitor/barcode-scanner"), .package(name: "CapacitorLocalNotifications", path: "../../../../../node_modules/.pnpm/@capacitor+local-notifications@8.0.2_@capacitor+core@8.2.0/node_modules/@capacitor/local-notifications"), .package(name: "CapacitorNativeSettings", path: "../../../../../node_modules/.pnpm/capacitor-native-settings@8.1.0_@capacitor+core@8.2.0/node_modules/capacitor-native-settings") ], @@ -21,6 +22,7 @@ let package = Package( dependencies: [ .product(name: "Capacitor", package: "capacitor-swift-pm"), .product(name: "Cordova", package: "capacitor-swift-pm"), + .product(name: "CapacitorBarcodeScanner", package: "CapacitorBarcodeScanner"), .product(name: "CapacitorLocalNotifications", package: "CapacitorLocalNotifications"), .product(name: "CapacitorNativeSettings", package: "CapacitorNativeSettings") ] diff --git a/apps/stage-pocket/package.json b/apps/stage-pocket/package.json index 97218e24b..9c0df2724 100644 --- a/apps/stage-pocket/package.json +++ b/apps/stage-pocket/package.json @@ -21,6 +21,7 @@ }, "dependencies": { "@capacitor/android": "catalog:", + "@capacitor/barcode-scanner": "catalog:", "@capacitor/core": "catalog:", "@capacitor/ios": "catalog:", "@capacitor/local-notifications": "catalog:", diff --git a/apps/stage-pocket/src/App.vue b/apps/stage-pocket/src/App.vue index 48ce3cf33..dd57d30e8 100644 --- a/apps/stage-pocket/src/App.vue +++ b/apps/stage-pocket/src/App.vue @@ -134,7 +134,7 @@ const extraSteps = computed(() => [ - + diff --git a/apps/stage-pocket/src/modules/server-channel-qr-probe.ts b/apps/stage-pocket/src/modules/server-channel-qr-probe.ts new file mode 100644 index 000000000..57b8693bf --- /dev/null +++ b/apps/stage-pocket/src/modules/server-channel-qr-probe.ts @@ -0,0 +1,39 @@ +import type { ServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr' + +import { errorMessageFrom } from '@moeru/std' +import { Client, WebSocketEventSource } from '@proj-airi/server-sdk' + +import { getHostWebSocketConstructor } from './websocket-bridge' + +export async function probeServerChannelQrPayload(payload: ServerChannelQrPayload) { + const websocketConstructor = getHostWebSocketConstructor() + if (!websocketConstructor) { + throw new Error('AIRI host websocket bridge is unavailable') + } + + const errors: string[] = [] + + for (const url of payload.urls) { + const client = new Client({ + autoConnect: false, + autoReconnect: false, + connectTimeoutMs: 2_000, + name: WebSocketEventSource.StageWeb, + token: payload.authToken, + url, + websocketConstructor, + }) + + try { + await client.connect({ timeout: 2_500 }) + client.close() + return url + } + catch (error) { + client.close() + errors.push(`${url}: ${errorMessageFrom(error) ?? 'Unknown websocket probe error'}`) + } + } + + throw new Error(`No candidate server channel URL was reachable. ${errors.join('; ')}`) +} diff --git a/apps/stage-pocket/src/pages/settings/connection/index.vue b/apps/stage-pocket/src/pages/settings/connection/index.vue new file mode 100644 index 000000000..075244ba7 --- /dev/null +++ b/apps/stage-pocket/src/pages/settings/connection/index.vue @@ -0,0 +1,26 @@ + + + + + +meta: + layout: settings + titleKey: settings.pages.connection.title + subtitleKey: settings.title + descriptionKey: settings.pages.connection.description + icon: i-solar:wi-fi-router-bold-duotone + settingsEntry: true + order: 8 + stageTransition: + name: slide + diff --git a/apps/stage-pocket/src/pages/settings/connection/server-channel-qr-scanner.vue b/apps/stage-pocket/src/pages/settings/connection/server-channel-qr-scanner.vue new file mode 100644 index 000000000..bf1938f24 --- /dev/null +++ b/apps/stage-pocket/src/pages/settings/connection/server-channel-qr-scanner.vue @@ -0,0 +1,66 @@ + + + diff --git a/apps/stage-pocket/vite.config.ts b/apps/stage-pocket/vite.config.ts index 05f25ea36..1dcc975a1 100644 --- a/apps/stage-pocket/vite.config.ts +++ b/apps/stage-pocket/vite.config.ts @@ -136,7 +136,13 @@ export default defineConfig({ importMode: 'async', routesFolder: [ resolve(import.meta.dirname, 'src', 'pages'), - resolve(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src', 'pages'), + { + src: resolve(import.meta.dirname, '..', '..', 'packages', 'stage-pages', 'src', 'pages'), + exclude: base => [ + ...base, + '**/settings/connection/index.vue', + ], + }, ], exclude: ['**/components/**'], }), diff --git a/apps/stage-tamagotchi/package.json b/apps/stage-tamagotchi/package.json index 0cd389680..e7489e26e 100644 --- a/apps/stage-tamagotchi/package.json +++ b/apps/stage-tamagotchi/package.json @@ -118,6 +118,7 @@ "three": "^0.183.2", "unified": "^11.0.5", "unspeech": "catalog:xsai", + "uqr": "catalog:", "uuid": "^13.0.0", "valibot": "^1.3.1", "vaul-vue": "^0.4.1", diff --git a/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts b/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts index d90f55013..cf92c8a23 100644 --- a/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts +++ b/apps/stage-tamagotchi/src/main/services/airi/channel-server/index.ts @@ -5,6 +5,7 @@ import type { ElectronServerChannelConfig } from '../../../../shared/eventa' import { randomUUID, X509Certificate } from 'node:crypto' import { existsSync, readFileSync, writeFileSync } from 'node:fs' +import { isIP } from 'node:net' import { join } from 'node:path' import { env, platform } from 'node:process' @@ -13,6 +14,7 @@ import { defineInvokeHandler } from '@moeru/eventa' import { createContext } from '@moeru/eventa/adapters/electron/main' import { errorMessageFrom } from '@moeru/std' import { createServer, getLocalIPs } from '@proj-airi/server-runtime/server' +import { createServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr' import { Mutex } from 'async-mutex' import { app, ipcMain, session } from 'electron' import { createCA, createCert } from 'mkcert' @@ -23,6 +25,7 @@ import { z } from 'zod' import { electronApplyServerChannelConfig, electronGetServerChannelConfig, + electronGetServerChannelQrPayload, } from '../../../../shared/eventa' import { createConfig } from '../../../libs/electron/persistence' import { ensureServerChannelConfigDefaults } from './config' @@ -75,6 +78,50 @@ function getServerChannelPort() { return env.SERVER_CHANNEL_PORT ? Number.parseInt(env.SERVER_CHANNEL_PORT) : 6121 } +const LOOPBACK_HOSTS = new Set(['localhost', '127.0.0.1', '::1']) + +function isLoopbackHost(host: string) { + return LOOPBACK_HOSTS.has(host) +} + +function getServerChannelQrHosts(config: ElectronServerChannelConfig, serverChannel: Server) { + if (config.hostname === '0.0.0.0') { + return Array.from(new Set(serverChannel.getConnectionHost())) + .filter(host => !isLoopbackHost(host)) + .sort() + } + + if (isLoopbackHost(config.hostname)) { + return [] + } + + return [config.hostname] +} + +function createServerChannelUrl(protocol: 'ws' | 'wss', host: string) { + const urlHost = isIP(host) === 6 ? `[${host}]` : host + // TODO: Deduplicate the server channel websocket path with `packages/server-runtime/src/index.ts` + // and `packages/server-sdk/src/client.ts` so this does not rely on three separate `/ws` literals. + return `${protocol}://${urlHost}:${getServerChannelPort()}/ws` +} + +function getServerChannelQrPayload(config: ElectronServerChannelConfig, serverChannel: Server) { + const protocol = config.tlsConfig ? 'wss' : 'ws' + const urls = getServerChannelQrHosts(config, serverChannel) + .map(host => createServerChannelUrl(protocol, host)) + + if (!urls.length) { + throw new Error('No reachable private LAN address is available for the current server channel host.') + } + + return createServerChannelQrPayload({ + type: 'airi:server-channel', + version: 1, + urls, + authToken: config.authToken, + }) +} + async function getChannelServerConfig(): Promise { const config = channelServerConfigStore.get() || { hostname: '127.0.0.1', authToken: '', tlsConfig: null } @@ -413,6 +460,11 @@ export async function createServerChannelService(params: { serverChannel: Server return await getChannelServerConfig() }) + defineInvokeHandler(context, electronGetServerChannelQrPayload, async () => { + const config = await getChannelServerConfig() + return getServerChannelQrPayload(config, params.serverChannel) + }) + defineInvokeHandler(context, electronApplyServerChannelConfig, async (req) => { const current = await getChannelServerConfig() const next = await normalizeChannelServerOptions(req, current) diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue index 3ffddd6a8..d4633489b 100644 --- a/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/connection/index.vue @@ -7,6 +7,8 @@ import { storeToRefs } from 'pinia' import { computed } from 'vue' import { useI18n } from 'vue-i18n' +import ServerChannelQrCard from './server-channel-qr-card.vue' + import { useServerChannelSettingsStore } from '../../../stores/settings/server-channel' import { hostnameFromExposureMode, @@ -121,6 +123,8 @@ const exposureModeOptions = computed(() => [ > {{ lastApplyError }} + + diff --git a/apps/stage-tamagotchi/src/renderer/pages/settings/connection/server-channel-qr-card.vue b/apps/stage-tamagotchi/src/renderer/pages/settings/connection/server-channel-qr-card.vue new file mode 100644 index 000000000..9693eda5a --- /dev/null +++ b/apps/stage-tamagotchi/src/renderer/pages/settings/connection/server-channel-qr-card.vue @@ -0,0 +1,180 @@ + + + diff --git a/apps/stage-tamagotchi/src/shared/eventa.ts b/apps/stage-tamagotchi/src/shared/eventa.ts index 441540652..64cef33bf 100644 --- a/apps/stage-tamagotchi/src/shared/eventa.ts +++ b/apps/stage-tamagotchi/src/shared/eventa.ts @@ -1,5 +1,6 @@ import type { Locale } from '@intlify/core' import type { ServerOptions } from '@proj-airi/server-runtime/server' +import type { ServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr' import type { ThreeHitTestReadTracePayload, ThreeSceneRenderInfoTracePayload, @@ -30,6 +31,7 @@ export interface ElectronServerChannelConfig { } export const electronGetServerChannelConfig = defineInvokeEventa('eventa:invoke:electron:server-channel:get-config') export const electronApplyServerChannelConfig = defineInvokeEventa>('eventa:invoke:electron:server-channel:apply-config') +export const electronGetServerChannelQrPayload = defineInvokeEventa('eventa:invoke:electron:server-channel:get-qr-payload') export const electronPluginList = defineInvokeEventa('eventa:invoke:electron:plugins:list') export const electronPluginSetEnabled = defineInvokeEventa('eventa:invoke:electron:plugins:set-enabled') diff --git a/packages/i18n/src/locales/en/settings.yaml b/packages/i18n/src/locales/en/settings.yaml index f569049ec..d847c34ac 100644 --- a/packages/i18n/src/locales/en/settings.yaml +++ b/packages/i18n/src/locales/en/settings.yaml @@ -34,13 +34,13 @@ dialogs: retry: Retry credentialsSafeLabel: Keep your API keys and credentials safe! credentialsSafeLocal: >- - AIRI is running pure locally in your PC, and we will never steal - your credentials for AI / LLM providers. But keep in mind that your API - keys are sensitive information. Make sure to keep them safe and do not - share them with anyone. + AIRI is running pure locally in your PC, and we will never steal your + credentials for AI / LLM providers. But keep in mind that your API keys + are sensitive information. Make sure to keep them safe and do not share + them with anyone. credentialsSafeOpenSource: >- - AIRI is open sourced at {github}, if you want to check how we handle - your credentials, feel free to inspect our code. + AIRI is open sourced at {github}, if you want to check how we handle your + credentials, feel free to inspect our code. enableChatCheck: >- Allow airi to send a "ping" message to the first available chat model to check availability @@ -70,7 +70,7 @@ dialogs: stateNotGranted: Not granted language: title: Language - description: > + description: | UI language. You can set characters' language later. controls-island: icon-size: @@ -82,7 +82,9 @@ controls-island: analytics: notice: title: Usage analytics - description: AIRI collects anonymous usage analytics to help us understand how the app is used and improve stability. No personal data is collected. + description: >- + AIRI collects anonymous usage analytics to help us understand how the app + is used and improve stability. No personal data is collected. privacyPrefix: Read the privacyLink: privacy policy onboardingHint: You can turn analytics off later in Settings > System > General. @@ -402,14 +404,21 @@ pages: description: Playing Minecraft! title: Minecraft enable: Enable Minecraft Integration - enable-description: Include Minecraft self-knowledge in Stage chat turns and keep passive runtime visibility enabled. + enable-description: >- + Include Minecraft self-knowledge in Stage chat turns and keep passive + runtime visibility enabled. configured: Minecraft is properly configured! read-only: title: Read-only shell - description: Stage currently exposes Minecraft as a passive shell. Runtime status and traffic are visible here, but service configuration stays in the Minecraft service files. + description: >- + Stage currently exposes Minecraft as a passive shell. Runtime status + and traffic are visible here, but service configuration stays in the + Minecraft service files. setup: title: Setup - description: You can find the instructions for setting up the bot in the following file + description: >- + You can find the instructions for setting up the bot in the following + file runtime: title: Observed runtime connection: Connection @@ -423,7 +432,7 @@ pages: status: service-online: Service online service-offline: Service offline - no-runtime-context: 'No bot context observed yet' + no-runtime-context: No bot context observed yet last-context-seconds: 'Last bot context: {seconds}s ago' last-context-stale: 'Last bot context: Stale' hearing: @@ -435,12 +444,18 @@ pages: description: Select the suitable speech recognition provider confidence-threshold: title: Confidence Threshold - description: Filter out low-confidence transcriptions to reduce Whisper hallucinations. Values closer to 0 are more strict; drag to the leftmost to disable. Only effective for providers supporting Whisper API (e.g., OpenAI, Groq). + description: >- + Filter out low-confidence transcriptions to reduce Whisper + hallucinations. Values closer to 0 are more strict; drag to the + leftmost to disable. Only effective for providers supporting + Whisper API (e.g., OpenAI, Groq). disabled: Disabled verbose-json-note: >- - Note: If your provider does not support verbose_json responses, this setting will have no effect. + Note: If your provider does not support verbose_json responses, + this setting will have no effect. verbose-json-unsupported: >- - Your provider did not return verbose_json segments. Confidence filtering had no effect on the last transcription. + Your provider did not return verbose_json segments. Confidence + filtering had no effect on the last transcription. memory-long-term: description: Long-term memory specific settings and management title: Long-Term Memory @@ -530,7 +545,9 @@ pages: apply-and-restart: Save and restart stdio MCP messages: opened: Config file opened at {path} - restarted: MCP servers restarted. Started {started}, failed {failed}, skipped {skipped} + restarted: >- + MCP servers restarted. Started {started}, failed {failed}, skipped + {skipped} flux: title: Flux buy: Charge @@ -608,7 +625,8 @@ pages: thinking-mode: label: Thinking Mode description: > - Controls Ollama thinking behavior. GPT-OSS only supports low/medium/high levels. + Controls Ollama thinking behavior. GPT-OSS only supports + low/medium/high levels. options: auto: Auto (provider default) disable: Disable @@ -652,7 +670,7 @@ pages: integration guide. common: continueAnyway: Continue Anyway - goToModelSelection: 'Select Model →' + goToModelSelection: Select Model → fields: field: api-key: @@ -946,17 +964,48 @@ pages: placeholder: ws://localhost:6121/ws server-hostname: label: Expose On Network - description: Choose whether the AIRI gateway only listens on this device or can be reached from your local network. + description: >- + Choose whether the AIRI gateway only listens on this device or can be + reached from your local network. advanced-label: Bind Hostname - advanced-description: Use a specific hostname or IP address when you need manual network binding. + advanced-description: >- + Use a specific hostname or IP address when you need manual network + binding. options: this-device: This device all: All advanced: Advanced server-auth-token: label: Auth Token - description: Security token required by remote clients that connect to this gateway. When left empty, AIRI generates a new token automatically. + description: >- + Security token required by remote clients that connect to this gateway. + When left empty, AIRI generates a new token automatically. placeholder: Auto-generate + qr: + title: Connect from Stage Pocket + description: >- + Scan this QR code from Stage Pocket to try all reachable local network + addresses and save the first working one. + refresh: Refresh + image-alt: AIRI server channel connection QR code + candidates: Candidate addresses + token-hint: >- + The QR code includes the auth token so Stage Pocket can authenticate + after connecting. Do not share it publicly. + errors: + title: QR code unavailable + unavailable: Failed to prepare the connection QR code. + qr-scan: + title: Quick Connect + description: Scan the QR code from Tamagotchi to quick connect + instructions: Point the camera at the AIRI connection QR code. + action: Scan + success: + title: Connected address saved + description: 'URL been used: {url}' + errors: + title: QR scan failed + failed: Failed to scan or connect with the QR code. scene: description: Configure the environment where the character lives title: Scene @@ -1032,7 +1081,7 @@ pages: transcripts performance-playground: title: Performance Playground - description: 'VRM expressions + TTS lip sync playground' + description: VRM expressions + TTS lip sync playground theme-presets: presets: default: diff --git a/packages/i18n/src/locales/zh-Hans/settings.yaml b/packages/i18n/src/locales/zh-Hans/settings.yaml index b478a3bd4..40dc75dd9 100644 --- a/packages/i18n/src/locales/zh-Hans/settings.yaml +++ b/packages/i18n/src/locales/zh-Hans/settings.yaml @@ -4,8 +4,7 @@ animations: stage-transitions: title: 是否开启舞台切换动画 use-page-specific-transitions: - description: >- - 某些页面会有自己的过场动画,这将覆盖舞台过场动画 + description: 某些页面会有自己的过场动画,这将覆盖舞台过场动画 title: 是否使用页面特定过场动画 dialogs: onboarding: @@ -22,7 +21,7 @@ dialogs: validationSuccess: 配置验证成功 validationPartial: Configuration partially validated validationFailed: 配置验证失败 - validationError: '验证错误:{error}' + validationError: 验证错误:{error} testGeneration: Ping API testGenerationRunning: Ping... testGenerationFailed: Ping test failed @@ -32,12 +31,11 @@ dialogs: next: 下一步 retry: 重试 credentialsSafeLabel: Keep your API keys and credentials safe! - credentialsSafeLocal: >- - AIRI完全本地运行在您的电脑中,我们绝不窃取您对大模型提供商的凭据。但请牢记,您的API密钥属于敏感信息。务必妥善保管,切勿与他人分享。 - credentialsSafeOpenSource: >- - AIRI在{github}开源,如果您想确认我们如何存储您的凭据,您可以自由浏览源码。 + credentialsSafeLocal: AIRI完全本地运行在您的电脑中,我们绝不窃取您对大模型提供商的凭据。但请牢记,您的API密钥属于敏感信息。务必妥善保管,切勿与他人分享。 + credentialsSafeOpenSource: AIRI在{github}开源,如果您想确认我们如何存储您的凭据,您可以自由浏览源码。 enableChatCheck: >- - Allow airi to send a "ping" message to the first available chat model to check availability + Allow airi to send a "ping" message to the first available chat model to + check availability start: 开始吧! loginPrompt: 登录并使用AIRI官方提供商以获得最佳体验。 loginAction: 登录 @@ -46,8 +44,7 @@ dialogs: buyFlux: Flux充值 select-model: 选择模型 no-models: 找不到可用模型 - no-models-help: >- - 请返回上一步并检查您的 API Key,或检查网络连接。 + no-models-help: 请返回上一步并检查您的 API Key,或检查网络连接。 permissions: title: 通知 description: 您现在可以允许通知或者继续并稍后设置。 @@ -63,7 +60,7 @@ dialogs: stateNotGranted: 未授权 language: title: 语言 - description: > + description: | 界面语言。你也可以稍后再设置角色语言。 controls-island: icon-size: @@ -121,7 +118,7 @@ live2d: title: 帧率 description: 限制渲染帧率 options: - unlimited: '∞' + unlimited: ∞ microphone: 麦克风 models: 模型 pages: @@ -173,36 +170,32 @@ pages: scenario: 你最近醒来,忘记了之前的所有生活。 systemprompt: 你将收到消息,请像真实人类一样回复。 posthistoryinstructions: 记得模仿人类的行为。 - modules_info: >- - 为此角色卡配置模块。如未设置,将使用默认模块配置。 + modules_info: 为此角色卡配置模块。如未设置,将使用默认模块配置。 use_default: 使用默认 use_default_not_configured: 使用默认(未配置) fields_info: - subtitle: >- - 您可以在这里填写有关您正在创建的角色的一些详细信息,解释他的背景和情境,以及应该如何回应您的互动。 + subtitle: 您可以在这里填写有关您正在创建的角色的一些详细信息,解释他的背景和情境,以及应该如何回应您的互动。 name: 是该角色的正式名称。 nickname: 您也可以提供一个昵称,它将被优先使用。 description: 该角色的描述。 notes: 如果您想添加一些个人备注。 - personality: >- - 在这里描述您的角色的个性。例如:害羞?好奇?其他? + personality: 在这里描述您的角色的个性。例如:害羞?好奇?其他? scenario: 周围环境是怎样的?当前的情境是什么? greetings_field: 问候语 greetings: 您的角色应该如何说“你好”? systemprompt: 在这里向 AI LLM 解释当被提示时应该如何回应。 posthistoryinstructions: 在消息历史之后,放入 AI LLM 应该阅读的内容。 - version: >- - 卡片版本,如果您从之前的卡片做了更改,应当增加此版本号。 + version: 卡片版本,如果您从之前的卡片做了更改,应当增加此版本号。 consciousness_model: 为这个角色选择 AI 模型。 speech_model: 为此角色指定语音合成模型与音色。 errors: name: 错误:你必须提供一个有效的名称! - version: '错误:版本号无效!' - description: '错误:你必须为此卡片提供描述。' - personality: '错误:必须为该角色提供性格描述。' - scenario: '错误:必须提供一个情境。' - systemprompt: '错误:请提供系统提示。' - posthistoryinstructions: '错误:必须提供消息历史后的提示。' + version: 错误:版本号无效! + description: 错误:你必须为此卡片提供描述。 + personality: 错误:必须为该角色提供性格描述。 + scenario: 错误:必须提供一个情境。 + systemprompt: 错误:请提供系统提示。 + posthistoryinstructions: 错误:必须提供消息历史后的提示。 modules: 模块 name_asc: 名称 (A-Z) name_desc: 名称 (Z-A) @@ -266,7 +259,7 @@ pages: reset: 重置桌面数据 confirmations: tooltip: 您确定吗? - 'yes': '是' + 'yes': 是 status: exported: 聊天会话已导出。 imported: 聊天会话已导入。 @@ -308,23 +301,19 @@ pages: title: 参数 parameters: adaptive_threshold: - description: >- - 是否根据信号随时间变化的方差应用自适应阈值 + description: 是否根据信号随时间变化的方差应用自适应阈值 label: 自适应阈值 buffer_duration: description: 内部分析缓冲区的持续时间 label: 缓冲时长 envelope_filter_frequency: - description: >- - 应用于平滑能量变化的包络滤波器频率 + description: 应用于平滑能量变化的包络滤波器频率 label: 包络滤波器频率 highpass_filter_frequency: - description: >- - 高通滤波器的频率设置,用于抑制低频噪声(如次低频噪声) + description: 高通滤波器的频率设置,用于抑制低频噪声(如次低频噪声) label: 高通滤波器频率 lowpass_filter_frequency: - description: >- - 低通滤波器的频率设置,用于削减高频信号(如人声) + description: 低通滤波器的频率设置,用于削减高频信号(如人声) label: 低通滤波器频率 min_beat_interval: description: 最大每分钟节拍数(BPM)或检测到的节拍之间最小间隔 @@ -344,7 +333,7 @@ pages: provider-model-selection: collapse: 折叠 custom_model_placeholder: 输入自订模型名称 - current_model_label: '当前模型:' + current_model_label: 当前模型: description: 为意识选择合适的 LLM 服务来源 error: 获取出错啦 health_check_failed: 健康检查失败 @@ -386,14 +375,21 @@ pages: description: 一起玩 Minecraft! title: 我的世界 Minecraft enable: 启用我的世界集成 - enable-description: Include Minecraft self-knowledge in Stage chat turns and keep passive runtime visibility enabled. + enable-description: >- + Include Minecraft self-knowledge in Stage chat turns and keep passive + runtime visibility enabled. configured: 我的世界已正确配置! read-only: title: Read-only shell - description: Stage currently exposes Minecraft as a passive shell. Runtime status and traffic are visible here, but service configuration stays in the Minecraft service files. + description: >- + Stage currently exposes Minecraft as a passive shell. Runtime status + and traffic are visible here, but service configuration stays in the + Minecraft service files. setup: title: Setup - description: You can find the instructions for setting up the bot in the following file + description: >- + You can find the instructions for setting up the bot in the following + file runtime: title: Observed runtime connection: Connection @@ -407,7 +403,7 @@ pages: status: service-online: Service online service-offline: Service offline - no-runtime-context: 'No bot context observed yet' + no-runtime-context: No bot context observed yet last-context-seconds: 'Last bot context: {seconds}s ago' last-context-stale: 'Last bot context: Stale' hearing: @@ -419,12 +415,18 @@ pages: description: 选择合适的语音转文本的服务来源 confidence-threshold: title: Confidence Threshold - description: Filter out low-confidence transcriptions to reduce Whisper hallucinations. Values closer to 0 are more strict; drag to the leftmost to disable. Only effective for providers supporting Whisper API (e.g., OpenAI, Groq). + description: >- + Filter out low-confidence transcriptions to reduce Whisper + hallucinations. Values closer to 0 are more strict; drag to the + leftmost to disable. Only effective for providers supporting + Whisper API (e.g., OpenAI, Groq). disabled: 禁用 verbose-json-note: >- - Note: If your provider does not support verbose_json responses, this setting will have no effect. + Note: If your provider does not support verbose_json responses, + this setting will have no effect. verbose-json-unsupported: >- - Your provider did not return verbose_json segments. Confidence filtering had no effect on the last transcription. + Your provider did not return verbose_json segments. Confidence + filtering had no effect on the last transcription. memory-long-term: description: 长期记忆 title: 长期记忆 @@ -468,7 +470,8 @@ pages: search_voices_results: 找到 {count} / {total} 个声线 unsupported_voice_warning_title: 没有支持的声线 unsupported_voice_warning_content: >- - 我们正在尽快支持该模型的所有音色,如果你迫切希望支持该模型音色,请在 GitHub 上联系我们 https://github.com/moeru-ai/airi/issues + 我们正在尽快支持该模型的所有音色,如果你迫切希望支持该模型音色,请在 GitHub 上联系我们 + https://github.com/moeru-ai/airi/issues show_less: 显示更少 show_more: 显示更多 title: 选择语音合成服务来源 @@ -542,11 +545,10 @@ pages: explained: chat: 文本生成模型服务来源,例如 OpenRouter, OpenAI, Ollama Speech: 语音(文本转语音)模型服务来源,例如 ElevenLabs, Azure Speech - Transcription: >- - 转录(语音转文本)模型服务来源,例如 Whisper.cpp, OpenAI, Azure Speech + Transcription: 转录(语音转文本)模型服务来源,例如 Whisper.cpp, OpenAI, Azure Speech helpinfo: title: 第一次使用? - description: > + description: | AIRI 需要配置至少一个 {chat} 服务来源,才能正常思考和运作。你可以把它看作是 AIRI 系统中角色的大脑。 catalog: edit: @@ -558,8 +560,7 @@ pages: validation: failed: title: 请检查您的服务来源设置,验证失败。 - description: >- - 如果你想要继续,你仍然可以保存当前的配置。 + description: 如果你想要继续,你仍然可以保存当前的配置。 action: 仍要保存 config: common: @@ -572,7 +573,8 @@ pages: base-url: label: Base URL description: >- - 用于连接远程服务的 baseUrl。通常看起来像是 http://localhost:11434/v1 或 https://example.com/v1。 + 用于连接远程服务的 baseUrl。通常看起来像是 http://localhost:11434/v1 或 + https://example.com/v1。 placeholder: 基础 Url…… headers: label: 添加自定义 HTTP 标头 @@ -583,7 +585,7 @@ pages: placeholder: 请求头数值 thinking-mode: label: 深度思考模式 - description: > + description: | 控制 Olama 深度思考的开关。GPT-OSS 只支持低/中/高水平。 options: auto: 自动(默认服务来源) @@ -618,10 +620,14 @@ pages: openai-compatible-check-connectivity: label: Ollama需要额外的配置 content: >- - 很抱歉,Ollama 连接检查失败。不要担心,如果是 Ollama,需要额外配置才能通过 Ollama。 您需要在启动 Olama 应用程序或 Ollama CLI 服务器时明确设置 OLLAMA_HOST 详情,请检查 Ollama 集成指南。 + 很抱歉,Ollama 连接检查失败。不要担心,如果是 Ollama,需要额外配置才能通过 Ollama。 + 您需要在启动 Olama 应用程序或 Ollama CLI 服务器时明确设置 + OLLAMA_HOST 详情,请检查 Ollama + 集成指南。 common: continueAnyway: 仍然继续 - goToModelSelection: '选择模型→' + goToModelSelection: 选择模型→ fields: field: api-key: @@ -666,14 +672,11 @@ pages: helpinfo: title: 开始之前 description: - part1: >- - 虽然 Anthropic 最近宣布他们正在提供对 OpenAI SDK 兼容性的 Beta 版支持 - part2: '(您可以在此处阅读更多信息)' + part1: 虽然 Anthropic 最近宣布他们正在提供对 OpenAI SDK 兼容性的 Beta 版支持 + part2: (您可以在此处阅读更多信息) part3: 但由于实现细节附带 - part4: >- - 与 OpenAI SDK 不一致,目前无法在浏览器中使用此提供程序。 - part5: >- - 如果您确实需要使用此提供程序,则需要一个专用的代理后端,例如在 + part4: 与 OpenAI SDK 不一致,目前无法在浏览器中使用此提供程序。 + part5: 如果您确实需要使用此提供程序,则需要一个专用的代理后端,例如在 part6: 上运行的Serverless Function,或者一些绕过 CORS 的服务,以绕过 CORS 限制。 cloudflare-workers-ai: description: Cloudflare.com @@ -758,8 +761,7 @@ pages: description: 较小的模型加载更快,但质量可能略有下降。 models: fp32-webgpu: - description: >- - 基于 WebGPU 的全精度模型 - 建议在支持的设备上使用 + description: 基于 WebGPU 的全精度模型 - 建议在支持的设备上使用 fp32: description: 全精度模型 fp16: @@ -911,17 +913,44 @@ pages: placeholder: ws://localhost:6121/ws server-hostname: label: Expose On Network - description: Choose whether the AIRI gateway only listens on this device or can be reached from your local network. + description: >- + Choose whether the AIRI gateway only listens on this device or can be + reached from your local network. advanced-label: Bind Hostname - advanced-description: Use a specific hostname or IP address when you need manual network binding. + advanced-description: >- + Use a specific hostname or IP address when you need manual network + binding. options: this-device: This device all: All advanced: Advanced server-auth-token: label: Auth Token - description: Security token required by remote clients that connect to this gateway. When left empty, AIRI generates a new token automatically. + description: >- + Security token required by remote clients that connect to this gateway. + When left empty, AIRI generates a new token automatically. placeholder: Auto-generate + qr: + title: 显示二维码 + description: 口袋 AIRI 可通过二维码快速连接到本机 + refresh: 刷新 + image-alt: AIRI 主脑连接二维码 + candidates: 候选地址 + token-hint: 二维码包含认证 token,用于让 Stage Pocket 连接后完成认证。不要公开分享。 + errors: + title: 无法生成二维码 + unavailable: 无法准备连接二维码。 + qr-scan: + title: 快速连接 + description: 扫描 Tamagotchi 上的二维码来快速连接主脑 + instructions: 将摄像头对准 AIRI 连接二维码。 + action: 扫描 + success: + title: 连接成功! + description: 使用的连接地址:{url} + errors: + title: 扫码失败 + failed: 无法通过此二维码扫码或连接。 scene: description: 配置角色所在环境 title: 场景 @@ -981,8 +1010,7 @@ pages: title: 拍立得 description: 拍摄模型照片的工具 context-flow: - description: >- - 检查传入的上下文更新和传出的聊天流事件 + description: 检查传入的上下文更新和传出的聊天流事件 websocket-inspector: title: WebSocket 检查器 description: 检查原始 WebSocket 流量 @@ -991,11 +1019,10 @@ pages: description: 测试颜色提取 providers-transcription-realtime-aliyun-nls: title: 阿里云实时转写 - description: >- - 将麦克风音频流式传输到阿里云 NLS 并检查实时转写 + description: 将麦克风音频流式传输到阿里云 NLS 并检查实时转写 performance-playground: title: 性能测试场地 - description: 'VRM 表情 + TTS lip sync 实验平台' + description: VRM 表情 + TTS lip sync 实验平台 theme-presets: presets: default: @@ -1061,8 +1088,7 @@ pages: color-6: 金黄色 color-7: 天蓝色 color-8: 赭黄色 - description: >- - 中国传统色彩,源自古代纺织品、瓷器和绘画 + description: 中国传统色彩,源自古代纺织品、瓷器和绘画 title: 中国传统颜色 title: 预设 title: 系统 @@ -1127,9 +1153,7 @@ vrm: skybox-specular-mix: 漫反射/镜面反射混合系数 websocket-secure-enabled: title: 启用安全WebSocket (WSS) - description: >- - 启用 WSS (WebSocket Secure) 以便在开发模式下允许移动设备(Pocket) 连接HTTPS 。这将自动生成一个自签名证书。 + description: 启用 WSS (WebSocket Secure) 以便在开发模式下允许移动设备(Pocket) 连接HTTPS 。这将自动生成一个自签名证书。 wip: title: 正在开发中 - description: >- - 此功能正在开发中,尚未公开发布。请在未来的更新中再次查看此功能。 + description: 此功能正在开发中,尚未公开发布。请在未来的更新中再次查看此功能。 diff --git a/packages/stage-shared/package.json b/packages/stage-shared/package.json index ef2449527..99bcb808c 100644 --- a/packages/stage-shared/package.json +++ b/packages/stage-shared/package.json @@ -18,6 +18,7 @@ ".": "./src/index.ts", "./auth": "./src/auth/index.ts", "./beat-sync": "./src/beat-sync/index.ts", + "./server-channel-qr": "./src/server-channel-qr.ts", "./electron-renderer": "./src/electron-renderer.d.ts", "./composables": "./src/composables/index.ts" }, @@ -27,6 +28,7 @@ "dependencies": { "@vueuse/core": "catalog:", "pinia": "catalog:", + "valibot": "catalog:", "vue": "catalog:" }, "devDependencies": { diff --git a/packages/stage-shared/src/server-channel-qr.ts b/packages/stage-shared/src/server-channel-qr.ts new file mode 100644 index 000000000..cc9b4fc78 --- /dev/null +++ b/packages/stage-shared/src/server-channel-qr.ts @@ -0,0 +1,44 @@ +import type { InferInput, InferOutput } from 'valibot' + +import { array, check, literal, minLength, object, parse, pipe, string, transform } from 'valibot' + +export const SERVER_CHANNEL_QR_PAYLOAD_TYPE = 'airi:server-channel' +export const SERVER_CHANNEL_QR_PAYLOAD_VERSION = 1 + +function isWebSocketUrl(value: string) { + try { + const url = new URL(value) + return (url.protocol === 'ws:' || url.protocol === 'wss:') && !!url.hostname + } + catch { + return false + } +} + +function normalizeWebSocketUrl(value: string) { + return new URL(value).toString() +} + +export const ServerChannelQrUrlSchema = pipe( + string(), + check(isWebSocketUrl, 'Expected a ws or wss URL.'), + transform(normalizeWebSocketUrl), +) + +export const ServerChannelQrPayloadSchema = object({ + type: literal(SERVER_CHANNEL_QR_PAYLOAD_TYPE), + version: literal(SERVER_CHANNEL_QR_PAYLOAD_VERSION), + urls: pipe(array(ServerChannelQrUrlSchema), minLength(1)), + authToken: string(), +}) + +export type ServerChannelQrPayloadInput = InferInput +export type ServerChannelQrPayload = InferOutput + +export function createServerChannelQrPayload(payload: ServerChannelQrPayloadInput) { + return parse(ServerChannelQrPayloadSchema, payload) +} + +export function parseServerChannelQrPayload(raw: string) { + return parse(ServerChannelQrPayloadSchema, JSON.parse(raw)) +} diff --git a/packages/stage-ui/src/stores/mods/api/channel-server.test.ts b/packages/stage-ui/src/stores/mods/api/channel-server.test.ts index 81f658f5c..bb95ad874 100644 --- a/packages/stage-ui/src/stores/mods/api/channel-server.test.ts +++ b/packages/stage-ui/src/stores/mods/api/channel-server.test.ts @@ -369,4 +369,34 @@ describe('channel-server store reconnect', () => { expect(serverSdkMocks.MockClient.instances).toHaveLength(2) }) + + it('uses the persisted websocket auth token when initialize does not receive an explicit token', async () => { + const store = useModsServerChannelStore() + store.websocketAuthToken = 'persisted-secret' + + const initializePromise = store.initialize() + const client = serverSdkMocks.MockClient.instances[0] + + expect(client.options.token).toBe('persisted-secret') + + client.simulateAuthenticated() + await initializePromise + }) + + it('reconnects when the persisted websocket auth token changes', async () => { + const store = useModsServerChannelStore() + store.websocketAuthToken = 'initial-secret' + + const initializePromise = store.initialize() + const firstClient = serverSdkMocks.MockClient.instances[0] + + firstClient.simulateAuthenticated() + await initializePromise + + store.websocketAuthToken = 'rotated-secret' + await nextTick() + + expect(serverSdkMocks.MockClient.instances.length).toBeGreaterThan(1) + expect(serverSdkMocks.MockClient.instances.at(-1)?.options.token).toBe('rotated-secret') + }) }) diff --git a/packages/stage-ui/src/stores/mods/api/channel-server.ts b/packages/stage-ui/src/stores/mods/api/channel-server.ts index 322a5e272..24e6a6ddb 100644 --- a/packages/stage-ui/src/stores/mods/api/channel-server.ts +++ b/packages/stage-ui/src/stores/mods/api/channel-server.ts @@ -59,6 +59,7 @@ export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:se const defaultWebSocketUrl = import.meta.env.VITE_AIRI_WS_URL || 'ws://localhost:6121/ws' const websocketUrl = useLocalStorage('settings/connection/websocket-url', defaultWebSocketUrl) + const websocketAuthToken = useLocalStorage('settings/connection/websocket-auth-token', '') const registeredListeners: ChannelListenerEntry[] = [] const replayableEvents = new Map>() @@ -109,7 +110,7 @@ export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:se client.value = new Client({ name: isStageWeb() ? WebSocketEventSource.StageWeb : isStageTamagotchi() ? WebSocketEventSource.StageTamagotchi : WebSocketEventSource.StageWeb, url: websocketUrl.value || defaultWebSocketUrl, - token: options?.token, + token: options?.token ?? (websocketAuthToken.value || undefined), websocketConstructor: websocketConstructor.value, heartbeat: { // Keep client and server heartbeat windows aligned to reduce false-positive disconnects. @@ -320,8 +321,8 @@ export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:se } } - watch(websocketUrl, (newUrl, oldUrl) => { - if (newUrl === oldUrl) + watch([websocketUrl, websocketAuthToken], ([newUrl, newToken], [oldUrl, oldToken]) => { + if (newUrl === oldUrl && newToken === oldToken) return if (!hasReconnectableWebSocketScheme(newUrl)) @@ -336,6 +337,7 @@ export const useModsServerChannelStore = defineStore('mods:channels:proj-airi:se return { connected, pendingSendCount, + websocketAuthToken, websocketUrl, ensureConnected, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7bd541f4d..ad037b48c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,6 +12,9 @@ catalogs: '@capacitor/android': specifier: ^8.2.0 version: 8.2.0 + '@capacitor/barcode-scanner': + specifier: ^3.0.2 + version: 3.0.2 '@capacitor/cli': specifier: ^8.2.0 version: 8.2.0 @@ -279,6 +282,9 @@ catalogs: unstorage: specifier: ^1.17.4 version: 1.17.4 + uqr: + specifier: ^0.1.3 + version: 0.1.3 valibot: specifier: 1.2.0 version: 1.2.0 @@ -684,6 +690,9 @@ importers: '@capacitor/android': specifier: 'catalog:' version: 8.2.0(@capacitor/core@8.2.0) + '@capacitor/barcode-scanner': + specifier: 'catalog:' + version: 3.0.2(@capacitor/core@8.2.0) '@capacitor/core': specifier: 'catalog:' version: 8.2.0 @@ -1315,6 +1324,9 @@ importers: unspeech: specifier: catalog:xsai version: 0.1.11 + uqr: + specifier: 'catalog:' + version: 0.1.3 uuid: specifier: ^13.0.0 version: 13.0.0 @@ -2881,6 +2893,9 @@ importers: pinia: specifier: 'catalog:' version: 3.0.4(typescript@5.9.3)(vue@3.5.30(typescript@5.9.3)) + valibot: + specifier: 'catalog:' + version: 1.2.0(typescript@5.9.3) vue: specifier: 'catalog:' version: 3.5.30(typescript@5.9.3) @@ -4917,6 +4932,11 @@ packages: peerDependencies: '@capacitor/core': ^8.2.0 + '@capacitor/barcode-scanner@3.0.2': + resolution: {integrity: sha512-eD8G0dj/vDTytzFY3qK/5EWZtv17NHMN97gx5KSHUuqFXOScf+5e4Rwwak8zDkX17VLXNym6y3eIn8AvCLY+Pg==} + peerDependencies: + '@capacitor/core': '>=8.0.0' + '@capacitor/cli@8.2.0': resolution: {integrity: sha512-1cMEk0d/I6tl1U+v/lnJR5Oylpx8ZBIHrvQxD5zK0MkjYOUyQAAGJgh97rkhGJqjAUvrGpa8H4BmyhNQN9a17A==} engines: {node: '>=22.0.0'} @@ -13281,6 +13301,9 @@ packages: resolution: {integrity: sha512-fPU6BHNpsyIhr8yyMpTLLxAbkaK8ArIBcmZIRiBLiDhjeqvXolaEmDGmELFuX9I4xDcaKKcJl+TKZLqruBbmWA==} engines: {node: '>=8.0.0'} + html5-qrcode@2.3.8: + resolution: {integrity: sha512-jsr4vafJhwoLVEDW3n1KvPnCCXWaQfRng0/EEYk1vNcQGcG/htAdhJX0be8YyqMoSz7+hZvOZSTAepsabiuhiQ==} + htmlparser2@10.0.0: resolution: {integrity: sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==} @@ -17053,6 +17076,9 @@ packages: resolution: {integrity: sha512-+dwUY4L35XFYEzE+OAL3sarJdUioVovq+8f7lcIJ7wnmnYQV5UD1Y/lcwaMSyaQ6Bj3JMj1XSTjZbNLHn/19yA==} engines: {node: '>=18'} + uqr@0.1.3: + resolution: {integrity: sha512-0rjE8iEJe4YmT9TOhwsZtqCMRLc5DXZUI2UEYUUg63ikBkqqE5EYWaI0etFe/5KUcmcYwLih2RND1kq+hrUJXA==} + uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} @@ -19008,6 +19034,11 @@ snapshots: dependencies: '@capacitor/core': 8.2.0 + '@capacitor/barcode-scanner@3.0.2(@capacitor/core@8.2.0)': + dependencies: + '@capacitor/core': 8.2.0 + html5-qrcode: 2.3.8 + '@capacitor/cli@8.2.0': dependencies: '@ionic/cli-framework-output': 2.2.8 @@ -27984,6 +28015,8 @@ snapshots: css-line-break: 2.1.0 text-segmentation: 1.0.3 + html5-qrcode@2.3.8: {} + htmlparser2@10.0.0: dependencies: domelementtype: 2.3.0 @@ -32535,6 +32568,8 @@ snapshots: semver: 7.7.4 xdg-basedir: 5.1.0 + uqr@0.1.3: {} + uri-js@4.4.1: dependencies: punycode: 2.3.1 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 65e458f5b..285fbe0cc 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -29,6 +29,7 @@ patchedDependencies: catalog: '@better-auth/oauth-provider': 1.5.6 '@capacitor/android': ^8.2.0 + '@capacitor/barcode-scanner': ^3.0.2 '@capacitor/cli': ^8.2.0 '@capacitor/core': ^8.2.0 '@capacitor/ios': ^8.2.0 @@ -118,6 +119,7 @@ catalog: uncrypto: ^0.1.3 unplugin-info: ^1.2.4 unstorage: ^1.17.4 + uqr: ^0.1.3 valibot: 1.2.0 vite: ^8.0.2 vite-plugin-inspect: ^11.3.3