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 {
|
||||
|
||||
@@ -170,7 +170,7 @@
|
||||
- 第二十七批另补 `streamDeltaEventSummary` 流式增量事件能力摘要:设备详情页会显示 agent delta、plan delta、reasoning delta、MCP progress、command output、terminal interaction 和 file output 等能力分组;该字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。
|
||||
- 当前任务执行态也已补 `executionProgress.streamEvents`:App Server runner 会把 agent / plan / reasoning / MCP / command / terminal / file 的流式 delta 归一成计数,Android 进度卡展示“流式增量”,不保存或渲染原始 delta、命令输出、终端输入、推理正文或文件输出。
|
||||
- 当前 App Server 任务取消已从“服务端标记”升级为“真实 turn 中断”:`POST /api/v1/master-agent/tasks/[taskId]/cancel` 仍负责把任务置为 `canceled`,新增 `GET /api/v1/master-agent/tasks/[taskId]/control-state` 供设备端轮询;`local-agent` 在 App Server turn 启动后会按取消状态调用 `turn/interrupt`,并把 `interrupted` 作为干净取消处理,避免取消后长任务继续跑或被误写成失败日志。
|
||||
- 当前本机 `codex remote-control` 已确认为官方 App Server daemon 远控入口;boss-agent 本机状态页会展示 `Codex Remote Control` 托管摘要和 `codex remote-control start --json` 默认启动命令,但状态页刷新不会自动启动 daemon。云端已补 `POST /api/v1/devices/[deviceId]/codex-remote-control`,要求显式 `confirmed=true`、设备在线和 `computer.control` 权限,成功后排 `device_maintenance / codex_remote_control` 任务给目标 local-agent 本机执行,并写入 `task.authorized / task.denied` 审计;独立 PC 后台已在设备表接入启动 / 停止按钮。APP 侧下一步复用该 API 做原生确认卡片。
|
||||
- 当前本机 `codex remote-control` 已确认为官方 App Server daemon 远控入口;boss-agent 本机状态页会展示 `Codex Remote Control` 托管摘要和 `codex remote-control start --json` 默认启动命令,但状态页刷新不会自动启动 daemon。云端已补 `POST /api/v1/devices/[deviceId]/codex-remote-control`,要求显式 `confirmed=true`、设备在线和 `computer.control` 权限,成功后排 `device_maintenance / codex_remote_control` 任务给目标 local-agent 本机执行,并写入 `task.authorized / task.denied` 审计;独立 PC 后台已在设备表接入启动 / 停止按钮,Android APP 设备详情页也已接入启动 / 停止远控原生确认入口。
|
||||
- 当前已补 Codex App Server 受控线程回滚:`POST /api/v1/projects/[projectId]/thread-rollback` 会创建 `intentCategory=thread_rollback` 任务,`local-agent` 调用 `thread/rollback` 回滚目标线程最近 N 轮;该链路不启动新 turn,不把 thread/turn/items 原文写回 APP,只提示“线程历史已回滚”,且不会自动还原本地文件变更。
|
||||
- 当前已补 Codex App Server 受控线程压缩:`POST /api/v1/projects/[projectId]/thread-compact` 会创建 `intentCategory=thread_compact` 任务,`local-agent` 调用 `thread/compact/start` 发起目标线程上下文压缩;该链路不启动普通 turn,不把 contextCompaction item 原文写回 APP,只提示“上下文压缩已发起”。
|
||||
- 当前已补 Codex App Server 受控线程归档 / 恢复:`POST /api/v1/projects/[projectId]/thread-archive` 会创建 `intentCategory=thread_archive|thread_unarchive` 任务,`local-agent` 直接调用 `thread/archive` 或 `thread/unarchive`;该链路不启动普通 turn,不把 thread 原始字段写回 APP,只提示“线程已归档/已恢复”。
|
||||
|
||||
@@ -157,7 +157,7 @@
|
||||
- 这两个入口只在本机 agent 上执行 `codex remote-control start|stop --json`,返回和日志都会清洗敏感字段;状态页刷新不会自动调用
|
||||
- 云端已新增 `POST /api/v1/devices/[deviceId]/codex-remote-control` 作为受控排队入口,参数为 `action=start|stop`、`confirmed=true` 和可选 `reason`
|
||||
- 云端入口要求登录态、目标设备在线、当前账号具备该设备 `computer.control` 权限;成功会排入 `device_maintenance / codex_remote_control` 任务,由目标设备 local-agent 认领执行,并写入 `task.authorized` 审计;未授权或离线会写入 `task.denied`
|
||||
- 独立 PC 后台已在 `全局设备 / 电脑与 Codex 接入` 表格中接入 `启动远控 / 停止远控` 操作,当前使用浏览器确认框做二次确认,后续 APP 侧可复用同一 API 做原生确认卡片
|
||||
- 独立 PC 后台已在 `全局设备 / 电脑与 Codex 接入` 表格中接入 `启动远控 / 停止远控` 操作,当前使用浏览器确认框做二次确认;Android APP 设备详情页已复用同一 API 做 `启动远控 / 停止远控` 原生确认入口
|
||||
- 当前 `browser_control / desktop_control` 任务已经可以被 `local-agent/server.mjs` 识别并分流;当本机配置了对应 runtime 命令时,会通过 JSON stdin/stdout 协议委托给外部进程执行,否则返回明确 runtime disabled 错误,不再回退占位成功结果
|
||||
- 当前 `browser_control / desktop_control` 的完成回写已贯通 `targetUrl / targetApp -> RemoteRuntimeAdapter -> /api/v1/master-agent/tasks/[taskId]/complete -> boss-state.json`,服务端写入 `control_summary` 消息时会保留 `controlTarget`,Android 会话页可直接渲染“目标:URL/应用名”
|
||||
- 相关配置项:
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
|
||||
- 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准
|
||||
- 当前 Codex server 调研结论已记录在 `docs/architecture/codex_server_progress_card_cn.md`:长期优先方向更新为 `Codex App Server / Remote Control -> Inter-Thread Broker -> CodexMcpBackendAdapter -> codex exec resume` 的分层 provider 策略;当前 boss-agent 默认打开 `Codex App Server` runner 作为 Codex 绑定入口,Boss 仍保留 `codex exec resume` 兜底,并继续用 `execution_progress` 结构化进度卡作为 APP 可见执行态。本机 `codex-cli 0.136.0-alpha.2` 协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.136.0-alpha.2/`,确认 151 个 method,并支持 WebSocket auth、`thread/inject_items`、`turn/steer`、`turn/interrupt`、`thread/archive`、`thread/unarchive`、`thread/fork`、`thread/compact/start`、`thread/rollback`、`thread/name/set`、`thread/metadata/update`、`thread/shellCommand`、`thread/unsubscribe`、`thread/realtime/*`、`thread/started|closed|archived|unarchived|name/updated`、`process/outputDelta|exited`、`rawResponseItem/completed`、`item/agentMessage/delta`、`item/plan/delta`、`item/reasoning/*Delta`、`item/mcpToolCall/progress`、`command/exec/outputDelta`、`item/commandExecution/terminalInteraction`、`item/fileChange/outputDelta`、`thread/goal/*`、`thread/settings/updated`、`thread/compacted`、`ThreadItem.contextCompaction`、`account/*`、`model/verification`、`configWarning`、`deprecationNotice`、`command/exec`、`command/exec/write`、`command/exec/resize`、`command/exec/terminate`、`model/list`、`skills/changed`、`skills/extraRoots/set`、`hooks/list`、`plugin/installed`、`plugin/install`、`plugin/uninstall`、`plugin/read`、`plugin/skill/read`、`plugin/share/*`、`config/value/write`、`config/batchWrite`、`config/mcpServer/reload`、`skills/config/write`、`fs/*`、`externalAgentConfig/import`、`marketplace/add|remove|upgrade`、`experimentalFeature/enablement/set`、`review/start`、`windowsSandbox/readiness|setupStart`、`fuzzyFileSearch/session*`、`mcpServer/oauth*`、`mcpServer/resource/read`、`mcpServer/tool/call`、`mcpServer/elicitation/request`、`item/tool/requestUserInput` 和 `thread/approveGuardianDeniedAction`
|
||||
- 当前本机 `codex remote-control` 已确认为官方 App Server daemon 远控管理入口;boss-agent 本机状态页会展示 `Codex Remote Control` 托管摘要和启动命令,默认只观测不启动。本批已新增云端受控入口 `POST /api/v1/devices/[deviceId]/codex-remote-control`,要求 `action=start|stop`、`confirmed=true`、设备在线和当前账号具备该设备 `computer.control` 权限;成功会排入 `device_maintenance / codex_remote_control` 任务,由目标设备 local-agent 本机执行 `codex remote-control start|stop --json` 并回写任务小结,同时写入 `task.authorized` 审计;未授权或设备离线写入 `task.denied`。独立 PC 后台的 `全局设备 / 电脑与 Codex 接入` 表格已接入 `启动远控 / 停止远控` 操作。
|
||||
- 当前本机 `codex remote-control` 已确认为官方 App Server daemon 远控管理入口;boss-agent 本机状态页会展示 `Codex Remote Control` 托管摘要和启动命令,默认只观测不启动。本批已新增云端受控入口 `POST /api/v1/devices/[deviceId]/codex-remote-control`,要求 `action=start|stop`、`confirmed=true`、设备在线和当前账号具备该设备 `computer.control` 权限;成功会排入 `device_maintenance / codex_remote_control` 任务,由目标设备 local-agent 本机执行 `codex remote-control start|stop --json` 并回写任务小结,同时写入 `task.authorized` 审计;未授权或设备离线写入 `task.denied`。独立 PC 后台的 `全局设备 / 电脑与 Codex 接入` 表格已接入 `启动远控 / 停止远控` 操作,Android APP 设备详情页已同步接入原生二次确认入口。
|
||||
- 2026-06-04 重新生成 0.136.0-alpha.2 协议快照后,manifest 识别 151 个 method,并新增 `itemTypes` 支持矩阵。当前本机 schema 已确认 `app/list`、`app/list/updated`、`configRequirements/read`、`mcpServerStatus/list` 和 `ThreadItem.contextCompaction`;官方 App Server 文档列出的 `collaborationMode/list`、`thread/turns/list`、`ThreadItem.collabToolCall` 在本机生成 schema 中仍未声明,所以 Boss 只把它们作为运行时兼容/官方文档跟进项,不把“线程间对话”写成无监管 P2P。
|
||||
- 当前 App Server 能力发现已新增治理摘要:local-agent 会在 heartbeat discovery 中拉取 `experimentalFeature/list / collaborationMode/list / permissionProfile/list / mcpServerStatus/list`,并把实验特性、协作模式、权限 Profile 与 MCP 服务状态写入设备 `codexAppServer.metadata`;Web 与原生 Android 设备详情页都会显示“治理”摘要。该链路只保留安全摘要,不保存 MCP resource URI、permission profile 文件规则、本地路径、token 或工具参数。
|
||||
- 当前 App Server 能力发现已新增账号与配置摘要:local-agent 会在 heartbeat discovery 中拉取 `account/read / account/rateLimits/read / config/read / configRequirements/read / externalAgentConfig/detect`,并把账号登录方式、套餐、额度使用率、App 配置计数、托管要求数量和外部 Agent 迁移候选数量写入设备 `codexAppServer.metadata`;Web 设备详情页会显示“账号 / 配置”摘要,原生 Android 设备详情页会显示“账号”摘要。该链路只读不写,不保存账号邮箱、完整 config、API key、本地路径或迁移描述。
|
||||
|
||||
Reference in New Issue
Block a user