mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-14 03:26:08 +00:00
fix(android): support gateway protocol v3 (#106205)
This commit is contained in:
committed by
GitHub
parent
c43c960aad
commit
16b1bd2032
@@ -2,6 +2,7 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
Allows Android to connect to Gateway protocol v3 or v4 while preserving the current v4 chat stream behavior.
|
||||
Creates or adopts Android's existing per-device chat session before loading connected history, preserving prior conversations while isolating each device. Thanks @snowzlmbot.
|
||||
Adds polished Installed/Browse skill management on Android with readiness filters, enable/disable controls, and readable Gateway-enforced ClawHub risk review. Thanks @snowzlmbot.
|
||||
Marks already installed ClawHub slugs in Browse and prevents fresh-install retries from overwriting or failing against existing skill directories. (#105741)
|
||||
|
||||
@@ -6,7 +6,7 @@ import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
const val GATEWAY_PROTOCOL_VERSION = 4
|
||||
const val GATEWAY_MIN_PROTOCOL_VERSION = 4
|
||||
const val GATEWAY_MIN_PROTOCOL_VERSION = 3
|
||||
|
||||
@Serializable
|
||||
data class GatewayProtocolError(
|
||||
|
||||
@@ -35,7 +35,7 @@ class ChatControllerStreamReplayTest {
|
||||
|
||||
@Test
|
||||
@OptIn(ExperimentalCoroutinesApi::class)
|
||||
fun cleanRunStreamsThenConvergesToHistoryWithoutDuplicates() =
|
||||
fun cleanRunStreamsV3AndV4DeltasThenConvergesToHistoryWithoutDuplicates() =
|
||||
runTest {
|
||||
val gateway = ScriptedGateway(json)
|
||||
gateway.respondChatSend(status = "started")
|
||||
@@ -50,7 +50,8 @@ class ChatControllerStreamReplayTest {
|
||||
.single { it.role == "user" }
|
||||
.id
|
||||
|
||||
controller.handleGatewayEvent("chat", chatDeltaPayload("main", runId, 1, "Str", "Str"))
|
||||
// V3 deltas carry the accumulated message without v4's deltaText field.
|
||||
controller.handleGatewayEvent("chat", chatDeltaPayload("main", runId, 1, null, "Str"))
|
||||
assertEquals("Str", controller.streamingAssistantText.value)
|
||||
controller.handleGatewayEvent(
|
||||
"chat",
|
||||
|
||||
@@ -134,12 +134,12 @@ internal fun historyResponse(
|
||||
)
|
||||
}.toString()
|
||||
|
||||
/** Protocol-valid gateway delta carrying both the incremental chunk and accumulated snapshot. */
|
||||
/** Gateway delta carrying the accumulated snapshot plus the v4 incremental chunk when present. */
|
||||
internal fun chatDeltaPayload(
|
||||
sessionKey: String,
|
||||
runId: String,
|
||||
seq: Int,
|
||||
deltaText: String,
|
||||
deltaText: String?,
|
||||
accumulatedText: String,
|
||||
): String =
|
||||
buildJsonObject {
|
||||
@@ -147,7 +147,7 @@ internal fun chatDeltaPayload(
|
||||
put("runId", JsonPrimitive(runId))
|
||||
put("seq", JsonPrimitive(seq))
|
||||
put("state", JsonPrimitive("delta"))
|
||||
put("deltaText", JsonPrimitive(deltaText))
|
||||
if (deltaText != null) put("deltaText", JsonPrimitive(deltaText))
|
||||
put(
|
||||
"message",
|
||||
buildJsonObject {
|
||||
|
||||
@@ -19,17 +19,22 @@ import {
|
||||
* and connect payloads aligned with the package source of truth.
|
||||
*/
|
||||
|
||||
/** Min/max protocol pair expected in every native client surface. */
|
||||
/** Min/max protocol pair expected in a native client surface. */
|
||||
type ProtocolLevels = {
|
||||
min: number;
|
||||
max: number;
|
||||
};
|
||||
|
||||
const expectedLevels: ProtocolLevels = {
|
||||
const expectedClientLevels: ProtocolLevels = {
|
||||
min: MIN_CLIENT_PROTOCOL_VERSION,
|
||||
max: PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
const expectedAndroidLevels: ProtocolLevels = {
|
||||
min: MIN_NODE_PROTOCOL_VERSION,
|
||||
max: PROTOCOL_VERSION,
|
||||
};
|
||||
|
||||
/** Reads a repo-relative source file used by a native protocol guard. */
|
||||
async function readRepoFile(relativePath: string): Promise<string> {
|
||||
return fs.readFile(path.join(process.cwd(), relativePath), "utf8");
|
||||
@@ -52,12 +57,16 @@ function extractInteger(
|
||||
}
|
||||
|
||||
/** Compares native min/max values to the TypeScript version constants. */
|
||||
function assertLevelsMatch(relativePath: string, actual: ProtocolLevels): void {
|
||||
if (actual.min === expectedLevels.min && actual.max === expectedLevels.max) {
|
||||
function assertLevelsMatch(
|
||||
relativePath: string,
|
||||
actual: ProtocolLevels,
|
||||
expected: ProtocolLevels = expectedClientLevels,
|
||||
): void {
|
||||
if (actual.min === expected.min && actual.max === expected.max) {
|
||||
return;
|
||||
}
|
||||
throw new Error(
|
||||
`${relativePath}: Gateway protocol level mismatch: expected min=${expectedLevels.min} max=${expectedLevels.max} from packages/gateway-protocol/src/version.ts, got min=${actual.min} max=${actual.max}. Update the native constants/generated artifacts before shipping.`,
|
||||
`${relativePath}: Gateway protocol level mismatch: expected min=${expected.min} max=${expected.max} from packages/gateway-protocol/src/version.ts, got min=${actual.min} max=${actual.max}. Update the native constants/generated artifacts before shipping.`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -134,20 +143,24 @@ describe("native Gateway protocol levels", () => {
|
||||
|
||||
const androidPath = "apps/android/app/src/main/java/ai/openclaw/app/gateway/GatewayProtocol.kt";
|
||||
const android = await readRepoFile(androidPath);
|
||||
assertLevelsMatch(androidPath, {
|
||||
min: extractInteger(
|
||||
android,
|
||||
/const val GATEWAY_MIN_PROTOCOL_VERSION = (\d+)/,
|
||||
androidPath,
|
||||
"GATEWAY_MIN_PROTOCOL_VERSION",
|
||||
),
|
||||
max: extractInteger(
|
||||
android,
|
||||
/const val GATEWAY_PROTOCOL_VERSION = (\d+)/,
|
||||
androidPath,
|
||||
"GATEWAY_PROTOCOL_VERSION",
|
||||
),
|
||||
});
|
||||
assertLevelsMatch(
|
||||
androidPath,
|
||||
{
|
||||
min: extractInteger(
|
||||
android,
|
||||
/const val GATEWAY_MIN_PROTOCOL_VERSION = (\d+)/,
|
||||
androidPath,
|
||||
"GATEWAY_MIN_PROTOCOL_VERSION",
|
||||
),
|
||||
max: extractInteger(
|
||||
android,
|
||||
/const val GATEWAY_PROTOCOL_VERSION = (\d+)/,
|
||||
androidPath,
|
||||
"GATEWAY_PROTOCOL_VERSION",
|
||||
),
|
||||
},
|
||||
expectedAndroidLevels,
|
||||
);
|
||||
});
|
||||
|
||||
it("uses the min constant for native connect compatibility ranges", async () => {
|
||||
|
||||
@@ -3,7 +3,7 @@ import { promises as fs } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import {
|
||||
MIN_CLIENT_PROTOCOL_VERSION,
|
||||
MIN_NODE_PROTOCOL_VERSION,
|
||||
PROTOCOL_VERSION,
|
||||
ProtocolSchemas,
|
||||
} from "../packages/gateway-protocol/src/schema.js";
|
||||
@@ -326,7 +326,8 @@ async function generate(): Promise<void> {
|
||||
"import kotlinx.serialization.json.JsonElement",
|
||||
"",
|
||||
`const val GATEWAY_PROTOCOL_VERSION = ${PROTOCOL_VERSION}`,
|
||||
`const val GATEWAY_MIN_PROTOCOL_VERSION = ${MIN_CLIENT_PROTOCOL_VERSION}`,
|
||||
// Android consumes v3 message-only chat deltas and uses the N-1 node transport.
|
||||
`const val GATEWAY_MIN_PROTOCOL_VERSION = ${MIN_NODE_PROTOCOL_VERSION}`,
|
||||
"",
|
||||
...emitWireModels().flatMap((model) => [model, ""]),
|
||||
emitGatewayCatalogEnum("GatewayMethod", gatewayMethods),
|
||||
|
||||
Reference in New Issue
Block a user