feat(stage-pocket): scan qr code to connect to tamagotchi

This commit is contained in:
LemonNeko
2026-04-07 19:04:59 +08:00
parent 6541969fdb
commit 0cd095b323
26 changed files with 696 additions and 105 deletions
@@ -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')
@@ -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')
@@ -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")
@@ -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
+2
View File
@@ -26,6 +26,8 @@
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>AIRI needs access to your local network to connect to your Tamagotchi host.</string>
<key>NSCameraUsageDescription</key>
<string>AIRI uses the camera to scan Tamagotchi connection QR codes.</string>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIMainStoryboardFile</key>
@@ -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
@@ -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")
]
+1
View File
@@ -21,6 +21,7 @@
},
"dependencies": {
"@capacitor/android": "catalog:",
"@capacitor/barcode-scanner": "catalog:",
"@capacitor/core": "catalog:",
"@capacitor/ios": "catalog:",
"@capacitor/local-notifications": "catalog:",
+1 -1
View File
@@ -134,7 +134,7 @@ const extraSteps = computed(() => [
</StageTransitionGroup>
<ToasterRoot @close="id => toast.dismiss(id)">
<Toaster />
<Toaster rich-colors />
</ToasterRoot>
<!-- First Time Setup Dialog -->
@@ -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('; ')}`)
}
@@ -0,0 +1,26 @@
<script setup lang="ts">
import ConnectionSettings from '@proj-airi/stage-pages/pages/settings/connection/ConnectionSettings.vue'
import ServerChannelQrScanner from './server-channel-qr-scanner.vue'
</script>
<template>
<ConnectionSettings>
<template #platform-specific>
<ServerChannelQrScanner />
</template>
</ConnectionSettings>
</template>
<route lang="yaml">
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
</route>
@@ -0,0 +1,66 @@
<script setup lang="ts">
import { CapacitorBarcodeScanner, CapacitorBarcodeScannerTypeHint } from '@capacitor/barcode-scanner'
import { errorMessageFrom } from '@moeru/std'
import { parseServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr'
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
import { Button } from '@proj-airi/ui'
import { shallowRef } from 'vue'
import { useI18n } from 'vue-i18n'
import { toast } from 'vue-sonner'
import { probeServerChannelQrPayload } from '../../../modules/server-channel-qr-probe'
const { t } = useI18n()
const serverChannelStore = useModsServerChannelStore()
const scanning = shallowRef(false)
const errorMessage = shallowRef('')
async function scanServerChannelQrCode() {
scanning.value = true
errorMessage.value = ''
try {
const scanResult = await CapacitorBarcodeScanner.scanBarcode({
hint: CapacitorBarcodeScannerTypeHint.QR_CODE,
scanInstructions: t('settings.pages.connection.qr-scan.instructions'),
})
const payload = parseServerChannelQrPayload(scanResult.ScanResult)
const url = await probeServerChannelQrPayload(payload)
serverChannelStore.websocketAuthToken = payload.authToken
serverChannelStore.websocketUrl = url
toast.success(t('settings.pages.connection.qr-scan.success.title'), {
description: t('settings.pages.connection.qr-scan.success.description', { url }),
})
}
catch (error) {
errorMessage.value = errorMessageFrom(error) ?? t('settings.pages.connection.qr-scan.errors.failed')
toast.error(t('settings.pages.connection.qr-scan.errors.title'), {
description: errorMessage.value,
})
}
finally {
scanning.value = false
}
}
</script>
<template>
<div :class="['flex flex-col items-start justify-between gap-3']">
<div :class="['flex flex-col gap-1']">
<div :class="['text-sm font-medium text-neutral-900 dark:text-neutral-100']">
{{ t('settings.pages.connection.qr-scan.title') }}
</div>
<p :class="['m-0 text-xs leading-5 text-neutral-500 dark:text-neutral-400']">
{{ t('settings.pages.connection.qr-scan.description') }}
</p>
</div>
<Button
variant="secondary-muted"
:loading="scanning"
:label="t('settings.pages.connection.qr-scan.action')"
@click="scanServerChannelQrCode"
/>
</div>
</template>
+7 -1
View File
@@ -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/**'],
}),
+1
View File
@@ -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",
@@ -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<ElectronServerChannelConfig> {
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)
@@ -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 }}
</Callout>
<ServerChannelQrCard />
</template>
</ConnectionSettings>
</template>
@@ -0,0 +1,180 @@
<script setup lang="ts">
import type { ServerChannelQrPayload } from '@proj-airi/stage-shared/server-channel-qr'
import { errorMessageFrom } from '@moeru/std'
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
import { Button, Callout, Collapsible } from '@proj-airi/ui'
import { storeToRefs } from 'pinia'
import { renderSVG } from 'uqr'
import { computed, shallowRef, watch } from 'vue'
import { useI18n } from 'vue-i18n'
import { electronGetServerChannelQrPayload } from '../../../../shared/eventa'
import { useServerChannelSettingsStore } from '../../../stores/settings/server-channel'
const { t } = useI18n()
const getServerChannelQrPayload = useElectronEventaInvoke(electronGetServerChannelQrPayload)
const { authToken, hostname, tlsConfig } = storeToRefs(useServerChannelSettingsStore())
const loading = shallowRef(false)
const payload = shallowRef<ServerChannelQrPayload>()
const errorMessage = shallowRef('')
const payloadText = computed(() => {
if (!payload.value) {
return ''
}
return JSON.stringify(payload.value)
})
const qrCodeSource = computed(() => {
if (!payloadText.value) {
return ''
}
const svg = renderSVG(payloadText.value, {
border: 2,
ecc: 'M',
pixelSize: 8,
whiteColor: 'transparent',
blackColor: '#111827',
})
return `data:image/svg+xml;utf8,${encodeURIComponent(svg)}`
})
async function refreshPayload() {
loading.value = true
errorMessage.value = ''
try {
payload.value = await getServerChannelQrPayload()
}
catch (error) {
payload.value = undefined
errorMessage.value = errorMessageFrom(error) ?? t('settings.pages.connection.qr.errors.unavailable')
}
finally {
loading.value = false
}
}
watch([hostname, tlsConfig, authToken], () => {
void refreshPayload()
}, { immediate: true })
</script>
<template>
<Collapsible :default="false">
<template #trigger="slotProps">
<button
:class="[
'w-full flex items-center justify-between gap-3 rounded-xl text-left outline-none transition-all duration-250 ease-in-out',
]"
@click="slotProps.setVisible(!slotProps.visible)"
>
<div :class="['min-w-0 flex flex-col gap-1']">
<div :class="['text-sm font-medium text-neutral-900 dark:text-neutral-100']">
{{ t('settings.pages.connection.qr.title') }}
</div>
<p :class="['m-0 text-xs leading-5 text-neutral-500 dark:text-neutral-400']">
{{ t('settings.pages.connection.qr.description') }}
</p>
</div>
<div
:class="[
'mt-0.5 shrink-0 text-neutral-400 transition-transform duration-250 dark:text-neutral-500',
'i-solar:alt-arrow-down-linear',
slotProps.visible ? 'rotate-180' : 'rotate-0',
]"
/>
</button>
</template>
<div
:class="[
'mt-3 rounded-2xl border border-neutral-200/70 bg-white/70 p-4 dark:border-neutral-700/70 dark:bg-neutral-900/50',
'flex flex-col gap-4',
]"
>
<div :class="['flex items-start justify-between gap-3']">
<p :class="['m-0 text-xs leading-5 text-neutral-500 dark:text-neutral-400']">
{{ t('settings.pages.connection.qr.token-hint') }}
</p>
</div>
<Callout
v-if="errorMessage"
theme="orange"
:label="t('settings.pages.connection.qr.errors.title')"
>
<p :class="['m-0 text-xs leading-5']">
{{ errorMessage }}
</p>
</Callout>
<div
v-else-if="payload"
:class="[
'grid grid-cols-1 gap-4',
'md:grid-cols-[auto_minmax(0,1fr)]',
]"
>
<img
:src="qrCodeSource"
:alt="t('settings.pages.connection.qr.image-alt')"
:class="['h-48 w-48']"
>
<Button
size="sm"
variant="secondary-muted"
:loading="loading"
:label="t('settings.pages.connection.qr.refresh')"
@click="refreshPayload"
/>
<div :class="['min-w-0 flex flex-col gap-3']">
<Collapsible :default="false">
<template #trigger="slotProps">
<button
:class="[
'w-full flex items-center justify-between gap-3 rounded-xl text-left outline-none transition-all duration-250 ease-in-out',
]"
@click="slotProps.setVisible(!slotProps.visible)"
>
<div :class="['text-xs font-medium text-neutral-600 dark:text-neutral-300']">
{{ t('settings.pages.connection.qr.candidates') }}
</div>
<div
:class="[
'shrink-0 text-neutral-400 transition-transform duration-250 dark:text-neutral-500',
'i-solar:alt-arrow-down-linear',
slotProps.visible ? 'rotate-180' : 'rotate-0',
]"
/>
</button>
</template>
<ul :class="['m-0 mt-3 list-none flex flex-col gap-2 p-0']">
<li
v-for="url in payload.urls"
:key="url"
:class="[
'rounded-lg',
'bg-neutral-100/80 dark:bg-neutral-800/80',
'px-3 py-2',
'font-mono text-xs text-neutral-700 dark:text-neutral-200',
'break-all',
]"
>
{{ url }}
</li>
</ul>
</Collapsible>
</div>
</div>
</div>
</Collapsible>
</template>
@@ -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<ElectronServerChannelConfig>('eventa:invoke:electron:server-channel:get-config')
export const electronApplyServerChannelConfig = defineInvokeEventa<ElectronServerChannelConfig, Partial<ElectronServerChannelConfig>>('eventa:invoke:electron:server-channel:apply-config')
export const electronGetServerChannelQrPayload = defineInvokeEventa<ServerChannelQrPayload>('eventa:invoke:electron:server-channel:get-qr-payload')
export const electronPluginList = defineInvokeEventa<PluginRegistrySnapshot>('eventa:invoke:electron:plugins:list')
export const electronPluginSetEnabled = defineInvokeEventa<PluginRegistrySnapshot, { name: string, enabled: boolean, path?: string }>('eventa:invoke:electron:plugins:set-enabled')
+71 -22
View File
@@ -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</a>.
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:
+100 -76
View File
@@ -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 服务器时明确设置 <code>OLLAMA_HOST</code> 详情,请检查 <a href="https://airi.moeru.ai/docs/en/docs/manual/integrations/providers/chat/ollama">Ollama 集成指南</a>。
很抱歉,Ollama 连接检查失败。不要担心,如果是 Ollama,需要额外配置才能通过 Ollama。
您需要在启动 Olama 应用程序或 Ollama CLI 服务器时明确设置
<code>OLLAMA_HOST</code> 详情,请检查 <a
href="https://airi.moeru.ai/docs/en/docs/manual/integrations/providers/chat/ollama">Ollama
集成指南</a>。
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: 此功能正在开发中,尚未公开发布。请在未来的更新中再次查看此功能。
+2
View File
@@ -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": {
@@ -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<typeof ServerChannelQrPayloadSchema>
export type ServerChannelQrPayload = InferOutput<typeof ServerChannelQrPayloadSchema>
export function createServerChannelQrPayload(payload: ServerChannelQrPayloadInput) {
return parse(ServerChannelQrPayloadSchema, payload)
}
export function parseServerChannelQrPayload(raw: string) {
return parse(ServerChannelQrPayloadSchema, JSON.parse(raw))
}
@@ -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')
})
})
@@ -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<keyof WebSocketEvents, WebSocketBaseEvent<any, any>>()
@@ -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,
+35
View File
@@ -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
+2
View File
@@ -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