6 Commits

Author SHA1 Message Date
kris
36a2cd8dfd feat: finish app-side master agent control surfaces 2026-04-16 06:18:27 +08:00
kris
e8304faebc feat: improve evolution screen feedback states 2026-04-16 06:06:03 +08:00
kris
4bedf75dc2 test: cover master agent evolution entry wiring 2026-04-16 06:02:27 +08:00
kris
363f732e29 feat: add native master agent evolution center 2026-04-16 05:59:12 +08:00
kris
f0490de180 feat: expose master agent evolution dashboard 2026-04-16 05:46:45 +08:00
kris
504d112218 feat: default master agent evolution to autonomous 2026-04-16 05:23:16 +08:00
34 changed files with 1440 additions and 66 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

@@ -54,6 +54,7 @@
<activity android:name=".MasterAgentPromptActivity" android:exported="false" android:screenOrientation="portrait" /> <activity android:name=".MasterAgentPromptActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".MasterAgentTakeoverActivity" android:exported="false" android:screenOrientation="portrait" /> <activity android:name=".MasterAgentTakeoverActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".MasterAgentMemoryActivity" android:exported="false" android:screenOrientation="portrait" /> <activity android:name=".MasterAgentMemoryActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".MasterAgentEvolutionActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".OpsCenterActivity" android:exported="false" android:screenOrientation="portrait" /> <activity android:name=".OpsCenterActivity" android:exported="false" android:screenOrientation="portrait" />
<activity android:name=".AboutActivity" android:exported="false" android:screenOrientation="portrait" /> <activity android:name=".AboutActivity" android:exported="false" android:screenOrientation="portrait" />

View File

@@ -265,6 +265,32 @@ public class BossApiClient {
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/memories", null); return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/memories", null);
} }
public ApiResponse getMasterAgentEvolution() throws IOException, JSONException {
return requestWithRestore("GET", "/api/v1/master-agent/evolution", null);
}
public ApiResponse updateMasterAgentEvolutionMode(String mode) throws IOException, JSONException {
JSONObject payload = new JSONObject();
payload.put("mode", mode);
return requestWithRestore("POST", "/api/v1/master-agent/evolution/config", payload);
}
public ApiResponse approveMasterAgentEvolutionProposal(String proposalId) throws IOException, JSONException {
return requestWithRestore(
"POST",
"/api/v1/master-agent/evolution/proposals/" + encode(proposalId) + "/approve",
new JSONObject()
);
}
public ApiResponse rejectMasterAgentEvolutionProposal(String proposalId) throws IOException, JSONException {
return requestWithRestore(
"POST",
"/api/v1/master-agent/evolution/proposals/" + encode(proposalId) + "/reject",
new JSONObject()
);
}
public ApiResponse createMasterAgentMemory(String projectId, JSONObject payload) throws IOException, JSONException { public ApiResponse createMasterAgentMemory(String projectId, JSONObject payload) throws IOException, JSONException {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/memories", payload); return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/memories", payload);
} }

View File

@@ -1813,6 +1813,24 @@ public class MainActivity extends AppCompatActivity {
case "security": case "security":
intent = new Intent(this, SecurityActivity.class); intent = new Intent(this, SecurityActivity.class);
break; break;
case "master_agent_prompt":
intent = new Intent(this, MasterAgentPromptActivity.class);
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;
case "ai_accounts": case "ai_accounts":
intent = new Intent(this, AiAccountsActivity.class); intent = new Intent(this, AiAccountsActivity.class);
break; break;

View File

@@ -0,0 +1,378 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
public class MasterAgentEvolutionActivity extends BossScreenActivity {
private static final long REALTIME_RELOAD_THROTTLE_MS = 900L;
private LinearLayout contentRoot;
private @Nullable BossRealtimeClient realtimeClient;
private long lastRealtimeReloadAt;
private boolean contentLoaded;
private @Nullable String currentMode;
private @Nullable String statusMessage;
private boolean statusIsError;
private boolean canManageEvolution = true;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("主 Agent 自动进化", "信号、提案与生效规则");
setHeaderAction("刷新", v -> reload());
contentRoot = new LinearLayout(this);
contentRoot.setOrientation(LinearLayout.VERTICAL);
replaceContent(contentRoot);
realtimeClient = new BossRealtimeClient(apiClient, this::handleRealtimeEvent);
reload();
}
@Override
protected void onResume() {
super.onResume();
updateRealtimeSubscription();
}
@Override
protected void onPause() {
stopRealtimeUpdates();
super.onPause();
}
@Override
protected void onDestroy() {
stopRealtimeUpdates();
super.onDestroy();
}
@Override
protected void reload() {
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.getMasterAgentEvolution();
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> {
clearStatusMessage();
renderDashboard(response.json);
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
contentLoaded = false;
showStatusMessage("自动进化加载失败:" + error.getMessage(), true);
renderLoadErrorState(error.getMessage());
});
}
});
}
private void updateRealtimeSubscription() {
if (apiClient != null && apiClient.hasSessionHints() && realtimeClient != null) {
realtimeClient.start();
return;
}
stopRealtimeUpdates();
}
private void stopRealtimeUpdates() {
if (realtimeClient != null) {
realtimeClient.stop();
}
}
void handleRealtimeEvent(BossRealtimeEvent event) {
if (event == null || !"master_agent.settings.updated".equals(event.eventName)) {
return;
}
long now = System.currentTimeMillis();
if (now - lastRealtimeReloadAt < REALTIME_RELOAD_THROTTLE_MS) {
return;
}
lastRealtimeReloadAt = now;
runOnUiThread(this::reload);
}
private void renderDashboard(JSONObject payload) {
JSONObject config = payload.optJSONObject("config");
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);
replaceContent(contentRoot);
contentRoot.removeAllViews();
contentRoot.addView(BossUi.buildSimpleProfileHeader(
this,
"主 Agent 自动进化",
"最近在学什么、打算怎么改、已经生效了什么",
canManageEvolution
? "支持在这里切换 controlled / autonomous并直接审核待处理提案。"
: "当前是只读视角,可以查看主 Agent 正在学习和生效的规则。"
));
contentRoot.addView(BossUi.buildSoftPanel(
this,
"当前模式",
"autonomous".equals(currentMode) ? "完全自我进化" : "受控自动进化",
autoApplyLowRiskRules ? "低风险提案会自动采纳。" : "所有提案都需要人工确认。"
));
maybeRenderStatusBanner();
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,
"待处理提案",
String.valueOf(countPendingProposals(proposals)) + "",
"待审核的策略变更会在这里集中展示。",
null,
null
));
renderPendingProposals(proposals);
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
"最近信号",
signals == null ? "0 条" : signals.length() + "",
"主 Agent 最近捕获到的问题和自我修正线索。",
null,
null
));
renderSignals(signals);
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
"已生效规则",
rules == null ? "0 条" : rules.length() + "",
"已经落进系统并开始影响主 Agent 行为的规则。",
null,
null
));
renderRules(rules);
contentLoaded = true;
setRefreshing(false);
}
private void renderPendingProposals(@Nullable JSONArray proposals) {
if (proposals == null || proposals.length() == 0) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前没有待审批提案。"));
return;
}
boolean rendered = false;
for (int i = 0; i < proposals.length(); i++) {
JSONObject proposal = proposals.optJSONObject(i);
if (proposal == null || !"pending_review".equals(proposal.optString("status", ""))) {
continue;
}
rendered = true;
String proposalId = proposal.optString("proposalId", "");
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
proposal.optString("title", "待审批提案"),
proposal.optString("summary", "暂无摘要"),
proposal.optString("proposalType", "-")
+ " · " + proposal.optString("riskLevel", "-")
+ " · " + formatTime(proposal.optString("createdAt", "-")),
null,
null
));
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, "当前没有待审批提案。"));
}
}
private void renderSignals(@Nullable JSONArray signals) {
if (signals == null || signals.length() == 0) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前还没有进化信号。"));
return;
}
for (int i = 0; i < Math.min(signals.length(), 8); i++) {
JSONObject signal = signals.optJSONObject(i);
if (signal == null) continue;
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
signal.optString("kind", "signal"),
signal.optString("requestText", ""),
formatTime(signal.optString("createdAt", "-")),
null,
null
));
}
}
private void renderRules(@Nullable JSONArray rules) {
if (rules == null || rules.length() == 0) {
contentRoot.addView(BossUi.buildEmptyCard(this, "当前还没有已生效规则。"));
return;
}
for (int i = 0; i < Math.min(rules.length(), 8); i++) {
JSONObject rule = rules.optJSONObject(i);
if (rule == null) continue;
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
rule.optString("ruleType", "rule"),
rule.optString("sourceProposalId", "直接创建"),
formatTime(rule.optString("createdAt", "-")),
null,
null
));
}
}
private int countPendingProposals(@Nullable JSONArray proposals) {
if (proposals == null) {
return 0;
}
int count = 0;
for (int i = 0; i < proposals.length(); i++) {
JSONObject proposal = proposals.optJSONObject(i);
if (proposal != null && "pending_review".equals(proposal.optString("status", ""))) {
count += 1;
}
}
return count;
}
private void switchMode(String mode) {
if (!contentLoaded) {
showMessage("自动进化尚未加载完成。");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.updateMasterAgentEvolutionMode(mode);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> {
showStatusMessage("已切到 " + ("autonomous".equals(mode) ? "完全自我进化" : "受控自动进化"), false);
setResult(RESULT_OK);
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showStatusMessage("切换失败:" + error.getMessage(), true);
});
}
});
}
private void reviewProposal(String proposalId, boolean approve) {
if (proposalId == null || proposalId.isEmpty()) {
showMessage("缺少 proposalId");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = approve
? apiClient.approveMasterAgentEvolutionProposal(proposalId)
: apiClient.rejectMasterAgentEvolutionProposal(proposalId);
if (!response.ok()) {
throw new IllegalStateException(response.message());
}
runOnUiThread(() -> {
showStatusMessage(approve ? "提案已批准" : "提案已拒绝", false);
setResult(RESULT_OK);
reload();
});
} catch (Exception error) {
runOnUiThread(() -> {
setRefreshing(false);
showStatusMessage((approve ? "批准失败:" : "拒绝失败:") + error.getMessage(), true);
});
}
});
}
private void maybeRenderStatusBanner() {
if (statusMessage == null || statusMessage.isEmpty()) {
return;
}
contentRoot.addView(BossUi.buildSoftPanel(
this,
statusIsError ? "最近状态" : "最近操作",
statusMessage,
statusIsError ? "你可以直接点顶部刷新重试,或继续切换模式/审批提案。" : "本页已经同步到最新自动进化状态。"
));
}
private void renderLoadErrorState(String message) {
replaceContent(contentRoot);
contentRoot.removeAllViews();
contentRoot.addView(BossUi.buildSimpleProfileHeader(
this,
"主 Agent 自动进化",
"信号、提案与生效规则",
"当前加载失败,保留在这个页面直接重试即可。"
));
maybeRenderStatusBanner();
Button retryButton = BossUi.buildMiniActionButton(this, "重新加载", true);
retryButton.setOnClickListener(v -> reload());
contentRoot.addView(BossUi.buildInlineActionRow(this, retryButton));
contentRoot.addView(BossUi.buildEmptyCard(this, "自动进化中心暂时不可用:" + message));
}
private void showStatusMessage(String message, boolean isError) {
statusMessage = message;
statusIsError = isError;
showMessage(message);
}
private void clearStatusMessage() {
if (!statusIsError) {
statusMessage = null;
}
statusIsError = false;
}
private String formatTime(String value) {
if (value == null || value.isEmpty() || "-".equals(value)) {
return "-";
}
String normalized = value.replace('T', ' ');
int plusIndex = normalized.indexOf('+');
if (plusIndex > 0) {
return normalized.substring(0, plusIndex);
}
int zIndex = normalized.indexOf('Z');
if (zIndex > 0) {
return normalized.substring(0, zIndex);
}
return normalized;
}
}

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

@@ -93,6 +93,7 @@ public class ProjectDetailActivity extends BossScreenActivity {
private ActivityResultLauncher<Intent> masterAgentPromptLauncher; private ActivityResultLauncher<Intent> masterAgentPromptLauncher;
private ActivityResultLauncher<Intent> masterAgentTakeoverLauncher; private ActivityResultLauncher<Intent> masterAgentTakeoverLauncher;
private ActivityResultLauncher<Intent> masterAgentMemoryLauncher; private ActivityResultLauncher<Intent> masterAgentMemoryLauncher;
private ActivityResultLauncher<Intent> masterAgentEvolutionLauncher;
private ActivityResultLauncher<Intent> forwardTargetLauncher; private ActivityResultLauncher<Intent> forwardTargetLauncher;
private ActivityResultLauncher<String> imagePickerLauncher; private ActivityResultLauncher<String> imagePickerLauncher;
private ActivityResultLauncher<String> videoPickerLauncher; private ActivityResultLauncher<String> videoPickerLauncher;
@@ -259,6 +260,14 @@ public class ProjectDetailActivity extends BossScreenActivity {
} }
} }
); );
masterAgentEvolutionLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(),
result -> {
if (result.getResultCode() == RESULT_OK) {
reload(true);
}
}
);
forwardTargetLauncher = registerForActivityResult( forwardTargetLauncher = registerForActivityResult(
new ActivityResultContracts.StartActivityForResult(), new ActivityResultContracts.StartActivityForResult(),
result -> { result -> {
@@ -1358,12 +1367,17 @@ public class ProjectDetailActivity extends BossScreenActivity {
masterAgentTakeoverLauncher.launch(intent); masterAgentTakeoverLauncher.launch(intent);
} }
private void openMasterAgentEvolution() {
Intent intent = new Intent(this, MasterAgentEvolutionActivity.class);
masterAgentEvolutionLauncher.launch(intent);
}
private void showMasterAgentMoreMenu() { private void showMasterAgentMoreMenu() {
if (!isMasterAgentConversation()) { if (!isMasterAgentConversation()) {
return; return;
} }
new AlertDialog.Builder(this) new AlertDialog.Builder(this)
.setItems(new CharSequence[]{"模型", "推理强度", "全局接管", "提示词", "记忆", "会话信息", "刷新"}, (dialog, which) -> { .setItems(new CharSequence[]{"模型", "推理强度", "全局接管", "自动进化", "提示词", "记忆", "会话信息", "刷新"}, (dialog, which) -> {
switch (which) { switch (which) {
case 0: case 0:
showMasterAgentModelPicker(); showMasterAgentModelPicker();
@@ -1375,15 +1389,18 @@ public class ProjectDetailActivity extends BossScreenActivity {
openMasterAgentTakeoverSettings(); openMasterAgentTakeoverSettings();
break; break;
case 3: case 3:
openMasterAgentPromptProfile(); openMasterAgentEvolution();
break; break;
case 4: case 4:
openMasterAgentMemories(); openMasterAgentPromptProfile();
break; break;
case 5: case 5:
openConversationInfo(); openMasterAgentMemories();
break; break;
case 6: case 6:
openConversationInfo();
break;
case 7:
reload(true); reload(true);
break; break;
default: default:

View File

@@ -19,6 +19,10 @@ 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_memory", "主 Agent 记忆", "维护用户通用记忆和项目记忆"),
new MeMenuItem("master_agent_takeover", "全局接管", "设置主 Agent 是否默认协同推进线程"),
new MeMenuItem("master_agent_evolution", "主 Agent 自动进化", "查看进化信号、提案与自动采纳规则"),
new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"), new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"),
new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"), new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"),
new MeMenuItem("ops", "运维与修复", "查看运维会话、修复回放与 standby 切换"), new MeMenuItem("ops", "运维与修复", "查看运维会话、修复回放与 standby 切换"),

View File

@@ -210,6 +210,44 @@ public class BossApiClientDispatchPlansTest {
assertEquals("GET", connection.requestMethodValue); assertEquals("GET", connection.requestMethodValue);
} }
@Test
public void getMasterAgentEvolutionUsesDashboardEndpoint() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/master-agent/evolution"));
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.getMasterAgentEvolution();
assertEquals(200, response.statusCode);
assertEquals("/api/v1/master-agent/evolution", apiClient.lastPath);
assertEquals("GET", connection.requestMethodValue);
}
@Test
public void updateMasterAgentEvolutionModeWritesModePayload() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/master-agent/evolution/config"));
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.updateMasterAgentEvolutionMode("controlled");
assertEquals(200, response.statusCode);
assertEquals("/api/v1/master-agent/evolution/config", apiClient.lastPath);
assertEquals("POST", connection.requestMethodValue);
assertEquals("{\"mode\":\"controlled\"}", connection.requestBody());
}
@Test
public void approveMasterAgentEvolutionProposalUsesProposalEndpoint() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/master-agent/evolution/proposals/proposal-1/approve"));
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.approveMasterAgentEvolutionProposal("proposal-1");
assertEquals(200, response.statusCode);
assertEquals("/api/v1/master-agent/evolution/proposals/proposal-1/approve", apiClient.lastPath);
assertEquals("POST", connection.requestMethodValue);
assertEquals("{}", connection.requestBody());
}
@Test @Test
public void createMasterAgentMemoryWritesStructuredPayload() throws Exception { public void createMasterAgentMemoryWritesStructuredPayload() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories")); RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"));

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("我的页应是资料头 + 6 条菜单", 7, 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,6 +49,10 @@ 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, "全局接管"));
assertTrue(viewTreeContainsText(content, "主 Agent 自动进化"));
assertTrue(viewTreeContainsText(content, "账号与安全")); assertTrue(viewTreeContainsText(content, "账号与安全"));
assertTrue(viewTreeContainsText(content, "设置")); assertTrue(viewTreeContainsText(content, "设置"));
assertTrue(viewTreeContainsText(content, "运维与修复")); assertTrue(viewTreeContainsText(content, "运维与修复"));

View File

@@ -0,0 +1,79 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import android.content.Intent;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class MainActivityMeEntryNavigationTest {
@Test
public void masterAgentPromptMeEntryOpensPromptActivityForMasterAgent() {
MainActivity activity = Robolectric.buildActivity(MainActivity.class).setup().get();
ReflectionHelpers.callInstanceMethod(
activity,
"openMeEntry",
ReflectionHelpers.ClassParameter.from(String.class, "master_agent_prompt")
);
Intent started = Shadows.shadowOf(activity).getNextStartedActivity();
assertEquals(MasterAgentPromptActivity.class.getName(), started.getComponent().getClassName());
assertEquals("master-agent", started.getStringExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID));
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();
ReflectionHelpers.callInstanceMethod(
activity,
"openMeEntry",
ReflectionHelpers.ClassParameter.from(String.class, "master_agent_evolution")
);
Intent started = Shadows.shadowOf(activity).getNextStartedActivity();
assertEquals(MasterAgentEvolutionActivity.class.getName(), started.getComponent().getClassName());
}
}

View File

@@ -0,0 +1,226 @@
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;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.json.JSONArray;
import org.json.JSONObject;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.robolectric.Robolectric;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.Shadows;
import org.robolectric.annotation.Config;
import org.robolectric.util.ReflectionHelpers;
@RunWith(RobolectricTestRunner.class)
@Config(sdk = 34)
public class MasterAgentEvolutionActivityTest {
@Test
public void renderDashboardShowsModePendingProposalSignalAndRule() throws Exception {
TestMasterAgentEvolutionActivity activity = Robolectric
.buildActivity(TestMasterAgentEvolutionActivity.class, new Intent())
.setup()
.get();
JSONObject payload = new JSONObject()
.put("config", new JSONObject()
.put("mode", "autonomous")
.put("autoApplyLowRiskRules", true))
.put("signals", new JSONArray()
.put(new JSONObject()
.put("signalId", "signal-1")
.put("kind", "repeated_question")
.put("requestText", "当前主节点在线吗")
.put("createdAt", "2026-04-16T12:00:00+08:00")))
.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()
.put(new JSONObject()
.put("ruleId", "rule-1")
.put("ruleType", "routing_preference_patch")
.put("sourceProposalId", "proposal-2")
.put("createdAt", "2026-04-16T12:02:00+08:00")));
ReflectionHelpers.callInstanceMethod(
activity,
"renderDashboard",
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
);
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "完全自我进化"));
assertTrue(viewTreeContainsText(content, "待处理提案"));
assertTrue(viewTreeContainsText(content, "新增 Fast Path"));
assertTrue(viewTreeContainsText(content, "repeated_question"));
assertTrue(viewTreeContainsText(content, "routing_preference_patch"));
}
@Test
public void matchingRealtimeEventTriggersReload() throws Exception {
TestMasterAgentEvolutionActivity activity = Robolectric
.buildActivity(TestMasterAgentEvolutionActivity.class, new Intent())
.setup()
.resume()
.get();
activity.reloadEnabled = true;
activity.reloadCount = 0;
ReflectionHelpers.callInstanceMethod(
activity,
"handleRealtimeEvent",
ReflectionHelpers.ClassParameter.from(
BossRealtimeEvent.class,
new BossRealtimeEvent("master_agent.settings.updated", new JSONObject())
)
);
Shadows.shadowOf(activity.getMainLooper()).idle();
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
.buildActivity(TestMasterAgentEvolutionActivity.class, new Intent())
.setup()
.get();
ReflectionHelpers.setField(activity, "statusMessage", "提案已批准");
ReflectionHelpers.setField(activity, "statusIsError", false);
JSONObject payload = new JSONObject()
.put("config", new JSONObject()
.put("mode", "controlled")
.put("autoApplyLowRiskRules", false))
.put("signals", new JSONArray()
.put(new JSONObject()
.put("signalId", "signal-1")
.put("kind", "backend_fallback")
.put("requestText", "主节点暂时不可用")
.put("createdAt", "2026-04-16T12:00:00+08:00")))
.put("proposals", new JSONArray())
.put("rules", new JSONArray()
.put(new JSONObject()
.put("ruleId", "rule-1")
.put("ruleType", "fast_path_rule")
.put("createdAt", "2026-04-16T12:02:00+08:00")));
ReflectionHelpers.callInstanceMethod(
activity,
"renderDashboard",
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
);
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "最近操作"));
assertTrue(viewTreeContainsText(content, "提案已批准"));
assertTrue(viewTreeContainsText(content, "2026-04-16 12:00:00"));
assertTrue(viewTreeContainsText(content, "2026-04-16 12:02:00"));
}
@Test
public void renderLoadErrorStateKeepsRetryEntryVisible() throws Exception {
TestMasterAgentEvolutionActivity activity = Robolectric
.buildActivity(TestMasterAgentEvolutionActivity.class, new Intent())
.setup()
.get();
ReflectionHelpers.setField(activity, "statusMessage", "自动进化加载失败network down");
ReflectionHelpers.setField(activity, "statusIsError", true);
ReflectionHelpers.callInstanceMethod(
activity,
"renderLoadErrorState",
ReflectionHelpers.ClassParameter.from(String.class, "network down")
);
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "当前加载失败,保留在这个页面直接重试即可。"));
assertTrue(viewTreeContainsText(content, "重新加载"));
assertTrue(viewTreeContainsText(content, "自动进化中心暂时不可用network down"));
assertTrue(viewTreeContainsText(content, "最近状态"));
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
if (text != null && text.toString().contains(expectedText)) {
return true;
}
}
if (!(root instanceof ViewGroup)) {
return false;
}
ViewGroup group = (ViewGroup) root;
for (int index = 0; index < group.getChildCount(); index += 1) {
if (viewTreeContainsText(group.getChildAt(index), expectedText)) {
return true;
}
}
return false;
}
public static class TestMasterAgentEvolutionActivity extends MasterAgentEvolutionActivity {
private boolean reloadEnabled;
private int reloadCount;
@Override
protected void reload() {
if (!reloadEnabled) {
return;
}
reloadCount += 1;
setRefreshing(false);
}
}
}

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

@@ -45,10 +45,11 @@ public class ProjectDetailActivityMasterAgentMenuTest {
assertMenuItem(listView, 0, "模型"); assertMenuItem(listView, 0, "模型");
assertMenuItem(listView, 1, "推理强度"); assertMenuItem(listView, 1, "推理强度");
assertMenuItem(listView, 2, "全局接管"); assertMenuItem(listView, 2, "全局接管");
assertMenuItem(listView, 3, "提示词"); assertMenuItem(listView, 3, "自动进化");
assertMenuItem(listView, 4, "记忆"); assertMenuItem(listView, 4, "提示词");
assertMenuItem(listView, 5, "会话信息"); assertMenuItem(listView, 5, "记忆");
assertMenuItem(listView, 6, "刷新"); assertMenuItem(listView, 6, "会话信息");
assertMenuItem(listView, 7, "刷新");
} }
@Test @Test

View File

@@ -0,0 +1,21 @@
package com.hyzq.boss;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
public class WechatSurfaceMapperMeMenuTest {
@Test
public void rootMeMenuIncludesMasterAgentEvolutionEntryAfterPromptMemory() {
WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems();
assertEquals("master_agent_prompt", items[0].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);
}
}

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[]{"账号与安全", "设置", "运维与修复", "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[]{"账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"}, new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
WechatSurfaceMapper.rootMeMenuTitles() WechatSurfaceMapper.rootMeMenuTitles()
); );
} }
@@ -380,15 +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(6, items.length); assertEquals(10, items.length);
assertEquals("security", items[0].key); assertEquals("master_agent_prompt", items[0].key);
assertEquals("账号与安全", items[0].title); assertEquals("主 Agent 提示词", items[0].title);
assertEquals("settings", items[1].key); assertEquals("master_agent_memory", items[1].key);
assertEquals("ops", items[2].key); assertEquals("主 Agent 记忆", items[1].title);
assertEquals("运维与修复", items[2].title); assertEquals("master_agent_takeover", items[2].key);
assertEquals("ai_accounts", items[3].key); assertEquals("全局接管", items[2].title);
assertEquals("skills", items[4].key); assertEquals("master_agent_evolution", items[3].key);
assertEquals("about", items[5].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 @Test

View File

@@ -54,6 +54,7 @@
- `android/app/src/main/java/com/hyzq/boss/MainActivity.java`:原生入口 Activity - `android/app/src/main/java/com/hyzq/boss/MainActivity.java`:原生入口 Activity
- `android/app/src/main/java/com/hyzq/boss/BossApiClient.java`:原生 API 客户端 - `android/app/src/main/java/com/hyzq/boss/BossApiClient.java`:原生 API 客户端
- `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java`:原生聊天优先项目页,只保留目标/版本轻入口 - `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java`:原生聊天优先项目页,只保留目标/版本轻入口
- `android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java`:原生主 Agent 自动进化页,可查看信号/提案/规则并切换模式
- `android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java`:原生微信式会话信息页,支持线程改名和发起群聊 - `android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java`:原生微信式会话信息页,支持线程改名和发起群聊
- `android/app/src/main/java/com/hyzq/boss/GroupInfoActivity.java`:原生群资料页,支持群名修改与成员查看 - `android/app/src/main/java/com/hyzq/boss/GroupInfoActivity.java`:原生群资料页,支持群名修改与成员查看
- `android/app/src/main/java/com/hyzq/boss/GroupCreateActivity.java`:原生独立群聊创建页 - `android/app/src/main/java/com/hyzq/boss/GroupCreateActivity.java`:原生独立群聊创建页
@@ -155,6 +156,8 @@
- `版本迭代记录` 只读,由主 Agent 汇总 - `版本迭代记录` 只读,由主 Agent 汇总
- `我的` 根页当前保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于` - `我的` 根页当前保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
- `我的 > 主 Agent 提示词 / 记忆` 当前可编辑管理员全局主提示词、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 项目记忆 - `我的 > 主 Agent 提示词 / 记忆` 当前可编辑管理员全局主提示词、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 项目记忆
- `我的 > 主 Agent 自动进化` 当前可查看进化信号、待审批提案、已生效规则,并切换 `controlled / autonomous`
- 原生 Android 也已接上 `我的 > 主 Agent 自动进化``主 Agent 会话右上角 ... > 自动进化` 两个入口
- `我的 > AI 账号` 必须可查看和切换 `主 GPT / 备用 GPT / API 容灾` - `我的 > AI 账号` 必须可查看和切换 `主 GPT / 备用 GPT / API 容灾`
- `我的 > 技能` 必须按绑定设备展示 Skill并支持一键复制调用语句 - `我的 > 技能` 必须按绑定设备展示 Skill并支持一键复制调用语句
- `设备` 页当前只允许出现生产设备,旧演示脏数据不能回流到正式视图 - `设备` 页当前只允许出现生产设备,旧演示脏数据不能回流到正式视图

View File

@@ -27,6 +27,7 @@
- 当前原生活动页: - 当前原生活动页:
- `MainActivity` - `MainActivity`
- `ProjectDetailActivity` - `ProjectDetailActivity`
- `MasterAgentEvolutionActivity`
- `ConversationInfoActivity` - `ConversationInfoActivity`
- `ThreadStatusActivity` - `ThreadStatusActivity`
- `GroupInfoActivity` - `GroupInfoActivity`
@@ -74,7 +75,7 @@
- 保留版本与 OTA 操作 - 保留版本与 OTA 操作
- 当前已补上 OTA 下载进度、失败重试、安装授权提示和返回关于页后的本地状态恢复 - 当前已补上 OTA 下载进度、失败重试、安装授权提示和返回关于页后的本地状态恢复
- 当前 `我的` 根页: - 当前 `我的` 根页:
- 保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于` - 保留 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化 / 账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
- `运维与修复` 直接进入 `OpsCenterActivity` - `运维与修复` 直接进入 `OpsCenterActivity`
- 当前 `OpenAiOnboardingActivity` - 当前 `OpenAiOnboardingActivity`
- 会先自动打开 `OpenAI Platform` 登录页 - 会先自动打开 `OpenAI Platform` 登录页
@@ -149,7 +150,7 @@
- `GET /me/settings` - `GET /me/settings`
- `GET /me/skills` - `GET /me/skills`
- `GET /me/master-agent` - `GET /me/master-agent`
- `GET /me/master-agent` - `GET /me/master-agent/evolution`
## 3. Web API 路由 ## 3. Web API 路由

View File

@@ -1,6 +1,6 @@
# Boss 当前运行与部署状态 # Boss 当前运行与部署状态
更新时间:`2026-04-03` 更新时间:`2026-04-16`
## 1. 本地状态 ## 1. 本地状态
@@ -134,7 +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`、批准或拒绝提案
- 当前原生 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` 备用账号,不再把失败日志直接原样回给用户
@@ -164,6 +166,8 @@ cd /Users/kris/code/boss
- 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本 - 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本
- 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride` 强制覆盖,也支持 `fastModelOverride / fastReasoningEffortOverride / smartModelOverride / smartReasoningEffortOverride` 这组策略默认值;主 Agent 普通对话默认按 fast 档选模型,深度任务可按 smart 档选模型,手动强制覆盖仍然优先级最高 - 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride` 强制覆盖,也支持 `fastModelOverride / fastReasoningEffortOverride / smartModelOverride / smartReasoningEffortOverride` 这组策略默认值;主 Agent 普通对话默认按 fast 档选模型,深度任务可按 smart 档选模型,手动强制覆盖仍然优先级最高
- 当前主 Agent 自动进化引擎已接入第一阶段共享内核:`masterAgentEvolutionConfig / Signals / Proposals / Rules / RunLogs` 已进入状态模型,`GET /api/v1/master-agent/evolution` 可查看进化状态,`POST /api/v1/master-agent/evolution/config` 可在 `controlled / autonomous` 间切换,提案支持 approve / reject`controlled` 只生成待审批提案,`autonomous` 可自动采纳低风险 `memory_patch / routing_preference_patch / fast_path_rule` - 当前主 Agent 自动进化引擎已接入第一阶段共享内核:`masterAgentEvolutionConfig / Signals / Proposals / Rules / RunLogs` 已进入状态模型,`GET /api/v1/master-agent/evolution` 可查看进化状态,`POST /api/v1/master-agent/evolution/config` 可在 `controlled / autonomous` 间切换,提案支持 approve / reject`controlled` 只生成待审批提案,`autonomous` 可自动采纳低风险 `memory_patch / routing_preference_patch / fast_path_rule`
- 当前 `codex/master-agent-autonomous-evolution` 分支把默认 evolution mode 改成 `autonomous`,新初始化状态会默认开启低风险自动采纳;`codex/master-agent-controlled-evolution` 保持 `controlled` 默认值
- 当前主 Agent 会在 fast path 判断前捕获状态类问题信号;重复短问句会沉淀为 `repeated_question`,显式后端回退会附带 `fallbackToBackendId`,在 autonomous 模式下可自动写回 `backendOverride`
- 当前 `group_dispatch_plan / device_import_resolution / attachment_analysis` 三类深度任务已经会把 `smart*` 策略下发到任务队列,并随任务持久化 `executionModel / executionReasoningEffort`local-agent 执行这类任务时会优先吃任务级模型,不再只依赖本机固定默认模型 - 当前 `group_dispatch_plan / device_import_resolution / attachment_analysis` 三类深度任务已经会把 `smart*` 策略下发到任务队列,并随任务持久化 `executionModel / executionReasoningEffort`local-agent 执行这类任务时会优先吃任务级模型,不再只依赖本机固定默认模型
- 当前对话级 `agentControls` 也已支持 `backendOverride``master-agent` 会话可在 `Claw Runtime``Hermes Runtime` 可用时显式选择 `claw-runtime / hermes-runtime`,普通单线程会话当前只开放 `hermes-runtime`;不可用时保存接口会直接拒绝,并返回人类可读原因 - 当前对话级 `agentControls` 也已支持 `backendOverride``master-agent` 会话可在 `Claw Runtime``Hermes Runtime` 可用时显式选择 `claw-runtime / hermes-runtime`,普通单线程会话当前只开放 `hermes-runtime`;不可用时保存接口会直接拒绝,并返回人类可读原因
- 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示 - 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示

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

@@ -0,0 +1,36 @@
import { RealtimeRefresh } from "@/components/app-runtime";
import { AppShell, PageNav, StatusBar } from "@/components/app-ui";
import { MasterAgentEvolutionClient } from "@/components/master-agent-evolution-client";
import { requirePageSession } from "@/lib/boss-auth";
import { getMasterAgentEvolutionDashboard } from "@/lib/master-agent-evolution";
export const dynamic = "force-dynamic";
export default async function MasterAgentEvolutionPage() {
const session = await requirePageSession();
const dashboard = await getMasterAgentEvolutionDashboard();
return (
<AppShell bottomNav={false}>
<RealtimeRefresh events={["master_agent.settings.updated"]} />
<StatusBar />
<PageNav title="主 Agent 自动进化" backHref="/me" />
<div className="px-[18px] pb-3">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4 text-[13px] leading-6 text-[#57606A]">
Agent
<br />
{session.role === "highest_admin"
? "你是管理员,可以在这里切换受控/全自动模式,并直接审批提案。"
: "你当前是只读视角,可以查看主 Agent 正在学什么。"}
</div>
</div>
<MasterAgentEvolutionClient
isAdmin={session.role === "highest_admin"}
config={dashboard.config}
signals={dashboard.signals}
proposals={dashboard.proposals}
rules={dashboard.rules}
/>
</AppShell>
);
}

View File

@@ -28,6 +28,11 @@ export default async function MePage() {
title="主 Agent 提示词 / 记忆" title="主 Agent 提示词 / 记忆"
description="配置全局主提示词、当前主提示词和用户记忆" description="配置全局主提示词、当前主提示词和用户记忆"
/> />
<MenuRow
href="/me/master-agent/evolution"
title="主 Agent 自动进化"
description="查看进化信号、提案、规则,并切换受控 / 全自动模式"
/>
<MenuRow <MenuRow
href="/me/storage" href="/me/storage"
title="附件与存储" title="附件与存储"

View File

@@ -0,0 +1,237 @@
"use client";
import { useState } from "react";
import { useRouter } from "next/navigation";
import clsx from "clsx";
type EvolutionConfig = {
mode: "controlled" | "autonomous";
autoApplyLowRiskRules: boolean;
};
type EvolutionSignal = {
signalId: string;
kind: string;
requestText: string;
createdAt: string;
};
type EvolutionProposal = {
proposalId: string;
proposalType: string;
title: string;
summary: string;
status: string;
confidence: number;
riskLevel: string;
createdAt: string;
};
type EvolutionRule = {
ruleId: string;
ruleType: string;
createdAt: string;
sourceProposalId?: string;
};
function formatTime(value?: string) {
if (!value) {
return "-";
}
return new Date(value).toLocaleString("zh-CN", { hour12: false });
}
async function readJson<T>(response: Response): Promise<T> {
return (await response.json()) as T;
}
export function MasterAgentEvolutionClient({
isAdmin,
config,
signals,
proposals,
rules,
}: {
isAdmin: boolean;
config: EvolutionConfig;
signals: EvolutionSignal[];
proposals: EvolutionProposal[];
rules: EvolutionRule[];
}) {
const router = useRouter();
const [busyKey, setBusyKey] = useState<string | null>(null);
const [message, setMessage] = useState("");
async function switchMode(mode: "controlled" | "autonomous") {
setBusyKey(`mode:${mode}`);
const response = await fetch("/api/v1/master-agent/evolution/config", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ mode }),
});
const result = await readJson<{ ok: boolean; message?: string }>(response);
setBusyKey(null);
setMessage(result.ok ? `已切到 ${mode === "autonomous" ? "完全自我进化" : "受控自动进化"}` : result.message ?? "切换失败。");
if (result.ok) {
router.refresh();
}
}
async function reviewProposal(proposalId: string, action: "approve" | "reject") {
setBusyKey(`${action}:${proposalId}`);
const response = await fetch(`/api/v1/master-agent/evolution/proposals/${proposalId}/${action}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({}),
});
const result = await readJson<{ ok: boolean; message?: string }>(response);
setBusyKey(null);
setMessage(result.ok ? (action === "approve" ? "提案已批准。" : "提案已拒绝。") : result.message ?? "提交失败。");
if (result.ok) {
router.refresh();
}
}
const pendingProposals = proposals.filter((item) => item.status === "pending_review");
return (
<div className="flex flex-col gap-4 px-[18px] pb-6">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div>
<div className="text-[16px] font-semibold text-[#111111]"></div>
<div className="mt-1 text-[12px] leading-6 text-[#8C8C8C]">
{config.mode === "autonomous" ? "完全自我进化" : "受控自动进化"}
{config.autoApplyLowRiskRules ? "低风险提案会自动采纳。" : "所有提案都需要人工确认。"}
</div>
</div>
<span
className={clsx(
"rounded-full px-3 py-1 text-[11px] font-semibold",
config.mode === "autonomous" ? "bg-[#EEF8F1] text-[#117A37]" : "bg-[#FFF5E8] text-[#B54708]",
)}
>
{config.mode === "autonomous" ? "全自动" : "受控"}
</span>
</div>
{isAdmin ? (
<div className="mt-4 grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void switchMode("controlled")}
disabled={busyKey === "mode:controlled" || config.mode === "controlled"}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === "mode:controlled" ? "切换中" : "切到受控模式"}
</button>
<button
type="button"
onClick={() => void switchMode("autonomous")}
disabled={busyKey === "mode:autonomous" || config.mode === "autonomous"}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === "mode:autonomous" ? "切换中" : "切到全自动模式"}
</button>
</div>
) : null}
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{pendingProposals.length}
</span>
</div>
<div className="mt-3 space-y-3">
{pendingProposals.length === 0 ? (
<div className="rounded-xl bg-[#F7F8FA] px-3 py-3 text-[13px] text-[#8C8C8C]"></div>
) : (
pendingProposals.map((proposal) => (
<div key={proposal.proposalId} className="rounded-xl border border-[#EAECEF] px-3 py-3">
<div className="flex items-start justify-between gap-3">
<div>
<div className="text-[14px] font-semibold text-[#111111]">{proposal.title}</div>
<div className="mt-1 text-[12px] leading-6 text-[#57606A]">{proposal.summary}</div>
</div>
<span className="rounded-full bg-[#FFF5E8] px-2 py-1 text-[11px] font-semibold text-[#B54708]">
{proposal.riskLevel} / {Math.round(proposal.confidence * 100)}%
</span>
</div>
<div className="mt-2 text-[12px] text-[#8C8C8C]">
{proposal.proposalType} · {formatTime(proposal.createdAt)}
</div>
{isAdmin ? (
<div className="mt-3 grid grid-cols-2 gap-3">
<button
type="button"
onClick={() => void reviewProposal(proposal.proposalId, "reject")}
disabled={busyKey === `reject:${proposal.proposalId}`}
className="rounded-full border border-[#E5E5EA] bg-white px-4 py-2 text-[13px] font-semibold text-[#111111] disabled:opacity-60"
>
{busyKey === `reject:${proposal.proposalId}` ? "处理中" : "拒绝"}
</button>
<button
type="button"
onClick={() => void reviewProposal(proposal.proposalId, "approve")}
disabled={busyKey === `approve:${proposal.proposalId}`}
className="rounded-full bg-[#07C160] px-4 py-2 text-[13px] font-semibold text-white disabled:opacity-60"
>
{busyKey === `approve:${proposal.proposalId}` ? "处理中" : "批准"}
</button>
</div>
) : null}
</div>
))
)}
</div>
</div>
<div className="grid gap-4 md:grid-cols-2">
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{signals.length}
</span>
</div>
<div className="mt-3 space-y-3">
{signals.slice(0, 8).map((signal) => (
<div key={signal.signalId} className="rounded-xl bg-[#F7F8FA] px-3 py-3">
<div className="text-[13px] font-semibold text-[#111111]">{signal.kind}</div>
<div className="mt-1 text-[12px] leading-6 text-[#57606A]">{signal.requestText}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">{formatTime(signal.createdAt)}</div>
</div>
))}
</div>
</div>
<div className="rounded-2xl border border-[#E5E5EA] bg-white px-4 py-4">
<div className="flex items-center justify-between gap-3">
<div className="text-[16px] font-semibold text-[#111111]"></div>
<span className="rounded-full bg-[#F2F3F5] px-3 py-1 text-[11px] font-semibold text-[#57606A]">
{rules.length}
</span>
</div>
<div className="mt-3 space-y-3">
{rules.slice(0, 8).map((rule) => (
<div key={rule.ruleId} className="rounded-xl bg-[#F7F8FA] px-3 py-3">
<div className="text-[13px] font-semibold text-[#111111]">{rule.ruleType}</div>
<div className="mt-1 text-[12px] text-[#8C8C8C]">
{formatTime(rule.createdAt)}
{rule.sourceProposalId ? ` · ${rule.sourceProposalId}` : ""}
</div>
</div>
))}
</div>
</div>
</div>
{message ? (
<div className="rounded-2xl border border-[#D8E8FF] bg-[#F4F8FF] px-4 py-3 text-[13px] text-[#2457C5]">
{message}
</div>
) : null}
</div>
);
}

View File

@@ -1537,8 +1537,8 @@ const initialState: BossState = {
userMasterPrompts: [], userMasterPrompts: [],
masterAgentMemories: [], masterAgentMemories: [],
masterAgentEvolutionConfig: { masterAgentEvolutionConfig: {
mode: "controlled", mode: "autonomous",
autoApplyLowRiskRules: false, autoApplyLowRiskRules: true,
updatedAt: nowIso(), updatedAt: nowIso(),
}, },
masterAgentEvolutionSignals: [], masterAgentEvolutionSignals: [],

View File

@@ -1726,10 +1726,6 @@ type MasterAgentFastIntentContext = {
effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>; effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>;
}; };
function buildMasterAgentRuntimeBackendLabel(context: MasterAgentFastIntentContext) {
return context.agentControls?.backendOverride?.trim() || "master-codex-node";
}
function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) { function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) {
const deviceId = const deviceId =
context.runtime.account.nodeId?.trim() || context.runtime.account.nodeId?.trim() ||
@@ -1952,7 +1948,7 @@ async function appendFastPathError(
} }
async function tryRecordMasterAgentEvolutionSignal(input: { async function tryRecordMasterAgentEvolutionSignal(input: {
kind: "fast_path_candidate" | "user_correction" | "backend_fallback"; kind: "fast_path_candidate" | "repeated_question" | "user_correction" | "backend_fallback";
account: string; account: string;
requestText: string; requestText: string;
replyText?: string; replyText?: string;
@@ -1972,6 +1968,47 @@ async function tryRecordMasterAgentEvolutionSignal(input: {
} }
} }
function isMasterAgentEvolutionStatusLikeRequest(requestText: string) {
return /(当前|现在|有没有|是否|哪个|什么|在线吗|状态)/i.test(requestText);
}
function normalizeMasterAgentEvolutionQuestion(requestText: string) {
return normalizeLexicalText(requestText)
.replace(/当前|现在|目前|一下|帮我|请问|请/g, "")
.replace(/吗|呢|呀|啊/g, "")
.trim();
}
async function isRepeatedMasterAgentStatusQuestion(params: {
requestText: string;
requestMessageId?: string;
}) {
const normalizedRequest = normalizeMasterAgentEvolutionQuestion(params.requestText);
if (!normalizedRequest) {
return false;
}
const state = await readState();
const masterProject = state.projects.find((project) => project.id === "master-agent");
if (!masterProject) {
return false;
}
const priorUserMessages = [...masterProject.messages]
.reverse()
.filter((message) => message.sender === "user" && message.id !== params.requestMessageId)
.slice(0, 8);
return priorUserMessages.some((message) => {
const normalizedBody = normalizeMasterAgentEvolutionQuestion(message.body);
if (!normalizedBody) {
return false;
}
return (
normalizedBody === normalizedRequest ||
normalizedBody.includes(normalizedRequest) ||
normalizedRequest.includes(normalizedBody)
);
});
}
function buildModelSummaryReply(context: MasterAgentFastIntentContext, requestText: string) { function buildModelSummaryReply(context: MasterAgentFastIntentContext, requestText: string) {
const normalized = normalizeLexicalText(requestText); const normalized = normalizeLexicalText(requestText);
const manualModel = context.agentControls?.modelOverride?.trim() || ""; const manualModel = context.agentControls?.modelOverride?.trim() || "";
@@ -3250,6 +3287,22 @@ export async function replyToMasterAgentUserMessage(params: {
currentSessionExpiresAt?: string; currentSessionExpiresAt?: string;
mode?: "wait" | "enqueue"; mode?: "wait" | "enqueue";
}) { }) {
if (isMasterAgentEvolutionStatusLikeRequest(params.requestText)) {
const repeatedQuestion = await isRepeatedMasterAgentStatusQuestion({
requestText: params.requestText,
requestMessageId: params.requestMessageId,
});
await tryRecordMasterAgentEvolutionSignal({
kind: repeatedQuestion ? "repeated_question" : "fast_path_candidate",
account: params.requestedByAccount,
requestText: params.requestText,
metadataJson: {
source: "replyToMasterAgentUserMessage.pre_fast_intent",
repeatedQuestion,
},
});
}
const fastIntentResult = await tryHandleMasterAgentFastIntent({ const fastIntentResult = await tryHandleMasterAgentFastIntent({
requestText: params.requestText, requestText: params.requestText,
requestedByAccount: params.requestedByAccount, requestedByAccount: params.requestedByAccount,
@@ -3258,17 +3311,6 @@ export async function replyToMasterAgentUserMessage(params: {
return fastIntentResult; return fastIntentResult;
} }
if (/(当前|现在|有没有|是否|哪个|什么|在线吗|状态)/i.test(params.requestText)) {
await tryRecordMasterAgentEvolutionSignal({
kind: "fast_path_candidate",
account: params.requestedByAccount,
requestText: params.requestText,
metadataJson: {
source: "replyToMasterAgentUserMessage.pre_slow_path",
},
});
}
const runtime = await getMasterAgentRuntimeAccount(); const runtime = await getMasterAgentRuntimeAccount();
if (!runtime?.account) { if (!runtime?.account) {
@@ -3311,6 +3353,22 @@ export async function replyToMasterAgentUserMessage(params: {
}; };
const selectedBackend = await selectExecutionBackend(backendSelectionInput); const selectedBackend = await selectExecutionBackend(backendSelectionInput);
const backendChoices = listExecutionBackendChoices(backendSelectionInput); const backendChoices = listExecutionBackendChoices(backendSelectionInput);
const requestedBackendId = executionConfig.agentControls?.backendOverride?.trim() || "";
if (
requestedBackendId &&
selectedBackend.backendId !== requestedBackendId &&
(selectedBackend.backendId === CLAW_BACKEND_ID || selectedBackend.backendId === HERMES_BACKEND_ID)
) {
await tryRecordMasterAgentEvolutionSignal({
kind: "backend_fallback",
account: params.requestedByAccount,
requestText: params.requestText,
metadataJson: {
requestedBackendId,
fallbackToBackendId: selectedBackend.backendId,
},
});
}
const agentControls = executionConfig.agentControls; const agentControls = executionConfig.agentControls;
const masterExecutionPrompt = buildMasterCodexNodePrompt( const masterExecutionPrompt = buildMasterCodexNodePrompt(
state, state,

View File

@@ -6,11 +6,12 @@ export const MASTER_AGENT_CHAT_PAGE_ANCHORS = {
} as const; } as const;
export const MASTER_AGENT_TAKEOVER_PAGE_HREF = "/me/master-agent/takeover"; export const MASTER_AGENT_TAKEOVER_PAGE_HREF = "/me/master-agent/takeover";
export const MASTER_AGENT_EVOLUTION_PAGE_HREF = "/me/master-agent/evolution";
export type MasterAgentChatPageAnchors = typeof MASTER_AGENT_CHAT_PAGE_ANCHORS; export type MasterAgentChatPageAnchors = typeof MASTER_AGENT_CHAT_PAGE_ANCHORS;
export type MasterAgentChatMenuItem = { export type MasterAgentChatMenuItem = {
key: "prompt" | "model" | "reasoning_effort" | "takeover" | "memory" | "refresh"; key: "prompt" | "model" | "reasoning_effort" | "takeover" | "evolution" | "memory" | "refresh";
label: string; label: string;
href?: string; href?: string;
action?: "refresh"; action?: "refresh";
@@ -37,6 +38,11 @@ export function getMasterAgentChatMenuItems(projectId: string): MasterAgentChatM
label: "全局接管", label: "全局接管",
href: MASTER_AGENT_TAKEOVER_PAGE_HREF, href: MASTER_AGENT_TAKEOVER_PAGE_HREF,
}, },
{
key: "evolution",
label: "自动进化",
href: MASTER_AGENT_EVOLUTION_PAGE_HREF,
},
{ {
key: "prompt", key: "prompt",
label: "提示词", label: "提示词",

View File

@@ -31,6 +31,7 @@ function inferProposalFromSignal(input: {
projectId: string; projectId: string;
requestText: string; requestText: string;
signalId: string; signalId: string;
metadataJson?: Record<string, unknown>;
}): Omit< }): Omit<
Parameters<typeof createMasterAgentEvolutionProposalInState>[0], Parameters<typeof createMasterAgentEvolutionProposalInState>[0],
"status" "status"
@@ -76,6 +77,11 @@ function inferProposalFromSignal(input: {
} }
if (input.kind === "backend_fallback") { if (input.kind === "backend_fallback") {
const fallbackToBackendId =
input.metadataJson?.fallbackToBackendId === "hermes-runtime" ||
input.metadataJson?.fallbackToBackendId === "claw-runtime"
? input.metadataJson.fallbackToBackendId
: undefined;
return { return {
proposalType: "routing_preference_patch", proposalType: "routing_preference_patch",
account: input.account, account: input.account,
@@ -84,9 +90,10 @@ function inferProposalFromSignal(input: {
summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。", summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。",
patchJson: { patchJson: {
backendPreference: "prefer_available_runtime", backendPreference: "prefer_available_runtime",
backendOverride: fallbackToBackendId,
}, },
sourceSignalIds: [input.signalId], sourceSignalIds: [input.signalId],
confidence: 0.72, confidence: fallbackToBackendId ? 0.8 : 0.72,
riskLevel: "low", riskLevel: "low",
}; };
} }
@@ -193,6 +200,7 @@ export async function recordMasterAgentEvolutionSignal(input: {
projectId: signal.projectId, projectId: signal.projectId,
requestText: signal.requestText, requestText: signal.requestText,
signalId: signal.signalId, signalId: signal.signalId,
metadataJson: signal.metadataJson,
}); });
if (!proposalInput) { if (!proposalInput) {
return { signal, proposal: null }; return { signal, proposal: null };

View File

@@ -34,6 +34,7 @@ test("master agent settings pages refresh when master agent config changes", asy
for (const relativePath of [ for (const relativePath of [
"src/app/me/master-agent/page.tsx", "src/app/me/master-agent/page.tsx",
"src/app/me/master-agent/takeover/page.tsx", "src/app/me/master-agent/takeover/page.tsx",
"src/app/me/master-agent/evolution/page.tsx",
]) { ]) {
const source = await readSource(relativePath); const source = await readSource(relativePath);
assert.match(source, /import \{ RealtimeRefresh \}/, `expected ${relativePath} to import RealtimeRefresh`); assert.match(source, /import \{ RealtimeRefresh \}/, `expected ${relativePath} to import RealtimeRefresh`);
@@ -45,3 +46,27 @@ test("master agent settings pages refresh when master agent config changes", asy
); );
} }
}); });
test("me page exposes master agent evolution entry", async () => {
const source = await readSource("src/app/me/page.tsx");
assert.match(source, /href="\/me\/master-agent\/evolution"/, "expected me page to link evolution page");
assert.match(source, /title="主 Agent 自动进化"/, "expected me page to show evolution menu title");
});
test("master agent evolution page renders admin-aware dashboard shell", async () => {
const pageSource = await readSource("src/app/me/master-agent/evolution/page.tsx");
const clientSource = await readSource("src/components/master-agent-evolution-client.tsx");
assert.match(pageSource, /getMasterAgentEvolutionDashboard/, "expected page to load evolution dashboard server-side");
assert.match(pageSource, /session\.role === "highest_admin"/, "expected page to gate admin actions by role");
assert.match(pageSource, /<MasterAgentEvolutionClient/, "expected page to render evolution client");
assert.match(clientSource, /待处理提案/, "expected dashboard to expose pending proposals");
assert.match(clientSource, /最近信号/, "expected dashboard to expose recent signals");
assert.match(clientSource, /已生效规则/, "expected dashboard to expose applied rules");
assert.match(clientSource, /\/api\/v1\/master-agent\/evolution\/config/, "expected mode switch API to be wired");
assert.match(
clientSource,
/\/api\/v1\/master-agent\/evolution\/proposals\/\$\{proposalId\}\/\$\{action\}/,
"expected proposal review API to be wired",
);
});

View File

@@ -2,18 +2,19 @@ import test from "node:test";
import assert from "node:assert/strict"; import assert from "node:assert/strict";
import { getMasterAgentChatMenuItems } from "../src/lib/master-agent-chat-menu"; import { getMasterAgentChatMenuItems } from "../src/lib/master-agent-chat-menu";
test("master-agent 聊天页菜单包含全局接管、提示词、模型、推理强度、记忆和刷新", () => { test("master-agent 聊天页菜单包含全局接管、进化、提示词、模型、推理强度、记忆和刷新", () => {
const items = getMasterAgentChatMenuItems("master-agent"); const items = getMasterAgentChatMenuItems("master-agent");
assert.deepEqual( assert.deepEqual(
items.map((item) => item.key), items.map((item) => item.key),
["model", "reasoning_effort", "takeover", "prompt", "memory", "refresh"], ["model", "reasoning_effort", "takeover", "evolution", "prompt", "memory", "refresh"],
); );
assert.equal(items[0]?.href, "/me/master-agent#model-section"); assert.equal(items[0]?.href, "/me/master-agent#model-section");
assert.equal(items[1]?.href, "/me/master-agent#reasoning-effort-section"); assert.equal(items[1]?.href, "/me/master-agent#reasoning-effort-section");
assert.equal(items[2]?.href, "/me/master-agent/takeover"); assert.equal(items[2]?.href, "/me/master-agent/takeover");
assert.equal(items[3]?.href, "/me/master-agent#prompt-section"); assert.equal(items[3]?.href, "/me/master-agent/evolution");
assert.equal(items[4]?.href, "/me/master-agent#memory-section"); assert.equal(items[4]?.href, "/me/master-agent#prompt-section");
assert.equal(items[5]?.action, "refresh"); assert.equal(items[5]?.href, "/me/master-agent#memory-section");
assert.equal(items[6]?.action, "refresh");
}); });
test("普通会话不返回主 Agent 专属菜单", () => { test("普通会话不返回主 Agent 专属菜单", () => {

View File

@@ -6,6 +6,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
let runtimeRoot = ""; let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"]; let readState: (typeof import("../src/lib/boss-data"))["readState"];
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
let recordMasterAgentEvolutionSignal: (typeof import("../src/lib/master-agent-evolution"))["recordMasterAgentEvolutionSignal"]; let recordMasterAgentEvolutionSignal: (typeof import("../src/lib/master-agent-evolution"))["recordMasterAgentEvolutionSignal"];
let listMasterAgentEvolutionProposals: (typeof import("../src/lib/master-agent-evolution"))["listMasterAgentEvolutionProposals"]; let listMasterAgentEvolutionProposals: (typeof import("../src/lib/master-agent-evolution"))["listMasterAgentEvolutionProposals"];
let setMasterAgentEvolutionMode: (typeof import("../src/lib/master-agent-evolution"))["setMasterAgentEvolutionMode"]; let setMasterAgentEvolutionMode: (typeof import("../src/lib/master-agent-evolution"))["setMasterAgentEvolutionMode"];
@@ -22,6 +23,7 @@ async function setup() {
import("../src/lib/master-agent-evolution.ts"), import("../src/lib/master-agent-evolution.ts"),
]); ]);
readState = data.readState; readState = data.readState;
getProjectAgentControls = data.getProjectAgentControls;
recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal; recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal;
listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals; listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals;
setMasterAgentEvolutionMode = evolution.setMasterAgentEvolutionMode; setMasterAgentEvolutionMode = evolution.setMasterAgentEvolutionMode;
@@ -40,6 +42,8 @@ test.beforeEach(async () => {
}); });
test("recording repeated deterministic questions creates a pending fast_path_rule proposal in controlled mode", async () => { test("recording repeated deterministic questions creates a pending fast_path_rule proposal in controlled mode", async () => {
await setMasterAgentEvolutionMode("controlled");
const result = await recordMasterAgentEvolutionSignal({ const result = await recordMasterAgentEvolutionSignal({
kind: "repeated_question", kind: "repeated_question",
projectId: "master-agent", projectId: "master-agent",
@@ -71,3 +75,25 @@ test("autonomous mode auto applies low risk fast path proposals as evolution rul
assert.equal(state.masterAgentEvolutionRules.length, 1); assert.equal(state.masterAgentEvolutionRules.length, 1);
assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "fast_path_rule"); assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "fast_path_rule");
}); });
test("autonomous mode auto applies backend fallback proposals into master-agent backend override", async () => {
await setMasterAgentEvolutionMode("autonomous");
const result = await recordMasterAgentEvolutionSignal({
kind: "backend_fallback",
projectId: "master-agent",
account: "17600003315",
requestText: "Hermes 不可用时自动回退到 Claw",
metadataJson: {
failedBackendId: "hermes-runtime",
fallbackToBackendId: "claw-runtime",
},
});
assert.equal(result.proposal?.status, "auto_applied");
const state = await readState();
const controls = await getProjectAgentControls("master-agent", "17600003315");
assert.equal(state.masterAgentEvolutionRules.length, 1);
assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "routing_preference_patch");
assert.equal(controls?.backendOverride ?? null, "claw-runtime");
});

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));
}); });

View File

@@ -32,10 +32,10 @@ test.beforeEach(async () => {
await mkdir(runtimeRoot, { recursive: true }); await mkdir(runtimeRoot, { recursive: true });
}); });
test("boss state 初始化时包含 master agent evolution 默认配置与空集合", async () => { test("boss state 初始化时包含 autonomous 默认配置与空集合", async () => {
const state = await readState(); const state = await readState();
assert.equal(state.masterAgentEvolutionConfig.mode, "controlled"); assert.equal(state.masterAgentEvolutionConfig.mode, "autonomous");
assert.equal(state.masterAgentEvolutionConfig.autoApplyLowRiskRules, false); assert.equal(state.masterAgentEvolutionConfig.autoApplyLowRiskRules, true);
assert.deepEqual(state.masterAgentEvolutionSignals, []); assert.deepEqual(state.masterAgentEvolutionSignals, []);
assert.deepEqual(state.masterAgentEvolutionProposals, []); assert.deepEqual(state.masterAgentEvolutionProposals, []);
assert.deepEqual(state.masterAgentEvolutionRules, []); assert.deepEqual(state.masterAgentEvolutionRules, []);

View File

@@ -359,6 +359,43 @@ test("master-agent 查询当前后端时直接走 fast path 返回后端摘要",
assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4"); assert.equal(reply?.senderLabel ?? "", "主Agent·gpt-5.4");
}); });
test("master-agent 重复追问状态类问题时会记录 repeated_question 进化信号", async () => {
await saveAiAccount({
accountId: "openai-repeat-status",
label: "OpenAI 主模型",
role: "primary",
provider: "openai_api",
displayName: "OpenAI 主模型",
model: "gpt-5.4",
apiKey: "sk-openai-repeat-status",
enabled: true,
setActive: true,
loginStatusNote: "用于重复状态问题测试。",
});
const firstResponse = await POST(
await createAuthedRequest("master-agent", {
body: "当前主节点在线吗",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(firstResponse.status, 200);
const secondResponse = await POST(
await createAuthedRequest("master-agent", {
body: "现在主节点在线吗",
}),
{ params: Promise.resolve({ projectId: "master-agent" }) },
);
assert.equal(secondResponse.status, 200);
const state = await readState();
assert.ok(
state.masterAgentEvolutionSignals.some((signal) => signal.kind === "repeated_question"),
"expected at least one repeated_question signal",
);
});
test("master-agent 查询全局接管状态时直接走 fast path 返回当前状态", async () => { test("master-agent 查询全局接管状态时直接走 fast path 返回当前状态", async () => {
await saveAiAccount({ await saveAiAccount({
accountId: "openai-takeover-status", accountId: "openai-takeover-status",