feat(core-agent): harden registry buckets and bridge ingest isolation (#1819)
This commit is contained in:
@@ -5,7 +5,7 @@ export type { AgentSessionPort } from './contracts/session-port'
|
||||
export type { AgentForegroundStreamPort } from './contracts/stream-port'
|
||||
|
||||
export { createChatHooks } from './runtime/agent-hooks'
|
||||
export type { ContextHistoryEntry, ContextRegistry } from './runtime/context-registry'
|
||||
export type { ContextHistoryEntry, ContextIngestResult, ContextRegistry } from './runtime/context-registry'
|
||||
export { createContextRegistry } from './runtime/context-registry'
|
||||
export {
|
||||
isContentArrayRelatedError,
|
||||
|
||||
@@ -0,0 +1,312 @@
|
||||
import type { ContextMessage } from '../types/chat'
|
||||
|
||||
import { ContextUpdateStrategy } from '@proj-airi/server-shared/types'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { createContextRegistry } from './context-registry'
|
||||
|
||||
type TestContextMessage = ContextMessage & { source?: string }
|
||||
|
||||
function createMetadata(pluginId: string, instanceId: string): NonNullable<ContextMessage['metadata']> {
|
||||
return {
|
||||
source: {
|
||||
id: instanceId,
|
||||
kind: 'plugin',
|
||||
plugin: {
|
||||
id: pluginId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createContextMessage(overrides: Partial<TestContextMessage> = {}): TestContextMessage {
|
||||
const id = overrides.id ?? 'context-1'
|
||||
|
||||
return {
|
||||
id,
|
||||
contextId: overrides.contextId ?? id,
|
||||
strategy: overrides.strategy ?? ContextUpdateStrategy.ReplaceSelf,
|
||||
text: overrides.text ?? 'context text',
|
||||
createdAt: overrides.createdAt ?? 1,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const registry = createContextRegistry()
|
||||
* registry.ingest({ strategy: ContextUpdateStrategy.ReplaceSelf, text: 'now' })
|
||||
*/
|
||||
describe('createContextRegistry', () => {
|
||||
/**
|
||||
* @example
|
||||
* replace-self from the same source leaves one active entry and reports replace.
|
||||
*/
|
||||
it('replaces the same source bucket for replace-self updates and returns entry count', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
const firstResult = registry.ingest(createContextMessage({
|
||||
id: 'first',
|
||||
source: 'sensor',
|
||||
text: 'first reading',
|
||||
}))
|
||||
const secondResult = registry.ingest(createContextMessage({
|
||||
id: 'second',
|
||||
source: 'sensor',
|
||||
text: 'second reading',
|
||||
}))
|
||||
|
||||
expect(firstResult).toEqual({
|
||||
sourceKey: 'sensor',
|
||||
mutation: 'replace',
|
||||
entryCount: 1,
|
||||
})
|
||||
expect(secondResult).toEqual({
|
||||
sourceKey: 'sensor',
|
||||
mutation: 'replace',
|
||||
entryCount: 1,
|
||||
})
|
||||
expect(registry.snapshot().sensor?.map(message => message.text)).toEqual(['second reading'])
|
||||
expect(registry.contextHistory().map(message => message.id)).toEqual(['first', 'second'])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* append-self from the same source grows the active bucket and reports append.
|
||||
*/
|
||||
it('appends to the same source bucket for append-self updates and returns the new entry count', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
const firstResult = registry.ingest(createContextMessage({
|
||||
id: 'first',
|
||||
source: 'sensor',
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'first reading',
|
||||
}))
|
||||
const secondResult = registry.ingest(createContextMessage({
|
||||
id: 'second',
|
||||
source: 'sensor',
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'second reading',
|
||||
}))
|
||||
|
||||
expect(firstResult).toEqual({
|
||||
sourceKey: 'sensor',
|
||||
mutation: 'append',
|
||||
entryCount: 1,
|
||||
})
|
||||
expect(secondResult).toEqual({
|
||||
sourceKey: 'sensor',
|
||||
mutation: 'append',
|
||||
entryCount: 2,
|
||||
})
|
||||
expect(registry.snapshot().sensor?.map(message => message.text)).toEqual(['first reading', 'second reading'])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* metadata.source.plugin.id + metadata.source.id becomes "plugin:instance".
|
||||
*/
|
||||
it('resolves metadata source keys before source fallback and unknown fallback', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
const pluginInstanceResult = registry.ingest(createContextMessage({
|
||||
id: 'with-instance',
|
||||
source: 'fallback-source',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
}))
|
||||
const pluginOnlyResult = registry.ingest(createContextMessage({
|
||||
id: 'plugin-only',
|
||||
metadata: createMetadata('weather', ''),
|
||||
}))
|
||||
const sourceResult = registry.ingest(createContextMessage({
|
||||
id: 'source-only',
|
||||
source: 'legacy-source',
|
||||
}))
|
||||
const unknownResult = registry.ingest(createContextMessage({
|
||||
id: 'unknown-source',
|
||||
}))
|
||||
|
||||
expect(pluginInstanceResult?.sourceKey).toBe('weather:station-1')
|
||||
expect(pluginOnlyResult?.sourceKey).toBe('weather')
|
||||
expect(sourceResult?.sourceKey).toBe('legacy-source')
|
||||
expect(unknownResult?.sourceKey).toBe('unknown')
|
||||
expect(Object.keys(registry.snapshot())).toEqual([
|
||||
'weather:station-1',
|
||||
'weather',
|
||||
'legacy-source',
|
||||
'unknown',
|
||||
])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* createContextRegistry({ historyLimit: 2 }) keeps only the two newest history entries.
|
||||
*/
|
||||
it('trims context history to the configured history limit', () => {
|
||||
const registry = createContextRegistry({ historyLimit: 2 })
|
||||
|
||||
registry.ingest(createContextMessage({ id: 'first', source: 'sensor' }))
|
||||
registry.ingest(createContextMessage({ id: 'second', source: 'sensor' }))
|
||||
registry.ingest(createContextMessage({ id: 'third', source: 'sensor' }))
|
||||
|
||||
expect(registry.contextHistory().map(message => message.id)).toEqual(['second', 'third'])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* createContextRegistry() keeps the latest 400 history entries by default.
|
||||
*/
|
||||
it('trims context history to the default 400 record history limit', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
for (let index = 0; index < 401; index += 1) {
|
||||
registry.ingest(createContextMessage({
|
||||
id: `context-${index}`,
|
||||
source: 'sensor',
|
||||
}))
|
||||
}
|
||||
|
||||
const historyIds = registry.contextHistory().map(message => message.id)
|
||||
expect(historyIds).toHaveLength(400)
|
||||
expect(historyIds[0]).toBe('context-1')
|
||||
expect(historyIds.at(-1)).toBe('context-400')
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* "__proto__" is a valid source key and cannot rewrite the snapshot prototype.
|
||||
*/
|
||||
it('keeps __proto__ source keys as bucket data instead of mutating object prototypes', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
const result = registry.ingest(createContextMessage({
|
||||
id: 'proto-source',
|
||||
source: '__proto__',
|
||||
text: 'safe proto bucket',
|
||||
}))
|
||||
const snapshot = registry.snapshot()
|
||||
|
||||
expect(result).toEqual({
|
||||
sourceKey: '__proto__',
|
||||
mutation: 'replace',
|
||||
entryCount: 1,
|
||||
})
|
||||
expect(Object.getPrototypeOf(snapshot)).toBe(Object.prototype)
|
||||
expect(Object.hasOwn(snapshot, '__proto__')).toBe(true)
|
||||
expect(Object.getOwnPropertyDescriptor(snapshot, '__proto__')?.value?.map((message: ContextMessage) => message.text)).toEqual(['safe proto bucket'])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* "toString" is a valid source key and cannot collide with inherited methods.
|
||||
*/
|
||||
it('keeps toString source keys as bucket data instead of colliding with inherited methods', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
const firstResult = registry.ingest(createContextMessage({
|
||||
id: 'first',
|
||||
source: 'toString',
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'first toString bucket entry',
|
||||
}))
|
||||
const secondResult = registry.ingest(createContextMessage({
|
||||
id: 'second',
|
||||
source: 'toString',
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'second toString bucket entry',
|
||||
}))
|
||||
|
||||
expect(firstResult?.entryCount).toBe(1)
|
||||
expect(secondResult).toEqual({
|
||||
sourceKey: 'toString',
|
||||
mutation: 'append',
|
||||
entryCount: 2,
|
||||
})
|
||||
expect(Object.getOwnPropertyDescriptor(registry.snapshot(), 'toString')?.value?.map((message: ContextMessage) => message.text)).toEqual([
|
||||
'first toString bucket entry',
|
||||
'second toString bucket entry',
|
||||
])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Mutating a returned snapshot never mutates the registry internals.
|
||||
*/
|
||||
it('returns cloned snapshots and active contexts so external mutation cannot pollute the registry', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
registry.ingest(createContextMessage({
|
||||
source: 'sensor',
|
||||
text: 'original',
|
||||
}))
|
||||
|
||||
const snapshot = registry.snapshot()
|
||||
const activeContexts = registry.activeContexts()
|
||||
const snapshotMessage = snapshot.sensor?.[0]
|
||||
const activeContextMessage = activeContexts.sensor?.[0]
|
||||
|
||||
expect(snapshotMessage).toBeDefined()
|
||||
expect(activeContextMessage).toBeDefined()
|
||||
if (!snapshotMessage || !activeContextMessage)
|
||||
throw new Error('Expected cloned registry messages to exist')
|
||||
|
||||
snapshotMessage.text = 'mutated snapshot'
|
||||
activeContextMessage.text = 'mutated active context'
|
||||
|
||||
expect(registry.snapshot().sensor?.[0]?.text).toBe('original')
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Unknown strategies return undefined but remain visible in history.
|
||||
*/
|
||||
it('records unknown strategies in history without returning a mutation result', () => {
|
||||
const registry = createContextRegistry()
|
||||
const unsupportedStrategy = 'unknown-strategy' as ContextMessage['strategy']
|
||||
|
||||
const result = registry.ingest(createContextMessage({
|
||||
id: 'unsupported',
|
||||
source: 'sensor',
|
||||
strategy: unsupportedStrategy,
|
||||
}))
|
||||
|
||||
expect(result).toBeUndefined()
|
||||
expect(registry.contextHistory()).toEqual([
|
||||
expect.objectContaining({
|
||||
id: 'unsupported',
|
||||
sourceKey: 'sensor',
|
||||
}),
|
||||
])
|
||||
expect(registry.activeContexts().sensor).toEqual([])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Failed cloning leaves the registry exactly as it was before ingest.
|
||||
*/
|
||||
it('keeps registry state unchanged when an envelope cannot be cloned', () => {
|
||||
const registry = createContextRegistry()
|
||||
|
||||
registry.ingest(createContextMessage({
|
||||
id: 'stable',
|
||||
source: 'sensor',
|
||||
text: 'stable context',
|
||||
}))
|
||||
|
||||
expect(() => registry.ingest(createContextMessage({
|
||||
id: 'uncloneable',
|
||||
source: 'broken-source',
|
||||
content: () => 'functions cannot be structured-cloned',
|
||||
}))).toThrow()
|
||||
expect(registry.snapshot()).toEqual({
|
||||
sensor: [
|
||||
expect.objectContaining({
|
||||
id: 'stable',
|
||||
text: 'stable context',
|
||||
}),
|
||||
],
|
||||
})
|
||||
expect(registry.contextHistory().map(message => message.id)).toEqual(['stable'])
|
||||
})
|
||||
})
|
||||
@@ -10,20 +10,54 @@ interface EventSourcePayload {
|
||||
metadata?: { source?: MetadataEventSource }
|
||||
}
|
||||
|
||||
/**
|
||||
* Stored context event with the registry bucket key resolved at ingest time.
|
||||
*/
|
||||
export interface ContextHistoryEntry extends ContextMessage {
|
||||
/** Stable source bucket key derived from metadata, source, or fallback. */
|
||||
sourceKey: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Observable result emitted when a context update mutates an active bucket.
|
||||
*/
|
||||
export interface ContextIngestResult {
|
||||
/** Stable source bucket key affected by the ingest. */
|
||||
sourceKey: string
|
||||
/** Registry mutation applied to the active bucket. */
|
||||
mutation: 'replace' | 'append'
|
||||
/** Number of active entries in the affected bucket after mutation. */
|
||||
entryCount: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Mutable runtime registry for active context buckets and bounded ingest history.
|
||||
*/
|
||||
export interface ContextRegistry {
|
||||
ingest: (envelope: ContextMessage) => void
|
||||
/** Stores a context message and returns a mutation summary for known strategies. */
|
||||
ingest: (envelope: ContextMessage) => ContextIngestResult | undefined
|
||||
/** Clears active context buckets and ingest history. */
|
||||
reset: () => void
|
||||
/** Returns a cloned active context bucket snapshot. */
|
||||
snapshot: () => Record<string, ContextMessage[]>
|
||||
/** Returns cloned active context buckets for callers that prefer explicit naming. */
|
||||
activeContexts: () => Record<string, ContextMessage[]>
|
||||
/** Returns cloned ingest history entries in chronological order. */
|
||||
contextHistory: () => ContextHistoryEntry[]
|
||||
}
|
||||
|
||||
interface CreateContextRegistryOptions {
|
||||
/**
|
||||
* Maximum number of history records retained by the registry.
|
||||
*
|
||||
* @default 400
|
||||
*/
|
||||
historyLimit?: number
|
||||
/**
|
||||
* Resolves a context message into a stable source bucket key.
|
||||
*
|
||||
* @default metadata plugin/instance key, then event source, then "unknown"
|
||||
*/
|
||||
getSourceKey?: (event: EventSourcePayload, fallback?: string) => string
|
||||
}
|
||||
|
||||
@@ -45,26 +79,52 @@ function defaultGetSourceKey(event: EventSourcePayload, fallback = 'unknown') {
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a context registry that owns active buckets and bounded ingest history.
|
||||
*
|
||||
* Use when:
|
||||
* - Runtime contexts need replace-self or append-self bucket semantics.
|
||||
* - UI or transport layers need cloned snapshots without owning mutation policy.
|
||||
*
|
||||
* Expects:
|
||||
* - Context messages are structured-cloneable before they enter the registry.
|
||||
* - Unknown strategies should still be recorded in history for observability.
|
||||
*
|
||||
* Returns:
|
||||
* - A registry whose snapshots cannot mutate internal active bucket state.
|
||||
*/
|
||||
export function createContextRegistry(options: CreateContextRegistryOptions = {}): ContextRegistry {
|
||||
const historyLimit = options.historyLimit ?? 400
|
||||
const getSourceKey = options.getSourceKey ?? defaultGetSourceKey
|
||||
|
||||
let currentActiveContexts: Record<string, ContextMessage[]> = {}
|
||||
let currentActiveContexts = new Map<string, ContextMessage[]>()
|
||||
let currentContextHistory: ContextHistoryEntry[] = []
|
||||
|
||||
function ingest(envelope: ContextMessage) {
|
||||
function ingest(envelope: ContextMessage): ContextIngestResult | undefined {
|
||||
const sourceKey = getSourceKey(envelope)
|
||||
if (!currentActiveContexts[sourceKey]) {
|
||||
currentActiveContexts[sourceKey] = []
|
||||
}
|
||||
|
||||
const safeEnvelopeToStore = structuredClone(envelope)
|
||||
|
||||
if (!currentActiveContexts.has(sourceKey)) {
|
||||
currentActiveContexts.set(sourceKey, [])
|
||||
}
|
||||
|
||||
let result: ContextIngestResult | undefined
|
||||
|
||||
if (envelope.strategy === CONTEXT_UPDATE_REPLACE_SELF) {
|
||||
currentActiveContexts[sourceKey] = [safeEnvelopeToStore]
|
||||
currentActiveContexts.set(sourceKey, [safeEnvelopeToStore])
|
||||
result = {
|
||||
sourceKey,
|
||||
mutation: 'replace',
|
||||
entryCount: currentActiveContexts.get(sourceKey)?.length ?? 0,
|
||||
}
|
||||
}
|
||||
else if (envelope.strategy === CONTEXT_UPDATE_APPEND_SELF) {
|
||||
currentActiveContexts[sourceKey].push(safeEnvelopeToStore)
|
||||
currentActiveContexts.get(sourceKey)?.push(safeEnvelopeToStore)
|
||||
result = {
|
||||
sourceKey,
|
||||
mutation: 'append',
|
||||
entryCount: currentActiveContexts.get(sourceKey)?.length ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
currentContextHistory = [
|
||||
@@ -74,22 +134,29 @@ export function createContextRegistry(options: CreateContextRegistryOptions = {}
|
||||
sourceKey,
|
||||
},
|
||||
].slice(-historyLimit)
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
function reset() {
|
||||
currentActiveContexts = {}
|
||||
currentActiveContexts = new Map<string, ContextMessage[]>()
|
||||
currentContextHistory = []
|
||||
}
|
||||
|
||||
function snapshot() {
|
||||
return structuredClone(currentActiveContexts)
|
||||
return Object.fromEntries(
|
||||
Array.from(currentActiveContexts, ([sourceKey, messages]) => [
|
||||
sourceKey,
|
||||
structuredClone(messages),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
return {
|
||||
ingest,
|
||||
reset,
|
||||
snapshot,
|
||||
activeContexts: () => structuredClone(currentActiveContexts),
|
||||
contextHistory: () => [...currentContextHistory],
|
||||
activeContexts: snapshot,
|
||||
contextHistory: () => structuredClone(currentContextHistory),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
import type { ContextMessage } from '../../types/chat'
|
||||
|
||||
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import { isReadonly, reactive } from 'vue'
|
||||
|
||||
import { useChatContextStore } from './context-store'
|
||||
|
||||
type TestContextMessage = ContextMessage & { source?: string }
|
||||
|
||||
function createMetadata(pluginId: string, instanceId: string): NonNullable<ContextMessage['metadata']> {
|
||||
return {
|
||||
source: {
|
||||
id: instanceId,
|
||||
kind: 'plugin',
|
||||
plugin: {
|
||||
id: pluginId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createContextMessage(overrides: Partial<TestContextMessage> = {}): TestContextMessage {
|
||||
const id = overrides.id ?? 'context-1'
|
||||
|
||||
return {
|
||||
id,
|
||||
contextId: overrides.contextId ?? id,
|
||||
strategy: overrides.strategy ?? ContextUpdateStrategy.ReplaceSelf,
|
||||
text: overrides.text ?? 'context text',
|
||||
createdAt: overrides.createdAt ?? 1,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @example
|
||||
* const store = useChatContextStore()
|
||||
* store.ingestContextMessage(contextMessage)
|
||||
*/
|
||||
describe('useChatContextStore', () => {
|
||||
beforeEach(() => {
|
||||
setActivePinia(createPinia())
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Ingesting append-self updates mirrors activeContexts and contextHistory from core registry.
|
||||
*/
|
||||
it('keeps reactive mirrors aligned with the core registry after ingest', () => {
|
||||
const store = useChatContextStore()
|
||||
const firstMessage = createContextMessage({
|
||||
id: 'first',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'sunny',
|
||||
})
|
||||
const secondMessage = createContextMessage({
|
||||
id: 'second',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'windy',
|
||||
})
|
||||
|
||||
const firstResult = store.ingestContextMessage(firstMessage)
|
||||
const secondResult = store.ingestContextMessage(secondMessage)
|
||||
|
||||
expect(firstResult).toEqual({
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'append',
|
||||
entryCount: 1,
|
||||
})
|
||||
expect(secondResult).toEqual({
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'append',
|
||||
entryCount: 2,
|
||||
})
|
||||
expect(store.activeContexts).toEqual(store.getContextsSnapshot())
|
||||
expect(store.activeContexts['weather:station-1']?.map(message => message.text)).toEqual(['sunny', 'windy'])
|
||||
expect(store.contextHistory.map(message => message.sourceKey)).toEqual(['weather:station-1', 'weather:station-1'])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Vue reactive envelopes are unwrapped before they enter the core registry.
|
||||
*/
|
||||
it('unwraps Vue reactive envelopes before ingesting through the core registry', () => {
|
||||
const store = useChatContextStore()
|
||||
const reactiveMessage = reactive(createContextMessage({
|
||||
id: 'reactive-message',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
text: 'reactive weather',
|
||||
}))
|
||||
|
||||
const result = store.ingestContextMessage(reactiveMessage)
|
||||
|
||||
expect(result).toEqual({
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'replace',
|
||||
entryCount: 1,
|
||||
})
|
||||
expect(store.getContextsSnapshot()['weather:station-1']?.[0]?.text).toBe('reactive weather')
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Consumers can read mirrors but cannot mutate the registry source of truth through them.
|
||||
*/
|
||||
it('exposes readonly mirrors that do not allow external writes to pollute registry state', () => {
|
||||
const store = useChatContextStore()
|
||||
|
||||
store.ingestContextMessage(createContextMessage({
|
||||
id: 'stable',
|
||||
source: 'sensor',
|
||||
text: 'stable context',
|
||||
}))
|
||||
|
||||
expect(isReadonly(store.activeContexts)).toBe(true)
|
||||
expect(isReadonly(store.contextHistory)).toBe(true)
|
||||
|
||||
Reflect.set(store.activeContexts, 'external', [createContextMessage({
|
||||
id: 'external',
|
||||
source: 'external',
|
||||
text: 'external write',
|
||||
})])
|
||||
Reflect.set(store.contextHistory, '0', {
|
||||
...createContextMessage({
|
||||
id: 'external-history',
|
||||
source: 'external',
|
||||
text: 'external history write',
|
||||
}),
|
||||
sourceKey: 'external',
|
||||
})
|
||||
|
||||
expect(store.activeContexts.external).toBeUndefined()
|
||||
expect(store.contextHistory[0]?.id).toBe('stable')
|
||||
expect(store.getContextsSnapshot()).toEqual({
|
||||
sensor: [
|
||||
expect.objectContaining({
|
||||
id: 'stable',
|
||||
text: 'stable context',
|
||||
}),
|
||||
],
|
||||
})
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* resetContexts() clears both Pinia mirrors and the backing registry snapshot.
|
||||
*/
|
||||
it('clears reactive mirrors and the backing registry when reset', () => {
|
||||
const store = useChatContextStore()
|
||||
|
||||
store.ingestContextMessage(createContextMessage({
|
||||
source: 'sensor',
|
||||
text: 'before reset',
|
||||
}))
|
||||
store.resetContexts()
|
||||
|
||||
expect(store.activeContexts).toEqual({})
|
||||
expect(store.contextHistory).toEqual([])
|
||||
expect(store.getContextsSnapshot()).toEqual({})
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* getContextBucketsSnapshot() returns entryCount, latestCreatedAt, and cloned messages.
|
||||
*/
|
||||
it('preserves context bucket snapshot fields and latestCreatedAt calculation', () => {
|
||||
const store = useChatContextStore()
|
||||
|
||||
store.ingestContextMessage(createContextMessage({
|
||||
id: 'first',
|
||||
source: 'sensor',
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'early',
|
||||
createdAt: 10,
|
||||
}))
|
||||
store.ingestContextMessage(createContextMessage({
|
||||
id: 'second',
|
||||
source: 'sensor',
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'late',
|
||||
createdAt: 30,
|
||||
}))
|
||||
|
||||
const bucket = store.getContextBucketsSnapshot().find(snapshot => snapshot.sourceKey === 'sensor')
|
||||
|
||||
expect(bucket).toBeDefined()
|
||||
if (!bucket)
|
||||
throw new Error('Expected sensor context bucket to exist')
|
||||
|
||||
expect(bucket.sourceKey).toBe('sensor')
|
||||
expect(bucket.entryCount).toBe(2)
|
||||
expect(bucket.latestCreatedAt).toBe(30)
|
||||
expect(bucket.messages.map(message => message.text)).toEqual(['early', 'late'])
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Mutating bucket snapshot messages never mutates the core registry.
|
||||
*/
|
||||
it('keeps bucket snapshot message mutation isolated from the core registry', () => {
|
||||
const store = useChatContextStore()
|
||||
|
||||
store.ingestContextMessage(createContextMessage({
|
||||
source: 'sensor',
|
||||
text: 'original bucket text',
|
||||
}))
|
||||
|
||||
const bucket = store.getContextBucketsSnapshot().find(snapshot => snapshot.sourceKey === 'sensor')
|
||||
|
||||
expect(bucket).toBeDefined()
|
||||
if (!bucket)
|
||||
throw new Error('Expected sensor context bucket to exist')
|
||||
|
||||
const message = bucket.messages[0]
|
||||
expect(message).toBeDefined()
|
||||
if (!message)
|
||||
throw new Error('Expected sensor context message to exist')
|
||||
|
||||
message.text = 'mutated bucket snapshot'
|
||||
|
||||
expect(store.getContextsSnapshot().sensor?.[0]?.text).toBe('original bucket text')
|
||||
})
|
||||
})
|
||||
@@ -1,102 +1,61 @@
|
||||
import type { ContextMessage } from '../../types/chat'
|
||||
import type { ContextHistoryEntry, ContextIngestResult, ContextMessage } from '@proj-airi/core-agent'
|
||||
|
||||
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
|
||||
import { createContextRegistry } from '@proj-airi/core-agent'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, toRaw } from 'vue'
|
||||
import { readonly, ref, toRaw } from 'vue'
|
||||
|
||||
import { getEventSourceKey } from '../../utils/event-source'
|
||||
|
||||
export type { ContextHistoryEntry, ContextIngestResult } from '@proj-airi/core-agent'
|
||||
|
||||
/**
|
||||
* UI-facing view of one active context source bucket.
|
||||
*/
|
||||
export interface ContextBucketSnapshot {
|
||||
/** Stable registry source bucket key. */
|
||||
sourceKey: string
|
||||
/** Number of active messages currently stored for this bucket. */
|
||||
entryCount: number
|
||||
/** Latest `createdAt` timestamp across messages in this bucket. */
|
||||
latestCreatedAt?: number
|
||||
/** Cloned context messages for devtools and UI consumers. */
|
||||
messages: ContextMessage[]
|
||||
}
|
||||
|
||||
export interface ContextIngestResult {
|
||||
sourceKey: string
|
||||
mutation: 'replace' | 'append'
|
||||
entryCount: number
|
||||
}
|
||||
|
||||
export interface ContextHistoryEntry extends ContextMessage {
|
||||
sourceKey: string
|
||||
}
|
||||
|
||||
const CONTEXT_HISTORY_LIMIT = 400
|
||||
|
||||
export const useChatContextStore = defineStore('chat-context', () => {
|
||||
const activeContexts = ref<Record<string, ContextMessage[]>>({})
|
||||
const contextHistory = ref<ContextHistoryEntry[]>([])
|
||||
const registry = createContextRegistry({
|
||||
historyLimit: CONTEXT_HISTORY_LIMIT,
|
||||
getSourceKey: getEventSourceKey,
|
||||
})
|
||||
const activeContextsMirror = ref<Record<string, ContextMessage[]>>({})
|
||||
const contextHistoryMirror = ref<ContextHistoryEntry[]>([])
|
||||
const activeContexts = readonly(activeContextsMirror)
|
||||
const contextHistory = readonly(contextHistoryMirror)
|
||||
|
||||
function cloneMessage(message: ContextMessage): ContextMessage {
|
||||
const rawMessage = toRaw(message)
|
||||
|
||||
try {
|
||||
return structuredClone(rawMessage)
|
||||
}
|
||||
catch {
|
||||
return JSON.parse(JSON.stringify(rawMessage)) as ContextMessage
|
||||
}
|
||||
function syncRegistrySnapshot() {
|
||||
activeContextsMirror.value = registry.activeContexts()
|
||||
contextHistoryMirror.value = registry.contextHistory()
|
||||
}
|
||||
|
||||
function cloneSnapshot() {
|
||||
return Object.fromEntries(
|
||||
Object.entries(activeContexts.value).map(([sourceKey, messages]) => [
|
||||
sourceKey,
|
||||
messages.map(cloneMessage),
|
||||
]),
|
||||
)
|
||||
}
|
||||
|
||||
function ingestContextMessage(envelope: ContextMessage) {
|
||||
const normalizedEnvelope = cloneMessage(envelope)
|
||||
const sourceKey = getEventSourceKey(normalizedEnvelope)
|
||||
let result: ContextIngestResult | undefined
|
||||
|
||||
if (!activeContexts.value[sourceKey]) {
|
||||
activeContexts.value[sourceKey] = []
|
||||
}
|
||||
|
||||
if (normalizedEnvelope.strategy === ContextUpdateStrategy.ReplaceSelf) {
|
||||
activeContexts.value[sourceKey] = [normalizedEnvelope]
|
||||
result = {
|
||||
sourceKey,
|
||||
mutation: 'replace',
|
||||
entryCount: activeContexts.value[sourceKey].length,
|
||||
} satisfies ContextIngestResult
|
||||
}
|
||||
else if (normalizedEnvelope.strategy === ContextUpdateStrategy.AppendSelf) {
|
||||
activeContexts.value[sourceKey].push(normalizedEnvelope)
|
||||
result = {
|
||||
sourceKey,
|
||||
mutation: 'append',
|
||||
entryCount: activeContexts.value[sourceKey].length,
|
||||
} satisfies ContextIngestResult
|
||||
}
|
||||
|
||||
contextHistory.value = [
|
||||
...contextHistory.value,
|
||||
{
|
||||
...normalizedEnvelope,
|
||||
sourceKey,
|
||||
},
|
||||
].slice(-CONTEXT_HISTORY_LIMIT)
|
||||
|
||||
function ingestContextMessage(envelope: ContextMessage): ContextIngestResult | undefined {
|
||||
const result = registry.ingest(toRaw(envelope))
|
||||
syncRegistrySnapshot()
|
||||
return result
|
||||
}
|
||||
|
||||
function resetContexts() {
|
||||
activeContexts.value = {}
|
||||
contextHistory.value = []
|
||||
registry.reset()
|
||||
syncRegistrySnapshot()
|
||||
}
|
||||
|
||||
function getContextsSnapshot() {
|
||||
return cloneSnapshot()
|
||||
return registry.snapshot()
|
||||
}
|
||||
|
||||
function getContextBucketsSnapshot() {
|
||||
return Object.entries(activeContexts.value).map(([sourceKey, messages]) => ({
|
||||
return Object.entries(registry.activeContexts()).map(([sourceKey, messages]) => ({
|
||||
sourceKey,
|
||||
entryCount: messages.length,
|
||||
latestCreatedAt: messages.reduce<number | undefined>((latest, message) => {
|
||||
@@ -104,7 +63,7 @@ export const useChatContextStore = defineStore('chat-context', () => {
|
||||
return message.createdAt
|
||||
return Math.max(latest, message.createdAt)
|
||||
}, undefined),
|
||||
messages: messages.map(cloneMessage),
|
||||
messages,
|
||||
} satisfies ContextBucketSnapshot))
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@ export type ContextLifecyclePhase
|
||||
| 'broadcast-posted'
|
||||
| 'broadcast-received'
|
||||
| 'store-ingested'
|
||||
| 'store-ingest-rejected'
|
||||
| 'before-compose'
|
||||
| 'prompt-context-built'
|
||||
| 'after-compose'
|
||||
|
||||
@@ -1,12 +1,16 @@
|
||||
import { ContextUpdateStrategy } from '@proj-airi/server-sdk'
|
||||
import { createPinia, setActivePinia } from 'pinia'
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
import { ref } from 'vue'
|
||||
|
||||
import { CHAT_STREAM_CHANNEL_NAME } from '../../chat/constants'
|
||||
import { CHAT_STREAM_CHANNEL_NAME, CONTEXT_CHANNEL_NAME } from '../../chat/constants'
|
||||
|
||||
type HookCallback = (...args: any[]) => Promise<void> | void
|
||||
type HookCallback = (...args: unknown[]) => Promise<void> | void
|
||||
type UseContextBridgeStore = typeof import('./context-bridge')['useContextBridgeStore']
|
||||
|
||||
const contextUpdateHooks: HookCallback[] = []
|
||||
const serverEventHooks = new Map<string, HookCallback[]>()
|
||||
|
||||
const chatContextIngestMock = vi.fn()
|
||||
const beginStreamMock = vi.fn()
|
||||
const appendStreamLiteralMock = vi.fn()
|
||||
@@ -15,9 +19,10 @@ const resetStreamMock = vi.fn()
|
||||
const serverSendMock = vi.fn()
|
||||
const ensureConnectedMock = vi.fn().mockResolvedValue(undefined)
|
||||
const onReconnectedMock = vi.fn(() => () => {})
|
||||
const onContextUpdateMock = vi.fn(() => () => {})
|
||||
const onEventMock = vi.fn(() => () => {})
|
||||
const onContextUpdateMock = vi.fn((callback: HookCallback) => registerHook(contextUpdateHooks, callback))
|
||||
const onEventMock = vi.fn((eventName: string, callback: HookCallback) => registerServerEventHook(eventName, callback))
|
||||
const getProviderInstanceMock = vi.fn()
|
||||
const recordLifecycleMock = vi.fn()
|
||||
|
||||
const activeProviderRef = ref<string | null>(null)
|
||||
const activeModelRef = ref<string | null>(null)
|
||||
@@ -47,6 +52,12 @@ function registerHook(target: HookCallback[], callback: HookCallback) {
|
||||
}
|
||||
}
|
||||
|
||||
function registerServerEventHook(eventName: string, callback: HookCallback) {
|
||||
const hooks = serverEventHooks.get(eventName) ?? []
|
||||
serverEventHooks.set(eventName, hooks)
|
||||
return registerHook(hooks, callback)
|
||||
}
|
||||
|
||||
function createTestChannel(name: string) {
|
||||
const channel = new BroadcastChannel(name)
|
||||
testChannels.push(channel)
|
||||
@@ -73,12 +84,62 @@ async function waitForBroadcastDelivery() {
|
||||
await new Promise(resolve => setTimeout(resolve, 50))
|
||||
}
|
||||
|
||||
async function emitHooks(target: HookCallback[], ...args: any[]) {
|
||||
async function emitHooks(target: HookCallback[], ...args: unknown[]) {
|
||||
for (const callback of target) {
|
||||
await callback(...args)
|
||||
}
|
||||
}
|
||||
|
||||
async function emitContextUpdate(event: unknown) {
|
||||
await emitHooks(contextUpdateHooks, event)
|
||||
}
|
||||
|
||||
async function emitServerEvent(eventName: string, event: unknown) {
|
||||
await emitHooks(serverEventHooks.get(eventName) ?? [], event)
|
||||
}
|
||||
|
||||
function createMetadata(pluginId: string, instanceId: string) {
|
||||
return {
|
||||
source: {
|
||||
id: instanceId,
|
||||
kind: 'plugin',
|
||||
plugin: {
|
||||
id: pluginId,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function createContextMessage(overrides: Record<string, unknown> = {}) {
|
||||
const id = typeof overrides.id === 'string' ? overrides.id : 'context-1'
|
||||
|
||||
return {
|
||||
id,
|
||||
contextId: typeof overrides.contextId === 'string' ? overrides.contextId : id,
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'context text',
|
||||
createdAt: 1,
|
||||
...overrides,
|
||||
}
|
||||
}
|
||||
|
||||
function createContextUpdateEvent(overrides: Record<string, unknown> = {}) {
|
||||
const id = typeof overrides.id === 'string' ? overrides.id : 'context-1'
|
||||
|
||||
return {
|
||||
type: 'context:update',
|
||||
source: 'plugin-module-host',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
data: {
|
||||
id,
|
||||
contextId: id,
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'weather changed',
|
||||
...overrides,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
const chatOrchestratorMock = {
|
||||
sending: false,
|
||||
ingest: vi.fn(),
|
||||
@@ -94,21 +155,21 @@ const chatOrchestratorMock = {
|
||||
onAssistantMessage: (callback: HookCallback) => registerHook(assistantMessageHooks, callback),
|
||||
onChatTurnComplete: (callback: HookCallback) => registerHook(turnCompleteHooks, callback),
|
||||
|
||||
emitBeforeMessageComposedHooks: (...args: any[]) => emitHooks(beforeComposeHooks, ...args),
|
||||
emitAfterMessageComposedHooks: (...args: any[]) => emitHooks(afterComposeHooks, ...args),
|
||||
emitBeforeSendHooks: (...args: any[]) => emitHooks(beforeSendHooks, ...args),
|
||||
emitAfterSendHooks: (...args: any[]) => emitHooks(afterSendHooks, ...args),
|
||||
emitTokenLiteralHooks: (...args: any[]) => emitHooks(tokenLiteralHooks, ...args),
|
||||
emitTokenSpecialHooks: (...args: any[]) => emitHooks(tokenSpecialHooks, ...args),
|
||||
emitStreamEndHooks: (...args: any[]) => emitHooks(streamEndHooks, ...args),
|
||||
emitAssistantResponseEndHooks: (...args: any[]) => emitHooks(assistantEndHooks, ...args),
|
||||
emitBeforeMessageComposedHooks: (...args: unknown[]) => emitHooks(beforeComposeHooks, ...args),
|
||||
emitAfterMessageComposedHooks: (...args: unknown[]) => emitHooks(afterComposeHooks, ...args),
|
||||
emitBeforeSendHooks: (...args: unknown[]) => emitHooks(beforeSendHooks, ...args),
|
||||
emitAfterSendHooks: (...args: unknown[]) => emitHooks(afterSendHooks, ...args),
|
||||
emitTokenLiteralHooks: (...args: unknown[]) => emitHooks(tokenLiteralHooks, ...args),
|
||||
emitTokenSpecialHooks: (...args: unknown[]) => emitHooks(tokenSpecialHooks, ...args),
|
||||
emitStreamEndHooks: (...args: unknown[]) => emitHooks(streamEndHooks, ...args),
|
||||
emitAssistantResponseEndHooks: (...args: unknown[]) => emitHooks(assistantEndHooks, ...args),
|
||||
}
|
||||
|
||||
vi.mock('pinia', async () => {
|
||||
const actual = await vi.importActual<typeof import('pinia')>('pinia')
|
||||
return {
|
||||
...actual,
|
||||
storeToRefs: (store: any) => store,
|
||||
storeToRefs: (store: unknown) => store,
|
||||
}
|
||||
})
|
||||
|
||||
@@ -170,7 +231,7 @@ vi.mock('../../chat/stream-store', () => ({
|
||||
|
||||
vi.mock('../../devtools/context-observability', () => ({
|
||||
useContextObservabilityStore: () => ({
|
||||
recordLifecycle: vi.fn(),
|
||||
recordLifecycle: recordLifecycleMock,
|
||||
}),
|
||||
}))
|
||||
|
||||
@@ -220,6 +281,7 @@ describe('context bridge contract', () => {
|
||||
onContextUpdateMock.mockClear()
|
||||
onEventMock.mockClear()
|
||||
getProviderInstanceMock.mockReset()
|
||||
recordLifecycleMock.mockReset()
|
||||
chatOrchestratorMock.ingest.mockReset()
|
||||
|
||||
activeProviderRef.value = null
|
||||
@@ -238,12 +300,251 @@ describe('context bridge contract', () => {
|
||||
assistantEndHooks.length = 0
|
||||
assistantMessageHooks.length = 0
|
||||
turnCompleteHooks.length = 0
|
||||
contextUpdateHooks.length = 0
|
||||
serverEventHooks.clear()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
closeTestChannels()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Broadcast context updates record store-ingested with core result fields.
|
||||
*/
|
||||
it('records core ingest result for broadcast context updates', async () => {
|
||||
chatContextIngestMock.mockReturnValueOnce({
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'append',
|
||||
entryCount: 2,
|
||||
})
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
const contextSender = createTestChannel(CONTEXT_CHANNEL_NAME)
|
||||
|
||||
contextSender.postMessage(createContextMessage({
|
||||
id: 'broadcast-context',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
text: 'broadcast weather',
|
||||
}))
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(chatContextIngestMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'store-ingested',
|
||||
channel: 'broadcast',
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'append',
|
||||
details: expect.objectContaining({
|
||||
entryCount: 2,
|
||||
}),
|
||||
}))
|
||||
|
||||
await store.dispose()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Server context updates record store-ingested before broadcast-posted.
|
||||
*/
|
||||
it('records core ingest result for server context updates before broadcasting', async () => {
|
||||
chatContextIngestMock.mockReturnValueOnce({
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'replace',
|
||||
entryCount: 1,
|
||||
})
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
|
||||
await emitContextUpdate(createContextUpdateEvent({
|
||||
id: 'server-context',
|
||||
strategy: ContextUpdateStrategy.ReplaceSelf,
|
||||
text: 'server weather',
|
||||
}))
|
||||
|
||||
expect(chatContextIngestMock).toHaveBeenCalledTimes(1)
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'store-ingested',
|
||||
channel: 'server',
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'replace',
|
||||
details: expect.objectContaining({
|
||||
entryCount: 1,
|
||||
}),
|
||||
}))
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'broadcast-posted',
|
||||
channel: 'broadcast',
|
||||
contextId: 'server-context',
|
||||
}))
|
||||
|
||||
await store.dispose()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Input context updates record store-ingested and stay in chat input payload.
|
||||
*/
|
||||
it('records core ingest result for input context updates and forwards accepted updates', async () => {
|
||||
chatContextIngestMock.mockReturnValueOnce({
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'append',
|
||||
entryCount: 1,
|
||||
})
|
||||
activeProviderRef.value = 'mock-provider'
|
||||
activeModelRef.value = 'mock-model'
|
||||
getProviderInstanceMock.mockResolvedValueOnce({})
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
|
||||
await emitServerEvent('input:text', {
|
||||
type: 'input:text',
|
||||
source: 'plugin-module-host',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
data: {
|
||||
text: 'hello',
|
||||
contextUpdates: [
|
||||
{
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'input weather',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(chatContextIngestMock).toHaveBeenCalledTimes(1)
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'store-ingested',
|
||||
channel: 'input',
|
||||
sourceKey: 'weather:station-1',
|
||||
mutation: 'append',
|
||||
details: expect.objectContaining({
|
||||
entryCount: 1,
|
||||
inputType: 'input:text',
|
||||
}),
|
||||
}))
|
||||
expect(chatOrchestratorMock.ingest).toHaveBeenCalledTimes(1)
|
||||
expect(chatOrchestratorMock.ingest.mock.calls[0]?.[1]?.input?.data.contextUpdates).toEqual([
|
||||
expect.objectContaining({
|
||||
contextId: expect.any(String),
|
||||
id: expect.any(String),
|
||||
text: 'input weather',
|
||||
}),
|
||||
])
|
||||
|
||||
await store.dispose()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Broadcast context ingest failures record store-ingest-rejected instead of escaping.
|
||||
*/
|
||||
it('records rejected lifecycle for broadcast ingest failures without interrupting the watcher', async () => {
|
||||
chatContextIngestMock.mockImplementationOnce(() => {
|
||||
throw new Error('Cannot clone broadcast context')
|
||||
})
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
const contextSender = createTestChannel(CONTEXT_CHANNEL_NAME)
|
||||
|
||||
contextSender.postMessage(createContextMessage({
|
||||
id: 'bad-broadcast-context',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
text: 'bad broadcast weather',
|
||||
}))
|
||||
|
||||
await vi.waitFor(() => {
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'store-ingest-rejected',
|
||||
channel: 'broadcast',
|
||||
contextId: 'bad-broadcast-context',
|
||||
details: expect.objectContaining({
|
||||
errorMessage: 'Cannot clone broadcast context',
|
||||
}),
|
||||
}))
|
||||
})
|
||||
|
||||
await store.dispose()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Server context ingest failures are not rebroadcast.
|
||||
*/
|
||||
it('records rejected lifecycle and skips broadcast when server context ingest fails', async () => {
|
||||
chatContextIngestMock.mockImplementationOnce(() => {
|
||||
throw new Error('Cannot clone server context')
|
||||
})
|
||||
const postedContexts = collectChannelMessages(CONTEXT_CHANNEL_NAME)
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
|
||||
await emitContextUpdate(createContextUpdateEvent({
|
||||
id: 'bad-server-context',
|
||||
text: 'bad server weather',
|
||||
}))
|
||||
await waitForBroadcastDelivery()
|
||||
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'store-ingest-rejected',
|
||||
channel: 'server',
|
||||
contextId: 'bad-server-context',
|
||||
details: expect.objectContaining({
|
||||
errorMessage: 'Cannot clone server context',
|
||||
}),
|
||||
}))
|
||||
expect(recordLifecycleMock).not.toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'broadcast-posted',
|
||||
contextId: 'bad-server-context',
|
||||
}))
|
||||
expect(postedContexts).toHaveLength(0)
|
||||
|
||||
await store.dispose()
|
||||
})
|
||||
|
||||
/**
|
||||
* @example
|
||||
* Input context ingest failures drop only the failed context update.
|
||||
*/
|
||||
it('records rejected lifecycle and continues text ingestion when input context ingest fails', async () => {
|
||||
chatContextIngestMock.mockImplementationOnce(() => {
|
||||
throw new Error('Cannot clone input context')
|
||||
})
|
||||
activeProviderRef.value = 'mock-provider'
|
||||
activeModelRef.value = 'mock-model'
|
||||
getProviderInstanceMock.mockResolvedValueOnce({})
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
|
||||
await emitServerEvent('input:text', {
|
||||
type: 'input:text',
|
||||
source: 'plugin-module-host',
|
||||
metadata: createMetadata('weather', 'station-1'),
|
||||
data: {
|
||||
text: 'hello',
|
||||
contextUpdates: [
|
||||
{
|
||||
strategy: ContextUpdateStrategy.AppendSelf,
|
||||
text: 'bad input weather',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
|
||||
expect(recordLifecycleMock).toHaveBeenCalledWith(expect.objectContaining({
|
||||
phase: 'store-ingest-rejected',
|
||||
channel: 'input',
|
||||
details: expect.objectContaining({
|
||||
errorMessage: 'Cannot clone input context',
|
||||
}),
|
||||
}))
|
||||
expect(chatOrchestratorMock.ingest).toHaveBeenCalledTimes(1)
|
||||
expect(chatOrchestratorMock.ingest.mock.calls[0]?.[1]?.input?.data.contextUpdates).toEqual([])
|
||||
|
||||
await store.dispose()
|
||||
})
|
||||
|
||||
it('replays remote stream lifecycle into sending and stream store APIs', async () => {
|
||||
const store = useContextBridgeStore()
|
||||
await store.initialize()
|
||||
|
||||
@@ -5,6 +5,7 @@ import type { UserMessage } from '@xsai/shared-chat'
|
||||
import type { ChatStreamEvent, ChatStreamEventContext, ContextMessage } from '../../../types/chat'
|
||||
import type { SparkNotifyReactionOptions } from './spark-notify-reaction'
|
||||
|
||||
import { errorMessageFrom } from '@moeru/std'
|
||||
import { isStageTamagotchi, isStageWeb } from '@proj-airi/stage-shared'
|
||||
import { useBroadcastChannel } from '@vueuse/core'
|
||||
import { Mutex } from 'es-toolkit'
|
||||
@@ -85,6 +86,53 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
let remoteStreamGuard: { sessionId: string, generation: number } | null = null
|
||||
let initialized = false
|
||||
|
||||
function recordContextIngestRejected(options: {
|
||||
channel: 'server' | 'broadcast' | 'input'
|
||||
contextMessage: ContextMessage
|
||||
details?: unknown
|
||||
error: unknown
|
||||
sourceLabel?: string
|
||||
}) {
|
||||
contextObservability.recordLifecycle({
|
||||
phase: 'store-ingest-rejected',
|
||||
channel: options.channel,
|
||||
sourceKey: getEventSourceKey(options.contextMessage),
|
||||
strategy: options.contextMessage.strategy,
|
||||
lane: options.contextMessage.lane,
|
||||
contextId: options.contextMessage.contextId,
|
||||
eventId: options.contextMessage.id,
|
||||
textPreview: options.contextMessage.text,
|
||||
sourceLabel: options.sourceLabel,
|
||||
details: {
|
||||
errorMessage: errorMessageFrom(options.error) ?? 'Unknown context ingest error',
|
||||
event: options.details,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function ingestContextMessageSafely(options: {
|
||||
channel: 'server' | 'broadcast' | 'input'
|
||||
contextMessage: ContextMessage
|
||||
details?: unknown
|
||||
sourceLabel?: string
|
||||
}) {
|
||||
try {
|
||||
return {
|
||||
ok: true as const,
|
||||
result: chatContext.ingestContextMessage(options.contextMessage),
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
recordContextIngestRejected({
|
||||
...options,
|
||||
error,
|
||||
})
|
||||
return {
|
||||
ok: false as const,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSparkNotifyReactionLocal(options: SparkNotifyReactionOptions) {
|
||||
const event: WebSocketEventOf<'spark:notify'> = {
|
||||
type: 'spark:notify',
|
||||
@@ -196,21 +244,26 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id,
|
||||
details: event,
|
||||
})
|
||||
const result = chatContext.ingestContextMessage(event)
|
||||
if (result) {
|
||||
const ingestAttempt = ingestContextMessageSafely({
|
||||
channel: 'broadcast',
|
||||
contextMessage: event,
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id,
|
||||
details: event,
|
||||
})
|
||||
if (ingestAttempt.ok && ingestAttempt.result) {
|
||||
contextObservability.recordLifecycle({
|
||||
phase: 'store-ingested',
|
||||
channel: 'broadcast',
|
||||
sourceKey: result.sourceKey,
|
||||
sourceKey: ingestAttempt.result.sourceKey,
|
||||
strategy: event.strategy,
|
||||
lane: event.lane,
|
||||
contextId: event.contextId,
|
||||
eventId: event.id,
|
||||
mutation: result.mutation,
|
||||
mutation: ingestAttempt.result.mutation,
|
||||
textPreview: event.text,
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id,
|
||||
details: {
|
||||
entryCount: result.entryCount,
|
||||
entryCount: ingestAttempt.result.entryCount,
|
||||
event,
|
||||
},
|
||||
})
|
||||
@@ -272,21 +325,29 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
metadata: event.metadata,
|
||||
createdAt: Date.now(),
|
||||
}
|
||||
const result = chatContext.ingestContextMessage(contextMessage)
|
||||
if (result) {
|
||||
const ingestAttempt = ingestContextMessageSafely({
|
||||
channel: 'server',
|
||||
contextMessage,
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
|
||||
details: event,
|
||||
})
|
||||
if (!ingestAttempt.ok)
|
||||
return
|
||||
|
||||
if (ingestAttempt.result) {
|
||||
contextObservability.recordLifecycle({
|
||||
phase: 'store-ingested',
|
||||
channel: 'server',
|
||||
sourceKey: result.sourceKey,
|
||||
sourceKey: ingestAttempt.result.sourceKey,
|
||||
strategy: contextMessage.strategy,
|
||||
lane: contextMessage.lane,
|
||||
contextId: contextMessage.contextId,
|
||||
eventId: contextMessage.id,
|
||||
mutation: result.mutation,
|
||||
mutation: ingestAttempt.result.mutation,
|
||||
textPreview: contextMessage.text,
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
|
||||
details: {
|
||||
entryCount: result.entryCount,
|
||||
entryCount: ingestAttempt.result.entryCount,
|
||||
event,
|
||||
},
|
||||
})
|
||||
@@ -330,6 +391,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
contextId,
|
||||
}
|
||||
})
|
||||
const acceptedContextUpdates: typeof normalizedContextUpdates = normalizedContextUpdates ? [] : undefined
|
||||
|
||||
if (normalizedContextUpdates?.length) {
|
||||
const createdAt = Date.now()
|
||||
@@ -348,26 +410,39 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
update,
|
||||
},
|
||||
})
|
||||
const contextMessage = {
|
||||
const contextMessage: ContextMessage = {
|
||||
...update,
|
||||
metadata: event.metadata,
|
||||
createdAt,
|
||||
}
|
||||
const result = chatContext.ingestContextMessage(contextMessage)
|
||||
if (result) {
|
||||
const ingestAttempt = ingestContextMessageSafely({
|
||||
channel: 'input',
|
||||
contextMessage,
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
|
||||
details: {
|
||||
inputType: event.type,
|
||||
update: contextMessage,
|
||||
},
|
||||
})
|
||||
if (!ingestAttempt.ok)
|
||||
continue
|
||||
|
||||
acceptedContextUpdates?.push(update)
|
||||
|
||||
if (ingestAttempt.result) {
|
||||
contextObservability.recordLifecycle({
|
||||
phase: 'store-ingested',
|
||||
channel: 'input',
|
||||
sourceKey: result.sourceKey,
|
||||
sourceKey: ingestAttempt.result.sourceKey,
|
||||
strategy: contextMessage.strategy,
|
||||
lane: contextMessage.lane,
|
||||
contextId: contextMessage.contextId,
|
||||
eventId: contextMessage.id,
|
||||
mutation: result.mutation,
|
||||
mutation: ingestAttempt.result.mutation,
|
||||
textPreview: contextMessage.text,
|
||||
sourceLabel: event.metadata?.source?.plugin?.id ?? event.metadata?.source?.id ?? event.source,
|
||||
details: {
|
||||
entryCount: result.entryCount,
|
||||
entryCount: ingestAttempt.result.entryCount,
|
||||
inputType: event.type,
|
||||
update: contextMessage,
|
||||
},
|
||||
@@ -427,7 +502,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
text,
|
||||
textRaw,
|
||||
overrides,
|
||||
contextUpdates: normalizedContextUpdates,
|
||||
contextUpdates: acceptedContextUpdates,
|
||||
},
|
||||
},
|
||||
}, targetSessionId)
|
||||
@@ -500,7 +575,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
'gen-ai:chat': {
|
||||
message: context.message as UserMessage,
|
||||
composedMessage: context.composedMessage,
|
||||
contexts: context.contexts as any,
|
||||
contexts: context.contexts,
|
||||
input: context.input,
|
||||
},
|
||||
},
|
||||
@@ -527,7 +602,7 @@ export const useContextBridgeStore = defineStore('mods:api:context-bridge', () =
|
||||
'gen-ai:chat': {
|
||||
message: context.message as UserMessage,
|
||||
composedMessage: context.composedMessage,
|
||||
contexts: context.contexts as any,
|
||||
contexts: context.contexts,
|
||||
input: context.input,
|
||||
},
|
||||
},
|
||||
|
||||
Generated
+238
-26
@@ -1453,7 +1453,7 @@ importers:
|
||||
version: 3.0.2(electron@41.2.1)
|
||||
'@electron-toolkit/tsconfig':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(@types/node@25.6.0)
|
||||
version: 2.0.0(@types/node@24.12.2)
|
||||
'@electron-toolkit/utils':
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(electron@41.2.1)
|
||||
@@ -1492,7 +1492,7 @@ importers:
|
||||
version: 3.1.0
|
||||
'@intlify/unplugin-vue-i18n':
|
||||
specifier: ^11.0.7
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@modelcontextprotocol/sdk':
|
||||
specifier: 'catalog:'
|
||||
version: 1.29.0(@cfworker/json-schema@4.1.1)(zod@4.3.6)
|
||||
@@ -1528,10 +1528,10 @@ importers:
|
||||
version: link:../../packages/ui-transitions
|
||||
'@proj-airi/unplugin-fetch':
|
||||
specifier: 'catalog:'
|
||||
version: 0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@proj-airi/unplugin-live2d-sdk':
|
||||
specifier: ^0.1.7
|
||||
version: 0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
'@types/audioworklet':
|
||||
specifier: 'catalog:'
|
||||
version: 0.0.97
|
||||
@@ -1558,7 +1558,7 @@ importers:
|
||||
version: 2.10.3
|
||||
'@vitejs/plugin-vue':
|
||||
specifier: ^6.0.6
|
||||
version: 6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar':
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
@@ -1591,7 +1591,7 @@ importers:
|
||||
version: 6.8.3
|
||||
electron-vite:
|
||||
specifier: ^5.0.0
|
||||
version: 5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
get-port-please:
|
||||
specifier: 'catalog:'
|
||||
version: 3.2.0
|
||||
@@ -1612,31 +1612,31 @@ importers:
|
||||
version: 2.2.6
|
||||
unocss-preset-scrollbar:
|
||||
specifier: ^4.0.0
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
version: 4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)))
|
||||
unplugin-info:
|
||||
specifier: ^1.3.2
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-yaml:
|
||||
specifier: ^4.1.0
|
||||
version: 4.1.0(@nuxt/kit@3.20.2(magicast@0.5.2))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite:
|
||||
specifier: 'catalog:'
|
||||
version: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
version: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-bundle-visualizer:
|
||||
specifier: ^1.2.1
|
||||
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
|
||||
vite-plugin-mkcert:
|
||||
specifier: 'catalog:'
|
||||
version: 2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
version: 2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-devtools:
|
||||
specifier: ^8.1.1
|
||||
version: 8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vite-plugin-vue-layouts:
|
||||
specifier: ^0.11.0
|
||||
version: 0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-macros:
|
||||
specifier: ^3.1.2
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
version: 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
vue-tsc:
|
||||
specifier: ^3.2.6
|
||||
version: 3.2.6(typescript@5.9.3)
|
||||
@@ -19916,9 +19916,9 @@ snapshots:
|
||||
dependencies:
|
||||
electron: 41.2.1
|
||||
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@25.6.0)':
|
||||
'@electron-toolkit/tsconfig@2.0.0(@types/node@24.12.2)':
|
||||
dependencies:
|
||||
'@types/node': 25.6.0
|
||||
'@types/node': 24.12.2
|
||||
|
||||
'@electron-toolkit/utils@4.0.0(electron@41.2.1)':
|
||||
dependencies:
|
||||
@@ -20812,6 +20812,31 @@ snapshots:
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
'@intlify/bundle-utils': 11.0.7(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))
|
||||
'@intlify/shared': 11.3.2
|
||||
'@intlify/vue-i18n-extensions': 8.0.0(@intlify/shared@11.3.2)(@vue/compiler-dom@3.5.32)(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
'@typescript-eslint/scope-manager': 8.58.1
|
||||
'@typescript-eslint/typescript-estree': 8.58.1(typescript@5.9.3)
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
pathe: 2.0.3
|
||||
picocolors: 1.1.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vue-i18n: 11.3.2(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- '@vue/compiler-dom'
|
||||
- eslint
|
||||
- rollup
|
||||
- supports-color
|
||||
- typescript
|
||||
|
||||
'@intlify/unplugin-vue-i18n@11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-i18n@11.3.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.2.1(jiti@2.6.1))
|
||||
@@ -22790,11 +22815,35 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- magicast
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-fetch@0.2.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
yauzl: 3.3.0
|
||||
transitivePeerDependencies:
|
||||
- '@types/node'
|
||||
- '@vitejs/devtools'
|
||||
- esbuild
|
||||
- jiti
|
||||
- less
|
||||
- sass
|
||||
- sass-embedded
|
||||
- stylus
|
||||
- sugarss
|
||||
- terser
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
'@proj-airi/unplugin-live2d-sdk@0.1.7(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)':
|
||||
dependencies:
|
||||
ofetch: 1.5.1
|
||||
@@ -23744,6 +23793,7 @@ snapshots:
|
||||
'@types/node@25.6.0':
|
||||
dependencies:
|
||||
undici-types: 7.19.2
|
||||
optional: true
|
||||
|
||||
'@types/nprogress@0.2.3': {}
|
||||
|
||||
@@ -24409,6 +24459,12 @@ snapshots:
|
||||
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
|
||||
'@vitejs/plugin-vue@6.0.6(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3))':
|
||||
dependencies:
|
||||
'@rolldown/pluginutils': 1.0.0-rc.13
|
||||
@@ -24489,9 +24545,9 @@ snapshots:
|
||||
obug: 2.1.1
|
||||
std-env: 4.1.0
|
||||
tinyrainbow: 3.1.0
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@25.6.0)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vitest: 4.1.4(@opentelemetry/api@1.9.1)(@types/node@24.12.2)(@vitest/browser-playwright@4.1.4)(@vitest/coverage-v8@4.1.4)(jsdom@29.0.2(@noble/hashes@2.0.1)(canvas@3.2.3))(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
optionalDependencies:
|
||||
'@vitest/browser': 4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||
'@vitest/browser': 4.1.4(bufferutil@4.1.0)(utf-8-validate@5.0.10)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vitest@4.1.4)
|
||||
|
||||
'@vitest/eslint-plugin@1.6.15(@typescript-eslint/eslint-plugin@8.58.1(@typescript-eslint/parser@8.58.1(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3))(eslint@10.2.1(jiti@2.6.1))(typescript@5.9.3)(vitest@4.1.4)':
|
||||
dependencies:
|
||||
@@ -24711,6 +24767,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- vue
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
optionalDependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- typescript
|
||||
|
||||
'@vue-macros/devtools@3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
sirv: 3.0.2
|
||||
@@ -26852,7 +26917,7 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
electron-vite@5.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-transform-arrow-functions': 7.27.1(@babel/core@7.29.0)
|
||||
@@ -26860,7 +26925,7 @@ snapshots:
|
||||
esbuild: 0.25.12
|
||||
magic-string: 0.30.21
|
||||
picocolors: 1.1.1
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
@@ -32787,7 +32852,8 @@ snapshots:
|
||||
|
||||
undici-types@7.16.0: {}
|
||||
|
||||
undici-types@7.19.2: {}
|
||||
undici-types@7.19.2:
|
||||
optional: true
|
||||
|
||||
undici@6.24.1: {}
|
||||
|
||||
@@ -32899,11 +32965,6 @@ snapshots:
|
||||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss-preset-scrollbar@4.0.0(unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))):
|
||||
dependencies:
|
||||
'@unocss/preset-mini': 66.6.8
|
||||
unocss: 66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
unocss@66.6.8(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vite@6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@unocss/cli': 66.6.8
|
||||
@@ -32988,6 +33049,14 @@ snapshots:
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
unplugin: 2.3.11
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-combine@2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
@@ -33022,6 +33091,19 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
git-url-parse: 16.1.0
|
||||
simple-git: 3.36.0
|
||||
unplugin: 2.3.11
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
unplugin-info@1.3.2(esbuild@0.27.2)(rollup@4.60.1)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ci-info: 4.4.0
|
||||
@@ -33131,6 +33213,17 @@ snapshots:
|
||||
rollup: 4.60.1
|
||||
vite: 6.4.2(@types/node@25.6.0)(jiti@2.6.1)(less@4.6.4)(lightningcss@1.32.0)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin-yaml@4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@rollup/pluginutils': 5.3.0(rollup@4.60.1)
|
||||
unplugin: 3.0.0
|
||||
yaml: 2.8.3
|
||||
optionalDependencies:
|
||||
esbuild: 0.27.2
|
||||
rolldown: 1.0.0-rc.16
|
||||
rollup: 4.60.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
unplugin@2.3.11:
|
||||
dependencies:
|
||||
'@jridgewell/remapping': 2.3.5
|
||||
@@ -33362,12 +33455,22 @@ snapshots:
|
||||
- rollup
|
||||
- supports-color
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-dev-rpc@1.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
birpc: 2.9.0
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-hot-client: 2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-hot-client@2.1.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
vite: 8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
@@ -33413,6 +33516,21 @@ snapshots:
|
||||
- tsx
|
||||
- yaml
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
error-stack-parser-es: 1.0.5
|
||||
ohash: 2.0.11
|
||||
open: 10.2.0
|
||||
perfect-debounce: 2.1.0
|
||||
sirv: 3.0.2
|
||||
unplugin-utils: 0.3.1
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-dev-rpc: 1.1.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-inspect@11.3.3(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
ansis: 4.2.0
|
||||
@@ -33444,6 +33562,13 @@ snapshots:
|
||||
- typescript
|
||||
- ws
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
supports-color: 10.2.2
|
||||
undici: 8.1.0
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
|
||||
vite-plugin-mkcert@2.0.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
@@ -33462,6 +33587,20 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue/devtools-kit': 8.1.1
|
||||
'@vue/devtools-shared': 8.1.1
|
||||
sirv: 3.0.2
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vite-plugin-inspect: 11.3.3(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
vite-plugin-vue-inspector: 5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
transitivePeerDependencies:
|
||||
- '@nuxt/kit'
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-devtools@8.1.1(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue/devtools-core': 8.1.1(vue@3.5.32(typescript@5.9.3))
|
||||
@@ -33476,6 +33615,21 @@ snapshots:
|
||||
- supports-color
|
||||
- vue
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
'@babel/plugin-proposal-decorators': 7.28.0(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-attributes': 7.28.6(@babel/core@7.29.0)
|
||||
'@babel/plugin-syntax-import-meta': 7.10.4(@babel/core@7.29.0)
|
||||
'@babel/plugin-transform-typescript': 7.28.5(@babel/core@7.29.0)
|
||||
'@vue/babel-plugin-jsx': 1.5.0(@babel/core@7.29.0)
|
||||
'@vue/compiler-dom': 3.5.32
|
||||
kolorist: 1.8.0
|
||||
magic-string: 0.30.21
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-inspector@5.3.2(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)):
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
@@ -33491,6 +33645,16 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
fast-glob: 3.3.3
|
||||
vite: 8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
vue-router: 5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
vite-plugin-vue-layouts@0.11.0(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-router@5.0.4(@pinia/colada@1.2.1(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(@vue/compiler-sfc@3.5.32)(pinia@3.0.4(typescript@5.9.3)(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
debug: 4.4.3(supports-color@10.2.2)
|
||||
@@ -33757,6 +33921,54 @@ snapshots:
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/boolean-prop': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/chain-call': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/common': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/config': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-emit': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-models': 3.1.2(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-prop': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props': 3.1.2(@vue-macros/reactivity-transform@3.1.2(vue@3.5.32(typescript@5.9.3)))(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-props-refs': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-slots': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/define-stylex': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/devtools': 3.1.2(typescript@5.9.3)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
'@vue-macros/export-expose': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-props': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/export-render': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/hoist-static': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/jsx-directive': 3.1.2(typescript@5.9.3)
|
||||
'@vue-macros/named-template': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/reactivity-transform': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/script-lang': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-block': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-component': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/setup-sfc': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-bind': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-emits': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/short-vmodel': 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
'@vue-macros/volar': 3.1.2(typescript@5.9.3)(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3))
|
||||
unplugin: 2.3.11
|
||||
unplugin-combine: 2.3.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(unplugin@2.3.11)(vite@8.0.8(@types/node@24.12.2)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))
|
||||
unplugin-vue-define-options: 3.1.2(vue@3.5.32(typescript@5.9.3))
|
||||
vue: 3.5.32(typescript@5.9.3)
|
||||
transitivePeerDependencies:
|
||||
- '@emnapi/core'
|
||||
- '@emnapi/runtime'
|
||||
- '@rspack/core'
|
||||
- '@vueuse/core'
|
||||
- esbuild
|
||||
- rolldown
|
||||
- rollup
|
||||
- typescript
|
||||
- vite
|
||||
- vue-tsc
|
||||
- webpack
|
||||
|
||||
vue-macros@3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(@vueuse/core@14.2.1(vue@3.5.32(typescript@5.9.3)))(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@4.60.1)(typescript@5.9.3)(vite@8.0.8(@types/node@25.6.0)(esbuild@0.27.2)(jiti@2.6.1)(less@4.6.4)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3))(vue-tsc@3.2.6(typescript@5.9.3))(vue@3.5.32(typescript@5.9.3)):
|
||||
dependencies:
|
||||
'@vue-macros/better-define': 3.1.2(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)(vue@3.5.32(typescript@5.9.3))
|
||||
|
||||
Reference in New Issue
Block a user