





















@@ -41,6 +41,78 @@ enum VoiceWakeForwarder {
4141var voiceWakeTrigger: String?
4242}
434344+private struct SessionListResponse: Decodable {
45+let sessions: [SessionRouteEntry]
46+}
47+48+struct SessionRouteEntry: Decodable, Equatable {
49+let key: String
50+let channel: String?
51+let lastChannel: String?
52+let lastTo: String?
53+let deliveryContext: DeliveryContext?
54+}
55+56+struct DeliveryContext: Decodable, Equatable {
57+let channel: String?
58+let to: String?
59+}
60+61+static func selectedSessionOptions(voiceWakeTrigger: String? = nil) async -> ForwardOptions {
62+let activeSessionKey = await MainActor.run { WebChatManager.shared.activeSessionKey }
63+let sessionKey: String = if let activeSessionKey = activeSessionKey?.trimmingCharacters(
64+ in: .whitespacesAndNewlines),
65+ !activeSessionKey.isEmpty
66+{
67+ activeSessionKey
68+} else {
69+await GatewayConnection.shared.mainSessionKey()
70+}
71+72+let routeEntry = await self.loadSessionRouteEntry(sessionKey: sessionKey)
73+return self.forwardOptions(
74+ sessionKey: sessionKey,
75+ routeEntry: routeEntry,
76+ voiceWakeTrigger: voiceWakeTrigger)
77+}
78+79+static func forwardOptions(
80+ sessionKey: String,
81+ routeEntry: SessionRouteEntry?,
82+ voiceWakeTrigger: String? = nil) -> ForwardOptions
83+{
84+let parsedRoute = self.parseSessionKeyRoute(sessionKey)
85+let channelRaw = self.firstNonEmpty(
86+ routeEntry?.deliveryContext?.channel,
87+ routeEntry?.lastChannel,
88+ routeEntry?.channel,
89+ parsedRoute?.channel)
90+let channel = channelRaw
91+.flatMap { GatewayAgentChannel(rawValue: $0.trimmingCharacters(in: .whitespacesAndNewlines).lowercased()) }
92+?? .webchat
93+let to = self.firstNonEmpty(
94+ routeEntry?.deliveryContext?.to,
95+ routeEntry?.lastTo,
96+ parsedRoute?.to)
97+98+return ForwardOptions(
99+ sessionKey: sessionKey,
100+ thinking: "low",
101+ deliver: true,
102+ to: to,
103+ channel: channel,
104+ voiceWakeTrigger: voiceWakeTrigger)
105+}
106+107+@discardableResult
108+static func forwardToSelectedSession(
109+ transcript: String,
110+ voiceWakeTrigger: String? = nil) async -> Result<Void, VoiceWakeForwardError>
111+{
112+let options = await self.selectedSessionOptions(voiceWakeTrigger: voiceWakeTrigger)
113+return await self.forward(transcript: transcript, options: options)
114+}
115+44116@discardableResult
45117static func forward(
46118 transcript: String,
@@ -72,4 +144,56 @@ enum VoiceWakeForwarder {
72144if status.ok { return .success(()) }
73145return .failure(.rpcFailed(status.error ?? "agent rpc unreachable"))
74146}
147+148+private static func loadSessionRouteEntry(sessionKey: String) async -> SessionRouteEntry? {
149+do {
150+let data = try await GatewayConnection.shared.request(
151+ method: "sessions.list",
152+ params: [
153+"includeGlobal": AnyCodable(false),
154+"includeUnknown": AnyCodable(false),
155+"limit": AnyCodable(500),
156+],
157+ timeoutMs: 10000)
158+let response = try JSONDecoder().decode(SessionListResponse.self, from: data)
159+return response.sessions.first {
160+ $0.key.trimmingCharacters(in: .whitespacesAndNewlines)
161+.caseInsensitiveCompare(sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)) == .orderedSame
162+}
163+} catch {
164+self.logger.debug(
165+"voice wake selected route lookup failed: \(error.localizedDescription, privacy: .public)")
166+return nil
167+}
168+}
169+170+private static func parseSessionKeyRoute(_ sessionKey: String) -> (channel: String, to: String?)? {
171+let trimmed = sessionKey.trimmingCharacters(in: .whitespacesAndNewlines)
172+guard !trimmed.isEmpty else { return nil }
173+let rawParts = trimmed.split(separator: ":", omittingEmptySubsequences: true).map(String.init)
174+let body: [String] = if rawParts.count >= 3, rawParts[0].caseInsensitiveCompare("agent") == .orderedSame {
175+Array(rawParts.dropFirst(2))
176+} else {
177+ rawParts
178+}
179+guard body.count >= 3 else { return nil }
180+let kind = body[1].trimmingCharacters(in: .whitespacesAndNewlines).lowercased()
181+guard kind == "direct" || kind == "group" || kind == "channel" else { return nil }
182+let channel = body[0].trimmingCharacters(in: .whitespacesAndNewlines)
183+guard !channel.isEmpty else { return nil }
184+let to = body.dropFirst(2)
185+.joined(separator: ":")
186+.trimmingCharacters(in: .whitespacesAndNewlines)
187+return (channel: channel, to: to.isEmpty ? nil : to)
188+}
189+190+private static func firstNonEmpty(_ values: String?...) -> String? {
191+for value in values {
192+let trimmed = value?.trimmingCharacters(in: .whitespacesAndNewlines)
193+if let trimmed, !trimmed.isEmpty {
194+return trimmed
195+}
196+}
197+return nil
198+}
75199}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。