feat: add claw backend adapter
This commit is contained in:
@@ -59,6 +59,7 @@ export async function POST(
|
||||
modelOverride?: unknown;
|
||||
reasoningEffortOverride?: unknown;
|
||||
promptOverride?: unknown;
|
||||
backendOverride?: unknown;
|
||||
};
|
||||
const hasModelOverride = Object.prototype.hasOwnProperty.call(payload, "modelOverride");
|
||||
const hasReasoningEffortOverride = Object.prototype.hasOwnProperty.call(
|
||||
@@ -66,9 +67,10 @@ export async function POST(
|
||||
"reasoningEffortOverride",
|
||||
);
|
||||
const hasPromptOverride = Object.prototype.hasOwnProperty.call(payload, "promptOverride");
|
||||
const allowedKeys = new Set(["modelOverride", "reasoningEffortOverride", "promptOverride"]);
|
||||
const hasBackendOverride = Object.prototype.hasOwnProperty.call(payload, "backendOverride");
|
||||
const allowedKeys = new Set(["modelOverride", "reasoningEffortOverride", "promptOverride", "backendOverride"]);
|
||||
const hasUnsupportedKeys = Object.keys(payload).some((key) => !allowedKeys.has(key));
|
||||
if ((!hasModelOverride && !hasReasoningEffortOverride && !hasPromptOverride) || hasUnsupportedKeys) {
|
||||
if ((!hasModelOverride && !hasReasoningEffortOverride && !hasPromptOverride && !hasBackendOverride) || hasUnsupportedKeys) {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_AGENT_CONTROLS_PAYLOAD" }, { status: 400 });
|
||||
}
|
||||
|
||||
@@ -90,6 +92,14 @@ export async function POST(
|
||||
if (hasPromptOverride && payload.promptOverride !== undefined && payload.promptOverride !== null && typeof payload.promptOverride !== "string") {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_OVERRIDE" }, { status: 400 });
|
||||
}
|
||||
if (
|
||||
hasBackendOverride &&
|
||||
payload.backendOverride !== undefined &&
|
||||
payload.backendOverride !== null &&
|
||||
payload.backendOverride !== "claw-runtime"
|
||||
) {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_BACKEND_OVERRIDE" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
const controls = await updateProjectAgentControls(
|
||||
@@ -98,6 +108,7 @@ export async function POST(
|
||||
...(hasModelOverride ? { modelOverride: payload.modelOverride } : {}),
|
||||
...(hasReasoningEffortOverride ? { reasoningEffortOverride: payload.reasoningEffortOverride } : {}),
|
||||
...(hasPromptOverride ? { promptOverride: payload.promptOverride } : {}),
|
||||
...(hasBackendOverride ? { backendOverride: payload.backendOverride } : {}),
|
||||
},
|
||||
session.account,
|
||||
);
|
||||
|
||||
@@ -71,12 +71,14 @@ export async function POST(
|
||||
const payload = body as {
|
||||
userPromptContent?: unknown;
|
||||
promptOverride?: unknown;
|
||||
backendOverride?: unknown;
|
||||
};
|
||||
const hasUserPromptContent = Object.prototype.hasOwnProperty.call(payload, "userPromptContent");
|
||||
const hasPromptOverride = Object.prototype.hasOwnProperty.call(payload, "promptOverride");
|
||||
const allowedKeys = new Set(["userPromptContent", "promptOverride"]);
|
||||
const hasBackendOverride = Object.prototype.hasOwnProperty.call(payload, "backendOverride");
|
||||
const allowedKeys = new Set(["userPromptContent", "promptOverride", "backendOverride"]);
|
||||
const hasUnsupportedKeys = Object.keys(payload).some((key) => !allowedKeys.has(key));
|
||||
if ((!hasUserPromptContent && !hasPromptOverride) || hasUnsupportedKeys) {
|
||||
if ((!hasUserPromptContent && !hasPromptOverride && !hasBackendOverride) || hasUnsupportedKeys) {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_PROFILE_PAYLOAD" }, { status: 400 });
|
||||
}
|
||||
if (hasUserPromptContent && payload.userPromptContent !== undefined && payload.userPromptContent !== null && typeof payload.userPromptContent !== "string") {
|
||||
@@ -85,6 +87,22 @@ export async function POST(
|
||||
if (hasPromptOverride && payload.promptOverride !== undefined && payload.promptOverride !== null && typeof payload.promptOverride !== "string") {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_PROMPT_OVERRIDE" }, { status: 400 });
|
||||
}
|
||||
if (
|
||||
hasBackendOverride
|
||||
&& payload.backendOverride !== undefined
|
||||
&& payload.backendOverride !== null
|
||||
&& typeof payload.backendOverride !== "string"
|
||||
) {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_BACKEND_OVERRIDE" }, { status: 400 });
|
||||
}
|
||||
if (
|
||||
hasBackendOverride
|
||||
&& typeof payload.backendOverride === "string"
|
||||
&& payload.backendOverride.trim() !== ""
|
||||
&& payload.backendOverride.trim() !== "claw-runtime"
|
||||
) {
|
||||
return NextResponse.json({ ok: false, message: "INVALID_BACKEND_OVERRIDE" }, { status: 400 });
|
||||
}
|
||||
|
||||
try {
|
||||
if (hasUserPromptContent) {
|
||||
@@ -96,9 +114,10 @@ export async function POST(
|
||||
}
|
||||
}
|
||||
|
||||
if (hasPromptOverride) {
|
||||
if (hasPromptOverride || hasBackendOverride) {
|
||||
await updateProjectAgentControls(projectId, {
|
||||
promptOverride: payload.promptOverride,
|
||||
...(hasPromptOverride ? { promptOverride: payload.promptOverride } : {}),
|
||||
...(hasBackendOverride ? { backendOverride: payload.backendOverride } : {}),
|
||||
}, session.account);
|
||||
}
|
||||
|
||||
|
||||
@@ -167,6 +167,9 @@ export function MasterAgentPromptMemoryClient({
|
||||
projectControls?.reasoningEffortOverride ?? "",
|
||||
);
|
||||
const [promptOverride, setPromptOverride] = useState(projectControls?.promptOverride ?? "");
|
||||
const [backendOverride, setBackendOverride] = useState(
|
||||
projectControls?.backendOverride === "claw-runtime" ? "claw-runtime" : "",
|
||||
);
|
||||
const [newMemory, setNewMemory] = useState<MemoryDraft>(makeNewMemoryDraft());
|
||||
const [memoryDrafts, setMemoryDrafts] = useState<Record<string, MemoryDraft>>(() => {
|
||||
const next: Record<string, MemoryDraft> = {};
|
||||
@@ -246,6 +249,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
modelOverride: modelOverride.trim() || null,
|
||||
reasoningEffortOverride: reasoningEffortOverride.trim() || null,
|
||||
promptOverride: promptOverride.trim() || null,
|
||||
backendOverride: backendOverride.trim() || null,
|
||||
}),
|
||||
});
|
||||
const result = (await response.json()) as { ok: boolean; message?: string };
|
||||
@@ -402,7 +406,7 @@ export function MasterAgentPromptMemoryClient({
|
||||
当前对话
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid gap-3 md:grid-cols-2">
|
||||
<div className="grid gap-3 md:grid-cols-3">
|
||||
<label id={anchors.model.split("#")[1]} className="space-y-1 scroll-mt-4">
|
||||
<div className="text-[12px] text-[#8C8C8C]">模型</div>
|
||||
<select
|
||||
@@ -429,6 +433,17 @@ export function MasterAgentPromptMemoryClient({
|
||||
<option value="high">high</option>
|
||||
</select>
|
||||
</label>
|
||||
<label className="space-y-1">
|
||||
<div className="text-[12px] text-[#8C8C8C]">执行后端</div>
|
||||
<select
|
||||
value={backendOverride}
|
||||
onChange={(event) => setBackendOverride(event.target.value)}
|
||||
className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none"
|
||||
>
|
||||
<option value="">默认</option>
|
||||
<option value="claw-runtime">Claw Runtime</option>
|
||||
</select>
|
||||
</label>
|
||||
</div>
|
||||
<TextArea
|
||||
label="当前对话附加提示词"
|
||||
|
||||
@@ -378,6 +378,7 @@ export interface ProjectAgentControls {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: ReasoningEffort;
|
||||
promptOverride?: string;
|
||||
backendOverride?: "claw-runtime";
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
@@ -1715,6 +1716,16 @@ function parseReasoningEffortOverride(value: unknown) {
|
||||
return { kind: "set" as const, value };
|
||||
}
|
||||
|
||||
function parseBackendOverride(value: unknown) {
|
||||
if (value === undefined || value === null) {
|
||||
return { kind: "clear" as const };
|
||||
}
|
||||
if (value !== "claw-runtime") {
|
||||
return { kind: "invalid" as const };
|
||||
}
|
||||
return { kind: "set" as const, value: "claw-runtime" as const };
|
||||
}
|
||||
|
||||
function normalizeStringSet(values: string[]) {
|
||||
return dedupeStrings(values.map((value) => value.trim()).filter(Boolean)).sort((a, b) => a.localeCompare(b));
|
||||
}
|
||||
@@ -2144,8 +2155,9 @@ function normalizeProjectAgentControls(
|
||||
? raw.reasoningEffortOverride
|
||||
: undefined;
|
||||
const promptOverride = trimToDefined(raw?.promptOverride);
|
||||
const backendOverride = raw?.backendOverride === "claw-runtime" ? raw.backendOverride : undefined;
|
||||
|
||||
if (!modelOverride && !reasoningEffortOverride && !promptOverride) {
|
||||
if (!modelOverride && !reasoningEffortOverride && !promptOverride && !backendOverride) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@@ -2153,6 +2165,7 @@ function normalizeProjectAgentControls(
|
||||
modelOverride,
|
||||
reasoningEffortOverride,
|
||||
promptOverride,
|
||||
backendOverride,
|
||||
updatedAt: raw?.updatedAt ?? nowIso(),
|
||||
};
|
||||
}
|
||||
@@ -3610,6 +3623,7 @@ export async function updateProjectAgentControls(
|
||||
modelOverride?: unknown;
|
||||
reasoningEffortOverride?: unknown;
|
||||
promptOverride?: unknown;
|
||||
backendOverride?: unknown;
|
||||
},
|
||||
account?: string,
|
||||
) {
|
||||
@@ -3626,6 +3640,9 @@ export async function updateProjectAgentControls(
|
||||
const promptOverrideInput = Object.prototype.hasOwnProperty.call(payload, "promptOverride")
|
||||
? parseControlTextOverride(payload.promptOverride)
|
||||
: { kind: "preserve" as const };
|
||||
const backendOverrideInput = Object.prototype.hasOwnProperty.call(payload, "backendOverride")
|
||||
? parseBackendOverride(payload.backendOverride)
|
||||
: { kind: "preserve" as const };
|
||||
if (modelOverrideInput.kind === "invalid") {
|
||||
throw new Error("INVALID_MODEL_OVERRIDE");
|
||||
}
|
||||
@@ -3635,6 +3652,9 @@ export async function updateProjectAgentControls(
|
||||
if (promptOverrideInput.kind === "invalid") {
|
||||
throw new Error("INVALID_PROMPT_OVERRIDE");
|
||||
}
|
||||
if (backendOverrideInput.kind === "invalid") {
|
||||
throw new Error("INVALID_BACKEND_OVERRIDE");
|
||||
}
|
||||
|
||||
return mutateStateIfChanged((state) => {
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
@@ -3661,14 +3681,22 @@ export async function updateProjectAgentControls(
|
||||
: promptOverrideInput.kind === "clear"
|
||||
? undefined
|
||||
: currentControls?.promptOverride;
|
||||
const backendOverride =
|
||||
backendOverrideInput.kind === "set"
|
||||
? backendOverrideInput.value
|
||||
: backendOverrideInput.kind === "clear"
|
||||
? undefined
|
||||
: currentControls?.backendOverride;
|
||||
|
||||
const currentModelOverride = currentControls?.modelOverride;
|
||||
const currentReasoningEffortOverride = currentControls?.reasoningEffortOverride;
|
||||
const currentPromptOverride = currentControls?.promptOverride;
|
||||
const currentBackendOverride = currentControls?.backendOverride;
|
||||
if (
|
||||
currentModelOverride === modelOverride &&
|
||||
currentReasoningEffortOverride === reasoningEffortOverride &&
|
||||
currentPromptOverride === promptOverride
|
||||
currentPromptOverride === promptOverride &&
|
||||
currentBackendOverride === backendOverride
|
||||
) {
|
||||
return { result: currentControls, changed: false };
|
||||
}
|
||||
@@ -3677,6 +3705,7 @@ export async function updateProjectAgentControls(
|
||||
modelOverride,
|
||||
reasoningEffortOverride,
|
||||
promptOverride,
|
||||
backendOverride,
|
||||
updatedAt: nowIso(),
|
||||
} satisfies ProjectAgentControls;
|
||||
const normalizedControls = normalizeProjectAgentControls(nextControls) ?? null;
|
||||
|
||||
@@ -29,6 +29,11 @@ import type {
|
||||
ReasoningEffort,
|
||||
} from "@/lib/boss-data";
|
||||
import { canInlineAttachmentText, extractAttachmentTextExcerpt } from "@/lib/boss-attachments";
|
||||
import {
|
||||
CLAW_BACKEND_ID,
|
||||
createClawBackend,
|
||||
getClawBackendSelectionState,
|
||||
} from "@/lib/execution/backends/claw-backend";
|
||||
import { listExecutionBackendChoices, selectExecutionBackend } from "@/lib/execution/backend-selector";
|
||||
import { resolveRuntimeRelevantMemories } from "@/lib/execution/memory-resolver";
|
||||
import type { RelevantMemory } from "@/lib/execution/memory-resolver";
|
||||
@@ -44,6 +49,7 @@ import {
|
||||
type MasterAgentReplyState = "queued" | "running" | "completed";
|
||||
const OPENAI_MASTER_AGENT_DEVICE_ID = "master-agent-openai";
|
||||
const ALIYUN_QWEN_DEVICE_ID = "master-agent-aliyun-qwen";
|
||||
const CLAW_RUNTIME_DEVICE_ID = "master-agent-claw";
|
||||
|
||||
type ApiCompatibleProvider = Extract<AiProvider, "openai_api" | "aliyun_qwen_api">;
|
||||
|
||||
@@ -144,6 +150,7 @@ function buildAgentControlsDigest(agentControls?: ProjectAgentControls | null) {
|
||||
"当前对话覆盖:",
|
||||
`model=${agentControls.modelOverride ?? "默认"}`,
|
||||
`reasoning=${agentControls.reasoningEffortOverride ?? "默认"}`,
|
||||
`backend=${agentControls.backendOverride ?? "默认"}`,
|
||||
`prompt=${agentControls.promptOverride ? "已配置" : "默认"}`,
|
||||
].join(" ");
|
||||
}
|
||||
@@ -456,7 +463,7 @@ interface ApiExecutionCandidate {
|
||||
}
|
||||
|
||||
async function buildApiExecutionCandidates(params: {
|
||||
backendChoices: Array<{ provider: AiProvider }>;
|
||||
backendChoices: Array<{ backendId?: string; provider?: AiProvider }>;
|
||||
runtimeAccount: AiAccount;
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
}) {
|
||||
@@ -464,7 +471,7 @@ async function buildApiExecutionCandidates(params: {
|
||||
const seenAccountIds = new Set<string>();
|
||||
|
||||
for (const backend of params.backendChoices) {
|
||||
if (!isApiCompatibleProvider(backend.provider)) {
|
||||
if (!backend.provider || !isApiCompatibleProvider(backend.provider)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -492,7 +499,7 @@ async function buildApiExecutionCandidates(params: {
|
||||
}
|
||||
|
||||
async function resolveMasterNodeExecutionCandidate(params: {
|
||||
backendChoices: Array<{ backendId: string; provider: AiProvider }>;
|
||||
backendChoices: Array<{ backendId: string; provider?: AiProvider }>;
|
||||
runtimeAccount: AiAccount;
|
||||
}) {
|
||||
const wantsMasterNode = params.backendChoices.some((backend) => backend.backendId === "master-codex-node");
|
||||
@@ -857,6 +864,106 @@ async function enqueueOpenAiMasterAgentReply(params: {
|
||||
return queuedReply;
|
||||
}
|
||||
|
||||
async function enqueueClawMasterAgentReply(params: {
|
||||
requestMessageId?: string;
|
||||
requestText: string;
|
||||
requestedBy: string;
|
||||
requestedByAccount: string;
|
||||
executionPrompt: string;
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
apiFallbackCandidates: ApiExecutionCandidate[];
|
||||
masterFallback?: {
|
||||
account: AiAccount;
|
||||
executionPrompt: string;
|
||||
} | null;
|
||||
}) {
|
||||
const task = await queueMasterAgentTask({
|
||||
requestMessageId: params.requestMessageId ?? "master-agent-manual",
|
||||
requestText: params.requestText,
|
||||
executionPrompt: params.executionPrompt,
|
||||
requestedBy: params.requestedBy,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
deviceId: CLAW_RUNTIME_DEVICE_ID,
|
||||
accountId: CLAW_BACKEND_ID,
|
||||
accountLabel: "Claw Runtime",
|
||||
});
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
void (async () => {
|
||||
const currentTask = await getMasterAgentTask(task.taskId);
|
||||
if (!currentTask || currentTask.status !== "queued") {
|
||||
return;
|
||||
}
|
||||
|
||||
const backend = createClawBackend();
|
||||
const result = await backend.execute({
|
||||
kind: "master_agent_reply",
|
||||
projectId: "master-agent",
|
||||
requestMessageId: params.requestMessageId ?? "master-agent-manual",
|
||||
body: params.requestText,
|
||||
executionPrompt: params.executionPrompt,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
requestedByLabel: params.requestedBy,
|
||||
taskId: task.taskId,
|
||||
modelOverride: params.agentControls?.modelOverride,
|
||||
reasoningEffortOverride: params.agentControls?.reasoningEffortOverride,
|
||||
});
|
||||
|
||||
if (result.status === "completed") {
|
||||
await completeMasterAgentTask({
|
||||
taskId: task.taskId,
|
||||
deviceId: CLAW_RUNTIME_DEVICE_ID,
|
||||
status: "completed",
|
||||
replyBody: result.output,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.status !== "failed") {
|
||||
await completeMasterAgentTask({
|
||||
taskId: task.taskId,
|
||||
deviceId: CLAW_RUNTIME_DEVICE_ID,
|
||||
status: "failed",
|
||||
errorMessage: "Claw Runtime 返回了当前链路尚不支持的状态。",
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (params.apiFallbackCandidates.length > 0 || params.masterFallback) {
|
||||
await queueAndStartOpenAiMasterAgentReply({
|
||||
candidates: params.apiFallbackCandidates,
|
||||
taskId: task.taskId,
|
||||
requestText: params.requestText,
|
||||
reasoningEffort: params.agentControls?.reasoningEffortOverride || "medium",
|
||||
agentControls: params.agentControls,
|
||||
masterFallback: params.masterFallback,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
await completeMasterAgentTask({
|
||||
taskId: task.taskId,
|
||||
deviceId: CLAW_RUNTIME_DEVICE_ID,
|
||||
status: "failed",
|
||||
errorMessage: normalizeClawExecutionError(result.error),
|
||||
});
|
||||
})();
|
||||
}, 0);
|
||||
timer.unref?.();
|
||||
|
||||
return {
|
||||
ok: true as const,
|
||||
accountId: CLAW_BACKEND_ID,
|
||||
taskId: task.taskId,
|
||||
masterReplyState: "queued" as const,
|
||||
task: {
|
||||
taskId: task.taskId,
|
||||
taskType: "conversation_reply" as const,
|
||||
status: "queued" as const,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export async function probeApiCompatibleAccount(params: {
|
||||
provider: ApiCompatibleProvider;
|
||||
apiKey: string;
|
||||
@@ -958,6 +1065,61 @@ function buildMasterCodexNodePrompt(
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeClawExecutionError(message: string) {
|
||||
const trimmed = message.trim();
|
||||
if (!trimmed) {
|
||||
return "Claw Runtime 当前执行失败。";
|
||||
}
|
||||
if (trimmed.length <= 240) {
|
||||
return trimmed;
|
||||
}
|
||||
return `${trimmed.slice(0, 237)}...`;
|
||||
}
|
||||
|
||||
async function replyViaClawBackend(params: {
|
||||
requestMessageId?: string;
|
||||
requestText: string;
|
||||
requestedBy: string;
|
||||
requestedByAccount: string;
|
||||
executionPrompt: string;
|
||||
agentControls?: ProjectAgentControls | null;
|
||||
}) {
|
||||
const backend = createClawBackend();
|
||||
const result = await backend.execute({
|
||||
kind: "master_agent_reply",
|
||||
projectId: "master-agent",
|
||||
requestMessageId: params.requestMessageId ?? "master-agent-manual",
|
||||
body: params.requestText,
|
||||
executionPrompt: params.executionPrompt,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
requestedByLabel: params.requestedBy,
|
||||
modelOverride: params.agentControls?.modelOverride,
|
||||
reasoningEffortOverride: params.agentControls?.reasoningEffortOverride,
|
||||
});
|
||||
|
||||
if (result.status === "completed") {
|
||||
await appendMasterAgentSystemReply(result.output, "主 Agent · Claw Runtime");
|
||||
return {
|
||||
ok: true as const,
|
||||
accountId: CLAW_BACKEND_ID,
|
||||
};
|
||||
}
|
||||
|
||||
if (result.status !== "failed") {
|
||||
return {
|
||||
ok: false as const,
|
||||
reason: "CLAW_EXEC_FAILED",
|
||||
message: "Claw Runtime 返回了当前链路尚不支持的状态。",
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: false as const,
|
||||
reason: "CLAW_EXEC_FAILED",
|
||||
message: normalizeClawExecutionError(result.error),
|
||||
};
|
||||
}
|
||||
|
||||
function summarizeDispatchRequest(requestText: string) {
|
||||
const compact = requestText.trim().replace(/\s+/g, " ");
|
||||
if (!compact) {
|
||||
@@ -1587,6 +1749,9 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
provider: account.provider,
|
||||
status: account.status,
|
||||
})),
|
||||
requestKind: "master_agent_reply" as const,
|
||||
requestedBackendId: executionConfig.agentControls?.backendOverride,
|
||||
claw: getClawBackendSelectionState(),
|
||||
};
|
||||
const selectedBackend = await selectExecutionBackend(backendSelectionInput);
|
||||
const backendChoices = listExecutionBackendChoices(backendSelectionInput);
|
||||
@@ -1701,6 +1866,24 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
};
|
||||
|
||||
if (params.mode === "enqueue") {
|
||||
if (selectedBackend.backendId === CLAW_BACKEND_ID) {
|
||||
return enqueueClawMasterAgentReply({
|
||||
requestMessageId: params.requestMessageId,
|
||||
requestText: params.requestText,
|
||||
requestedBy: params.requestedBy,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
executionPrompt: masterExecutionPrompt,
|
||||
agentControls,
|
||||
apiFallbackCandidates: apiExecutionCandidates,
|
||||
masterFallback: hasMasterFallback && selectedMasterAccount
|
||||
? {
|
||||
account: selectedMasterAccount,
|
||||
executionPrompt: masterExecutionPrompt,
|
||||
}
|
||||
: null,
|
||||
});
|
||||
}
|
||||
|
||||
if (selectedBackend.backendId === "master-codex-node") {
|
||||
return runMasterNodeExecution();
|
||||
}
|
||||
@@ -1729,6 +1912,27 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBackend.backendId === CLAW_BACKEND_ID) {
|
||||
const clawReply = await replyViaClawBackend({
|
||||
requestMessageId: params.requestMessageId,
|
||||
requestText: params.requestText,
|
||||
requestedBy: params.requestedBy,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
executionPrompt: masterExecutionPrompt,
|
||||
agentControls,
|
||||
});
|
||||
if (clawReply.ok) {
|
||||
return clawReply;
|
||||
}
|
||||
if (apiExecutionCandidates.length === 0 && !(hasMasterFallback && selectedMasterAccount)) {
|
||||
await appendMasterAgentSystemReply(
|
||||
`我已经收到你的消息,但 Claw Runtime 当前执行失败:${clawReply.message}。请检查 Claw 可执行入口,或先切回其他主控后再试。`,
|
||||
"主 Agent · Claw Runtime",
|
||||
);
|
||||
return clawReply;
|
||||
}
|
||||
}
|
||||
|
||||
if (selectedBackend.backendId === "master-codex-node") {
|
||||
return runMasterNodeExecution();
|
||||
}
|
||||
|
||||
@@ -3,11 +3,17 @@ import {
|
||||
ALIYUN_QWEN_BACKEND,
|
||||
isReadyAliyunQwenBackend,
|
||||
} from "@/lib/execution/backends/aliyun-qwen-backend";
|
||||
import {
|
||||
CLAW_BACKEND,
|
||||
type ClawBackendSelectionState,
|
||||
isClawRequestKindSupported,
|
||||
} from "@/lib/execution/backends/claw-backend";
|
||||
import {
|
||||
MASTER_CODEX_NODE_BACKEND,
|
||||
isReadyMasterCodexNodeBackend,
|
||||
} from "@/lib/execution/backends/master-codex-node-backend";
|
||||
import { OPENAI_BACKEND, isReadyOpenAiBackend } from "@/lib/execution/backends/openai-backend";
|
||||
import type { ExecutionRequestKind } from "@/lib/execution/types";
|
||||
|
||||
export interface ExecutionBackendSelectionInput {
|
||||
primary: {
|
||||
@@ -18,9 +24,13 @@ export interface ExecutionBackendSelectionInput {
|
||||
provider: AiProvider;
|
||||
status: AiAccountStatus;
|
||||
}>;
|
||||
requestKind?: ExecutionRequestKind;
|
||||
requestedBackendId?: string;
|
||||
claw?: ClawBackendSelectionState;
|
||||
}
|
||||
|
||||
export type ExecutionBackendChoice =
|
||||
| typeof CLAW_BACKEND
|
||||
| typeof MASTER_CODEX_NODE_BACKEND
|
||||
| typeof OPENAI_BACKEND
|
||||
| typeof ALIYUN_QWEN_BACKEND;
|
||||
@@ -39,6 +49,14 @@ function resolveBackendByProvider(provider: AiProvider): ExecutionBackendChoice
|
||||
}
|
||||
|
||||
function isReadyBackend(choice: ExecutionBackendChoice, input: ExecutionBackendSelectionInput) {
|
||||
if (choice.backendId === CLAW_BACKEND.backendId) {
|
||||
const requestKind = input.requestKind;
|
||||
if (!input.claw?.enabled || !requestKind) {
|
||||
return false;
|
||||
}
|
||||
return isClawRequestKindSupported(requestKind);
|
||||
}
|
||||
|
||||
const candidates = [
|
||||
...(input.primary.provider === choice.provider ? [input.primary] : []),
|
||||
...input.backups.filter((item) => item.provider === choice.provider),
|
||||
@@ -79,6 +97,13 @@ export function listExecutionBackendChoices(
|
||||
seen.add(backend.backendId);
|
||||
};
|
||||
|
||||
if (
|
||||
input.requestedBackendId === CLAW_BACKEND.backendId &&
|
||||
isReadyBackend(CLAW_BACKEND, input)
|
||||
) {
|
||||
pushBackend(CLAW_BACKEND);
|
||||
}
|
||||
|
||||
if (input.primary.status === "ready") {
|
||||
pushBackend(primaryBackend);
|
||||
}
|
||||
|
||||
103
src/lib/execution/backends/claw-backend.ts
Normal file
103
src/lib/execution/backends/claw-backend.ts
Normal file
@@ -0,0 +1,103 @@
|
||||
import type { ExecutionBackend } from "@/lib/execution/execution-backend";
|
||||
import type {
|
||||
ExecutionBackendDescription,
|
||||
ExecutionImmediateResult,
|
||||
ExecutionRequest,
|
||||
ExecutionRequestKind,
|
||||
} from "@/lib/execution/types";
|
||||
import {
|
||||
getClawBackendConfig,
|
||||
isClawBackendConfigured,
|
||||
type ClawBackendConfig,
|
||||
} from "@/lib/execution/backends/claw-config";
|
||||
import { runClawCommand } from "@/lib/execution/backends/claw-runner";
|
||||
|
||||
export const CLAW_BACKEND_ID = "claw-runtime";
|
||||
|
||||
export const CLAW_BACKEND = {
|
||||
backendId: CLAW_BACKEND_ID,
|
||||
label: "Claw Runtime",
|
||||
mode: "local",
|
||||
} as const satisfies ExecutionBackendDescription;
|
||||
|
||||
const SUPPORTED_CLAW_KINDS = new Set<ExecutionRequestKind>([
|
||||
"master_agent_reply",
|
||||
"thread_reply",
|
||||
]);
|
||||
|
||||
type ClawRunnerInput = Parameters<typeof runClawCommand>[0];
|
||||
type ClawRunner = (input: ClawRunnerInput) => Promise<ExecutionImmediateResult>;
|
||||
|
||||
export interface ClawBackendSelectionState {
|
||||
enabled: boolean;
|
||||
supportsKinds: ExecutionRequestKind[];
|
||||
}
|
||||
|
||||
function createFailedResult(error: string): ExecutionImmediateResult {
|
||||
return {
|
||||
status: "failed",
|
||||
backendId: CLAW_BACKEND_ID,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
export function isClawRequestKindSupported(kind: ExecutionRequestKind) {
|
||||
return SUPPORTED_CLAW_KINDS.has(kind);
|
||||
}
|
||||
|
||||
export function getClawBackendSelectionState(
|
||||
config: ClawBackendConfig = getClawBackendConfig(),
|
||||
): ClawBackendSelectionState {
|
||||
return {
|
||||
enabled: isClawBackendConfigured(config),
|
||||
supportsKinds: [...SUPPORTED_CLAW_KINDS],
|
||||
};
|
||||
}
|
||||
|
||||
function buildClawPayload(input: ExecutionRequest, config: ClawBackendConfig) {
|
||||
return {
|
||||
kind: input.kind,
|
||||
projectId: input.projectId,
|
||||
requestMessageId: input.requestMessageId,
|
||||
body: input.body,
|
||||
executionPrompt: input.executionPrompt ?? input.body,
|
||||
model: input.modelOverride ?? config.defaultModel,
|
||||
reasoningEffort: input.reasoningEffortOverride ?? "medium",
|
||||
...(input.targetProjectId ? { targetProjectId: input.targetProjectId } : {}),
|
||||
...(input.targetThreadId ? { targetThreadId: input.targetThreadId } : {}),
|
||||
...(input.requestedByAccount ? { requestedByAccount: input.requestedByAccount } : {}),
|
||||
...(input.requestedByLabel ? { requestedByLabel: input.requestedByLabel } : {}),
|
||||
...(input.taskId ? { taskId: input.taskId } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
export function createClawBackend(options?: {
|
||||
config?: ClawBackendConfig;
|
||||
runner?: ClawRunner;
|
||||
}): ExecutionBackend {
|
||||
const config = options?.config ?? getClawBackendConfig();
|
||||
const runner = options?.runner ?? runClawCommand;
|
||||
|
||||
return {
|
||||
backendId: CLAW_BACKEND_ID,
|
||||
async canHandle(input) {
|
||||
return isClawBackendConfigured(config) && isClawRequestKindSupported(input.kind);
|
||||
},
|
||||
async execute(input) {
|
||||
const canHandle = await this.canHandle(input);
|
||||
if (!canHandle) {
|
||||
return createFailedResult("CLAW_BACKEND_NOT_AVAILABLE");
|
||||
}
|
||||
return runner({
|
||||
config,
|
||||
payload: buildClawPayload(input, config),
|
||||
});
|
||||
},
|
||||
async describe() {
|
||||
return CLAW_BACKEND;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export const CLAW_BACKEND_ADAPTER = createClawBackend();
|
||||
export const createClawBackendForTesting = createClawBackend;
|
||||
42
src/lib/execution/backends/claw-config.ts
Normal file
42
src/lib/execution/backends/claw-config.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
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);
|
||||
}
|
||||
|
||||
function parseTimeoutMs(value: string | undefined) {
|
||||
const parsed = Number.parseInt(value || "", 10);
|
||||
return Number.isFinite(parsed) && parsed > 0 ? parsed : 45000;
|
||||
}
|
||||
|
||||
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: parseTimeoutMs(process.env.BOSS_CLAW_TIMEOUT_MS),
|
||||
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;
|
||||
118
src/lib/execution/backends/claw-runner.ts
Normal file
118
src/lib/execution/backends/claw-runner.ts
Normal file
@@ -0,0 +1,118 @@
|
||||
import { spawn, type ChildProcessWithoutNullStreams } from "node:child_process";
|
||||
|
||||
import type { ClawBackendConfig } from "@/lib/execution/backends/claw-config";
|
||||
import type { ExecutionImmediateResult } from "@/lib/execution/types";
|
||||
|
||||
const CLAW_BACKEND_ID = "claw-runtime";
|
||||
|
||||
function createFailedResult(error: string): ExecutionImmediateResult {
|
||||
return {
|
||||
status: "failed",
|
||||
backendId: CLAW_BACKEND_ID,
|
||||
error,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeClawProcessResult(input: {
|
||||
exitCode: number;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
}): ExecutionImmediateResult {
|
||||
if (input.exitCode !== 0) {
|
||||
return createFailedResult(input.stderr.trim() || `CLAW_EXIT_${input.exitCode}`);
|
||||
}
|
||||
|
||||
let parsed: unknown;
|
||||
try {
|
||||
parsed = JSON.parse(input.stdout);
|
||||
} catch {
|
||||
return createFailedResult("INVALID_CLAW_RESPONSE");
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
(parsed as { status?: unknown }).status === "completed" &&
|
||||
typeof (parsed as { output?: unknown }).output === "string"
|
||||
) {
|
||||
return {
|
||||
status: "completed",
|
||||
backendId: CLAW_BACKEND_ID,
|
||||
output: (parsed as { output: string }).output,
|
||||
};
|
||||
}
|
||||
|
||||
if (
|
||||
typeof parsed === "object" &&
|
||||
parsed !== null &&
|
||||
(parsed as { status?: unknown }).status === "failed" &&
|
||||
typeof (parsed as { error?: unknown }).error === "string"
|
||||
) {
|
||||
return createFailedResult((parsed as { error: string }).error);
|
||||
}
|
||||
|
||||
return createFailedResult("INVALID_CLAW_RESPONSE");
|
||||
}
|
||||
|
||||
export async function runClawCommand(input: {
|
||||
config: ClawBackendConfig;
|
||||
payload: unknown;
|
||||
}): Promise<ExecutionImmediateResult> {
|
||||
const command = input.config.command;
|
||||
if (!command) {
|
||||
return createFailedResult("CLAW_COMMAND_NOT_CONFIGURED");
|
||||
}
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const child: ChildProcessWithoutNullStreams = spawn(command, input.config.args, {
|
||||
cwd: input.config.cwd,
|
||||
env: process.env,
|
||||
stdio: ["pipe", "pipe", "pipe"] as const,
|
||||
});
|
||||
|
||||
let stdout = "";
|
||||
let stderr = "";
|
||||
let settled = false;
|
||||
|
||||
const finish = (result: ExecutionImmediateResult) => {
|
||||
if (settled) {
|
||||
return;
|
||||
}
|
||||
settled = true;
|
||||
clearTimeout(timer);
|
||||
resolve(result);
|
||||
};
|
||||
|
||||
const timer = setTimeout(() => {
|
||||
child.kill("SIGKILL");
|
||||
finish(createFailedResult("CLAW_TIMEOUT"));
|
||||
}, input.config.timeoutMs);
|
||||
|
||||
child.stdout.on("data", (chunk: Buffer | string) => {
|
||||
stdout += String(chunk);
|
||||
});
|
||||
|
||||
child.stderr.on("data", (chunk: Buffer | string) => {
|
||||
stderr += String(chunk);
|
||||
});
|
||||
|
||||
child.on("error", (error: Error) => {
|
||||
finish(createFailedResult(error.message));
|
||||
});
|
||||
|
||||
child.on("close", (code: number | null) => {
|
||||
finish(
|
||||
normalizeClawProcessResult({
|
||||
exitCode: code ?? 1,
|
||||
stdout,
|
||||
stderr,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
child.stdin.end(JSON.stringify(input.payload ?? null));
|
||||
});
|
||||
}
|
||||
|
||||
export const runClawCommandForTesting = runClawCommand;
|
||||
export const createClawProcessResultForTesting = normalizeClawProcessResult;
|
||||
@@ -11,8 +11,10 @@ export interface ExecutionRequest {
|
||||
projectId: string;
|
||||
requestMessageId: string;
|
||||
body: string;
|
||||
executionPrompt?: string;
|
||||
requestedByAccount?: string;
|
||||
requestedByLabel?: string;
|
||||
requestedBackendId?: string;
|
||||
taskId?: string;
|
||||
targetThreadId?: string;
|
||||
targetProjectId?: string;
|
||||
@@ -55,8 +57,10 @@ export function createExecutionRequest(input: ExecutionRequest): ExecutionReques
|
||||
projectId: input.projectId,
|
||||
requestMessageId: input.requestMessageId,
|
||||
body: input.body,
|
||||
executionPrompt: input.executionPrompt ?? undefined,
|
||||
requestedByAccount: input.requestedByAccount ?? undefined,
|
||||
requestedByLabel: input.requestedByLabel ?? undefined,
|
||||
requestedBackendId: input.requestedBackendId ?? undefined,
|
||||
taskId: input.taskId ?? undefined,
|
||||
targetThreadId: input.targetThreadId ?? undefined,
|
||||
targetProjectId: input.targetProjectId ?? undefined,
|
||||
|
||||
Reference in New Issue
Block a user