fix: 完善整合包同步与启动器交互
Axolotl desktop CI / guardrails (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
Axolotl desktop CI / guardrails (push) Has been cancelled
Repository checks / typos (push) Has been cancelled
Repository checks / tombi (push) Has been cancelled
Rust checks / shear (push) Has been cancelled
Sync source to CNB / Sync Git ref (push) Has been cancelled
Axolotl desktop CI / desktop (macos-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (ubuntu-latest) (push) Has been cancelled
Axolotl desktop CI / desktop (windows-latest) (push) Has been cancelled
Axolotl desktop CI / website (push) Has been cancelled
Sync LobeHub models / sync (push) Has been cancelled
This commit is contained in:
1 parent
bd09fed0ed
commit
51fb0c30f7
45 files changed
+1804
-219
No files matched your search
@@ -3,6 +3,7 @@ import {
|
||||
requestSkinSiteDownloadToken,
|
||||
skinSiteStatus,
|
||||
skinSiteUser,
|
||||
waitForSkinSiteSession,
|
||||
} from '../composables/skin-site-session.ts'
|
||||
|
||||
let sessionUpdate: Promise<void> = Promise.resolve()
|
||||
@@ -28,6 +29,7 @@ export function clearHostedSession(): Promise<void> {
|
||||
}
|
||||
|
||||
export async function prepareHostedSession(): Promise<void> {
|
||||
await waitForSkinSiteSession()
|
||||
const userId = skinSiteUser.value?.uuid
|
||||
const token = await requestSkinSiteDownloadToken()
|
||||
const update = sessionUpdate
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import test from 'node:test'
|
||||
import { useNetworkStatus } from '../composables/useNetworkStatus.ts'
|
||||
import {
|
||||
setSkinSiteFrame,
|
||||
resetSkinSiteSession,
|
||||
receiveSkinSiteMessage,
|
||||
SKIN_SITE_ORIGIN,
|
||||
} from '../composables/skin-site-session.ts'
|
||||
import {
|
||||
prepareInstancePlayer,
|
||||
registerInstancePlayerPicker,
|
||||
saveInstancePlayer,
|
||||
onInstancePlayerChanged,
|
||||
type InstancePlayer,
|
||||
} from './instance-player.ts'
|
||||
|
||||
test('instance player selection persists independently from the globally selected account', async () => {
|
||||
const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
|
||||
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { onLine: true } })
|
||||
useNetworkStatus().refreshBrowserOffline()
|
||||
const saved: InstancePlayer = { id: 'saved', name: 'Saved', account_type: 'microsoft' }
|
||||
let binding: InstancePlayer | null = saved
|
||||
let accounts = [
|
||||
{ profile: { id: 'other', name: 'Other' }, account_type: 'microsoft' },
|
||||
{ profile: { id: 'saved', name: 'Saved' }, account_type: 'microsoft' },
|
||||
]
|
||||
const previous = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
__TAURI_INTERNALS__: {
|
||||
invoke: async (command: string) => {
|
||||
if (command === 'plugin:auth|get_instance_player') return binding
|
||||
if (command === 'plugin:auth|get_users') return accounts
|
||||
throw new Error(`Unexpected command ${command}`)
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
const requests: Array<InstancePlayer | null> = []
|
||||
const unregister = registerInstancePlayerPicker(async (_id, locked) => {
|
||||
requests.push(locked)
|
||||
binding = saved
|
||||
return saved
|
||||
})
|
||||
try {
|
||||
setSkinSiteFrame({ postMessage() {} } as unknown as Window)
|
||||
await prepareInstancePlayer('instance')
|
||||
assert.equal(requests.length, 0)
|
||||
setSkinSiteFrame(null)
|
||||
binding = null
|
||||
await Promise.all([prepareInstancePlayer('instance'), prepareInstancePlayer('instance')])
|
||||
assert.deepEqual(requests, [null])
|
||||
await prepareInstancePlayer('instance')
|
||||
assert.equal(requests.length, 1)
|
||||
accounts = accounts.filter((account) => account.profile.id !== saved.id)
|
||||
await prepareInstancePlayer('instance')
|
||||
assert.equal(
|
||||
requests[1],
|
||||
saved,
|
||||
'Missing saved player must remain locked, not fall back to the other account',
|
||||
)
|
||||
} finally {
|
||||
unregister()
|
||||
if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator)
|
||||
else Reflect.deleteProperty(globalThis, 'navigator')
|
||||
if (previous) Object.defineProperty(globalThis, 'window', previous)
|
||||
else Reflect.deleteProperty(globalThis, 'window')
|
||||
}
|
||||
})
|
||||
|
||||
test('signed-in skin site renews the saved player without prompting and only notifies after saving', async () => {
|
||||
const previous = Object.getOwnPropertyDescriptor(globalThis, 'window')
|
||||
const previousNavigator = Object.getOwnPropertyDescriptor(globalThis, 'navigator')
|
||||
Object.defineProperty(globalThis, 'navigator', { configurable: true, value: { onLine: true } })
|
||||
useNetworkStatus().refreshBrowserOffline()
|
||||
const player: InstancePlayer = {
|
||||
id: '12345678-1234-1234-1234-123456789abc',
|
||||
name: 'Skin',
|
||||
account_type: 'yggdrasil',
|
||||
skin_site_user: 'owner',
|
||||
}
|
||||
const calls: string[] = []
|
||||
let failSave = false
|
||||
const frame = {
|
||||
postMessage(data: { type: string; requestId: string }) {
|
||||
if (data.type === 'starlight-pack-token-request')
|
||||
receiveSkinSiteMessage(
|
||||
{
|
||||
origin: SKIN_SITE_ORIGIN,
|
||||
source: frame,
|
||||
data: {
|
||||
type: 'starlight-pack-token-result',
|
||||
requestId: data.requestId,
|
||||
token: 'fixture.jwt',
|
||||
},
|
||||
} as unknown as MessageEvent,
|
||||
frame,
|
||||
)
|
||||
},
|
||||
} as unknown as Window
|
||||
Object.defineProperty(globalThis, 'window', {
|
||||
configurable: true,
|
||||
value: {
|
||||
__TAURI_INTERNALS__: {
|
||||
async invoke(command: string, args: Record<string, unknown>) {
|
||||
calls.push(command)
|
||||
if (command.endsWith('get_instance_player')) return player
|
||||
if (command.endsWith('login_skin_site_player')) {
|
||||
assert.equal(args.playerId, player.id)
|
||||
assert.equal(args.userId, 'owner')
|
||||
assert.equal(args.token, 'fixture.jwt')
|
||||
} else if (command.endsWith('set_instance_player') && failSave)
|
||||
throw new Error('Save failed')
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
let prompts = 0
|
||||
const unregister = registerInstancePlayerPicker(async (_id, locked) => {
|
||||
prompts++
|
||||
assert.equal(locked, player)
|
||||
return player
|
||||
})
|
||||
const changes: string[] = []
|
||||
const stop = onInstancePlayerChanged((id) => changes.push(id))
|
||||
try {
|
||||
resetSkinSiteSession()
|
||||
setSkinSiteFrame(frame)
|
||||
receiveSkinSiteMessage(
|
||||
{
|
||||
origin: SKIN_SITE_ORIGIN,
|
||||
source: frame,
|
||||
data: {
|
||||
type: 'starlight-skin-session',
|
||||
status: 'signed-in',
|
||||
user: { uuid: 'owner', username: 'Owner' },
|
||||
},
|
||||
} as unknown as MessageEvent,
|
||||
frame,
|
||||
)
|
||||
await prepareInstancePlayer('one')
|
||||
assert.equal(prompts, 0)
|
||||
assert.ok(calls.includes('plugin:auth|login_skin_site_player'))
|
||||
await saveInstancePlayer('one', player)
|
||||
assert.deepEqual(changes, ['one'])
|
||||
failSave = true
|
||||
await assert.rejects(saveInstancePlayer('two', player), /Save failed/)
|
||||
assert.deepEqual(changes, ['one'])
|
||||
resetSkinSiteSession()
|
||||
await prepareInstancePlayer('one')
|
||||
assert.equal(prompts, 1)
|
||||
} finally {
|
||||
stop()
|
||||
unregister()
|
||||
resetSkinSiteSession()
|
||||
setSkinSiteFrame(null)
|
||||
if (previous) Object.defineProperty(globalThis, 'window', previous)
|
||||
else Reflect.deleteProperty(globalThis, 'window')
|
||||
if (previousNavigator) Object.defineProperty(globalThis, 'navigator', previousNavigator)
|
||||
else Reflect.deleteProperty(globalThis, 'navigator')
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,104 @@
|
||||
import { invoke } from '@tauri-apps/api/core'
|
||||
|
||||
import {
|
||||
requestSkinSiteDownloadToken,
|
||||
skinSiteStatus,
|
||||
skinSiteUser,
|
||||
waitForSkinSiteSession,
|
||||
} from '../composables/skin-site-session.ts'
|
||||
export { waitForSkinSiteSession } from '../composables/skin-site-session.ts'
|
||||
import { users } from './auth.js'
|
||||
import { isOfflineMode } from '../composables/useNetworkStatus.ts'
|
||||
|
||||
export type InstancePlayer = {
|
||||
id: string
|
||||
name: string
|
||||
account_type: 'microsoft' | 'yggdrasil' | 'offline'
|
||||
skin_site_user?: string | null
|
||||
}
|
||||
export type PlayerChoice = InstancePlayer & { head?: string }
|
||||
type Picker = (instanceId: string, locked: InstancePlayer | null) => Promise<InstancePlayer>
|
||||
let picker: Picker | undefined
|
||||
const preparing = new Map<string, Promise<void>>()
|
||||
const listeners = new Set<(instanceId: string, player: InstancePlayer) => void>()
|
||||
|
||||
export function onInstancePlayerChanged(
|
||||
listener: (instanceId: string, player: InstancePlayer) => void,
|
||||
) {
|
||||
listeners.add(listener)
|
||||
return () => {
|
||||
listeners.delete(listener)
|
||||
}
|
||||
}
|
||||
|
||||
export function registerInstancePlayerPicker(value: Picker) {
|
||||
picker = value
|
||||
return () => {
|
||||
if (picker === value) picker = undefined
|
||||
}
|
||||
}
|
||||
export const getInstancePlayer = (instanceId: string) =>
|
||||
invoke<InstancePlayer | null>('plugin:auth|get_instance_player', { instanceId })
|
||||
|
||||
export async function authenticateInstancePlayer(player: InstancePlayer) {
|
||||
if (!player.skin_site_user) return
|
||||
if (skinSiteStatus.value !== 'signed-in' || skinSiteUser.value?.uuid !== player.skin_site_user) {
|
||||
throw new Error(`请登录实例玩家 ${player.name} 所属的皮肤站账号。`)
|
||||
}
|
||||
const token = await requestSkinSiteDownloadToken()
|
||||
await invoke('plugin:auth|login_skin_site_player', {
|
||||
token,
|
||||
playerId: player.id,
|
||||
userId: player.skin_site_user,
|
||||
})
|
||||
if (skinSiteUser.value?.uuid !== player.skin_site_user || skinSiteStatus.value !== 'signed-in') {
|
||||
throw new Error('皮肤站账号已变化,请重试。')
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveInstancePlayer(instanceId: string, player: InstancePlayer) {
|
||||
await authenticateInstancePlayer(player)
|
||||
await invoke('plugin:auth|set_instance_player', { instanceId, player })
|
||||
for (const listener of listeners) listener(instanceId, player)
|
||||
}
|
||||
|
||||
export async function chooseInstancePlayer(
|
||||
instanceId: string,
|
||||
locked: InstancePlayer | null = null,
|
||||
) {
|
||||
if (!picker) throw new Error('玩家选择界面尚未就绪,请稍后重试。')
|
||||
return picker(instanceId, locked)
|
||||
}
|
||||
|
||||
export function prepareInstancePlayer(instanceId: string): Promise<void> {
|
||||
if (isOfflineMode()) return Promise.resolve()
|
||||
const current = preparing.get(instanceId)
|
||||
if (current) return current
|
||||
const task = (async () => {
|
||||
const saved = await getInstancePlayer(instanceId)
|
||||
if (saved && !saved.skin_site_user) {
|
||||
const available = await users()
|
||||
if (
|
||||
available.some(
|
||||
(account) =>
|
||||
account.profile.id === saved.id && account.account_type === saved.account_type,
|
||||
)
|
||||
)
|
||||
return
|
||||
}
|
||||
if (saved?.skin_site_user) await waitForSkinSiteSession()
|
||||
if (
|
||||
saved?.skin_site_user &&
|
||||
skinSiteUser.value?.uuid === saved.skin_site_user &&
|
||||
skinSiteStatus.value === 'signed-in'
|
||||
) {
|
||||
await authenticateInstancePlayer(saved)
|
||||
return
|
||||
}
|
||||
await chooseInstancePlayer(instanceId, saved)
|
||||
})().finally(() => {
|
||||
preparing.delete(instanceId)
|
||||
})
|
||||
preparing.set(instanceId, task)
|
||||
return task
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import { detectGcStrategy, GC_STRATEGY_DEFINITIONS } from '@/helpers/gc/strategi
|
||||
import type { GcContext, ResolvedGcStrategyId } from '@/helpers/gc/types'
|
||||
import { setLastGcLaunchReport } from '@/helpers/gc-notice'
|
||||
import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs'
|
||||
import { prepareInstancePlayer } from '@/helpers/instance-player'
|
||||
import { AUTO_GC_PRESET_ARG } from '@/helpers/java-arguments'
|
||||
import { get_jre, get_memory_status } from '@/helpers/jre.js'
|
||||
import { get as getSettings } from '@/helpers/settings'
|
||||
@@ -1074,6 +1075,7 @@ export async function run(
|
||||
instanceId: string,
|
||||
serverAddress: string | null = null,
|
||||
): Promise<InstanceRunResult> {
|
||||
await prepareInstancePlayer(instanceId)
|
||||
if (await getInstanceMode(instanceId) === 'starlight') await prepareHostedSession()
|
||||
const { args, gcIntent } = await resolveGcLaunchIntent(instanceId)
|
||||
const result = await invoke<InstanceRunResult>('plugin:instance|instance_run', {
|
||||
|
||||
@@ -4,6 +4,8 @@ import { invoke } from '@tauri-apps/api/core'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
import { isOfflineMode } from '@/composables/useNetworkStatus'
|
||||
import { prepareInstancePlayer } from '@/helpers/instance-player'
|
||||
import { getInstanceMode, prepareHostedSession } from '@/helpers/hosted-packs'
|
||||
import { get_full_path, resolveGcLaunchIntent } from '@/helpers/instance'
|
||||
import { openPath } from '@/helpers/utils'
|
||||
|
||||
@@ -248,6 +250,8 @@ export async function start_join_singleplayer_world(
|
||||
instanceId: string,
|
||||
world: string,
|
||||
): Promise<unknown> {
|
||||
await prepareInstancePlayer(instanceId)
|
||||
if (await getInstanceMode(instanceId) === 'starlight') await prepareHostedSession()
|
||||
const { args, gcIntent } = await resolveGcLaunchIntent(instanceId)
|
||||
return await invoke('plugin:worlds|start_join_singleplayer_world', {
|
||||
instanceId,
|
||||
@@ -259,6 +263,8 @@ export async function start_join_singleplayer_world(
|
||||
}
|
||||
|
||||
export async function start_join_server(instanceId: string, address: string): Promise<unknown> {
|
||||
await prepareInstancePlayer(instanceId)
|
||||
if (await getInstanceMode(instanceId) === 'starlight') await prepareHostedSession()
|
||||
const { args, gcIntent } = await resolveGcLaunchIntent(instanceId)
|
||||
return await invoke('plugin:worlds|start_join_server', {
|
||||
instanceId,
|
||||
|
||||
Reference in new issue
Block a user