@openclaw/voice-call 2026.3.13 → 2026.5.2-beta.1
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/README.md +27 -5
- package/api.ts +16 -0
- package/cli-metadata.ts +10 -0
- package/config-api.ts +12 -0
- package/index.test.ts +943 -0
- package/index.ts +379 -149
- package/openclaw.plugin.json +384 -157
- package/package.json +35 -5
- package/runtime-api.ts +20 -0
- package/runtime-entry.ts +1 -0
- package/setup-api.ts +47 -0
- package/src/allowlist.test.ts +18 -0
- package/src/cli.ts +533 -68
- package/src/config-compat.test.ts +120 -0
- package/src/config-compat.ts +227 -0
- package/src/config.test.ts +273 -12
- package/src/config.ts +355 -72
- package/src/core-bridge.ts +2 -147
- package/src/deep-merge.test.ts +40 -0
- package/src/gateway-continue-operation.ts +200 -0
- package/src/http-headers.ts +6 -3
- package/src/manager/context.ts +6 -5
- package/src/manager/events.test.ts +243 -19
- package/src/manager/events.ts +61 -31
- package/src/manager/lifecycle.ts +53 -0
- package/src/manager/lookup.test.ts +52 -0
- package/src/manager/outbound.test.ts +528 -0
- package/src/manager/outbound.ts +163 -57
- package/src/manager/store.ts +18 -6
- package/src/manager/timers.test.ts +129 -0
- package/src/manager/timers.ts +4 -3
- package/src/manager/twiml.test.ts +13 -0
- package/src/manager/twiml.ts +8 -0
- package/src/manager.closed-loop.test.ts +30 -12
- package/src/manager.inbound-allowlist.test.ts +77 -10
- package/src/manager.notify.test.ts +344 -20
- package/src/manager.restore.test.ts +95 -8
- package/src/manager.test-harness.ts +8 -6
- package/src/manager.ts +79 -5
- package/src/media-stream.test.ts +578 -81
- package/src/media-stream.ts +235 -54
- package/src/providers/base.ts +19 -0
- package/src/providers/mock.ts +7 -1
- package/src/providers/plivo.test.ts +50 -6
- package/src/providers/plivo.ts +14 -6
- package/src/providers/shared/call-status.ts +2 -1
- package/src/providers/shared/guarded-json-api.test.ts +106 -0
- package/src/providers/shared/guarded-json-api.ts +1 -1
- package/src/providers/telnyx.test.ts +178 -6
- package/src/providers/telnyx.ts +40 -3
- package/src/providers/twilio/api.test.ts +145 -0
- package/src/providers/twilio/api.ts +67 -16
- package/src/providers/twilio/twiml-policy.ts +6 -10
- package/src/providers/twilio/webhook.ts +1 -1
- package/src/providers/twilio.test.ts +425 -25
- package/src/providers/twilio.ts +230 -77
- package/src/providers/twilio.types.ts +17 -0
- package/src/realtime-defaults.ts +3 -0
- package/src/realtime-fast-context.test.ts +88 -0
- package/src/realtime-fast-context.ts +165 -0
- package/src/realtime-transcription.runtime.ts +4 -0
- package/src/realtime-voice.runtime.ts +5 -0
- package/src/response-generator.test.ts +321 -0
- package/src/response-generator.ts +213 -53
- package/src/response-model.test.ts +71 -0
- package/src/response-model.ts +23 -0
- package/src/runtime.test.ts +429 -0
- package/src/runtime.ts +270 -24
- package/src/telephony-audio.test.ts +61 -0
- package/src/telephony-audio.ts +1 -79
- package/src/telephony-tts.test.ts +133 -12
- package/src/telephony-tts.ts +155 -2
- package/src/test-fixtures.ts +28 -7
- package/src/tts-provider-voice.test.ts +34 -0
- package/src/tts-provider-voice.ts +21 -0
- package/src/tunnel.test.ts +166 -0
- package/src/tunnel.ts +1 -1
- package/src/types.ts +24 -37
- package/src/utils.test.ts +17 -0
- package/src/voice-mapping.test.ts +34 -0
- package/src/voice-mapping.ts +3 -2
- package/src/webhook/realtime-handler.test.ts +598 -0
- package/src/webhook/realtime-handler.ts +485 -0
- package/src/webhook/stale-call-reaper.test.ts +88 -0
- package/src/webhook/stale-call-reaper.ts +5 -0
- package/src/webhook/tailscale.test.ts +214 -0
- package/src/webhook/tailscale.ts +19 -5
- package/src/webhook-exposure.test.ts +33 -0
- package/src/webhook-exposure.ts +84 -0
- package/src/webhook-security.test.ts +172 -21
- package/src/webhook-security.ts +43 -29
- package/src/webhook.hangup-once.lifecycle.test.ts +135 -0
- package/src/webhook.test.ts +1145 -27
- package/src/webhook.ts +523 -102
- package/src/webhook.types.ts +5 -0
- package/src/websocket-test-support.ts +72 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -121
- package/src/providers/index.ts +0 -10
- package/src/providers/stt-openai-realtime.test.ts +0 -42
- package/src/providers/stt-openai-realtime.ts +0 -311
- package/src/providers/tts-openai.test.ts +0 -43
- package/src/providers/tts-openai.ts +0 -221
package/src/core-bridge.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
|
-
import
|
|
2
|
-
import path from "node:path";
|
|
3
|
-
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
1
|
+
import type { OpenClawPluginApi } from "../api.js";
|
|
4
2
|
import type { VoiceCallTtsConfig } from "./config.js";
|
|
5
3
|
|
|
6
4
|
export type CoreConfig = {
|
|
@@ -13,147 +11,4 @@ export type CoreConfig = {
|
|
|
13
11
|
[key: string]: unknown;
|
|
14
12
|
};
|
|
15
13
|
|
|
16
|
-
type CoreAgentDeps =
|
|
17
|
-
resolveAgentDir: (cfg: CoreConfig, agentId: string) => string;
|
|
18
|
-
resolveAgentWorkspaceDir: (cfg: CoreConfig, agentId: string) => string;
|
|
19
|
-
resolveAgentIdentity: (
|
|
20
|
-
cfg: CoreConfig,
|
|
21
|
-
agentId: string,
|
|
22
|
-
) => { name?: string | null } | null | undefined;
|
|
23
|
-
resolveThinkingDefault: (params: {
|
|
24
|
-
cfg: CoreConfig;
|
|
25
|
-
provider?: string;
|
|
26
|
-
model?: string;
|
|
27
|
-
}) => string;
|
|
28
|
-
runEmbeddedPiAgent: (params: {
|
|
29
|
-
sessionId: string;
|
|
30
|
-
sessionKey?: string;
|
|
31
|
-
messageProvider?: string;
|
|
32
|
-
sessionFile: string;
|
|
33
|
-
workspaceDir: string;
|
|
34
|
-
config?: CoreConfig;
|
|
35
|
-
prompt: string;
|
|
36
|
-
provider?: string;
|
|
37
|
-
model?: string;
|
|
38
|
-
thinkLevel?: string;
|
|
39
|
-
verboseLevel?: string;
|
|
40
|
-
timeoutMs: number;
|
|
41
|
-
runId: string;
|
|
42
|
-
lane?: string;
|
|
43
|
-
extraSystemPrompt?: string;
|
|
44
|
-
agentDir?: string;
|
|
45
|
-
}) => Promise<{
|
|
46
|
-
payloads?: Array<{ text?: string; isError?: boolean }>;
|
|
47
|
-
meta?: { aborted?: boolean };
|
|
48
|
-
}>;
|
|
49
|
-
resolveAgentTimeoutMs: (opts: { cfg: CoreConfig }) => number;
|
|
50
|
-
ensureAgentWorkspace: (params?: { dir: string }) => Promise<void>;
|
|
51
|
-
resolveStorePath: (store?: string, opts?: { agentId?: string }) => string;
|
|
52
|
-
loadSessionStore: (storePath: string) => Record<string, unknown>;
|
|
53
|
-
saveSessionStore: (storePath: string, store: Record<string, unknown>) => Promise<void>;
|
|
54
|
-
resolveSessionFilePath: (
|
|
55
|
-
sessionId: string,
|
|
56
|
-
entry: unknown,
|
|
57
|
-
opts?: { agentId?: string },
|
|
58
|
-
) => string;
|
|
59
|
-
DEFAULT_MODEL: string;
|
|
60
|
-
DEFAULT_PROVIDER: string;
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
let coreRootCache: string | null = null;
|
|
64
|
-
let coreDepsPromise: Promise<CoreAgentDeps> | null = null;
|
|
65
|
-
|
|
66
|
-
function findPackageRoot(startDir: string, name: string): string | null {
|
|
67
|
-
let dir = startDir;
|
|
68
|
-
for (;;) {
|
|
69
|
-
const pkgPath = path.join(dir, "package.json");
|
|
70
|
-
try {
|
|
71
|
-
if (fs.existsSync(pkgPath)) {
|
|
72
|
-
const raw = fs.readFileSync(pkgPath, "utf8");
|
|
73
|
-
const pkg = JSON.parse(raw) as { name?: string };
|
|
74
|
-
if (pkg.name === name) {
|
|
75
|
-
return dir;
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
} catch {
|
|
79
|
-
// ignore parse errors and keep walking
|
|
80
|
-
}
|
|
81
|
-
const parent = path.dirname(dir);
|
|
82
|
-
if (parent === dir) {
|
|
83
|
-
return null;
|
|
84
|
-
}
|
|
85
|
-
dir = parent;
|
|
86
|
-
}
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function resolveOpenClawRoot(): string {
|
|
90
|
-
if (coreRootCache) {
|
|
91
|
-
return coreRootCache;
|
|
92
|
-
}
|
|
93
|
-
const override = process.env.OPENCLAW_ROOT?.trim();
|
|
94
|
-
if (override) {
|
|
95
|
-
coreRootCache = override;
|
|
96
|
-
return override;
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
const candidates = new Set<string>();
|
|
100
|
-
if (process.argv[1]) {
|
|
101
|
-
candidates.add(path.dirname(process.argv[1]));
|
|
102
|
-
}
|
|
103
|
-
candidates.add(process.cwd());
|
|
104
|
-
try {
|
|
105
|
-
const urlPath = fileURLToPath(import.meta.url);
|
|
106
|
-
candidates.add(path.dirname(urlPath));
|
|
107
|
-
} catch {
|
|
108
|
-
// ignore
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
for (const start of candidates) {
|
|
112
|
-
for (const name of ["openclaw"]) {
|
|
113
|
-
const found = findPackageRoot(start, name);
|
|
114
|
-
if (found) {
|
|
115
|
-
coreRootCache = found;
|
|
116
|
-
return found;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
throw new Error("Unable to resolve core root. Set OPENCLAW_ROOT to the package root.");
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
async function importCoreExtensionAPI(): Promise<{
|
|
125
|
-
resolveAgentDir: CoreAgentDeps["resolveAgentDir"];
|
|
126
|
-
resolveAgentWorkspaceDir: CoreAgentDeps["resolveAgentWorkspaceDir"];
|
|
127
|
-
DEFAULT_MODEL: string;
|
|
128
|
-
DEFAULT_PROVIDER: string;
|
|
129
|
-
resolveAgentIdentity: CoreAgentDeps["resolveAgentIdentity"];
|
|
130
|
-
resolveThinkingDefault: CoreAgentDeps["resolveThinkingDefault"];
|
|
131
|
-
runEmbeddedPiAgent: CoreAgentDeps["runEmbeddedPiAgent"];
|
|
132
|
-
resolveAgentTimeoutMs: CoreAgentDeps["resolveAgentTimeoutMs"];
|
|
133
|
-
ensureAgentWorkspace: CoreAgentDeps["ensureAgentWorkspace"];
|
|
134
|
-
resolveStorePath: CoreAgentDeps["resolveStorePath"];
|
|
135
|
-
loadSessionStore: CoreAgentDeps["loadSessionStore"];
|
|
136
|
-
saveSessionStore: CoreAgentDeps["saveSessionStore"];
|
|
137
|
-
resolveSessionFilePath: CoreAgentDeps["resolveSessionFilePath"];
|
|
138
|
-
}> {
|
|
139
|
-
// Do not import any other module. You can't touch this or you will be fired.
|
|
140
|
-
const distPath = path.join(resolveOpenClawRoot(), "dist", "extensionAPI.js");
|
|
141
|
-
if (!fs.existsSync(distPath)) {
|
|
142
|
-
throw new Error(
|
|
143
|
-
`Missing core module at ${distPath}. Run \`pnpm build\` or install the official package.`,
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
return await import(pathToFileURL(distPath).href);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
export async function loadCoreAgentDeps(): Promise<CoreAgentDeps> {
|
|
150
|
-
if (coreDepsPromise) {
|
|
151
|
-
return coreDepsPromise;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
coreDepsPromise = (async () => {
|
|
155
|
-
return await importCoreExtensionAPI();
|
|
156
|
-
})();
|
|
157
|
-
|
|
158
|
-
return coreDepsPromise;
|
|
159
|
-
}
|
|
14
|
+
export type CoreAgentDeps = OpenClawPluginApi["runtime"]["agent"];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { deepMergeDefined } from "./deep-merge.js";
|
|
3
|
+
|
|
4
|
+
describe("deepMergeDefined", () => {
|
|
5
|
+
it("deep merges nested plain objects and preserves base values for undefined overrides", () => {
|
|
6
|
+
expect(
|
|
7
|
+
deepMergeDefined(
|
|
8
|
+
{
|
|
9
|
+
provider: { voice: "alloy", language: "en" },
|
|
10
|
+
enabled: true,
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
provider: { voice: "echo", language: undefined },
|
|
14
|
+
enabled: undefined,
|
|
15
|
+
},
|
|
16
|
+
),
|
|
17
|
+
).toEqual({
|
|
18
|
+
provider: { voice: "echo", language: "en" },
|
|
19
|
+
enabled: true,
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
it("replaces non-objects directly and blocks dangerous prototype keys", () => {
|
|
24
|
+
expect(deepMergeDefined(["a"], ["b"])).toEqual(["b"]);
|
|
25
|
+
expect(deepMergeDefined("base", undefined)).toBe("base");
|
|
26
|
+
expect(
|
|
27
|
+
deepMergeDefined(
|
|
28
|
+
{ safe: { keep: true } },
|
|
29
|
+
{
|
|
30
|
+
safe: { next: true },
|
|
31
|
+
__proto__: { polluted: true },
|
|
32
|
+
constructor: { polluted: true },
|
|
33
|
+
prototype: { polluted: true },
|
|
34
|
+
},
|
|
35
|
+
),
|
|
36
|
+
).toEqual({
|
|
37
|
+
safe: { keep: true, next: true },
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
});
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
|
3
|
+
import type { VoiceCallConfig } from "./config.js";
|
|
4
|
+
import type { CoreConfig } from "./core-bridge.js";
|
|
5
|
+
import type { VoiceCallRuntime } from "./runtime.js";
|
|
6
|
+
import { TELEPHONY_DEFAULT_TTS_TIMEOUT_MS } from "./telephony-tts.js";
|
|
7
|
+
|
|
8
|
+
const VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS = 30000;
|
|
9
|
+
const VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS = 5 * 60 * 1000;
|
|
10
|
+
|
|
11
|
+
type VoiceCallContinueOperation =
|
|
12
|
+
| {
|
|
13
|
+
operationId: string;
|
|
14
|
+
status: "pending";
|
|
15
|
+
callId: string;
|
|
16
|
+
startedAtMs: number;
|
|
17
|
+
pollTimeoutMs: number;
|
|
18
|
+
}
|
|
19
|
+
| {
|
|
20
|
+
operationId: string;
|
|
21
|
+
status: "completed";
|
|
22
|
+
callId: string;
|
|
23
|
+
startedAtMs: number;
|
|
24
|
+
completedAtMs: number;
|
|
25
|
+
pollTimeoutMs: number;
|
|
26
|
+
result: { success: true; transcript?: string };
|
|
27
|
+
}
|
|
28
|
+
| {
|
|
29
|
+
operationId: string;
|
|
30
|
+
status: "failed";
|
|
31
|
+
callId: string;
|
|
32
|
+
startedAtMs: number;
|
|
33
|
+
completedAtMs: number;
|
|
34
|
+
pollTimeoutMs: number;
|
|
35
|
+
error: string;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
type VoiceCallContinueOperationStartPayload = {
|
|
39
|
+
operationId: string;
|
|
40
|
+
status: "pending";
|
|
41
|
+
pollTimeoutMs: number;
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
type VoiceCallContinueOperationResultPayload =
|
|
45
|
+
| {
|
|
46
|
+
operationId: string;
|
|
47
|
+
status: "pending";
|
|
48
|
+
pollTimeoutMs: number;
|
|
49
|
+
}
|
|
50
|
+
| {
|
|
51
|
+
operationId: string;
|
|
52
|
+
status: "completed";
|
|
53
|
+
result: { success: true; transcript?: string };
|
|
54
|
+
}
|
|
55
|
+
| {
|
|
56
|
+
operationId: string;
|
|
57
|
+
status: "failed";
|
|
58
|
+
error: string;
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
type VoiceCallContinueOperationRequest = {
|
|
62
|
+
rt: VoiceCallRuntime;
|
|
63
|
+
callId: string;
|
|
64
|
+
message: string;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export function createVoiceCallContinueOperationStore(params: {
|
|
68
|
+
config: VoiceCallConfig;
|
|
69
|
+
coreConfig: CoreConfig;
|
|
70
|
+
}) {
|
|
71
|
+
const operations = new Map<string, VoiceCallContinueOperation>();
|
|
72
|
+
|
|
73
|
+
const resolvePollTimeoutMs = (rt: VoiceCallRuntime): number => {
|
|
74
|
+
const ttsTimeoutMs =
|
|
75
|
+
rt.config.tts?.timeoutMs ??
|
|
76
|
+
params.config.tts?.timeoutMs ??
|
|
77
|
+
params.coreConfig.messages?.tts?.timeoutMs ??
|
|
78
|
+
TELEPHONY_DEFAULT_TTS_TIMEOUT_MS;
|
|
79
|
+
return (
|
|
80
|
+
(rt.config.transcriptTimeoutMs ?? params.config.transcriptTimeoutMs) +
|
|
81
|
+
ttsTimeoutMs +
|
|
82
|
+
VOICE_CALL_CONTINUE_OPERATION_BUFFER_MS
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const scheduleCleanup = (operationId: string) => {
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
operations.delete(operationId);
|
|
89
|
+
}, VOICE_CALL_CONTINUE_OPERATION_CLEANUP_MS);
|
|
90
|
+
timer.unref?.();
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
const start = (
|
|
94
|
+
request: VoiceCallContinueOperationRequest,
|
|
95
|
+
): VoiceCallContinueOperationStartPayload => {
|
|
96
|
+
const operationId = randomUUID();
|
|
97
|
+
const startedAtMs = Date.now();
|
|
98
|
+
const pollTimeoutMs = resolvePollTimeoutMs(request.rt);
|
|
99
|
+
operations.set(operationId, {
|
|
100
|
+
operationId,
|
|
101
|
+
status: "pending",
|
|
102
|
+
callId: request.callId,
|
|
103
|
+
startedAtMs,
|
|
104
|
+
pollTimeoutMs,
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
void request.rt.manager
|
|
108
|
+
.continueCall(request.callId, request.message)
|
|
109
|
+
.then((result) => {
|
|
110
|
+
const current = operations.get(operationId);
|
|
111
|
+
if (!current || current.status !== "pending") {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
if (!result.success) {
|
|
115
|
+
operations.set(operationId, {
|
|
116
|
+
operationId,
|
|
117
|
+
status: "failed",
|
|
118
|
+
callId: request.callId,
|
|
119
|
+
startedAtMs,
|
|
120
|
+
completedAtMs: Date.now(),
|
|
121
|
+
pollTimeoutMs,
|
|
122
|
+
error: result.error || "continue failed",
|
|
123
|
+
});
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
operations.set(operationId, {
|
|
127
|
+
operationId,
|
|
128
|
+
status: "completed",
|
|
129
|
+
callId: request.callId,
|
|
130
|
+
startedAtMs,
|
|
131
|
+
completedAtMs: Date.now(),
|
|
132
|
+
pollTimeoutMs,
|
|
133
|
+
result: { success: true, transcript: result.transcript },
|
|
134
|
+
});
|
|
135
|
+
})
|
|
136
|
+
.catch((err) => {
|
|
137
|
+
const current = operations.get(operationId);
|
|
138
|
+
if (!current || current.status !== "pending") {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
operations.set(operationId, {
|
|
142
|
+
operationId,
|
|
143
|
+
status: "failed",
|
|
144
|
+
callId: request.callId,
|
|
145
|
+
startedAtMs,
|
|
146
|
+
completedAtMs: Date.now(),
|
|
147
|
+
pollTimeoutMs,
|
|
148
|
+
error: formatErrorMessage(err),
|
|
149
|
+
});
|
|
150
|
+
})
|
|
151
|
+
.finally(() => {
|
|
152
|
+
scheduleCleanup(operationId);
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return { operationId, status: "pending", pollTimeoutMs };
|
|
156
|
+
};
|
|
157
|
+
|
|
158
|
+
const read = (
|
|
159
|
+
operationId: string,
|
|
160
|
+
):
|
|
161
|
+
| { ok: true; payload: VoiceCallContinueOperationResultPayload }
|
|
162
|
+
| { ok: false; error: string } => {
|
|
163
|
+
const operation = operations.get(operationId);
|
|
164
|
+
if (!operation) {
|
|
165
|
+
return { ok: false, error: "operation not found" };
|
|
166
|
+
}
|
|
167
|
+
if (operation.status === "pending") {
|
|
168
|
+
return {
|
|
169
|
+
ok: true,
|
|
170
|
+
payload: {
|
|
171
|
+
operationId,
|
|
172
|
+
status: "pending",
|
|
173
|
+
pollTimeoutMs: operation.pollTimeoutMs,
|
|
174
|
+
},
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
if (operation.status === "failed") {
|
|
178
|
+
operations.delete(operationId);
|
|
179
|
+
return {
|
|
180
|
+
ok: true,
|
|
181
|
+
payload: {
|
|
182
|
+
operationId,
|
|
183
|
+
status: "failed",
|
|
184
|
+
error: operation.error,
|
|
185
|
+
},
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
operations.delete(operationId);
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
payload: {
|
|
192
|
+
operationId,
|
|
193
|
+
status: "completed",
|
|
194
|
+
result: operation.result,
|
|
195
|
+
},
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
return { start, read };
|
|
200
|
+
}
|
package/src/http-headers.ts
CHANGED
|
@@ -1,10 +1,13 @@
|
|
|
1
|
-
|
|
1
|
+
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
|
2
|
+
|
|
3
|
+
type HttpHeaderMap = Record<string, string | string[] | undefined>;
|
|
2
4
|
|
|
3
5
|
export function getHeader(headers: HttpHeaderMap, name: string): string | undefined {
|
|
4
|
-
const target = name
|
|
6
|
+
const target = normalizeLowercaseStringOrEmpty(name);
|
|
5
7
|
const direct = headers[target];
|
|
6
8
|
const value =
|
|
7
|
-
direct ??
|
|
9
|
+
direct ??
|
|
10
|
+
Object.entries(headers).find(([key]) => normalizeLowercaseStringOrEmpty(key) === target)?.[1];
|
|
8
11
|
if (Array.isArray(value)) {
|
|
9
12
|
return value[0];
|
|
10
13
|
}
|
package/src/manager/context.ts
CHANGED
|
@@ -2,14 +2,14 @@ import type { VoiceCallConfig } from "../config.js";
|
|
|
2
2
|
import type { VoiceCallProvider } from "../providers/base.js";
|
|
3
3
|
import type { CallId, CallRecord } from "../types.js";
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
type TranscriptWaiter = {
|
|
6
6
|
resolve: (text: string) => void;
|
|
7
7
|
reject: (err: Error) => void;
|
|
8
8
|
timeout: NodeJS.Timeout;
|
|
9
9
|
turnToken?: string;
|
|
10
10
|
};
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
type CallManagerRuntimeState = {
|
|
13
13
|
activeCalls: Map<CallId, CallRecord>;
|
|
14
14
|
providerCallIdMap: Map<string, CallId>;
|
|
15
15
|
processedEventIds: Set<string>;
|
|
@@ -17,20 +17,21 @@ export type CallManagerRuntimeState = {
|
|
|
17
17
|
rejectedProviderCallIds: Set<string>;
|
|
18
18
|
};
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
type CallManagerRuntimeDeps = {
|
|
21
21
|
provider: VoiceCallProvider | null;
|
|
22
22
|
config: VoiceCallConfig;
|
|
23
23
|
storePath: string;
|
|
24
24
|
webhookUrl: string | null;
|
|
25
25
|
};
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
type CallManagerTransientState = {
|
|
28
28
|
activeTurnCalls: Set<CallId>;
|
|
29
29
|
transcriptWaiters: Map<CallId, TranscriptWaiter>;
|
|
30
30
|
maxDurationTimers: Map<CallId, NodeJS.Timeout>;
|
|
31
|
+
initialMessageInFlight: Set<CallId>;
|
|
31
32
|
};
|
|
32
33
|
|
|
33
|
-
|
|
34
|
+
type CallManagerHooks = {
|
|
34
35
|
/** Optional runtime hook invoked after an event transitions a call into answered state. */
|
|
35
36
|
onCallAnswered?: (call: CallRecord) => void;
|
|
36
37
|
};
|