Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,10 @@ adb install -r app/build/outputs/apk/debug/app-debug.apk

## 连接异常排查

若会话被其他 Codex 客户端占用,Android 会以 external-writer 模式查看并自动更新原会话历史。可选的 [Desktop companion](companion/README.md) 支持将文字交给持有原会话的电脑客户端,避免另建对话;当前为实验功能,需要独立部署伴随代理。

同步时直接更新最新回合的完整处理过程;历史回合的详情按需加载并保留缓存。同一服务端摘要不会因为详情中多了中间回复而反复重置加载状态。

生产版本通过 Logcat 的 `CodexConnection` 标签记录连接失败、非主动断开、请求超时和会话恢复失败,正常连接、消息收发和主动取消不记录

```bash
Expand Down
22 changes: 22 additions & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
import org.gradle.api.file.ConfigurableFileCollection
import org.gradle.api.tasks.Classpath
import org.gradle.api.tasks.testing.Test
import org.gradle.process.CommandLineArgumentProvider

plugins {
alias(libs.plugins.android.application)
alias(libs.plugins.kotlin.compose)
Expand All @@ -6,6 +11,21 @@ plugins {
alias(libs.plugins.room)
}

val mockitoAgent = configurations.create("mockitoAgent")

abstract class MockitoAgentProvider : CommandLineArgumentProvider {
@get:Classpath
abstract val agentJar: ConfigurableFileCollection

override fun asArguments(): List<String> = listOf("-javaagent:${agentJar.asPath}")
}

tasks.withType<Test>().configureEach {
jvmArgumentProviders.add(objects.newInstance<MockitoAgentProvider>().apply {
agentJar.from(mockitoAgent)
})
}

val releaseSigningEnabled = providers.gradleProperty("releaseSigning").orNull == "true"

fun requiredSigningEnvironmentVariable(name: String): String =
Expand Down Expand Up @@ -102,6 +122,8 @@ dependencies {
debugImplementation(libs.androidx.compose.ui.tooling)

testImplementation(libs.junit)
testImplementation(libs.mockito.core)
mockitoAgent(libs.mockito.core) { isTransitive = false }
testImplementation(libs.kotlinx.coroutines.test)

ksp(libs.androidx.room.compiler)
Expand Down
34 changes: 34 additions & 0 deletions app/src/main/java/com/hebo/codex/data/remote/AppServerApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,7 @@ class AppServerApi(
StableRpcMethod.THREAD_READ,
buildJsonObject {
put("threadId", threadId)
put("includeTurns", false)
},
).jsonObject

Expand Down Expand Up @@ -213,6 +214,39 @@ class AppServerApi(
timeoutMillis = input.submissionTimeoutMillis(),
).jsonObject

// Optional codex-android companion extension, separate from Codex CLI RPCs.
suspend fun discoverDesktopControl(threadId: String): Boolean = try {
val result = connection.request(
"codexAndroid/desktop/discover",
buildJsonObject { put("threadId", threadId) },
).jsonObject
if (result.requiredString("threadId") != threadId || result.requiredInt("protocolVersion") != 1) {
throw ProtocolException("电脑伴随服务的会话或协议不匹配")
}
result.requiredBoolean("available")
} catch (error: RpcException) {
val unsupported = error.code == -32601 || (error.code == -32600 &&
error.message.startsWith("Invalid request: unknown variant `codexAndroid/desktop/discover`"))
if (!unsupported) throw error
false
}

suspend fun sendDesktopMessage(threadId: String, clientMessageId: String, text: String): JsonObject =
connection.request(
"codexAndroid/desktop/turn/start",
buildJsonObject {
put("threadId", threadId)
put("clientUserMessageId", clientMessageId)
put("text", text)
},
timeoutMillis = 120_000,
).jsonObject.also { result ->
if (result.requiredString("threadId") != threadId ||
result.requiredString("clientUserMessageId") != clientMessageId
) throw ProtocolException("电脑端的发送确认与当前消息不一致")
result.requiredObject("turn").requiredString("id")
}

suspend fun steerTurn(
threadId: String,
turnId: String,
Expand Down
Loading