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

View File

@@ -54,6 +54,7 @@
<activity android:name=".MasterAgentPromptActivity" 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=".MasterAgentEvolutionActivity" 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" />

View File

@@ -265,6 +265,32 @@ public class BossApiClient {
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 {
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/memories", payload);
}

View File

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

View File

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

View File

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

View File

@@ -19,6 +19,10 @@ public final class WechatSurfaceMapper {
);
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("settings", "设置", "默认首页、提醒方式与危险操作确认"),
new MeMenuItem("ops", "运维与修复", "查看运维会话、修复回放与 standby 切换"),

View File

@@ -210,6 +210,44 @@ public class BossApiClientDispatchPlansTest {
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
public void createMasterAgentMemoryWritesStructuredPayload() throws Exception {
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");
LinearLayout content = ReflectionHelpers.getField(activity, "screenContent");
assertEquals("我的页应是资料头 + 6 条菜单", 7, content.getChildCount());
assertEquals("我的页应是资料头 + 10 条菜单", 11, content.getChildCount());
View header = content.getChildAt(0);
assertEquals("资料头不应保留浮层卡片感", 0f, header.getElevation(), 0.01f);
@@ -49,6 +49,10 @@ public class BossUiRootSurfaceTest {
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, "运维与修复"));

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 启动脚本"));
}
@Test
public void renderPromptProfileIncludesHermesRuntimeWhenSelectable() throws Exception {
TestMasterAgentPromptActivity activity = Robolectric
.buildActivity(
TestMasterAgentPromptActivity.class,
new Intent()
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent")
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent")
)
.setup()
.get();
JSONObject payload = new JSONObject()
.put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词"))
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
.put("projectControls", new JSONObject()
.put("promptOverride", "当前对话提示词")
.put("backendOverride", "hermes-runtime"))
.put("clawAvailability", new JSONObject()
.put("status", "ready")
.put("selectable", true)
.put("reasonLabel", "Claw Runtime 可用。"))
.put("hermesAvailability", new JSONObject()
.put("status", "ready")
.put("selectable", true)
.put("reasonLabel", "Hermes Runtime 可用。"));
ReflectionHelpers.callInstanceMethod(
activity,
"renderPromptProfile",
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
);
Spinner backendSpinner = ReflectionHelpers.getField(activity, "backendSpinner");
assertEquals(3, backendSpinner.getAdapter().getCount());
assertEquals(2, backendSpinner.getSelectedItemPosition());
assertEquals("Hermes Runtime", String.valueOf(backendSpinner.getAdapter().getItem(2)));
TextView previewTextView = ReflectionHelpers.getField(activity, "previewTextView");
assertTrue(String.valueOf(previewTextView.getText()).contains("【执行后端】"));
assertTrue(String.valueOf(previewTextView.getText()).contains("hermes-runtime"));
}
@Test
public void renderPromptProfileShowsHermesUnavailableHintWhenStoredOverrideCannotBeSelected() throws Exception {
TestMasterAgentPromptActivity activity = Robolectric
.buildActivity(
TestMasterAgentPromptActivity.class,
new Intent()
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, "master-agent")
.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, "主 Agent")
)
.setup()
.get();
JSONObject payload = new JSONObject()
.put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词"))
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
.put("projectControls", new JSONObject()
.put("promptOverride", "当前对话提示词")
.put("backendOverride", "hermes-runtime"))
.put("hermesAvailability", new JSONObject()
.put("status", "misconfigured")
.put("selectable", false)
.put("reason", "script_not_found")
.put("reasonLabel", "未检测到有效的 Hermes 启动脚本,将自动回退到默认后端。"));
ReflectionHelpers.callInstanceMethod(
activity,
"renderPromptProfile",
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
);
Spinner backendSpinner = ReflectionHelpers.getField(activity, "backendSpinner");
assertEquals(1, backendSpinner.getAdapter().getCount());
View content = activity.findViewById(R.id.screen_content);
assertTrue(viewTreeContainsText(content, "Hermes Runtime 当前不可用"));
assertTrue(viewTreeContainsText(content, "未检测到有效的 Hermes 启动脚本"));
assertTrue(viewTreeContainsText(content, "当前对话之前保存过 Hermes Runtime"));
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();

View File

@@ -45,10 +45,11 @@ public class ProjectDetailActivityMasterAgentMenuTest {
assertMenuItem(listView, 0, "模型");
assertMenuItem(listView, 1, "推理强度");
assertMenuItem(listView, 2, "全局接管");
assertMenuItem(listView, 3, "提示词");
assertMenuItem(listView, 4, "记忆");
assertMenuItem(listView, 5, "会话信息");
assertMenuItem(listView, 6, "刷新");
assertMenuItem(listView, 3, "自动进化");
assertMenuItem(listView, 4, "提示词");
assertMenuItem(listView, 5, "记忆");
assertMenuItem(listView, 6, "会话信息");
assertMenuItem(listView, 7, "刷新");
}
@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
public void rootMeMenuTitles_matchLegacyWechatMenuWithOpsEntry() throws Exception {
assertArrayEquals(
new String[]{"账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
WechatSurfaceMapper.rootMeMenuTitles()
);
}
@@ -208,7 +208,7 @@ public class WechatSurfaceMapperTest {
@Test
public void mainPage_keepsOpsEntryInStableWechatMenuOrder() throws Exception {
assertArrayEquals(
new String[]{"账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
new String[]{"主 Agent 提示词", "主 Agent 记忆", "全局接管", "主 Agent 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
WechatSurfaceMapper.rootMeMenuTitles()
);
}
@@ -380,15 +380,22 @@ public class WechatSurfaceMapperTest {
public void meMenuItems_useStableKeysInsteadOfDisplayTitlesForRouting() throws Exception {
WechatSurfaceMapper.MeMenuItem[] items = WechatSurfaceMapper.rootMeMenuItems();
assertEquals(6, items.length);
assertEquals("security", items[0].key);
assertEquals("账号与安全", items[0].title);
assertEquals("settings", items[1].key);
assertEquals("ops", items[2].key);
assertEquals("运维与修复", items[2].title);
assertEquals("ai_accounts", items[3].key);
assertEquals("skills", items[4].key);
assertEquals("about", items[5].key);
assertEquals(10, items.length);
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);
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

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

View File

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

View File

@@ -1,6 +1,6 @@
# Boss 当前运行与部署状态
更新时间:`2026-04-03`
更新时间:`2026-04-16`
## 1. 本地状态
@@ -134,7 +134,9 @@ cd /Users/kris/code/boss
- 当前 `AI 账号` 页顶部会直接展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个入口,切换主控后不必再手动退回会话页验证
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。``master-agent` 会话
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖
- 当前 `我的` 根页已经拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 四个独立入口;提示词页用于管理员全局主提示词、用户主提示词、当前对话附加提示词与执行后端设置,记忆页用于用户通用记忆和项目记忆的查看编辑与归档
- 当前 `我的 > 主 Agent 自动进化` 页面已接通Web `/me/master-agent/evolution` 可查看最近信号、待审批提案和已生效规则,并允许管理员切换 `controlled / autonomous`、批准或拒绝提案
- 当前原生 Android 也已接通 `主 Agent 自动进化``我的` 根页可直接进入,`master-agent` 会话右上角 `...` 菜单也可直达;页面支持查看最近信号、待审批提案、已生效规则。管理员可切换 `controlled / autonomous` 并批准/拒绝提案,非管理员显示只读视角
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
- 当前如果主控身份还是 `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`,真实回复随后再回写消息账本
- 当前对话级 `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`
- 当前 `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 执行这类任务时会优先吃任务级模型,不再只依赖本机固定默认模型
- 当前对话级 `agentControls` 也已支持 `backendOverride``master-agent` 会话可在 `Claw Runtime``Hermes Runtime` 可用时显式选择 `claw-runtime / hermes-runtime`,普通单线程会话当前只开放 `hermes-runtime`;不可用时保存接口会直接拒绝,并返回人类可读原因
- 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示

View File

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

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 提示词 / 记忆"
description="配置全局主提示词、当前主提示词和用户记忆"
/>
<MenuRow
href="/me/master-agent/evolution"
title="主 Agent 自动进化"
description="查看进化信号、提案、规则,并切换受控 / 全自动模式"
/>
<MenuRow
href="/me/storage"
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: [],
masterAgentMemories: [],
masterAgentEvolutionConfig: {
mode: "controlled",
autoApplyLowRiskRules: false,
mode: "autonomous",
autoApplyLowRiskRules: true,
updatedAt: nowIso(),
},
masterAgentEvolutionSignals: [],

View File

@@ -1726,10 +1726,6 @@ type MasterAgentFastIntentContext = {
effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>;
};
function buildMasterAgentRuntimeBackendLabel(context: MasterAgentFastIntentContext) {
return context.agentControls?.backendOverride?.trim() || "master-codex-node";
}
function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) {
const deviceId =
context.runtime.account.nodeId?.trim() ||
@@ -1952,7 +1948,7 @@ async function appendFastPathError(
}
async function tryRecordMasterAgentEvolutionSignal(input: {
kind: "fast_path_candidate" | "user_correction" | "backend_fallback";
kind: "fast_path_candidate" | "repeated_question" | "user_correction" | "backend_fallback";
account: string;
requestText: 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) {
const normalized = normalizeLexicalText(requestText);
const manualModel = context.agentControls?.modelOverride?.trim() || "";
@@ -3250,6 +3287,22 @@ export async function replyToMasterAgentUserMessage(params: {
currentSessionExpiresAt?: string;
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({
requestText: params.requestText,
requestedByAccount: params.requestedByAccount,
@@ -3258,17 +3311,6 @@ export async function replyToMasterAgentUserMessage(params: {
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();
if (!runtime?.account) {
@@ -3311,6 +3353,22 @@ export async function replyToMasterAgentUserMessage(params: {
};
const selectedBackend = await selectExecutionBackend(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 masterExecutionPrompt = buildMasterCodexNodePrompt(
state,

View File

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

View File

@@ -31,6 +31,7 @@ function inferProposalFromSignal(input: {
projectId: string;
requestText: string;
signalId: string;
metadataJson?: Record<string, unknown>;
}): Omit<
Parameters<typeof createMasterAgentEvolutionProposalInState>[0],
"status"
@@ -76,6 +77,11 @@ function inferProposalFromSignal(input: {
}
if (input.kind === "backend_fallback") {
const fallbackToBackendId =
input.metadataJson?.fallbackToBackendId === "hermes-runtime" ||
input.metadataJson?.fallbackToBackendId === "claw-runtime"
? input.metadataJson.fallbackToBackendId
: undefined;
return {
proposalType: "routing_preference_patch",
account: input.account,
@@ -84,9 +90,10 @@ function inferProposalFromSignal(input: {
summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。",
patchJson: {
backendPreference: "prefer_available_runtime",
backendOverride: fallbackToBackendId,
},
sourceSignalIds: [input.signalId],
confidence: 0.72,
confidence: fallbackToBackendId ? 0.8 : 0.72,
riskLevel: "low",
};
}
@@ -193,6 +200,7 @@ export async function recordMasterAgentEvolutionSignal(input: {
projectId: signal.projectId,
requestText: signal.requestText,
signalId: signal.signalId,
metadataJson: signal.metadataJson,
});
if (!proposalInput) {
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 [
"src/app/me/master-agent/page.tsx",
"src/app/me/master-agent/takeover/page.tsx",
"src/app/me/master-agent/evolution/page.tsx",
]) {
const source = await readSource(relativePath);
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 { getMasterAgentChatMenuItems } from "../src/lib/master-agent-chat-menu";
test("master-agent 聊天页菜单包含全局接管、提示词、模型、推理强度、记忆和刷新", () => {
test("master-agent 聊天页菜单包含全局接管、进化、提示词、模型、推理强度、记忆和刷新", () => {
const items = getMasterAgentChatMenuItems("master-agent");
assert.deepEqual(
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[1]?.href, "/me/master-agent#reasoning-effort-section");
assert.equal(items[2]?.href, "/me/master-agent/takeover");
assert.equal(items[3]?.href, "/me/master-agent#prompt-section");
assert.equal(items[4]?.href, "/me/master-agent#memory-section");
assert.equal(items[5]?.action, "refresh");
assert.equal(items[3]?.href, "/me/master-agent/evolution");
assert.equal(items[4]?.href, "/me/master-agent#prompt-section");
assert.equal(items[5]?.href, "/me/master-agent#memory-section");
assert.equal(items[6]?.action, "refresh");
});
test("普通会话不返回主 Agent 专属菜单", () => {

View File

@@ -6,6 +6,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
let runtimeRoot = "";
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 listMasterAgentEvolutionProposals: (typeof import("../src/lib/master-agent-evolution"))["listMasterAgentEvolutionProposals"];
let setMasterAgentEvolutionMode: (typeof import("../src/lib/master-agent-evolution"))["setMasterAgentEvolutionMode"];
@@ -22,6 +23,7 @@ async function setup() {
import("../src/lib/master-agent-evolution.ts"),
]);
readState = data.readState;
getProjectAgentControls = data.getProjectAgentControls;
recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal;
listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals;
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 () => {
await setMasterAgentEvolutionMode("controlled");
const result = await recordMasterAgentEvolutionSignal({
kind: "repeated_question",
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[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"),
);
assert.equal(response.status, 200);
const payload = await response.json() as { ok: boolean; proposals: unknown[]; rules: unknown[] };
const payload = await response.json() as { ok: boolean; canManage?: boolean; proposals: unknown[]; rules: unknown[] };
assert.equal(payload.ok, true);
assert.equal(payload.canManage, true);
assert.ok(Array.isArray(payload.proposals));
assert.ok(Array.isArray(payload.rules));
});

View File

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

View File

@@ -359,6 +359,43 @@ test("master-agent 查询当前后端时直接走 fast path 返回后端摘要",
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 () => {
await saveAiAccount({
accountId: "openai-takeover-status",