refactor(stage-ui): move auth lib to stage-ui

This commit is contained in:
RainbowBird
2026-01-05 17:40:37 +08:00
parent 39759337b7
commit fe1c576994
16 changed files with 63 additions and 50 deletions
+2 -1
View File
@@ -55,7 +55,8 @@
"unspeech": "catalog:xsai",
"vue": "^3.5.25",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.4"
"vue-router": "^4.6.4",
"vue-sonner": "catalog:"
},
"devDependencies": {
"@nekopaw/tempora": "catalog:",
@@ -1,13 +1,12 @@
<script setup lang="ts">
import { listSessions, signOut } from '@proj-airi/stage-ui/libs/auth'
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
import { onClickOutside } from '@vueuse/core'
import { storeToRefs } from 'pinia'
import { computed, ref } from 'vue'
import { RouterLink } from 'vue-router'
import { toast } from 'vue-sonner'
import { listSessions, signOut } from '../../libs/auth'
import { useAuthStore } from '../../stores/auth'
const authStore = useAuthStore()
const { isAuthenticated, user } = storeToRefs(authStore)
+3
View File
@@ -102,11 +102,13 @@
"@xsai/tool": "catalog:",
"@xsai/utils-chat": "catalog:",
"animejs": "^4.2.2",
"better-auth": "catalog:",
"culori": "^4.0.2",
"date-fns": "^4.1.0",
"dompurify": "^3.3.1",
"es-toolkit": "catalog:",
"gpuu": "^1.0.6",
"hono": "catalog:",
"html2canvas": "^1.4.1",
"jszip": "^3.10.1",
"localforage": "^1.10.0",
@@ -132,6 +134,7 @@
"unist-builder": "^4.0.0",
"unspeech": "catalog:xsai",
"uuid": "^13.0.0",
"valibot": "catalog:",
"vaul-vue": "^0.4.1",
"vue-i18n": "^11.2.2",
"vue-router": "^4.6.4",
+21
View File
@@ -0,0 +1,21 @@
import type { AppType } from '../../../../apps/server/src/app'
import { useAuthStore } from '@proj-airi/stage-ui/stores/auth'
import { hc } from 'hono/client'
import { SERVER_URL } from '../libs/auth'
export const client = hc<AppType>(SERVER_URL, {
fetch: (input: RequestInfo | URL, init?: RequestInit) => {
const authStore = useAuthStore()
const headers = new Headers(init?.headers)
if (authStore.authToken) {
headers.set('Authorization', `Bearer ${authStore.authToken}`)
}
return fetch(input, {
...init,
headers,
credentials: 'include', // Send cookies with request (for sessions, etc)
})
},
})
+48
View File
@@ -0,0 +1,48 @@
import { createAuthClient } from 'better-auth/vue'
import { useAuthStore } from '../stores/auth'
export const SERVER_URL = import.meta.env.VITE_SERVER_URL || 'https://airi-api.moeru.ai'
const authStore = useAuthStore()
export const authClient = createAuthClient({
baseURL: SERVER_URL,
credentials: 'include',
fetchOptions: {
auth: {
type: 'Bearer',
token: () => authStore.authToken,
},
onSuccess: (ctx) => {
const newToken = ctx.response.headers.get('set-auth-token')
if (newToken) {
authStore.authToken = newToken
}
},
},
})
export async function fetchSession() {
const { data } = await authClient.getSession()
if (data) {
authStore.user = data.user
authStore.session = data.session
return true
}
return false
}
export async function listSessions() {
return await authClient.listSessions()
}
export async function signOut() {
await authClient.signOut()
authStore.user = undefined
authStore.session = undefined
authStore.authToken = ''
}
+21
View File
@@ -0,0 +1,21 @@
import type { Session, User } from 'better-auth'
import { useLocalStorage } from '@vueuse/core'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
export const useAuthStore = defineStore('auth', () => {
const authToken = useLocalStorage('auth/token', '')
const user = ref<User>()
const session = ref<Session>()
const isAuthenticated = computed(() => !!user.value && !!session.value)
// TODO: include fetchSession here for pulling and updating better-auth session with initialize(...) action
return {
authToken,
user,
session,
isAuthenticated,
}
})
+147
View File
@@ -0,0 +1,147 @@
import type { Character, CreateCharacterPayload, UpdateCharacterPayload } from '../types/character'
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { client } from '../composables/api'
export const useCharacterStore = defineStore('characters', () => {
const characters = ref<Map<string, Character>>(new Map())
const isLoading = ref(false)
const error = ref<unknown>(null)
async function fetchList() {
isLoading.value = true
error.value = null
try {
const res = await client.api.characters.$get()
if (!res.ok) {
throw new Error('Failed to fetch characters')
}
const data = await res.json()
characters.value.clear()
for (const char of data) {
characters.value.set(char.id, char as Character)
}
}
catch (err) {
error.value = err
throw err
}
finally {
isLoading.value = false
}
}
async function fetchById(id: string) {
isLoading.value = true
error.value = null
try {
const res = await client.api.characters[':id'].$get({
param: { id },
})
if (!res.ok) {
throw new Error('Failed to fetch character')
}
const data = await res.json()
characters.value.set(data.id, data as Character)
return data
}
catch (err) {
error.value = err
throw err
}
finally {
isLoading.value = false
}
}
async function create(payload: CreateCharacterPayload) {
isLoading.value = true
error.value = null
try {
const res = await client.api.characters.$post({
json: payload,
})
if (!res.ok) {
throw new Error('Failed to create character')
}
const data = await res.json()
characters.value.set(data.id, data as Character)
return data
}
catch (err) {
error.value = err
throw err
}
finally {
isLoading.value = false
}
}
async function update(id: string, payload: UpdateCharacterPayload) {
isLoading.value = true
error.value = null
try {
const res = await client.api.characters[':id'].$patch({
param: { id },
json: payload,
})
if (!res.ok) {
throw new Error('Failed to update character')
}
const data = await res.json()
characters.value.set(data.id, data as Character)
return data
}
catch (err) {
error.value = err
throw err
}
finally {
isLoading.value = false
}
}
async function remove(id: string) {
isLoading.value = true
error.value = null
try {
const res = await client.api.characters[':id'].$delete({
param: { id },
})
if (!res.ok) {
throw new Error('Failed to delete character')
}
characters.value.delete(id)
}
catch (err) {
error.value = err
throw err
}
finally {
isLoading.value = false
}
}
function getCharacter(id: string) {
return characters.value.get(id)
}
return {
characters,
isLoading,
error,
fetchList,
fetchById,
create,
update,
remove,
getCharacter,
}
})
+168
View File
@@ -0,0 +1,168 @@
import type { InferOutput } from 'valibot'
import { array, date, literal, number, object, optional, pipe, string, transform, union } from 'valibot'
// --- Enums & Configs ---
export const AvatarModelConfigSchema = object({
vrm: optional(object({
urls: array(string()),
})),
live2d: optional(object({
urls: array(string()),
})),
})
export const CharacterCapabilityConfigSchema = object({
apiKey: string(),
apiBaseUrl: string(),
llm: optional(object({
temperature: number(),
model: string(),
})),
tts: optional(object({
ssml: string(),
voiceId: string(),
speed: number(),
pitch: number(),
})),
vlm: optional(object({
image: string(),
})),
asr: optional(object({
audio: string(),
})),
})
const CharacterCapabilityTypeSchema = union([
literal('llm'),
literal('tts'),
literal('vlm'),
literal('asr'),
])
const AvatarModelTypeSchema = union([
literal('vrm'),
literal('live2d'),
])
const PromptTypeSchema = union([
literal('system'),
literal('personality'),
literal('greetings'),
])
const DateSchema = pipe(
union([string(), date()]),
transform(v => new Date(v)),
)
// --- Base Entities (mimicking database tables) ---
export const CharacterBaseSchema = object({
id: string(),
version: string(),
coverUrl: string(),
creatorId: string(),
ownerId: string(),
characterId: string(),
createdAt: DateSchema,
updatedAt: DateSchema,
})
export const CharacterCapabilitySchema = object({
id: string(),
characterId: string(),
type: CharacterCapabilityTypeSchema,
config: CharacterCapabilityConfigSchema,
})
export const AvatarModelSchema = object({
id: string(),
characterId: string(),
name: string(),
type: AvatarModelTypeSchema,
description: string(),
config: AvatarModelConfigSchema,
createdAt: DateSchema,
updatedAt: DateSchema,
})
export const CharacterI18nSchema = object({
id: string(),
characterId: string(),
language: string(),
name: string(),
description: string(),
tags: array(string()),
createdAt: DateSchema,
updatedAt: DateSchema,
})
export const CharacterPromptSchema = object({
id: string(),
characterId: string(),
language: string(),
type: PromptTypeSchema,
content: string(),
})
// --- Aggregated Character (with relations) ---
export const CharacterWithRelationsSchema = object({
...CharacterBaseSchema.entries,
capabilities: array(CharacterCapabilitySchema),
avatarModels: array(AvatarModelSchema),
i18n: array(CharacterI18nSchema),
prompts: array(CharacterPromptSchema),
})
// --- API Request Schemas ---
export const CreateCharacterSchema = object({
character: object({
version: string(),
coverUrl: string(),
characterId: string(),
// creatorId & ownerId are handled by server
}),
capabilities: optional(array(object({
type: CharacterCapabilityTypeSchema,
config: CharacterCapabilityConfigSchema,
}))),
avatarModels: optional(array(object({
name: string(),
type: AvatarModelTypeSchema,
description: string(),
config: AvatarModelConfigSchema,
}))),
i18n: optional(array(object({
language: string(),
name: string(),
description: string(),
tags: array(string()),
}))),
prompts: optional(array(object({
language: string(),
type: PromptTypeSchema,
content: string(),
}))),
})
export const UpdateCharacterSchema = object({
version: optional(string()),
coverUrl: optional(string()),
characterId: optional(string()),
})
// --- Type Exports ---
export type Character = InferOutput<typeof CharacterWithRelationsSchema>
export type CharacterBase = InferOutput<typeof CharacterBaseSchema>
export type CharacterCapability = InferOutput<typeof CharacterCapabilitySchema>
export type AvatarModel = InferOutput<typeof AvatarModelSchema>
export type CharacterI18n = InferOutput<typeof CharacterI18nSchema>
export type CharacterPrompt = InferOutput<typeof CharacterPromptSchema>
export type CreateCharacterPayload = InferOutput<typeof CreateCharacterSchema>
export type UpdateCharacterPayload = InferOutput<typeof UpdateCharacterSchema>