diff --git a/README.md b/README.md index d7d3e76..71d103e 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` 与 `oh-my-codex` 仍未正式接入生产执行链;当前状态是 contract-ready,可在后续通过 adapter 方式接入 +- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有在显式配置 `BOSS_CLAW_*` 并在 `master-agent` 当前对话里显式选择 `claw-runtime` 时才会参与执行候选 +- 当前 `oh-my-codex` 仍未正式接入生产执行链;当前状态是 orchestration-ready,后续将通过独立 adapter 接入 - `GET http://127.0.0.1:4317/api/v1/skills` 正常,已返回本机扫描到的 Codex Skill - `POST http://127.0.0.1:4317/api/v1/heartbeat` 正常,且会顺带触发 `thread-context` 上报 - `launchd` 已加载:`~/Library/LaunchAgents/com.hyzq.boss.local-agent.plist` 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 337abfd..14cbe2f 100644 --- a/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java @@ -4,8 +4,11 @@ import android.os.Bundle; import android.text.Editable; import android.text.TextWatcher; import android.text.TextUtils; +import android.widget.AdapterView; +import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.LinearLayout; +import android.widget.Spinner; import android.widget.TextView; import androidx.annotation.Nullable; @@ -13,6 +16,8 @@ import androidx.annotation.Nullable; import org.json.JSONObject; 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"; @@ -25,8 +30,10 @@ public class MasterAgentPromptActivity extends BossScreenActivity { private @Nullable String adminPromptText; private @Nullable String userPromptText; private @Nullable String projectPromptOverrideText; + private @Nullable String backendOverrideText; private EditText userPromptInput; private EditText projectPromptInput; + private Spinner backendSpinner; private TextView previewTextView; @Override @@ -78,6 +85,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity { "projectPromptOverride", projectControls == null ? "" : projectControls.optString("promptOverride", "") ); + backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", ""); replaceContent(); appendContent(BossUi.buildSimpleProfileHeader( @@ -115,6 +123,27 @@ public class MasterAgentPromptActivity extends BossScreenActivity { projectPromptInput )); + backendSpinner = new Spinner(this); + backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, BACKEND_OVERRIDE_LABELS)); + backendSpinner.setSelection(indexOfBackendOverride(backendOverrideText)); + backendSpinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { + @Override + public void onItemSelected(AdapterView parent, android.view.View view, int position, long id) { + refreshPreview(); + } + + @Override + public void onNothingSelected(AdapterView parent) { + refreshPreview(); + } + }); + appendContent(BossUi.buildFormCell( + this, + "执行后端", + "默认沿用 Boss 当前主链;需要时可显式切到 Claw Runtime。", + backendSpinner + )); + previewTextView = new TextView(this); previewTextView.setText(buildPreviewText()); previewTextView.setTextSize(14); @@ -172,6 +201,12 @@ public class MasterAgentPromptActivity extends BossScreenActivity { if (!TextUtils.isEmpty(projectText)) { builder.append("【当前对话提示词】\n").append(projectText).append("\n\n"); } + String backendValue = backendSpinner == null + ? (backendOverrideText == null ? "" : backendOverrideText) + : BACKEND_OVERRIDE_VALUES[backendSpinner.getSelectedItemPosition()]; + if (!TextUtils.isEmpty(backendValue)) { + builder.append("【执行后端】\n").append(backendValue).append("\n\n"); + } if (builder.length() == 0) { return "当前没有任何提示词内容。"; } @@ -185,12 +220,16 @@ public class MasterAgentPromptActivity extends BossScreenActivity { } final String userContent = userPromptInput == null ? "" : userPromptInput.getText().toString(); final String promptOverride = projectPromptInput == null ? "" : projectPromptInput.getText().toString(); + final String backendOverride = backendSpinner == null + ? "" + : BACKEND_OVERRIDE_VALUES[backendSpinner.getSelectedItemPosition()]; setRefreshing(true); executor.execute(() -> { try { JSONObject payload = new JSONObject(); payload.put("userPromptContent", userContent); payload.put("promptOverride", promptOverride); + payload.put("backendOverride", TextUtils.isEmpty(backendOverride) ? JSONObject.NULL : backendOverride); BossApiClient.ApiResponse response = apiClient.updateMasterAgentPromptProfile(projectId, payload); if (!response.ok()) { throw new IllegalStateException(response.message()); @@ -215,4 +254,16 @@ public class MasterAgentPromptActivity extends BossScreenActivity { headerActionButton.setAlpha(contentLoaded ? 1f : 0.45f); } } + + private int indexOfBackendOverride(@Nullable String value) { + if (TextUtils.isEmpty(value)) { + return 0; + } + for (int index = 0; index < BACKEND_OVERRIDE_VALUES.length; index += 1) { + if (value.equals(BACKEND_OVERRIDE_VALUES[index])) { + return index; + } + } + return 0; + } } 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 e144eb1..fd6912b 100644 --- a/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue; import android.content.Intent; import android.view.View; import android.view.ViewGroup; +import android.widget.Spinner; import android.widget.EditText; import android.widget.TextView; @@ -51,7 +52,9 @@ public class MasterAgentPromptActivityTest { JSONObject payload = new JSONObject() .put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词")) .put("userPrompt", new JSONObject().put("content", "用户私有主提示词")) - .put("projectControls", new JSONObject().put("promptOverride", "当前对话提示词")); + .put("projectControls", new JSONObject() + .put("promptOverride", "当前对话提示词") + .put("backendOverride", "claw-runtime")); ReflectionHelpers.callInstanceMethod( activity, @@ -64,6 +67,7 @@ public class MasterAgentPromptActivityTest { assertTrue(viewTreeContainsText(content, "全局主提示词")); assertTrue(viewTreeContainsText(content, "用户私有主提示词")); assertTrue(viewTreeContainsText(content, "当前对话提示词")); + assertTrue(viewTreeContainsText(content, "执行后端")); assertTrue(viewTreeContainsText(content, "合成预览")); } @@ -91,7 +95,9 @@ public class MasterAgentPromptActivityTest { JSONObject payload = new JSONObject() .put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词")) .put("userPrompt", new JSONObject().put("content", "用户私有主提示词")) - .put("projectControls", new JSONObject().put("promptOverride", "当前对话提示词")); + .put("projectControls", new JSONObject() + .put("promptOverride", "当前对话提示词") + .put("backendOverride", "claw-runtime")); ReflectionHelpers.callInstanceMethod( activity, @@ -101,14 +107,16 @@ public class MasterAgentPromptActivityTest { EditText userInput = ReflectionHelpers.getField(activity, "userPromptInput"); EditText conversationInput = ReflectionHelpers.getField(activity, "projectPromptInput"); + Spinner backendSpinner = ReflectionHelpers.getField(activity, "backendSpinner"); userInput.setText("更新后的用户提示词"); conversationInput.setText("更新后的对话提示词"); + backendSpinner.setSelection(0); ReflectionHelpers.callInstanceMethod(activity, "savePromptProfile"); org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle(); assertEquals( - "{\"userPromptContent\":\"更新后的用户提示词\",\"promptOverride\":\"更新后的对话提示词\"}", + "{\"userPromptContent\":\"更新后的用户提示词\",\"promptOverride\":\"更新后的对话提示词\",\"backendOverride\":null}", ((ScriptedBossApiClient) ReflectionHelpers.getField(activity, "apiClient")).connection.requestBody() ); } @@ -128,7 +136,9 @@ public class MasterAgentPromptActivityTest { JSONObject payload = new JSONObject() .put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词")) .put("userPrompt", new JSONObject().put("content", "用户私有主提示词")) - .put("projectControls", new JSONObject().put("promptOverride", "当前对话提示词")); + .put("projectControls", new JSONObject() + .put("promptOverride", "当前对话提示词") + .put("backendOverride", "claw-runtime")); ReflectionHelpers.callInstanceMethod( activity, diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 9c1d83b..4f651cc 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 自身执行链 - - 当前未直接接入 `claw-code` 或 `oh-my-codex` + - 当前已最小接入 `ClawBackendAdapter`,但默认关闭,仅在显式配置和显式选择时参与执行 + - 当前尚未接入 `oh-my-codex` ### 3.2 认证相关 @@ -373,17 +374,18 @@ #### `GET /api/v1/projects/[projectId]/agent-controls` -- 用途:读取当前对话级别的 `modelOverride / reasoningEffortOverride` +- 用途:读取当前对话级别的 `modelOverride / reasoningEffortOverride / backendOverride` - 当前约束: - 当前只支持 `projectId=master-agent` - 未配置时返回 `controls: null` #### `POST /api/v1/projects/[projectId]/agent-controls` -- 用途:更新当前对话级别的 `modelOverride / reasoningEffortOverride` +- 用途:更新当前对话级别的 `modelOverride / reasoningEffortOverride / promptOverride / backendOverride` - 当前约束: - 当前只支持 `projectId=master-agent` - 仅 `highest_admin` 可写 + - `backendOverride` 当前仅支持 `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 1a2349d..98d17f7 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` 与 `oh-my-codex` 还未正式接入生产链,只是已经具备 adapter-ready 的 contract 基础 +- 当前 `claw-code` 已以最小 `ClawBackendAdapter` 形式接入执行底座,但默认关闭;只有显式配置 `BOSS_CLAW_*` 并在 `master-agent` 当前对话中选择 `claw-runtime` 时才会参与执行候选 +- 当前 `oh-my-codex` 还未正式接入生产链,只是已经具备 orchestration adapter-ready 的 contract 基础 本地已知运行方式: @@ -144,6 +145,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` 在当前对话里优先尝试对应后端 - 原生 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/src/app/api/v1/projects/[projectId]/agent-controls/route.ts b/src/app/api/v1/projects/[projectId]/agent-controls/route.ts index 3af497f..03972bf 100644 --- a/src/app/api/v1/projects/[projectId]/agent-controls/route.ts +++ b/src/app/api/v1/projects/[projectId]/agent-controls/route.ts @@ -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, ); 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 71b0e1c..905822a 100644 --- a/src/app/api/v1/projects/[projectId]/prompt-profile/route.ts +++ b/src/app/api/v1/projects/[projectId]/prompt-profile/route.ts @@ -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); } diff --git a/src/components/master-agent-prompt-memory-client.tsx b/src/components/master-agent-prompt-memory-client.tsx index 07a8b46..98f680b 100644 --- a/src/components/master-agent-prompt-memory-client.tsx +++ b/src/components/master-agent-prompt-memory-client.tsx @@ -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(makeNewMemoryDraft()); const [memoryDrafts, setMemoryDrafts] = useState>(() => { const next: Record = {}; @@ -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({ 当前对话 -
+
+