feat: finish app-side master agent control surfaces

This commit is contained in:
kris
2026-04-16 06:18:27 +08:00
parent e8304faebc
commit 36a2cd8dfd
16 changed files with 258 additions and 49 deletions

View File

@@ -106,7 +106,7 @@ Android APK
- Android 真机无线调试如果要尽量稳定,优先使用“同一局域网 + 初次 USB 启用后执行 `adb tcpip 5555` + `adb connect <phone-ip>:5555`”这条链路;它通常比只依赖系统“无线调试配对码”更稳 - Android 真机无线调试如果要尽量稳定,优先使用“同一局域网 + 初次 USB 启用后执行 `adb tcpip 5555` + `adb connect <phone-ip>:5555`”这条链路;它通常比只依赖系统“无线调试配对码”更稳
- Android 系统层面对“无线调试”没有真正的永久不掉线开关;重启手机、切 WiFi、切热点、ADB server 重启、USB 调试被重新切换后,都可能导致无线调试自动失效 - Android 系统层面对“无线调试”没有真正的永久不掉线开关;重启手机、切 WiFi、切热点、ADB server 重启、USB 调试被重新切换后,都可能导致无线调试自动失效
- 真机调试时建议固定同一 SSID、避免代理/VPN 改路、开发者选项里开启“保持唤醒”,并在需要长时间稳定调试时优先保留 USB 兜底;如果必须完全避免自动断开,不要只依赖无线调试 - 真机调试时建议固定同一 SSID、避免代理/VPN 改路、开发者选项里开启“保持唤醒”,并在需要长时间稳定调试时优先保留 USB 兜底;如果必须完全避免自动断开,不要只依赖无线调试
- 当前原生活动页已经覆盖会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、主 Agent 提示词 / 记忆、技能、运维中心、关于 - 当前原生活动页已经覆盖会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、主 Agent 提示词、主 Agent 记忆、全局接管、主 Agent 自动进化、技能、运维中心、关于
- 当前原生一级体验已回退到微信式交互:`会话 / 设备 / 我的` 固定底部 tab会话首页是简单聊天列表`主 Agent / 审计对话` 以普通置顶会话样式排在最前;项目详情页是聊天优先,只保留 `项目目标 / 版本记录` 两个轻入口 - 当前原生一级体验已回退到微信式交互:`会话 / 设备 / 我的` 固定底部 tab会话首页是简单聊天列表`主 Agent / 审计对话` 以普通置顶会话样式排在最前;项目详情页是聊天优先,只保留 `项目目标 / 版本记录` 两个轻入口
- 当前会话首页右上角已切回 `+` 入口:直接从首页发起独立群聊;设备页右上角仍是 `+添加` - 当前会话首页右上角已切回 `+` 入口:直接从首页发起独立群聊;设备页右上角仍是 `+添加`
- 当前会话首页已升级成“项目聚合 + 线程下钻”的结构:如果某个 Codex 文件夹只导入了 1 个线程,会话列表直接显示这个线程;如果同一文件夹导入了多个线程,会话首页只显示该文件夹归档项,点进去再看这个项目下的全部线程 - 当前会话首页已升级成“项目聚合 + 线程下钻”的结构:如果某个 Codex 文件夹只导入了 1 个线程,会话列表直接显示这个线程;如果同一文件夹导入了多个线程,会话首页只显示该文件夹归档项,点进去再看这个项目下的全部线程
@@ -127,7 +127,7 @@ Android APK
- 当前 `AI 账号` 页顶部会显式展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个动作,切换主控后可直接验证聊天通路 - 当前 `AI 账号` 页顶部会显式展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个动作,切换主控后可直接验证聊天通路
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。``master-agent` 会话 - 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。``master-agent` 会话
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有在预设不适用时才需要填自定义模型 - 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有在预设不适用时才需要填自定义模型
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已补:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆的新增、编辑、删除接口;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖 - 当前 `我的` 根页已拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 四个独立入口;其中提示词页支持管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词与执行后端切换,记忆页支持用户通用记忆 / 跨项目项目记忆的新增、编辑与归档
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新` - 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果有新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口 - 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果有新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
- 当前 `OpenAiOnboardingActivity` 在登录成功后会直接给出 `测试主 Agent 对话` 入口,可一键跳到 `master-agent` 聊天页 - 当前 `OpenAiOnboardingActivity` 在登录成功后会直接给出 `测试主 Agent 对话` 入口,可一键跳到 `master-agent` 聊天页
@@ -326,7 +326,7 @@ npm run aab:release
- 登录成功后的进入首页链路已做稳态处理:会先确认 `/api/auth/session` 可读,再执行 `replace(/conversations)`,并附带一次原生级兜底跳转,避免真机 WebView 偶发停留在“正在进入会话首页” - 登录成功后的进入首页链路已做稳态处理:会先确认 `/api/auth/session` 可读,再执行 `replace(/conversations)`,并附带一次原生级兜底跳转,避免真机 WebView 偶发停留在“正在进入会话首页”
- `/api/v1/events` 已作为 SSE 出口使用,会话页、设备页、技能页和项目详情页会按事件自动刷新,不再只靠手动刷新 - `/api/v1/events` 已作为 SSE 出口使用,会话页、设备页、技能页和项目详情页会按事件自动刷新,不再只靠手动刷新
- 我的页新增 `技能` 入口,`/me/skills` 会按设备分组展示 Skill并支持一键复制调用语句 - 我的页新增 `技能` 入口,`/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 账号` 入口,`/me/ai-accounts` 会展示主 GPT / 备用 GPT / API 容灾,并明确主链路优先走已登录 `ChatGPT Plus / Codex``Master Codex Node`
- `AI 账号` 页面当前已补上显式 `登录指引`:手机端不会直接弹出 ChatGPT OAuth主 GPT 的登录动作必须在绑定电脑上的 Codex / ChatGPT Plus 会话里完成,再回手机端点“测试连接 / 校验连接” - `AI 账号` 页面当前已补上显式 `登录指引`:手机端不会直接弹出 ChatGPT OAuth主 GPT 的登录动作必须在绑定电脑上的 Codex / ChatGPT Plus 会话里完成,再回手机端点“测试连接 / 校验连接”
- `AI 账号` 页面当前已升级成双入口:首页会显式展示 `登录 OpenAI 平台账号``绑定电脑上的 Codex 节点` - `AI 账号` 页面当前已升级成双入口:首页会显式展示 `登录 OpenAI 平台账号``绑定电脑上的 Codex 节点`

View File

@@ -1818,6 +1818,16 @@ public class MainActivity extends AppCompatActivity {
intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent"); intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent");
intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent"); intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent");
break; 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": case "master_agent_evolution":
intent = new Intent(this, MasterAgentEvolutionActivity.class); intent = new Intent(this, MasterAgentEvolutionActivity.class);
break; break;

View File

@@ -19,6 +19,7 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity {
private @Nullable String currentMode; private @Nullable String currentMode;
private @Nullable String statusMessage; private @Nullable String statusMessage;
private boolean statusIsError; private boolean statusIsError;
private boolean canManageEvolution = true;
@Override @Override
protected void onCreate(@Nullable Bundle savedInstanceState) { protected void onCreate(@Nullable Bundle savedInstanceState) {
@@ -105,6 +106,7 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity {
JSONArray signals = payload.optJSONArray("signals"); JSONArray signals = payload.optJSONArray("signals");
JSONArray proposals = payload.optJSONArray("proposals"); JSONArray proposals = payload.optJSONArray("proposals");
JSONArray rules = payload.optJSONArray("rules"); JSONArray rules = payload.optJSONArray("rules");
canManageEvolution = payload.optBoolean("canManage", true);
currentMode = config == null ? "controlled" : config.optString("mode", "controlled"); currentMode = config == null ? "controlled" : config.optString("mode", "controlled");
boolean autoApplyLowRiskRules = config != null && config.optBoolean("autoApplyLowRiskRules", false); boolean autoApplyLowRiskRules = config != null && config.optBoolean("autoApplyLowRiskRules", false);
@@ -116,7 +118,9 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity {
this, this,
"主 Agent 自动进化", "主 Agent 自动进化",
"最近在学什么、打算怎么改、已经生效了什么", "最近在学什么、打算怎么改、已经生效了什么",
"支持在这里切换 controlled / autonomous并直接审核待处理提案。" canManageEvolution
? "支持在这里切换 controlled / autonomous并直接审核待处理提案。"
: "当前是只读视角,可以查看主 Agent 正在学习和生效的规则。"
)); ));
contentRoot.addView(BossUi.buildSoftPanel( contentRoot.addView(BossUi.buildSoftPanel(
@@ -127,13 +131,17 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity {
)); ));
maybeRenderStatusBanner(); maybeRenderStatusBanner();
Button controlledButton = BossUi.buildMiniActionButton(this, "切到受控模式", false); if (canManageEvolution) {
controlledButton.setEnabled(!"controlled".equals(currentMode)); Button controlledButton = BossUi.buildMiniActionButton(this, "切到受控模式", false);
controlledButton.setOnClickListener(v -> switchMode("controlled")); controlledButton.setEnabled(!"controlled".equals(currentMode));
Button autonomousButton = BossUi.buildMiniActionButton(this, "切到全自动模式", true); controlledButton.setOnClickListener(v -> switchMode("controlled"));
autonomousButton.setEnabled(!"autonomous".equals(currentMode)); Button autonomousButton = BossUi.buildMiniActionButton(this, "切到全自动模式", true);
autonomousButton.setOnClickListener(v -> switchMode("autonomous")); autonomousButton.setEnabled(!"autonomous".equals(currentMode));
contentRoot.addView(BossUi.buildInlineActionRow(this, controlledButton, autonomousButton)); autonomousButton.setOnClickListener(v -> switchMode("autonomous"));
contentRoot.addView(BossUi.buildInlineActionRow(this, controlledButton, autonomousButton));
} else {
contentRoot.addView(BossUi.buildEmptyCard(this, "你当前没有管理权限,模式切换和提案审批仅管理员可操作。"));
}
contentRoot.addView(BossUi.buildWechatMenuRow( contentRoot.addView(BossUi.buildWechatMenuRow(
this, this,
@@ -192,11 +200,13 @@ public class MasterAgentEvolutionActivity extends BossScreenActivity {
null, null,
null null
)); ));
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false); if (canManageEvolution) {
rejectButton.setOnClickListener(v -> reviewProposal(proposalId, false)); Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
Button approveButton = BossUi.buildMiniActionButton(this, "批准", true); rejectButton.setOnClickListener(v -> reviewProposal(proposalId, false));
approveButton.setOnClickListener(v -> reviewProposal(proposalId, true)); Button approveButton = BossUi.buildMiniActionButton(this, "批准", true);
contentRoot.addView(BossUi.buildInlineActionRow(this, rejectButton, approveButton)); approveButton.setOnClickListener(v -> reviewProposal(proposalId, true));
contentRoot.addView(BossUi.buildInlineActionRow(this, rejectButton, approveButton));
}
} }
if (!rendered) { if (!rendered) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有待审批提案。")); contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有待审批提案。"));

View File

@@ -104,7 +104,7 @@ public class MasterAgentMemoryActivity extends BossScreenActivity {
appendContent(BossUi.buildSoftPanel( appendContent(BossUi.buildSoftPanel(
this, this,
"记忆说明", "记忆说明",
"主 Agent 会自动沉淀长期有用的信息。你也可以在这里手动新增、编辑或删除", "主 Agent 会自动沉淀长期有用的信息。你也可以在这里手动新增、编辑或归档",
"底层是结构化存储,项目记忆会显示真实 projectId。" "底层是结构化存储,项目记忆会显示真实 projectId。"
)); ));
@@ -242,23 +242,23 @@ public class MasterAgentMemoryActivity extends BossScreenActivity {
)); ));
if (memory != null) { if (memory != null) {
builder.setNeutralButton("删除", (dialog, which) -> confirmDeleteMemory(memory)); builder.setNeutralButton("归档", (dialog, which) -> confirmArchiveMemory(memory));
} }
builder.show(); builder.show();
} }
private void confirmDeleteMemory(JSONObject memory) { private void confirmArchiveMemory(JSONObject memory) {
final String memoryId = memory.optString("memoryId", ""); final String memoryId = memory.optString("memoryId", "");
if (memoryId.isEmpty()) { if (memoryId.isEmpty()) {
showMessage("缺少 memoryId"); showMessage("缺少 memoryId");
return; return;
} }
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setTitle("删除记忆") .setTitle("归档记忆")
.setMessage("确定删除这条记忆吗?") .setMessage("确定归档这条记忆吗?归档后会从当前列表移除,不是永久删除。")
.setNegativeButton("取消", null) .setNegativeButton("取消", null)
.setPositiveButton("删除", (dialog, which) -> deleteMemory(memoryId)) .setPositiveButton("归档", (dialog, which) -> archiveMemory(memoryId))
.show(); .show();
} }
@@ -329,7 +329,7 @@ public class MasterAgentMemoryActivity extends BossScreenActivity {
}); });
} }
private void deleteMemory(String memoryId) { private void archiveMemory(String memoryId) {
setRefreshing(true); setRefreshing(true);
executor.execute(() -> { executor.execute(() -> {
try { try {
@@ -338,14 +338,14 @@ public class MasterAgentMemoryActivity extends BossScreenActivity {
throw new IllegalStateException(response.message()); throw new IllegalStateException(response.message());
} }
runOnUiThread(() -> { runOnUiThread(() -> {
showMessage("记忆已删除"); showMessage("记忆已归档");
setResult(RESULT_OK); setResult(RESULT_OK);
finish(); finish();
}); });
} catch (Exception error) { } catch (Exception error) {
runOnUiThread(() -> { runOnUiThread(() -> {
setRefreshing(false); setRefreshing(false);
showMessage("记忆删除失败:" + error.getMessage()); showMessage("记忆归档失败:" + error.getMessage());
}); });
} }
}); });

View File

@@ -34,6 +34,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
private @Nullable String backendOverrideText; private @Nullable String backendOverrideText;
private boolean clawSelectable; private boolean clawSelectable;
private @Nullable String clawReasonLabel; private @Nullable String clawReasonLabel;
private boolean hermesSelectable;
private @Nullable String hermesReasonLabel;
private final List<String> backendOverrideValues = new ArrayList<>(); private final List<String> backendOverrideValues = new ArrayList<>();
private EditText userPromptInput; private EditText userPromptInput;
private EditText projectPromptInput; private EditText projectPromptInput;
@@ -84,6 +86,7 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
userPrompt = payload.optJSONObject("userPrompt"); userPrompt = payload.optJSONObject("userPrompt");
projectControls = payload.optJSONObject("projectControls"); projectControls = payload.optJSONObject("projectControls");
JSONObject clawAvailability = payload.optJSONObject("clawAvailability"); JSONObject clawAvailability = payload.optJSONObject("clawAvailability");
JSONObject hermesAvailability = payload.optJSONObject("hermesAvailability");
adminPromptText = promptPolicy == null ? null : promptPolicy.optString("globalPrompt", ""); adminPromptText = promptPolicy == null ? null : promptPolicy.optString("globalPrompt", "");
userPromptText = userPrompt == null ? "" : userPrompt.optString("content", ""); userPromptText = userPrompt == null ? "" : userPrompt.optString("content", "");
projectPromptOverrideText = payload.optString( projectPromptOverrideText = payload.optString(
@@ -93,6 +96,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", ""); backendOverrideText = projectControls == null ? "" : projectControls.optString("backendOverride", "");
clawSelectable = clawAvailability != null && clawAvailability.optBoolean("selectable", false); clawSelectable = clawAvailability != null && clawAvailability.optBoolean("selectable", false);
clawReasonLabel = clawAvailability == null ? "" : clawAvailability.optString("reasonLabel", ""); clawReasonLabel = clawAvailability == null ? "" : clawAvailability.optString("reasonLabel", "");
hermesSelectable = hermesAvailability != null && hermesAvailability.optBoolean("selectable", false);
hermesReasonLabel = hermesAvailability == null ? "" : hermesAvailability.optString("reasonLabel", "");
replaceContent(); replaceContent();
appendContent(BossUi.buildSimpleProfileHeader( appendContent(BossUi.buildSimpleProfileHeader(
@@ -138,6 +143,10 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
backendOverrideValues.add("claw-runtime"); backendOverrideValues.add("claw-runtime");
backendLabels.add("Claw Runtime"); backendLabels.add("Claw Runtime");
} }
if (hermesSelectable) {
backendOverrideValues.add("hermes-runtime");
backendLabels.add("Hermes Runtime");
}
backendSpinner = new Spinner(this); backendSpinner = new Spinner(this);
backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, backendLabels)); backendSpinner.setAdapter(new ArrayAdapter<>(this, android.R.layout.simple_spinner_dropdown_item, backendLabels));
@@ -170,6 +179,16 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
: "恢复可用后,执行后端下拉框会重新出现 Claw Runtime。" : "恢复可用后,执行后端下拉框会重新出现 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 = new TextView(this);
previewTextView.setText(buildPreviewText()); previewTextView.setText(buildPreviewText());
@@ -235,6 +254,8 @@ public class MasterAgentPromptActivity extends BossScreenActivity {
builder.append("【执行后端】\n").append(backendValue).append("\n\n"); builder.append("【执行后端】\n").append(backendValue).append("\n\n");
} else if (TextUtils.equals(backendOverrideText, "claw-runtime") && !clawSelectable) { } else if (TextUtils.equals(backendOverrideText, "claw-runtime") && !clawSelectable) {
builder.append("【执行后端】\n默认Claw Runtime 当前不可用,运行时会自动回退)\n\n"); 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) { if (builder.length() == 0) {
return "当前没有任何提示词内容。"; return "当前没有任何提示词内容。";

View File

@@ -19,7 +19,9 @@ public final class WechatSurfaceMapper {
); );
private static final List<MeMenuItem> ROOT_ME_MENU_ITEMS = Arrays.asList( private static final List<MeMenuItem> 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("master_agent_evolution", "主 Agent 自动进化", "查看进化信号、提案与自动采纳规则"),
new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"), new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"),
new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"), new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"),

View File

@@ -40,7 +40,7 @@ public class BossUiRootSurfaceTest {
ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot"); ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot");
LinearLayout content = ReflectionHelpers.getField(activity, "screenContent"); LinearLayout content = ReflectionHelpers.getField(activity, "screenContent");
assertEquals("我的页应是资料头 + 8 条菜单", 9, content.getChildCount()); assertEquals("我的页应是资料头 + 10 条菜单", 11, content.getChildCount());
View header = content.getChildAt(0); View header = content.getChildAt(0);
assertEquals("资料头不应保留浮层卡片感", 0f, header.getElevation(), 0.01f); assertEquals("资料头不应保留浮层卡片感", 0f, header.getElevation(), 0.01f);
@@ -49,7 +49,9 @@ public class BossUiRootSurfaceTest {
assertTrue(viewTreeContainsText(header, "最高管理员")); assertTrue(viewTreeContainsText(header, "最高管理员"));
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, "主 Agent 自动进化"));
assertTrue(viewTreeContainsText(content, "账号与安全")); assertTrue(viewTreeContainsText(content, "账号与安全"));
assertTrue(viewTreeContainsText(content, "设置")); assertTrue(viewTreeContainsText(content, "设置"));

View File

@@ -31,6 +31,38 @@ public class MainActivityMeEntryNavigationTest {
assertEquals("主 Agent", started.getStringExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME)); 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 @Test
public void masterAgentEvolutionMeEntryOpensEvolutionActivity() { public void masterAgentEvolutionMeEntryOpensEvolutionActivity() {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get(); MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();

View File

@@ -1,6 +1,7 @@
package com.hyzq.boss; package com.hyzq.boss;
import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import android.content.Intent; import android.content.Intent;
@@ -91,6 +92,43 @@ public class MasterAgentEvolutionActivityTest {
assertEquals(1, activity.reloadCount); 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 @Test
public void renderDashboardShowsStatusBannerAndFormattedTimes() throws Exception { public void renderDashboardShowsStatusBannerAndFormattedTimes() throws Exception {
TestMasterAgentEvolutionActivity activity = Robolectric TestMasterAgentEvolutionActivity activity = Robolectric

View File

@@ -193,6 +193,86 @@ public class MasterAgentPromptActivityTest {
assertTrue(viewTreeContainsText(content, "未检测到有效的 Claw 启动脚本")); 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) { private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) { if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText(); CharSequence text = ((TextView) root).getText();

View File

@@ -10,8 +10,12 @@ public class WechatSurfaceMapperMeMenuTest {
WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems(); WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems();
assertEquals("master_agent_prompt", items[0].key); assertEquals("master_agent_prompt", items[0].key);
assertEquals("主 Agent 提示词 / 记忆", items[0].title); assertEquals("主 Agent 提示词", items[0].title);
assertEquals("master_agent_evolution", items[1].key); assertEquals("master_agent_memory", items[1].key);
assertEquals("主 Agent 自动进化", items[1].title); 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);
} }
} }

View File

@@ -192,7 +192,7 @@ public class WechatSurfaceMapperTest {
@Test @Test
public void rootMeMenuTitles_matchLegacyWechatMenuWithOpsEntry() throws Exception { public void rootMeMenuTitles_matchLegacyWechatMenuWithOpsEntry() throws Exception {
assertArrayEquals( assertArrayEquals(
new String[]{"主 Agent 提示词 / 记忆", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
WechatSurfaceMapper.rootMeMenuTitles() WechatSurfaceMapper.rootMeMenuTitles()
); );
} }
@@ -208,7 +208,7 @@ public class WechatSurfaceMapperTest {
@Test @Test
public void mainPage_keepsOpsEntryInStableWechatMenuOrder() throws Exception { public void mainPage_keepsOpsEntryInStableWechatMenuOrder() throws Exception {
assertArrayEquals( assertArrayEquals(
new String[]{"主 Agent 提示词 / 记忆", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
WechatSurfaceMapper.rootMeMenuTitles() WechatSurfaceMapper.rootMeMenuTitles()
); );
} }
@@ -380,18 +380,22 @@ public class WechatSurfaceMapperTest {
public void meMenuItems_useStableKeysInsteadOfDisplayTitlesForRouting() throws Exception { public void meMenuItems_useStableKeysInsteadOfDisplayTitlesForRouting() throws Exception {
WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems(); WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems();
assertEquals(8, items.length); assertEquals(10, items.length);
assertEquals("master_agent_prompt", items[0].key); assertEquals("master_agent_prompt", items[0].key);
assertEquals("主 Agent 提示词 / 记忆", items[0].title); assertEquals("主 Agent 提示词", items[0].title);
assertEquals("master_agent_evolution", items[1].key); assertEquals("master_agent_memory", items[1].key);
assertEquals("主 Agent 自动进化", items[1].title); assertEquals("主 Agent 记忆", items[1].title);
assertEquals("security", items[2].key); assertEquals("master_agent_takeover", items[2].key);
assertEquals("settings", items[3].key); assertEquals("全局接管", items[2].title);
assertEquals("ops", items[4].key); assertEquals("master_agent_evolution", items[3].key);
assertEquals("运维与修复", items[4].title); assertEquals("主 Agent 自动进化", items[3].title);
assertEquals("ai_accounts", items[5].key); assertEquals("security", items[4].key);
assertEquals("skills", items[6].key); assertEquals("settings", items[5].key);
assertEquals("about", items[7].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 @Test

View File

@@ -75,7 +75,7 @@
- 保留版本与 OTA 操作 - 保留版本与 OTA 操作
- 当前已补上 OTA 下载进度、失败重试、安装授权提示和返回关于页后的本地状态恢复 - 当前已补上 OTA 下载进度、失败重试、安装授权提示和返回关于页后的本地状态恢复
- 当前 `我的` 根页: - 当前 `我的` 根页:
- 保留 `主 Agent 提示词 / 记忆 / 主 Agent 自动进化 / 账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于` - 保留 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化 / 账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
- `运维与修复` 直接进入 `OpsCenterActivity` - `运维与修复` 直接进入 `OpsCenterActivity`
- 当前 `OpenAiOnboardingActivity` - 当前 `OpenAiOnboardingActivity`
- 会先自动打开 `OpenAI Platform` 登录页 - 会先自动打开 `OpenAI Platform` 登录页

View File

@@ -134,9 +134,9 @@ cd /Users/kris/code/boss
- 当前 `AI 账号` 页顶部会直接展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个入口,切换主控后不必再手动退回会话页验证 - 当前 `AI 账号` 页顶部会直接展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个入口,切换主控后不必再手动退回会话页验证
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。``master-agent` 会话 - 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。``master-agent` 会话
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型 - 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖 - 当前 `我的` 根页已经拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 四个独立入口;提示词页用于管理员全局主提示词、用户主提示词、当前对话附加提示词与执行后端设置,记忆页用于用户通用记忆和项目记忆的查看编辑与归档
- 当前 `我的 > 主 Agent 自动进化` 页面已接通Web `/me/master-agent/evolution` 可查看最近信号、待审批提案和已生效规则,并允许管理员切换 `controlled / autonomous`、批准或拒绝提案 - 当前 `我的 > 主 Agent 自动进化` 页面已接通Web `/me/master-agent/evolution` 可查看最近信号、待审批提案和已生效规则,并允许管理员切换 `controlled / autonomous`、批准或拒绝提案
- 当前原生 Android 也已接通 `主 Agent 自动进化``我的` 根页可直接进入,`master-agent` 会话右上角 `...` 菜单也可直达;页面支持查看最近信号、待审批提案、已生效规则,并可直接切换 `controlled / autonomous` 批准/拒绝提案 - 当前原生 Android 也已接通 `主 Agent 自动进化``我的` 根页可直接进入,`master-agent` 会话右上角 `...` 菜单也可直达;页面支持查看最近信号、待审批提案、已生效规则。管理员可切换 `controlled / autonomous` 批准/拒绝提案,非管理员显示只读视角
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新` - 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口 - 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
- 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,不再把失败日志直接原样回给用户 - 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,不再把失败日志直接原样回给用户

View File

@@ -12,5 +12,10 @@ export async function GET(request: NextRequest) {
} }
const dashboard = await getMasterAgentEvolutionDashboard(); const dashboard = await getMasterAgentEvolutionDashboard();
return jsonNoStore({ ok: true, ...dashboard }); return jsonNoStore({
ok: true,
canManage: session.role === "highest_admin",
role: session.role,
...dashboard,
});
} }

View File

@@ -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"), await createAdminRequest("http://127.0.0.1:3000/api/v1/master-agent/evolution"),
); );
assert.equal(response.status, 200); 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.ok, true);
assert.equal(payload.canManage, true);
assert.ok(Array.isArray(payload.proposals)); assert.ok(Array.isArray(payload.proposals));
assert.ok(Array.isArray(payload.rules)); assert.ok(Array.isArray(payload.rules));
}); });