@voicyclaw/voicyclaw 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/index.ts +28 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +76 -0
- package/src/channel.ts +125 -0
- package/src/config.ts +297 -0
- package/src/dispatch.ts +175 -0
- package/src/gateway.ts +218 -0
- package/src/protocol.ts +205 -0
- package/src/runtime.ts +152 -0
- package/src/socket-client.ts +267 -0
- package/tsconfig.json +14 -0
package/src/dispatch.ts
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
OpenClawConfig,
|
|
3
|
+
PluginRuntime,
|
|
4
|
+
ReplyPayload,
|
|
5
|
+
} from "openclaw/plugin-sdk";
|
|
6
|
+
|
|
7
|
+
import type { ResolvedVoicyClawAccount } from "./config.js";
|
|
8
|
+
import type { VoicyClawSttResultMessage } from "./protocol.js";
|
|
9
|
+
|
|
10
|
+
type Logger = {
|
|
11
|
+
info: (message: string) => void;
|
|
12
|
+
warn: (message: string) => void;
|
|
13
|
+
error: (message: string) => void;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
type VoicyClawReplyClient = {
|
|
17
|
+
sendPreview: (utteranceId: string, text: string, isFinal?: boolean) => void;
|
|
18
|
+
sendText: (utteranceId: string, text: string, isFinal?: boolean) => void;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
type VoicyClawChannelRuntime = Pick<
|
|
22
|
+
PluginRuntime["channel"],
|
|
23
|
+
"reply" | "routing" | "session"
|
|
24
|
+
>;
|
|
25
|
+
|
|
26
|
+
export async function dispatchVoicyClawTranscript(params: {
|
|
27
|
+
account: ResolvedVoicyClawAccount;
|
|
28
|
+
cfg: OpenClawConfig;
|
|
29
|
+
channelRuntime: VoicyClawChannelRuntime;
|
|
30
|
+
client: VoicyClawReplyClient;
|
|
31
|
+
message: VoicyClawSttResultMessage;
|
|
32
|
+
log: Logger;
|
|
33
|
+
onOutbound?: () => void;
|
|
34
|
+
}) {
|
|
35
|
+
const { account, cfg, channelRuntime, client, message, log, onOutbound } =
|
|
36
|
+
params;
|
|
37
|
+
const route = channelRuntime.routing.resolveAgentRoute({
|
|
38
|
+
cfg,
|
|
39
|
+
channel: "voicyclaw",
|
|
40
|
+
accountId: account.accountId,
|
|
41
|
+
peer: {
|
|
42
|
+
kind: "direct",
|
|
43
|
+
id: buildVoicyClawPeerId(account),
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
const storePath = channelRuntime.session.resolveStorePath(
|
|
47
|
+
cfg.session?.store,
|
|
48
|
+
{
|
|
49
|
+
agentId: route.agentId,
|
|
50
|
+
},
|
|
51
|
+
);
|
|
52
|
+
const previousTimestamp = channelRuntime.session.readSessionUpdatedAt({
|
|
53
|
+
storePath,
|
|
54
|
+
sessionKey: route.sessionKey,
|
|
55
|
+
});
|
|
56
|
+
const envelopeOptions =
|
|
57
|
+
channelRuntime.reply.resolveEnvelopeFormatOptions(cfg);
|
|
58
|
+
const receivedAt = Date.now();
|
|
59
|
+
const body = channelRuntime.reply.formatAgentEnvelope({
|
|
60
|
+
channel: "VoicyClaw",
|
|
61
|
+
from: buildConversationLabel(account),
|
|
62
|
+
timestamp: receivedAt,
|
|
63
|
+
previousTimestamp,
|
|
64
|
+
envelope: envelopeOptions,
|
|
65
|
+
body: message.text,
|
|
66
|
+
});
|
|
67
|
+
const ctxPayload = channelRuntime.reply.finalizeInboundContext({
|
|
68
|
+
Body: body,
|
|
69
|
+
BodyForAgent: message.text,
|
|
70
|
+
RawBody: message.text,
|
|
71
|
+
CommandBody: message.text,
|
|
72
|
+
From: buildVoicyClawTarget(account),
|
|
73
|
+
To: buildVoicyClawTarget(account),
|
|
74
|
+
SessionKey: route.sessionKey,
|
|
75
|
+
AccountId: route.accountId,
|
|
76
|
+
ChatType: "direct",
|
|
77
|
+
ConversationLabel: buildConversationLabel(account),
|
|
78
|
+
SenderName: "VoicyClaw User",
|
|
79
|
+
SenderId: buildVoicyClawPeerId(account),
|
|
80
|
+
Provider: "voicyclaw",
|
|
81
|
+
Surface: "voicyclaw",
|
|
82
|
+
MessageSid: message.utterance_id,
|
|
83
|
+
MessageSidFull: message.utterance_id,
|
|
84
|
+
Timestamp: receivedAt,
|
|
85
|
+
OriginatingChannel: "voicyclaw",
|
|
86
|
+
OriginatingTo: buildVoicyClawTarget(account),
|
|
87
|
+
CommandAuthorized: true,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await channelRuntime.session.recordInboundSession({
|
|
91
|
+
storePath,
|
|
92
|
+
sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
|
|
93
|
+
ctx: ctxPayload,
|
|
94
|
+
onRecordError: (error) => {
|
|
95
|
+
log.warn(
|
|
96
|
+
`[voicyclaw] failed recording inbound session for ${message.utterance_id}: ${String(
|
|
97
|
+
error,
|
|
98
|
+
)}`,
|
|
99
|
+
);
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
const result =
|
|
104
|
+
await channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher({
|
|
105
|
+
ctx: ctxPayload,
|
|
106
|
+
cfg,
|
|
107
|
+
dispatcherOptions: {
|
|
108
|
+
deliver: async (payload, info) => {
|
|
109
|
+
const text = replyPayloadToText(payload);
|
|
110
|
+
if (!text) {
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if (info.kind === "final") {
|
|
115
|
+
client.sendText(message.utterance_id, text, true);
|
|
116
|
+
} else {
|
|
117
|
+
client.sendPreview(message.utterance_id, text, false);
|
|
118
|
+
}
|
|
119
|
+
onOutbound?.();
|
|
120
|
+
},
|
|
121
|
+
onReplyStart: () => {
|
|
122
|
+
log.info(
|
|
123
|
+
`[voicyclaw] agent reply started for ${message.utterance_id} on ${route.sessionKey}`,
|
|
124
|
+
);
|
|
125
|
+
},
|
|
126
|
+
onError: (error, info) => {
|
|
127
|
+
log.warn(
|
|
128
|
+
`[voicyclaw] ${info.kind} reply failed for ${message.utterance_id}: ${String(
|
|
129
|
+
error,
|
|
130
|
+
)}`,
|
|
131
|
+
);
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
log.info(
|
|
137
|
+
`[voicyclaw] dispatched ${message.utterance_id} via ${route.agentId} (queuedFinal=${String(
|
|
138
|
+
result.queuedFinal,
|
|
139
|
+
)})`,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
route,
|
|
144
|
+
ctxPayload,
|
|
145
|
+
result,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function buildConversationLabel(account: ResolvedVoicyClawAccount) {
|
|
150
|
+
if (account.workspaceId) {
|
|
151
|
+
return `${account.workspaceId}/${account.channelId}`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return account.channelId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function buildVoicyClawPeerId(account: ResolvedVoicyClawAccount) {
|
|
158
|
+
return account.workspaceId
|
|
159
|
+
? `${account.workspaceId}:${account.channelId}`
|
|
160
|
+
: account.channelId;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function buildVoicyClawTarget(account: ResolvedVoicyClawAccount) {
|
|
164
|
+
return `voicyclaw:${buildVoicyClawPeerId(account)}`;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function replyPayloadToText(payload: ReplyPayload) {
|
|
168
|
+
const parts = [
|
|
169
|
+
payload.text?.trim(),
|
|
170
|
+
payload.mediaUrl?.trim(),
|
|
171
|
+
...(payload.mediaUrls ?? []).map((entry) => entry.trim()),
|
|
172
|
+
].filter(Boolean);
|
|
173
|
+
|
|
174
|
+
return parts.join("\n").trim();
|
|
175
|
+
}
|
package/src/gateway.ts
ADDED
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import type { ChannelGatewayAdapter, PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
|
+
import type { ResolvedVoicyClawAccount } from "./config.js";
|
|
3
|
+
import { buildVoicyClawSocketUrl } from "./config.js";
|
|
4
|
+
import { dispatchVoicyClawTranscript } from "./dispatch.js";
|
|
5
|
+
import type { VoicyClawRuntime } from "./runtime.js";
|
|
6
|
+
import { VoicyClawSocketClient } from "./socket-client.js";
|
|
7
|
+
|
|
8
|
+
export function createVoicyClawGatewayAdapter(
|
|
9
|
+
runtime: VoicyClawRuntime,
|
|
10
|
+
channelRuntime: PluginRuntime["channel"],
|
|
11
|
+
): ChannelGatewayAdapter<ResolvedVoicyClawAccount> {
|
|
12
|
+
return {
|
|
13
|
+
startAccount: async (ctx) => {
|
|
14
|
+
const account = ctx.account;
|
|
15
|
+
runtime.ensureAccount(account);
|
|
16
|
+
|
|
17
|
+
if (!account.enabled) {
|
|
18
|
+
runtime.markStopped(account);
|
|
19
|
+
ctx.setStatus({
|
|
20
|
+
accountId: account.accountId,
|
|
21
|
+
running: false,
|
|
22
|
+
connected: false,
|
|
23
|
+
lastStopAt: Date.now(),
|
|
24
|
+
});
|
|
25
|
+
await waitUntilAbortSignal(ctx.abortSignal);
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!account.configured) {
|
|
30
|
+
runtime.markStopped(account);
|
|
31
|
+
ctx.setStatus({
|
|
32
|
+
accountId: account.accountId,
|
|
33
|
+
running: false,
|
|
34
|
+
connected: false,
|
|
35
|
+
lastStopAt: Date.now(),
|
|
36
|
+
lastError: "missing VoicyClaw token",
|
|
37
|
+
});
|
|
38
|
+
await waitUntilAbortSignal(ctx.abortSignal);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
while (!ctx.abortSignal.aborted) {
|
|
43
|
+
const socketUrl = buildVoicyClawSocketUrl(account.url);
|
|
44
|
+
let client: VoicyClawSocketClient | null = null;
|
|
45
|
+
|
|
46
|
+
runtime.markStarting(account);
|
|
47
|
+
ctx.setStatus({
|
|
48
|
+
accountId: account.accountId,
|
|
49
|
+
running: true,
|
|
50
|
+
connected: false,
|
|
51
|
+
lastStartAt: Date.now(),
|
|
52
|
+
lastError: null,
|
|
53
|
+
baseUrl: account.url,
|
|
54
|
+
audience: account.channelId,
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
try {
|
|
58
|
+
client = new VoicyClawSocketClient({
|
|
59
|
+
account,
|
|
60
|
+
socketUrl,
|
|
61
|
+
logger: ctx.log ?? consoleLogger,
|
|
62
|
+
onTranscript: async (message) => {
|
|
63
|
+
runtime.markInbound(account.accountId);
|
|
64
|
+
ctx.setStatus({
|
|
65
|
+
accountId: account.accountId,
|
|
66
|
+
lastInboundAt: Date.now(),
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (!message.is_final) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (account.devEchoReplies) {
|
|
74
|
+
client?.sendText(
|
|
75
|
+
message.utterance_id,
|
|
76
|
+
`VoicyClaw plugin echo: ${message.text || "(empty transcript)"}`,
|
|
77
|
+
true,
|
|
78
|
+
);
|
|
79
|
+
runtime.markOutbound(account.accountId);
|
|
80
|
+
ctx.setStatus({
|
|
81
|
+
accountId: account.accountId,
|
|
82
|
+
lastOutboundAt: Date.now(),
|
|
83
|
+
});
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const activeClient = client;
|
|
88
|
+
if (!activeClient) {
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
await dispatchVoicyClawTranscript({
|
|
93
|
+
account,
|
|
94
|
+
cfg: ctx.cfg,
|
|
95
|
+
channelRuntime,
|
|
96
|
+
client: activeClient,
|
|
97
|
+
message,
|
|
98
|
+
log: ctx.log ?? consoleLogger,
|
|
99
|
+
onOutbound: () => {
|
|
100
|
+
runtime.markOutbound(account.accountId);
|
|
101
|
+
ctx.setStatus({
|
|
102
|
+
accountId: account.accountId,
|
|
103
|
+
lastOutboundAt: Date.now(),
|
|
104
|
+
});
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const welcome = await client.connect();
|
|
111
|
+
runtime.markConnected(account, welcome.session_id);
|
|
112
|
+
ctx.setStatus({
|
|
113
|
+
accountId: account.accountId,
|
|
114
|
+
running: true,
|
|
115
|
+
connected: true,
|
|
116
|
+
lastConnectedAt: Date.now(),
|
|
117
|
+
lastError: null,
|
|
118
|
+
lastDisconnect: null,
|
|
119
|
+
});
|
|
120
|
+
ctx.log?.info?.(
|
|
121
|
+
`[voicyclaw] connected ${account.accountId} to ${socketUrl}`,
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
await client.waitUntilClosed();
|
|
125
|
+
if (ctx.abortSignal.aborted) {
|
|
126
|
+
break;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
runtime.markDisconnected(account, "socket closed");
|
|
130
|
+
ctx.setStatus({
|
|
131
|
+
accountId: account.accountId,
|
|
132
|
+
running: true,
|
|
133
|
+
connected: false,
|
|
134
|
+
lastError: "socket closed",
|
|
135
|
+
lastDisconnect: {
|
|
136
|
+
at: Date.now(),
|
|
137
|
+
error: "socket closed",
|
|
138
|
+
},
|
|
139
|
+
});
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const message =
|
|
142
|
+
error instanceof Error ? error.message : String(error);
|
|
143
|
+
runtime.markDisconnected(account, message);
|
|
144
|
+
ctx.setStatus({
|
|
145
|
+
accountId: account.accountId,
|
|
146
|
+
running: true,
|
|
147
|
+
connected: false,
|
|
148
|
+
lastError: message,
|
|
149
|
+
lastDisconnect: {
|
|
150
|
+
at: Date.now(),
|
|
151
|
+
error: message,
|
|
152
|
+
},
|
|
153
|
+
});
|
|
154
|
+
ctx.log?.warn?.(
|
|
155
|
+
`[voicyclaw] connector ${account.accountId} disconnected: ${message}`,
|
|
156
|
+
);
|
|
157
|
+
} finally {
|
|
158
|
+
await client?.close().catch(() => undefined);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (ctx.abortSignal.aborted) {
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await sleep(account.reconnectBackoffMs, ctx.abortSignal);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
runtime.markStopped(account);
|
|
169
|
+
ctx.setStatus({
|
|
170
|
+
accountId: account.accountId,
|
|
171
|
+
running: false,
|
|
172
|
+
connected: false,
|
|
173
|
+
lastStopAt: Date.now(),
|
|
174
|
+
});
|
|
175
|
+
},
|
|
176
|
+
};
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const consoleLogger = {
|
|
180
|
+
info: (message: string) => console.log(message),
|
|
181
|
+
warn: (message: string) => console.warn(message),
|
|
182
|
+
error: (message: string) => console.error(message),
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
async function sleep(ms: number, signal: AbortSignal) {
|
|
186
|
+
if (signal.aborted) {
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await new Promise<void>((resolve) => {
|
|
191
|
+
const timeout = globalThis.setTimeout(() => {
|
|
192
|
+
cleanup();
|
|
193
|
+
resolve();
|
|
194
|
+
}, ms);
|
|
195
|
+
|
|
196
|
+
const cleanup = () => {
|
|
197
|
+
globalThis.clearTimeout(timeout);
|
|
198
|
+
signal.removeEventListener("abort", onAbort);
|
|
199
|
+
};
|
|
200
|
+
|
|
201
|
+
const onAbort = () => {
|
|
202
|
+
cleanup();
|
|
203
|
+
resolve();
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
async function waitUntilAbortSignal(signal: AbortSignal) {
|
|
211
|
+
if (signal.aborted) {
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await new Promise<void>((resolve) => {
|
|
216
|
+
signal.addEventListener("abort", () => resolve(), { once: true });
|
|
217
|
+
});
|
|
218
|
+
}
|
package/src/protocol.ts
ADDED
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
export const VOICYCLAW_PROTOCOL_VERSION = "0.1" as const;
|
|
2
|
+
|
|
3
|
+
export type VoicyClawProtocolVersion = typeof VOICYCLAW_PROTOCOL_VERSION;
|
|
4
|
+
|
|
5
|
+
export type VoicyClawHelloMessage = {
|
|
6
|
+
type: "HELLO";
|
|
7
|
+
api_key: string;
|
|
8
|
+
bot_id: string;
|
|
9
|
+
channel_id: string;
|
|
10
|
+
protocol_version: VoicyClawProtocolVersion;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
export type VoicyClawWelcomeMessage = {
|
|
14
|
+
type: "WELCOME";
|
|
15
|
+
session_id: string;
|
|
16
|
+
channel_id: string;
|
|
17
|
+
bot_id: string;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
export type VoicyClawErrorMessage = {
|
|
21
|
+
type: "ERROR";
|
|
22
|
+
code:
|
|
23
|
+
| "AUTH_FAILED"
|
|
24
|
+
| "CHANNEL_NOT_FOUND"
|
|
25
|
+
| "BOT_ALREADY_CONNECTED"
|
|
26
|
+
| "PROTOCOL_VERSION_UNSUPPORTED";
|
|
27
|
+
message: string;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
export type VoicyClawDisconnectMessage = {
|
|
31
|
+
type: "DISCONNECT";
|
|
32
|
+
session_id: string;
|
|
33
|
+
reason: string;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
export type VoicyClawAudioStartMessage = {
|
|
37
|
+
type: "AUDIO_START";
|
|
38
|
+
session_id: string;
|
|
39
|
+
utterance_id: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
export type VoicyClawAudioEndMessage = {
|
|
43
|
+
type: "AUDIO_END";
|
|
44
|
+
session_id: string;
|
|
45
|
+
utterance_id: string;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export type VoicyClawSttResultMessage = {
|
|
49
|
+
type: "STT_RESULT";
|
|
50
|
+
session_id: string;
|
|
51
|
+
utterance_id: string;
|
|
52
|
+
text: string;
|
|
53
|
+
is_final: boolean;
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
export type VoicyClawTtsTextMessage = {
|
|
57
|
+
type: "TTS_TEXT";
|
|
58
|
+
session_id: string;
|
|
59
|
+
utterance_id: string;
|
|
60
|
+
text: string;
|
|
61
|
+
is_final: boolean;
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
export type VoicyClawPreviewTextMessage = {
|
|
65
|
+
type: "BOT_PREVIEW";
|
|
66
|
+
session_id: string;
|
|
67
|
+
utterance_id: string;
|
|
68
|
+
text: string;
|
|
69
|
+
is_final: boolean;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
export type VoicyClawServerMessage =
|
|
73
|
+
| VoicyClawWelcomeMessage
|
|
74
|
+
| VoicyClawErrorMessage
|
|
75
|
+
| VoicyClawDisconnectMessage
|
|
76
|
+
| VoicyClawAudioStartMessage
|
|
77
|
+
| VoicyClawAudioEndMessage
|
|
78
|
+
| VoicyClawSttResultMessage;
|
|
79
|
+
|
|
80
|
+
export function createHelloMessage(params: {
|
|
81
|
+
token: string;
|
|
82
|
+
botId: string;
|
|
83
|
+
channelId: string;
|
|
84
|
+
}): VoicyClawHelloMessage {
|
|
85
|
+
return {
|
|
86
|
+
type: "HELLO",
|
|
87
|
+
api_key: params.token,
|
|
88
|
+
bot_id: params.botId,
|
|
89
|
+
channel_id: params.channelId,
|
|
90
|
+
protocol_version: VOICYCLAW_PROTOCOL_VERSION,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export function createPreviewTextMessage(params: {
|
|
95
|
+
sessionId: string;
|
|
96
|
+
utteranceId: string;
|
|
97
|
+
text: string;
|
|
98
|
+
isFinal?: boolean;
|
|
99
|
+
}): VoicyClawPreviewTextMessage {
|
|
100
|
+
return {
|
|
101
|
+
type: "BOT_PREVIEW",
|
|
102
|
+
session_id: params.sessionId,
|
|
103
|
+
utterance_id: params.utteranceId,
|
|
104
|
+
text: params.text,
|
|
105
|
+
is_final: params.isFinal ?? false,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function createTtsTextMessage(params: {
|
|
110
|
+
sessionId: string;
|
|
111
|
+
utteranceId: string;
|
|
112
|
+
text: string;
|
|
113
|
+
isFinal?: boolean;
|
|
114
|
+
}): VoicyClawTtsTextMessage {
|
|
115
|
+
return {
|
|
116
|
+
type: "TTS_TEXT",
|
|
117
|
+
session_id: params.sessionId,
|
|
118
|
+
utterance_id: params.utteranceId,
|
|
119
|
+
text: params.text,
|
|
120
|
+
is_final: params.isFinal ?? true,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function parseVoicyClawServerMessage(
|
|
125
|
+
value: unknown,
|
|
126
|
+
): VoicyClawServerMessage | null {
|
|
127
|
+
if (!isRecord(value) || typeof value.type !== "string") {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
switch (value.type) {
|
|
132
|
+
case "WELCOME":
|
|
133
|
+
return isString(value.session_id) &&
|
|
134
|
+
isString(value.channel_id) &&
|
|
135
|
+
isString(value.bot_id)
|
|
136
|
+
? {
|
|
137
|
+
type: "WELCOME",
|
|
138
|
+
session_id: value.session_id,
|
|
139
|
+
channel_id: value.channel_id,
|
|
140
|
+
bot_id: value.bot_id,
|
|
141
|
+
}
|
|
142
|
+
: null;
|
|
143
|
+
|
|
144
|
+
case "ERROR":
|
|
145
|
+
return isString(value.code) && isString(value.message)
|
|
146
|
+
? {
|
|
147
|
+
type: "ERROR",
|
|
148
|
+
code: value.code as VoicyClawErrorMessage["code"],
|
|
149
|
+
message: value.message,
|
|
150
|
+
}
|
|
151
|
+
: null;
|
|
152
|
+
|
|
153
|
+
case "DISCONNECT":
|
|
154
|
+
return isString(value.session_id) && isString(value.reason)
|
|
155
|
+
? {
|
|
156
|
+
type: "DISCONNECT",
|
|
157
|
+
session_id: value.session_id,
|
|
158
|
+
reason: value.reason,
|
|
159
|
+
}
|
|
160
|
+
: null;
|
|
161
|
+
|
|
162
|
+
case "AUDIO_START":
|
|
163
|
+
return isString(value.session_id) && isString(value.utterance_id)
|
|
164
|
+
? {
|
|
165
|
+
type: "AUDIO_START",
|
|
166
|
+
session_id: value.session_id,
|
|
167
|
+
utterance_id: value.utterance_id,
|
|
168
|
+
}
|
|
169
|
+
: null;
|
|
170
|
+
|
|
171
|
+
case "AUDIO_END":
|
|
172
|
+
return isString(value.session_id) && isString(value.utterance_id)
|
|
173
|
+
? {
|
|
174
|
+
type: "AUDIO_END",
|
|
175
|
+
session_id: value.session_id,
|
|
176
|
+
utterance_id: value.utterance_id,
|
|
177
|
+
}
|
|
178
|
+
: null;
|
|
179
|
+
|
|
180
|
+
case "STT_RESULT":
|
|
181
|
+
return isString(value.session_id) &&
|
|
182
|
+
isString(value.utterance_id) &&
|
|
183
|
+
isString(value.text) &&
|
|
184
|
+
typeof value.is_final === "boolean"
|
|
185
|
+
? {
|
|
186
|
+
type: "STT_RESULT",
|
|
187
|
+
session_id: value.session_id,
|
|
188
|
+
utterance_id: value.utterance_id,
|
|
189
|
+
text: value.text,
|
|
190
|
+
is_final: value.is_final,
|
|
191
|
+
}
|
|
192
|
+
: null;
|
|
193
|
+
|
|
194
|
+
default:
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
200
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function isString(value: unknown): value is string {
|
|
204
|
+
return typeof value === "string" && value.trim().length > 0;
|
|
205
|
+
}
|