惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Stack Overflow Blog
Stack Overflow Blog
云风的 BLOG
云风的 BLOG
G
Google Developers Blog
J
Java Code Geeks
C
Check Point Blog
Last Week in AI
Last Week in AI
Microsoft Azure Blog
Microsoft Azure Blog
Blog — PlanetScale
Blog — PlanetScale
月光博客
月光博客
Vercel News
Vercel News
The GitHub Blog
The GitHub Blog
L
LangChain Blog
有赞技术团队
有赞技术团队
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
博客园 - 司徒正美
IT之家
IT之家
Martin Fowler
Martin Fowler
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
T
Tailwind CSS Blog
U
Unit 42
Jina AI
Jina AI
Microsoft Security Blog
Microsoft Security Blog
I
InfoQ

Recent Commits to openclaw:main

test: merge chat side-result checks · openclaw/openclaw@ddd2c2a test: merge cron history checks · openclaw/openclaw@f7eb746 test: merge responsive navigation shell checks · openclaw/openclaw@c2e4b47 docs(changelog): add codex oauth fixes · openclaw/openclaw@628e6cd test: merge navigation routing cases · openclaw/openclaw@5d8cecb Tests: mock channel registry bundled fallback · openclaw/openclaw@2b08233 Secrets: avoid broad web search discovery for single plugin config · openclaw/openclaw@a464f59 test: merge config view browser checks · openclaw/openclaw@20cf511 fix(status): align oauth health with runtime · openclaw/openclaw@eed7116 feat: add macOS screen snapshots for monitor preview (#67954) thanks … · openclaw/openclaw@f377db1 fix: report shared auth scopes in hello-ok (#67810) thanks @BunsDev · openclaw/openclaw@0b6c39b Auto-reply: avoid eager bundled route fallback · openclaw/openclaw@3ea1bf4 Tests: narrow session binding contract setup · openclaw/openclaw@54e4e16 fix(macOS): enable undo/redo in webchat composer text input (#34962) · openclaw/openclaw@00951dc Tests: speed up channel setup promotion · openclaw/openclaw@82b529a Docs: refresh agent instructions · openclaw/openclaw@5775fe2 fix(auth): serialize OAuth refresh across agents to fix #26322 (#67876) · openclaw/openclaw@8e79080 test: allow ollama public surface boundary test · openclaw/openclaw@7d4f1a6 Docs: add test performance guardrails · openclaw/openclaw@89706d3 Tests: restore context-engine usage proof · openclaw/openclaw@e4c4f95 Tests: slim context engine runtime coverage · openclaw/openclaw@74c198f ci: retry failed custom checkouts · openclaw/openclaw@0ee5baf test: trim duplicate provider auth onboarding cases · openclaw/openclaw@1ffc02e matrix: fix sessions_spawn --thread subagent session spawning (#67643) · openclaw/openclaw@1ce2596 test: reduce auth choice fixture churn · openclaw/openclaw@857b9cd test: mock health status config boundaries · openclaw/openclaw@9d5ab4a test: mock onboard config io boundary · openclaw/openclaw@299694d test: mock legacy state plugin boundaries · openclaw/openclaw@2713089 test: mock channel install boundaries · openclaw/openclaw@b945248 test: mock doctor preview channel boundaries · openclaw/openclaw@b1a3ad4
fix: route macos voice wake to selected session · opencla...
steipete · 2026-05-02 · via Recent Commits to openclaw:main

@@ -41,6 +41,78 @@ enum VoiceWakeForwarder {

4141

var 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

45117

static func forward(

46118

transcript: String,

@@ -72,4 +144,56 @@ enum VoiceWakeForwarder {

72144

if status.ok { return .success(()) }

73145

return .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

}