

























@@ -44,3 +44,160 @@ final class WatchReplyCoordinator {
4444self.queuedReplies.count
4545}
4646}
47+48+@MainActor
49+final class WatchChatCoordinator {
50+enum Decision {
51+case dropMissingFields
52+case dropMissingTarget
53+case deduped(commandId: String)
54+case queue(commandId: String)
55+case forward
56+}
57+58+private static let persistedQueueKey = "watch.chat.command.queue.v1"
59+private static let maxRecentCommandIds = 128
60+61+private struct QueuedCommand: Codable, Equatable {
62+var gatewayStableID: String
63+var event: WatchAppCommandEvent
64+}
65+66+private let defaults: UserDefaults
67+private var queuedCommands: [QueuedCommand] = []
68+private var recentCommandIds: [String] = []
69+private var seenCommandIds = Set<String>()
70+71+init(defaults: UserDefaults = .standard) {
72+self.defaults = defaults
73+self.restoreQueue()
74+}
75+76+func ingest(
77+ _ event: WatchAppCommandEvent,
78+ isChatAvailable: Bool,
79+ gatewayStableID: String?) -> Decision
80+{
81+let commandId = event.commandId.trimmingCharacters(in: .whitespacesAndNewlines)
82+let text = event.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
83+if commandId.isEmpty || text.isEmpty {
84+return .dropMissingFields
85+}
86+if self.seenCommandIds.contains(commandId) {
87+return .deduped(commandId: commandId)
88+}
89+self.rememberRecentCommandId(commandId)
90+if !isChatAvailable {
91+let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
92+guard !owner.isEmpty else { return .dropMissingTarget }
93+self.queuedCommands.append(
94+QueuedCommand(gatewayStableID: owner, event: self.command(event, taggedFor: owner)))
95+self.rebuildSeenCommandIds()
96+self.persistQueue()
97+return .queue(commandId: commandId)
98+}
99+return .forward
100+}
101+102+func nextQueuedCommand(isChatAvailable: Bool, gatewayStableID: String?) -> WatchAppCommandEvent? {
103+let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
104+guard isChatAvailable, !owner.isEmpty else { return nil }
105+return self.queuedCommands.first { $0.gatewayStableID == owner }?.event
106+}
107+108+func removeQueuedCommand(commandId: String, gatewayStableID: String?) {
109+let commandId = commandId.trimmingCharacters(in: .whitespacesAndNewlines)
110+let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
111+guard !commandId.isEmpty, !owner.isEmpty else { return }
112+guard let index = self.queuedCommands.firstIndex(where: {
113+ $0.gatewayStableID == owner && $0.event.commandId == commandId
114+}) else { return }
115+self.queuedCommands.remove(at: index)
116+self.rememberRecentCommandId(commandId)
117+self.persistQueue()
118+}
119+120+func requeueFront(_ event: WatchAppCommandEvent, gatewayStableID: String?) {
121+let commandId = event.commandId.trimmingCharacters(in: .whitespacesAndNewlines)
122+let owner = gatewayStableID?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
123+guard !owner.isEmpty else { return }
124+if !commandId.isEmpty {
125+self.rememberRecentCommandId(commandId)
126+self.queuedCommands.removeAll { $0.event.commandId == commandId }
127+}
128+self.queuedCommands.insert(
129+QueuedCommand(gatewayStableID: owner, event: self.command(event, taggedFor: owner)),
130+ at: 0)
131+self.rebuildSeenCommandIds()
132+self.persistQueue()
133+}
134+135+var queuedCount: Int {
136+self.queuedCommands.count
137+}
138+139+var queuedCommandIds: [String] {
140+self.queuedCommands.map(\.event.commandId)
141+}
142+143+private func restoreQueue() {
144+guard let data = defaults.data(forKey: Self.persistedQueueKey),
145+let persisted = try? JSONDecoder().decode([QueuedCommand].self, from: data)
146+else {
147+return
148+}
149+150+var seen: [String] = []
151+var seenSet = Set<String>()
152+self.queuedCommands = persisted.compactMap { queued in
153+let owner = queued.gatewayStableID.trimmingCharacters(in: .whitespacesAndNewlines)
154+let commandId = queued.event.commandId.trimmingCharacters(in: .whitespacesAndNewlines)
155+let text = queued.event.text?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
156+guard !owner.isEmpty, !commandId.isEmpty, !text.isEmpty, seenSet.insert(commandId).inserted else {
157+return nil
158+}
159+ seen.append(commandId)
160+return QueuedCommand(gatewayStableID: owner, event: self.command(queued.event, taggedFor: owner))
161+}
162+self.recentCommandIds = Array(seen.suffix(Self.maxRecentCommandIds))
163+self.rebuildSeenCommandIds()
164+if self.queuedCommands.count != persisted.count {
165+self.persistQueue()
166+}
167+}
168+169+private func rememberRecentCommandId(_ commandId: String) {
170+guard !commandId.isEmpty else { return }
171+self.recentCommandIds.removeAll { $0 == commandId }
172+self.recentCommandIds.append(commandId)
173+if self.recentCommandIds.count > Self.maxRecentCommandIds {
174+self.recentCommandIds.removeFirst(self.recentCommandIds.count - Self.maxRecentCommandIds)
175+}
176+self.rebuildSeenCommandIds()
177+}
178+179+private func rebuildSeenCommandIds() {
180+var ids = Set(self.recentCommandIds)
181+ ids.formUnion(self.queuedCommands.map(\.event.commandId))
182+self.seenCommandIds = ids
183+}
184+185+private func persistQueue() {
186+if self.queuedCommands.isEmpty {
187+self.defaults.removeObject(forKey: Self.persistedQueueKey)
188+return
189+}
190+guard let data = try? JSONEncoder().encode(queuedCommands) else { return }
191+self.defaults.set(data, forKey: Self.persistedQueueKey)
192+}
193+194+private func command(_ event: WatchAppCommandEvent, taggedFor gatewayStableID: String) -> WatchAppCommandEvent {
195+var tagged = event
196+ tagged.gatewayStableID = gatewayStableID
197+return tagged
198+}
199+200+static func resetPersistedQueue(defaults: UserDefaults = .standard) {
201+ defaults.removeObject(forKey: self.persistedQueueKey)
202+}
203+}
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。