feat: queue codex remote control actions
This commit is contained in:
@@ -17,6 +17,7 @@ import {
|
|||||||
fetchAdminBackups,
|
fetchAdminBackups,
|
||||||
fetchBossAdminBackoffice,
|
fetchBossAdminBackoffice,
|
||||||
postAdminAccess,
|
postAdminAccess,
|
||||||
|
postDeviceCodexRemoteControl,
|
||||||
postRiskAction,
|
postRiskAction,
|
||||||
postSkillLifecycleRequest,
|
postSkillLifecycleRequest,
|
||||||
restoreAdminBackup,
|
restoreAdminBackup,
|
||||||
@@ -455,6 +456,19 @@ async function handleRisk(record: AdminRecord, action: string) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleCodexRemoteControl(record: AdminRecord, action: "start" | "stop") {
|
||||||
|
const deviceId = text(record.id);
|
||||||
|
const deviceName = text(record.name) || deviceId;
|
||||||
|
const actionLabel = action === "start" ? "启动 Codex Remote Control" : "停止 Codex Remote Control";
|
||||||
|
if (!window.confirm(`确认对 ${deviceName} ${actionLabel}?该操作会由对应电脑的 boss-agent 本机执行。`)) return;
|
||||||
|
await runMutation(actionLabel, () =>
|
||||||
|
postDeviceCodexRemoteControl(deviceId, {
|
||||||
|
action,
|
||||||
|
reason: `${actionLabel} · enterprise-admin-web`,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
async function createSkillRequest() {
|
async function createSkillRequest() {
|
||||||
await runMutation("创建 Skill 请求", () =>
|
await runMutation("创建 Skill 请求", () =>
|
||||||
postSkillLifecycleRequest({
|
postSkillLifecycleRequest({
|
||||||
@@ -776,6 +790,18 @@ onMounted(async () => {
|
|||||||
<a-table-column title="CLI" data-index="codexCliOnline" />
|
<a-table-column title="CLI" data-index="codexCliOnline" />
|
||||||
<a-table-column title="项目数" data-index="projectCount" />
|
<a-table-column title="项目数" data-index="projectCount" />
|
||||||
<a-table-column title="风险" data-index="openRiskCount" />
|
<a-table-column title="风险" data-index="openRiskCount" />
|
||||||
|
<a-table-column title="Codex 远控">
|
||||||
|
<template #default="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button size="small" :disabled="text(record.status) !== 'online'" @click="handleCodexRemoteControl(record, 'start')">
|
||||||
|
启动远控
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" :disabled="text(record.status) !== 'online'" @click="handleCodexRemoteControl(record, 'stop')">
|
||||||
|
停止远控
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-table-column>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
<a-card title="能力分布" :bordered="false">
|
<a-card title="能力分布" :bordered="false">
|
||||||
@@ -1061,6 +1087,18 @@ onMounted(async () => {
|
|||||||
<a-table-column title="控制模式" data-index="preferredExecutionMode" />
|
<a-table-column title="控制模式" data-index="preferredExecutionMode" />
|
||||||
<a-table-column title="最近心跳" data-index="lastSeenAt" />
|
<a-table-column title="最近心跳" data-index="lastSeenAt" />
|
||||||
<a-table-column title="风险" data-index="openRiskCount" />
|
<a-table-column title="风险" data-index="openRiskCount" />
|
||||||
|
<a-table-column title="Codex 远控">
|
||||||
|
<template #default="{ record }">
|
||||||
|
<a-space>
|
||||||
|
<a-button size="small" :disabled="text(record.status) !== 'online'" @click="handleCodexRemoteControl(record, 'start')">
|
||||||
|
启动远控
|
||||||
|
</a-button>
|
||||||
|
<a-button size="small" :disabled="text(record.status) !== 'online'" @click="handleCodexRemoteControl(record, 'stop')">
|
||||||
|
停止远控
|
||||||
|
</a-button>
|
||||||
|
</a-space>
|
||||||
|
</template>
|
||||||
|
</a-table-column>
|
||||||
</a-table>
|
</a-table>
|
||||||
</a-card>
|
</a-card>
|
||||||
<a-card title="项目线程" :bordered="false">
|
<a-card title="项目线程" :bordered="false">
|
||||||
|
|||||||
@@ -133,6 +133,22 @@ export async function postSkillLifecycleRequest(payload: Record<string, unknown>
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function postDeviceCodexRemoteControl(
|
||||||
|
deviceId: string,
|
||||||
|
payload: { action: "start" | "stop"; reason?: string },
|
||||||
|
) {
|
||||||
|
return requestJson<Record<string, unknown>>(
|
||||||
|
`/api/v1/devices/${encodeURIComponent(deviceId)}/codex-remote-control`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({
|
||||||
|
...payload,
|
||||||
|
confirmed: true,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAdminBackups(): Promise<BossAdminBackupsPayload> {
|
export async function fetchAdminBackups(): Promise<BossAdminBackupsPayload> {
|
||||||
return requestJson<BossAdminBackupsPayload>("/api/v1/admin/backups");
|
return requestJson<BossAdminBackupsPayload>("/api/v1/admin/backups");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -170,7 +170,7 @@
|
|||||||
- 第二十七批另补 `streamDeltaEventSummary` 流式增量事件能力摘要:设备详情页会显示 agent delta、plan delta、reasoning delta、MCP progress、command output、terminal interaction 和 file output 等能力分组;该字段只读,不保存原始增量文本、命令输出、推理正文或文件输出。
|
- 第二十七批另补 `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、命令输出、终端输入、推理正文或文件输出。
|
- 当前任务执行态也已补 `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` 作为干净取消处理,避免取消后长任务继续跑或被误写成失败日志。
|
- 当前 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。后续真正启动、停止或重启该 daemon 必须走显式用户动作、RBAC、审批和审计链路。
|
- 当前本机 `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 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-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-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,只提示“线程已归档/已恢复”。
|
- 当前已补 Codex App Server 受控线程归档 / 恢复:`POST /api/v1/projects/[projectId]/thread-archive` 会创建 `intentCategory=thread_archive|thread_unarchive` 任务,`local-agent` 直接调用 `thread/archive` 或 `thread/unarchive`;该链路不启动普通 turn,不把 thread 原始字段写回 APP,只提示“线程已归档/已恢复”。
|
||||||
|
|||||||
@@ -154,7 +154,10 @@
|
|||||||
- 当前本机 boss-agent 还新增 Codex Remote Control 显式控制入口:
|
- 当前本机 boss-agent 还新增 Codex Remote Control 显式控制入口:
|
||||||
- `POST http://127.0.0.1:4317/api/v1/boss-agent/codex-remote-control/start`
|
- `POST http://127.0.0.1:4317/api/v1/boss-agent/codex-remote-control/start`
|
||||||
- `POST http://127.0.0.1:4317/api/v1/boss-agent/codex-remote-control/stop`
|
- `POST http://127.0.0.1:4317/api/v1/boss-agent/codex-remote-control/stop`
|
||||||
- 这两个入口只在本机 agent 上执行 `codex remote-control start|stop --json`,返回和日志都会清洗敏感字段;状态页刷新不会自动调用,后续接入 APP/后台时仍必须加显式操作、RBAC、审批和审计
|
- 这两个入口只在本机 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 做原生确认卡片
|
||||||
- 当前 `browser_control / desktop_control` 任务已经可以被 `local-agent/server.mjs` 识别并分流;当本机配置了对应 runtime 命令时,会通过 JSON stdin/stdout 协议委托给外部进程执行,否则返回明确 runtime disabled 错误,不再回退占位成功结果
|
- 当前 `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/应用名”
|
- 当前 `browser_control / desktop_control` 的完成回写已贯通 `targetUrl / targetApp -> RemoteRuntimeAdapter -> /api/v1/master-agent/tasks/[taskId]/complete -> boss-state.json`,服务端写入 `control_summary` 消息时会保留 `controlTarget`,Android 会话页可直接渲染“目标:URL/应用名”
|
||||||
- 相关配置项:
|
- 相关配置项:
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ Boss 不能直接把 App Server 原始 Thread / Turn / Item 字段写进业务
|
|||||||
- 本机 `codex app-server --help` 已可用;本机 help 当前显示 `--listen` 支持 `stdio://`、`unix://`、`unix://PATH`、`ws://IP:PORT` 和 `off`
|
- 本机 `codex app-server --help` 已可用;本机 help 当前显示 `--listen` 支持 `stdio://`、`unix://`、`unix://PATH`、`ws://IP:PORT` 和 `off`
|
||||||
- 本机 `codex app-server --help` 当前已经支持 `--ws-auth capability-token|signed-bearer-token`、`--ws-token-file`、`--ws-token-sha256`、`--ws-shared-secret-file`、issuer/audience/clock-skew 等 WebSocket 认证参数
|
- 本机 `codex app-server --help` 当前已经支持 `--ws-auth capability-token|signed-bearer-token`、`--ws-token-file`、`--ws-token-sha256`、`--ws-shared-secret-file`、issuer/audience/clock-skew 等 WebSocket 认证参数
|
||||||
- 本机 `codex remote-control --help` 已可用;它会管理“带 remote control enabled 的 App Server daemon”,当前 boss-agent 状态页已把 `codexRemoteControlEnabled / codexRemoteControlCommand / codexRemoteControlArgs` 归一成 `Codex Remote Control` 摘要,但状态页刷新不会自动执行 `start`
|
- 本机 `codex remote-control --help` 已可用;它会管理“带 remote control enabled 的 App Server daemon”,当前 boss-agent 状态页已把 `codexRemoteControlEnabled / codexRemoteControlCommand / codexRemoteControlArgs` 归一成 `Codex Remote Control` 摘要,但状态页刷新不会自动执行 `start`
|
||||||
|
- 2026-06-04 已新增云端受控启动 / 停止链路:`POST /api/v1/devices/[deviceId]/codex-remote-control` 会在登录态、设备在线、`computer.control` 权限和 `confirmed=true` 都满足后,排入 `device_maintenance / codex_remote_control` 任务;目标 local-agent 认领后执行本机 `codex remote-control start|stop --json`,结果通过 MasterAgentTask completion 回写,并进入权限审计日志
|
||||||
- 本机 `codex mcp-server --help` 已可用;它用于把 Codex 暴露为 stdio MCP server,后续进入 `CodexMcpBackendAdapter` 兼容 provider,而不是替代 App Server 主链
|
- 本机 `codex mcp-server --help` 已可用;它用于把 Codex 暴露为 stdio MCP server,后续进入 `CodexMcpBackendAdapter` 兼容 provider,而不是替代 App Server 主链
|
||||||
- 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.136.0-alpha.2/`,共识别 151 个协议方法;确认支持 `thread/inject_items`、`thread/rollback`、`thread/archive`、`thread/unarchive`、`thread/fork`、`thread/compact/start`、`thread/name/set`、`thread/metadata/update`、`thread/shellCommand`、`thread/unsubscribe`、`thread/goal/*`、`turn/steer`、`turn/interrupt`、`command/exec`、`command/exec/write`、`command/exec/resize`、`command/exec/terminate`、`command/exec/outputDelta`、`process/outputDelta|exited`、`rawResponseItem/completed`、`thread/realtime/*`、`thread/started|closed|archived|unarchived|name/updated`、`item/agentMessage/delta`、`item/plan/delta`、`item/reasoning/*Delta`、`item/mcpToolCall/progress`、`item/commandExecution/terminalInteraction`、`item/fileChange/outputDelta`、`account/*`、`model/verification`、`configWarning`、`deprecationNotice`、`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`
|
- 本机协议快照已生成到 `docs/protocol-snapshots/codex-app-server/0.136.0-alpha.2/`,共识别 151 个协议方法;确认支持 `thread/inject_items`、`thread/rollback`、`thread/archive`、`thread/unarchive`、`thread/fork`、`thread/compact/start`、`thread/name/set`、`thread/metadata/update`、`thread/shellCommand`、`thread/unsubscribe`、`thread/goal/*`、`turn/steer`、`turn/interrupt`、`command/exec`、`command/exec/write`、`command/exec/resize`、`command/exec/terminate`、`command/exec/outputDelta`、`process/outputDelta|exited`、`rawResponseItem/completed`、`thread/realtime/*`、`thread/started|closed|archived|unarchived|name/updated`、`item/agentMessage/delta`、`item/plan/delta`、`item/reasoning/*Delta`、`item/mcpToolCall/progress`、`item/commandExecution/terminalInteraction`、`item/fileChange/outputDelta`、`account/*`、`model/verification`、`configWarning`、`deprecationNotice`、`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`
|
||||||
- Boss 当前默认仍以 `stdio` 作为本机 agent 接入方式;`ws://127.0.0.1:<port>` 和 `unix://PATH` 本地长驻 transport 已可灰度接入,WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer <token>`;非 loopback signed bearer/JWT、自动重连和健康探测仍保留为后续增强,不直接替换当前稳定链路
|
- Boss 当前默认仍以 `stdio` 作为本机 agent 接入方式;`ws://127.0.0.1:<port>` 和 `unix://PATH` 本地长驻 transport 已可灰度接入,WebSocket/Unix WebSocket handshake 支持 `Authorization: Bearer <token>`;非 loopback signed bearer/JWT、自动重连和健康探测仍保留为后续增强,不直接替换当前稳定链路
|
||||||
|
|||||||
@@ -36,7 +36,7 @@
|
|||||||
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
|
- 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现
|
||||||
- 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准
|
- 当前生产主链仍然沿用 `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 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` 托管摘要和启动命令,默认只观测不启动,后续真正启动 / 停止必须走显式操作、权限、审批和审计链路。
|
- 当前本机 `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 接入` 表格已接入 `启动远控 / 停止远控` 操作。
|
||||||
- 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。
|
- 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 中拉取 `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、本地路径或迁移描述。
|
- 当前 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、本地路径或迁移描述。
|
||||||
|
|||||||
@@ -664,6 +664,26 @@ function normalizeInterruptPollIntervalMs(config) {
|
|||||||
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 750;
|
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : 750;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canHandleCodexRemoteControlMaintenanceTask(task) {
|
||||||
|
return (
|
||||||
|
task?.taskType === "device_maintenance" &&
|
||||||
|
task?.maintenanceKind === "codex_remote_control" &&
|
||||||
|
(task?.codexRemoteControlAction === "start" || task?.codexRemoteControlAction === "stop")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildCodexRemoteControlMaintenanceReply(task, result) {
|
||||||
|
const actionLabel = task.codexRemoteControlAction === "start" ? "已启动" : "已停止";
|
||||||
|
const lines = [`Codex Remote Control ${actionLabel}。`];
|
||||||
|
if (result.commandLabel) {
|
||||||
|
lines.push(`本机命令:${result.commandLabel}`);
|
||||||
|
}
|
||||||
|
if (result.outputSummary) {
|
||||||
|
lines.push(`返回摘要:${result.outputSummary}`);
|
||||||
|
}
|
||||||
|
return lines.join("\n");
|
||||||
|
}
|
||||||
|
|
||||||
async function claimSkillLifecycleRequest(config, runtime) {
|
async function claimSkillLifecycleRequest(config, runtime) {
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`${config.controlPlaneUrl.replace(/\/$/, "")}/api/v1/devices/${config.deviceId}/skill-requests/claim`,
|
`${config.controlPlaneUrl.replace(/\/$/, "")}/api/v1/devices/${config.deviceId}/skill-requests/claim`,
|
||||||
@@ -881,6 +901,23 @@ async function runMasterAgentTask(config, runtime, task) {
|
|||||||
try {
|
try {
|
||||||
let activeChild = null;
|
let activeChild = null;
|
||||||
const executionResult = await (async () => {
|
const executionResult = await (async () => {
|
||||||
|
if (canHandleCodexRemoteControlMaintenanceTask(task)) {
|
||||||
|
const daemonResult = await runCodexRemoteControlDaemonAction(
|
||||||
|
task.codexRemoteControlAction,
|
||||||
|
config,
|
||||||
|
);
|
||||||
|
if (daemonResult.status === "failed") {
|
||||||
|
throw new Error(
|
||||||
|
daemonResult.error ||
|
||||||
|
daemonResult.outputSummary ||
|
||||||
|
`CODEX_REMOTE_CONTROL_${String(task.codexRemoteControlAction).toUpperCase()}_FAILED`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
replyBody: buildCodexRemoteControlMaintenanceReply(task, daemonResult),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
if (canHandleBrowserControlTask(task)) {
|
if (canHandleBrowserControlTask(task)) {
|
||||||
const browserResult = await executeBrowserControlTask(task, config);
|
const browserResult = await executeBrowserControlTask(task, config);
|
||||||
if (browserResult.status === "failed") {
|
if (browserResult.status === "failed") {
|
||||||
|
|||||||
493
public/admin-web/assets/index-BBKOTElI.js
Normal file
493
public/admin-web/assets/index-BBKOTElI.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -4,7 +4,7 @@
|
|||||||
<meta charset="UTF-8" />
|
<meta charset="UTF-8" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>Boss 企业后台</title>
|
<title>Boss 企业后台</title>
|
||||||
<script type="module" crossorigin src="/admin-web/assets/index-D8-R1LUW.js"></script>
|
<script type="module" crossorigin src="/admin-web/assets/index-BBKOTElI.js"></script>
|
||||||
<link rel="stylesheet" crossorigin href="/admin-web/assets/index-BVg8rLlq.css">
|
<link rel="stylesheet" crossorigin href="/admin-web/assets/index-BVg8rLlq.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -0,0 +1,95 @@
|
|||||||
|
import { NextRequest } from "next/server";
|
||||||
|
import { jsonNoStore } from "@/lib/api-response";
|
||||||
|
import { buildRequestAuditMeta } from "@/lib/boss-audit";
|
||||||
|
import { requireRequestSession } from "@/lib/boss-auth";
|
||||||
|
import {
|
||||||
|
appendPermissionAuditLog,
|
||||||
|
queueCodexRemoteControlTask,
|
||||||
|
readState,
|
||||||
|
type CodexRemoteControlAction,
|
||||||
|
} from "@/lib/boss-data";
|
||||||
|
import { canAccessDevice } from "@/lib/boss-permissions";
|
||||||
|
|
||||||
|
function normalizeAction(value: unknown): CodexRemoteControlAction | null {
|
||||||
|
return value === "start" || value === "stop" ? value : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function recordDenied(input: {
|
||||||
|
actorAccount: string;
|
||||||
|
deviceId: string;
|
||||||
|
reason: string;
|
||||||
|
request: NextRequest;
|
||||||
|
}) {
|
||||||
|
const auditMeta = buildRequestAuditMeta(input.request);
|
||||||
|
await appendPermissionAuditLog({
|
||||||
|
actorAccount: input.actorAccount,
|
||||||
|
action: "task.denied",
|
||||||
|
deviceId: input.deviceId,
|
||||||
|
permissions: ["computer.control"],
|
||||||
|
detail: input.reason,
|
||||||
|
...auditMeta,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function POST(
|
||||||
|
request: NextRequest,
|
||||||
|
context: { params: Promise<{ deviceId: string }> },
|
||||||
|
) {
|
||||||
|
const session = await requireRequestSession(request);
|
||||||
|
if (!session) {
|
||||||
|
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const { deviceId } = await context.params;
|
||||||
|
const body = (await request.json().catch(() => ({}))) as {
|
||||||
|
action?: unknown;
|
||||||
|
confirmed?: unknown;
|
||||||
|
reason?: unknown;
|
||||||
|
};
|
||||||
|
const action = normalizeAction(body.action);
|
||||||
|
if (!action) {
|
||||||
|
return jsonNoStore({ ok: false, message: "CODEX_REMOTE_CONTROL_ACTION_INVALID" }, { status: 400 });
|
||||||
|
}
|
||||||
|
if (body.confirmed !== true) {
|
||||||
|
return jsonNoStore({ ok: false, message: "CODEX_REMOTE_CONTROL_CONFIRMATION_REQUIRED" }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const state = await readState();
|
||||||
|
const device = state.devices.find((item) => item.id === deviceId);
|
||||||
|
if (!device) {
|
||||||
|
return jsonNoStore({ ok: false, message: "DEVICE_NOT_FOUND" }, { status: 404 });
|
||||||
|
}
|
||||||
|
if (device.status !== "online") {
|
||||||
|
await recordDenied({
|
||||||
|
actorAccount: session.account,
|
||||||
|
deviceId,
|
||||||
|
reason: `codex_remote_control:${action}:device_offline`,
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
return jsonNoStore({ ok: false, message: "DEVICE_OFFLINE" }, { status: 409 });
|
||||||
|
}
|
||||||
|
if (!canAccessDevice(state, session, deviceId, "computer.control")) {
|
||||||
|
await recordDenied({
|
||||||
|
actorAccount: session.account,
|
||||||
|
deviceId,
|
||||||
|
reason: `codex_remote_control:${action}:forbidden`,
|
||||||
|
request,
|
||||||
|
});
|
||||||
|
return jsonNoStore({ ok: false, message: "FORBIDDEN" }, { status: 403 });
|
||||||
|
}
|
||||||
|
|
||||||
|
const task = await queueCodexRemoteControlTask({
|
||||||
|
deviceId,
|
||||||
|
action,
|
||||||
|
requestedBy: session.displayName || session.account,
|
||||||
|
requestedByAccount: session.account,
|
||||||
|
reason: typeof body.reason === "string" ? body.reason : undefined,
|
||||||
|
auditMeta: buildRequestAuditMeta(request),
|
||||||
|
});
|
||||||
|
|
||||||
|
return jsonNoStore({
|
||||||
|
ok: true,
|
||||||
|
task,
|
||||||
|
message: action === "start" ? "CODEX_REMOTE_CONTROL_START_QUEUED" : "CODEX_REMOTE_CONTROL_STOP_QUEUED",
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -477,8 +477,11 @@ export type MasterAgentTaskType =
|
|||||||
| "group_dispatch_plan"
|
| "group_dispatch_plan"
|
||||||
| "dispatch_execution"
|
| "dispatch_execution"
|
||||||
| "device_import_resolution"
|
| "device_import_resolution"
|
||||||
|
| "device_maintenance"
|
||||||
| "browser_control"
|
| "browser_control"
|
||||||
| "desktop_control";
|
| "desktop_control";
|
||||||
|
export type DeviceMaintenanceKind = "codex_remote_control";
|
||||||
|
export type CodexRemoteControlAction = "start" | "stop";
|
||||||
export type ComputerControlIntentCategory =
|
export type ComputerControlIntentCategory =
|
||||||
| "discussion_only"
|
| "discussion_only"
|
||||||
| "project_development"
|
| "project_development"
|
||||||
@@ -1353,6 +1356,8 @@ export interface MasterAgentTask {
|
|||||||
deviceImportDraftId?: string;
|
deviceImportDraftId?: string;
|
||||||
deviceImportCandidateId?: string;
|
deviceImportCandidateId?: string;
|
||||||
deviceImportCandidateFolderName?: string;
|
deviceImportCandidateFolderName?: string;
|
||||||
|
maintenanceKind?: DeviceMaintenanceKind;
|
||||||
|
codexRemoteControlAction?: CodexRemoteControlAction;
|
||||||
projectUnderstandingTargetProjectId?: string;
|
projectUnderstandingTargetProjectId?: string;
|
||||||
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
|
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
|
||||||
projectUnderstandingReplyProjectId?: string;
|
projectUnderstandingReplyProjectId?: string;
|
||||||
@@ -4769,6 +4774,12 @@ export function migrateBossState(raw: Partial<BossState> | undefined): BossState
|
|||||||
deviceImportDraftId: task.deviceImportDraftId,
|
deviceImportDraftId: task.deviceImportDraftId,
|
||||||
deviceImportCandidateId: task.deviceImportCandidateId,
|
deviceImportCandidateId: task.deviceImportCandidateId,
|
||||||
deviceImportCandidateFolderName: task.deviceImportCandidateFolderName,
|
deviceImportCandidateFolderName: task.deviceImportCandidateFolderName,
|
||||||
|
maintenanceKind:
|
||||||
|
task.maintenanceKind === "codex_remote_control" ? task.maintenanceKind : undefined,
|
||||||
|
codexRemoteControlAction:
|
||||||
|
task.codexRemoteControlAction === "start" || task.codexRemoteControlAction === "stop"
|
||||||
|
? task.codexRemoteControlAction
|
||||||
|
: undefined,
|
||||||
projectUnderstandingTargetProjectId: task.projectUnderstandingTargetProjectId,
|
projectUnderstandingTargetProjectId: task.projectUnderstandingTargetProjectId,
|
||||||
projectUnderstandingReason:
|
projectUnderstandingReason:
|
||||||
task.projectUnderstandingReason === "heartbeat_activity" || task.projectUnderstandingReason === "thread_reply"
|
task.projectUnderstandingReason === "heartbeat_activity" || task.projectUnderstandingReason === "thread_reply"
|
||||||
@@ -6020,6 +6031,14 @@ function normalizeExecutionProgressStreamEvents(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function defaultExecutionProgressStepTexts(task: Pick<MasterAgentTask, "taskType" | "relayViaMasterAgent">) {
|
function defaultExecutionProgressStepTexts(task: Pick<MasterAgentTask, "taskType" | "relayViaMasterAgent">) {
|
||||||
|
if (task.taskType === "device_maintenance") {
|
||||||
|
return [
|
||||||
|
"接收设备维护指令",
|
||||||
|
"确认设备权限和绑定状态",
|
||||||
|
"执行本机维护动作",
|
||||||
|
"回写维护结果",
|
||||||
|
];
|
||||||
|
}
|
||||||
if (task.taskType === "browser_control") {
|
if (task.taskType === "browser_control") {
|
||||||
return [
|
return [
|
||||||
"接收远程控制指令",
|
"接收远程控制指令",
|
||||||
@@ -6098,6 +6117,9 @@ function normalizeExecutionProgressSteps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
function resolveTaskExecutionProgressProjectId(task: Pick<MasterAgentTask, "projectId" | "taskType" | "targetProjectId">) {
|
function resolveTaskExecutionProgressProjectId(task: Pick<MasterAgentTask, "projectId" | "taskType" | "targetProjectId">) {
|
||||||
|
if (task.taskType === "device_maintenance") {
|
||||||
|
return task.projectId?.trim() || "";
|
||||||
|
}
|
||||||
if (task.taskType === "browser_control" || task.taskType === "desktop_control") {
|
if (task.taskType === "browser_control" || task.taskType === "desktop_control") {
|
||||||
return task.projectId?.trim() || "";
|
return task.projectId?.trim() || "";
|
||||||
}
|
}
|
||||||
@@ -6111,6 +6133,9 @@ function resolveTaskExecutionProgressProjectId(task: Pick<MasterAgentTask, "proj
|
|||||||
}
|
}
|
||||||
|
|
||||||
function shouldShowTaskExecutionProgress(task: Pick<MasterAgentTask, "projectId" | "taskType" | "targetProjectId" | "targetThreadId">) {
|
function shouldShowTaskExecutionProgress(task: Pick<MasterAgentTask, "projectId" | "taskType" | "targetProjectId" | "targetThreadId">) {
|
||||||
|
if (task.taskType === "device_maintenance") {
|
||||||
|
return Boolean(task.projectId?.trim());
|
||||||
|
}
|
||||||
if (task.taskType === "browser_control" || task.taskType === "desktop_control") {
|
if (task.taskType === "browser_control" || task.taskType === "desktop_control") {
|
||||||
return Boolean(task.projectId?.trim());
|
return Boolean(task.projectId?.trim());
|
||||||
}
|
}
|
||||||
@@ -6135,7 +6160,11 @@ function buildExecutionProgressSnapshot(
|
|||||||
}
|
}
|
||||||
const normalizedStatus = normalizeExecutionProgressStatus(status);
|
const normalizedStatus = normalizeExecutionProgressStatus(status);
|
||||||
const steps = normalizeExecutionProgressSteps(task, normalizedStatus, input?.steps);
|
const steps = normalizeExecutionProgressSteps(task, normalizedStatus, input?.steps);
|
||||||
const nativeRemoteControl = task.taskType === "browser_control" || task.taskType === "desktop_control";
|
const nativeRemoteControl =
|
||||||
|
task.taskType === "browser_control" ||
|
||||||
|
task.taskType === "desktop_control" ||
|
||||||
|
task.taskType === "device_maintenance";
|
||||||
|
const maintenanceControl = task.taskType === "device_maintenance";
|
||||||
return {
|
return {
|
||||||
taskId: task.taskId,
|
taskId: task.taskId,
|
||||||
projectId,
|
projectId,
|
||||||
@@ -6145,7 +6174,7 @@ function buildExecutionProgressSnapshot(
|
|||||||
runtimeKind: task.runtimeKind,
|
runtimeKind: task.runtimeKind,
|
||||||
controlPlatform: task.controlPlatform,
|
controlPlatform: task.controlPlatform,
|
||||||
computerUseProvider: task.computerUseProvider,
|
computerUseProvider: task.computerUseProvider,
|
||||||
title: nativeRemoteControl ? "远程控制进度" : "进度",
|
title: maintenanceControl ? "设备维护进度" : nativeRemoteControl ? "远程控制进度" : "进度",
|
||||||
status: normalizedStatus,
|
status: normalizedStatus,
|
||||||
steps,
|
steps,
|
||||||
branch: nativeRemoteControl ? undefined : normalizeExecutionProgressBranch(input?.branch),
|
branch: nativeRemoteControl ? undefined : normalizeExecutionProgressBranch(input?.branch),
|
||||||
@@ -8882,6 +8911,8 @@ export async function queueMasterAgentTask(payload: {
|
|||||||
orchestrationBackendLabel?: string;
|
orchestrationBackendLabel?: string;
|
||||||
deviceImportCandidateId?: string;
|
deviceImportCandidateId?: string;
|
||||||
deviceImportCandidateFolderName?: string;
|
deviceImportCandidateFolderName?: string;
|
||||||
|
maintenanceKind?: DeviceMaintenanceKind;
|
||||||
|
codexRemoteControlAction?: CodexRemoteControlAction;
|
||||||
projectUnderstandingTargetProjectId?: string;
|
projectUnderstandingTargetProjectId?: string;
|
||||||
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
|
projectUnderstandingReason?: "heartbeat_activity" | "thread_reply";
|
||||||
projectUnderstandingReplyProjectId?: string;
|
projectUnderstandingReplyProjectId?: string;
|
||||||
@@ -8956,6 +8987,12 @@ export async function queueMasterAgentTask(payload: {
|
|||||||
orchestrationBackendLabel: payload.orchestrationBackendLabel,
|
orchestrationBackendLabel: payload.orchestrationBackendLabel,
|
||||||
deviceImportCandidateId: payload.deviceImportCandidateId,
|
deviceImportCandidateId: payload.deviceImportCandidateId,
|
||||||
deviceImportCandidateFolderName: payload.deviceImportCandidateFolderName,
|
deviceImportCandidateFolderName: payload.deviceImportCandidateFolderName,
|
||||||
|
maintenanceKind:
|
||||||
|
payload.maintenanceKind === "codex_remote_control" ? payload.maintenanceKind : undefined,
|
||||||
|
codexRemoteControlAction:
|
||||||
|
payload.codexRemoteControlAction === "start" || payload.codexRemoteControlAction === "stop"
|
||||||
|
? payload.codexRemoteControlAction
|
||||||
|
: undefined,
|
||||||
projectUnderstandingTargetProjectId: payload.projectUnderstandingTargetProjectId,
|
projectUnderstandingTargetProjectId: payload.projectUnderstandingTargetProjectId,
|
||||||
projectUnderstandingReason: payload.projectUnderstandingReason,
|
projectUnderstandingReason: payload.projectUnderstandingReason,
|
||||||
projectUnderstandingReplyProjectId: payload.projectUnderstandingReplyProjectId,
|
projectUnderstandingReplyProjectId: payload.projectUnderstandingReplyProjectId,
|
||||||
@@ -9027,6 +9064,55 @@ export async function queueMasterAgentTask(payload: {
|
|||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function queueCodexRemoteControlTask(input: {
|
||||||
|
deviceId: string;
|
||||||
|
action: CodexRemoteControlAction;
|
||||||
|
requestedBy: string;
|
||||||
|
requestedByAccount: string;
|
||||||
|
reason?: string;
|
||||||
|
auditMeta?: PermissionAuditMeta;
|
||||||
|
}) {
|
||||||
|
if (input.action !== "start" && input.action !== "stop") {
|
||||||
|
throw new Error("CODEX_REMOTE_CONTROL_ACTION_INVALID");
|
||||||
|
}
|
||||||
|
const actionLabel = input.action === "start" ? "启动" : "停止";
|
||||||
|
const reason = trimToDefined(input.reason) ?? `${actionLabel} Codex Remote Control。`;
|
||||||
|
const task = await queueMasterAgentTask({
|
||||||
|
projectId: "master-agent",
|
||||||
|
taskType: "device_maintenance",
|
||||||
|
requestMessageId: randomToken("msg-maintenance"),
|
||||||
|
requestText: reason,
|
||||||
|
executionPrompt: `设备维护:${actionLabel} Codex Remote Control。`,
|
||||||
|
requestedBy: input.requestedBy,
|
||||||
|
requestedByAccount: input.requestedByAccount,
|
||||||
|
authorizedDeviceIds: [input.deviceId],
|
||||||
|
requiredPermissions: ["computer.control"],
|
||||||
|
deviceId: input.deviceId,
|
||||||
|
maintenanceKind: "codex_remote_control",
|
||||||
|
codexRemoteControlAction: input.action,
|
||||||
|
runtimeKind: "codex-thread-runtime",
|
||||||
|
confirmationPolicy: "strong_confirm",
|
||||||
|
requiresUserConfirmation: true,
|
||||||
|
confirmationScopeKey: `device:${input.deviceId}:codex_remote_control`,
|
||||||
|
});
|
||||||
|
await appendPermissionAuditLog({
|
||||||
|
actorAccount: input.requestedByAccount,
|
||||||
|
action: "task.authorized",
|
||||||
|
deviceId: input.deviceId,
|
||||||
|
permissions: ["computer.control"],
|
||||||
|
detail: `codex_remote_control:${input.action}`,
|
||||||
|
ipAddress: input.auditMeta?.ipAddress,
|
||||||
|
userAgent: input.auditMeta?.userAgent,
|
||||||
|
requestId: input.auditMeta?.requestId,
|
||||||
|
afterJson: {
|
||||||
|
taskId: task.taskId,
|
||||||
|
maintenanceKind: "codex_remote_control",
|
||||||
|
codexRemoteControlAction: input.action,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return task;
|
||||||
|
}
|
||||||
|
|
||||||
export async function createDispatchPlan(input: {
|
export async function createDispatchPlan(input: {
|
||||||
groupProjectId: string;
|
groupProjectId: string;
|
||||||
requestMessageId: string;
|
requestMessageId: string;
|
||||||
|
|||||||
@@ -28,11 +28,12 @@ test("independent Boss admin web app uses the backoffice BFF with cookie session
|
|||||||
assert.match(apiSource, /\/api\/v1\/admin\/risks\/actions/);
|
assert.match(apiSource, /\/api\/v1\/admin\/risks\/actions/);
|
||||||
assert.match(apiSource, /\/api\/v1\/admin\/skills\/requests/);
|
assert.match(apiSource, /\/api\/v1\/admin\/skills\/requests/);
|
||||||
assert.match(apiSource, /\/api\/v1\/admin\/backups/);
|
assert.match(apiSource, /\/api\/v1\/admin\/backups/);
|
||||||
|
assert.match(apiSource, /\/api\/v1\/devices\/\$\{encodeURIComponent\(deviceId\)\}\/codex-remote-control/);
|
||||||
assert.match(apiSource, /credentials:\s*["']include["']/);
|
assert.match(apiSource, /credentials:\s*["']include["']/);
|
||||||
assert.match(apiSource, /menuTree/);
|
assert.match(apiSource, /menuTree/);
|
||||||
assert.match(apiSource, /tenants/);
|
assert.match(apiSource, /tenants/);
|
||||||
assert.match(apiSource, /resourceGroups/);
|
assert.match(apiSource, /resourceGroups/);
|
||||||
for (const fn of ["postAdminAccess", "postRiskAction", "postSkillLifecycleRequest", "fetchAdminBackups", "createAdminBackup", "restoreAdminBackup"]) {
|
for (const fn of ["postAdminAccess", "postRiskAction", "postSkillLifecycleRequest", "postDeviceCodexRemoteControl", "fetchAdminBackups", "createAdminBackup", "restoreAdminBackup"]) {
|
||||||
assert.match(apiSource, new RegExp(`function\\s+${fn}|const\\s+${fn}`));
|
assert.match(apiSource, new RegExp(`function\\s+${fn}|const\\s+${fn}`));
|
||||||
}
|
}
|
||||||
for (const action of ["create_snapshot", "restore_snapshot"]) {
|
for (const action of ["create_snapshot", "restore_snapshot"]) {
|
||||||
@@ -103,6 +104,8 @@ test("independent Boss admin web app exposes management actions instead of read
|
|||||||
"关闭风险",
|
"关闭风险",
|
||||||
"创建工单",
|
"创建工单",
|
||||||
"创建 Skill 请求",
|
"创建 Skill 请求",
|
||||||
|
"启动远控",
|
||||||
|
"停止远控",
|
||||||
"创建状态快照",
|
"创建状态快照",
|
||||||
"恢复到此快照",
|
"恢复到此快照",
|
||||||
"快照清单",
|
"快照清单",
|
||||||
@@ -128,6 +131,7 @@ test("independent Boss admin web app exposes management actions instead of read
|
|||||||
assert.match(appSource, /loadBackupSnapshots/);
|
assert.match(appSource, /loadBackupSnapshots/);
|
||||||
assert.match(appSource, /createAdminBackup/);
|
assert.match(appSource, /createAdminBackup/);
|
||||||
assert.match(appSource, /restoreAdminBackup/);
|
assert.match(appSource, /restoreAdminBackup/);
|
||||||
|
assert.match(appSource, /handleCodexRemoteControl/);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("root Next project isolates the independent Vue admin workspace", async () => {
|
test("root Next project isolates the independent Vue admin workspace", async () => {
|
||||||
|
|||||||
178
tests/device-codex-remote-control-route.test.ts
Normal file
178
tests/device-codex-remote-control-route.test.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
import test from "node:test";
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import os from "node:os";
|
||||||
|
import path from "node:path";
|
||||||
|
import { mkdtemp, rm } from "node:fs/promises";
|
||||||
|
import { NextRequest } from "next/server";
|
||||||
|
|
||||||
|
let runtimeRoot = "";
|
||||||
|
let data: typeof import("../src/lib/boss-data");
|
||||||
|
let authCookie = "";
|
||||||
|
let postRemoteControl: (typeof import("../src/app/api/v1/devices/[deviceId]/codex-remote-control/route"))["POST"];
|
||||||
|
let baseState: Awaited<ReturnType<typeof import("../src/lib/boss-data")["readState"]>>;
|
||||||
|
|
||||||
|
async function setup() {
|
||||||
|
if (runtimeRoot) return;
|
||||||
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-codex-remote-control-"));
|
||||||
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||||
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||||
|
|
||||||
|
const [dataModule, authModule, routeModule] = await Promise.all([
|
||||||
|
import("../src/lib/boss-data.ts"),
|
||||||
|
import("../src/lib/boss-auth.ts"),
|
||||||
|
import("../src/app/api/v1/devices/[deviceId]/codex-remote-control/route.ts"),
|
||||||
|
]);
|
||||||
|
data = dataModule;
|
||||||
|
authCookie = authModule.AUTH_SESSION_COOKIE;
|
||||||
|
postRemoteControl = routeModule.POST;
|
||||||
|
baseState = structuredClone(await data.readState());
|
||||||
|
}
|
||||||
|
|
||||||
|
test.beforeEach(async () => {
|
||||||
|
await setup();
|
||||||
|
await rm(runtimeRoot, { recursive: true, force: true });
|
||||||
|
const state = structuredClone(baseState);
|
||||||
|
const now = "2026-06-04T10:00:00+08:00";
|
||||||
|
state.authAccounts = [
|
||||||
|
{
|
||||||
|
id: "account-owner",
|
||||||
|
account: "owner@boss.test",
|
||||||
|
passwordHash: "secret",
|
||||||
|
displayName: "企业老板",
|
||||||
|
role: "highest_admin",
|
||||||
|
primaryDeviceId: "mac-1",
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "account-operator",
|
||||||
|
account: "operator@boss.test",
|
||||||
|
passwordHash: "secret",
|
||||||
|
displayName: "设备操作者",
|
||||||
|
role: "member",
|
||||||
|
primaryDeviceId: "mac-1",
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
state.authSessions = [];
|
||||||
|
state.accountDeviceGrants = [];
|
||||||
|
state.permissionAuditLogs = [];
|
||||||
|
state.devices = [
|
||||||
|
{
|
||||||
|
id: "mac-1",
|
||||||
|
name: "客户 Mac",
|
||||||
|
avatar: "M",
|
||||||
|
account: "owner@boss.test",
|
||||||
|
source: "production",
|
||||||
|
status: "online",
|
||||||
|
projects: ["master-agent"],
|
||||||
|
quota5h: 0,
|
||||||
|
quota7d: 0,
|
||||||
|
lastSeenAt: now,
|
||||||
|
preferredExecutionMode: "cli",
|
||||||
|
capabilities: {
|
||||||
|
gui: { connected: true, lastSeenAt: now },
|
||||||
|
cli: { connected: true, lastSeenAt: now },
|
||||||
|
browserAutomation: { connected: true, lastSeenAt: now },
|
||||||
|
computerUse: { connected: true, lastSeenAt: now },
|
||||||
|
codexAppServer: {
|
||||||
|
connected: true,
|
||||||
|
lastSeenAt: now,
|
||||||
|
metadata: {
|
||||||
|
remoteControlSummary: {
|
||||||
|
supported: true,
|
||||||
|
startCommandLabel: "codex remote-control start --json",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
await data.writeState(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
test.after(async () => {
|
||||||
|
if (runtimeRoot) {
|
||||||
|
await rm(runtimeRoot, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
async function sessionCookie(account: string, role: "member" | "admin" | "highest_admin") {
|
||||||
|
const session = await data.createAuthSession({
|
||||||
|
account,
|
||||||
|
role,
|
||||||
|
displayName: account,
|
||||||
|
loginMethod: "password",
|
||||||
|
});
|
||||||
|
return `${authCookie}=${session.sessionToken}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function postAs(
|
||||||
|
account: string,
|
||||||
|
role: "member" | "admin" | "highest_admin",
|
||||||
|
body: Record<string, unknown>,
|
||||||
|
) {
|
||||||
|
return postRemoteControl(
|
||||||
|
new NextRequest("http://127.0.0.1:3000/api/v1/devices/mac-1/codex-remote-control", {
|
||||||
|
method: "POST",
|
||||||
|
headers: {
|
||||||
|
"content-type": "application/json",
|
||||||
|
cookie: await sessionCookie(account, role),
|
||||||
|
"x-request-id": "req-remote-control",
|
||||||
|
},
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ deviceId: "mac-1" }) },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
test("codex remote control action requires login, explicit confirmation, and computer control permission", async () => {
|
||||||
|
await setup();
|
||||||
|
|
||||||
|
const anonymous = await postRemoteControl(
|
||||||
|
new NextRequest("http://127.0.0.1:3000/api/v1/devices/mac-1/codex-remote-control", {
|
||||||
|
method: "POST",
|
||||||
|
body: JSON.stringify({ action: "start", confirmed: true }),
|
||||||
|
}),
|
||||||
|
{ params: Promise.resolve({ deviceId: "mac-1" }) },
|
||||||
|
);
|
||||||
|
assert.equal(anonymous.status, 401);
|
||||||
|
|
||||||
|
const missingConfirmation = await postAs("owner@boss.test", "highest_admin", { action: "start" });
|
||||||
|
assert.equal(missingConfirmation.status, 400);
|
||||||
|
|
||||||
|
const forbidden = await postAs("operator@boss.test", "member", { action: "start", confirmed: true });
|
||||||
|
assert.equal(forbidden.status, 403);
|
||||||
|
|
||||||
|
const state = await data.readState();
|
||||||
|
assert.equal(state.masterAgentTasks.length, 0);
|
||||||
|
assert.equal(state.permissionAuditLogs.at(0)?.action, "task.denied");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("highest admin can queue a device maintenance task for codex remote control start", async () => {
|
||||||
|
const response = await postAs("owner@boss.test", "highest_admin", {
|
||||||
|
action: "start",
|
||||||
|
confirmed: true,
|
||||||
|
reason: "开启 Codex Remote Control 供 Boss App 真时控制。",
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
const payload = await response.json();
|
||||||
|
assert.equal(payload.ok, true);
|
||||||
|
assert.equal(payload.task.taskType, "device_maintenance");
|
||||||
|
assert.equal(payload.task.maintenanceKind, "codex_remote_control");
|
||||||
|
assert.equal(payload.task.codexRemoteControlAction, "start");
|
||||||
|
assert.deepEqual(payload.task.requiredPermissions, ["computer.control"]);
|
||||||
|
|
||||||
|
const claimed = await data.claimNextMasterAgentTask("mac-1");
|
||||||
|
assert.equal(claimed?.taskId, payload.task.taskId);
|
||||||
|
assert.equal(claimed?.maintenanceKind, "codex_remote_control");
|
||||||
|
assert.equal(claimed?.codexRemoteControlAction, "start");
|
||||||
|
|
||||||
|
const state = await data.readState();
|
||||||
|
const audit = state.permissionAuditLogs.find((item) => item.action === "task.authorized");
|
||||||
|
assert.equal(audit?.actorAccount, "owner@boss.test");
|
||||||
|
assert.equal(audit?.deviceId, "mac-1");
|
||||||
|
assert.equal(audit?.detail, "codex_remote_control:start");
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user