feat: add android codex remote control actions
This commit is contained in:
@@ -544,6 +544,18 @@ public class BossApiClient {
|
||||
return updateDevice(deviceId, payload);
|
||||
}
|
||||
|
||||
public ApiResponse queueCodexRemoteControl(String deviceId, String action, String reason) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("action", action);
|
||||
payload.put("confirmed", true);
|
||||
payload.put("reason", reason == null ? "" : reason);
|
||||
return requestWithRestore(
|
||||
"POST",
|
||||
"/api/v1/devices/" + encode(deviceId) + "/codex-remote-control",
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
||||
public ApiResponse getDeviceSkills(String deviceId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/devices/" + encode(deviceId) + "/skills", null);
|
||||
}
|
||||
|
||||
@@ -270,6 +270,30 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
null,
|
||||
v -> showPreferredExecutionModeDialog(device)
|
||||
));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"Codex 远程控制",
|
||||
"默认走 Codex Computer Use;失效时回退 boss-agent 本机控制",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"启动远控",
|
||||
"拉起本机 Codex Remote Control 守护进程",
|
||||
"需在线设备",
|
||||
null,
|
||||
v -> showCodexRemoteControlConfirmDialog("start")
|
||||
));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"停止远控",
|
||||
"停止本机 Codex Remote Control 守护进程",
|
||||
"需在线设备",
|
||||
null,
|
||||
v -> showCodexRemoteControlConfirmDialog("stop")
|
||||
));
|
||||
if (primaryPolicy != null) {
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
@@ -367,6 +391,19 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showCodexRemoteControlConfirmDialog(String action) {
|
||||
String normalizedAction = "stop".equals(action) ? "stop" : "start";
|
||||
boolean startAction = "start".equals(normalizedAction);
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle(startAction ? "启动 Codex 远控" : "停止 Codex 远控")
|
||||
.setMessage("该操作会由这台电脑的 boss-agent 本机执行,并进入权限审计。")
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton(startAction ? "确认启动" : "确认停止", (dialog, which) ->
|
||||
queueCodexRemoteControl(normalizedAction)
|
||||
)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void openEditDialog() {
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
@@ -382,6 +419,34 @@ public class DeviceDetailActivity extends BossScreenActivity {
|
||||
});
|
||||
}
|
||||
|
||||
private void queueCodexRemoteControl(String action) {
|
||||
if (deviceId == null || deviceId.trim().isEmpty()) {
|
||||
showMessage("缺少设备 ID");
|
||||
return;
|
||||
}
|
||||
boolean startAction = "start".equals(action);
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.queueCodexRemoteControl(
|
||||
deviceId,
|
||||
action,
|
||||
startAction ? "APP 设备详情页确认启动 Codex 远控" : "APP 设备详情页确认停止 Codex 远控"
|
||||
);
|
||||
if (!response.ok()) throw new IllegalStateException(response.message());
|
||||
runOnUiThread(() -> {
|
||||
showMessage(startAction ? "已提交启动远控" : "已提交停止远控");
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage((startAction ? "启动远控失败:" : "停止远控失败:") + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void savePreferredExecutionMode(String preferredExecutionMode) {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
|
||||
@@ -58,6 +58,23 @@ public class BossApiClientDeviceModeTest {
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void queueCodexRemoteControlWritesConfirmedActionBody() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/devices/device-1/codex-remote-control")
|
||||
);
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
apiClient.queueCodexRemoteControl("device-1", "start", "APP 设备详情页确认启动");
|
||||
|
||||
assertEquals("/api/v1/devices/device-1/codex-remote-control", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"action\":\"start\",\"confirmed\":true,\"reason\":\"APP 设备详情页确认启动\"}",
|
||||
connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
private static final class RecordingBossApiClient extends BossApiClient {
|
||||
private final RecordingConnection connection;
|
||||
private String lastPath = "";
|
||||
|
||||
@@ -76,6 +76,31 @@ public class DeviceDetailActivityTest {
|
||||
assertTrue(viewTreeContainsText(content, "未连接"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderDeviceShowsCodexRemoteControlActions() throws Exception {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestDeviceDetailActivity.class,
|
||||
new Intent()
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, "device-1")
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, "Mac Studio")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderDevice",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, buildDevicePayload())
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "Codex 远程控制"));
|
||||
assertTrue(viewTreeContainsText(content, "启动远控"));
|
||||
assertTrue(viewTreeContainsText(content, "停止远控"));
|
||||
assertTrue(viewTreeContainsText(content, "默认走 Codex Computer Use"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void renderDeviceShowsCodexAppServerProtocolAndCollaborationSummary() throws Exception {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
@@ -228,6 +253,43 @@ public class DeviceDetailActivityTest {
|
||||
assertEquals("allow_always", apiClient.lastPayload.optString("conflictDecision"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void codexRemoteControlConfirmDialogQueuesStartAction() throws Exception {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestDeviceDetailActivity.class,
|
||||
new Intent()
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, "device-1")
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, "Mac Studio")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(
|
||||
activity.getSharedPreferences("test-boss-api", Context.MODE_PRIVATE),
|
||||
"https://boss.hyzq.net"
|
||||
);
|
||||
ReflectionHelpers.setField(activity, "apiClient", apiClient);
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"showCodexRemoteControlConfirmDialog",
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "start")
|
||||
);
|
||||
|
||||
android.app.Dialog latestDialog = ShadowDialog.getLatestDialog();
|
||||
assertTrue(latestDialog instanceof AlertDialog);
|
||||
AlertDialog dialog = (AlertDialog) latestDialog;
|
||||
dialog.getButton(AlertDialog.BUTTON_POSITIVE).performClick();
|
||||
Shadows.shadowOf(activity.getMainLooper()).idle();
|
||||
|
||||
assertEquals(1, apiClient.queueCodexRemoteControlCalls);
|
||||
assertEquals("device-1", apiClient.lastCodexRemoteControlDeviceId);
|
||||
assertEquals("start", apiClient.lastCodexRemoteControlAction);
|
||||
assertEquals("APP 设备详情页确认启动 Codex 远控", apiClient.lastCodexRemoteControlReason);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void matchingDevicesUpdatedEventTriggersReload() throws Exception {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
@@ -462,6 +524,10 @@ public class DeviceDetailActivityTest {
|
||||
private int updateDeviceCalls;
|
||||
private String lastDeviceId;
|
||||
private JSONObject lastPayload;
|
||||
private int queueCodexRemoteControlCalls;
|
||||
private String lastCodexRemoteControlDeviceId;
|
||||
private String lastCodexRemoteControlAction;
|
||||
private String lastCodexRemoteControlReason;
|
||||
|
||||
RecordingBossApiClient(android.content.SharedPreferences prefs, String baseUrl) {
|
||||
super(prefs, baseUrl);
|
||||
@@ -478,6 +544,19 @@ public class DeviceDetailActivityTest {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ApiResponse queueCodexRemoteControl(String deviceId, String action, String reason) {
|
||||
queueCodexRemoteControlCalls += 1;
|
||||
lastCodexRemoteControlDeviceId = deviceId;
|
||||
lastCodexRemoteControlAction = action;
|
||||
lastCodexRemoteControlReason = reason;
|
||||
try {
|
||||
return new ApiResponse(200, new JSONObject().put("ok", true));
|
||||
} catch (Exception error) {
|
||||
throw new RuntimeException(error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DirectExecutorService extends AbstractExecutorService {
|
||||
|
||||
Reference in New Issue
Block a user