docs: add gui cli capability implementation plan
This commit is contained in:
@@ -0,0 +1,859 @@
|
||||
# 设备 GUI+CLI 双能力接入与并行冲突控制 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 让 Boss 支持同一台 Mac/Windows 设备同时接入 Codex GUI 与 CLI,并在同项目 GUI/CLI 并行写入风险出现时默认阻断,由用户在当前异常项目/文件夹级别选择禁止、允许本次或永久放行。
|
||||
|
||||
**Architecture:** 继续沿用现有单设备模型,不拆成两个虚拟设备。设备层新增 `gui/cli` 双能力状态和默认执行模式;项目/文件夹层新增并行冲突状态与放行策略;执行链在进入 CLI 写入任务前先做规则层冲突检测,命中后返回结构化风险卡,等待用户确认。
|
||||
|
||||
**Tech Stack:** Next.js App Router、TypeScript、文件型状态存储 `data/boss-state.json`、现有 `local-agent` heartbeat 链路、Android 原生客户端、Node test runner、Gradle unit tests
|
||||
|
||||
---
|
||||
|
||||
## 文件结构
|
||||
|
||||
### 状态模型与执行冲突检测
|
||||
|
||||
- Modify: `/Users/kris/code/boss/src/lib/boss-data.ts`
|
||||
- Test: `/Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts`
|
||||
- Test: `/Users/kris/code/boss/tests/device-execution-conflict.test.ts`
|
||||
|
||||
职责:
|
||||
- 给 `Device` 增加 `gui/cli` 能力状态
|
||||
- 给项目/文件夹增加并行冲突策略与状态
|
||||
- 在进入 CLI 写入型任务前做规则层冲突检测
|
||||
- 把 `禁止 / 允许本次 / 永久放行` 限定在当前异常项目/文件夹
|
||||
|
||||
### 设备详情与设备 API
|
||||
|
||||
- Modify: `/Users/kris/code/boss/src/lib/boss-projections.ts`
|
||||
- Modify: `/Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts`
|
||||
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
|
||||
|
||||
职责:
|
||||
- 把设备的 GUI/CLI 双能力与默认执行模式暴露给 Web/Android
|
||||
- 允许设备详情页更新 `preferredExecutionMode`
|
||||
|
||||
### 本地 agent 心跳能力上报
|
||||
|
||||
- Modify: `/Users/kris/code/boss/local-agent/server.mjs`
|
||||
- Modify: `/Users/kris/code/boss/local-agent/config.example.json`
|
||||
- Test: `/Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs`
|
||||
|
||||
职责:
|
||||
- heartbeat 上报当前设备的 `gui/cli` 能力状态
|
||||
- 不改变现有 `local-agent -> codex exec resume` 主链
|
||||
|
||||
### Android 设备详情与冲突提示
|
||||
|
||||
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java`
|
||||
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java`
|
||||
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java`
|
||||
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/DeviceDetailActivityTest.java`
|
||||
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/BossApiClientDeviceModeTest.java`
|
||||
|
||||
职责:
|
||||
- 在设备详情页显示 `GUI / CLI` 状态
|
||||
- 增加默认执行模式切换
|
||||
- 展示当前项目/文件夹级冲突状态和放行策略
|
||||
|
||||
### Web 设备详情与冲突提示
|
||||
|
||||
- Modify: `/Users/kris/code/boss/src/components/app-ui.tsx`
|
||||
- Modify: `/Users/kris/code/boss/src/app/devices/page.tsx`
|
||||
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
|
||||
|
||||
职责:
|
||||
- Web 端设备详情显示双能力与默认执行模式
|
||||
- 当前异常项目/文件夹的冲突卡三动作可操作
|
||||
|
||||
### 文档与回归
|
||||
|
||||
- Modify: `/Users/kris/code/boss/README.md`
|
||||
- Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md`
|
||||
|
||||
职责:
|
||||
- 记录 GUI/CLI 双能力设备模型
|
||||
- 记录项目/文件夹级冲突控制行为
|
||||
|
||||
---
|
||||
|
||||
### Task 1: 先定义设备双能力与项目级冲突状态
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/src/lib/boss-data.ts`
|
||||
- Test: `/Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,锁住设备双能力默认值与局部冲突策略**
|
||||
|
||||
```ts
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readState, writeStateForTests } from "@/lib/boss-data";
|
||||
|
||||
test("device stores gui and cli capabilities without splitting the physical device", async () => {
|
||||
const state = await readState();
|
||||
const device = state.devices.find((item) => item.id === "mac-studio");
|
||||
assert.ok(device);
|
||||
assert.equal(device.capabilities?.gui?.connected, true);
|
||||
assert.equal(device.capabilities?.cli?.connected, true);
|
||||
assert.equal(device.preferredExecutionMode, "cli");
|
||||
});
|
||||
|
||||
test("conflict policy is scoped to the active folder instead of the whole device", async () => {
|
||||
const state = await readState();
|
||||
state.projectExecutionPolicies = [
|
||||
{
|
||||
deviceId: "mac-studio",
|
||||
folderKey: "mac-studio:boss",
|
||||
projectId: "thread-ui",
|
||||
allowPolicy: "allow_always",
|
||||
conflictState: "warning",
|
||||
updatedAt: "2026-04-06T10:00:00.000Z",
|
||||
},
|
||||
];
|
||||
await writeStateForTests(state);
|
||||
|
||||
const nextState = await readState();
|
||||
const bossPolicy = nextState.projectExecutionPolicies.find((item) => item.folderKey === "mac-studio:boss");
|
||||
const otherPolicy = nextState.projectExecutionPolicies.find((item) => item.folderKey === "mac-studio:talking");
|
||||
assert.equal(bossPolicy?.allowPolicy, "allow_always");
|
||||
assert.equal(otherPolicy, undefined);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试,确认先失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,提示 `Device` 没有 `capabilities` 或 `preferredExecutionMode`
|
||||
|
||||
- [ ] **Step 3: 在状态模型里增加设备能力与冲突策略类型**
|
||||
|
||||
在 `/Users/kris/code/boss/src/lib/boss-data.ts` 增加最小实现:
|
||||
|
||||
```ts
|
||||
export type DeviceExecutionMode = "gui" | "cli";
|
||||
export type ProjectConflictAllowPolicy = "forbid" | "allow_once" | "allow_always";
|
||||
export type ProjectConflictState = "none" | "warning" | "blocked";
|
||||
|
||||
export interface DeviceCapabilityState {
|
||||
connected: boolean;
|
||||
lastSeenAt?: string;
|
||||
lastActiveProjectId?: string;
|
||||
}
|
||||
|
||||
export interface DeviceCapabilities {
|
||||
gui: DeviceCapabilityState;
|
||||
cli: DeviceCapabilityState;
|
||||
}
|
||||
|
||||
export interface ProjectExecutionPolicy {
|
||||
deviceId: string;
|
||||
folderKey?: string;
|
||||
projectId: string;
|
||||
allowPolicy: ProjectConflictAllowPolicy;
|
||||
conflictState: ProjectConflictState;
|
||||
activeCliExecution?: boolean;
|
||||
recentExternalActivityAt?: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export interface Device {
|
||||
id: string;
|
||||
name: string;
|
||||
avatar: string;
|
||||
account: string;
|
||||
source: DeviceSource;
|
||||
status: DeviceStatus;
|
||||
projects: string[];
|
||||
quota5h: number;
|
||||
quota7d: number;
|
||||
lastSeenAt: string;
|
||||
endpoint?: string;
|
||||
token?: string;
|
||||
note?: string;
|
||||
capabilities?: DeviceCapabilities;
|
||||
preferredExecutionMode?: DeviceExecutionMode;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 给初始种子设备补默认值**
|
||||
|
||||
在 `/Users/kris/code/boss/src/lib/boss-data.ts` 的种子设备里补:
|
||||
|
||||
```ts
|
||||
capabilities: {
|
||||
gui: { connected: true, lastSeenAt: "2026-04-06T09:00:00+08:00", lastActiveProjectId: "master-agent" },
|
||||
cli: { connected: true, lastSeenAt: "2026-04-06T09:00:00+08:00", lastActiveProjectId: "master-agent" },
|
||||
},
|
||||
preferredExecutionMode: "cli",
|
||||
```
|
||||
|
||||
Windows demo 设备补:
|
||||
|
||||
```ts
|
||||
capabilities: {
|
||||
gui: { connected: true, lastSeenAt: "2026-04-06T08:50:00+08:00", lastActiveProjectId: "audit-collab" },
|
||||
cli: { connected: false, lastSeenAt: "2026-04-06T08:40:00+08:00", lastActiveProjectId: "" },
|
||||
},
|
||||
preferredExecutionMode: "gui",
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 给状态恢复逻辑补兼容默认值**
|
||||
|
||||
在 `readState()` 的 normalize 阶段补:
|
||||
|
||||
```ts
|
||||
capabilities: {
|
||||
gui: {
|
||||
connected: Boolean(device.capabilities?.gui?.connected),
|
||||
lastSeenAt: device.capabilities?.gui?.lastSeenAt || device.lastSeenAt,
|
||||
lastActiveProjectId: device.capabilities?.gui?.lastActiveProjectId || "",
|
||||
},
|
||||
cli: {
|
||||
connected: Boolean(device.capabilities?.cli?.connected),
|
||||
lastSeenAt: device.capabilities?.cli?.lastSeenAt || device.lastSeenAt,
|
||||
lastActiveProjectId: device.capabilities?.cli?.lastActiveProjectId || "",
|
||||
},
|
||||
},
|
||||
preferredExecutionMode:
|
||||
device.preferredExecutionMode === "gui" || device.preferredExecutionMode === "cli"
|
||||
? device.preferredExecutionMode
|
||||
: "cli",
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 重跑测试,确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/src/lib/boss-data.ts /Users/kris/code/boss/tests/device-gui-cli-capabilities.test.ts
|
||||
git commit -m "feat: add gui cli device capability state"
|
||||
```
|
||||
|
||||
### Task 2: 在规则层实现项目/文件夹级并行冲突检测
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/src/lib/boss-data.ts`
|
||||
- Test: `/Users/kris/code/boss/tests/device-execution-conflict.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,锁住默认阻断、允许本次、永久放行都只作用于当前 folder**
|
||||
|
||||
```ts
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
applyProjectConflictDecision,
|
||||
detectProjectExecutionConflict,
|
||||
readState,
|
||||
writeStateForTests,
|
||||
} from "@/lib/boss-data";
|
||||
|
||||
test("detectProjectExecutionConflict blocks cli execution when the same folder has new external activity", async () => {
|
||||
const state = await readState();
|
||||
state.projectExecutionPolicies = [];
|
||||
await writeStateForTests(state);
|
||||
|
||||
const result = await detectProjectExecutionConflict({
|
||||
deviceId: "mac-studio",
|
||||
folderKey: "mac-studio:boss",
|
||||
projectId: "thread-ui",
|
||||
executionMode: "cli",
|
||||
activityAt: "2026-04-06T10:05:00.000Z",
|
||||
externalActivityAt: "2026-04-06T10:04:00.000Z",
|
||||
});
|
||||
|
||||
assert.equal(result.blocked, true);
|
||||
assert.equal(result.policy.allowPolicy, "forbid");
|
||||
});
|
||||
|
||||
test("allow_once only clears the active folder conflict after a single execution", async () => {
|
||||
await applyProjectConflictDecision({
|
||||
deviceId: "mac-studio",
|
||||
folderKey: "mac-studio:boss",
|
||||
projectId: "thread-ui",
|
||||
decision: "allow_once",
|
||||
});
|
||||
|
||||
let result = await detectProjectExecutionConflict({
|
||||
deviceId: "mac-studio",
|
||||
folderKey: "mac-studio:boss",
|
||||
projectId: "thread-ui",
|
||||
executionMode: "cli",
|
||||
activityAt: "2026-04-06T10:10:00.000Z",
|
||||
externalActivityAt: "2026-04-06T10:09:00.000Z",
|
||||
});
|
||||
assert.equal(result.blocked, false);
|
||||
|
||||
result = await detectProjectExecutionConflict({
|
||||
deviceId: "mac-studio",
|
||||
folderKey: "mac-studio:boss",
|
||||
projectId: "thread-ui",
|
||||
executionMode: "cli",
|
||||
activityAt: "2026-04-06T10:20:00.000Z",
|
||||
externalActivityAt: "2026-04-06T10:19:00.000Z",
|
||||
});
|
||||
assert.equal(result.blocked, true);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试,确认先失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-execution-conflict.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,提示函数不存在或策略未生效
|
||||
|
||||
- [ ] **Step 3: 增加冲突检测与决策函数**
|
||||
|
||||
在 `/Users/kris/code/boss/src/lib/boss-data.ts` 增加:
|
||||
|
||||
```ts
|
||||
export async function detectProjectExecutionConflict(input: {
|
||||
deviceId: string;
|
||||
folderKey?: string;
|
||||
projectId: string;
|
||||
executionMode: DeviceExecutionMode;
|
||||
activityAt: string;
|
||||
externalActivityAt?: string;
|
||||
}) {
|
||||
const state = await readState();
|
||||
const policy = matchProjectExecutionPolicy(state, input);
|
||||
const hasConflict =
|
||||
input.executionMode === "cli" &&
|
||||
Boolean(input.externalActivityAt) &&
|
||||
input.externalActivityAt! <= input.activityAt;
|
||||
|
||||
if (!hasConflict) {
|
||||
return { blocked: false, policy };
|
||||
}
|
||||
|
||||
if (policy?.allowPolicy === "allow_always") {
|
||||
return { blocked: false, policy };
|
||||
}
|
||||
|
||||
if (policy?.allowPolicy === "allow_once") {
|
||||
await clearAllowOncePolicy(input);
|
||||
return { blocked: false, policy };
|
||||
}
|
||||
|
||||
const nextPolicy = await upsertProjectExecutionPolicy({
|
||||
deviceId: input.deviceId,
|
||||
folderKey: input.folderKey,
|
||||
projectId: input.projectId,
|
||||
allowPolicy: "forbid",
|
||||
conflictState: "blocked",
|
||||
activeCliExecution: true,
|
||||
recentExternalActivityAt: input.externalActivityAt,
|
||||
});
|
||||
return { blocked: true, policy: nextPolicy };
|
||||
}
|
||||
|
||||
export async function applyProjectConflictDecision(input: {
|
||||
deviceId: string;
|
||||
folderKey?: string;
|
||||
projectId: string;
|
||||
decision: ProjectConflictAllowPolicy;
|
||||
}) {
|
||||
return upsertProjectExecutionPolicy({
|
||||
deviceId: input.deviceId,
|
||||
folderKey: input.folderKey,
|
||||
projectId: input.projectId,
|
||||
allowPolicy: input.decision,
|
||||
conflictState: input.decision === "forbid" ? "blocked" : "warning",
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 确保匹配范围只落在当前异常项目/文件夹**
|
||||
|
||||
匹配函数必须先按:
|
||||
|
||||
```ts
|
||||
deviceId + folderKey
|
||||
```
|
||||
|
||||
再退化到:
|
||||
|
||||
```ts
|
||||
deviceId + projectId
|
||||
```
|
||||
|
||||
不得在设备级匹配整个设备的默认策略。
|
||||
|
||||
- [ ] **Step 5: 重跑测试,确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-execution-conflict.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/src/lib/boss-data.ts /Users/kris/code/boss/tests/device-execution-conflict.test.ts
|
||||
git commit -m "feat: add folder scoped gui cli conflict guard"
|
||||
```
|
||||
|
||||
### Task 3: 让设备详情 API 与投影视图暴露 GUI/CLI 双能力
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/src/lib/boss-projections.ts`
|
||||
- Modify: `/Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts`
|
||||
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,锁住设备详情返回 GUI/CLI 能力与默认执行模式**
|
||||
|
||||
```ts
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { GET, PATCH } from "@/app/api/v1/devices/[deviceId]/route";
|
||||
|
||||
test("device detail exposes gui cli capabilities and preferred mode", async () => {
|
||||
const response = await GET(new Request("http://localhost/api/v1/devices/mac-studio"), {
|
||||
params: Promise.resolve({ deviceId: "mac-studio" }),
|
||||
});
|
||||
const data = await response.json();
|
||||
const device = data.workspace.selectedDevice;
|
||||
assert.equal(device.capabilities.gui.connected, true);
|
||||
assert.equal(device.capabilities.cli.connected, true);
|
||||
assert.equal(device.preferredExecutionMode, "cli");
|
||||
});
|
||||
|
||||
test("device detail patch updates preferred execution mode only", async () => {
|
||||
const response = await PATCH(
|
||||
new Request("http://localhost/api/v1/devices/mac-studio", {
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ preferredExecutionMode: "gui" }),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
}),
|
||||
{ params: Promise.resolve({ deviceId: "mac-studio" }) },
|
||||
);
|
||||
const data = await response.json();
|
||||
assert.equal(data.device.preferredExecutionMode, "gui");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试,确认先失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,提示返回字段缺失
|
||||
|
||||
- [ ] **Step 3: 在投影视图里暴露 GUI/CLI 状态**
|
||||
|
||||
在 `/Users/kris/code/boss/src/lib/boss-projections.ts` 的 `DeviceWorkspaceView` 构造里把:
|
||||
|
||||
```ts
|
||||
selectedDevice: {
|
||||
...device,
|
||||
capabilities: device.capabilities,
|
||||
preferredExecutionMode: device.preferredExecutionMode ?? "cli",
|
||||
},
|
||||
```
|
||||
|
||||
稳定输出。
|
||||
|
||||
- [ ] **Step 4: 在设备 PATCH 路由里支持只更新默认执行模式**
|
||||
|
||||
在 `/Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts` 收窄允许字段:
|
||||
|
||||
```ts
|
||||
const body = await request.json();
|
||||
const payload = {
|
||||
preferredExecutionMode:
|
||||
body.preferredExecutionMode === "gui" || body.preferredExecutionMode === "cli"
|
||||
? body.preferredExecutionMode
|
||||
: undefined,
|
||||
};
|
||||
const device = await updateDevice(deviceId, payload);
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 重跑测试,确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/src/lib/boss-projections.ts /Users/kris/code/boss/src/app/api/v1/devices/[deviceId]/route.ts /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
|
||||
git commit -m "feat: expose gui cli device capability details"
|
||||
```
|
||||
|
||||
### Task 4: 让 local-agent heartbeat 上报 CLI 能力
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/local-agent/server.mjs`
|
||||
- Modify: `/Users/kris/code/boss/local-agent/config.example.json`
|
||||
- Test: `/Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,锁住 heartbeat 载荷包含 `capabilities.cli`**
|
||||
|
||||
```js
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { buildHeartbeatPayload } from "../local-agent/server.mjs";
|
||||
|
||||
test("heartbeat payload includes cli capability state", async () => {
|
||||
const payload = await buildHeartbeatPayload({
|
||||
deviceId: "mac-studio",
|
||||
name: "Mac Studio",
|
||||
cliConnected: true,
|
||||
guiConnected: false,
|
||||
});
|
||||
|
||||
assert.equal(payload.device.capabilities.cli.connected, true);
|
||||
assert.equal(payload.device.capabilities.gui.connected, false);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试,确认先失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test /Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,提示 heartbeat payload 未包含该字段
|
||||
|
||||
- [ ] **Step 3: 在 heartbeat 载荷里新增双能力状态**
|
||||
|
||||
在 `/Users/kris/code/boss/local-agent/server.mjs` 的 heartbeat payload 里补:
|
||||
|
||||
```js
|
||||
capabilities: {
|
||||
gui: {
|
||||
connected: Boolean(config.guiConnected),
|
||||
lastSeenAt: now,
|
||||
lastActiveProjectId: "",
|
||||
},
|
||||
cli: {
|
||||
connected: true,
|
||||
lastSeenAt: now,
|
||||
lastActiveProjectId: "",
|
||||
},
|
||||
},
|
||||
preferredExecutionMode: config.preferredExecutionMode || "cli",
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在示例配置中补默认模式**
|
||||
|
||||
在 `/Users/kris/code/boss/local-agent/config.example.json` 补:
|
||||
|
||||
```json
|
||||
"preferredExecutionMode": "cli",
|
||||
"guiConnected": false
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 重跑测试,确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
node --test /Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/local-agent/server.mjs /Users/kris/code/boss/local-agent/config.example.json /Users/kris/code/boss/tests/local-agent-heartbeat-capabilities.test.mjs
|
||||
git commit -m "feat: report gui cli capabilities in heartbeat"
|
||||
```
|
||||
|
||||
### Task 5: Android 设备详情页显示 GUI/CLI 双能力并支持默认模式切换
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java`
|
||||
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java`
|
||||
- Modify: `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java`
|
||||
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/DeviceDetailActivityTest.java`
|
||||
- Test: `/Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/BossApiClientDeviceModeTest.java`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,锁住设备详情显示 GUI/CLI 状态与默认模式**
|
||||
|
||||
```java
|
||||
@Test
|
||||
public void renderDeviceShowsGuiCliCapabilitiesAndPreferredMode() {
|
||||
TestDeviceDetailActivity activity = Robolectric
|
||||
.buildActivity(TestDeviceDetailActivity.class, new Intent()
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_ID, "device-1")
|
||||
.putExtra(DeviceDetailActivity.EXTRA_DEVICE_NAME, "Mac Studio"))
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "GUI"));
|
||||
assertTrue(viewTreeContainsText(content, "CLI"));
|
||||
assertTrue(viewTreeContainsText(content, "默认执行模式"));
|
||||
assertTrue(viewTreeContainsText(content, "CLI"));
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试,确认先失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.DeviceDetailActivityTest --no-daemon
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL,提示页面文本不存在
|
||||
|
||||
- [ ] **Step 3: 给 Android client 增加更新默认模式接口**
|
||||
|
||||
在 `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java` 加:
|
||||
|
||||
```java
|
||||
public ApiResponse updateDeviceExecutionMode(String deviceId, String mode) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("preferredExecutionMode", mode);
|
||||
return patch("/api/v1/devices/" + encodePathSegment(deviceId), payload);
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 在设备详情页新增能力状态与模式切换**
|
||||
|
||||
在 `/Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java` 的 `renderDevice(...)` 中追加:
|
||||
|
||||
```java
|
||||
JSONObject capabilities = device.optJSONObject("capabilities");
|
||||
JSONObject gui = capabilities == null ? null : capabilities.optJSONObject("gui");
|
||||
JSONObject cli = capabilities == null ? null : capabilities.optJSONObject("cli");
|
||||
String preferredExecutionMode = device.optString("preferredExecutionMode", "cli");
|
||||
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"GUI",
|
||||
gui != null && gui.optBoolean("connected", false) ? "已连接" : "未连接",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
"CLI",
|
||||
cli != null && cli.optBoolean("connected", false) ? "已连接" : "未连接",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
appendContent(BossUi.buildMenuRow(
|
||||
this,
|
||||
"默认执行模式",
|
||||
"当前:" + ("gui".equals(preferredExecutionMode) ? "GUI" : "CLI"),
|
||||
null,
|
||||
v -> openExecutionModeDialog(preferredExecutionMode)
|
||||
));
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 实现切换弹窗和保存**
|
||||
|
||||
在 `openExecutionModeDialog(...)` 里:
|
||||
|
||||
```java
|
||||
String[] items = new String[] { "GUI", "CLI" };
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("默认执行模式")
|
||||
.setSingleChoiceItems(items, "gui".equals(currentMode) ? 0 : 1, null)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("保存", (dialog, which) -> saveExecutionMode(selectedMode))
|
||||
.show();
|
||||
```
|
||||
|
||||
- [ ] **Step 6: 重跑测试,确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.DeviceDetailActivityTest --tests com.hyzq.boss.BossApiClientDeviceModeTest --no-daemon
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 7: Commit**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/BossApiClient.java /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/DeviceDetailActivity.java /Users/kris/code/boss/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/DeviceDetailActivityTest.java /Users/kris/code/boss/android/app/src/test/java/com/hyzq/boss/BossApiClientDeviceModeTest.java
|
||||
git commit -m "feat: surface gui cli capabilities on android devices"
|
||||
```
|
||||
|
||||
### Task 6: Web 设备详情页显示双能力并支持冲突策略
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/src/components/app-ui.tsx`
|
||||
- Modify: `/Users/kris/code/boss/src/app/devices/page.tsx`
|
||||
- Test: `/Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts`
|
||||
|
||||
- [ ] **Step 1: 写失败测试,锁住设备详情展示 GUI/CLI 与默认模式**
|
||||
|
||||
```ts
|
||||
test("device page renders gui cli capability badges and preferred mode", async () => {
|
||||
const view = getDeviceWorkspaceView(await readState(), "mac-studio");
|
||||
assert.equal(view.selectedDevice?.capabilities?.gui.connected, true);
|
||||
assert.equal(view.selectedDevice?.capabilities?.cli.connected, true);
|
||||
assert.equal(view.selectedDevice?.preferredExecutionMode, "cli");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: 跑测试,确认先失败**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- FAIL
|
||||
|
||||
- [ ] **Step 3: 在 Web 设备详情区域渲染能力与默认模式**
|
||||
|
||||
在 `/Users/kris/code/boss/src/components/app-ui.tsx` 的设备详情块加入:
|
||||
|
||||
```tsx
|
||||
<div className="text-xs text-zinc-500">
|
||||
GUI:{device.capabilities?.gui?.connected ? "已连接" : "未连接"} · CLI:
|
||||
{device.capabilities?.cli?.connected ? "已连接" : "未连接"}
|
||||
</div>
|
||||
<button onClick={() => openDeviceExecutionMode(device.id)}>
|
||||
默认执行模式:{device.preferredExecutionMode === "gui" ? "GUI" : "CLI"}
|
||||
</button>
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 为冲突卡预留当前项目/文件夹级入口**
|
||||
|
||||
在同一区块加入:
|
||||
|
||||
```tsx
|
||||
{policy ? (
|
||||
<div>
|
||||
<div>当前项目并行策略:{policy.allowPolicy}</div>
|
||||
<button>禁止</button>
|
||||
<button>允许本次</button>
|
||||
<button>永久放行</button>
|
||||
</div>
|
||||
) : null}
|
||||
```
|
||||
|
||||
先把数据通道和入口铺上,不在这一任务里完成所有交互。
|
||||
|
||||
- [ ] **Step 5: 重跑测试,确认通过**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
npx --yes tsx --test /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 6: Commit**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/src/components/app-ui.tsx /Users/kris/code/boss/src/app/devices/page.tsx /Users/kris/code/boss/tests/device-detail-capabilities-route.test.ts
|
||||
git commit -m "feat: show gui cli capability state on web devices"
|
||||
```
|
||||
|
||||
### Task 7: 完整验证并同步文档
|
||||
|
||||
**Files:**
|
||||
- Modify: `/Users/kris/code/boss/README.md`
|
||||
- Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md`
|
||||
|
||||
- [ ] **Step 1: 跑服务端与 Android 关键回归**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/boss && npx --yes tsx --test tests/device-gui-cli-capabilities.test.ts tests/device-execution-conflict.test.ts tests/device-detail-capabilities-route.test.ts
|
||||
cd /Users/kris/code/boss/android && ./gradlew testDebugUnitTest --tests com.hyzq.boss.DeviceDetailActivityTest --tests com.hyzq.boss.BossApiClientDeviceModeTest --no-daemon
|
||||
```
|
||||
|
||||
Expected:
|
||||
- PASS
|
||||
|
||||
- [ ] **Step 2: 跑 lint / build / release**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/boss && npm run lint
|
||||
cd /Users/kris/code/boss && npm run build
|
||||
cd /Users/kris/code/boss/android && ./gradlew assembleRelease --no-daemon
|
||||
```
|
||||
|
||||
Expected:
|
||||
- 全部 PASS
|
||||
|
||||
- [ ] **Step 3: 更新文档**
|
||||
|
||||
在 `/Users/kris/code/boss/README.md` 和 `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md` 增加:
|
||||
|
||||
```md
|
||||
- 当前设备模型已支持同一台设备同时接入 Codex GUI 与 CLI
|
||||
- 设备详情页可查看 GUI / CLI 状态,并切换默认执行模式
|
||||
- 同项目 GUI/CLI 并行写入风险默认阻断,用户可在当前异常项目/文件夹级别选择 禁止 / 允许本次 / 永久放行
|
||||
```
|
||||
|
||||
- [ ] **Step 4: 提交文档与收口代码**
|
||||
|
||||
```bash
|
||||
git add /Users/kris/code/boss/README.md /Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md
|
||||
git commit -m "docs: describe gui cli device capability workflow"
|
||||
```
|
||||
|
||||
- [ ] **Step 5: 推分支并部署**
|
||||
|
||||
Run:
|
||||
|
||||
```bash
|
||||
cd /Users/kris/code/boss && git push gitea codex/wechat-native-ui-rollback
|
||||
cd /Users/kris/code/boss && ./scripts/deploy-server.sh
|
||||
```
|
||||
|
||||
Expected:
|
||||
- 分支推送成功
|
||||
- 服务器健康检查正常
|
||||
Reference in New Issue
Block a user