From 8e2350e89d69774414e8a381c03d451108236527 Mon Sep 17 00:00:00 2001 From: kris Date: Fri, 3 Apr 2026 02:11:41 +0800 Subject: [PATCH] feat: gate claw runtime selection by availability --- README.md | 3 +- .../hyzq/boss/MasterAgentPromptActivity.java | 42 +++- .../boss/MasterAgentPromptActivityTest.java | 37 ++++ .../launchd/com.hyzq.boss.local-agent.plist | 2 +- .../api_and_service_inventory_cn.md | 4 +- .../current_runtime_and_deploy_status_cn.md | 5 +- scripts/install-local-launchagent.sh | 1 + .../[projectId]/agent-controls/route.ts | 24 ++- .../[projectId]/prompt-profile/route.ts | 27 ++- src/app/me/master-agent/page.tsx | 5 +- .../master-agent-prompt-memory-client.tsx | 33 +++- src/lib/boss-master-agent.ts | 3 +- src/lib/execution/backend-selector.ts | 2 +- src/lib/execution/backends/claw-backend.ts | 11 +- src/lib/execution/backends/claw-config.ts | 144 ++++++++++++++ tests/claw-backend-config.test.ts | 49 +++++ tests/execution-backend-selector.test.ts | 16 ++ tests/master-agent-chat-controls.test.ts | 183 ++++++++++-------- ...master-agent-prompts-memory-routes.test.ts | 96 +++++++-- 19 files changed, 564 insertions(+), 123 deletions(-) diff --git a/README.md b/README.md index e8aa286..7dc0084 100644 --- a/README.md +++ b/README.md @@ -57,7 +57,8 @@ - `GET http://127.0.0.1:3000/api/v1/user/ota/package` 正常,当前会返回最新 APK 包 - 当前这台开发机的 `launchd` 常驻 `local-agent` 已恢复:`GET http://127.0.0.1:4317/health` 现在可在数十毫秒内返回,且在手动 heartbeat 执行期间仍能正常回包 - 当前 Boss 已新增 `src/lib/execution/` 执行底座抽象层;当前生产主链仍然沿用 `local-agent -> codex exec resume`,只是执行责任已开始通过 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现收束 -- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 并在 `master-agent` 当前对话里显式选择 `claw-runtime` 时才会参与执行候选 +- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话里才会出现并允许选择 `claw-runtime` +- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在前台显示明确原因 - 当前 `oh-my-codex` 仍未正式接入生产执行链;当前状态是 orchestration-ready,后续将通过独立 adapter 接入 - 当前仓库已自带一个本地 smoke runtime:`scripts/claw-runtime-smoke.mjs`。在还没有真实 `claw-code` 可执行文件时,可以先用它验证 `ClawBackendAdapter -> backendOverride -> 异步回流` 整条链 - `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill diff --git a/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java b/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java index 14cbe2f..b4a1d79 100644 --- a/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java @@ -15,9 +15,10 @@ import androidx.annotation.Nullable; import org.json.JSONObject; +import java.util.ArrayList; +import java.util.List; + public class MasterAgentPromptActivity extends BossScreenActivity { - private static final String[] BACKEND_OVERRIDE_VALUES = {"", "claw-runtime"}; - private static final String[] BACKEND_OVERRIDE_LABELS = {"默认", "Claw Runtime"}; public static final String EXTRA_PROJECT_ID = "project_id"; public static final String EXTRA_PROJECT_NAME = "project_name"; @@ -31,6 +32,9 @@ public class MasterAgentPromptActivity extends BossScreenActivity { private @Nullable String userPromptText; private @Nullable String projectPromptOverrideText; private @Nullable String backendOverrideText; + private boolean clawSelectable; + private @Nullable String clawReasonLabel; + private final List backendOverrideValues = new ArrayList<>(); private EditText userPromptInput; private EditText projectPromptInput; private Spinner backendSpinner; @@ -79,6 +83,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity { promptPolicy = payload.optJSONObject("promptPolicy"); userPrompt = payload.optJSONObject("userPrompt"); projectControls = payload.optJSONObject("projectControls"); + JSONObject clawAvailability = payload.optJSONObject("clawAvailability"); adminPromptText = promptPolicy == null ? null : promptPolicy.optString("globalPrompt", ""); userPromptText = userPrompt == null ? "" : userPrompt.optString("content", ""); projectPromptOverrideText = payload.optString( @@ -86,6 +91,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity { projectControls == null ? "" : projectControls.optString("promptOverride", "") ); backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", ""); + clawSelectable = clawAvailability != null && clawAvailability.optBoolean("selectable", false); + clawReasonLabel = clawAvailability == null ? "" : clawAvailability.optString("reasonLabel", ""); replaceContent(); appendContent(BossUi.buildSimpleProfileHeader( @@ -123,8 +130,17 @@ public class MasterAgentPromptActivity extends BossScreenActivity { projectPromptInput )); + backendOverrideValues.clear(); + List backendLabels = new ArrayList<>(); + backendOverrideValues.add(""); + backendLabels.add("默认"); + if (clawSelectable) { + backendOverrideValues.add("claw-runtime"); + backendLabels.add("Claw Runtime"); + } + backendSpinner = new Spinner(this); - backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, BACKEND_OVERRIDE_LABELS)); + backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, backendLabels)); backendSpinner.setSelection(indexOfBackendOverride(backendOverrideText)); backendSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { @Override @@ -143,6 +159,16 @@ public class MasterAgentPromptActivity extends BossScreenActivity { "默认沿用 Boss 当前主链;需要时可显式切到 Claw Runtime。", backendSpinner )); + if (!clawSelectable) { + appendContent(BossUi.buildSoftPanel( + this, + "Claw Runtime 当前不可用", + TextUtils.isEmpty(clawReasonLabel) ? "当前环境未满足 Claw Runtime 的启动条件。" : clawReasonLabel, + TextUtils.equals(backendOverrideText, "claw-runtime") + ? "当前对话之前保存过 Claw Runtime,运行时会自动回退到默认后端。" + : "恢复可用后,执行后端下拉框会重新出现 Claw Runtime。" + )); + } previewTextView = new TextView(this); previewTextView.setText(buildPreviewText()); @@ -203,9 +229,11 @@ public class MasterAgentPromptActivity extends BossScreenActivity { } String backendValue = backendSpinner == null ? (backendOverrideText == null ? "" : backendOverrideText) - : BACKEND_OVERRIDE_VALUES[backendSpinner.getSelectedItemPosition()]; + : backendOverrideValues.get(backendSpinner.getSelectedItemPosition()); if (!TextUtils.isEmpty(backendValue)) { builder.append("【执行后端】\n").append(backendValue).append("\n\n"); + } else if (TextUtils.equals(backendOverrideText, "claw-runtime") && !clawSelectable) { + builder.append("【执行后端】\n默认(Claw Runtime 当前不可用,运行时会自动回退)\n\n"); } if (builder.length() == 0) { return "当前没有任何提示词内容。"; @@ -222,7 +250,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity { final String promptOverride = projectPromptInput == null ? "" : projectPromptInput.getText().toString(); final String backendOverride = backendSpinner == null ? "" - : BACKEND_OVERRIDE_VALUES[backendSpinner.getSelectedItemPosition()]; + : backendOverrideValues.get(backendSpinner.getSelectedItemPosition()); setRefreshing(true); executor.execute(() -> { try { @@ -259,8 +287,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity { if (TextUtils.isEmpty(value)) { return 0; } - for (int index = 0; index < BACKEND_OVERRIDE_VALUES.length; index += 1) { - if (value.equals(BACKEND_OVERRIDE_VALUES[index])) { + for (int index = 0; index < backendOverrideValues.size(); index += 1) { + if (value.equals(backendOverrideValues.get(index))) { return index; } } diff --git a/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java b/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java index fd6912b..06ba150 100644 --- a/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java @@ -156,6 +156,43 @@ public class MasterAgentPromptActivityTest { assertTrue(viewTreeContainsText(content, "新的当前对话提示词")); } + @Test + public void renderPromptProfileShowsClawUnavailableHintWhenBackendCannotBeSelected() throws Exception { + TestMasterAgentPromptActivity activity = Robolectric + .buildActivity( + TestMasterAgentPromptActivity.class, + new Intent() + .putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent") + .putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent") + ) + .setup() + .get(); + + JSONObject payload = new JSONObject() + .put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词")) + .put("userPrompt", new JSONObject().put("content", "用户私有主提示词")) + .put("projectControls", new JSONObject() + .put("promptOverride", "当前对话提示词") + .put("backendOverride", "claw-runtime")) + .put("clawAvailability", new JSONObject() + .put("status", "misconfigured") + .put("selectable", false) + .put("reason", "script_not_found") + .put("reasonLabel", "未检测到有效的 Claw 启动脚本,将自动回退到默认后端。")); + + ReflectionHelpers.callInstanceMethod( + activity, + "renderPromptProfile", + ReflectionHelpers.ClassParameter.from(JSONObject.class, payload) + ); + + Spinner backendSpinner = ReflectionHelpers.getField(activity, "backendSpinner"); + assertEquals(1, backendSpinner.getAdapter().getCount()); + View content = activity.findViewById(R.id.screen_content); + assertTrue(viewTreeContainsText(content, "Claw Runtime 当前不可用")); + assertTrue(viewTreeContainsText(content, "未检测到有效的 Claw 启动脚本")); + } + private static boolean viewTreeContainsText(View root, String expectedText) { if (root instanceof TextView) { CharSequence text = ((TextView) root).getText(); diff --git a/deployment/launchd/com.hyzq.boss.local-agent.plist b/deployment/launchd/com.hyzq.boss.local-agent.plist index 7f4c916..f38b162 100644 --- a/deployment/launchd/com.hyzq.boss.local-agent.plist +++ b/deployment/launchd/com.hyzq.boss.local-agent.plist @@ -8,7 +8,7 @@ /bin/zsh -lc - cd /Users/kris/code/boss && ./scripts/start-local-agent.sh __BOSS_AGENT_CONFIG__ + cd /Users/kris/code/boss && ./scripts/start-local-agent.sh __BOSS_AGENT_CONFIG__ RunAtLoad diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 944feef..5fcda43 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -177,7 +177,8 @@ - 当前状态: - 已在生产代码中被 `boss-master-agent.ts`、`local-agent/server.mjs` 和 `master-agent task complete route` 使用 - 当前仍服务 Boss 自身执行链 - - 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置和显式选择时参与执行 + - 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置且可用性探测通过时才参与执行 + - 如果历史 `backendOverride=claw-runtime` 当前不可用,运行时会自动回退到默认后端,并把原因回给前台 - 当前仓库自带 `scripts/claw-runtime-smoke.mjs` 作为兼容 JSON 协议的 smoke runtime,可用于本地和服务器验证 `ClawBackendAdapter` - 当前尚未接入 `oh-my-codex` @@ -387,6 +388,7 @@ - 当前只支持 `projectId=master-agent` - 仅 `highest_admin` 可写 - `backendOverride` 当前仅支持 `claw-runtime` + - 只有在 `Claw Runtime` 可用性探测通过时才允许保存 `claw-runtime` - 显式传 `null` 或空字符串表示清空覆盖;省略字段表示保留原值 #### `GET /api/v1/projects/[projectId]/participants` diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index 3a9066d..5105a25 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -28,7 +28,8 @@ - `launchd` 已安装:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist` - 当前执行底座抽象层已落地在 `src/lib/execution/`,并已补齐 `ExecutionBackend / PromptAssembler / PermissionPolicy / RemoteRuntimeAdapter / OrchestrationBackend` 默认实现 - 当前生产主链仍然沿用 `local-agent -> codex exec resume -> /api/v1/master-agent/tasks/[taskId]/complete`,执行底座重构以“先抽象、不改行为”为准 -- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 并在 `master-agent` 当前对话中选择 `claw-runtime` 时才会参与执行候选 +- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 且可用性探测通过时,`master-agent` 当前对话中才会出现并允许选择 `claw-runtime` +- 如果历史上已经保存过 `backendOverride=claw-runtime`,但当前 `Claw Runtime` 不可用,运行时会自动回退到默认后端,并在 Web/Android 前台给出明确原因 - 当前仓库已自带 `scripts/claw-runtime-smoke.mjs` 作为本地 smoke runtime;在没有真实 `claw-code` 可执行文件时,可先用 `BOSS_CLAW_COMMAND=node` 与 `BOSS_CLAW_ARGS=scripts/claw-runtime-smoke.mjs` 验证整条链 - 当前 `oh-my-codex` 还未正式接入生产链,只是已经具备 orchestration adapter-ready 的 contract 基础 @@ -146,7 +147,7 @@ cd /Users/kris/code/boss - 主 Agent 当前真实对话链路已验证通过:`Boss Web -> /api/v1/projects/master-agent/messages -> master-agent task queue -> local-agent -> codex exec -> /complete -> 项目消息账本` - 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本 - 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride`,并会优先作用到实际 OpenAI 回复和 Master Codex Node 执行 prompt -- 当前对话级 `agentControls` 也已支持 `backendOverride`:`master-agent` 会话可显式选择 `claw-runtime`,由 `ExecutionBackendSelector` 在当前对话里优先尝试对应后端 +- 当前对话级 `agentControls` 也已支持 `backendOverride`:`master-agent` 会话可在 `Claw Runtime` 可用时显式选择 `claw-runtime`,由 `ExecutionBackendSelector` 在当前对话里优先尝试对应后端;不可用时保存接口会直接拒绝,并返回人类可读原因 - 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示 - `GET /api/v1/app-logs` 当前已支持登录态分页查询 - `POST /api/v1/app-logs`、`POST /api/v1/devices/[deviceId]/skills`、`POST /api/v1/workers/[workerId]/thread-context` 当前都要求有效设备 token 或匹配登录会话 diff --git a/scripts/install-local-launchagent.sh b/scripts/install-local-launchagent.sh index de365bf..95522ba 100755 --- a/scripts/install-local-launchagent.sh +++ b/scripts/install-local-launchagent.sh @@ -25,6 +25,7 @@ config_path = sys.argv[2] text = plist_path.read_text() plist_path.write_text(text.replace("__BOSS_AGENT_CONFIG__", config_path)) PY +plutil -lint "$PLIST_TARGET" >/dev/null launchctl unload "$PLIST_TARGET" >/dev/null 2>&1 || true launchctl load "$PLIST_TARGET" echo "Loaded $PLIST_TARGET with $CONFIG_PATH" diff --git a/src/app/api/v1/projects/[projectId]/agent-controls/route.ts b/src/app/api/v1/projects/[projectId]/agent-controls/route.ts index 03972bf..7110ef8 100644 --- a/src/app/api/v1/projects/[projectId]/agent-controls/route.ts +++ b/src/app/api/v1/projects/[projectId]/agent-controls/route.ts @@ -5,6 +5,7 @@ import { hasPersistedProject, updateProjectAgentControls, } from "@/lib/boss-data"; +import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config"; const reasoningEffortValues = new Set(["low", "medium", "high"]); @@ -27,8 +28,11 @@ export async function GET( return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 }); } - const controls = await getProjectAgentControls(projectId, session.account); - return NextResponse.json({ ok: true, controls }); + const [controls, clawAvailability] = await Promise.all([ + getProjectAgentControls(projectId, session.account), + getClawBackendAvailability(), + ]); + return NextResponse.json({ ok: true, controls, clawAvailability }); } export async function POST( @@ -102,6 +106,16 @@ export async function POST( } try { + if (hasBackendOverride && payload.backendOverride === "claw-runtime") { + const clawAvailability = await getClawBackendAvailability(); + if (!clawAvailability.selectable) { + return NextResponse.json( + { ok: false, message: clawAvailability.reasonLabel, clawAvailability }, + { status: 400 }, + ); + } + } + const controls = await updateProjectAgentControls( projectId, { @@ -112,7 +126,11 @@ export async function POST( }, session.account, ); - return NextResponse.json({ ok: true, controls: controls ?? null }); + return NextResponse.json({ + ok: true, + controls: controls ?? null, + clawAvailability: await getClawBackendAvailability(), + }); } catch (error) { return NextResponse.json( { ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" }, diff --git a/src/app/api/v1/projects/[projectId]/prompt-profile/route.ts b/src/app/api/v1/projects/[projectId]/prompt-profile/route.ts index 905822a..9aa4192 100644 --- a/src/app/api/v1/projects/[projectId]/prompt-profile/route.ts +++ b/src/app/api/v1/projects/[projectId]/prompt-profile/route.ts @@ -9,6 +9,7 @@ import { updateProjectAgentControls, updateUserMasterPrompt, } from "@/lib/boss-data"; +import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config"; export async function GET( request: NextRequest, @@ -25,10 +26,11 @@ export async function GET( return NextResponse.json({ ok: false, message: "PROJECT_NOT_FOUND" }, { status: 404 }); } - const [promptPolicy, userPrompt, projectControls] = await Promise.all([ + const [promptPolicy, userPrompt, projectControls, clawAvailability] = await Promise.all([ getMasterAgentPromptPolicy(), getUserMasterPrompt(session.account), getProjectAgentControls(projectId, session.account), + getClawBackendAvailability(), ]); return NextResponse.json({ @@ -38,6 +40,7 @@ export async function GET( userPrompt, projectControls, projectPromptOverride: projectControls?.promptOverride ?? null, + clawAvailability, account: session.account, }); } @@ -105,6 +108,24 @@ export async function POST( } try { + if ( + hasBackendOverride && + typeof payload.backendOverride === "string" && + payload.backendOverride.trim() === "claw-runtime" + ) { + const clawAvailability = await getClawBackendAvailability(); + if (!clawAvailability.selectable) { + return NextResponse.json( + { + ok: false, + message: clawAvailability.reasonLabel, + clawAvailability, + }, + { status: 400 }, + ); + } + } + if (hasUserPromptContent) { const userPromptContent = typeof payload.userPromptContent === "string" ? payload.userPromptContent.trim() : ""; if (userPromptContent) { @@ -121,10 +142,11 @@ export async function POST( }, session.account); } - const [promptPolicy, userPrompt, projectControls] = await Promise.all([ + const [promptPolicy, userPrompt, projectControls, clawAvailability] = await Promise.all([ getMasterAgentPromptPolicy(), getUserMasterPrompt(session.account), getProjectAgentControls(projectId, session.account), + getClawBackendAvailability(), ]); return NextResponse.json({ @@ -134,6 +156,7 @@ export async function POST( userPrompt, projectControls, projectPromptOverride: projectControls?.promptOverride ?? null, + clawAvailability, account: session.account, }); } catch (error) { diff --git a/src/app/me/master-agent/page.tsx b/src/app/me/master-agent/page.tsx index 44199ec..d793087 100644 --- a/src/app/me/master-agent/page.tsx +++ b/src/app/me/master-agent/page.tsx @@ -2,6 +2,7 @@ import { AppShell, PageNav, StatusBar } from "@/components/app-ui"; import { MasterAgentPromptMemoryClient } from "@/components/master-agent-prompt-memory-client"; import { requirePageSession } from "@/lib/boss-auth"; import { MASTER_AGENT_CHAT_PAGE_ANCHORS } from "@/lib/master-agent-chat-menu"; +import { getClawBackendAvailability } from "@/lib/execution/backends/claw-config"; import { getMasterAgentPromptPolicy, getProjectAgentControls, @@ -13,13 +14,14 @@ export const dynamic = "force-dynamic"; export default async function MasterAgentPromptMemoryPage() { const session = await requirePageSession(); - const [promptPolicy, userPrompt, projectControls, globalMemories, projectMemories] = + const [promptPolicy, userPrompt, projectControls, globalMemories, projectMemories, clawAvailability] = await Promise.all([ getMasterAgentPromptPolicy(), getUserMasterPrompt(session.account), getProjectAgentControls("master-agent", session.account), listUserMasterMemories(session.account, { includeArchived: false, scope: "global" }), listUserMasterMemories(session.account, { includeArchived: false, scope: "project" }), + getClawBackendAvailability(), ]); return ( @@ -42,6 +44,7 @@ export default async function MasterAgentPromptMemoryPage() { promptPolicy={promptPolicy} userPrompt={userPrompt} projectControls={projectControls} + clawAvailability={clawAvailability} globalMemories={globalMemories} projectMemories={projectMemories} anchors={MASTER_AGENT_CHAT_PAGE_ANCHORS} diff --git a/src/components/master-agent-prompt-memory-client.tsx b/src/components/master-agent-prompt-memory-client.tsx index 98f680b..25a3cdf 100644 --- a/src/components/master-agent-prompt-memory-client.tsx +++ b/src/components/master-agent-prompt-memory-client.tsx @@ -24,6 +24,13 @@ type MemoryDraft = { sourceMessageId: string; }; +type ClawAvailability = { + status: "disabled" | "misconfigured" | "ready"; + selectable: boolean; + reason: string; + reasonLabel: string; +}; + const memoryScopeOptions: Array<{ value: MasterMemoryScope; label: string }> = [ { value: "global", label: "通用记忆" }, { value: "project", label: "项目记忆" }, @@ -145,6 +152,7 @@ export function MasterAgentPromptMemoryClient({ promptPolicy, userPrompt, projectControls, + clawAvailability, globalMemories, projectMemories, anchors, @@ -153,6 +161,7 @@ export function MasterAgentPromptMemoryClient({ promptPolicy: MasterAgentPromptPolicy | null; userPrompt: UserMasterPrompt | null; projectControls: ProjectAgentControls | null; + clawAvailability: ClawAvailability; globalMemories: MasterAgentMemory[]; projectMemories: MasterAgentMemory[]; anchors: MasterAgentChatPageAnchors; @@ -167,8 +176,10 @@ export function MasterAgentPromptMemoryClient({ projectControls?.reasoningEffortOverride ?? "", ); const [promptOverride, setPromptOverride] = useState(projectControls?.promptOverride ?? ""); + const storedClawOverrideUnavailable = + projectControls?.backendOverride === "claw-runtime" && !clawAvailability.selectable; const [backendOverride, setBackendOverride] = useState( - projectControls?.backendOverride === "claw-runtime" ? "claw-runtime" : "", + projectControls?.backendOverride === "claw-runtime" && clawAvailability.selectable ? "claw-runtime" : "", ); const [newMemory, setNewMemory] = useState(makeNewMemoryDraft()); const [memoryDrafts, setMemoryDrafts] = useState>(() => { @@ -185,9 +196,14 @@ export function MasterAgentPromptMemoryClient({ globalPrompt.trim() ? `【管理员全局主提示词】\n${globalPrompt.trim()}` : null, userPromptContent.trim() ? `【用户私有主提示词】\n${userPromptContent.trim()}` : null, promptOverride.trim() ? `【当前对话附加提示词】\n${promptOverride.trim()}` : null, + backendOverride.trim() + ? `【执行后端】\n${backendOverride.trim()}` + : storedClawOverrideUnavailable + ? "【执行后端】\n默认(Claw Runtime 当前不可用,运行时会自动回退)" + : null, ].filter(Boolean); return sections.length > 0 ? sections.join("\n\n") : "当前还没有组合后的提示词内容。"; - }, [globalPrompt, userPromptContent, promptOverride]); + }, [backendOverride, globalPrompt, promptOverride, storedClawOverrideUnavailable, userPromptContent]); function updateMemoryDraft(memoryId: string, updater: (draft: MemoryDraft) => MemoryDraft) { setMemoryDrafts((current) => ({ @@ -441,10 +457,21 @@ export function MasterAgentPromptMemoryClient({ className="w-full rounded-xl border border-[#E5E5EA] bg-[#F7F8FA] px-3 py-2 text-[13px] text-[#111111] outline-none" > - + {clawAvailability.selectable ? : null} + {!clawAvailability.selectable ? ( +
+
Claw Runtime 当前不可用
+
{clawAvailability.reasonLabel}
+ {storedClawOverrideUnavailable ? ( +
+ 当前对话之前保存过 Claw Runtime,运行时会自动回退到默认后端。 +
+ ) : null} +
+ ) : null}