refactor(pipelines-audio): better token processing pipeline, ACT, DELAY, CALL, unified

This commit is contained in:
Neko Ayaka
2026-05-18 01:20:21 +08:00
parent c459e647d0
commit 4622b99e2a
35 changed files with 2454 additions and 59 deletions
@@ -110,6 +110,7 @@ const { context: iframeContext, iframeLoadError, onIframeError, onIframeLoad } =
const handled = await publishWidgetSparkNotifyReaction(event, {
dispatchSparkNotifyReaction: options => contextBridgeStore.dispatchSparkNotifyReaction(options),
dispatchSparkNotifyPerformance: options => contextBridgeStore.dispatchSparkNotifyPerformance(options),
emit: (eventDefinition, payload) => iframeContext.emit(eventDefinition, payload),
})
if (handled) {
@@ -129,4 +129,80 @@ describe('publishWidgetSparkNotifyReaction', () => {
headline: 'Quick move',
}))
})
/**
* @example
* await publishWidgetSparkNotifyReaction(eventWithCalls, options)
* expect(options.dispatchSparkNotifyPerformance).toHaveBeenCalledWith(expect.objectContaining({ timeoutMs: 15000 }))
*/
it('uses awaitable performance notify when the widget declares calls', async () => {
const dispatchSparkNotifyReaction = vi.fn(async () => 'unused')
const dispatchSparkNotifyPerformance = vi.fn(async () => ({
type: 'called' as const,
name: 'chess.play',
reaction: 'Played.',
}))
const emit = vi.fn()
const result = await publishWidgetSparkNotifyReaction({
route: {
namespace: 'airi.plugin.game.chess.commentary',
name: 'request',
},
payload: {
requestId: 'req-call',
fallbackResponseText: 'fallback',
calls: [
{
name: 'chess.play',
prompt: 'Play the prepared chess reply.',
examples: [
'<|CALL ["chess.play", {"move":"Nf3"}]|>',
],
},
],
timeoutMs: 15000,
sparkNotify: {
kind: 'ping',
urgency: 'immediate',
headline: 'A move is ready',
destinations: ['character'],
},
},
}, {
dispatchSparkNotifyReaction,
dispatchSparkNotifyPerformance,
emit,
})
expect(result).toBe(true)
expect(dispatchSparkNotifyReaction).not.toHaveBeenCalled()
expect(dispatchSparkNotifyPerformance).toHaveBeenCalledWith(expect.objectContaining({
headline: 'A move is ready',
fallbackResponseText: 'fallback',
timeoutMs: 15000,
calls: [
{
manifest: {
name: 'chess.play',
prompt: 'Play the prepared chess reply.',
examples: [
'<|CALL ["chess.play", {"move":"Nf3"}]|>',
],
},
handler: expect.any(Function),
},
],
}))
expect(emit).toHaveBeenCalledWith(widgetsIframeBroadcastEvent, expect.objectContaining({
payload: expect.objectContaining({
requestId: 'req-call',
text: 'Played.',
performance: {
type: 'called',
name: 'chess.play',
},
}),
}))
})
})
@@ -1,11 +1,12 @@
import type { SparkNotifyReactionOptions } from '@proj-airi/stage-ui/stores/mods/api/spark-notify-reaction'
import type { SparkNotifyPerformanceResult, SparkNotifyReactionOptions } from '@proj-airi/stage-ui/stores/mods/api/spark-notify-reaction'
import { widgetsIframeBroadcastEvent } from '@proj-airi/plugin-sdk-tamagotchi/widgets'
import { sparkNotifyReactionOptionsSchema } from '@proj-airi/stage-ui/stores/mods/api/spark-notify-reaction'
import { looseObject, nonEmpty, optional, pipe, record, safeParse, string, trim, unknown } from 'valibot'
import { array, finite, looseObject, nonEmpty, number, optional, pipe, record, safeParse, string, trim, unknown } from 'valibot'
interface PublishWidgetSparkNotifyReactionOptions {
dispatchSparkNotifyReaction: (options: SparkNotifyReactionOptions) => Promise<string>
dispatchSparkNotifyPerformance?: (options: SparkNotifyReactionOptions) => Promise<SparkNotifyPerformanceResult>
emit: (event: typeof widgetsIframeBroadcastEvent, payload: Record<string, unknown>) => void
}
@@ -29,6 +30,12 @@ const widgetSparkNotifyEventSchema = looseObject({
// because it owns the user-facing fallback for its current UI state.
fallbackResponseText: string(),
responseRoute: optional(record(string(), unknown())),
calls: optional(array(looseObject({
name: pipe(string(), trim(), nonEmpty()),
prompt: pipe(string(), trim(), nonEmpty()),
examples: optional(array(string())),
}))),
timeoutMs: optional(pipe(number(), finite())),
sparkNotify: looseObject({}),
}),
})
@@ -60,6 +67,8 @@ function createSparkNotifyReactionOptions(event: Record<string, unknown>) {
return {
requestId: payload.requestId,
responseRoute,
calls: payload.calls,
timeoutMs: payload.timeoutMs,
reactionOptions: reactionOptionsResult.output satisfies SparkNotifyReactionOptions,
}
}
@@ -89,13 +98,35 @@ export async function publishWidgetSparkNotifyReaction(
return false
}
const text = await options.dispatchSparkNotifyReaction(request.reactionOptions)
const widgetCallManifests = request.calls ?? []
const performance = widgetCallManifests.length > 0 && options.dispatchSparkNotifyPerformance
? await options.dispatchSparkNotifyPerformance({
...request.reactionOptions,
timeoutMs: request.timeoutMs,
calls: widgetCallManifests.map(manifest => ({
manifest,
handler: async () => undefined,
})),
})
: undefined
const text = performance
? performance.reaction
: await options.dispatchSparkNotifyReaction(request.reactionOptions)
options.emit(widgetsIframeBroadcastEvent, {
route: request.responseRoute,
payload: {
...(request.requestId ? { requestId: request.requestId } : {}),
text,
...(performance
? {
performance: {
type: performance.type,
name: performance.name,
},
}
: {}),
},
})
+5
View File
@@ -1,9 +1,14 @@
import { cwd } from 'node:process'
import vue from '@vitejs/plugin-vue'
import { loadEnv } from 'vite'
import { defineConfig } from 'vitest/config'
export default defineConfig({
plugins: [
vue(),
],
test: {
env: loadEnv('test', cwd(), ''),
include: ['src/**/*.test.ts', 'scripts/**/*.test.ts'],