refactor(stage-ui,stage-tamagotchi): have plugin-tools, mcp-tools to register directly to llm.ts

This commit is contained in:
Neko Ayaka
2026-04-22 03:34:40 +08:00
parent 7e4486c068
commit 5bbf9559a4
18 changed files with 786 additions and 128 deletions
+52 -17
View File
@@ -9,7 +9,6 @@ import { useCharacterOrchestratorStore } from '@proj-airi/stage-ui/stores/charac
import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store' import { useChatSessionStore } from '@proj-airi/stage-ui/stores/chat/session-store'
import { usePluginHostInspectorStore } from '@proj-airi/stage-ui/stores/devtools/plugin-host-debug' import { usePluginHostInspectorStore } from '@proj-airi/stage-ui/stores/devtools/plugin-host-debug'
import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models' import { useDisplayModelsStore } from '@proj-airi/stage-ui/stores/display-models'
import { clearMcpToolBridge, setMcpToolBridge } from '@proj-airi/stage-ui/stores/mcp-tool-bridge'
import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server' import { useModsServerChannelStore } from '@proj-airi/stage-ui/stores/mods/api/channel-server'
import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge' import { useContextBridgeStore } from '@proj-airi/stage-ui/stores/mods/api/context-bridge'
import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card' import { useAiriCardStore } from '@proj-airi/stage-ui/stores/modules/airi-card'
@@ -27,12 +26,11 @@ import ResizeHandler from './components/ResizeHandler.vue'
import { import {
electronGetServerChannelConfig, electronGetServerChannelConfig,
electronMcpCallTool,
electronMcpListTools,
electronPluginInspect, electronPluginInspect,
electronPluginList, electronPluginList,
electronPluginLoad, electronPluginLoad,
electronPluginLoadEnabled, electronPluginLoadEnabled,
electronPluginSetAutoReload,
electronPluginSetEnabled, electronPluginSetEnabled,
electronPluginUnload, electronPluginUnload,
electronPluginUpdateCapability, electronPluginUpdateCapability,
@@ -44,6 +42,8 @@ import {
} from '../shared/eventa' } from '../shared/eventa'
import { initializeElectronAuthCallbackBridge } from './bridges/electron-auth-callback' import { initializeElectronAuthCallbackBridge } from './bridges/electron-auth-callback'
import { initializeStageThreeRuntimeTraceBridge } from './bridges/stage-three-runtime-trace' import { initializeStageThreeRuntimeTraceBridge } from './bridges/stage-three-runtime-trace'
import { useTamagotchiMcpToolsStore } from './stores/mcp-tools'
import { useTamagotchiPluginToolsStore } from './stores/plugin-tools'
import { useServerChannelSettingsStore } from './stores/settings/server-channel' import { useServerChannelSettingsStore } from './stores/settings/server-channel'
import { useStageWindowLifecycleStore } from './stores/stage-window-lifecycle' import { useStageWindowLifecycleStore } from './stores/stage-window-lifecycle'
@@ -63,6 +63,8 @@ const characterOrchestratorStore = useCharacterOrchestratorStore()
const analyticsStore = useSharedAnalyticsStore() const analyticsStore = useSharedAnalyticsStore()
const inferencePreload = useInferencePreload() const inferencePreload = useInferencePreload()
const pluginHostInspectorStore = usePluginHostInspectorStore() const pluginHostInspectorStore = usePluginHostInspectorStore()
const mcpToolsStore = useTamagotchiMcpToolsStore()
const pluginToolsStore = useTamagotchiPluginToolsStore()
const stageWindowLifecycleStore = useStageWindowLifecycleStore() const stageWindowLifecycleStore = useStageWindowLifecycleStore()
const settingsAudioDeviceStore = useSettingsAudioDevice() const settingsAudioDeviceStore = useSettingsAudioDevice()
const context = useElectronEventaContext() const context = useElectronEventaContext()
@@ -73,33 +75,63 @@ void stageWindowLifecycleStore.initializeWindowLifecycleBridge()
const getServerChannelConfig = useElectronEventaInvoke(electronGetServerChannelConfig) const getServerChannelConfig = useElectronEventaInvoke(electronGetServerChannelConfig)
const listPlugins = useElectronEventaInvoke(electronPluginList) const listPlugins = useElectronEventaInvoke(electronPluginList)
const setPluginEnabled = useElectronEventaInvoke(electronPluginSetEnabled) const setPluginEnabled = useElectronEventaInvoke(electronPluginSetEnabled)
const setPluginAutoReload = useElectronEventaInvoke(electronPluginSetAutoReload)
const loadEnabledPlugins = useElectronEventaInvoke(electronPluginLoadEnabled) const loadEnabledPlugins = useElectronEventaInvoke(electronPluginLoadEnabled)
const loadPlugin = useElectronEventaInvoke(electronPluginLoad) const loadPlugin = useElectronEventaInvoke(electronPluginLoad)
const unloadPlugin = useElectronEventaInvoke(electronPluginUnload) const unloadPlugin = useElectronEventaInvoke(electronPluginUnload)
const inspectPluginHost = useElectronEventaInvoke(electronPluginInspect) const inspectPluginHost = useElectronEventaInvoke(electronPluginInspect)
const startTrackingCursorPoint = useElectronEventaInvoke(electronStartTrackMousePosition) const startTrackingCursorPoint = useElectronEventaInvoke(electronStartTrackMousePosition)
const reportPluginCapability = useElectronEventaInvoke(electronPluginUpdateCapability) const reportPluginCapability = useElectronEventaInvoke(electronPluginUpdateCapability)
const listMcpTools = useElectronEventaInvoke(electronMcpListTools)
const callMcpTool = useElectronEventaInvoke(electronMcpCallTool)
const setLocale = useElectronEventaInvoke(i18nSetLocale) const setLocale = useElectronEventaInvoke(i18nSetLocale)
const isChatWindowRoute = () => route.path === '/chat' const isChatWindowRoute = () => route.path === '/chat'
const isWidgetsWindowRoute = () => route.path === '/widgets'
async function refreshPluginRuntimeTools() {
try {
await pluginToolsStore.refresh()
}
catch (error) {
console.warn('[App] Failed to refresh plugin runtime tools:', error)
}
}
watch(() => route.path, () => {
contextBridgeStore.setSparkNotifyHostRole(isWidgetsWindowRoute() ? 'client' : 'main')
}, { immediate: true })
// NOTICE: register plugin host bridge during setup to avoid race with pages using it in immediate watchers. // NOTICE: register plugin host bridge during setup to avoid race with pages using it in immediate watchers.
pluginHostInspectorStore.setBridge({ pluginHostInspectorStore.setBridge({
list: () => listPlugins(), list: () => listPlugins(),
setEnabled: payload => setPluginEnabled(payload), setEnabled: async (payload) => {
loadEnabled: () => loadEnabledPlugins(), const result = await setPluginEnabled(payload)
load: payload => loadPlugin(payload), await refreshPluginRuntimeTools()
unload: payload => unloadPlugin(payload), return result
},
setAutoReload: payload => setPluginAutoReload(payload),
loadEnabled: async () => {
const result = await loadEnabledPlugins()
await refreshPluginRuntimeTools()
return result
},
load: async (payload) => {
const result = await loadPlugin(payload)
await refreshPluginRuntimeTools()
return result
},
unload: async (payload) => {
const result = await unloadPlugin(payload)
await refreshPluginRuntimeTools()
return result
},
inspect: () => inspectPluginHost(), inspect: () => inspectPluginHost(),
}) })
// NOTICE: MCP tools are declared from stage-ui and executed during model streaming. // NOTICE: Runtime tool stores must register during setup so renderer consumers can see them
// Register runtime bridge during setup to avoid missing bridge in early tool invocations. // before `onMounted()` finishes the rest of the startup flow.
setMcpToolBridge({ void mcpToolsStore.refresh().catch((error) => {
listTools: () => listMcpTools(), console.warn('[App] Failed to refresh MCP runtime tools:', error)
callTool: payload => callMcpTool(payload),
}) })
void refreshPluginRuntimeTools()
watch(language, () => { watch(language, () => {
i18n.locale.value = language.value i18n.locale.value = language.value
@@ -143,8 +175,10 @@ onMounted(async () => {
}).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err)) }).catch(err => console.error('Failed to initialize Mods Server Channel in App.vue:', err))
if (!isChatWindowRoute()) { if (!isChatWindowRoute()) {
contextBridgeStore.initialize() contextBridgeStore.initialize()
characterOrchestratorStore.initialize() if (!isWidgetsWindowRoute()) {
await startTrackingCursorPoint() characterOrchestratorStore.initialize()
await startTrackingCursorPoint()
}
} }
// Expose stage provider definitions to plugin host APIs. // Expose stage provider definitions to plugin host APIs.
@@ -176,7 +210,8 @@ onUnmounted(() => {
if (!isChatWindowRoute()) { if (!isChatWindowRoute()) {
contextBridgeStore.dispose() contextBridgeStore.dispose()
} }
clearMcpToolBridge() mcpToolsStore.dispose()
pluginToolsStore.dispose()
}) })
</script> </script>
@@ -0,0 +1,95 @@
import type { Tool } from '@xsai/shared-chat'
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const invokeMocks = vi.hoisted(() => ({
callMcpTool: vi.fn(async () => ({
content: [{ type: 'text', text: 'ok' }],
isError: false,
})),
listMcpTools: vi.fn(async () => [{
serverName: 'filesystem',
name: 'filesystem::search',
toolName: 'search',
description: 'Search files.',
inputSchema: {
type: 'object',
properties: {},
},
}]),
}))
vi.mock('@proj-airi/electron-vueuse', () => ({
useElectronEventaInvoke: (event: { receiveEvent?: { id?: string } }) => {
if (event?.receiveEvent?.id === 'eventa:invoke:electron:mcp:list-tools-receive')
return invokeMocks.listMcpTools
if (event?.receiveEvent?.id === 'eventa:invoke:electron:mcp:call-tool-receive')
return invokeMocks.callMcpTool
throw new Error(`Unexpected eventa invoke: ${JSON.stringify(event)}`)
},
}))
describe('useTamagotchiMcpToolsStore', async () => {
const { useTamagotchiMcpToolsStore } = await import('./mcp-tools')
beforeEach(() => {
setActivePinia(createPinia())
invokeMocks.listMcpTools.mockClear()
invokeMocks.callMcpTool.mockClear()
})
/**
* @example
* await store.refresh()
* expect(llmToolsStore.toolsByProvider.mcp).toHaveLength(2)
*/
it('loads MCP tools, proxies execution, and clears them from the shared llm-tools store', async () => {
const llmToolsStore = useLlmToolsStore()
const store = useTamagotchiMcpToolsStore()
const toolOptions = {} as Parameters<Tool['execute']>[1]
await store.refresh()
const mcpTools = llmToolsStore.toolsByProvider.mcp
const listTools = mcpTools?.find(tool => tool.function.name === 'builtIn_mcpListTools')
const callTool = mcpTools?.find(tool => tool.function.name === 'builtIn_mcpCallTool')
expect(mcpTools).toEqual([
expect.objectContaining({ function: expect.objectContaining({ name: 'builtIn_mcpListTools' }) }),
expect.objectContaining({ function: expect.objectContaining({ name: 'builtIn_mcpCallTool' }) }),
])
const listResult = await listTools?.execute({}, toolOptions)
const callResult = await callTool?.execute({
name: 'filesystem::search',
arguments: JSON.stringify({ query: 'hello', limit: 10 }),
}, toolOptions)
expect(invokeMocks.listMcpTools).toHaveBeenCalledTimes(1)
expect(invokeMocks.callMcpTool).toHaveBeenCalledWith({
name: 'filesystem::search',
arguments: { query: 'hello', limit: 10 },
})
expect(listResult).toEqual([{
serverName: 'filesystem',
name: 'filesystem::search',
toolName: 'search',
description: 'Search files.',
inputSchema: {
type: 'object',
properties: {},
},
}])
expect(callResult).toEqual({
content: [{ type: 'text', text: 'ok' }],
isError: false,
})
store.dispose()
expect(llmToolsStore.toolsByProvider.mcp).toBeUndefined()
})
})
@@ -0,0 +1,40 @@
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
import { createMcpTools } from '@proj-airi/stage-ui/tools/mcp'
import { defineStore } from 'pinia'
import { electronMcpCallTool, electronMcpListTools } from '../../shared/eventa'
/**
* Registers Electron-backed MCP tools into the shared LLM tools store.
*
* Use when:
* - The Tamagotchi renderer needs live MCP tools during chat streaming
*
* Expects:
* - Electron Eventa handlers for MCP listing and invocation are available
*
* Returns:
* - Store actions for refreshing and disposing MCP runtime tools
*/
export const useTamagotchiMcpToolsStore = defineStore('tamagotchi-mcp-tools', () => {
const llmToolsStore = useLlmToolsStore()
const listMcpTools = useElectronEventaInvoke(electronMcpListTools)
const callMcpTool = useElectronEventaInvoke(electronMcpCallTool)
async function refresh() {
return llmToolsStore.registerTools('mcp', Promise.all(createMcpTools({
listTools: () => listMcpTools(),
callTool: payload => callMcpTool(payload),
})))
}
function dispose() {
llmToolsStore.clearTools('mcp')
}
return {
dispose,
refresh,
}
})
@@ -0,0 +1,84 @@
import type { Tool } from '@xsai/shared-chat'
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
const invokeMocks = vi.hoisted(() => ({
invokePluginTool: vi.fn(async (payload: unknown) => payload),
listPluginXsaiTools: vi.fn(async () => [
{
ownerPluginId: 'plugin-chess',
name: 'play_chess',
description: 'Play a chess move.',
parameters: {
type: 'object',
properties: {},
},
},
]),
}))
vi.mock('@proj-airi/electron-vueuse', () => ({
useElectronEventaInvoke: (event: { receiveEvent?: { id?: string } }) => {
if (event?.receiveEvent?.id === 'eventa:invoke:electron:plugins:tools:list-xsai-receive')
return invokeMocks.listPluginXsaiTools
if (event?.receiveEvent?.id === 'eventa:invoke:electron:plugins:tools:invoke-receive')
return invokeMocks.invokePluginTool
throw new Error(`Unexpected eventa invoke: ${JSON.stringify(event)}`)
},
}))
describe('useTamagotchiPluginToolsStore', async () => {
const { useTamagotchiPluginToolsStore } = await import('./plugin-tools')
beforeEach(() => {
setActivePinia(createPinia())
invokeMocks.listPluginXsaiTools.mockClear()
invokeMocks.invokePluginTool.mockClear()
})
/**
* @example
* await store.refresh()
* expect(llmToolsStore.toolsByProvider['plugin-tools']).toHaveLength(1)
*/
it('loads plugin xsai tools, proxies execution, and clears them from the shared llm-tools store', async () => {
const llmToolsStore = useLlmToolsStore()
const store = useTamagotchiPluginToolsStore()
const toolOptions = {} as Parameters<Tool['execute']>[1]
await store.refresh()
const pluginTools = llmToolsStore.toolsByProvider['plugin-tools']
const playChessTool = pluginTools?.find(tool => tool.function.name === 'play_chess')
expect(pluginTools).toEqual([
expect.objectContaining({ function: expect.objectContaining({ name: 'play_chess' }) }),
])
const executionResult = await playChessTool?.execute({
move: 'e2e4',
}, toolOptions)
expect(invokeMocks.invokePluginTool).toHaveBeenCalledWith({
ownerPluginId: 'plugin-chess',
name: 'play_chess',
input: {
move: 'e2e4',
},
})
expect(executionResult).toEqual({
ownerPluginId: 'plugin-chess',
name: 'play_chess',
input: {
move: 'e2e4',
},
})
store.dispose()
expect(llmToolsStore.toolsByProvider['plugin-tools']).toBeUndefined()
})
})
@@ -0,0 +1,50 @@
import { useElectronEventaInvoke } from '@proj-airi/electron-vueuse'
import { useLlmToolsStore } from '@proj-airi/stage-ui/stores/llm-tools'
import { rawTool } from '@xsai/tool'
import { defineStore } from 'pinia'
import { electronPluginInvokeTool, electronPluginListXsaiTools } from '../../shared/eventa'
/**
* Registers Electron-backed plugin xsai tools into the shared LLM tools store.
*
* Use when:
* - The Tamagotchi renderer needs plugin-provided xsai tools during chat streaming
*
* Expects:
* - Electron Eventa handlers for listing and invoking plugin tools are available
*
* Returns:
* - Store actions for refreshing and disposing plugin runtime tools
*/
export const useTamagotchiPluginToolsStore = defineStore('tamagotchi-plugin-tools', () => {
const llmToolsStore = useLlmToolsStore()
const listPluginXsaiToolDefinitions = useElectronEventaInvoke(electronPluginListXsaiTools)
const invokePluginTool = useElectronEventaInvoke(electronPluginInvokeTool)
async function refresh() {
return llmToolsStore.registerTools('plugin-tools', listPluginXsaiToolDefinitions().then(definitions =>
definitions.map(definition =>
rawTool({
name: definition.name,
description: definition.description,
parameters: definition.parameters,
execute: async input => invokePluginTool({
ownerPluginId: definition.ownerPluginId,
name: definition.name,
input,
}),
}),
),
))
}
function dispose() {
llmToolsStore.clearTools('plugin-tools')
}
return {
dispose,
refresh,
}
})
+1
View File
@@ -112,6 +112,7 @@ words:
- formkit - formkit
- frontmatter - frontmatter
- gamelet - gamelet
- gamelets
- Genshin - Genshin
- giteeai - giteeai
- gltf - gltf
@@ -105,7 +105,9 @@ export async function streamFrom({
headers: options?.headers, headers: options?.headers,
stopWhen: stepCountAtLeast(10), stopWhen: stepCountAtLeast(10),
tools, tools,
captureToolErrors: true, // NOTICE: Some OpenAI-compatible gateways reject the wire-level
// `capture_tool_errors` parameter with 400 unknown_parameter.
// Keep this unset here so the request remains provider-compatible.
onEvent, onEvent,
}) })
+2
View File
@@ -29,11 +29,13 @@
"./libs/inference": "./src/libs/inference/index.ts", "./libs/inference": "./src/libs/inference/index.ts",
"./libs/*": "./src/libs/*.ts", "./libs/*": "./src/libs/*.ts",
"./libs": "./src/libs/index.ts", "./libs": "./src/libs/index.ts",
"./tools/mcp": "./src/tools/mcp.ts",
"./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts", "./stores/providers/aliyun": "./src/stores/providers/aliyun/index.ts",
"./stores/analytics": "./src/stores/analytics/index.ts", "./stores/analytics": "./src/stores/analytics/index.ts",
"./stores/analytics/posthog": "./src/stores/analytics/posthog.ts", "./stores/analytics/posthog": "./src/stores/analytics/posthog.ts",
"./stores/analytics/privacy-policy": "./src/stores/analytics/privacy-policy.ts", "./stores/analytics/privacy-policy": "./src/stores/analytics/privacy-policy.ts",
"./stores/character": "./src/stores/character/index.ts", "./stores/character": "./src/stores/character/index.ts",
"./stores/character/orchestrator/spark-notify-agent": "./src/stores/character/orchestrator/spark-notify-agent.ts",
"./stores/settings/analytics": "./src/stores/settings/analytics.ts", "./stores/settings/analytics": "./src/stores/settings/analytics.ts",
"./stores/settings": "./src/stores/settings/index.ts", "./stores/settings": "./src/stores/settings/index.ts",
"./stores/modules/vision": "./src/stores/modules/vision/index.ts", "./stores/modules/vision": "./src/stores/modules/vision/index.ts",
@@ -0,0 +1,128 @@
import type { Tool } from '@xsai/shared-chat'
import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest'
import { useLlmToolsStore } from './llm-tools'
describe('useLlmToolsStore', () => {
beforeEach(() => {
setActivePinia(createPinia())
})
it('registers and merges tools by provider', () => {
const store = useLlmToolsStore()
const mcpTool = { function: { name: 'builtIn_mcpListTools' } } as Tool
const pluginTool = { function: { name: 'play_chess' } } as Tool
store.registerTools('mcp', [mcpTool])
store.registerTools('plugin-tools', [pluginTool])
expect(store.toolsByProvider).toEqual({
'mcp': [mcpTool],
'plugin-tools': [pluginTool],
})
expect(store.activeTools).toEqual([mcpTool, pluginTool])
})
it('replaces tools for the same provider instead of appending forever', () => {
const store = useLlmToolsStore()
const first = { function: { name: 'first' } } as Tool
const second = { function: { name: 'second' } } as Tool
store.registerTools('plugin-tools', [first])
store.registerTools('plugin-tools', [second])
expect(store.toolsByProvider).toEqual({
'plugin-tools': [second],
})
expect(store.activeTools).toEqual([second])
})
it('clears one provider without touching the others', () => {
const store = useLlmToolsStore()
const mcpTool = { function: { name: 'builtIn_mcpListTools' } } as Tool
const pluginTool = { function: { name: 'play_chess' } } as Tool
store.registerTools('mcp', [mcpTool])
store.registerTools('plugin-tools', [pluginTool])
store.clearTools('plugin-tools')
expect(store.toolsByProvider).toEqual({
mcp: [mcpTool],
})
expect(store.activeTools).toEqual([mcpTool])
})
it('does not change store state when the caller mutates the registered array later', () => {
const store = useLlmToolsStore()
const first = { function: { name: 'first' } } as Tool
const second = { function: { name: 'second' } } as Tool
const tools = [first]
store.registerTools('plugin-tools', tools)
tools.push(second)
expect(store.toolsByProvider).toEqual({
'plugin-tools': [first],
})
expect(store.activeTools).toEqual([first])
})
/**
* @example
* store.registerTools('plugin-tools', Promise.resolve([pluginTool]))
* await store.awaitPendingRegistrations()
*/
it('waits for async tool registrations before exposing them as active tools', async () => {
const store = useLlmToolsStore()
const pluginTool = { function: { name: 'play_chess' } } as Tool
let resolveTools: ((tools: Tool[]) => void) | undefined
const pendingTools = new Promise<Tool[]>((resolve) => {
resolveTools = resolve
})
const onSettled = vi.fn()
store.registerTools('plugin-tools', pendingTools)
const pendingWait = store.awaitPendingRegistrations().then(() => {
onSettled()
})
await Promise.resolve()
expect(store.toolsByProvider['plugin-tools']).toBeUndefined()
expect(store.activeTools).toEqual([])
expect(onSettled).not.toHaveBeenCalled()
resolveTools?.([pluginTool])
await pendingWait
expect(onSettled).toHaveBeenCalledTimes(1)
expect(store.toolsByProvider['plugin-tools']).toEqual([pluginTool])
expect(store.activeTools).toEqual([pluginTool])
})
/**
* @example
* store.registerTools('plugin-tools', slowTools)
* store.registerTools('plugin-tools', [latestTool])
*/
it('ignores stale async registrations after newer tools replace the same provider', async () => {
const store = useLlmToolsStore()
const staleTool = { function: { name: 'stale' } } as Tool
const latestTool = { function: { name: 'latest' } } as Tool
let resolveTools: ((tools: Tool[]) => void) | undefined
const pendingTools = new Promise<Tool[]>((resolve) => {
resolveTools = resolve
})
store.registerTools('plugin-tools', pendingTools)
store.registerTools('plugin-tools', [latestTool])
resolveTools?.([staleTool])
await store.awaitPendingRegistrations()
expect(store.toolsByProvider['plugin-tools']).toEqual([latestTool])
expect(store.activeTools).toEqual([latestTool])
})
})
+81
View File
@@ -0,0 +1,81 @@
import type { Tool } from '@xsai/shared-chat'
import { defineStore } from 'pinia'
import { computed, ref } from 'vue'
type ToolRegistration = Promise<Tool[]> | Tool[]
/**
* Stores runtime-registered xsai tools keyed by provider.
*
* Use when:
* - App runtimes need to publish additional LLM tools into shared stage-ui logic
*
* Expects:
* - Provider names are stable identifiers such as `mcp` or `plugin-tools`
*
* Returns:
* - A merged reactive list of all currently registered tools
*/
export const useLlmToolsStore = defineStore('llm-tools', () => {
const toolsByProvider = ref<Record<string, Tool[]>>({})
const providerRegistrationTokens = new Map<string, symbol>()
const pendingRegistrations = new Map<string, Promise<void>>()
function assignTools(provider: string, tools: Tool[]) {
toolsByProvider.value = {
...toolsByProvider.value,
[provider]: [...tools],
}
}
function registerTools(provider: string, tools: ToolRegistration) {
const registrationToken = Symbol(provider)
providerRegistrationTokens.set(provider, registrationToken)
if (Array.isArray(tools)) {
pendingRegistrations.delete(provider)
assignTools(provider, tools)
return Promise.resolve([...tools])
}
const registration = Promise.resolve(tools)
.then((resolvedTools) => {
if (providerRegistrationTokens.get(provider) !== registrationToken)
return resolvedTools
assignTools(provider, resolvedTools)
return resolvedTools
})
.finally(() => {
if (providerRegistrationTokens.get(provider) === registrationToken)
pendingRegistrations.delete(provider)
})
pendingRegistrations.set(provider, registration.then(() => undefined, () => undefined))
return registration
}
function clearTools(provider: string) {
providerRegistrationTokens.set(provider, Symbol(provider))
pendingRegistrations.delete(provider)
const { [provider]: _removed, ...remaining } = toolsByProvider.value
toolsByProvider.value = remaining
}
async function awaitPendingRegistrations() {
while (pendingRegistrations.size > 0)
await Promise.all(pendingRegistrations.values())
}
const activeTools = computed(() => Object.values(toolsByProvider.value).flat())
// TODO: Track provider support/loading/error state if runtime diagnostics need it later.
return {
activeTools,
awaitPendingRegistrations,
clearTools,
registerTools,
toolsByProvider,
}
})
+153 -4
View File
@@ -1,10 +1,11 @@
import type { ChatProvider } from '@xsai-ext/providers/utils' import type { ChatProvider } from '@xsai-ext/providers/utils'
import type { Message } from '@xsai/shared-chat' import type { Message, Tool } from '@xsai/shared-chat'
import { createPinia, setActivePinia } from 'pinia' import { createPinia, setActivePinia } from 'pinia'
import { beforeEach, describe, expect, it, vi } from 'vitest' import { beforeEach, describe, expect, it, vi } from 'vitest'
import { isToolRelatedError, useLLM } from './llm' import { isToolRelatedError, useLLM } from './llm'
import { useLlmToolsStore } from './llm-tools'
const { const {
streamTextMock, streamTextMock,
@@ -13,9 +14,9 @@ const {
createSparkCommandToolMock, createSparkCommandToolMock,
} = vi.hoisted(() => ({ } = vi.hoisted(() => ({
streamTextMock: vi.fn(), streamTextMock: vi.fn(),
mcpMock: vi.fn(async () => []), mcpMock: vi.fn(async (): Promise<Tool[]> => []),
debugMock: vi.fn(async () => []), debugMock: vi.fn(async (): Promise<Tool[]> => []),
createSparkCommandToolMock: vi.fn(async () => ({ createSparkCommandToolMock: vi.fn(async (): Promise<unknown> => ({
name: 'spark', name: 'spark',
description: '', description: '',
parameters: {}, parameters: {},
@@ -56,6 +57,20 @@ function createMockStreamResult() {
} }
} }
function toolNameFrom(tool: unknown) {
if (typeof tool !== 'object' || tool === null)
return undefined
const candidate = tool as {
name?: string
function?: {
name?: string
}
}
return candidate.function?.name ?? candidate.name
}
describe('isToolRelatedError', () => { describe('isToolRelatedError', () => {
beforeEach(() => { beforeEach(() => {
streamTextMock.mockReset() streamTextMock.mockReset()
@@ -158,7 +173,18 @@ describe('isToolRelatedError', () => {
it('keeps builtin tools and auto-disables tools after tool-related errors', async () => { it('keeps builtin tools and auto-disables tools after tool-related errors', async () => {
const store = useLLM() const store = useLLM()
const llmToolsStore = useLlmToolsStore()
const customTool = { name: 'custom-tool' } as any const customTool = { name: 'custom-tool' } as any
const runtimeTool = {
function: {
name: 'runtime_play_chess_match',
description: 'Start a runtime chess match.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(),
}
llmToolsStore.registerTools('plugin-tools', [runtimeTool as any])
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => { streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => { queueMicrotask(async () => {
@@ -176,6 +202,7 @@ describe('isToolRelatedError', () => {
expect(mcpMock).toHaveBeenCalledTimes(1) expect(mcpMock).toHaveBeenCalledTimes(1)
expect(debugMock).toHaveBeenCalledTimes(1) expect(debugMock).toHaveBeenCalledTimes(1)
expect(firstCallTools).toContain(customTool) expect(firstCallTools).toContain(customTool)
expect(firstCallTools?.map(toolNameFrom)).toContain('runtime_play_chess_match')
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => { streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => { queueMicrotask(async () => {
@@ -191,4 +218,126 @@ describe('isToolRelatedError', () => {
const secondCallTools = streamTextMock.mock.calls[1]?.[0]?.tools const secondCallTools = streamTextMock.mock.calls[1]?.[0]?.tools
expect(secondCallTools).toBeUndefined() expect(secondCallTools).toBeUndefined()
}) })
it('merges runtime-registered tools from the llm-tools store into the builtin tool resolver', async () => {
const store = useLLM()
const llmToolsStore = useLlmToolsStore()
const playChessTool = {
function: {
name: 'runtime_open_chess_board',
description: 'Open the runtime chess board.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(),
}
const runtimeMcpStatusTool = {
function: {
name: 'runtime_sync_mcp_status',
description: 'Sync runtime MCP status.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(),
}
llmToolsStore.registerTools('mcp', [runtimeMcpStatusTool as any])
llmToolsStore.registerTools('plugin-tools', [playChessTool as any])
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'finish', finishReason: 'stop' })
})
return createMockStreamResult()
})
await store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools
expect(mergedTools).toEqual(expect.arrayContaining([runtimeMcpStatusTool, playChessTool]))
})
it('prefers runtime-registered tools when duplicate tool names collide with builtin tools', async () => {
const store = useLLM()
const llmToolsStore = useLlmToolsStore()
const builtinTool = {
function: {
name: 'duplicate_runtime_tool',
description: 'Builtin version.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(),
} as unknown as Tool
const runtimeTool = {
function: {
name: 'duplicate_runtime_tool',
description: 'Runtime version.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(),
}
mcpMock.mockResolvedValueOnce([builtinTool] as Tool[])
llmToolsStore.registerTools('plugin-tools', [runtimeTool as any])
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'finish', finishReason: 'stop' })
})
return createMockStreamResult()
})
await store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools as Array<{ function?: { name?: string } }>
const duplicateNameTools = mergedTools.filter(tool => tool.function?.name === 'duplicate_runtime_tool')
expect(duplicateNameTools).toHaveLength(1)
expect(duplicateNameTools[0]).toMatchObject({
function: {
name: 'duplicate_runtime_tool',
description: 'Runtime version.',
},
})
})
/**
* @example
* llmToolsStore.registerTools('plugin-tools', pendingRuntimeTools)
* await store.stream('model-a', provider, messages)
*/
it('waits for pending runtime tool registrations before building stream tools', async () => {
const store = useLLM()
const llmToolsStore = useLlmToolsStore()
const runtimeTool = {
function: {
name: 'runtime_pending_tool',
description: 'Pending runtime tool.',
parameters: { type: 'object', properties: {} },
},
execute: vi.fn(),
}
let resolveTools: ((tools: unknown[]) => void) | undefined
const pendingTools = new Promise<unknown[]>((resolve) => {
resolveTools = resolve
})
llmToolsStore.registerTools('plugin-tools', pendingTools as Promise<any[]>)
streamTextMock.mockImplementationOnce((options: { onEvent: (event: unknown) => Promise<void>, tools?: unknown[] }) => {
queueMicrotask(async () => {
await options.onEvent({ type: 'finish', finishReason: 'stop' })
})
return createMockStreamResult()
})
const pendingStream = store.stream('model-a', provider, [{ role: 'user', content: 'play chess' }] as Message[])
await Promise.resolve()
expect(streamTextMock).not.toHaveBeenCalled()
resolveTools?.([runtimeTool])
await pendingStream
const mergedTools = streamTextMock.mock.calls[0]?.[0]?.tools
expect(mergedTools?.map(toolNameFrom)).toContain('runtime_pending_tool')
})
}) })
+28 -6
View File
@@ -1,22 +1,36 @@
import type { StreamOptions } from '@proj-airi/core-agent' import type { StreamOptions } from '@proj-airi/core-agent'
import type { WebSocketEvents } from '@proj-airi/server-sdk' import type { WebSocketEvents } from '@proj-airi/server-sdk'
import type { ChatProvider } from '@xsai-ext/providers/utils' import type { ChatProvider } from '@xsai-ext/providers/utils'
import type { Message } from '@xsai/shared-chat' import type { Message, Tool } from '@xsai/shared-chat'
import { streamFrom as coreStreamFrom, isToolRelatedError, modelKey } from '@proj-airi/core-agent' import { streamFrom as coreStreamFrom, isToolRelatedError, modelKey } from '@proj-airi/core-agent'
import { listModels } from '@xsai/model' import { listModels } from '@xsai/model'
import { uniqBy } from 'es-toolkit'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
import { ref } from 'vue' import { ref } from 'vue'
import { createSparkCommandTool, debug, mcp } from '../tools' import { createSparkCommandTool, debug, mcp } from '../tools'
import { useLlmToolsStore } from './llm-tools'
import { useModsServerChannelStore } from './mods/api/channel-server' import { useModsServerChannelStore } from './mods/api/channel-server'
export type { StreamEvent, StreamOptions } from '@proj-airi/core-agent' export type { StreamEvent, StreamOptions } from '@proj-airi/core-agent'
export { isToolRelatedError } from '@proj-airi/core-agent' export { isToolRelatedError } from '@proj-airi/core-agent'
function toolNameFrom(tool: Tool) {
const candidate = tool as Tool & {
name?: string
function?: {
name?: string
}
}
return candidate.function?.name ?? candidate.name
}
export const useLLM = defineStore('llm', () => { export const useLLM = defineStore('llm', () => {
const toolsCompatibility = ref<Map<string, boolean>>(new Map()) const toolsCompatibility = ref<Map<string, boolean>>(new Map())
const modsServerChannelStore = useModsServerChannelStore() const modsServerChannelStore = useModsServerChannelStore()
const llmToolsStore = useLlmToolsStore()
async function stream(model: string, chatProvider: ChatProvider, messages: Message[], options?: StreamOptions) { async function stream(model: string, chatProvider: ChatProvider, messages: Message[], options?: StreamOptions) {
const key = modelKey(model, chatProvider) const key = modelKey(model, chatProvider)
@@ -42,11 +56,19 @@ export const useLLM = defineStore('llm', () => {
chatProvider, chatProvider,
messages, messages,
options: { ...options, toolsCompatibility: toolsCompatibility.value }, options: { ...options, toolsCompatibility: toolsCompatibility.value },
builtinToolsResolver: async () => [ builtinToolsResolver: async () => {
...await mcp(), await llmToolsStore.awaitPendingRegistrations()
...await debug(),
await createSparkCommandTool({ sendSparkCommand }), // Reverse twice so later runtime registrations win while original tool order stays stable.
], return uniqBy(
[
...await mcp(),
...await debug(),
...await createSparkCommandTool({ sendSparkCommand }),
].toReversed(),
tool => toolNameFrom(tool) ?? tool,
).toReversed()
},
}) })
} }
catch (err) { catch (err) {
@@ -1,42 +0,0 @@
export interface McpToolDescriptor {
serverName: string
name: string
toolName: string
description?: string
inputSchema: Record<string, unknown>
}
export interface McpCallToolPayload {
name: string
arguments?: Record<string, unknown>
}
export interface McpCallToolResult {
content?: Array<Record<string, unknown>>
structuredContent?: Record<string, unknown>
toolResult?: unknown
isError?: boolean
}
interface McpToolBridge {
listTools: () => Promise<McpToolDescriptor[]>
callTool: (payload: McpCallToolPayload) => Promise<McpCallToolResult>
}
let bridge: McpToolBridge | undefined
export function setMcpToolBridge(nextBridge: McpToolBridge) {
bridge = nextBridge
}
export function clearMcpToolBridge() {
bridge = undefined
}
export function getMcpToolBridge(): McpToolBridge {
if (!bridge) {
throw new Error('MCP tool bridge is not available in this runtime.')
}
return bridge
}
@@ -88,20 +88,20 @@ describe('tools/character/orchestrator/spark-command', () => {
}) })
it('emits a strict parameter schema', async () => { it('emits a strict parameter schema', async () => {
const tool = await createSparkCommandTool({ const tools = await createSparkCommandTool({
sendSparkCommand: () => undefined, sendSparkCommand: () => undefined,
}) })
expect(tool.function.name).toBe('builtIn_emitSparkCommand') expect(tools[0].function.name).toBe('builtIn_emitSparkCommand')
expect(tool.function.parameters.additionalProperties).toBe(false) expect(tools[0].function.parameters.additionalProperties).toBe(false)
}) })
it('avoids propertyNames in provider-facing schema', async () => { it('avoids propertyNames in provider-facing schema', async () => {
const tool = await createSparkCommandTool({ const tools = await createSparkCommandTool({
sendSparkCommand: () => undefined, sendSparkCommand: () => undefined,
}) })
const schema = tool.function.parameters as JsonSchema const schema = tools[0].function.parameters as JsonSchema
const guidance = getObjectSchema(schema.properties?.guidance as JsonSchema) const guidance = getObjectSchema(schema.properties?.guidance as JsonSchema)
const guidancePersona = guidance?.properties?.persona as JsonSchema const guidancePersona = guidance?.properties?.persona as JsonSchema
const contexts = getArraySchema(schema.properties?.contexts as JsonSchema) const contexts = getArraySchema(schema.properties?.contexts as JsonSchema)
@@ -113,11 +113,11 @@ describe('tools/character/orchestrator/spark-command', () => {
}) })
it('uses explicit required keys for nested strict option objects', async () => { it('uses explicit required keys for nested strict option objects', async () => {
const tool = await createSparkCommandTool({ const tools = await createSparkCommandTool({
sendSparkCommand: () => undefined, sendSparkCommand: () => undefined,
}) })
const schema = tool.function.parameters as JsonSchema const schema = tools[0].function.parameters as JsonSchema
expect(schema.required).toEqual([ expect(schema.required).toEqual([
'destinations', 'destinations',
'interrupt', 'interrupt',
@@ -170,11 +170,11 @@ describe('tools/character/orchestrator/spark-command', () => {
it('builds and dispatches spark commands with generated ids', async () => { it('builds and dispatches spark commands with generated ids', async () => {
const sendSparkCommand = vi.fn() const sendSparkCommand = vi.fn()
const tool = await createSparkCommandTool({ const tools = await createSparkCommandTool({
sendSparkCommand, sendSparkCommand,
}) })
const result = await tool.execute({ const result = await tools[0].execute({
destinations: ['minecraft'], destinations: ['minecraft'],
interrupt: 'soft', interrupt: 'soft',
priority: 'high', priority: 'high',
@@ -26,45 +26,47 @@ export async function createSparkCommandTool(options: CreateSparkCommandToolOpti
// like Azure that reject some `anyOf` nullable forms and strict-object optional-field shapes. // like Azure that reject some `anyOf` nullable forms and strict-object optional-field shapes.
const parameters = normalizeNullableAnyOf(await toJsonSchema(sparkCommandToolSchema) as any) const parameters = normalizeNullableAnyOf(await toJsonSchema(sparkCommandToolSchema) as any)
return rawTool({ return [
name: 'builtIn_emitSparkCommand', rawTool({
description: 'Send a spark:command to one or more frontend-connected modules or sub-agents.', name: 'builtIn_emitSparkCommand',
parameters, description: 'Send a spark:command to one or more frontend-connected modules or sub-agents.',
execute: async (rawPayload) => { parameters,
const payload = rawPayload as z.infer<typeof sparkCommandToolSchema> execute: async (rawPayload) => {
const command = { const payload = rawPayload as z.infer<typeof sparkCommandToolSchema>
id: nanoid(), const command = {
eventId: nanoid(),
parentEventId: payload.parentEventId ?? undefined,
commandId: nanoid(),
interrupt: payload.interrupt ?? false,
priority: payload.priority ?? 'normal',
intent: payload.intent ?? 'action',
ack: payload.ack ?? undefined,
guidance: payload.guidance
? {
type: payload.guidance.type,
persona: normalizeSparkCommandPersona(payload.guidance.persona ?? undefined),
options: normalizeSparkCommandGuidanceOptions(payload.guidance.options),
}
: undefined,
contexts: payload.contexts?.map(context => ({
id: nanoid(), id: nanoid(),
contextId: nanoid(), eventId: nanoid(),
lane: normalizeSparkCommandStringValue(context.lane), parentEventId: payload.parentEventId ?? undefined,
ideas: normalizeSparkCommandStringList(context.ideas), commandId: nanoid(),
hints: normalizeSparkCommandStringList(context.hints), interrupt: payload.interrupt ?? false,
strategy: context.strategy, priority: payload.priority ?? 'normal',
text: context.text, intent: payload.intent ?? 'action',
destinations: normalizeSparkCommandDestinations(context.destinations), ack: payload.ack ?? undefined,
metadata: normalizeSparkCommandMetadata(context.metadata ?? undefined), guidance: payload.guidance
})), ? {
destinations: payload.destinations, type: payload.guidance.type,
} satisfies WebSocketEvents['spark:command'] persona: normalizeSparkCommandPersona(payload.guidance.persona ?? undefined),
options: normalizeSparkCommandGuidanceOptions(payload.guidance.options),
}
: undefined,
contexts: payload.contexts?.map(context => ({
id: nanoid(),
contextId: nanoid(),
lane: normalizeSparkCommandStringValue(context.lane),
ideas: normalizeSparkCommandStringList(context.ideas),
hints: normalizeSparkCommandStringList(context.hints),
strategy: context.strategy,
text: context.text,
destinations: normalizeSparkCommandDestinations(context.destinations),
metadata: normalizeSparkCommandMetadata(context.metadata ?? undefined),
})),
destinations: payload.destinations,
} satisfies WebSocketEvents['spark:command']
options.sendSparkCommand(command) options.sendSparkCommand(command)
return `spark:command sent (${command.commandId}) to ${command.destinations.join(', ')}` return `spark:command sent (${command.commandId}) to ${command.destinations.join(', ')}`
}, },
}) }),
]
} }
+3 -1
View File
@@ -17,6 +17,7 @@
"chess.js": "catalog:", "chess.js": "catalog:",
"reka-ui": "catalog:", "reka-ui": "catalog:",
"stockfish": "catalog:", "stockfish": "catalog:",
"valibot": "catalog:",
"vue": "catalog:" "vue": "catalog:"
}, },
"devDependencies": { "devDependencies": {
@@ -32,6 +33,7 @@
"@xsai/stream-text": "catalog:", "@xsai/stream-text": "catalog:",
"pinia": "catalog:", "pinia": "catalog:",
"unocss": "^66.6.7", "unocss": "^66.6.7",
"vieval": "catalog:" "vieval": "catalog:",
"xsschema": "catalog:"
} }
} }
+16 -10
View File
@@ -1011,7 +1011,7 @@ importers:
version: 1.2.45 version: 1.2.45
'@intlify/unplugin-vue-i18n': '@intlify/unplugin-vue-i18n':
specifier: ^11.0.7 specifier: ^11.0.7
version: 11.0.7(@vue/compiler-dom@3.5.32)(eslint@10.2.1(jiti@2.6.1))(rollup@2.80.0)(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@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))
'@proj-airi/cap-vite': '@proj-airi/cap-vite':
specifier: workspace:* specifier: workspace:*
version: link:../../packages/cap-vite version: link:../../packages/cap-vite
@@ -1077,16 +1077,16 @@ importers:
version: 4.6.4 version: 4.6.4
unplugin-info: unplugin-info:
specifier: ^1.3.2 specifier: ^1.3.2
version: 1.3.2(esbuild@0.27.2)(rollup@2.80.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: 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))
unplugin-yaml: unplugin-yaml:
specifier: ^4.1.0 specifier: ^4.1.0
version: 4.1.0(esbuild@0.27.2)(rolldown@1.0.0-rc.16)(rollup@2.80.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: 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))
vite: vite:
specifier: 'catalog:' 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@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-bundle-visualizer: vite-bundle-visualizer:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@2.80.0) version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1)
vite-plugin-mkcert: vite-plugin-mkcert:
specifier: 'catalog:' 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@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))
@@ -1101,7 +1101,7 @@ importers:
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(@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@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(@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: vue-macros:
specifier: ^3.1.2 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@2.80.0)(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@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))
vue-tsc: vue-tsc:
specifier: ^3.2.6 specifier: ^3.2.6
version: 3.2.6(typescript@5.9.3) version: 3.2.6(typescript@5.9.3)
@@ -1889,7 +1889,7 @@ importers:
version: 1.2.45 version: 1.2.45
'@intlify/unplugin-vue-i18n': '@intlify/unplugin-vue-i18n':
specifier: ^11.0.7 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@2.80.0)(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))
'@proj-airi/iconify-meteocons': '@proj-airi/iconify-meteocons':
specifier: 'catalog:' specifier: 'catalog:'
version: 0.1.5 version: 0.1.5
@@ -1952,16 +1952,16 @@ importers:
version: 4.6.4 version: 4.6.4
unplugin-info: unplugin-info:
specifier: ^1.3.2 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@2.80.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))
unplugin-yaml: unplugin-yaml:
specifier: ^4.1.0 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@2.80.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: vite:
specifier: 'catalog:' 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@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-bundle-visualizer: vite-bundle-visualizer:
specifier: ^1.2.1 specifier: ^1.2.1
version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@4.60.1) version: 1.2.1(rolldown@1.0.0-rc.16)(rollup@2.80.0)
vite-plugin-mkcert: vite-plugin-mkcert:
specifier: 'catalog:' 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@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))
@@ -1976,7 +1976,7 @@ importers:
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(@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@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(@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: vue-macros:
specifier: ^3.1.2 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@2.80.0)(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))
vue-tsc: vue-tsc:
specifier: ^3.2.6 specifier: ^3.2.6
version: 3.2.6(typescript@5.9.3) version: 3.2.6(typescript@5.9.3)
@@ -3861,6 +3861,9 @@ importers:
stockfish: stockfish:
specifier: 'catalog:' specifier: 'catalog:'
version: 18.0.7 version: 18.0.7
valibot:
specifier: 'catalog:'
version: 1.2.0(typescript@5.9.3)
vue: vue:
specifier: 'catalog:' specifier: 'catalog:'
version: 3.5.32(typescript@5.9.3) version: 3.5.32(typescript@5.9.3)
@@ -3904,6 +3907,9 @@ importers:
vieval: vieval:
specifier: 'catalog:' specifier: 'catalog:'
version: 0.0.1(@types/node@25.6.0)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3) version: 0.0.1(@types/node@25.6.0)(chokidar@5.0.0)(dotenv@17.4.2)(esbuild@0.27.2)(giget@2.0.0)(jiti@2.6.1)(less@4.6.4)(magicast@0.5.2)(terser@5.46.1)(tsx@4.21.0)(yaml@2.8.3)
xsschema:
specifier: 'catalog:'
version: 0.5.0-beta.2(@valibot/to-json-schema@1.0.0-rc.0(valibot@1.3.1(typescript@5.9.3)))(zod-to-json-schema@3.25.2(zod@4.3.6))(zod@4.3.6)
plugins/airi-plugin-homeassistant: plugins/airi-plugin-homeassistant:
dependencies: dependencies:
+1
View File
@@ -153,6 +153,7 @@ catalogs:
unspeech: ^0.1.13 unspeech: ^0.1.13
ignoredBuiltDependencies: ignoredBuiltDependencies:
- '@ax-llm/ax'
- '@prisma/client' - '@prisma/client'
- better-sqlite3 - better-sqlite3
- simple-git-hooks # [workaround] postinstall script bundled in simple-git-hooks fails to execute with `enableGlobalVirtualStore: true`. Using local postinstall script to run `npx simple-git-hooks` instead. - simple-git-hooks # [workaround] postinstall script bundled in simple-git-hooks fails to execute with `enableGlobalVirtualStore: true`. Using local postinstall script to run `npx simple-git-hooks` instead.