






















@@ -19,7 +19,11 @@ import {
1919recoverCurrentMeetTab,
2020recoverCurrentMeetTabOnNode,
2121} from "./transports/chrome.js";
22-import { buildMeetDtmfSequence, normalizeDialInNumber } from "./transports/twilio.js";
22+import {
23+buildMeetDtmfSequence,
24+normalizeDialInNumber,
25+prefixDtmfWait,
26+} from "./transports/twilio.js";
2327import type {
2428GoogleMeetChromeHealth,
2529GoogleMeetJoinRequest,
@@ -28,6 +32,8 @@ import type {
2832} from "./transports/types.js";
2933import {
3034endMeetVoiceCallGatewayCall,
35+getMeetVoiceCallGatewayCall,
36+isVoiceCallMissingError,
3137joinMeetViaVoiceCallGateway,
3238speakMeetViaVoiceCallGateway,
3339} from "./voice-call-gateway.js";
@@ -133,6 +139,10 @@ function isManagedChromeBrowserSession(session: GoogleMeetSession): boolean {
133139);
134140}
135141142+function noteSession(session: GoogleMeetSession, note: string): void {
143+session.notes = [...session.notes.filter((item) => item !== note), note];
144+}
145+136146function evaluateSpeechReadiness(session: GoogleMeetSession): {
137147ready: boolean;
138148reason?: NonNullable<GoogleMeetChromeHealth["speechBlockedReason"]>;
@@ -365,20 +375,23 @@ export class GoogleMeetRuntime {
365375const url = normalizeMeetUrl(request.url);
366376const transport = resolveTransport(request.transport, this.params.config);
367377const mode = resolveMode(request.mode, this.params.config);
368-const reusable = this.list().find(
378+let reusable = this.list().find(
369379(session) =>
370380session.state === "active" &&
371381isSameMeetUrlForReuse(session.url, url) &&
372382session.transport === transport &&
373383session.mode === mode,
374384);
385+if (reusable?.transport === "twilio") {
386+await this.#refreshTwilioVoiceCallStatus(reusable);
387+if (reusable.state !== "active") {
388+reusable = undefined;
389+}
390+}
375391const speechInstructions = request.message ?? this.params.config.realtime.introMessage;
376392if (reusable) {
377393await this.#refreshBrowserHealthForChromeSession(reusable);
378-reusable.notes = [
379- ...reusable.notes.filter((note) => note !== "Reused existing active Meet session."),
380-"Reused existing active Meet session.",
381-];
394+noteSession(reusable, "Reused existing active Meet session.");
382395reusable.updatedAt = nowIso();
383396const spoken =
384397isGoogleMeetTalkBackMode(mode) && speechInstructions
@@ -472,10 +485,14 @@ export class GoogleMeetRuntime {
472485"Twilio transport requires a Meet dial-in phone number. Google Meet URLs do not include dial-in details; pass dialInNumber with optional pin/dtmfSequence, configure twilio.defaultDialInNumber, or use chrome/chrome-node transport.",
473486);
474487}
475-const dtmfSequence = buildMeetDtmfSequence({
488+const rawDtmfSequence = buildMeetDtmfSequence({
476489pin: request.pin ?? this.params.config.twilio.defaultPin,
477490dtmfSequence: request.dtmfSequence ?? this.params.config.twilio.defaultDtmfSequence,
478491});
492+const dtmfSequence =
493+request.dtmfSequence || this.params.config.twilio.defaultDtmfSequence
494+ ? rawDtmfSequence
495+ : prefixDtmfWait(rawDtmfSequence, this.params.config.voiceCall.dtmfDelayMs);
479496const voiceCallResult = this.params.config.voiceCall.enabled
480497 ? await joinMeetViaVoiceCallGateway({
481498config: this.params.config,
@@ -543,7 +560,12 @@ export class GoogleMeetRuntime {
543560this.#sessionStops.delete(sessionId);
544561this.#sessionSpeakers.delete(sessionId);
545562this.#sessionHealth.delete(sessionId);
546-await stop();
563+try {
564+await stop();
565+} finally {
566+session.state = "ended";
567+session.updatedAt = nowIso();
568+}
547569}
548570session.state = "ended";
549571session.updatedAt = nowIso();
@@ -559,15 +581,23 @@ export class GoogleMeetRuntime {
559581return { found: false, spoken: false };
560582}
561583if (session.transport === "twilio" && session.twilio?.voiceCallId) {
562-await speakMeetViaVoiceCallGateway({
563-config: this.params.config,
564-callId: session.twilio.voiceCallId,
565-message:
566-instructions ||
567-this.params.config.voiceCall.introMessage ||
568-this.params.config.realtime.introMessage ||
569-"",
570-});
584+try {
585+await speakMeetViaVoiceCallGateway({
586+config: this.params.config,
587+callId: session.twilio.voiceCallId,
588+message:
589+instructions ||
590+this.params.config.voiceCall.introMessage ||
591+this.params.config.realtime.introMessage ||
592+"",
593+});
594+} catch (err) {
595+if (!isVoiceCallMissingError(err)) {
596+throw err;
597+}
598+this.#markTwilioSessionEnded(session, "Voice Call is no longer active.");
599+return { found: true, spoken: false, session };
600+}
571601session.twilio.introSent = true;
572602session.updatedAt = nowIso();
573603return { found: true, spoken: true, session };
@@ -801,6 +831,41 @@ export class GoogleMeetRuntime {
801831await this.#refreshBrowserHealthForChromeSession(session, { force: true, readOnly: true });
802832return;
803833}
834+if (session.transport === "twilio") {
835+await this.#refreshTwilioVoiceCallStatus(session);
836+return;
837+}
838+this.#refreshSpeechReadiness(session);
839+}
840+841+ #markTwilioSessionEnded(session: GoogleMeetSession, reason: string) {
842+session.state = "ended";
843+session.updatedAt = nowIso();
844+this.#sessionStops.delete(session.id);
845+this.#sessionSpeakers.delete(session.id);
846+this.#sessionHealth.delete(session.id);
847+noteSession(session, reason);
848+}
849+850+async #refreshTwilioVoiceCallStatus(session: GoogleMeetSession) {
851+const callId = session.twilio?.voiceCallId;
852+if (!callId || session.state !== "active") {
853+this.#refreshSpeechReadiness(session);
854+return;
855+}
856+try {
857+const status = await getMeetVoiceCallGatewayCall({
858+config: this.params.config,
859+ callId,
860+});
861+if (status.found === false) {
862+this.#markTwilioSessionEnded(session, "Voice Call is no longer active.");
863+}
864+} catch (error) {
865+this.params.logger.debug?.(
866+`[google-meet] voice-call status refresh ignored: ${formatErrorMessage(error)}`,
867+);
868+}
804869this.#refreshSpeechReadiness(session);
805870}
806871此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。