mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-05-01 06:36:23 +08:00
chore: update app Swift package releases
This commit is contained in:
@@ -7,6 +7,7 @@ Docs: https://docs.openclaw.ai
|
||||
### Changes
|
||||
|
||||
- Codex: add Computer Use setup for Codex-mode agents, including `/codex computer-use status/install`, marketplace discovery, optional auto-install, and fail-closed MCP server checks before Codex-mode turns start. Fixes #72094. (#71842) Thanks @pash-openai.
|
||||
- Apps: consume Peekaboo 3.0.0-beta4 and ElevenLabsKit 0.1.1, align Swabble on Commander 0.2.2, and refresh macOS/iOS SwiftPM resolutions against the released dependency graph. Thanks @Blaizzy.
|
||||
- Plugin SDK: expose shared channel route normalization, parser-driven target resolution, raw-target compact keys, parsed-target types, and route comparison helpers through `openclaw/plugin-sdk/channel-route`, switch native approval origin matching onto that route contract with optional delivery and match-only target normalization, and retire the internal channel-route shim behind dated compatibility aliases for legacy key/comparable-target helpers. Thanks @vincentkoc.
|
||||
- Docs/Codex: document how Codex Computer Use, direct `cua-driver mcp`, and OpenClaw.app's PeekabooBridge fit together so desktop-control setup choices are clearer. Thanks @pash-openai and @trycua.
|
||||
- Matrix/streaming: stream tool-progress updates into live Matrix preview edits by default when preview streaming is active, with `streaming.preview.toolProgress: false` to keep answer previews while hiding interim tool lines. Thanks @gumadeiras.
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "c0677e232394b5f6b0191b6dbb5bae553d55264f65ae725cd03a8ffdfda9cdd3",
|
||||
"originHash" : "e6910acc97de62dc423c0a391985c1c2f28207951e356081539abde41f9ffc72",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "commander",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/Commander.git",
|
||||
"state" : {
|
||||
"revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce",
|
||||
"version" : "0.2.1"
|
||||
"revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b",
|
||||
"version" : "0.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -13,7 +13,7 @@ let package = Package(
|
||||
.executable(name: "swabble", targets: ["SwabbleCLI"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.1"),
|
||||
.package(url: "https://github.com/steipete/Commander.git", exact: "0.2.2"),
|
||||
.package(url: "https://github.com/apple/swift-testing", from: "0.99.0"),
|
||||
],
|
||||
targets: [
|
||||
@@ -43,7 +43,6 @@ let package = Package(
|
||||
],
|
||||
swiftSettings: [
|
||||
.enableUpcomingFeature("StrictConcurrency"),
|
||||
.enableExperimentalFeature("SwiftTesting"),
|
||||
]),
|
||||
.testTarget(
|
||||
name: "swabbleTests",
|
||||
|
||||
@@ -45,6 +45,15 @@ extension AttributedString {
|
||||
}
|
||||
|
||||
return ranges.compactMap { range in
|
||||
guard #available(macOS 26.0, iOS 26.0, *) else {
|
||||
return AttributedString(self[range].characters)
|
||||
}
|
||||
return self.sentenceWithAudioTimeRange(range)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 26.0, iOS 26.0, *)
|
||||
private func sentenceWithAudioTimeRange(_ range: Range<AttributedString.Index>) -> AttributedString? {
|
||||
let audioTimeRanges = self[range].runs.filter {
|
||||
!String(self[$0.range].characters)
|
||||
.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty
|
||||
@@ -57,6 +66,5 @@ extension AttributedString {
|
||||
start: start,
|
||||
end: end)
|
||||
return AttributedString(self[range].characters, attributes: attributes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,29 +17,35 @@ public enum OutputFormat: String {
|
||||
case .txt:
|
||||
return String(transcript.characters)
|
||||
case .srt:
|
||||
func format(_ timeInterval: TimeInterval) -> String {
|
||||
let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000)
|
||||
let s = Int(timeInterval) % 60
|
||||
let m = (Int(timeInterval) / 60) % 60
|
||||
let h = Int(timeInterval) / 60 / 60
|
||||
return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms)
|
||||
}
|
||||
|
||||
return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> (
|
||||
CMTimeRange,
|
||||
String)? in
|
||||
guard let timeRange = sentence.audioTimeRange else { return nil }
|
||||
return (timeRange, String(sentence.characters))
|
||||
}.enumerated().map { index, run in
|
||||
let (timeRange, text) = run
|
||||
return """
|
||||
|
||||
\(index + 1)
|
||||
\(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds))
|
||||
\(text.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
|
||||
"""
|
||||
}.joined().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard #available(macOS 26.0, iOS 26.0, *) else { return "" }
|
||||
return self.srtText(for: transcript, maxLength: maxLength)
|
||||
}
|
||||
}
|
||||
|
||||
@available(macOS 26.0, iOS 26.0, *)
|
||||
private func srtText(for transcript: AttributedString, maxLength: Int) -> String {
|
||||
func format(_ timeInterval: TimeInterval) -> String {
|
||||
let ms = Int(timeInterval.truncatingRemainder(dividingBy: 1) * 1000)
|
||||
let s = Int(timeInterval) % 60
|
||||
let m = (Int(timeInterval) / 60) % 60
|
||||
let h = Int(timeInterval) / 60 / 60
|
||||
return String(format: "%0.2d:%0.2d:%0.2d,%0.3d", h, m, s, ms)
|
||||
}
|
||||
|
||||
return transcript.sentences(maxLength: maxLength).compactMap { (sentence: AttributedString) -> (
|
||||
CMTimeRange,
|
||||
String)? in
|
||||
guard let timeRange = sentence.audioTimeRange else { return nil }
|
||||
return (timeRange, String(sentence.characters))
|
||||
}.enumerated().map { index, run in
|
||||
let (timeRange, text) = run
|
||||
return """
|
||||
|
||||
\(index + 1)
|
||||
\(format(timeRange.start.seconds)) --> \(format(timeRange.end.seconds))
|
||||
\(text.trimmingCharacters(in: .whitespacesAndNewlines))
|
||||
|
||||
"""
|
||||
}.joined().trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import Speech
|
||||
import Swabble
|
||||
|
||||
@MainActor
|
||||
@available(macOS 26.0, *)
|
||||
struct TranscribeCommand: ParsableCommand {
|
||||
@Argument(help: "Path to audio/video file") var inputFile: String = ""
|
||||
@Option(name: .long("locale"), help: "Locale identifier", parsing: .singleValue) var locale: String = Locale.current
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import Foundation
|
||||
import SwabbleKit
|
||||
import Testing
|
||||
import XCTest
|
||||
|
||||
@Suite struct WakeWordGateTests {
|
||||
@Test func matchRequiresGapAfterTrigger() {
|
||||
final class WakeWordGateTests: XCTestCase {
|
||||
func testMatchRequiresGapAfterTrigger() {
|
||||
let transcript = "hey clawd do thing"
|
||||
let segments = makeSegments(
|
||||
transcript: transcript,
|
||||
@@ -14,10 +14,10 @@ import Testing
|
||||
("thing", 0.5, 0.1),
|
||||
])
|
||||
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
|
||||
#expect(WakeWordGate.match(transcript: transcript, segments: segments, config: config) == nil)
|
||||
XCTAssertNil(WakeWordGate.match(transcript: transcript, segments: segments, config: config))
|
||||
}
|
||||
|
||||
@Test func matchAllowsGapAndExtractsCommand() {
|
||||
func testMatchAllowsGapAndExtractsCommand() {
|
||||
let transcript = "hey clawd do thing"
|
||||
let segments = makeSegments(
|
||||
transcript: transcript,
|
||||
@@ -29,10 +29,10 @@ import Testing
|
||||
])
|
||||
let config = WakeWordGateConfig(triggers: ["clawd"], minPostTriggerGap: 0.3)
|
||||
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
|
||||
#expect(match?.command == "do thing")
|
||||
XCTAssertEqual(match?.command, "do thing")
|
||||
}
|
||||
|
||||
@Test func matchHandlesMultiWordTriggers() {
|
||||
func testMatchHandlesMultiWordTriggers() {
|
||||
let transcript = "hey clawd do it"
|
||||
let segments = makeSegments(
|
||||
transcript: transcript,
|
||||
@@ -44,10 +44,10 @@ import Testing
|
||||
])
|
||||
let config = WakeWordGateConfig(triggers: ["hey clawd"], minPostTriggerGap: 0.3)
|
||||
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
|
||||
#expect(match?.command == "do it")
|
||||
XCTAssertEqual(match?.command, "do it")
|
||||
}
|
||||
|
||||
@Test func matchPrefersMostSpecificTriggerWhenOverlapping() {
|
||||
func testMatchPrefersMostSpecificTriggerWhenOverlapping() {
|
||||
let transcript = "hey clawd do it"
|
||||
let segments = makeSegments(
|
||||
transcript: transcript,
|
||||
@@ -59,10 +59,10 @@ import Testing
|
||||
])
|
||||
let config = WakeWordGateConfig(triggers: ["clawd", "hey clawd"], minPostTriggerGap: 0.3)
|
||||
let match = WakeWordGate.match(transcript: transcript, segments: segments, config: config)
|
||||
#expect(match?.trigger == "hey clawd")
|
||||
XCTAssertEqual(match?.trigger, "hey clawd")
|
||||
}
|
||||
|
||||
@Test func commandTextHandlesForeignRangeIndices() {
|
||||
func testCommandTextHandlesForeignRangeIndices() {
|
||||
let transcript = "hey clawd do thing"
|
||||
let other = "do thing"
|
||||
let foreignRange = other.range(of: "do")
|
||||
@@ -78,7 +78,7 @@ import Testing
|
||||
segments: segments,
|
||||
triggerEndTime: 0.3)
|
||||
|
||||
#expect(command == "do thing")
|
||||
XCTAssertEqual(command, "do thing")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,23 +1,22 @@
|
||||
import Foundation
|
||||
import Testing
|
||||
@testable import Swabble
|
||||
import XCTest
|
||||
|
||||
@Test
|
||||
func configRoundTrip() throws {
|
||||
var cfg = SwabbleConfig()
|
||||
cfg.wake.word = "robot"
|
||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json")
|
||||
defer { try? FileManager.default.removeItem(at: url) }
|
||||
final class ConfigTests: XCTestCase {
|
||||
func testConfigRoundTrip() throws {
|
||||
var cfg = SwabbleConfig()
|
||||
cfg.wake.word = "robot"
|
||||
let url = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".json")
|
||||
defer { try? FileManager.default.removeItem(at: url) }
|
||||
|
||||
try ConfigLoader.save(cfg, at: url)
|
||||
let loaded = try ConfigLoader.load(at: url)
|
||||
#expect(loaded.wake.word == "robot")
|
||||
#expect(loaded.hook.prefix.contains("Voice swabble"))
|
||||
}
|
||||
try ConfigLoader.save(cfg, at: url)
|
||||
let loaded = try ConfigLoader.load(at: url)
|
||||
XCTAssertEqual(loaded.wake.word, "robot")
|
||||
XCTAssertTrue(loaded.hook.prefix.contains("Voice swabble"))
|
||||
}
|
||||
|
||||
@Test
|
||||
func configMissingThrows() {
|
||||
#expect(throws: ConfigError.missingConfig) {
|
||||
_ = try ConfigLoader.load(at: FileManager.default.temporaryDirectory.appendingPathComponent("nope.json"))
|
||||
func testConfigMissingThrows() {
|
||||
XCTAssertThrowsError(
|
||||
try ConfigLoader.load(at: FileManager.default.temporaryDirectory.appendingPathComponent("nope.json")))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
{
|
||||
"originHash" : "77c5e32a542e4c2ca3c7fff037abaa02066ea47cb2c2afc17927eda5a56aa5c0",
|
||||
"originHash" : "45e1ade868f67cf9cac4811c3b8c8b7dab7cef3f932ddebac6e292fdf9d6973c",
|
||||
"pins" : [
|
||||
{
|
||||
"identity" : "axorcist",
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/AXorcist.git",
|
||||
"state" : {
|
||||
"revision" : "c75d06f7f93e264a9786edc2b78c04973061cb2f",
|
||||
"version" : "0.1.0"
|
||||
"revision" : "289d403c53ce26cdd2fcb2bfb8672cd43adc2b43",
|
||||
"version" : "0.1.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -15,8 +15,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/Commander.git",
|
||||
"state" : {
|
||||
"revision" : "9e349575c8e3c6745e81fe19e5bb5efa01b078ce",
|
||||
"version" : "0.2.1"
|
||||
"revision" : "ae2ce746b386ff94b26648cfe5625cfa8d02639b",
|
||||
"version" : "0.2.2"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -24,8 +24,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/ElevenLabsKit",
|
||||
"state" : {
|
||||
"revision" : "c8679fbd37416a8780fe43be88a497ff16209e2d",
|
||||
"version" : "0.1.0"
|
||||
"revision" : "0f1e4c039bd0e22b03c0cb7f43c00c1865858f0b",
|
||||
"version" : "0.1.1"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -42,8 +42,8 @@
|
||||
"kind" : "remoteSourceControl",
|
||||
"location" : "https://github.com/steipete/Peekaboo.git",
|
||||
"state" : {
|
||||
"branch" : "main",
|
||||
"revision" : "461bc2e1ae4bfd6757eb003e528e8e26d55e06e9"
|
||||
"revision" : "bb57c83935ebc27aae69a23042a9f9fe6ca8e404",
|
||||
"version" : "3.0.0-beta4"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -19,7 +19,7 @@ let package = Package(
|
||||
.package(url: "https://github.com/swiftlang/swift-subprocess.git", from: "0.4.0"),
|
||||
.package(url: "https://github.com/apple/swift-log.git", from: "1.10.1"),
|
||||
.package(url: "https://github.com/sparkle-project/Sparkle", from: "2.9.0"),
|
||||
.package(url: "https://github.com/steipete/Peekaboo.git", branch: "main"),
|
||||
.package(url: "https://github.com/steipete/Peekaboo.git", exact: "3.0.0-beta4"),
|
||||
.package(path: "../shared/OpenClawKit"),
|
||||
.package(path: "../../Swabble"),
|
||||
],
|
||||
|
||||
@@ -14,7 +14,7 @@ let package = Package(
|
||||
.library(name: "OpenClawChatUI", targets: ["OpenClawChatUI"]),
|
||||
],
|
||||
dependencies: [
|
||||
.package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.0"),
|
||||
.package(url: "https://github.com/steipete/ElevenLabsKit", exact: "0.1.1"),
|
||||
.package(url: "https://github.com/gonzalezreal/textual", exact: "0.3.1"),
|
||||
],
|
||||
targets: [
|
||||
|
||||
@@ -141,6 +141,7 @@ private final class FakeGatewayWebSocketTask: WebSocketTasking, @unchecked Senda
|
||||
"maxBufferedBytes": 1,
|
||||
"tickIntervalMs": 30_000,
|
||||
],
|
||||
"auth": [:],
|
||||
]
|
||||
if let auth {
|
||||
payload["auth"] = auth
|
||||
|
||||
@@ -12,6 +12,25 @@ private struct TalkConfigContractFixture: Decodable {
|
||||
let payloadValid: Bool
|
||||
let expectedSelection: ExpectedSelection?
|
||||
let talk: [String: AnyCodable]
|
||||
|
||||
var gatewayResponseTalk: [String: AnyCodable] {
|
||||
guard let expectedSelection else { return self.talk }
|
||||
var config: [String: AnyCodable] = [:]
|
||||
if let voiceId = expectedSelection.voiceId {
|
||||
config["voiceId"] = AnyCodable(voiceId)
|
||||
}
|
||||
if let apiKey = expectedSelection.apiKey {
|
||||
config["apiKey"] = AnyCodable(apiKey)
|
||||
}
|
||||
var response = self.talk
|
||||
response["provider"] = AnyCodable(expectedSelection.provider)
|
||||
response["providers"] = AnyCodable([expectedSelection.provider: config])
|
||||
response["resolved"] = AnyCodable([
|
||||
"provider": AnyCodable(expectedSelection.provider),
|
||||
"config": AnyCodable(config),
|
||||
])
|
||||
return response
|
||||
}
|
||||
}
|
||||
|
||||
struct ExpectedSelection: Decodable {
|
||||
@@ -53,7 +72,7 @@ struct TalkConfigContractTests {
|
||||
@Test func selectionFixtures() throws {
|
||||
for fixture in try TalkConfigContractFixtureLoader.load().selectionCases {
|
||||
let selection = TalkConfigParsing.selectProviderConfig(
|
||||
fixture.talk,
|
||||
fixture.gatewayResponseTalk,
|
||||
defaultProvider: fixture.defaultProvider)
|
||||
if let expected = fixture.expectedSelection {
|
||||
#expect(selection != nil)
|
||||
|
||||
@@ -84,7 +84,7 @@
|
||||
"payloadValid": true,
|
||||
"expectedSelection": {
|
||||
"provider": "elevenlabs",
|
||||
"normalizedPayload": false,
|
||||
"normalizedPayload": true,
|
||||
"voiceId": "voice-legacy",
|
||||
"apiKey": "xxxxx"
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user