import { c as normalizeOptionalString, s as normalizeOptionalLowercaseString } from "../../string-coerce-Bje8XVt9.js";
import { i as formatErrorMessage } from "../../errors-CSrrtEb1.js";
import { _ as sleep } from "../../utils-DvkbxKCZ.js";
import "../../text-runtime-BXDoRNVl.js";
import { t as definePluginEntry } from "../../plugin-entry-DyZc6JGI.js";
import "../../error-runtime-WuGIzMy0.js";
import "../../api-BKmC2vtG.js";
import { i as validateProviderConfig, r as resolveVoiceCallConfig } from "../../config-DSGQSJLJ.js";
import { a as resolveUserPath, i as setupTailscaleExposureRoute, n as cleanupTailscaleExposureRoute, r as getTailscaleSelfInfo, t as createVoiceCallRuntime } from "../../runtime-entry-B77su_y6.js";
import { i as parseVoiceCallPluginConfig, r as normalizeVoiceCallLegacyConfigInput, t as formatVoiceCallLegacyConfigWarnings } from "../../config-compat-DSn_xBvQ.js";
import fs from "node:fs";
import path from "node:path";
import os from "node:os";
import { format } from "node:util";
import { Type } from "typebox";
//#region extensions/voice-call/src/cli.ts
function writeStdoutLine(...values) {
	process.stdout.write(`${format(...values)}\n`);
}
function writeStdoutJson(value) {
	process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
}
function resolveMode(input) {
	const raw = normalizeOptionalLowercaseString(input) ?? "";
	if (raw === "serve" || raw === "off") return raw;
	return "funnel";
}
function resolveDefaultStorePath(config) {
	const resolvedPreferred = resolveUserPath(path.join(os.homedir(), ".openclaw", "voice-calls"));
	const existing = [resolvedPreferred].find((dir) => {
		try {
			return fs.existsSync(path.join(dir, "calls.jsonl")) || fs.existsSync(dir);
		} catch {
			return false;
		}
	}) ?? resolvedPreferred;
	const base = config.store?.trim() ? resolveUserPath(config.store) : existing;
	return path.join(base, "calls.jsonl");
}
function percentile(values, p) {
	if (values.length === 0) return 0;
	const sorted = [...values].toSorted((a, b) => a - b);
	return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(p / 100 * sorted.length) - 1))] ?? 0;
}
function summarizeSeries(values) {
	if (values.length === 0) return {
		count: 0,
		minMs: 0,
		maxMs: 0,
		avgMs: 0,
		p50Ms: 0,
		p95Ms: 0
	};
	const minMs = values.reduce((min, value) => value < min ? value : min, Number.POSITIVE_INFINITY);
	const maxMs = values.reduce((max, value) => value > max ? value : max, Number.NEGATIVE_INFINITY);
	const avgMs = values.reduce((sum, value) => sum + value, 0) / values.length;
	return {
		count: values.length,
		minMs,
		maxMs,
		avgMs,
		p50Ms: percentile(values, 50),
		p95Ms: percentile(values, 95)
	};
}
function resolveCallMode(mode) {
	return mode === "notify" || mode === "conversation" ? mode : void 0;
}
function hasPublicExposure(config) {
	return Boolean(config.publicUrl || config.tunnel?.provider && config.tunnel.provider !== "none" || config.tailscale?.mode && config.tailscale.mode !== "off");
}
function buildSetupStatus(config) {
	const validation = validateProviderConfig(config);
	const checks = [
		{
			id: "plugin-enabled",
			ok: config.enabled,
			message: config.enabled ? "Voice Call 插件已启用" : "Enable plugins.entries.voice-call.enabled"
		},
		{
			id: "provider",
			ok: Boolean(config.provider),
			message: config.provider ? `Provider configured: ${config.provider}` : "Set plugins.entries.voice-call.config.provider"
		},
		{
			id: "provider-config",
			ok: validation.valid,
			message: validation.valid ? "提供商凭证/配置看起来完整" : validation.errors.join("; ")
		},
		{
			id: "webhook-exposure",
			ok: config.provider === "mock" || hasPublicExposure(config),
			message: config.provider === "mock" ? "Mock provider does not need a public webhook" : hasPublicExposure(config) ? config.publicUrl ? `Public webhook URL configured: ${config.publicUrl}` : "Webhook exposure configured through tunnel or Tailscale" : "Set publicUrl or configure tunnel/tailscale so the provider can reach webhooks"
		},
		{
			id: "mode",
			ok: !(config.streaming.enabled && config.realtime.enabled),
			message: config.streaming.enabled && config.realtime.enabled ? "streaming.enabled and realtime.enabled cannot both be true" : config.realtime.enabled ? `Realtime voice enabled (${config.realtime.provider ?? "first registered provider"})` : config.streaming.enabled ? `Streaming transcription enabled (${config.streaming.provider ?? "first registered provider"})` : "Notify/conversation 通话使用正常的 TTS/STT 流程"
		}
	];
	return {
		ok: checks.every((check) => check.ok),
		checks
	};
}
function writeSetupStatus(status) {
	writeStdoutLine("Voice Call setup: %s", status.ok ? "OK" : "needs attention");
	for (const check of status.checks) writeStdoutLine("%s %s: %s", check.ok ? "OK" : "FAIL", check.id, check.message);
}
async function initiateCallAndPrintId(params) {
	const result = await params.runtime.manager.initiateCall(params.to, void 0, {
		message: params.message,
		mode: resolveCallMode(params.mode)
	});
	if (!result.success) throw new Error(result.error || "initiate failed");
	writeStdoutJson({ callId: result.callId });
}
function registerVoiceCallCli(params) {
	const { program, config, ensureRuntime, logger } = params;
	const root = program.command("voicecall").description("语音通话工具").addHelpText("after", () => `\nDocs: https://docs.openclaw.ai/cli/voicecall\n`);
	root.command("setup").description("显示 Voice Call 提供商和 webhook 设置状态").option("--json", "打印机器可读 JSON").action((options) => {
		const status = buildSetupStatus(config);
		if (options.json) {
			writeStdoutJson(status);
			return;
		}
		writeSetupStatus(status);
	});
	root.command("smoke").description("检查 Voice Call 就绪状态，并可选择发起一次简短外呼测试").option("-t, --to <phone>", "测试外呼的电话号码").option("--message <text>", "测试通话时朗读的消息", "OpenClaw 语音通话测试。").option("--mode <mode>", "通话模式：notify 或 conversation", "notify").option("--yes", "实际发起外呼通话").option("--json", "打印机器可读 JSON").action(async (options) => {
		const setup = buildSetupStatus(config);
		if (!setup.ok) {
			if (options.json) writeStdoutJson({
				ok: false,
				setup
			});
			else writeSetupStatus(setup);
			process.exitCode = 1;
			return;
		}
		if (!options.to) {
			if (options.json) writeStdoutJson({
				ok: true,
				setup,
				liveCall: false
			});
			else {
				writeSetupStatus(setup);
				writeStdoutLine("live-call: skipped (pass --to and --yes to place one)");
			}
			return;
		}
		if (!options.yes) {
			if (options.json) writeStdoutJson({
				ok: true,
				setup,
				liveCall: false,
				wouldCall: options.to
			});
			else {
				writeSetupStatus(setup);
				writeStdoutLine("live-call: dry run for %s (add --yes to place it)", options.to);
			}
			return;
		}
		const result = await (await ensureRuntime()).manager.initiateCall(options.to, void 0, {
			message: options.message,
			mode: resolveCallMode(options.mode) ?? "notify"
		});
		if (!result.success) throw new Error(result.error || "smoke call failed");
		if (options.json) {
			writeStdoutJson({
				ok: true,
				setup,
				liveCall: true,
				callId: result.callId
			});
			return;
		}
		writeSetupStatus(setup);
		writeStdoutLine("live-call: started %s", result.callId);
	});
	root.command("call").description("发起外呼语音通话").requiredOption("-m, --message <text>", "通话连接时朗读的消息").option("-t, --to <phone>", "呼叫的电话号码 (E.164 格式，如果未设置则使用配置的 toNumber)").option("--mode <mode>", "通话模式: notify (消息后挂断) 或 conversation (保持通话)", "conversation").action(async (options) => {
		const rt = await ensureRuntime();
		const to = options.to ?? rt.config.toNumber;
		if (!to) throw new Error("缺少 --to 且未配置 toNumber");
		await initiateCallAndPrintId({
			runtime: rt,
			to,
			message: options.message,
			mode: options.mode
		});
	});
	root.command("start").description("voicecall call 的别名").requiredOption("--to <phone>", "呼叫的电话号码").option("--message <text>", "通话连接时朗读的消息").option("--mode <mode>", "通话模式: notify (消息后挂断) 或 conversation (保持通话)", "conversation").action(async (options) => {
		await initiateCallAndPrintId({
			runtime: await ensureRuntime(),
			to: options.to,
			message: options.message,
			mode: options.mode
		});
	});
	root.command("continue").description("朗读消息并等待响应").requiredOption("--call-id <id>", "通话 ID").requiredOption("--message <text>", "要朗读的消息").action(async (options) => {
		const result = await (await ensureRuntime()).manager.continueCall(options.callId, options.message);
		if (!result.success) throw new Error(result.error || "continue failed");
		writeStdoutJson(result);
	});
	root.command("speak").description("朗读消息而不等待响应").requiredOption("--call-id <id>", "通话 ID").requiredOption("--message <text>", "要朗读的消息").action(async (options) => {
		const result = await (await ensureRuntime()).manager.speak(options.callId, options.message);
		if (!result.success) throw new Error(result.error || "speak failed");
		writeStdoutJson(result);
	});
	root.command("dtmf").description("向活动通话发送 DTMF 数字").requiredOption("--call-id <id>", "通话 ID").requiredOption("--digits <digits>", "DTMF 数字").action(async (options) => {
		const result = await (await ensureRuntime()).manager.sendDtmf(options.callId, options.digits);
		if (!result.success) throw new Error(result.error || "dtmf failed");
		writeStdoutJson(result);
	});
	root.command("end").description("挂断活动通话").requiredOption("--call-id <id>", "通话 ID").action(async (options) => {
		const result = await (await ensureRuntime()).manager.endCall(options.callId);
		if (!result.success) throw new Error(result.error || "end failed");
		writeStdoutJson(result);
	});
	root.command("status").description("显示通话状态").requiredOption("--call-id <id>", "通话 ID").action(async (options) => {
		writeStdoutJson((await ensureRuntime()).manager.getCall(options.callId) ?? { found: false });
	});
	root.command("tail").description("跟踪 voice-call JSONL 日志 (打印新行；用于提供商测试)").option("--file <path>", "calls.jsonl 的路径", resolveDefaultStorePath(config)).option("--since <n>", "先打印最后 N 行", "25").option("--poll <ms>", "轮询间隔 (毫秒)", "250").action(async (options) => {
		const file = options.file;
		const since = Math.max(0, Number(options.since ?? 0));
		const pollMs = Math.max(50, Number(options.poll ?? 250));
		if (!fs.existsSync(file)) {
			logger.error(`No log file at ${file}`);
			process.exit(1);
		}
		const initial = fs.readFileSync(file, "utf8");
		const lines = initial.split("\n").filter(Boolean);
		for (const line of lines.slice(Math.max(0, lines.length - since))) writeStdoutLine(line);
		let offset = Buffer.byteLength(initial, "utf8");
		for (;;) {
			try {
				const stat = fs.statSync(file);
				if (stat.size < offset) offset = 0;
				if (stat.size > offset) {
					const fd = fs.openSync(file, "r");
					try {
						const buf = Buffer.alloc(stat.size - offset);
						fs.readSync(fd, buf, 0, buf.length, offset);
						offset = stat.size;
						const text = buf.toString("utf8");
						for (const line of text.split("\n").filter(Boolean)) writeStdoutLine(line);
					} finally {
						fs.closeSync(fd);
					}
				}
			} catch {}
			await sleep(pollMs);
		}
	});
	root.command("latency").description("Summarize turn latency metrics from voice-call JSONL logs").option("--file <path>", "calls.jsonl 的路径", resolveDefaultStorePath(config)).option("--last <n>", "Analyze last N records", "200").action(async (options) => {
		const file = options.file;
		const last = Math.max(1, Number(options.last ?? 200));
		if (!fs.existsSync(file)) throw new Error("No log file at " + file);
		const lines = fs.readFileSync(file, "utf8").split("\n").filter(Boolean).slice(-last);
		const turnLatencyMs = [];
		const listenWaitMs = [];
		for (const line of lines) try {
			const parsed = JSON.parse(line);
			const latency = parsed.metadata?.lastTurnLatencyMs;
			const listenWait = parsed.metadata?.lastTurnListenWaitMs;
			if (typeof latency === "number" && Number.isFinite(latency)) turnLatencyMs.push(latency);
			if (typeof listenWait === "number" && Number.isFinite(listenWait)) listenWaitMs.push(listenWait);
		} catch {}
		writeStdoutJson({
			recordsScanned: lines.length,
			turnLatency: summarizeSeries(turnLatencyMs),
			listenWait: summarizeSeries(listenWaitMs)
		});
	});
	root.command("expose").description("启用/禁用 Webhook 的 Tailscale serve/funnel").option("--mode <mode>", "off | serve (Tailnet) | funnel (公共)", "funnel").option("--path <path>", "Tailscale path to expose (recommend matching serve.path)").option("--port <port>", "本地 Webhook 端口").option("--serve-path <path>", "本地 Webhook 路径").action(async (options) => {
		const mode = resolveMode(options.mode ?? "funnel");
		const servePort = Number(options.port ?? config.serve.port ?? 3334);
		const servePath = options.servePath ?? config.serve.path ?? "/voice/webhook";
		const tsPath = options.path ?? config.tailscale?.path ?? servePath;
		const localUrl = `http://127.0.0.1:${servePort}`;
		if (mode === "off") {
			await cleanupTailscaleExposureRoute({
				mode: "serve",
				path: tsPath
			});
			await cleanupTailscaleExposureRoute({
				mode: "funnel",
				path: tsPath
			});
			writeStdoutJson({
				ok: true,
				mode: "off",
				path: tsPath
			});
			return;
		}
		const publicUrl = await setupTailscaleExposureRoute({
			mode,
			path: tsPath,
			localUrl
		});
		const tsInfo = publicUrl ? null : await getTailscaleSelfInfo();
		const enableUrl = tsInfo?.nodeId ? `https://login.tailscale.com/f/${mode}?node=${tsInfo.nodeId}` : null;
		writeStdoutJson({
			ok: Boolean(publicUrl),
			mode,
			path: tsPath,
			localUrl,
			publicUrl,
			hint: publicUrl ? void 0 : {
				note: "Tailscale serve/funnel may be disabled on this tailnet (or require admin enable).",
				enableUrl
			}
		});
	});
}
//#endregion
//#region extensions/voice-call/index.ts
const voiceCallConfigSchema = {
	parse(value) {
		const normalized = normalizeVoiceCallLegacyConfigInput(value);
		const enabled = typeof normalized.enabled === "boolean" ? normalized.enabled : true;
		return parseVoiceCallPluginConfig({
			...normalized,
			enabled,
			provider: normalized.provider ?? (enabled ? "mock" : void 0)
		});
	},
	uiHints: {
		provider: {
			label: "提供商",
			help: "开发/无网络环境使用 twilio, telnyx 或 mock。"
		},
		fromNumber: {
			label: "主叫号码",
			placeholder: "+15550001234"
		},
		toNumber: {
			label: "默认被叫号码",
			placeholder: "+15550001234"
		},
		inboundPolicy: { label: "呼入策略" },
		allowFrom: { label: "呼入白名单" },
		inboundGreeting: {
			label: "呼入问候语",
			advanced: true
		},
		"telnyx.apiKey": {
			label: "Telnyx API 密钥",
			sensitive: true
		},
		"telnyx.connectionId": { label: "Telnyx 连接 ID" },
		"telnyx.publicKey": {
			label: "Telnyx 公钥",
			sensitive: true
		},
		"twilio.accountSid": { label: "Twilio 账户 SID" },
		"twilio.authToken": {
			label: "Twilio 认证令牌",
			sensitive: true
		},
		"outbound.defaultMode": { label: "默认通话模式" },
		"outbound.notifyHangupDelaySec": {
			label: "挂断通知延迟 (秒)",
			advanced: true
		},
		"serve.port": { label: "Webhook 端口" },
		"serve.bind": { label: "Webhook 绑定" },
		"serve.path": { label: "Webhook 路径" },
		"tailscale.mode": {
			label: "Tailscale 模式",
			advanced: true
		},
		"tailscale.path": {
			label: "Tailscale 路径",
			advanced: true
		},
		"tunnel.provider": {
			label: "隧道提供商",
			advanced: true
		},
		"tunnel.ngrokAuthToken": {
			label: "ngrok 认证令牌",
			sensitive: true,
			advanced: true
		},
		"tunnel.ngrokDomain": {
			label: "ngrok 域名",
			advanced: true
		},
		"tunnel.allowNgrokFreeTierLoopbackBypass": {
			label: "允许 ngrok 免费层 (回环绕过)",
			advanced: true
		},
		"streaming.enabled": {
			label: "启用流式传输",
			advanced: true
		},
		"streaming.provider": {
			label: "流式提供商",
			help: "Uses the first registered realtime transcription provider when unset.",
			advanced: true
		},
		"streaming.providers": {
			label: "流式提供商配置",
			advanced: true
		},
		"streaming.streamPath": {
			label: "媒体流路径",
			advanced: true
		},
		"realtime.enabled": {
			label: "启用实时语音",
			advanced: true
		},
		"realtime.provider": {
			label: "实时语音提供商",
			help: "Uses the first registered realtime voice provider when unset.",
			advanced: true
		},
		"realtime.streamPath": {
			label: "实时流路径",
			advanced: true
		},
		"realtime.instructions": {
			label: "实时指令",
			advanced: true
		},
		"realtime.toolPolicy": {
			label: "实时工具策略",
			help: "控制共享的 openclaw_agent_consult 工具。",
			advanced: true
		},
		"realtime.providers": {
			label: "实时提供商配置",
			advanced: true
		},
		"tts.provider": {
			label: "TTS 提供商覆盖",
			help: "与 messages.tts 深度合并 (通话忽略 Microsoft)。",
			advanced: true
		},
		"tts.providers": {
			label: "TTS 提供商配置",
			advanced: true
		},
		publicUrl: {
			label: "公共 Webhook URL",
			advanced: true
		},
		skipSignatureVerification: {
			label: "跳过签名验证",
			advanced: true
		},
		store: {
			label: "通话日志存储路径",
			advanced: true
		},
		agentId: {
			label: "响应代理 ID",
			help: "语音响应生成所使用的代理工作区。默认为 \"main\"。",
			advanced: true
		},
		responseModel: {
			label: "响应模型",
			help: "Optional override. Falls back to the runtime default model when unset.",
			advanced: true
		},
		responseSystemPrompt: {
			label: "响应系统提示词",
			advanced: true
		},
		responseTimeoutMs: {
			label: "响应超时 (毫秒)",
			advanced: true
		}
	}
};
const VoiceCallToolSchema = Type.Union([
	Type.Object({
		action: Type.Literal("initiate_call"),
		to: Type.Optional(Type.String({ description: "通话目标" })),
		message: Type.String({ description: "开场消息" }),
		mode: Type.Optional(Type.Union([Type.Literal("notify"), Type.Literal("conversation")]))
	}),
	Type.Object({
		action: Type.Literal("continue_call"),
		callId: Type.String({ description: "通话 ID" }),
		message: Type.String({ description: "跟进消息" })
	}),
	Type.Object({
		action: Type.Literal("speak_to_user"),
		callId: Type.String({ description: "通话 ID" }),
		message: Type.String({ description: "要朗读的消息" })
	}),
	Type.Object({
		action: Type.Literal("send_dtmf"),
		callId: Type.String({ description: "通话 ID" }),
		digits: Type.String({ description: "DTMF digits to send" })
	}),
	Type.Object({
		action: Type.Literal("end_call"),
		callId: Type.String({ description: "通话 ID" })
	}),
	Type.Object({
		action: Type.Literal("get_status"),
		callId: Type.String({ description: "通话 ID" })
	}),
	Type.Object({
		mode: Type.Optional(Type.Union([Type.Literal("call"), Type.Literal("status")])),
		to: Type.Optional(Type.String({ description: "通话目标" })),
		sid: Type.Optional(Type.String({ description: "通话 SID" })),
		message: Type.Optional(Type.String({ description: "可选开场消息" }))
	})
]);
function asParamRecord(params) {
	return params && typeof params === "object" && !Array.isArray(params) ? params : {};
}
const VOICE_CALL_RUNTIME_KEY = Symbol.for("openclaw.voice-call.runtime");
const VOICE_CALL_RUNTIME_PROMISE_KEY = Symbol.for("openclaw.voice-call.runtimePromise");
const VOICE_CALL_RUNTIME_STOP_PROMISE_KEY = Symbol.for("openclaw.voice-call.runtimeStopPromise");
function getVoiceCallRuntimeGlobalState() {
	const state = globalThis;
	state[VOICE_CALL_RUNTIME_KEY] ??= null;
	state[VOICE_CALL_RUNTIME_PROMISE_KEY] ??= null;
	state[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] ??= null;
	return state;
}
var voice_call_default = definePluginEntry({
	id: "voice-call",
	name: "语音通话",
	description: "支持 Telnyx/Twilio/Plivo 的语音通话插件",
	configSchema: voiceCallConfigSchema,
	register(api) {
		const config = resolveVoiceCallConfig(voiceCallConfigSchema.parse(api.pluginConfig));
		const validation = validateProviderConfig(config);
		if (api.pluginConfig && typeof api.pluginConfig === "object") for (const warning of formatVoiceCallLegacyConfigWarnings({
			value: api.pluginConfig,
			configPathPrefix: "plugins.entries.voice-call.config",
			doctorFixCommand: "openclaw doctor --fix"
		})) api.logger.warn(warning);
		const runtimeState = getVoiceCallRuntimeGlobalState();
		const ensureRuntime = async () => {
			if (!config.enabled) throw new Error("语音通话在插件配置中已禁用");
			if (!validation.valid) throw new Error(validation.errors.join("; "));
			while (true) {
				if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
					await runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY];
					continue;
				}
				const runtime = runtimeState[VOICE_CALL_RUNTIME_KEY];
				if (runtime) return runtime;
				let runtimePromise = runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY];
				if (!runtimePromise) {
					runtimePromise = createVoiceCallRuntime({
						config,
						coreConfig: api.config,
						fullConfig: api.config,
						agentRuntime: api.runtime.agent,
						ttsRuntime: api.runtime.tts,
						logger: api.logger
					});
					runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = runtimePromise;
				}
				try {
					const createdRuntime = await runtimePromise;
					if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) continue;
					if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] !== runtimePromise) continue;
					runtimeState[VOICE_CALL_RUNTIME_KEY] = createdRuntime;
					return createdRuntime;
				} catch (err) {
					if (runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] === runtimePromise) {
						runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
						runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
					}
					throw err;
				}
			}
		};
		const sendError = (respond, err) => {
			respond(false, { error: formatErrorMessage(err) });
		};
		const resolveCallMessageRequest = async (params) => {
			const callId = normalizeOptionalString(params?.callId) ?? "";
			const message = normalizeOptionalString(params?.message) ?? "";
			if (!callId || !message) return { error: "callId 和消息必填" };
			return {
				rt: await ensureRuntime(),
				callId,
				message
			};
		};
		const initiateCallAndRespond = async (params) => {
			const result = await params.rt.manager.initiateCall(params.to, void 0, {
				message: params.message,
				mode: params.mode
			});
			if (!result.success) {
				params.respond(false, { error: result.error || "initiate failed" });
				return;
			}
			params.respond(true, {
				callId: result.callId,
				initiated: true
			});
		};
		const respondToCallMessageAction = async (params) => {
			const request = await resolveCallMessageRequest(params.requestParams);
			if ("error" in request) {
				params.respond(false, { error: request.error });
				return;
			}
			const result = await params.action(request);
			if (!result.success) {
				params.respond(false, { error: result.error || params.failure });
				return;
			}
			params.respond(true, params.includeTranscript ? {
				success: true,
				transcript: result.transcript
			} : { success: true });
		};
		api.registerGatewayMethod("voicecall.initiate", async ({ params, respond }) => {
			try {
				const message = normalizeOptionalString(params?.message) ?? "";
				if (!message) {
					respond(false, { error: "message required" });
					return;
				}
				const rt = await ensureRuntime();
				const to = normalizeOptionalString(params?.to) ?? rt.config.toNumber;
				if (!to) {
					respond(false, { error: "to required" });
					return;
				}
				await initiateCallAndRespond({
					rt,
					respond,
					to,
					message,
					mode: params?.mode === "notify" || params?.mode === "conversation" ? params.mode : void 0
				});
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerGatewayMethod("voicecall.continue", async ({ params, respond }) => {
			try {
				await respondToCallMessageAction({
					requestParams: params,
					respond,
					action: (request) => request.rt.manager.continueCall(request.callId, request.message),
					failure: "继续失败",
					includeTranscript: true
				});
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerGatewayMethod("voicecall.speak", async ({ params, respond }) => {
			try {
				await respondToCallMessageAction({
					requestParams: params,
					respond,
					action: (request) => request.rt.manager.speak(request.callId, request.message),
					failure: "speak failed"
				});
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerGatewayMethod("voicecall.dtmf", async ({ params, respond }) => {
			try {
				const callId = normalizeOptionalString(params?.callId) ?? "";
				const digits = normalizeOptionalString(params?.digits) ?? "";
				if (!callId || !digits) {
					respond(false, { error: "callId and digits required" });
					return;
				}
				const result = await (await ensureRuntime()).manager.sendDtmf(callId, digits);
				if (!result.success) {
					respond(false, { error: result.error || "dtmf failed" });
					return;
				}
				respond(true, { success: true });
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerGatewayMethod("voicecall.end", async ({ params, respond }) => {
			try {
				const callId = normalizeOptionalString(params?.callId) ?? "";
				if (!callId) {
					respond(false, { error: "callId required" });
					return;
				}
				const result = await (await ensureRuntime()).manager.endCall(callId);
				if (!result.success) {
					respond(false, { error: result.error || "end failed" });
					return;
				}
				respond(true, { success: true });
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerGatewayMethod("voicecall.status", async ({ params, respond }) => {
			try {
				const raw = normalizeOptionalString(params?.callId) ?? normalizeOptionalString(params?.sid) ?? "";
				if (!raw) {
					respond(false, { error: "callId required" });
					return;
				}
				const rt = await ensureRuntime();
				const call = rt.manager.getCall(raw) || rt.manager.getCallByProviderCallId(raw);
				if (!call) {
					respond(true, { found: false });
					return;
				}
				respond(true, {
					found: true,
					call
				});
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerGatewayMethod("voicecall.start", async ({ params, respond }) => {
			try {
				const to = normalizeOptionalString(params?.to) ?? "";
				const message = normalizeOptionalString(params?.message) ?? "";
				if (!to) {
					respond(false, { error: "to required" });
					return;
				}
				await initiateCallAndRespond({
					rt: await ensureRuntime(),
					respond,
					to,
					message: message || void 0
				});
			} catch (err) {
				sendError(respond, err);
			}
		});
		api.registerTool({
			name: "voice_call",
			label: "Voice Call",
			description: "通过语音通话插件拨打电话并进行语音对话。",
			parameters: VoiceCallToolSchema,
			async execute(_toolCallId, params) {
				const rawParams = asParamRecord(params);
				const json = (payload) => ({
					content: [{
						type: "text",
						text: JSON.stringify(payload, null, 2)
					}],
					details: payload
				});
				try {
					const rt = await ensureRuntime();
					if (typeof rawParams.action === "string") switch (rawParams.action) {
						case "initiate_call": {
							const message = normalizeOptionalString(rawParams.message) ?? "";
							if (!message) throw new Error("消息必填");
							const to = normalizeOptionalString(rawParams.to) ?? rt.config.toNumber;
							if (!to) throw new Error("目标(to)必填");
							const result = await rt.manager.initiateCall(to, void 0, {
								message,
								mode: rawParams.mode === "notify" || rawParams.mode === "conversation" ? rawParams.mode : void 0
							});
							if (!result.success) throw new Error(result.error || "发起失败");
							return json({
								callId: result.callId,
								initiated: true
							});
						}
						case "continue_call": {
							const callId = normalizeOptionalString(rawParams.callId) ?? "";
							const message = normalizeOptionalString(rawParams.message) ?? "";
							if (!callId || !message) throw new Error("callId 和消息必填");
							const result = await rt.manager.continueCall(callId, message);
							if (!result.success) throw new Error(result.error || "继续失败");
							return json({
								success: true,
								transcript: result.transcript
							});
						}
						case "speak_to_user": {
							const callId = normalizeOptionalString(rawParams.callId) ?? "";
							const message = normalizeOptionalString(rawParams.message) ?? "";
							if (!callId || !message) throw new Error("callId 和消息必填");
							const result = await rt.manager.speak(callId, message);
							if (!result.success) throw new Error(result.error || "朗读失败");
							return json({ success: true });
						}
						case "send_dtmf": {
							const callId = normalizeOptionalString(rawParams.callId) ?? "";
							const digits = normalizeOptionalString(rawParams.digits) ?? "";
							if (!callId || !digits) throw new Error("callId and digits required");
							const result = await rt.manager.sendDtmf(callId, digits);
							if (!result.success) throw new Error(result.error || "dtmf failed");
							return json({ success: true });
						}
						case "end_call": {
							const callId = normalizeOptionalString(rawParams.callId) ?? "";
							if (!callId) throw new Error("callId 必填");
							const result = await rt.manager.endCall(callId);
							if (!result.success) throw new Error(result.error || "挂断失败");
							return json({ success: true });
						}
						case "get_status": {
							const callId = normalizeOptionalString(rawParams.callId) ?? "";
							if (!callId) throw new Error("callId 必填");
							const call = rt.manager.getCall(callId) || rt.manager.getCallByProviderCallId(callId);
							return json(call ? {
								found: true,
								call
							} : { found: false });
						}
					}
					if ((rawParams.mode ?? "call") === "status") {
						const sid = normalizeOptionalString(rawParams.sid) ?? "";
						if (!sid) throw new Error("状态查询需要 sid");
						const call = rt.manager.getCall(sid) || rt.manager.getCallByProviderCallId(sid);
						return json(call ? {
							found: true,
							call
						} : { found: false });
					}
					const to = normalizeOptionalString(rawParams.to) ?? rt.config.toNumber;
					if (!to) throw new Error("呼叫需要目标(to)");
					const result = await rt.manager.initiateCall(to, void 0, { message: normalizeOptionalString(rawParams.message) });
					if (!result.success) throw new Error(result.error || "发起失败");
					return json({
						callId: result.callId,
						initiated: true
					});
				} catch (err) {
					return json({ error: formatErrorMessage(err) });
				}
			}
		});
		api.registerCli(({ program }) => registerVoiceCallCli({
			program,
			config,
			ensureRuntime,
			logger: api.logger
		}), { commands: ["voicecall"] });
		api.registerService({
			id: "voicecall",
			start: () => {
				if (!config.enabled) return;
				if (!validation.valid) {
					api.logger.warn(`[voice-call] Runtime not started; setup incomplete: ${validation.errors.join("; ")}`);
					return;
				}
				ensureRuntime().catch((err) => {
					api.logger.error(`[voice-call] Failed to start runtime: ${formatErrorMessage(err)}`);
				});
			},
			stop: async () => {
				if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY]) {
					await runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY];
					return;
				}
				const runtime = runtimeState[VOICE_CALL_RUNTIME_KEY];
				const runtimePromise = runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY];
				if (!runtime && !runtimePromise) return;
				runtimeState[VOICE_CALL_RUNTIME_KEY] = null;
				runtimeState[VOICE_CALL_RUNTIME_PROMISE_KEY] = null;
				const stopPromise = (async () => {
					await (runtime ?? await runtimePromise).stop();
				})();
				runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] = stopPromise;
				try {
					await stopPromise;
				} finally {
					if (runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] === stopPromise) runtimeState[VOICE_CALL_RUNTIME_STOP_PROMISE_KEY] = null;
				}
			}
		});
	}
});
//#endregion
export { voice_call_default as default };
