feat: add tiered channel agent and mobile Codex bridge

This commit is contained in:
冰朔 2026-08-21 08:21:03 +08:00
commit 71db11e7e8
35 changed files with 2242 additions and 76 deletions

View file

@ -53,7 +53,11 @@ final class HoloLakeSyncClient: ObservableObject {
sessionKey: sessionKey,
desktopName: response.desktopName,
counter: 0,
cursor: 0
cursor: 0,
agentConversationID: nil,
pendingAgentRequestID: nil,
pendingAgentContent: nil,
pendingAgentDestination: nil
)
try HoloLakeKeychain.save(next)
session = next
@ -69,9 +73,28 @@ final class HoloLakeSyncClient: ObservableObject {
await pair(with: url)
}
func sync(captureTitle: String? = nil, captureBody: String? = nil) async {
func sync(
captureTitle: String? = nil,
captureBody: String? = nil,
agentContent: String? = nil,
agentDestination: String = "HOLOLAKE_AGENT"
) async {
await perform("正在与电脑同步…") {
guard var current = session else { throw HoloLakeMobileError.missingSession }
let requestedAgentContent = agentContent?.trimmingCharacters(in: .whitespacesAndNewlines)
if let requestedAgentContent, !requestedAgentContent.isEmpty {
if let pending = current.pendingAgentContent, pending != requestedAgentContent {
throw HoloLakeMobileError.pendingAgentRequest
}
if let pending = current.pendingAgentDestination, pending != agentDestination {
throw HoloLakeMobileError.pendingAgentRequest
}
if current.pendingAgentRequestID == nil {
current.pendingAgentRequestID = UUID().uuidString.lowercased()
current.pendingAgentContent = requestedAgentContent
current.pendingAgentDestination = agentDestination
}
}
current.counter += 1
// Consume and persist the counter before transport. If the root node accepts a
// request but its response is lost, the next manual retry still moves forward.
@ -87,13 +110,32 @@ final class HoloLakeSyncClient: ObservableObject {
} else {
capture = nil
}
let agentMessage: MobileAgentMessageInput?
if let requestID = current.pendingAgentRequestID,
let content = current.pendingAgentContent,
requestedAgentContent != nil {
agentMessage = MobileAgentMessageInput(
destination: current.pendingAgentDestination ?? "HOLOLAKE_AGENT",
codexThreadID: (current.pendingAgentDestination == "CODEX_SELECTED_THREAD") ? snapshot?.codexTarget?.threadID : nil,
conversationID: (current.pendingAgentDestination == "CODEX_SELECTED_THREAD") ? nil : current.agentConversationID,
content: content,
requestID: requestID
)
} else {
agentMessage = nil
}
let aad = Data("hololake.mobile.sync/v1:\(current.deviceID)".utf8)
let responseData = try await Self.exchange(
url: current.baseURL.appending(path: "v1/sync"),
key: current.sessionKey,
aad: aad,
headers: ["X-HoloLake-Device-ID": current.deviceID],
payload: SyncRequest(counter: current.counter, afterCursor: nil, capture: capture)
payload: SyncRequest(
counter: current.counter,
afterCursor: nil,
capture: capture,
agentMessage: agentMessage
)
)
let nextSnapshot = try JSONDecoder().decode(MobileSyncSnapshot.self, from: responseData)
guard nextSnapshot.schema == "hololake.mobile-sync/v1",
@ -105,6 +147,16 @@ final class HoloLakeSyncClient: ObservableObject {
throw HoloLakeMobileError.invalidResponse
}
current.cursor = nextSnapshot.cursor
if let exchange = nextSnapshot.agentExchange,
exchange.requestID == current.pendingAgentRequestID,
exchange.state == "COMPLETED" {
if current.pendingAgentDestination != "CODEX_SELECTED_THREAD" {
current.agentConversationID = exchange.conversationID
}
current.pendingAgentRequestID = nil
current.pendingAgentContent = nil
current.pendingAgentDestination = nil
}
try HoloLakeKeychain.save(current)
session = current
snapshot = nextSnapshot
@ -112,6 +164,10 @@ final class HoloLakeSyncClient: ObservableObject {
}
}
func sendAgentMessage(_ content: String, destination: String) async {
await sync(agentContent: content, agentDestination: destination)
}
func disconnect() {
do {
try HoloLakeKeychain.remove()
@ -156,7 +212,7 @@ final class HoloLakeSyncClient: ObservableObject {
)
var request = URLRequest(url: url)
request.httpMethod = "POST"
request.timeoutInterval = 8
request.timeoutInterval = 120
request.cachePolicy = .reloadIgnoringLocalCacheData
request.httpBody = sealed.ciphertext + sealed.tag
request.setValue("application/octet-stream", forHTTPHeaderField: "Content-Type")