docs: add claw backend adapter plan

This commit is contained in:
kris
2026-04-03 00:48:51 +08:00
parent bfb7c43447
commit 8daaea01fd

View File

@@ -0,0 +1,760 @@
# Boss ClawBackendAdapter 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 新增一个默认关闭、可显式启用的 `ClawBackendAdapter`,让 `claw-code` 能作为单次执行后端候选接入 `master_agent_reply / thread_reply`
**Architecture:** 先在 `src/lib/execution/backends/` 下增加 `claw-config / claw-runner / claw-backend` 三个 focused 模块,再把 `ExecutionBackendSelector` 扩成支持 `claw` 候选但默认不参与。整个过程坚持“先最小单次执行,不改群聊审批和设备导入行为”,并通过环境变量驱动启用状态与外部命令配置。
**Tech Stack:** Next.js App Router、TypeScript、Node.js child_process、现有 `src/lib/execution/*` 抽象层、`tsx --test`
---
### Task 1: 定义 Claw backend 配置与最小运行时可用性判断
**Files:**
- Create: `/Users/kris/code/boss/src/lib/execution/backends/claw-config.ts`
- Test: `/Users/kris/code/boss/tests/claw-backend-config.test.ts`
- [ ] **Step 1: 写失败测试,固定默认关闭与配置解析行为**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import {
getClawBackendConfigForTesting,
isClawBackendConfiguredForTesting,
} from "../src/lib/execution/backends/claw-config.ts";
test("Claw backend 在未配置时默认关闭", () => {
const previous = { ...process.env };
delete process.env.BOSS_CLAW_ENABLED;
delete process.env.BOSS_CLAW_COMMAND;
const config = getClawBackendConfigForTesting();
assert.equal(config.enabled, false);
assert.equal(isClawBackendConfiguredForTesting(config), false);
process.env = previous;
});
test("Claw backend 在配置完整时返回 command、args 和 timeout", () => {
const previous = { ...process.env };
process.env.BOSS_CLAW_ENABLED = "true";
process.env.BOSS_CLAW_COMMAND = "claw";
process.env.BOSS_CLAW_ARGS = "run --json";
process.env.BOSS_CLAW_WORKDIR = "/tmp/claw";
process.env.BOSS_CLAW_TIMEOUT_MS = "45000";
const config = getClawBackendConfigForTesting();
assert.equal(config.enabled, true);
assert.equal(config.command, "claw");
assert.deepEqual(config.args, ["run", "--json"]);
assert.equal(config.cwd, "/tmp/claw");
assert.equal(config.timeoutMs, 45000);
assert.equal(isClawBackendConfiguredForTesting(config), true);
process.env = previous;
});
```
- [ ] **Step 2: 运行测试确认当前失败**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend-config.test.ts
```
Expected:
```text
ERR_MODULE_NOT_FOUND
```
- [ ] **Step 3: 写最小配置解析实现**
```ts
// /Users/kris/code/boss/src/lib/execution/backends/claw-config.ts
export interface ClawBackendConfig {
enabled: boolean;
command?: string;
args: string[];
cwd?: string;
timeoutMs: number;
defaultModel?: string;
}
function parseBoolean(value: string | undefined) {
return value?.trim().toLowerCase() === "true";
}
function parseArgs(value: string | undefined) {
return String(value || "")
.trim()
.split(/\s+/)
.filter(Boolean);
}
export function getClawBackendConfig(): ClawBackendConfig {
return {
enabled: parseBoolean(process.env.BOSS_CLAW_ENABLED),
command: process.env.BOSS_CLAW_COMMAND?.trim() || undefined,
args: parseArgs(process.env.BOSS_CLAW_ARGS),
cwd: process.env.BOSS_CLAW_WORKDIR?.trim() || undefined,
timeoutMs: Number.parseInt(process.env.BOSS_CLAW_TIMEOUT_MS || "45000", 10) || 45000,
defaultModel: process.env.BOSS_CLAW_DEFAULT_MODEL?.trim() || undefined,
};
}
export function isClawBackendConfigured(config: ClawBackendConfig) {
return config.enabled && Boolean(config.command);
}
export const getClawBackendConfigForTesting = getClawBackendConfig;
export const isClawBackendConfiguredForTesting = isClawBackendConfigured;
```
- [ ] **Step 4: 再跑测试确认通过**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend-config.test.ts
```
Expected:
```text
# pass 2
```
- [ ] **Step 5: 提交**
```bash
cd /Users/kris/code/boss
git add tests/claw-backend-config.test.ts src/lib/execution/backends/claw-config.ts
git commit -m "feat: add claw backend config"
```
### Task 2: 抽出 Claw runner统一外部命令协议
**Files:**
- Create: `/Users/kris/code/boss/src/lib/execution/backends/claw-runner.ts`
- Test: `/Users/kris/code/boss/tests/claw-runner.test.ts`
- [ ] **Step 1: 写失败测试,固定 JSON 协议映射**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import { createClawProcessResultForTesting } from "../src/lib/execution/backends/claw-runner.ts";
test("Claw runner 会把成功 JSON 映射成 completed", () => {
const result = createClawProcessResultForTesting({
exitCode: 0,
stdout: JSON.stringify({ status: "completed", output: "链路正常" }),
stderr: "",
});
assert.equal(result.status, "completed");
assert.equal(result.output, "链路正常");
});
test("Claw runner 会把非法 JSON 映射成 failed", () => {
const result = createClawProcessResultForTesting({
exitCode: 0,
stdout: "not-json",
stderr: "",
});
assert.equal(result.status, "failed");
assert.match(result.error, /INVALID_CLAW_RESPONSE/);
});
test("Claw runner 会把非零退出码映射成 failed", () => {
const result = createClawProcessResultForTesting({
exitCode: 2,
stdout: "",
stderr: "claw crashed",
});
assert.equal(result.status, "failed");
assert.match(result.error, /claw crashed/);
});
```
- [ ] **Step 2: 运行测试确认当前失败**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-runner.test.ts
```
Expected:
```text
ERR_MODULE_NOT_FOUND
```
- [ ] **Step 3: 写最小 runner 实现**
```ts
// /Users/kris/code/boss/src/lib/execution/backends/claw-runner.ts
import { spawn } from "node:child_process";
import type { ExecutionImmediateResult } from "@/lib/execution/types";
import type { ClawBackendConfig } from "@/lib/execution/backends/claw-config";
function normalizeClawProcessResult(input: {
exitCode: number;
stdout: string;
stderr: string;
}): ExecutionImmediateResult {
if (input.exitCode !== 0) {
return {
status: "failed",
backendId: "claw-runtime",
error: input.stderr.trim() || `CLAW_EXIT_${input.exitCode}`,
};
}
try {
const parsed = JSON.parse(input.stdout);
if (parsed?.status === "completed" && typeof parsed.output === "string") {
return {
status: "completed",
backendId: "claw-runtime",
output: parsed.output,
};
}
if (parsed?.status === "failed" && typeof parsed.error === "string") {
return {
status: "failed",
backendId: "claw-runtime",
error: parsed.error,
};
}
return {
status: "failed",
backendId: "claw-runtime",
error: "INVALID_CLAW_RESPONSE",
};
} catch {
return {
status: "failed",
backendId: "claw-runtime",
error: "INVALID_CLAW_RESPONSE",
};
}
}
export async function runClawCommand(input: {
config: ClawBackendConfig;
payload: unknown;
}): Promise<ExecutionImmediateResult> {
if (!input.config.command) {
return {
status: "failed",
backendId: "claw-runtime",
error: "CLAW_COMMAND_NOT_CONFIGURED",
};
}
return new Promise((resolve) => {
const child = spawn(input.config.command, input.config.args, {
cwd: input.config.cwd,
env: process.env,
stdio: ["pipe", "pipe", "pipe"],
});
let stdout = "";
let stderr = "";
const timer = setTimeout(() => {
child.kill("SIGKILL");
resolve({
status: "failed",
backendId: "claw-runtime",
error: "CLAW_TIMEOUT",
});
}, input.config.timeoutMs);
child.stdout.on("data", (chunk) => {
stdout += String(chunk);
});
child.stderr.on("data", (chunk) => {
stderr += String(chunk);
});
child.on("error", (error) => {
clearTimeout(timer);
resolve({
status: "failed",
backendId: "claw-runtime",
error: error.message,
});
});
child.on("close", (code) => {
clearTimeout(timer);
resolve(normalizeClawProcessResult({ exitCode: code ?? 1, stdout, stderr }));
});
child.stdin.end(JSON.stringify(input.payload));
});
}
export const createClawProcessResultForTesting = normalizeClawProcessResult;
```
- [ ] **Step 4: 再跑测试确认通过**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-runner.test.ts
```
Expected:
```text
# pass 3
```
- [ ] **Step 5: 提交**
```bash
cd /Users/kris/code/boss
git add tests/claw-runner.test.ts src/lib/execution/backends/claw-runner.ts
git commit -m "feat: add claw runtime runner"
```
### Task 3: 新增 `ClawBackendAdapter`,只承接单次执行
**Files:**
- Create: `/Users/kris/code/boss/src/lib/execution/backends/claw-backend.ts`
- Test: `/Users/kris/code/boss/tests/claw-backend.test.ts`
- [ ] **Step 1: 写失败测试,固定可处理范围和描述信息**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import { createExecutionRequest } from "../src/lib/execution/types.ts";
import { createClawBackendForTesting } from "../src/lib/execution/backends/claw-backend.ts";
test("ClawBackendAdapter 未启用时不可处理任何请求", async () => {
const backend = createClawBackendForTesting({ enabled: false, args: [], timeoutMs: 1000 });
const canHandle = await backend.canHandle(
createExecutionRequest({
kind: "master_agent_reply",
projectId: "master-agent",
requestMessageId: "msg-1",
body: "继续",
}),
);
assert.equal(canHandle, false);
});
test("ClawBackendAdapter 仅接 master_agent_reply 和 thread_reply", async () => {
const backend = createClawBackendForTesting({ enabled: true, command: "claw", args: [], timeoutMs: 1000 });
assert.equal(
await backend.canHandle(createExecutionRequest({ kind: "master_agent_reply", projectId: "master-agent", requestMessageId: "1", body: "a" })),
true,
);
assert.equal(
await backend.canHandle(createExecutionRequest({ kind: "thread_reply", projectId: "p1", requestMessageId: "2", body: "b" })),
true,
);
assert.equal(
await backend.canHandle(createExecutionRequest({ kind: "dispatch_execution", projectId: "g1", requestMessageId: "3", body: "c" })),
false,
);
});
test("ClawBackendAdapter describe 返回 claw backend 元信息", async () => {
const backend = createClawBackendForTesting({ enabled: true, command: "claw", args: [], timeoutMs: 1000 });
const description = await backend.describe(
createExecutionRequest({ kind: "master_agent_reply", projectId: "master-agent", requestMessageId: "1", body: "a" }),
);
assert.equal(description.backendId, "claw-runtime");
assert.equal(description.mode, "remote");
});
```
- [ ] **Step 2: 运行测试确认当前失败**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend.test.ts
```
Expected:
```text
ERR_MODULE_NOT_FOUND
```
- [ ] **Step 3: 写最小 backend 实现**
```ts
// /Users/kris/code/boss/src/lib/execution/backends/claw-backend.ts
import type { ExecutionBackend } from "@/lib/execution/execution-backend";
import type { ExecutionRequest } from "@/lib/execution/types";
import { getClawBackendConfig, isClawBackendConfigured } from "@/lib/execution/backends/claw-config";
import { runClawCommand } from "@/lib/execution/backends/claw-runner";
function isSupportedKind(kind: ExecutionRequest["kind"]) {
return kind === "master_agent_reply" || kind === "thread_reply";
}
export function createClawBackend(config = getClawBackendConfig()): ExecutionBackend {
return {
backendId: "claw-runtime",
async canHandle(input) {
return isClawBackendConfigured(config) && isSupportedKind(input.kind);
},
async execute(input) {
return runClawCommand({
config,
payload: {
kind: input.kind,
projectId: input.projectId,
requestMessageId: input.requestMessageId,
body: input.body,
targetProjectId: input.targetProjectId,
targetThreadId: input.targetThreadId,
modelOverride: input.modelOverride ?? config.defaultModel,
reasoningEffortOverride: input.reasoningEffortOverride,
},
});
},
async describe() {
return {
backendId: "claw-runtime",
label: "Claw Runtime",
mode: "remote",
};
},
};
}
export const CLAW_BACKEND = createClawBackend();
export const createClawBackendForTesting = createClawBackend;
```
- [ ] **Step 4: 再跑测试确认通过**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend.test.ts
```
Expected:
```text
# pass 3
```
- [ ] **Step 5: 提交**
```bash
cd /Users/kris/code/boss
git add tests/claw-backend.test.ts src/lib/execution/backends/claw-backend.ts
git commit -m "feat: add claw backend adapter"
```
### Task 4: 扩展 selector让 `claw` 仅在显式启用时参与候选
**Files:**
- Modify: `/Users/kris/code/boss/src/lib/execution/backend-selector.ts`
- Modify: `/Users/kris/code/boss/src/lib/execution/backends/openai-backend.ts`
- Modify: `/Users/kris/code/boss/src/lib/execution/backends/master-codex-node-backend.ts`
- Modify: `/Users/kris/code/boss/src/lib/execution/backends/aliyun-qwen-backend.ts`
- Create: `/Users/kris/code/boss/tests/claw-backend-selector.test.ts`
- [ ] **Step 1: 写失败测试,固定 selector 行为不被 claw 默认影响**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import { listExecutionBackendChoices } from "../src/lib/execution/backend-selector.ts";
test("claw 未启用时 selector 行为保持不变", () => {
delete process.env.BOSS_CLAW_ENABLED;
delete process.env.BOSS_CLAW_COMMAND;
const choices = listExecutionBackendChoices({
primary: { provider: "master_codex_node", status: "ready" },
backups: [{ provider: "openai_api", status: "ready" }],
});
assert.equal(choices[0]?.backendId, "master-codex-node");
assert.equal(choices.some((item) => item.backendId === "claw-runtime"), false);
});
test("claw 启用且显式选中时会进入候选列表顶部", () => {
process.env.BOSS_CLAW_ENABLED = "true";
process.env.BOSS_CLAW_COMMAND = "claw";
const choices = listExecutionBackendChoices({
primary: { provider: "master_codex_node", status: "ready" },
backups: [],
requestedBackendId: "claw-runtime",
requestKind: "master_agent_reply",
});
assert.equal(choices[0]?.backendId, "claw-runtime");
});
```
- [ ] **Step 2: 运行测试确认当前失败**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/claw-backend-selector.test.ts
```
Expected:
```text
FAIL
```
- [ ] **Step 3: 扩展 selector 输入与 claw 候选插入规则**
```ts
// /Users/kris/code/boss/src/lib/execution/backend-selector.ts
// 在现有 ExecutionBackendSelectionInput 上补:
requestedBackendId?: string;
requestKind?: "master_agent_reply" | "thread_reply" | "dispatch_execution" | "attachment_analysis";
// 新增 Claw backend descriptor并在 listExecutionBackendChoices 中:
// 1. 默认不插入 claw
// 2. 只有当 requestedBackendId === "claw-runtime" 且 requestKind 可支持 且 claw 配置完整时,才 push 到最前面
```
要求:
- 不改变当前不带 `requestedBackendId` 的所有既有行为
- 不让 `dispatch_execution` 进入 claw 候选
- 不让未启用的 claw 影响任何现有选择顺序
- [ ] **Step 4: 再跑 selector 测试**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/execution-backend-selector.test.ts tests/claw-backend-selector.test.ts
```
Expected:
```text
# pass
```
- [ ] **Step 5: 提交**
```bash
cd /Users/kris/code/boss
git add tests/claw-backend-selector.test.ts tests/execution-backend-selector.test.ts src/lib/execution/backend-selector.ts src/lib/execution/backends/openai-backend.ts src/lib/execution/backends/master-codex-node-backend.ts src/lib/execution/backends/aliyun-qwen-backend.ts
git commit -m "refactor: support optional claw backend selection"
```
### Task 5: 把 `boss-master-agent` 接到可选 claw backend但默认保持原主链
**Files:**
- Modify: `/Users/kris/code/boss/src/lib/boss-master-agent.ts`
- Test: `/Users/kris/code/boss/tests/master-agent-claw-backend.test.ts`
- [ ] **Step 1: 写失败测试,固定“默认不走 claw显式选中才走”**
```ts
import test from "node:test";
import assert from "node:assert/strict";
import { readState, saveAiAccount } from "../src/lib/boss-data.ts";
import { replyToMasterAgentUserMessage } from "../src/lib/boss-master-agent.ts";
test("master-agent 默认不因为 claw 启用而改变现有后端选择", async () => {
process.env.BOSS_CLAW_ENABLED = "true";
process.env.BOSS_CLAW_COMMAND = "claw";
const result = await replyToMasterAgentUserMessage({
requestMessageId: "msg-1",
requestText: "继续",
requestedBy: "Boss 超级管理员",
requestedByAccount: "17600003315",
});
assert.notEqual(result.accountId, "claw-runtime");
});
```
再补一条可测试 helper 场景:
```ts
// 新增一个 resolveMasterAgentBackendForTesting(...) helper
// 显式传入 requestedBackendId="claw-runtime" 时,返回 claw backend
```
- [ ] **Step 2: 运行测试确认失败**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/master-agent-claw-backend.test.ts
```
Expected:
```text
FAIL
```
- [ ] **Step 3: 在 boss-master-agent 中引入 requestedBackendId 但不改变默认行为**
实现要求:
- 当前默认调用路径不传 `requestedBackendId`
- 只在后续显式会话配置存在时,才把 `requestedBackendId="claw-runtime"` 传给 selector
- 本轮不新增前台 UI只把内部能力接好
- 如果 selector 选中了 `claw-runtime`,则调用 `CLAW_BACKEND.execute(...)`
- 如果 claw 返回 `failed`,继续沿用现有后端回退逻辑
- [ ] **Step 4: 再跑 master-agent 相关测试**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test tests/master-agent-claw-backend.test.ts tests/master-agent-openai-fallback.test.ts tests/master-agent-message-queue.test.ts tests/master-agent-config-resolution.test.ts
```
Expected:
```text
# pass
```
- [ ] **Step 5: 提交**
```bash
cd /Users/kris/code/boss
git add tests/master-agent-claw-backend.test.ts src/lib/boss-master-agent.ts
git commit -m "feat: wire optional claw backend into master agent"
```
### Task 6: 跑完整验证并补文档
**Files:**
- Modify: `/Users/kris/code/boss/README.md`
- Modify: `/Users/kris/code/boss/docs/architecture/current_runtime_and_deploy_status_cn.md`
- Modify: `/Users/kris/code/boss/docs/architecture/api_and_service_inventory_cn.md`
- [ ] **Step 1: 跑本轮完整验证矩阵**
Run:
```bash
cd /Users/kris/code/boss
npx --yes tsx --test \
tests/claw-backend-config.test.ts \
tests/claw-runner.test.ts \
tests/claw-backend.test.ts \
tests/claw-backend-selector.test.ts \
tests/master-agent-claw-backend.test.ts \
tests/execution-backend-selector.test.ts \
tests/master-agent-openai-fallback.test.ts \
tests/master-agent-message-queue.test.ts \
tests/master-agent-config-resolution.test.ts \
tests/remote-runtime-adapter.test.ts \
tests/dispatch-execution-result.test.ts
node --test tests/local-agent-codex-task-runner.test.mjs
npm run lint
npm run build
```
Expected:
```text
All tests pass
Next.js build succeeds
```
- [ ] **Step 2: 更新文档,明确 claw 已接入为可选 backend但默认不启用**
至少补充:
```md
- 当前 Boss 已新增 `ClawBackendAdapter`
- 当前 `claw-code` 仅作为可选单次执行 backend
- 当前默认主链仍不是 claw
- 当前 claw 只覆盖 `master_agent_reply / thread_reply`
- 当前未接群聊审批、设备导入、多租户账本
```
- [ ] **Step 3: 再跑一次 lint/build 确认最终状态一致**
Run:
```bash
cd /Users/kris/code/boss
npm run lint
npm run build
```
Expected:
```text
No new errors
```
- [ ] **Step 4: 提交**
```bash
cd /Users/kris/code/boss
git add README.md docs/architecture/current_runtime_and_deploy_status_cn.md docs/architecture/api_and_service_inventory_cn.md
git commit -m "docs: record optional claw backend support"
```
## Self-Review
### Spec coverage
- 已覆盖 `ClawBackendAdapter` 默认关闭
- 已覆盖最小接入范围只限单次执行
- 已覆盖 CLI/JSON 协议
- 已覆盖 selector 显式选择与失败回退
- 已覆盖文档同步
### Placeholder scan
- 无 TBD / TODO / implement later
- 每个任务都有明确文件、测试、命令和提交边界
### Type consistency
- `ClawBackendConfig / ExecutionRequest / ExecutionImmediateResult / requestedBackendId` 命名保持一致
- `claw-runtime` backendId 在任务间保持一致
## Execution Handoff
Plan complete and saved to `/Users/kris/code/boss/docs/superpowers/plans/2026-04-03-claw-backend-adapter.md`. Two execution options:
**1. Subagent-Driven (recommended)** - I dispatch a fresh subagent per task, review between tasks, fast iteration
**2. Inline Execution** - Execute tasks in this session using executing-plans, batch execution with checkpoints
**Which approach?**