From 36a2cd8dfd691f86a228a2e183cf5543d9ec1f82 Mon Sep 17 00:00:00 2001 From: kris Date: Thu, 16 Apr 2026 06:18:27 +0800 Subject: [PATCH] feat: finish app-side master agent control surfaces --- README.md | 6 +- .../main/java/com/hyzq/boss/MainActivity.java | 10 +++ .../boss/MasterAgentEvolutionActivity.java | 36 ++++++--- .../hyzq/boss/MasterAgentMemoryActivity.java | 18 ++--- .../hyzq/boss/MasterAgentPromptActivity.java | 21 +++++ .../com/hyzq/boss/WechatSurfaceMapper.java | 4 +- .../com/hyzq/boss/BossUiRootSurfaceTest.java | 6 +- .../MainActivityMeEntryNavigationTest.java | 32 ++++++++ .../MasterAgentEvolutionActivityTest.java | 38 +++++++++ .../boss/MasterAgentPromptActivityTest.java | 80 +++++++++++++++++++ .../boss/WechatSurfaceMapperMeMenuTest.java | 10 ++- .../hyzq/boss/WechatSurfaceMapperTest.java | 30 ++++--- .../api_and_service_inventory_cn.md | 2 +- .../current_runtime_and_deploy_status_cn.md | 4 +- .../api/v1/master-agent/evolution/route.ts | 7 +- tests/master-agent-evolution-routes.test.ts | 3 +- 16 files changed, 258 insertions(+), 49 deletions(-) diff --git a/README.md b/README.md index 52140e7..891c995 100644 --- a/README.md +++ b/README.md @@ -106,7 +106,7 @@ Android APK: - Android 真机无线调试如果要尽量稳定,优先使用“同一局域网 + 初次 USB 启用后执行 `adb tcpip 5555` + `adb connect :5555`”这条链路;它通常比只依赖系统“无线调试配对码”更稳 - Android 系统层面对“无线调试”没有真正的永久不掉线开关;重启手机、切 Wi‑Fi、切热点、ADB server 重启、USB 调试被重新切换后,都可能导致无线调试自动失效 - 真机调试时建议固定同一 SSID、避免代理/VPN 改路、开发者选项里开启“保持唤醒”,并在需要长时间稳定调试时优先保留 USB 兜底;如果必须完全避免自动断开,不要只依赖无线调试 -- 当前原生活动页已经覆盖:会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、主 Agent 提示词 / 记忆、技能、运维中心、关于 +- 当前原生活动页已经覆盖:会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、主 Agent 提示词、主 Agent 记忆、全局接管、主 Agent 自动进化、技能、运维中心、关于 - 当前原生一级体验已回退到微信式交互:`会话 / 设备 / 我的` 固定底部 tab,会话首页是简单聊天列表,`主 Agent / 审计对话` 以普通置顶会话样式排在最前;项目详情页是聊天优先,只保留 `项目目标 / 版本记录` 两个轻入口 - 当前会话首页右上角已切回 `+` 入口:直接从首页发起独立群聊;设备页右上角仍是 `+添加` - 当前会话首页已升级成“项目聚合 + 线程下钻”的结构:如果某个 Codex 文件夹只导入了 1 个线程,会话列表直接显示这个线程;如果同一文件夹导入了多个线程,会话首页只显示该文件夹归档项,点进去再看这个项目下的全部线程 @@ -127,7 +127,7 @@ Android APK: - 当前 `AI 账号` 页顶部会显式展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个动作,切换主控后可直接验证聊天通路 - 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。` 到 `master-agent` 会话 - 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择:Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有在预设不适用时才需要填自定义模型 -- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已补:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆的新增、编辑、删除接口;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖 +- 当前 `我的` 根页已拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 四个独立入口;其中提示词页支持管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词与执行后端切换,记忆页支持用户通用记忆 / 跨项目项目记忆的新增、编辑与归档 - 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新` - 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果有新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口 - 当前 `OpenAiOnboardingActivity` 在登录成功后会直接给出 `测试主 Agent 对话` 入口,可一键跳到 `master-agent` 聊天页 @@ -326,7 +326,7 @@ npm run aab:release - 登录成功后的进入首页链路已做稳态处理:会先确认 `/api/auth/session` 可读,再执行 `replace(/conversations)`,并附带一次原生级兜底跳转,避免真机 WebView 偶发停留在“正在进入会话首页” - `/api/v1/events` 已作为 SSE 出口使用,会话页、设备页、技能页和项目详情页会按事件自动刷新,不再只靠手动刷新 - 我的页新增 `技能` 入口,`/me/skills` 会按设备分组展示 Skill,并支持一键复制调用语句 -- 我的页新增 `主 Agent 提示词 / 记忆` 入口,`/me/master-agent` 会展示管理员全局主提示词、用户主提示词、当前对话附加提示词、组合预览,以及当前用户的通用记忆和跨项目项目记忆 +- 我的页已拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 入口;`/me/master-agent` 继续展示管理员全局主提示词、用户主提示词、当前对话附加提示词、组合预览,以及当前用户的通用记忆和跨项目项目记忆 - 我的页新增 `AI 账号` 入口,`/me/ai-accounts` 会展示主 GPT / 备用 GPT / API 容灾,并明确主链路优先走已登录 `ChatGPT Plus / Codex` 的 `Master Codex Node` - `AI 账号` 页面当前已补上显式 `登录指引`:手机端不会直接弹出 ChatGPT OAuth;主 GPT 的登录动作必须在绑定电脑上的 Codex / ChatGPT Plus 会话里完成,再回手机端点“测试连接 / 校验连接” - `AI 账号` 页面当前已升级成双入口:首页会显式展示 `登录 OpenAI 平台账号` 和 `绑定电脑上的 Codex 节点` diff --git a/android/app/src/main/java/com/hyzq/boss/MainActivity.java b/android/app/src/main/java/com/hyzq/boss/MainActivity.java index 765c1a9..f19b423 100644 --- a/android/app/src/main/java/com/hyzq/boss/MainActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MainActivity.java @@ -1818,6 +1818,16 @@ public class MainActivity extends AppCompatActivity { intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent"); intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent"); break; + case "master_agent_memory": + intent = new Intent(this, MasterAgentMemoryActivity.class); + intent.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID, "master-agent"); + intent.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME, "主 Agent"); + break; + case "master_agent_takeover": + intent = new Intent(this, MasterAgentTakeoverActivity.class); + intent.putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_ID, "master-agent"); + intent.putExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_NAME, "主 Agent"); + break; case "master_agent_evolution": intent = new Intent(this, MasterAgentEvolutionActivity.class); break; diff --git a/android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java b/android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java index 3579120..c062b2b 100644 --- a/android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java @@ -19,6 +19,7 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity { private @Nullable String currentMode; private @Nullable String statusMessage; private boolean statusIsError; + private boolean canManageEvolution = true; @Override protected void onCreate(@Nullable Bundle savedInstanceState) { @@ -105,6 +106,7 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity { JSONArray signals = payload.optJSONArray("signals"); JSONArray proposals = payload.optJSONArray("proposals"); JSONArray rules = payload.optJSONArray("rules"); + canManageEvolution = payload.optBoolean("canManage", true); currentMode = config == null ? "controlled" : config.optString("mode", "controlled"); boolean autoApplyLowRiskRules = config != null && config.optBoolean("autoApplyLowRiskRules", false); @@ -116,7 +118,9 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity { this, "主 Agent 自动进化", "最近在学什么、打算怎么改、已经生效了什么", - "支持在这里切换 controlled / autonomous,并直接审核待处理提案。" + canManageEvolution + ? "支持在这里切换 controlled / autonomous,并直接审核待处理提案。" + : "当前是只读视角,可以查看主 Agent 正在学习和生效的规则。" )); contentRoot.addView(BossUi.buildSoftPanel( @@ -127,13 +131,17 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity { )); maybeRenderStatusBanner(); - Button controlledButton = BossUi.buildMiniActionButton(this, "切到受控模式", false); - controlledButton.setEnabled(!"controlled".equals(currentMode)); - controlledButton.setOnClickListener(v -> switchMode("controlled")); - Button autonomousButton = BossUi.buildMiniActionButton(this, "切到全自动模式", true); - autonomousButton.setEnabled(!"autonomous".equals(currentMode)); - autonomousButton.setOnClickListener(v -> switchMode("autonomous")); - contentRoot.addView(BossUi.buildInlineActionRow(this, controlledButton, autonomousButton)); + if (canManageEvolution) { + Button controlledButton = BossUi.buildMiniActionButton(this, "切到受控模式", false); + controlledButton.setEnabled(!"controlled".equals(currentMode)); + controlledButton.setOnClickListener(v -> switchMode("controlled")); + Button autonomousButton = BossUi.buildMiniActionButton(this, "切到全自动模式", true); + autonomousButton.setEnabled(!"autonomous".equals(currentMode)); + autonomousButton.setOnClickListener(v -> switchMode("autonomous")); + contentRoot.addView(BossUi.buildInlineActionRow(this, controlledButton, autonomousButton)); + } else { + contentRoot.addView(BossUi.buildEmptyCard(this, "你当前没有管理权限,模式切换和提案审批仅管理员可操作。")); + } contentRoot.addView(BossUi.buildWechatMenuRow( this, @@ -192,11 +200,13 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity { null, null )); - Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false); - rejectButton.setOnClickListener(v -> reviewProposal(proposalId, false)); - Button approveButton = BossUi.buildMiniActionButton(this, "批准", true); - approveButton.setOnClickListener(v -> reviewProposal(proposalId, true)); - contentRoot.addView(BossUi.buildInlineActionRow(this, rejectButton, approveButton)); + if (canManageEvolution) { + Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false); + rejectButton.setOnClickListener(v -> reviewProposal(proposalId, false)); + Button approveButton = BossUi.buildMiniActionButton(this, "批准", true); + approveButton.setOnClickListener(v -> reviewProposal(proposalId, true)); + contentRoot.addView(BossUi.buildInlineActionRow(this, rejectButton, approveButton)); + } } if (!rendered) { contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有待审批提案。")); diff --git a/android/app/src/main/java/com/hyzq/boss/MasterAgentMemoryActivity.java b/android/app/src/main/java/com/hyzq/boss/MasterAgentMemoryActivity.java index a4f133a..95c7144 100644 --- a/android/app/src/main/java/com/hyzq/boss/MasterAgentMemoryActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MasterAgentMemoryActivity.java @@ -104,7 +104,7 @@ public class MasterAgentMemoryActivity extends BossScreenActivity { appendContent(BossUi.buildSoftPanel( this, "记忆说明", - "主 Agent 会自动沉淀长期有用的信息。你也可以在这里手动新增、编辑或删除。", + "主 Agent 会自动沉淀长期有用的信息。你也可以在这里手动新增、编辑或归档。", "底层是结构化存储,项目记忆会显示真实 projectId。" )); @@ -242,23 +242,23 @@ public class MasterAgentMemoryActivity extends BossScreenActivity { )); if (memory != null) { - builder.setNeutralButton("删除", (dialog, which) -> confirmDeleteMemory(memory)); + builder.setNeutralButton("归档", (dialog, which) -> confirmArchiveMemory(memory)); } builder.show(); } - private void confirmDeleteMemory(JSONObject memory) { + private void confirmArchiveMemory(JSONObject memory) { final String memoryId = memory.optString("memoryId", ""); if (memoryId.isEmpty()) { showMessage("缺少 memoryId"); return; } new AlertDialog.Builder(this) - .setTitle("删除记忆") - .setMessage("确定删除这条记忆吗?") + .setTitle("归档记忆") + .setMessage("确定归档这条记忆吗?归档后会从当前列表移除,不是永久删除。") .setNegativeButton("取消", null) - .setPositiveButton("删除", (dialog, which) -> deleteMemory(memoryId)) + .setPositiveButton("归档", (dialog, which) -> archiveMemory(memoryId)) .show(); } @@ -329,7 +329,7 @@ public class MasterAgentMemoryActivity extends BossScreenActivity { }); } - private void deleteMemory(String memoryId) { + private void archiveMemory(String memoryId) { setRefreshing(true); executor.execute(() -> { try { @@ -338,14 +338,14 @@ public class MasterAgentMemoryActivity extends BossScreenActivity { throw new IllegalStateException(response.message()); } runOnUiThread(() -> { - showMessage("记忆已删除"); + showMessage("记忆已归档"); setResult(RESULT_OK); finish(); }); } catch (Exception error) { runOnUiThread(() -> { setRefreshing(false); - showMessage("记忆删除失败:" + error.getMessage()); + showMessage("记忆归档失败:" + error.getMessage()); }); } }); 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 473eb6d..ecc8e46 100644 --- a/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/MasterAgentPromptActivity.java @@ -34,6 +34,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity { private @Nullable String backendOverrideText; private boolean clawSelectable; private @Nullable String clawReasonLabel; + private boolean hermesSelectable; + private @Nullable String hermesReasonLabel; private final List backendOverrideValues = new ArrayList<>(); private EditText userPromptInput; private EditText projectPromptInput; @@ -84,6 +86,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity { userPrompt = payload.optJSONObject("userPrompt"); projectControls = payload.optJSONObject("projectControls"); JSONObject clawAvailability = payload.optJSONObject("clawAvailability"); + JSONObject hermesAvailability = payload.optJSONObject("hermesAvailability"); adminPromptText = promptPolicy == null ? null : promptPolicy.optString("globalPrompt", ""); userPromptText = userPrompt == null ? "" : userPrompt.optString("content", ""); projectPromptOverrideText = payload.optString( @@ -93,6 +96,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity { backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", ""); clawSelectable = clawAvailability != null && clawAvailability.optBoolean("selectable", false); clawReasonLabel = clawAvailability == null ? "" : clawAvailability.optString("reasonLabel", ""); + hermesSelectable = hermesAvailability != null && hermesAvailability.optBoolean("selectable", false); + hermesReasonLabel = hermesAvailability == null ? "" : hermesAvailability.optString("reasonLabel", ""); replaceContent(); appendContent(BossUi.buildSimpleProfileHeader( @@ -138,6 +143,10 @@ public class MasterAgentPromptActivity extends BossScreenActivity { backendOverrideValues.add("claw-runtime"); backendLabels.add("Claw Runtime"); } + if (hermesSelectable) { + backendOverrideValues.add("hermes-runtime"); + backendLabels.add("Hermes Runtime"); + } backendSpinner = new Spinner(this); backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, backendLabels)); @@ -170,6 +179,16 @@ public class MasterAgentPromptActivity extends BossScreenActivity { : "恢复可用后,执行后端下拉框会重新出现 Claw Runtime。" )); } + if (!hermesSelectable) { + appendContent(BossUi.buildSoftPanel( + this, + "Hermes Runtime 当前不可用", + TextUtils.isEmpty(hermesReasonLabel) ? "当前环境未满足 Hermes Runtime 的启动条件。" : hermesReasonLabel, + TextUtils.equals(backendOverrideText, "hermes-runtime") + ? "当前对话之前保存过 Hermes Runtime,运行时会自动回退到默认后端。" + : "恢复可用后,执行后端下拉框会重新出现 Hermes Runtime。" + )); + } previewTextView = new TextView(this); previewTextView.setText(buildPreviewText()); @@ -235,6 +254,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity { builder.append("【执行后端】\n").append(backendValue).append("\n\n"); } else if (TextUtils.equals(backendOverrideText, "claw-runtime") && !clawSelectable) { builder.append("【执行后端】\n默认(Claw Runtime 当前不可用,运行时会自动回退)\n\n"); + } else if (TextUtils.equals(backendOverrideText, "hermes-runtime") && !hermesSelectable) { + builder.append("【执行后端】\n默认(Hermes Runtime 当前不可用,运行时会自动回退)\n\n"); } if (builder.length() == 0) { return "当前没有任何提示词内容。"; diff --git a/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java b/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java index 5661cb6..900bde7 100644 --- a/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java +++ b/android/app/src/main/java/com/hyzq/boss/WechatSurfaceMapper.java @@ -19,7 +19,9 @@ public final class WechatSurfaceMapper { ); private static final List ROOT_ME_MENU_ITEMS = Arrays.asList( - new MeMenuItem("master_agent_prompt", "主 Agent 提示词 / 记忆", "配置全局主提示词、当前主提示词和用户记忆"), + new MeMenuItem("master_agent_prompt", "主 Agent 提示词", "配置全局主提示词和当前对话提示词"), + new MeMenuItem("master_agent_memory", "主 Agent 记忆", "维护用户通用记忆和项目记忆"), + new MeMenuItem("master_agent_takeover", "全局接管", "设置主 Agent 是否默认协同推进线程"), new MeMenuItem("master_agent_evolution", "主 Agent 自动进化", "查看进化信号、提案与自动采纳规则"), new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"), new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"), diff --git a/android/app/src/test/java/com/hyzq/boss/BossUiRootSurfaceTest.java b/android/app/src/test/java/com/hyzq/boss/BossUiRootSurfaceTest.java index ff171c5..b622438 100644 --- a/android/app/src/test/java/com/hyzq/boss/BossUiRootSurfaceTest.java +++ b/android/app/src/test/java/com/hyzq/boss/BossUiRootSurfaceTest.java @@ -40,7 +40,7 @@ public class BossUiRootSurfaceTest { ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot"); LinearLayout content = ReflectionHelpers.getField(activity, "screenContent"); - assertEquals("我的页应是资料头 + 8 条菜单", 9, content.getChildCount()); + assertEquals("我的页应是资料头 + 10 条菜单", 11, content.getChildCount()); View header = content.getChildAt(0); assertEquals("资料头不应保留浮层卡片感", 0f, header.getElevation(), 0.01f); @@ -49,7 +49,9 @@ public class BossUiRootSurfaceTest { assertTrue(viewTreeContainsText(header, "最高管理员")); assertTrue(viewTreeContainsText(header, "主控账号已启用安全保护")); - assertTrue(viewTreeContainsText(content, "主 Agent 提示词 / 记忆")); + assertTrue(viewTreeContainsText(content, "主 Agent 提示词")); + assertTrue(viewTreeContainsText(content, "主 Agent 记忆")); + assertTrue(viewTreeContainsText(content, "全局接管")); assertTrue(viewTreeContainsText(content, "主 Agent 自动进化")); assertTrue(viewTreeContainsText(content, "账号与安全")); assertTrue(viewTreeContainsText(content, "设置")); diff --git a/android/app/src/test/java/com/hyzq/boss/MainActivityMeEntryNavigationTest.java b/android/app/src/test/java/com/hyzq/boss/MainActivityMeEntryNavigationTest.java index 07688ad..8a32f59 100644 --- a/android/app/src/test/java/com/hyzq/boss/MainActivityMeEntryNavigationTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MainActivityMeEntryNavigationTest.java @@ -31,6 +31,38 @@ public class MainActivityMeEntryNavigationTest { assertEquals("主 Agent", started.getStringExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME)); } + @Test + public void masterAgentMemoryMeEntryOpensMemoryActivityForMasterAgent() { + MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get(); + + ReflectionHelpers.callInstanceMethod( + activity, + "openMeEntry", + ReflectionHelpers.ClassParameter.from(String.class, "master_agent_memory") + ); + + Intent started = Shadows.shadowOf(activity).getNextStartedActivity(); + assertEquals(MasterAgentMemoryActivity.class.getName(), started.getComponent().getClassName()); + assertEquals("master-agent", started.getStringExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID)); + assertEquals("主 Agent", started.getStringExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME)); + } + + @Test + public void masterAgentTakeoverMeEntryOpensTakeoverActivityForMasterAgent() { + MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get(); + + ReflectionHelpers.callInstanceMethod( + activity, + "openMeEntry", + ReflectionHelpers.ClassParameter.from(String.class, "master_agent_takeover") + ); + + Intent started = Shadows.shadowOf(activity).getNextStartedActivity(); + assertEquals(MasterAgentTakeoverActivity.class.getName(), started.getComponent().getClassName()); + assertEquals("master-agent", started.getStringExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_ID)); + assertEquals("主 Agent", started.getStringExtra(MasterAgentTakeoverActivity.EXTRA_PROJECT_NAME)); + } + @Test public void masterAgentEvolutionMeEntryOpensEvolutionActivity() { MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get(); diff --git a/android/app/src/test/java/com/hyzq/boss/MasterAgentEvolutionActivityTest.java b/android/app/src/test/java/com/hyzq/boss/MasterAgentEvolutionActivityTest.java index 570b504..714ba91 100644 --- a/android/app/src/test/java/com/hyzq/boss/MasterAgentEvolutionActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MasterAgentEvolutionActivityTest.java @@ -1,6 +1,7 @@ package com.hyzq.boss; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import android.content.Intent; @@ -91,6 +92,43 @@ public class MasterAgentEvolutionActivityTest { assertEquals(1, activity.reloadCount); } + @Test + public void renderDashboardHidesManageActionsForReadonlyView() throws Exception { + TestMasterAgentEvolutionActivity activity = Robolectric + .buildActivity(TestMasterAgentEvolutionActivity.class, new Intent()) + .setup() + .get(); + + JSONObject payload = new JSONObject() + .put("canManage", false) + .put("config", new JSONObject() + .put("mode", "controlled") + .put("autoApplyLowRiskRules", false)) + .put("signals", new JSONArray()) + .put("proposals", new JSONArray() + .put(new JSONObject() + .put("proposalId", "proposal-1") + .put("status", "pending_review") + .put("proposalType", "fast_path_rule") + .put("riskLevel", "low") + .put("createdAt", "2026-04-16T12:01:00+08:00") + .put("title", "新增 Fast Path") + .put("summary", "把状态查询加入本地直答"))) + .put("rules", new JSONArray()); + + ReflectionHelpers.callInstanceMethod( + activity, + "renderDashboard", + ReflectionHelpers.ClassParameter.from(JSONObject.class, payload) + ); + + View content = activity.findViewById(R.id.screen_content); + assertTrue(viewTreeContainsText(content, "当前是只读视角")); + assertTrue(viewTreeContainsText(content, "你当前没有管理权限")); + assertFalse(viewTreeContainsText(content, "切到全自动模式")); + assertFalse(viewTreeContainsText(content, "批准")); + } + @Test public void renderDashboardShowsStatusBannerAndFormattedTimes() throws Exception { TestMasterAgentEvolutionActivity activity = Robolectric 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 162df79..635d1fc 100644 --- a/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java +++ b/android/app/src/test/java/com/hyzq/boss/MasterAgentPromptActivityTest.java @@ -193,6 +193,86 @@ public class MasterAgentPromptActivityTest { assertTrue(viewTreeContainsText(content, "未检测到有效的 Claw 启动脚本")); } + @Test + public void renderPromptProfileIncludesHermesRuntimeWhenSelectable() 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", "hermes-runtime")) + .put("clawAvailability", new JSONObject() + .put("status", "ready") + .put("selectable", true) + .put("reasonLabel", "Claw Runtime 可用。")) + .put("hermesAvailability", new JSONObject() + .put("status", "ready") + .put("selectable", true) + .put("reasonLabel", "Hermes Runtime 可用。")); + + ReflectionHelpers.callInstanceMethod( + activity, + "renderPromptProfile", + ReflectionHelpers.ClassParameter.from(JSONObject.class, payload) + ); + + Spinner backendSpinner = ReflectionHelpers.getField(activity, "backendSpinner"); + assertEquals(3, backendSpinner.getAdapter().getCount()); + assertEquals(2, backendSpinner.getSelectedItemPosition()); + assertEquals("Hermes Runtime", String.valueOf(backendSpinner.getAdapter().getItem(2))); + TextView previewTextView = ReflectionHelpers.getField(activity, "previewTextView"); + assertTrue(String.valueOf(previewTextView.getText()).contains("【执行后端】")); + assertTrue(String.valueOf(previewTextView.getText()).contains("hermes-runtime")); + } + + @Test + public void renderPromptProfileShowsHermesUnavailableHintWhenStoredOverrideCannotBeSelected() 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", "hermes-runtime")) + .put("hermesAvailability", new JSONObject() + .put("status", "misconfigured") + .put("selectable", false) + .put("reason", "script_not_found") + .put("reasonLabel", "未检测到有效的 Hermes 启动脚本,将自动回退到默认后端。")); + + 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, "Hermes Runtime 当前不可用")); + assertTrue(viewTreeContainsText(content, "未检测到有效的 Hermes 启动脚本")); + assertTrue(viewTreeContainsText(content, "当前对话之前保存过 Hermes Runtime")); + } + private static boolean viewTreeContainsText(View root, String expectedText) { if (root instanceof TextView) { CharSequence text = ((TextView) root).getText(); diff --git a/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperMeMenuTest.java b/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperMeMenuTest.java index 78f026c..caf8b15 100644 --- a/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperMeMenuTest.java +++ b/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperMeMenuTest.java @@ -10,8 +10,12 @@ public class WechatSurfaceMapperMeMenuTest { WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems(); assertEquals("master_agent_prompt", items[0].key); - assertEquals("主 Agent 提示词 / 记忆", items[0].title); - assertEquals("master_agent_evolution", items[1].key); - assertEquals("主 Agent 自动进化", items[1].title); + assertEquals("主 Agent 提示词", items[0].title); + assertEquals("master_agent_memory", items[1].key); + assertEquals("主 Agent 记忆", items[1].title); + assertEquals("master_agent_takeover", items[2].key); + assertEquals("全局接管", items[2].title); + assertEquals("master_agent_evolution", items[3].key); + assertEquals("主 Agent 自动进化", items[3].title); } } diff --git a/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperTest.java b/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperTest.java index df2a6d6..32e0a76 100644 --- a/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperTest.java +++ b/android/app/src/test/java/com/hyzq/boss/WechatSurfaceMapperTest.java @@ -192,7 +192,7 @@ public class WechatSurfaceMapperTest { @Test public void rootMeMenuTitles_matchLegacyWechatMenuWithOpsEntry() throws Exception { assertArrayEquals( - new String[]{"主 Agent 提示词 / 记忆", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, + new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, WechatSurfaceMapper.rootMeMenuTitles() ); } @@ -208,7 +208,7 @@ public class WechatSurfaceMapperTest { @Test public void mainPage_keepsOpsEntryInStableWechatMenuOrder() throws Exception { assertArrayEquals( - new String[]{"主 Agent 提示词 / 记忆", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, + new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, WechatSurfaceMapper.rootMeMenuTitles() ); } @@ -380,18 +380,22 @@ public class WechatSurfaceMapperTest { public void meMenuItems_useStableKeysInsteadOfDisplayTitlesForRouting() throws Exception { WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems(); - assertEquals(8, items.length); + assertEquals(10, items.length); assertEquals("master_agent_prompt", items[0].key); - assertEquals("主 Agent 提示词 / 记忆", items[0].title); - assertEquals("master_agent_evolution", items[1].key); - assertEquals("主 Agent 自动进化", items[1].title); - assertEquals("security", items[2].key); - assertEquals("settings", items[3].key); - assertEquals("ops", items[4].key); - assertEquals("运维与修复", items[4].title); - assertEquals("ai_accounts", items[5].key); - assertEquals("skills", items[6].key); - assertEquals("about", items[7].key); + assertEquals("主 Agent 提示词", items[0].title); + assertEquals("master_agent_memory", items[1].key); + assertEquals("主 Agent 记忆", items[1].title); + assertEquals("master_agent_takeover", items[2].key); + assertEquals("全局接管", items[2].title); + assertEquals("master_agent_evolution", items[3].key); + assertEquals("主 Agent 自动进化", items[3].title); + assertEquals("security", items[4].key); + assertEquals("settings", items[5].key); + assertEquals("ops", items[6].key); + assertEquals("运维与修复", items[6].title); + assertEquals("ai_accounts", items[7].key); + assertEquals("skills", items[8].key); + assertEquals("about", items[9].key); } @Test diff --git a/docs/architecture/api_and_service_inventory_cn.md b/docs/architecture/api_and_service_inventory_cn.md index 9ee1ba7..1478c5e 100644 --- a/docs/architecture/api_and_service_inventory_cn.md +++ b/docs/architecture/api_and_service_inventory_cn.md @@ -75,7 +75,7 @@ - 保留版本与 OTA 操作 - 当前已补上 OTA 下载进度、失败重试、安装授权提示和返回关于页后的本地状态恢复 - 当前 `我的` 根页: - - 保留 `主 Agent 提示词 / 记忆 / 主 Agent 自动进化 / 账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于` + - 保留 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化 / 账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于` - `运维与修复` 直接进入 `OpsCenterActivity` - 当前 `OpenAiOnboardingActivity`: - 会先自动打开 `OpenAI Platform` 登录页 diff --git a/docs/architecture/current_runtime_and_deploy_status_cn.md b/docs/architecture/current_runtime_and_deploy_status_cn.md index c03c286..bf234ec 100644 --- a/docs/architecture/current_runtime_and_deploy_status_cn.md +++ b/docs/architecture/current_runtime_and_deploy_status_cn.md @@ -134,9 +134,9 @@ cd /Users/kris/code/boss - 当前 `AI 账号` 页顶部会直接展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个入口,切换主控后不必再手动退回会话页验证 - 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。` 到 `master-agent` 会话 - 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择:Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型 -- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看和编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖 +- 当前 `我的` 根页已经拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 四个独立入口;提示词页用于管理员全局主提示词、用户主提示词、当前对话附加提示词与执行后端设置,记忆页用于用户通用记忆和项目记忆的查看、编辑与归档 - 当前 `我的 > 主 Agent 自动进化` 页面已接通:Web `/me/master-agent/evolution` 可查看最近信号、待审批提案和已生效规则,并允许管理员切换 `controlled / autonomous`、批准或拒绝提案 -- 当前原生 Android 也已接通 `主 Agent 自动进化`:`我的` 根页可直接进入,`master-agent` 会话右上角 `...` 菜单也可直达;页面支持查看最近信号、待审批提案、已生效规则,并可直接切换 `controlled / autonomous` 与批准/拒绝提案 +- 当前原生 Android 也已接通 `主 Agent 自动进化`:`我的` 根页可直接进入,`master-agent` 会话右上角 `...` 菜单也可直达;页面支持查看最近信号、待审批提案、已生效规则。管理员可切换 `controlled / autonomous` 并批准/拒绝提案,非管理员显示只读视角 - 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新` - 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口 - 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,不再把失败日志直接原样回给用户 diff --git a/src/app/api/v1/master-agent/evolution/route.ts b/src/app/api/v1/master-agent/evolution/route.ts index 47156f6..a8ff547 100644 --- a/src/app/api/v1/master-agent/evolution/route.ts +++ b/src/app/api/v1/master-agent/evolution/route.ts @@ -12,5 +12,10 @@ export async function GET(request: NextRequest) { } const dashboard = await getMasterAgentEvolutionDashboard(); - return jsonNoStore({ ok: true, ...dashboard }); + return jsonNoStore({ + ok: true, + canManage: session.role === "highest_admin", + role: session.role, + ...dashboard, + }); } diff --git a/tests/master-agent-evolution-routes.test.ts b/tests/master-agent-evolution-routes.test.ts index d306bc2..20aaab0 100644 --- a/tests/master-agent-evolution-routes.test.ts +++ b/tests/master-agent-evolution-routes.test.ts @@ -73,8 +73,9 @@ test("GET /api/v1/master-agent/evolution returns config proposals and rules", as await createAdminRequest("http://127.0.0.1:3000/api/v1/master-agent/evolution"), ); assert.equal(response.status, 200); - const payload = await response.json() as { ok: boolean; proposals: unknown[]; rules: unknown[] }; + const payload = await response.json() as { ok: boolean; canManage?: boolean; proposals: unknown[]; rules: unknown[] }; assert.equal(payload.ok, true); + assert.equal(payload.canManage, true); assert.ok(Array.isArray(payload.proposals)); assert.ok(Array.isArray(payload.rules)); });