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

This commit is contained in:
Disy committed 2026-09-16 17:01:22 +08:00
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', {
+6
View File
@@ -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,