refactor(stage-ui,stage-tamagotchi): have plugin-tools, mcp-tools to register directly to llm.ts
This commit is contained in:
@@ -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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -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])
|
||||||
|
})
|
||||||
|
})
|
||||||
@@ -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,
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -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')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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(', ')}`
|
||||||
},
|
},
|
||||||
})
|
}),
|
||||||
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+16
-10
@@ -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:
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
Reference in New Issue
Block a user