7 Commits

39 changed files with 2971 additions and 54 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

@@ -24,6 +24,7 @@
7. `docs/architecture/thread_context_budget_and_handoff_protocol_cn.md`
8. `prompts/codex_fullstack_build_and_deploy_prompt_cn.md`
9. `docs/superpowers/specs/2026-04-16-master-agent-fast-path-design.md`
10. `docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md`
## 3. 当前有效实现边界
@@ -36,6 +37,7 @@
- `src/lib/boss-device-auth.ts`:设备 token / 登录会话混合鉴权辅助
- `src/lib/boss-events.ts`SSE 事件总线
- `src/lib/boss-master-agent.ts`:主 Agent 真实回复链路、Master Codex Node relay 与 API 容灾逻辑
- `src/lib/master-agent-evolution.ts`:主 Agent 自动进化引擎,负责 signal、proposal、rule 与 controlled / autonomous 模式
- `src/lib/boss-attachments.ts`:附件类型识别、分析状态决策和下载头
- `src/lib/boss-storage.ts`:附件存储抽象、配置校验和脱敏输出
- `src/lib/boss-storage-server-file.ts`:服务器文件存储上传 / 读取
@@ -52,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`:原生独立群聊创建页
@@ -153,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` 备用账号,不再把失败日志直接原样回给用户
@@ -163,6 +165,9 @@ cd /Users/kris/code/boss
- 主 Agent 当前真实对话链路已验证通过:`Boss Web -> /api/v1/projects/master-agent/messages -> master-agent task queue -> local-agent -> codex exec -> /complete -> 项目消息账本`
- 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本
- 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride` 强制覆盖,也支持 `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

@@ -0,0 +1,287 @@
# Master Agent Evolution Engine Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 给 Boss 主Agent 增加一套统一的自动进化引擎,并支持 `controlled``autonomous` 两种模式以及双分支交付。
**Architecture:** 在现有 `boss-data``boss-master-agent` 上新增 evolution state、engine 和 API。共享内核一次实现分支差异只保留在默认模式与自动采纳开关上避免维护两份逻辑。
**Tech Stack:** Next.js 16、TypeScript、文件型状态存储、Node test runner
---
### Task 1: 增加 Evolution 状态模型与持久化
**Files:**
- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/boss-data.ts`
- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-state.test.ts`
- [ ] **Step 1: 写失败测试,定义 evolution state 的默认结构与读写**
```ts
test("boss state 初始化时包含 master agent evolution 默认配置与空集合", async () => {
const state = await readState();
assert.deepEqual(state.masterAgentEvolutionConfig?.mode, "controlled");
assert.deepEqual(state.masterAgentEvolutionSignals, []);
assert.deepEqual(state.masterAgentEvolutionProposals, []);
assert.deepEqual(state.masterAgentEvolutionRules, []);
assert.deepEqual(state.masterAgentEvolutionRunLogs, []);
});
```
- [ ] **Step 2: 运行测试,确认当前失败**
Run: `npx tsx --test tests/master-agent-evolution-state.test.ts`
Expected: FAIL提示缺少 `masterAgentEvolution*` 字段或断言不匹配
- [ ] **Step 3: 在 `boss-data.ts` 中新增类型和默认状态**
```ts
export type MasterAgentEvolutionMode = "controlled" | "autonomous";
export interface MasterAgentEvolutionConfig {
mode: MasterAgentEvolutionMode;
autoApplyLowRiskRules: boolean;
updatedAt: string;
}
```
- [ ] **Step 4: 扩展 `BossState`、normalize 逻辑和默认初始值**
Run: `npx tsx --test tests/master-agent-evolution-state.test.ts`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add src/lib/boss-data.ts tests/master-agent-evolution-state.test.ts
git commit -m "feat: add master agent evolution state"
```
### Task 2: 实现 Evolution Engine 核心
**Files:**
- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/master-agent-evolution.ts`
- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-engine.test.ts`
- [ ] **Step 1: 写失败测试,覆盖 signal -> proposal 的最小闭环**
```ts
test("recording repeated deterministic questions creates a fast_path_rule proposal", async () => {
await recordMasterAgentEvolutionSignal({
kind: "repeated_question",
projectId: "master-agent",
account: "17600003315",
content: "当前主节点在线吗",
});
const proposals = await listMasterAgentEvolutionProposals();
assert.equal(proposals[0]?.proposalType, "fast_path_rule");
});
```
- [ ] **Step 2: 运行测试,确认失败**
Run: `npx tsx --test tests/master-agent-evolution-engine.test.ts`
Expected: FAIL提示函数或 proposal 类型不存在
- [ ] **Step 3: 实现最小 engine API**
```ts
export async function recordMasterAgentEvolutionSignal(...) {}
export async function listMasterAgentEvolutionProposals(...) {}
export async function applyMasterAgentEvolutionProposal(...) {}
```
- [ ] **Step 4: 实现第一批 proposal 生成规则**
规则最小集:
- repeated deterministic questions -> `fast_path_rule`
- repeated backend/model corrections -> `routing_preference_patch`
- repeated user preference statements -> `memory_patch`
- [ ] **Step 5: 运行测试确认通过**
Run: `npx tsx --test tests/master-agent-evolution-engine.test.ts`
Expected: PASS
- [ ] **Step 6: 提交**
```bash
git add src/lib/master-agent-evolution.ts tests/master-agent-evolution-engine.test.ts
git commit -m "feat: add master agent evolution engine"
```
### Task 3: 把 Evolution Engine 挂到主Agent主链
**Files:**
- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/boss-master-agent.ts`
- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-integration.test.ts`
- [ ] **Step 1: 写失败测试,验证快路径未命中后会记录 signal**
```ts
test("master agent slow deterministic query records evolution signal when fast path misses", async () => {
await replyToMasterAgentUserMessage({
requestText: "当前线程是不是正在运行",
requestedBy: "Boss 超级管理员",
requestedByAccount: "17600003315",
mode: "enqueue",
});
const signals = await listMasterAgentEvolutionSignals();
assert.ok(signals.some((item) => item.kind === "fast_path_candidate"));
});
```
- [ ] **Step 2: 运行测试,确认失败**
Run: `npx tsx --test tests/master-agent-evolution-integration.test.ts`
Expected: FAIL
- [ ] **Step 3: 在 `replyToMasterAgentUserMessage()` 和相关成功/失败路径中记录 signal**
最小挂载点:
- Fast Path 命中
- Fast Path 未命中但命中确定性查询模式
- 后端 fallback
- 用户手动切模型/切后端/切接管
- [ ] **Step 4: 运行测试确认通过**
Run: `npx tsx --test tests/master-agent-evolution-integration.test.ts`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add src/lib/boss-master-agent.ts tests/master-agent-evolution-integration.test.ts
git commit -m "feat: integrate evolution engine into master agent"
```
### Task 4: 实现 Evolution API
**Files:**
- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/route.ts`
- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/config/route.ts`
- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts`
- Create: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts`
- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-routes.test.ts`
- [ ] **Step 1: 写失败测试,定义 GET/POST contract**
```ts
test("GET /api/v1/master-agent/evolution returns config proposals and rules", async () => {
const response = await GET(request);
const payload = await response.json();
assert.equal(payload.ok, true);
assert.ok(Array.isArray(payload.proposals));
});
```
- [ ] **Step 2: 运行测试,确认失败**
Run: `npx tsx --test tests/master-agent-evolution-routes.test.ts`
Expected: FAIL
- [ ] **Step 3: 实现读接口和 config 切换接口**
- [ ] **Step 4: 实现 approve / reject 接口**
- [ ] **Step 5: 运行测试确认通过**
Run: `npx tsx --test tests/master-agent-evolution-routes.test.ts`
Expected: PASS
- [ ] **Step 6: 提交**
```bash
git add src/app/api/v1/master-agent/evolution src/app/api/v1/master-agent/evolution/config src/app/api/v1/master-agent/evolution/proposals tests/master-agent-evolution-routes.test.ts
git commit -m "feat: add master agent evolution routes"
```
### Task 5: 加入 Controlled / Autonomous 自动采纳分界
**Files:**
- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/src/lib/master-agent-evolution.ts`
- Test: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/tests/master-agent-evolution-autonomous.test.ts`
- [ ] **Step 1: 写失败测试,验证 controlled 不自动采纳、autonomous 可自动采纳低风险 proposal**
```ts
test("controlled mode keeps proposal pending_review", async () => {
await setMasterAgentEvolutionMode("controlled");
// record signal...
assert.equal(proposals[0]?.status, "pending_review");
});
test("autonomous mode auto applies low risk routing preference patch", async () => {
await setMasterAgentEvolutionMode("autonomous");
// record signal...
assert.equal(proposals[0]?.status, "auto_applied");
});
```
- [ ] **Step 2: 运行测试,确认失败**
Run: `npx tsx --test tests/master-agent-evolution-autonomous.test.ts`
Expected: FAIL
- [ ] **Step 3: 实现 mode gating 与低风险自动采纳白名单**
白名单:
- `memory_patch`
- `routing_preference_patch`
- `fast_path_rule`
黑名单:
- 管理员全局主提示词修改
- 高风险提示词覆盖
- [ ] **Step 4: 运行测试确认通过**
Run: `npx tsx --test tests/master-agent-evolution-autonomous.test.ts`
Expected: PASS
- [ ] **Step 5: 提交**
```bash
git add src/lib/master-agent-evolution.ts tests/master-agent-evolution-autonomous.test.ts
git commit -m "feat: add controlled and autonomous evolution modes"
```
### Task 6: 更新交接文档并切双分支
**Files:**
- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/docs/architecture/ai_handoff_index_cn.md`
- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/docs/architecture/current_runtime_and_deploy_status_cn.md`
- Modify: `/Users/kris/.config/superpowers/worktrees/boss/codex-hermes-backend-mvp/docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md`
- [ ] **Step 1: 文档中加入 evolution engine 与两种模式说明**
- [ ] **Step 2: 运行组合验证**
Run: `npx tsx --test tests/master-agent-evolution*.test.ts tests/master-agent-message-queue.test.ts tests/master-agent-chat-controls.test.ts`
Expected: PASS
Run: `npm run build`
Expected: PASS
- [ ] **Step 3: 提交共享内核**
```bash
git add docs/architecture/ai_handoff_index_cn.md docs/architecture/current_runtime_and_deploy_status_cn.md docs/superpowers/specs/2026-04-16-master-agent-evolution-engine-design.md
git commit -m "docs: document master agent evolution engine"
```
- [ ] **Step 4: 创建 controlled 分支并推送**
```bash
git branch codex/master-agent-controlled-evolution
git push gitea codex/master-agent-controlled-evolution
```
- [ ] **Step 5: 创建 autonomous 分支,在其上把默认 mode 切到 autonomous 并推送**
```bash
git checkout -b codex/master-agent-autonomous-evolution
git push gitea codex/master-agent-autonomous-evolution
```

View File

@@ -0,0 +1,198 @@
# 主Agent 自动进化引擎设计
更新时间:`2026-04-16`
## 1. 背景
当前 Boss 主Agent 已经具备:
- 长期记忆:`masterAgentMemories`
- 提示词策略:`masterAgentPromptPolicy``userMasterPrompts`
- 对话控制:`userProjectAgentControls`
- 运行时自适应Master Node / Hermes / Claw / API fallback
但它还不具备真正的自动进化闭环。现状只能“记住”与“回退”,不能持续发现问题、形成提案、沉淀规则并在后续回复中生效。
## 2. 目标
- 给主Agent增加统一的 `Evolution Engine`
- 支持两种能力范围:
- `controlled`:受控自动进化,只生成提案,等待用户批准后落地
- `autonomous`:完全自我进化,对高置信度提案自动落地
- 进化逻辑对现有主链最小侵入,不重写现有回复链、任务链、记忆链
- 所有进化行为可追踪、可审计、可回滚
## 3. 核心概念
### 3.1 Evolution Signal
进化信号,记录“为什么应该考虑进化”。
第一批信号来源:
- 高频重复问句
- 慢回复问题
- 用户纠正主Agent
- 模型/后端频繁手动切换
- 后端回退或执行失败
- Fast Path 未命中但属于确定性状态问题
### 3.2 Evolution Proposal
进化提案,记录“建议怎么改”。
第一批提案类型:
- `fast_path_rule`
- `prompt_policy_patch`
- `memory_patch`
- `agent_control_patch`
- `routing_preference_patch`
### 3.3 Evolution Rule
已生效规则,表示被批准或自动采纳后真正进入系统长期状态的变更。
### 3.4 Evolution Run Log
进化执行日志,记录某次信号分析、提案生成、采纳、拒绝、自动落地的全过程。
## 4. 运行模式
### 4.1 Controlled
受控自动进化:
- 主Agent可以自动记录 signal
- 可以自动生成 proposal
- 不允许自动改长期状态
- 只能在用户批准后写入 memory / prompt / controls / fast-path rules
适合生产稳态与审计要求更高的环境。
### 4.2 Autonomous
完全自我进化:
- 主Agent可以自动记录 signal
- 自动生成 proposal
- 对满足阈值的 proposal 直接自动采纳
- 自动把结果写入长期状态
- 仍保留完整日志,允许后续回滚
适合开发期快速迭代,用更高风险换更快自优化速度。
## 5. 架构
### 5.1 新增状态模型
`BossState` 中新增:
- `masterAgentEvolutionConfig`
- `masterAgentEvolutionSignals`
- `masterAgentEvolutionProposals`
- `masterAgentEvolutionRules`
- `masterAgentEvolutionRunLogs`
### 5.2 新增引擎文件
- `src/lib/master-agent-evolution.ts`
- 统一入口
- signal 归一化
- proposal 生成
- 自动采纳判断
- 规则落地
- `src/lib/master-agent-evolution-rules.ts`
- Fast Path 规则、路由偏好规则解析
- `src/lib/master-agent-evolution-projections.ts`
- 给 Web/API 返回 evolution 状态摘要
### 5.3 挂载点
第一批最小侵入挂载点:
- `replyToMasterAgentUserMessage()`
- 记录用户问题、Fast Path 命中/未命中、慢路径耗时、模型/后端实际选择
- `appendMasterAgentSystemReply()`
- 记录主Agent输出结果
- `completeMasterAgentTask()`
- 记录异步任务成功/失败、fallback、超时
- `updateProjectAgentControls()` / `updateMasterAgentPromptPolicy()` / `createUserMasterMemory()`
- 记录用户主动纠偏行为,作为 evolution signal 输入
## 6. 第一批自动进化能力
### Controlled 与 Autonomous 共用
- 记录重复问句
- 记录低价值慢路径
- 记录用户纠正模型/后端/接管设置
- 自动生成三类提案:
- 新增 Fast Path 规则
- 调整默认快模型/强模型/后端偏好
- 追加 workflow_rule / user_preference 记忆
### Controlled 专属行为
- 所有提案状态默认为 `pending_review`
- 需要用户批准后才能落地
### Autonomous 专属行为
- 当 proposal 满足“高置信度 + 低破坏性”时自动落地
- 第一批只允许自动落地:
- `memory_patch`
- `routing_preference_patch`
- `fast_path_rule`
- 不允许自动改管理员全局主提示词
## 7. 前台入口
第一批只做最小 Web 能力:
- `GET /api/v1/master-agent/evolution`
- 返回 config / pending proposals / recent run logs / effective rules
- `POST /api/v1/master-agent/evolution/config`
- 切换 `controlled` / `autonomous`
- `POST /api/v1/master-agent/evolution/proposals/[proposalId]/approve`
- `POST /api/v1/master-agent/evolution/proposals/[proposalId]/reject`
先不做复杂 UI先把数据和接口打通必要时从 `我的 > 主Agent` 进入。
## 8. 双分支策略
统一内核先落在共享提交里:
- evolution state
- evolution engine
- proposal / rule / run-log 结构
- API 与测试
然后从共享基线分两条分支:
- `codex/master-agent-controlled-evolution`
- 默认 `mode=controlled`
- 关闭自动采纳
- `codex/master-agent-autonomous-evolution`
- 默认 `mode=autonomous`
- 开启自动采纳阈值逻辑
用户先使用 autonomous 分支验证真实开发效率;一旦发现策略漂移或错误写入,可直接回退 controlled 分支。
## 9. 风险控制
- 管理员全局主提示词永远不允许 autonomous 自动改写
- 自动落地范围先限制在低风险对象
- 每次自动采纳都必须写 run log
- 所有 rule 都要带 `sourceProposalId`
- 所有 proposal 都要可拒绝、可归档、可回滚
## 10. 验证基线
- `npx tsx --test tests/master-agent-evolution*.test.ts`
- `npx tsx --test tests/master-agent-message-queue.test.ts tests/master-agent-chat-controls.test.ts`
- `npm run build`
- 线上切到 `autonomous` 后:
- 主Agent可记录进化信号
- 可自动生成并落地低风险规则
- `controlled` 分支同一套信号与提案只记录、不自动采纳

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { jsonNoStore } from "@/lib/api-response";
import { setMasterAgentEvolutionMode } from "@/lib/master-agent-evolution";
export const runtime = "nodejs";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
return jsonNoStore({ ok: false, message: "METHOD_NOT_ALLOWED" }, { status: 405 });
}
export async function POST(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const rawBody = await request.text().catch(() => "");
let body: unknown;
try {
body = JSON.parse(rawBody);
} catch {
return NextResponse.json({ ok: false, message: "INVALID_JSON_PAYLOAD" }, { status: 400 });
}
if (!body || typeof body !== "object" || Array.isArray(body)) {
return NextResponse.json({ ok: false, message: "INVALID_EVOLUTION_CONFIG_PAYLOAD" }, { status: 400 });
}
const payload = body as { mode?: unknown };
if (payload.mode !== "controlled" && payload.mode !== "autonomous") {
return NextResponse.json({ ok: false, message: "INVALID_EVOLUTION_MODE" }, { status: 400 });
}
const config = await setMasterAgentEvolutionMode(payload.mode);
return NextResponse.json({ ok: true, config });
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { approveMasterAgentEvolutionProposal } from "@/lib/master-agent-evolution";
export const runtime = "nodejs";
export async function POST(
request: NextRequest,
context: { params: Promise<{ proposalId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const { proposalId } = await context.params;
try {
const proposal = await approveMasterAgentEvolutionProposal(proposalId, session.account);
return NextResponse.json({ ok: true, proposal });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,29 @@
import { NextRequest, NextResponse } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { rejectMasterAgentEvolutionProposal } from "@/lib/master-agent-evolution";
export const runtime = "nodejs";
export async function POST(
request: NextRequest,
context: { params: Promise<{ proposalId: string }> },
) {
const session = await requireRequestSession(request);
if (!session) {
return NextResponse.json({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
if (session.role !== "highest_admin") {
return NextResponse.json({ ok: false, message: "FORBIDDEN" }, { status: 403 });
}
const { proposalId } = await context.params;
try {
const proposal = await rejectMasterAgentEvolutionProposal(proposalId, session.account);
return NextResponse.json({ ok: true, proposal });
} catch (error) {
return NextResponse.json(
{ ok: false, message: error instanceof Error ? error.message : "UNKNOWN_ERROR" },
{ status: 400 },
);
}
}

View File

@@ -0,0 +1,21 @@
import { NextRequest } from "next/server";
import { requireRequestSession } from "@/lib/boss-auth";
import { jsonNoStore } from "@/lib/api-response";
import { getMasterAgentEvolutionDashboard } from "@/lib/master-agent-evolution";
export const runtime = "nodejs";
export async function GET(request: NextRequest) {
const session = await requireRequestSession(request);
if (!session) {
return jsonNoStore({ ok: false, message: "UNAUTHORIZED" }, { status: 401 });
}
const dashboard = await getMasterAgentEvolutionDashboard();
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

@@ -270,6 +270,86 @@ export interface MasterAgentMemory {
archived: boolean;
}
export type MasterAgentEvolutionMode = "controlled" | "autonomous";
export type MasterAgentEvolutionSignalKind =
| "repeated_question"
| "fast_path_candidate"
| "slow_path"
| "user_correction"
| "backend_fallback";
export type MasterAgentEvolutionProposalType =
| "fast_path_rule"
| "prompt_policy_patch"
| "memory_patch"
| "agent_control_patch"
| "routing_preference_patch";
export type MasterAgentEvolutionProposalStatus =
| "pending_review"
| "approved"
| "rejected"
| "auto_applied";
export type MasterAgentEvolutionRunAction =
| "signal_recorded"
| "proposal_created"
| "proposal_approved"
| "proposal_rejected"
| "proposal_auto_applied";
export interface MasterAgentEvolutionConfig {
mode: MasterAgentEvolutionMode;
autoApplyLowRiskRules: boolean;
updatedAt: string;
}
export interface MasterAgentEvolutionSignal {
signalId: string;
kind: MasterAgentEvolutionSignalKind;
account: string;
projectId: string;
requestText: string;
replyText?: string;
metadataJson?: Record<string, unknown>;
createdAt: string;
}
export interface MasterAgentEvolutionProposal {
proposalId: string;
proposalType: MasterAgentEvolutionProposalType;
status: MasterAgentEvolutionProposalStatus;
account: string;
projectId: string;
title: string;
summary: string;
patchJson: Record<string, unknown>;
sourceSignalIds: string[];
confidence: number;
riskLevel: "low" | "medium" | "high";
createdAt: string;
updatedAt: string;
}
export interface MasterAgentEvolutionRule {
ruleId: string;
ruleType: MasterAgentEvolutionProposalType;
account: string;
projectId?: string;
sourceProposalId: string;
patchJson: Record<string, unknown>;
createdAt: string;
updatedAt: string;
}
export interface MasterAgentEvolutionRunLog {
runId: string;
action: MasterAgentEvolutionRunAction;
account: string;
projectId: string;
signalId?: string;
proposalId?: string;
note: string;
createdAt: string;
}
export interface GoalItem {
id: string;
text: string;
@@ -1078,6 +1158,11 @@ export interface BossState {
masterAgentPromptPolicy: MasterAgentPromptPolicy | null;
userMasterPrompts: UserMasterPrompt[];
masterAgentMemories: MasterAgentMemory[];
masterAgentEvolutionConfig: MasterAgentEvolutionConfig;
masterAgentEvolutionSignals: MasterAgentEvolutionSignal[];
masterAgentEvolutionProposals: MasterAgentEvolutionProposal[];
masterAgentEvolutionRules: MasterAgentEvolutionRule[];
masterAgentEvolutionRunLogs: MasterAgentEvolutionRunLog[];
userProjectAgentControls: UserProjectAgentControls[];
threadContextSnapshots: ThreadContextSnapshot[];
threadHandoffPackages: ThreadHandoffPackage[];
@@ -1451,6 +1536,15 @@ const initialState: BossState = {
masterAgentPromptPolicy: null,
userMasterPrompts: [],
masterAgentMemories: [],
masterAgentEvolutionConfig: {
mode: "autonomous",
autoApplyLowRiskRules: true,
updatedAt: nowIso(),
},
masterAgentEvolutionSignals: [],
masterAgentEvolutionProposals: [],
masterAgentEvolutionRules: [],
masterAgentEvolutionRunLogs: [],
userProjectAgentControls: [],
masterAgentTasks: [],
dispatchPlans: [],
@@ -2967,6 +3061,101 @@ function normalizeUserMasterMemory(
};
}
function normalizeMasterAgentEvolutionConfig(
raw: Partial<MasterAgentEvolutionConfig> | null | undefined,
fallback?: MasterAgentEvolutionConfig,
): MasterAgentEvolutionConfig {
return {
mode: raw?.mode === "autonomous" ? "autonomous" : fallback?.mode ?? "controlled",
autoApplyLowRiskRules:
typeof raw?.autoApplyLowRiskRules === "boolean"
? raw.autoApplyLowRiskRules
: fallback?.autoApplyLowRiskRules ?? false,
updatedAt: raw?.updatedAt ?? fallback?.updatedAt ?? nowIso(),
};
}
function normalizeMasterAgentEvolutionSignal(
raw: Partial<MasterAgentEvolutionSignal>,
fallback?: MasterAgentEvolutionSignal,
): MasterAgentEvolutionSignal {
return {
signalId: raw.signalId ?? fallback?.signalId ?? randomToken("evo-signal"),
kind: raw.kind ?? fallback?.kind ?? "slow_path",
account: trimToDefined(raw.account ?? fallback?.account) ?? "",
projectId: trimToDefined(raw.projectId ?? fallback?.projectId) ?? "master-agent",
requestText: trimToDefined(raw.requestText ?? fallback?.requestText) ?? "",
replyText: trimToDefined(raw.replyText ?? fallback?.replyText),
metadataJson:
raw.metadataJson && typeof raw.metadataJson === "object"
? raw.metadataJson
: fallback?.metadataJson ?? {},
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
};
}
function normalizeMasterAgentEvolutionProposal(
raw: Partial<MasterAgentEvolutionProposal>,
fallback?: MasterAgentEvolutionProposal,
): MasterAgentEvolutionProposal {
return {
proposalId: raw.proposalId ?? fallback?.proposalId ?? randomToken("evo-proposal"),
proposalType: raw.proposalType ?? fallback?.proposalType ?? "memory_patch",
status: raw.status ?? fallback?.status ?? "pending_review",
account: trimToDefined(raw.account ?? fallback?.account) ?? "",
projectId: trimToDefined(raw.projectId ?? fallback?.projectId) ?? "master-agent",
title: trimToDefined(raw.title ?? fallback?.title) ?? "进化提案",
summary: trimToDefined(raw.summary ?? fallback?.summary) ?? "",
patchJson:
raw.patchJson && typeof raw.patchJson === "object"
? raw.patchJson
: fallback?.patchJson ?? {},
sourceSignalIds: dedupeStrings(
ensureArray(raw.sourceSignalIds, fallback?.sourceSignalIds ?? []).map((value) => value.trim()),
),
confidence:
typeof raw.confidence === "number" ? raw.confidence : fallback?.confidence ?? 0.5,
riskLevel: raw.riskLevel ?? fallback?.riskLevel ?? "low",
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
};
}
function normalizeMasterAgentEvolutionRule(
raw: Partial<MasterAgentEvolutionRule>,
fallback?: MasterAgentEvolutionRule,
): MasterAgentEvolutionRule {
return {
ruleId: raw.ruleId ?? fallback?.ruleId ?? randomToken("evo-rule"),
ruleType: raw.ruleType ?? fallback?.ruleType ?? "memory_patch",
account: trimToDefined(raw.account ?? fallback?.account) ?? "",
projectId: trimToDefined(raw.projectId ?? fallback?.projectId),
sourceProposalId: trimToDefined(raw.sourceProposalId ?? fallback?.sourceProposalId) ?? "",
patchJson:
raw.patchJson && typeof raw.patchJson === "object"
? raw.patchJson
: fallback?.patchJson ?? {},
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
updatedAt: raw.updatedAt ?? fallback?.updatedAt ?? nowIso(),
};
}
function normalizeMasterAgentEvolutionRunLog(
raw: Partial<MasterAgentEvolutionRunLog>,
fallback?: MasterAgentEvolutionRunLog,
): MasterAgentEvolutionRunLog {
return {
runId: raw.runId ?? fallback?.runId ?? randomToken("evo-run"),
action: raw.action ?? fallback?.action ?? "signal_recorded",
account: trimToDefined(raw.account ?? fallback?.account) ?? "",
projectId: trimToDefined(raw.projectId ?? fallback?.projectId) ?? "master-agent",
signalId: trimToDefined(raw.signalId ?? fallback?.signalId),
proposalId: trimToDefined(raw.proposalId ?? fallback?.proposalId),
note: trimToDefined(raw.note ?? fallback?.note) ?? "",
createdAt: raw.createdAt ?? fallback?.createdAt ?? nowIso(),
};
}
function normalizeProject(raw: Partial<Project>, fallback?: Project): Project {
const base = fallback ?? cloneInitialState().projects[0];
const projectId = raw.id ?? base.id;
@@ -3495,6 +3684,46 @@ function normalizeState(raw: Partial<BossState> | undefined): BossState {
base.masterAgentMemories[index % Math.max(1, base.masterAgentMemories.length)],
),
),
masterAgentEvolutionConfig: normalizeMasterAgentEvolutionConfig(
raw.masterAgentEvolutionConfig,
base.masterAgentEvolutionConfig,
),
masterAgentEvolutionSignals: ensureArray(
raw.masterAgentEvolutionSignals,
base.masterAgentEvolutionSignals,
).map((signal, index) =>
normalizeMasterAgentEvolutionSignal(
signal,
base.masterAgentEvolutionSignals[index % Math.max(1, base.masterAgentEvolutionSignals.length)],
),
),
masterAgentEvolutionProposals: ensureArray(
raw.masterAgentEvolutionProposals,
base.masterAgentEvolutionProposals,
).map((proposal, index) =>
normalizeMasterAgentEvolutionProposal(
proposal,
base.masterAgentEvolutionProposals[index % Math.max(1, base.masterAgentEvolutionProposals.length)],
),
),
masterAgentEvolutionRules: ensureArray(
raw.masterAgentEvolutionRules,
base.masterAgentEvolutionRules,
).map((rule, index) =>
normalizeMasterAgentEvolutionRule(
rule,
base.masterAgentEvolutionRules[index % Math.max(1, base.masterAgentEvolutionRules.length)],
),
),
masterAgentEvolutionRunLogs: ensureArray(
raw.masterAgentEvolutionRunLogs,
base.masterAgentEvolutionRunLogs,
).map((log, index) =>
normalizeMasterAgentEvolutionRunLog(
log,
base.masterAgentEvolutionRunLogs[index % Math.max(1, base.masterAgentEvolutionRunLogs.length)],
),
),
userProjectAgentControls: ensureArray(
raw.userProjectAgentControls,
base.userProjectAgentControls,
@@ -4991,6 +5220,191 @@ export async function touchUserMasterMemories(memoryIds: string[], account: stri
});
}
export async function getMasterAgentEvolutionState() {
const state = await readState();
return {
config: state.masterAgentEvolutionConfig,
signals: [...state.masterAgentEvolutionSignals],
proposals: [...state.masterAgentEvolutionProposals],
rules: [...state.masterAgentEvolutionRules],
runLogs: [...state.masterAgentEvolutionRunLogs],
};
}
export async function updateMasterAgentEvolutionConfig(input: {
mode?: MasterAgentEvolutionMode;
autoApplyLowRiskRules?: boolean;
}) {
const result = await mutateState((state) => {
const current = state.masterAgentEvolutionConfig;
const next = normalizeMasterAgentEvolutionConfig({
mode: input.mode ?? current.mode,
autoApplyLowRiskRules:
typeof input.autoApplyLowRiskRules === "boolean"
? input.autoApplyLowRiskRules
: current.autoApplyLowRiskRules,
updatedAt: nowIso(),
}, current);
if (next.mode === "controlled") {
next.autoApplyLowRiskRules = false;
}
if (next.mode === "autonomous" && input.autoApplyLowRiskRules === undefined) {
next.autoApplyLowRiskRules = true;
}
state.masterAgentEvolutionConfig = next;
return next;
});
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
return result;
}
export async function recordMasterAgentEvolutionSignalInState(input: {
kind: MasterAgentEvolutionSignalKind;
account: string;
projectId?: string;
requestText: string;
replyText?: string;
metadataJson?: Record<string, unknown>;
}) {
const result = await mutateState((state) => {
const signal = normalizeMasterAgentEvolutionSignal({
signalId: randomToken("evo-signal"),
kind: input.kind,
account: input.account,
projectId: input.projectId ?? "master-agent",
requestText: input.requestText,
replyText: input.replyText,
metadataJson: input.metadataJson ?? {},
createdAt: nowIso(),
});
state.masterAgentEvolutionSignals.unshift(signal);
state.masterAgentEvolutionRunLogs.unshift(
normalizeMasterAgentEvolutionRunLog({
runId: randomToken("evo-run"),
action: "signal_recorded",
account: signal.account,
projectId: signal.projectId,
signalId: signal.signalId,
note: `记录进化信号:${signal.kind}`,
createdAt: nowIso(),
}),
);
return signal;
});
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
return result;
}
export async function createMasterAgentEvolutionProposalInState(input: {
proposalType: MasterAgentEvolutionProposalType;
account: string;
projectId?: string;
title: string;
summary: string;
patchJson: Record<string, unknown>;
sourceSignalIds: string[];
confidence: number;
riskLevel?: "low" | "medium" | "high";
status?: MasterAgentEvolutionProposalStatus;
}) {
const result = await mutateState((state) => {
const now = nowIso();
const proposal = normalizeMasterAgentEvolutionProposal({
proposalId: randomToken("evo-proposal"),
proposalType: input.proposalType,
status: input.status ?? "pending_review",
account: input.account,
projectId: input.projectId ?? "master-agent",
title: input.title,
summary: input.summary,
patchJson: input.patchJson,
sourceSignalIds: input.sourceSignalIds,
confidence: input.confidence,
riskLevel: input.riskLevel ?? "low",
createdAt: now,
updatedAt: now,
});
state.masterAgentEvolutionProposals.unshift(proposal);
state.masterAgentEvolutionRunLogs.unshift(
normalizeMasterAgentEvolutionRunLog({
runId: randomToken("evo-run"),
action: proposal.status === "auto_applied" ? "proposal_auto_applied" : "proposal_created",
account: proposal.account,
projectId: proposal.projectId,
proposalId: proposal.proposalId,
note: proposal.title,
createdAt: now,
}),
);
return proposal;
});
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
return result;
}
export async function updateMasterAgentEvolutionProposalStatus(input: {
proposalId: string;
status: MasterAgentEvolutionProposalStatus;
account: string;
note?: string;
}) {
const result = await mutateState((state) => {
const proposal = state.masterAgentEvolutionProposals.find((item) => item.proposalId === input.proposalId);
if (!proposal) {
throw new Error("MASTER_AGENT_EVOLUTION_PROPOSAL_NOT_FOUND");
}
proposal.status = input.status;
proposal.updatedAt = nowIso();
state.masterAgentEvolutionRunLogs.unshift(
normalizeMasterAgentEvolutionRunLog({
runId: randomToken("evo-run"),
action:
input.status === "approved"
? "proposal_approved"
: input.status === "rejected"
? "proposal_rejected"
: input.status === "auto_applied"
? "proposal_auto_applied"
: "proposal_created",
account: input.account,
projectId: proposal.projectId,
proposalId: proposal.proposalId,
note: input.note ?? proposal.title,
createdAt: nowIso(),
}),
);
return proposal;
});
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
return result;
}
export async function createMasterAgentEvolutionRuleInState(input: {
ruleType: MasterAgentEvolutionProposalType;
account: string;
projectId?: string;
sourceProposalId: string;
patchJson: Record<string, unknown>;
}) {
const result = await mutateState((state) => {
const now = nowIso();
const rule = normalizeMasterAgentEvolutionRule({
ruleId: randomToken("evo-rule"),
ruleType: input.ruleType,
account: input.account,
projectId: input.projectId,
sourceProposalId: input.sourceProposalId,
patchJson: input.patchJson,
createdAt: now,
updatedAt: now,
});
state.masterAgentEvolutionRules.unshift(rule);
return rule;
});
publishBossEvent("master_agent.settings.updated", { projectId: "master-agent" });
return result;
}
function normalizeAutoMemoryText(value: string | undefined) {
return (value ?? "")
.replace(/\s+/g, " ")

View File

@@ -60,6 +60,7 @@ import {
listUserMasterMemoriesView,
} from "@/lib/boss-projections";
import { normalizeRemoteExecutionResult } from "@/lib/execution/remote-runtime-adapter";
import { recordMasterAgentEvolutionSignal } from "@/lib/master-agent-evolution";
type MasterAgentReplyState = "queued" | "running" | "completed";
type MasterAgentExecutionIntent = "chat" | "deep_task";
@@ -1725,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() ||
@@ -1950,6 +1947,68 @@ async function appendFastPathError(
};
}
async function tryRecordMasterAgentEvolutionSignal(input: {
kind: "fast_path_candidate" | "repeated_question" | "user_correction" | "backend_fallback";
account: string;
requestText: string;
replyText?: string;
metadataJson?: Record<string, unknown>;
}) {
try {
await recordMasterAgentEvolutionSignal({
kind: input.kind,
account: input.account,
projectId: "master-agent",
requestText: input.requestText,
replyText: input.replyText,
metadataJson: input.metadataJson,
});
} catch {
// Evolution capture is best-effort and must not break replies.
}
}
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() || "";
@@ -2060,10 +2119,15 @@ async function tryHandleMasterAgentBackendSwitchCommand(params: {
{ backendOverride: requestedBackend },
params.requestedByAccount,
);
return appendFastPathReply(
`已把默认后端切到 ${requestedBackend}`,
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
);
const reply = `已把默认后端切到 ${requestedBackend}`;
await tryRecordMasterAgentEvolutionSignal({
kind: "user_correction",
account: params.requestedByAccount,
requestText: params.requestText,
replyText: reply,
metadataJson: { requestedBackend },
});
return appendFastPathReply(reply, buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model));
}
async function tryHandleMasterAgentTakeoverCommand(params: {
@@ -2099,10 +2163,15 @@ async function tryHandleMasterAgentTakeoverCommand(params: {
{ globalTakeoverEnabled: nextEnabled },
params.requestedByAccount,
);
return appendFastPathReply(
nextEnabled ? "已开启全局接管。" : "已关闭全局接管。",
buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model),
);
const reply = nextEnabled ? "已开启全局接管。" : "已关闭全局接管。";
await tryRecordMasterAgentEvolutionSignal({
kind: "user_correction",
account: params.requestedByAccount,
requestText: params.requestText,
replyText: reply,
metadataJson: { globalTakeoverEnabled: nextEnabled },
});
return appendFastPathReply(reply, buildMasterAgentModelSenderLabel(params.context.effectiveChatPolicy.model));
}
async function tryHandleMasterAgentExecutionModeStatusQuery(params: {
@@ -2228,6 +2297,13 @@ async function tryHandleMasterAgentModelCommand(params: {
await updateProjectAgentControls("master-agent", patch, params.requestedByAccount);
const reply = `已把主 Agent 的${scopeLabel}切到 ${requestedModel}${availableModelsSuffix}`;
await tryRecordMasterAgentEvolutionSignal({
kind: "user_correction",
account: params.requestedByAccount,
requestText: params.requestText,
replyText: reply,
metadataJson: { scope, requestedModel },
});
return appendFastPathReply(reply, buildMasterAgentModelSenderLabel(requestedModel));
}
@@ -3211,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,
@@ -3261,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

@@ -0,0 +1,234 @@
import {
createMasterAgentEvolutionProposalInState,
createMasterAgentEvolutionRuleInState,
createUserMasterMemory,
getMasterAgentEvolutionState,
recordMasterAgentEvolutionSignalInState,
updateMasterAgentEvolutionConfig,
updateMasterAgentEvolutionProposalStatus,
updateProjectAgentControls,
} from "@/lib/boss-data";
import type {
MasterAgentEvolutionProposal,
MasterAgentEvolutionProposalType,
MasterAgentEvolutionSignalKind,
MasterAgentEvolutionMode,
} from "@/lib/boss-data";
const AUTO_APPLY_LOW_RISK_TYPES = new Set<MasterAgentEvolutionProposalType>([
"fast_path_rule",
"memory_patch",
"routing_preference_patch",
]);
function isDeterministicStatusQuestion(text: string) {
return /(当前|现在|有没有|是否|什么|哪个|在线|状态|模型|后端|接管|gui|cli|hermes|claw)/i.test(text);
}
function inferProposalFromSignal(input: {
kind: MasterAgentEvolutionSignalKind;
account: string;
projectId: string;
requestText: string;
signalId: string;
metadataJson?: Record<string, unknown>;
}): Omit<
Parameters<typeof createMasterAgentEvolutionProposalInState>[0],
"status"
> | null {
const requestText = input.requestText.trim();
if (
(input.kind === "repeated_question" || input.kind === "fast_path_candidate") &&
isDeterministicStatusQuestion(requestText)
) {
return {
proposalType: "fast_path_rule",
account: input.account,
projectId: input.projectId,
title: `新增 Fast Path${requestText.slice(0, 32)}`,
summary: `检测到确定性问题反复进入主链,建议把“${requestText}”归入本地 Fast Path。`,
patchJson: {
matcher: requestText,
action: "local_status_reply",
},
sourceSignalIds: [input.signalId],
confidence: 0.82,
riskLevel: "low",
};
}
if (input.kind === "user_correction") {
return {
proposalType: "memory_patch",
account: input.account,
projectId: input.projectId,
title: "沉淀用户纠正",
summary: `检测到用户纠正主Agent行为建议写入长期工作规则${requestText}`,
patchJson: {
scope: "global",
memoryType: "workflow_rule",
title: "用户纠正 · 工作规则",
content: requestText,
},
sourceSignalIds: [input.signalId],
confidence: 0.78,
riskLevel: "low",
};
}
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,
projectId: input.projectId,
title: "调整后端路由偏好",
summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。",
patchJson: {
backendPreference: "prefer_available_runtime",
backendOverride: fallbackToBackendId,
},
sourceSignalIds: [input.signalId],
confidence: fallbackToBackendId ? 0.8 : 0.72,
riskLevel: "low",
};
}
return null;
}
async function applyLowRiskProposal(proposal: MasterAgentEvolutionProposal) {
if (proposal.proposalType === "memory_patch") {
const patch = proposal.patchJson as {
scope?: "global" | "project";
projectId?: string;
title?: string;
content?: string;
memoryType?: "workflow_rule" | "user_preference" | "decision" | "project_progress";
tags?: string[];
};
await createUserMasterMemory({
account: proposal.account,
scope: patch.scope ?? "global",
projectId: patch.scope === "project" ? patch.projectId ?? proposal.projectId : undefined,
title: patch.title ?? proposal.title,
content: patch.content ?? proposal.summary,
memoryType: patch.memoryType ?? "workflow_rule",
tags: patch.tags ?? ["evolution"],
});
}
if (proposal.proposalType === "routing_preference_patch") {
const patch = proposal.patchJson as { backendOverride?: "hermes-runtime" | "claw-runtime" };
if (patch.backendOverride) {
await updateProjectAgentControls("master-agent", { backendOverride: patch.backendOverride }, proposal.account);
}
}
await createMasterAgentEvolutionRuleInState({
ruleType: proposal.proposalType,
account: proposal.account,
projectId: proposal.projectId,
sourceProposalId: proposal.proposalId,
patchJson: proposal.patchJson,
});
}
async function maybeAutoApplyProposal(proposal: MasterAgentEvolutionProposal) {
const state = await getMasterAgentEvolutionState();
if (
state.config.mode !== "autonomous" ||
!state.config.autoApplyLowRiskRules ||
proposal.riskLevel !== "low" ||
proposal.confidence < 0.75 ||
!AUTO_APPLY_LOW_RISK_TYPES.has(proposal.proposalType)
) {
return proposal;
}
await applyLowRiskProposal(proposal);
return updateMasterAgentEvolutionProposalStatus({
proposalId: proposal.proposalId,
status: "auto_applied",
account: proposal.account,
note: "autonomous 模式自动采纳低风险提案",
});
}
export async function setMasterAgentEvolutionMode(mode: MasterAgentEvolutionMode) {
return updateMasterAgentEvolutionConfig({
mode,
autoApplyLowRiskRules: mode === "autonomous",
});
}
export async function listMasterAgentEvolutionSignals() {
return (await getMasterAgentEvolutionState()).signals;
}
export async function listMasterAgentEvolutionProposals() {
return (await getMasterAgentEvolutionState()).proposals;
}
export async function getMasterAgentEvolutionDashboard() {
return getMasterAgentEvolutionState();
}
export async function recordMasterAgentEvolutionSignal(input: {
kind: MasterAgentEvolutionSignalKind;
account: string;
projectId?: string;
requestText: string;
replyText?: string;
metadataJson?: Record<string, unknown>;
}) {
const signal = await recordMasterAgentEvolutionSignalInState({
kind: input.kind,
account: input.account,
projectId: input.projectId ?? "master-agent",
requestText: input.requestText,
replyText: input.replyText,
metadataJson: input.metadataJson,
});
const proposalInput = inferProposalFromSignal({
kind: signal.kind,
account: signal.account,
projectId: signal.projectId,
requestText: signal.requestText,
signalId: signal.signalId,
metadataJson: signal.metadataJson,
});
if (!proposalInput) {
return { signal, proposal: null };
}
const proposal = await createMasterAgentEvolutionProposalInState(proposalInput);
const appliedProposal = await maybeAutoApplyProposal(proposal);
return { signal, proposal: appliedProposal };
}
export async function approveMasterAgentEvolutionProposal(proposalId: string, account: string) {
const proposal = (await getMasterAgentEvolutionState()).proposals.find((item) => item.proposalId === proposalId);
if (!proposal) {
throw new Error("MASTER_AGENT_EVOLUTION_PROPOSAL_NOT_FOUND");
}
await applyLowRiskProposal(proposal);
return updateMasterAgentEvolutionProposalStatus({
proposalId,
status: "approved",
account,
note: "用户批准进化提案",
});
}
export async function rejectMasterAgentEvolutionProposal(proposalId: string, account: string) {
return updateMasterAgentEvolutionProposalStatus({
proposalId,
status: "rejected",
account,
note: "用户拒绝进化提案",
});
}

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

@@ -0,0 +1,99 @@
import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
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"];
async function setup() {
if (runtimeRoot) {
return;
}
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-evolution-engine-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [data, evolution] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/master-agent-evolution.ts"),
]);
readState = data.readState;
getProjectAgentControls = data.getProjectAgentControls;
recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal;
listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals;
setMasterAgentEvolutionMode = evolution.setMasterAgentEvolutionMode;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test.beforeEach(async () => {
await setup();
await rm(runtimeRoot, { recursive: true, force: true });
await mkdir(runtimeRoot, { recursive: true });
});
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",
account: "17600003315",
requestText: "当前主节点在线吗",
});
assert.equal(result.signal.kind, "repeated_question");
assert.equal(result.proposal?.proposalType, "fast_path_rule");
assert.equal(result.proposal?.status, "pending_review");
const proposals = await listMasterAgentEvolutionProposals();
assert.equal(proposals[0]?.proposalType, "fast_path_rule");
assert.equal(proposals[0]?.status, "pending_review");
});
test("autonomous mode auto applies low risk fast path proposals as evolution rules", async () => {
await setMasterAgentEvolutionMode("autonomous");
const result = await recordMasterAgentEvolutionSignal({
kind: "repeated_question",
projectId: "master-agent",
account: "17600003315",
requestText: "当前主节点在线吗",
});
assert.equal(result.proposal?.status, "auto_applied");
const state = await readState();
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

@@ -0,0 +1,126 @@
import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
import { NextRequest } from "next/server";
let runtimeRoot = "";
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
let recordMasterAgentEvolutionSignal: (typeof import("../src/lib/master-agent-evolution"))["recordMasterAgentEvolutionSignal"];
let GETEvolutionRoute: (typeof import("../src/app/api/v1/master-agent/evolution/route"))["GET"];
let POSTEvolutionConfigRoute: (typeof import("../src/app/api/v1/master-agent/evolution/config/route"))["POST"];
let POSTApproveRoute: (typeof import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route"))["POST"];
let POSTRejectRoute: (typeof import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route"))["POST"];
let AUTH_SESSION_COOKIE = "";
async function setup() {
if (runtimeRoot) {
return;
}
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-evolution-routes-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const [data, auth, evolution, evolutionRoute, configRoute, approveRoute, rejectRoute] = await Promise.all([
import("../src/lib/boss-data.ts"),
import("../src/lib/boss-auth.ts"),
import("../src/lib/master-agent-evolution.ts"),
import("../src/app/api/v1/master-agent/evolution/route.ts"),
import("../src/app/api/v1/master-agent/evolution/config/route.ts"),
import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/approve/route.ts"),
import("../src/app/api/v1/master-agent/evolution/proposals/[proposalId]/reject/route.ts"),
]);
createAuthSession = data.createAuthSession;
recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal;
GETEvolutionRoute = evolutionRoute.GET;
POSTEvolutionConfigRoute = configRoute.POST;
POSTApproveRoute = approveRoute.POST;
POSTRejectRoute = rejectRoute.POST;
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
}
async function createAdminRequest(url: string, body?: unknown) {
const session = await createAuthSession({
account: "17600003315",
role: "highest_admin",
displayName: "Boss 超级管理员",
loginMethod: "password",
});
return new NextRequest(url, {
method: body === undefined ? "GET" : "POST",
headers: {
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
...(body === undefined ? {} : { "content-type": "application/json" }),
},
body: body === undefined ? undefined : JSON.stringify(body),
});
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test.beforeEach(async () => {
await setup();
await rm(runtimeRoot, { recursive: true, force: true });
await mkdir(runtimeRoot, { recursive: true });
});
test("GET /api/v1/master-agent/evolution returns config proposals and rules", async () => {
const response = await GETEvolutionRoute(
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; 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));
});
test("POST /api/v1/master-agent/evolution/config switches mode", async () => {
const response = await POSTEvolutionConfigRoute(
await createAdminRequest("http://127.0.0.1:3000/api/v1/master-agent/evolution/config", { mode: "autonomous" }),
);
assert.equal(response.status, 200);
const payload = await response.json() as { ok: boolean; config?: { mode?: string } };
assert.equal(payload.ok, true);
assert.equal(payload.config?.mode, "autonomous");
});
test("approve and reject evolution proposals update proposal status", async () => {
const created = await recordMasterAgentEvolutionSignal({
kind: "repeated_question",
account: "17600003315",
requestText: "当前主节点在线吗",
});
const proposalId = created.proposal?.proposalId ?? "";
assert.ok(proposalId);
const approveResponse = await POSTApproveRoute(
await createAdminRequest(`http://127.0.0.1:3000/api/v1/master-agent/evolution/proposals/${proposalId}/approve`, {}),
{ params: Promise.resolve({ proposalId }) },
);
assert.equal(approveResponse.status, 200);
const approvePayload = await approveResponse.json() as { ok: boolean; proposal?: { status?: string } };
assert.equal(approvePayload.ok, true);
assert.equal(approvePayload.proposal?.status, "approved");
const created2 = await recordMasterAgentEvolutionSignal({
kind: "backend_fallback",
account: "17600003315",
requestText: "切到稳定后端",
});
const proposalId2 = created2.proposal?.proposalId ?? "";
assert.ok(proposalId2);
const rejectResponse = await POSTRejectRoute(
await createAdminRequest(`http://127.0.0.1:3000/api/v1/master-agent/evolution/proposals/${proposalId2}/reject`, {}),
{ params: Promise.resolve({ proposalId: proposalId2 }) },
);
assert.equal(rejectResponse.status, 200);
const rejectPayload = await rejectResponse.json() as { ok: boolean; proposal?: { status?: string } };
assert.equal(rejectPayload.ok, true);
assert.equal(rejectPayload.proposal?.status, "rejected");
});

View File

@@ -0,0 +1,49 @@
import test from "node:test";
import assert from "node:assert/strict";
import os from "node:os";
import path from "node:path";
import { mkdir, mkdtemp, rm } from "node:fs/promises";
let runtimeRoot = "";
let readState: (typeof import("../src/lib/boss-data"))["readState"];
let updateMasterAgentEvolutionConfig: (typeof import("../src/lib/boss-data"))["updateMasterAgentEvolutionConfig"];
async function setup() {
if (runtimeRoot) {
return;
}
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-evolution-state-"));
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
const data = await import("../src/lib/boss-data.ts");
readState = data.readState;
updateMasterAgentEvolutionConfig = data.updateMasterAgentEvolutionConfig;
}
test.after(async () => {
if (runtimeRoot) {
await rm(runtimeRoot, { recursive: true, force: true });
}
});
test.beforeEach(async () => {
await setup();
await rm(runtimeRoot, { recursive: true, force: true });
await mkdir(runtimeRoot, { recursive: true });
});
test("boss state 初始化时包含 autonomous 默认配置与空集合", async () => {
const state = await readState();
assert.equal(state.masterAgentEvolutionConfig.mode, "autonomous");
assert.equal(state.masterAgentEvolutionConfig.autoApplyLowRiskRules, true);
assert.deepEqual(state.masterAgentEvolutionSignals, []);
assert.deepEqual(state.masterAgentEvolutionProposals, []);
assert.deepEqual(state.masterAgentEvolutionRules, []);
assert.deepEqual(state.masterAgentEvolutionRunLogs, []);
});
test("master agent evolution 配置可切换 autonomous 并自动打开低风险自动采纳", async () => {
const config = await updateMasterAgentEvolutionConfig({ mode: "autonomous" });
assert.equal(config.mode, "autonomous");
assert.equal(config.autoApplyLowRiskRules, true);
});

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",