fix(stage-ui): preserve character card runtime data (#2111)
This commit is contained in:
+26
-36
@@ -5,7 +5,7 @@ import type { AiriExtension } from '@proj-airi/stage-ui/stores/modules/airi-card
|
||||
import { isCustomProvidersDisabled } from '@proj-airi/stage-shared'
|
||||
import { useAnalytics } from '@proj-airi/stage-ui/composables'
|
||||
import { DEFAULT_ARTISTRY_WIDGET_INSTRUCTION } from '@proj-airi/stage-ui/constants/prompts/artistry-instruction'
|
||||
import { safeParseAiriCardDraft } from '@proj-airi/stage-ui/services/airi-card-editor'
|
||||
import { applyAiriCardEditorModules, safeParseAiriCardDraft } from '@proj-airi/stage-ui/services/airi-card-editor'
|
||||
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
|
||||
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
|
||||
import { useArtistryStore } from '@proj-airi/stage-ui/stores/modules/artistry'
|
||||
@@ -315,42 +315,32 @@ function saveCard(card: Card, activate: boolean): boolean {
|
||||
showError.value = false
|
||||
const { card: rawCard, artistryOptions } = draftResult.output
|
||||
|
||||
// Build card with modules extension
|
||||
const cardWithModules = {
|
||||
...rawCard,
|
||||
extensions: {
|
||||
...rawCard.extensions,
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: {
|
||||
provider: selectedConsciousnessProvider.value || consciousnessProvider.value,
|
||||
model: selectedConsciousnessModel.value || defaultConsciousnessModel.value,
|
||||
},
|
||||
vision: {
|
||||
provider: selectedVisionProvider.value || visionProvider.value,
|
||||
model: selectedVisionModel.value || defaultVisionModel.value,
|
||||
},
|
||||
speech: {
|
||||
provider: selectedSpeechProvider.value || speechProvider.value,
|
||||
model: selectedSpeechModel.value || defaultSpeechModel.value,
|
||||
voice_id: selectedSpeechVoiceId.value || defaultSpeechVoiceId.value,
|
||||
},
|
||||
displayModelId: selectedDisplayModelId.value || defaultDisplayModelId.value,
|
||||
artistry: {
|
||||
provider: selectedArtistryProvider.value || defaultArtistryProvider.value,
|
||||
model: selectedArtistryModel.value,
|
||||
promptPrefix: selectedArtistryPromptPrefix.value,
|
||||
widgetInstruction: selectedArtistryWidgetInstruction.value,
|
||||
spawnMode: selectedArtistrySpawnMode.value,
|
||||
options: artistryOptions,
|
||||
autonomousEnabled: selectedArtistryAutonomousEnabled.value,
|
||||
autonomousThreshold: selectedArtistryAutonomousThreshold.value,
|
||||
},
|
||||
},
|
||||
agents: {},
|
||||
} as AiriExtension,
|
||||
const cardWithModules = applyAiriCardEditorModules(rawCard, {
|
||||
consciousness: {
|
||||
provider: selectedConsciousnessProvider.value || consciousnessProvider.value,
|
||||
model: selectedConsciousnessModel.value || defaultConsciousnessModel.value,
|
||||
},
|
||||
}
|
||||
vision: {
|
||||
provider: selectedVisionProvider.value || visionProvider.value,
|
||||
model: selectedVisionModel.value || defaultVisionModel.value,
|
||||
},
|
||||
speech: {
|
||||
provider: selectedSpeechProvider.value || speechProvider.value,
|
||||
model: selectedSpeechModel.value || defaultSpeechModel.value,
|
||||
voice_id: selectedSpeechVoiceId.value || defaultSpeechVoiceId.value,
|
||||
},
|
||||
displayModelId: selectedDisplayModelId.value || defaultDisplayModelId.value,
|
||||
artistry: {
|
||||
provider: selectedArtistryProvider.value || defaultArtistryProvider.value,
|
||||
model: selectedArtistryModel.value,
|
||||
promptPrefix: selectedArtistryPromptPrefix.value,
|
||||
widgetInstruction: selectedArtistryWidgetInstruction.value,
|
||||
spawnMode: selectedArtistrySpawnMode.value,
|
||||
options: artistryOptions,
|
||||
autonomousEnabled: selectedArtistryAutonomousEnabled.value,
|
||||
autonomousThreshold: selectedArtistryAutonomousThreshold.value,
|
||||
},
|
||||
})
|
||||
let savedCardId: string
|
||||
if (isEditMode.value && props.cardId) {
|
||||
// Edit mode: update existing card
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { Card } from '@proj-airi/ccc'
|
||||
|
||||
import type { AiriExtension } from '../types/airiCard'
|
||||
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { safeParseAiriCardDraft } from './airi-card-editor'
|
||||
import { applyAiriCardEditorModules, safeParseAiriCardDraft } from './airi-card-editor'
|
||||
|
||||
describe('airi card editor validation', () => {
|
||||
// https://github.com/moeru-ai/airi/issues/2108
|
||||
@@ -67,6 +69,107 @@ describe('airi card editor validation', () => {
|
||||
|
||||
expect(result.output.artistryOptions).toBeUndefined()
|
||||
})
|
||||
|
||||
it('preserves AIRI extension fields that are not editable in the form', () => {
|
||||
// ROOT CAUSE:
|
||||
//
|
||||
// Saving the editor rebuilt `extensions.airi.modules` from visible form
|
||||
// controls and reset `agents` to an empty object. Hidden settings such as
|
||||
// body models, backgrounds, advanced speech/artistry fields, and agent
|
||||
// prompts were therefore lost after an otherwise unrelated edit.
|
||||
//
|
||||
// We fix this by applying the editor-owned fields as a structured patch
|
||||
// over the existing extension.
|
||||
const existing: AiriExtension = {
|
||||
modules: {
|
||||
consciousness: { provider: 'old-chat', model: 'old-chat-model' },
|
||||
vision: { provider: 'old-vision', model: 'old-vision-model' },
|
||||
speech: {
|
||||
provider: 'old-speech',
|
||||
model: 'old-speech-model',
|
||||
voice_id: 'old-voice',
|
||||
pitch: 1.2,
|
||||
rate: 0.9,
|
||||
ssml: true,
|
||||
language: 'ja',
|
||||
},
|
||||
vrm: { source: 'url', url: 'https://example.com/avatar.vrm' },
|
||||
live2d: { source: 'file', file: 'models/avatar.model3.json' },
|
||||
displayModelId: 'old-display-model',
|
||||
activeBackgroundId: 'background-1',
|
||||
artistry: {
|
||||
enabled: true,
|
||||
provider: 'old-artistry',
|
||||
model: 'old-artistry-model',
|
||||
workflowId: 'workflow-1',
|
||||
autonomousTarget: 'user',
|
||||
},
|
||||
},
|
||||
agents: {
|
||||
minecraft: { prompt: 'Keep building.', enabled: true },
|
||||
},
|
||||
}
|
||||
|
||||
const result = applyAiriCardEditorModules({
|
||||
...createCard(),
|
||||
extensions: {
|
||||
thirdParty: { keep: true },
|
||||
airi: {
|
||||
...existing,
|
||||
futureRootField: { keep: true },
|
||||
},
|
||||
},
|
||||
}, {
|
||||
consciousness: { provider: 'new-chat', model: 'new-chat-model' },
|
||||
vision: { provider: 'new-vision', model: 'new-vision-model' },
|
||||
speech: { provider: 'new-speech', model: 'new-speech-model', voice_id: 'new-voice' },
|
||||
displayModelId: 'new-display-model',
|
||||
artistry: {
|
||||
provider: 'new-artistry',
|
||||
model: 'new-artistry-model',
|
||||
promptPrefix: 'portrait',
|
||||
widgetInstruction: 'Use the image widget.',
|
||||
spawnMode: 'widget',
|
||||
options: { steps: 12 },
|
||||
autonomousEnabled: true,
|
||||
autonomousThreshold: 80,
|
||||
},
|
||||
})
|
||||
|
||||
const extension = result.extensions.airi
|
||||
|
||||
expect(result.extensions.thirdParty).toEqual({ keep: true })
|
||||
expect(extension).toHaveProperty('futureRootField', { keep: true })
|
||||
expect(extension.modules.consciousness).toEqual({ provider: 'new-chat', model: 'new-chat-model' })
|
||||
expect(extension.modules.vision).toEqual({ provider: 'new-vision', model: 'new-vision-model' })
|
||||
expect(extension.modules.speech).toEqual({
|
||||
provider: 'new-speech',
|
||||
model: 'new-speech-model',
|
||||
voice_id: 'new-voice',
|
||||
pitch: 1.2,
|
||||
rate: 0.9,
|
||||
ssml: true,
|
||||
language: 'ja',
|
||||
})
|
||||
expect(extension.modules.vrm).toEqual(existing.modules.vrm)
|
||||
expect(extension.modules.live2d).toEqual(existing.modules.live2d)
|
||||
expect(extension.modules.displayModelId).toBe('new-display-model')
|
||||
expect(extension.modules.activeBackgroundId).toBe('background-1')
|
||||
expect(extension.modules.artistry).toEqual({
|
||||
enabled: true,
|
||||
provider: 'new-artistry',
|
||||
model: 'new-artistry-model',
|
||||
promptPrefix: 'portrait',
|
||||
workflowId: 'workflow-1',
|
||||
widgetInstruction: 'Use the image widget.',
|
||||
spawnMode: 'widget',
|
||||
options: { steps: 12 },
|
||||
autonomousEnabled: true,
|
||||
autonomousThreshold: 80,
|
||||
autonomousTarget: 'user',
|
||||
})
|
||||
expect(extension.agents).toEqual(existing.agents)
|
||||
})
|
||||
})
|
||||
|
||||
function createCard(): Card {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Card } from '@proj-airi/ccc'
|
||||
|
||||
import type { AiriExtension } from '../types/airiCard'
|
||||
|
||||
import {
|
||||
check,
|
||||
nonEmpty,
|
||||
@@ -16,6 +18,31 @@ import {
|
||||
|
||||
export type AiriCardDraftValidationError = 'name' | 'version' | 'invalid_artistry_json'
|
||||
|
||||
/** Module settings owned by the AIRI Card editor form. */
|
||||
interface AiriCardEditorModules {
|
||||
consciousness: AiriExtension['modules']['consciousness']
|
||||
vision: AiriExtension['modules']['vision']
|
||||
speech: Pick<AiriExtension['modules']['speech'], 'provider' | 'model' | 'voice_id'>
|
||||
displayModelId?: string
|
||||
artistry: Pick<
|
||||
NonNullable<AiriExtension['modules']['artistry']>,
|
||||
| 'provider'
|
||||
| 'model'
|
||||
| 'promptPrefix'
|
||||
| 'widgetInstruction'
|
||||
| 'spawnMode'
|
||||
| 'options'
|
||||
| 'autonomousEnabled'
|
||||
| 'autonomousThreshold'
|
||||
>
|
||||
}
|
||||
|
||||
type CardWithAiriExtension = Card & {
|
||||
extensions: NonNullable<Card['extensions']> & {
|
||||
airi: AiriExtension
|
||||
}
|
||||
}
|
||||
|
||||
export type AiriCardDraftValidationResult
|
||||
= | {
|
||||
success: true
|
||||
@@ -87,6 +114,51 @@ export function safeParseAiriCardDraft(card: Card, artistryOptionsJson: string):
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies editor-owned module fields to a complete character card.
|
||||
*
|
||||
* The returned card preserves extension fields the form cannot display,
|
||||
* including body models, backgrounds, advanced speech/artistry settings, and
|
||||
* agent configuration.
|
||||
*/
|
||||
export function applyAiriCardEditorModules(
|
||||
card: Card,
|
||||
edited: AiriCardEditorModules,
|
||||
): CardWithAiriExtension {
|
||||
const existing = isAiriExtension(card.extensions?.airi)
|
||||
? card.extensions.airi
|
||||
: undefined
|
||||
|
||||
return {
|
||||
...card,
|
||||
extensions: {
|
||||
...card.extensions,
|
||||
airi: {
|
||||
...existing,
|
||||
modules: {
|
||||
...existing?.modules,
|
||||
...edited,
|
||||
speech: {
|
||||
...existing?.modules.speech,
|
||||
...edited.speech,
|
||||
},
|
||||
artistry: {
|
||||
...existing?.modules.artistry,
|
||||
...edited.artistry,
|
||||
},
|
||||
},
|
||||
agents: existing?.agents ?? {},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function isAiriExtension(value: unknown): value is AiriExtension {
|
||||
return isRecord(value)
|
||||
&& isRecord(value.modules)
|
||||
&& isRecord(value.agents)
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import type { AiriCard, AiriExtension } from '../stores/modules/airi-card'
|
||||
import type { AiriCard, AiriExtension } from '../types/airiCard'
|
||||
|
||||
import JSZip from 'jszip'
|
||||
|
||||
@@ -17,7 +17,7 @@ describe('airi card package import/export', () => {
|
||||
vi.unstubAllGlobals()
|
||||
})
|
||||
|
||||
it('exports sanitized packages and restores display models', async () => {
|
||||
it('exports shareable fields, sanitizes runtime state, and restores display models', async () => {
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const fetch = vi.fn(async () => new Response('preset-vrm-model'))
|
||||
vi.stubGlobal('fetch', fetch)
|
||||
@@ -49,6 +49,32 @@ describe('airi card package import/export', () => {
|
||||
expect(airiFrom(imported).modules.displayModelId).toBe('display-model-imported')
|
||||
})
|
||||
|
||||
it('applies the share-field whitelist to externally edited package JSON', async () => {
|
||||
const displayModelsStore = useDisplayModelsStore()
|
||||
const source = exportToJSON(createCard('preset-live2d-1'))
|
||||
source.data.extensions.third_party = { token: 'do-not-import' }
|
||||
|
||||
const imported = await importAiriCardPackage({
|
||||
file: await packageFile(source),
|
||||
displayModelsStore,
|
||||
})
|
||||
const airi = airiFrom(imported)
|
||||
|
||||
expect(imported.data).toMatchObject({
|
||||
name: 'AIRI / Test Card',
|
||||
nickname: 'Tester',
|
||||
character_version: '1.2.3',
|
||||
description: 'Description',
|
||||
creator: '',
|
||||
tags: [],
|
||||
mes_example: '',
|
||||
})
|
||||
expect(imported.data.extensions).not.toHaveProperty('third_party')
|
||||
expect(airi.modules).not.toHaveProperty('activeBackgroundId')
|
||||
expect(airi.modules.artistry).not.toHaveProperty('workflowId')
|
||||
expect(airi.agents).toEqual({})
|
||||
})
|
||||
|
||||
it('classifies invalid packages', async () => {
|
||||
const emptyZip = new JSZip()
|
||||
const invalidJsonZip = new JSZip()
|
||||
|
||||
@@ -2,7 +2,7 @@ import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||
import type { GenericSchema, InferOutput } from 'valibot'
|
||||
|
||||
import type { DisplayModel, useDisplayModelsStore } from '../stores/display-models'
|
||||
import type { AiriCard, AiriExtension } from '../stores/modules/airi-card'
|
||||
import type { AiriCard, AiriExtension } from '../types/airiCard'
|
||||
|
||||
import JSZip from 'jszip'
|
||||
|
||||
@@ -22,7 +22,7 @@ const MODEL_EXT: Partial<Record<DisplayModelFormat, string>> = {
|
||||
}
|
||||
|
||||
type DisplayModelsStore = ReturnType<typeof useDisplayModelsStore>
|
||||
type ExportableCard = Card & { extensions: { airi: AiriExtension } }
|
||||
type ShareableAiriCard = Card & { extensions: { airi: AiriExtension } }
|
||||
|
||||
const manifestSchema = object({
|
||||
format: literal(FORMAT),
|
||||
@@ -68,7 +68,14 @@ export class AiriCardPackageError extends Error {
|
||||
}
|
||||
}
|
||||
|
||||
/** Exports only the creation/edit form whitelist; provider globals and runtime state are never cloned. */
|
||||
/**
|
||||
* Creates a portable AIRI Card share package.
|
||||
*
|
||||
* This format is intentionally not a lossless backup. It includes fields the
|
||||
* creation editor lets the sender review, a sanitized AIRI module subset, and
|
||||
* the selected display model. Unreviewed CCv3 metadata, custom extensions,
|
||||
* agent prompts, and machine-local runtime references are omitted.
|
||||
*/
|
||||
export async function exportAiriCardPackage({ card, displayModelsStore }: { card: AiriCard, displayModelsStore: DisplayModelsStore }): Promise<Blob> {
|
||||
const exportableCard = cardFromAiriCard(card)
|
||||
const displayModel = await exportDisplayModel(exportableCard, displayModelsStore)
|
||||
@@ -89,7 +96,13 @@ export async function exportAiriCardPackage({ card, displayModelsStore }: { card
|
||||
return zip.generateAsync({ type: 'blob' })
|
||||
}
|
||||
|
||||
/** Imports a package as sanitized CCv3 JSON; edited zip payloads cannot smuggle extra AIRI fields through. */
|
||||
/**
|
||||
* Imports a portable package through the same share-field whitelist.
|
||||
*
|
||||
* The returned CCv3 object is safe to pass through the normal card creation
|
||||
* path: package authors cannot smuggle custom extensions, agent prompts, or
|
||||
* machine-local references into persisted AIRI state.
|
||||
*/
|
||||
export async function importAiriCardPackage({ file, displayModelsStore }: { file: File, displayModelsStore: DisplayModelsStore }): Promise<ccv3.CharacterCardV3> {
|
||||
const zip = await loadZip(file)
|
||||
const manifest = await readJsonFile(zip, MANIFEST_PATH, manifestSchema)
|
||||
@@ -99,7 +112,7 @@ export async function importAiriCardPackage({ file, displayModelsStore }: { file
|
||||
return exportToJSON(cardFromCharacterCard(cardJson, displayModelId))
|
||||
}
|
||||
|
||||
async function exportDisplayModel(card: ExportableCard, store: DisplayModelsStore) {
|
||||
async function exportDisplayModel(card: ShareableAiriCard, store: DisplayModelsStore) {
|
||||
const displayModelId = card.extensions.airi.modules.displayModelId
|
||||
if (!displayModelId)
|
||||
return
|
||||
@@ -167,7 +180,7 @@ async function readJsonFile<S extends GenericSchema>(zip: JSZip, path: string, s
|
||||
}
|
||||
}
|
||||
|
||||
function cardFromAiriCard(card: AiriCard): ExportableCard {
|
||||
function cardFromAiriCard(card: AiriCard): ShareableAiriCard {
|
||||
return {
|
||||
name: card.name,
|
||||
nickname: card.nickname,
|
||||
@@ -183,7 +196,7 @@ function cardFromAiriCard(card: AiriCard): ExportableCard {
|
||||
}
|
||||
}
|
||||
|
||||
function cardFromCharacterCard(card: CharacterCardPackageJson, displayModelId?: string): ExportableCard {
|
||||
function cardFromCharacterCard(card: CharacterCardPackageJson, displayModelId?: string): ShareableAiriCard {
|
||||
const data = card.data
|
||||
return {
|
||||
name: data.name,
|
||||
|
||||
@@ -242,6 +242,49 @@ describe('airi-card store', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps position-sensitive CCv3 fields separate from the stable system prompt', () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
|
||||
const cardId = cardStore.addCard({
|
||||
name: 'Runtime context card',
|
||||
version: '1.0.0',
|
||||
systemPrompt: 'Follow the character rules.',
|
||||
description: 'A patient field researcher.',
|
||||
personality: 'Curious and precise.',
|
||||
scenario: 'The conversation takes place in an observatory.',
|
||||
postHistoryInstructions: 'Answer the latest observation in one paragraph.',
|
||||
greetings: ['Welcome to the observatory.'],
|
||||
messageExample: [
|
||||
['{{user}}: What did you find?', '{{char}}: A new comet.'],
|
||||
],
|
||||
extensions: {
|
||||
airi: {
|
||||
modules: {
|
||||
consciousness: { provider: 'mock-consciousness-provider', model: 'mock-consciousness-model' },
|
||||
vision: { provider: 'mock-vision-provider', model: 'mock-vision-model' },
|
||||
speech: { provider: 'mock-speech-provider', model: 'mock-speech-model', voice_id: 'mock-speech-voice' },
|
||||
artistry: { widgetInstruction: 'Use the image widget for star charts.' },
|
||||
},
|
||||
agents: {},
|
||||
},
|
||||
},
|
||||
}, 'scratch')
|
||||
|
||||
cardStore.activeCardId = cardId
|
||||
|
||||
expect(cardStore.systemPrompt).toBe([
|
||||
'Follow the character rules.',
|
||||
'A patient field researcher.',
|
||||
'Curious and precise.',
|
||||
'The conversation takes place in an observatory.',
|
||||
'Use the image widget for star charts.',
|
||||
].join('\n\n'))
|
||||
expect(cardStore.systemPrompt).not.toContain('Answer the latest observation')
|
||||
expect(cardStore.systemPrompt).not.toContain('Welcome to the observatory')
|
||||
expect(cardStore.systemPrompt).not.toContain('What did you find?')
|
||||
})
|
||||
|
||||
it('falls back to the default card when the active custom card is deleted', () => {
|
||||
const cardStore = useAiriCardStore()
|
||||
cardStore.initialize()
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import type { Card, ccv3 } from '@proj-airi/ccc'
|
||||
|
||||
import type { AiriCard, AiriExtension } from '../../types/airiCard'
|
||||
|
||||
import { useLocalStorageManualReset } from '@proj-airi/stage-shared/composables'
|
||||
import { nanoid } from 'nanoid'
|
||||
import { defineStore, storeToRefs } from 'pinia'
|
||||
@@ -16,72 +18,23 @@ import { useConsciousnessStore } from './consciousness'
|
||||
import { useSpeechStore } from './speech'
|
||||
import { useVisionStore } from './vision'
|
||||
|
||||
export interface AiriExtension {
|
||||
modules: {
|
||||
consciousness: {
|
||||
provider: string // Example: "openai"
|
||||
model: string // Example: "gpt-4o"
|
||||
}
|
||||
export type { AiriCard, AiriExtension } from '../../types/airiCard'
|
||||
|
||||
vision: {
|
||||
provider: string // Example: "ollama"
|
||||
model: string // Example: "llava"
|
||||
}
|
||||
function resolveSystemPrompt(card: AiriCard | undefined): string {
|
||||
if (!card)
|
||||
return ''
|
||||
|
||||
speech: {
|
||||
provider: string // Example: "elevenlabs"
|
||||
model: string // Example: "eleven_multilingual_v2"
|
||||
voice_id: string // Example: "alloy"
|
||||
// Position-sensitive CCv3 fields are deliberately excluded until provider
|
||||
// message assembly owns their ordering and role semantics.
|
||||
const systemPromptParts = [
|
||||
card.systemPrompt,
|
||||
card.description,
|
||||
card.personality,
|
||||
card.scenario,
|
||||
card.extensions.airi.modules.artistry?.widgetInstruction,
|
||||
].filter((part): part is string => typeof part === 'string' && part.trim().length > 0)
|
||||
|
||||
pitch?: number
|
||||
rate?: number
|
||||
ssml?: boolean
|
||||
language?: string
|
||||
}
|
||||
|
||||
vrm?: {
|
||||
source?: 'file' | 'url'
|
||||
file?: string // Example: "vrm/model.vrm"
|
||||
url?: string // Example: "https://example.com/vrm/model.vrm"
|
||||
}
|
||||
|
||||
live2d?: {
|
||||
source?: 'file' | 'url'
|
||||
file?: string // Example: "live2d/model.json"
|
||||
url?: string // Example: "https://example.com/live2d/model.json"
|
||||
}
|
||||
|
||||
// ID from display-models store (e.g. 'preset-live2d-1', 'display-model-<nanoid>')
|
||||
displayModelId?: string
|
||||
activeBackgroundId?: string
|
||||
|
||||
artistry?: {
|
||||
enabled?: boolean
|
||||
provider?: string
|
||||
model?: string
|
||||
promptPrefix?: string
|
||||
workflowId?: string
|
||||
widgetInstruction?: string
|
||||
spawnMode?: 'bg' | 'widget' | 'inline' | 'bg_widget'
|
||||
options?: Record<string, any>
|
||||
autonomousEnabled?: boolean
|
||||
autonomousThreshold?: number
|
||||
autonomousTarget?: 'user' | 'assistant'
|
||||
}
|
||||
}
|
||||
|
||||
agents: {
|
||||
[key: string]: { // example: minecraft
|
||||
prompt: string
|
||||
enabled?: boolean
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export interface AiriCard extends Card {
|
||||
extensions: {
|
||||
airi: AiriExtension
|
||||
} & Card['extensions']
|
||||
return systemPromptParts.join('\n\n')
|
||||
}
|
||||
|
||||
export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
@@ -450,20 +403,6 @@ export const useAiriCardStore = defineStore('airi-card', () => {
|
||||
activeBackgroundId: activeCard.value?.extensions?.airi?.modules?.activeBackgroundId,
|
||||
} satisfies AiriExtension['modules']
|
||||
}),
|
||||
|
||||
systemPrompt: computed(() => {
|
||||
const card = activeCard.value
|
||||
if (!card)
|
||||
return ''
|
||||
|
||||
const components = [
|
||||
card.systemPrompt,
|
||||
card.description,
|
||||
card.personality,
|
||||
card.extensions?.airi?.modules?.artistry?.widgetInstruction,
|
||||
].filter(Boolean)
|
||||
|
||||
return components.join('\n\n')
|
||||
}),
|
||||
systemPrompt: computed(() => resolveSystemPrompt(activeCard.value)),
|
||||
}
|
||||
})
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { Card } from '@proj-airi/ccc'
|
||||
|
||||
/**
|
||||
* AIRI-specific runtime configuration embedded in a character card.
|
||||
*
|
||||
* The extension is persisted with the card. Editor surfaces must preserve
|
||||
* fields they do not own so independent runtime modules can evolve without
|
||||
* losing each other's configuration.
|
||||
*/
|
||||
export interface AiriExtension {
|
||||
modules: {
|
||||
consciousness: {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
|
||||
vision: {
|
||||
provider: string
|
||||
model: string
|
||||
}
|
||||
|
||||
speech: {
|
||||
provider: string
|
||||
model: string
|
||||
voice_id: string
|
||||
|
||||
pitch?: number
|
||||
rate?: number
|
||||
ssml?: boolean
|
||||
language?: string
|
||||
}
|
||||
|
||||
vrm?: {
|
||||
source?: 'file' | 'url'
|
||||
file?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
live2d?: {
|
||||
source?: 'file' | 'url'
|
||||
file?: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
/** ID from the display-models store. */
|
||||
displayModelId?: string
|
||||
activeBackgroundId?: string
|
||||
|
||||
artistry?: {
|
||||
enabled?: boolean
|
||||
provider?: string
|
||||
model?: string
|
||||
promptPrefix?: string
|
||||
workflowId?: string
|
||||
widgetInstruction?: string
|
||||
spawnMode?: 'bg' | 'widget' | 'inline' | 'bg_widget'
|
||||
options?: Record<string, unknown>
|
||||
autonomousEnabled?: boolean
|
||||
autonomousThreshold?: number
|
||||
autonomousTarget?: 'user' | 'assistant'
|
||||
}
|
||||
}
|
||||
|
||||
agents: Record<string, {
|
||||
prompt: string
|
||||
enabled?: boolean
|
||||
}>
|
||||
}
|
||||
|
||||
/** Character card normalized with the AIRI extension required by the runtime. */
|
||||
export interface AiriCard extends Card {
|
||||
extensions: {
|
||||
airi: AiriExtension
|
||||
} & Card['extensions']
|
||||
}
|
||||
@@ -1 +1,2 @@
|
||||
export type * from './airiCard'
|
||||
export * from './chat'
|
||||
|
||||
Reference in New Issue
Block a user