feat: add session updates and message dialogs
This commit is contained in:
@@ -18,6 +18,7 @@ export type VoicePreviewStatus = "idle" | "connecting" | "connected" | "failed";
|
||||
export type NetworkQuality = "unknown" | "good" | "fair" | "poor";
|
||||
|
||||
const NETWORK_SAMPLE_INTERVAL_MS = 2_000;
|
||||
const SESSION_UPDATE_TIMEOUT_MS = 10_000;
|
||||
|
||||
type NetworkStatsSample = {
|
||||
packetsSent: number;
|
||||
@@ -46,7 +47,7 @@ export type ChatMessage = {
|
||||
};
|
||||
|
||||
type AppMessage = Record<string, unknown> & { type?: string };
|
||||
type DynamicVariableValue = string | number | boolean;
|
||||
export type DynamicVariableValue = string | number | boolean;
|
||||
export type UserInputPart =
|
||||
| { type: "input_text"; text: string }
|
||||
| {
|
||||
@@ -57,6 +58,15 @@ export type UserInputResult = {
|
||||
inputId: string;
|
||||
status: "accepted";
|
||||
};
|
||||
export type SessionUpdateRequest = {
|
||||
dynamicVariables: Record<string, DynamicVariableValue>;
|
||||
};
|
||||
export type SessionUpdateResult = {
|
||||
updateId: string;
|
||||
status: "accepted";
|
||||
changed: string[];
|
||||
dynamicVariables: Record<string, DynamicVariableValue>;
|
||||
};
|
||||
export type ClientToolHandler = (
|
||||
argumentsValue: Record<string, unknown>,
|
||||
) => unknown | Promise<unknown>;
|
||||
@@ -67,6 +77,12 @@ type PendingUserInput = {
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
type PendingSessionUpdate = {
|
||||
resolve: (result: SessionUpdateResult) => void;
|
||||
reject: (error: Error) => void;
|
||||
timeout: number;
|
||||
};
|
||||
|
||||
function publicVariableSnapshot(
|
||||
value: unknown,
|
||||
): Record<string, DynamicVariableValue> {
|
||||
@@ -142,6 +158,13 @@ function newInputId(): string {
|
||||
return `input_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function newUpdateId(): string {
|
||||
if (typeof crypto !== "undefined" && "randomUUID" in crypto) {
|
||||
return `update_${crypto.randomUUID()}`;
|
||||
}
|
||||
return `update_${Date.now()}_${Math.random().toString(16).slice(2)}`;
|
||||
}
|
||||
|
||||
function readNetworkMetrics(report: RTCStatsReport): NetworkMetrics | null {
|
||||
const outbound: Partial<Record<"audio" | "video", number>> = {};
|
||||
const lost: Partial<Record<"audio" | "video", number>> = {};
|
||||
@@ -246,6 +269,9 @@ export function useVoicePreview(
|
||||
new Map<string, { timestamp: string }>(),
|
||||
);
|
||||
const pendingUserInputsRef = useRef(new Map<string, PendingUserInput>());
|
||||
const pendingSessionUpdatesRef = useRef(
|
||||
new Map<string, PendingSessionUpdate>(),
|
||||
);
|
||||
const clientToolHandlersRef = useRef(new Map<string, ClientToolHandler>());
|
||||
const endedByServerRef = useRef(false);
|
||||
const selectedDeviceIdRef = useRef("");
|
||||
@@ -306,6 +332,11 @@ export function useVoicePreview(
|
||||
reject(new Error("连接已关闭,用户输入未完成"));
|
||||
});
|
||||
pendingUserInputsRef.current.clear();
|
||||
pendingSessionUpdatesRef.current.forEach(({ reject, timeout }) => {
|
||||
window.clearTimeout(timeout);
|
||||
reject(new Error("连接已关闭,会话状态更新未完成"));
|
||||
});
|
||||
pendingSessionUpdatesRef.current.clear();
|
||||
networkStatsRef.current = null;
|
||||
onNodeActiveRef.current?.(null);
|
||||
}, []);
|
||||
@@ -405,6 +436,33 @@ export function useVoicePreview(
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
msg.type === "session-update-result" &&
|
||||
typeof msg.update_id === "string"
|
||||
) {
|
||||
const pending = pendingSessionUpdatesRef.current.get(msg.update_id);
|
||||
if (!pending) return;
|
||||
pendingSessionUpdatesRef.current.delete(msg.update_id);
|
||||
window.clearTimeout(pending.timeout);
|
||||
if (msg.status === "accepted") {
|
||||
const dynamicVariables = publicVariableSnapshot(msg.dynamic_variables);
|
||||
const changed = Array.isArray(msg.changed)
|
||||
? msg.changed.filter((name): name is string => typeof name === "string")
|
||||
: [];
|
||||
setSessionVariables(dynamicVariables);
|
||||
pending.resolve({
|
||||
updateId: msg.update_id,
|
||||
status: "accepted",
|
||||
changed,
|
||||
dynamicVariables,
|
||||
});
|
||||
} else {
|
||||
pending.reject(
|
||||
new Error(
|
||||
typeof msg.message === "string" ? msg.message : "会话状态更新失败",
|
||||
),
|
||||
);
|
||||
}
|
||||
} else if (
|
||||
msg.type === "assistant-text-start" &&
|
||||
typeof msg.turn_id === "string"
|
||||
@@ -754,6 +812,45 @@ export function useVoicePreview(
|
||||
[],
|
||||
);
|
||||
|
||||
const updateSession = useCallback(
|
||||
({
|
||||
dynamicVariables,
|
||||
}: SessionUpdateRequest): Promise<SessionUpdateResult> => {
|
||||
if (Object.keys(dynamicVariables).length === 0) {
|
||||
return Promise.reject(new Error("至少需要更新一个动态变量"));
|
||||
}
|
||||
const transport = transportRef.current;
|
||||
if (!transport || transport.state !== "connected") {
|
||||
return Promise.reject(new Error("当前未连接语音服务"));
|
||||
}
|
||||
const updateId = newUpdateId();
|
||||
return new Promise<SessionUpdateResult>((resolve, reject) => {
|
||||
const timeout = window.setTimeout(() => {
|
||||
pendingSessionUpdatesRef.current.delete(updateId);
|
||||
reject(new Error("等待会话状态更新结果超时"));
|
||||
}, SESSION_UPDATE_TIMEOUT_MS);
|
||||
pendingSessionUpdatesRef.current.set(updateId, {
|
||||
resolve,
|
||||
reject,
|
||||
timeout,
|
||||
});
|
||||
try {
|
||||
transport.sendAppMessage({
|
||||
type: "session-update",
|
||||
schema_version: 1,
|
||||
update_id: updateId,
|
||||
dynamic_variables: dynamicVariables,
|
||||
});
|
||||
} catch (sendError) {
|
||||
window.clearTimeout(timeout);
|
||||
pendingSessionUpdatesRef.current.delete(updateId);
|
||||
reject(new Error(errorMessage(sendError, "发送会话状态更新失败")));
|
||||
}
|
||||
});
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
const registerClientTool = useCallback(
|
||||
(functionName: string, handler: ClientToolHandler): (() => void) => {
|
||||
clientToolHandlersRef.current.set(functionName, handler);
|
||||
@@ -789,6 +886,7 @@ export function useVoicePreview(
|
||||
supportsOutputSelection,
|
||||
sendText,
|
||||
sendUserInput,
|
||||
updateSession,
|
||||
registerClientTool,
|
||||
connect,
|
||||
replaceVideoStream,
|
||||
|
||||
Reference in New Issue
Block a user