feat: add native master agent evolution center
This commit is contained in:
@@ -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" />
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -1813,6 +1813,14 @@ 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_evolution":
|
||||
intent = new Intent(this, MasterAgentEvolutionActivity.class);
|
||||
break;
|
||||
case "ai_accounts":
|
||||
intent = new Intent(this, AiAccountsActivity.class);
|
||||
break;
|
||||
|
||||
@@ -0,0 +1,304 @@
|
||||
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;
|
||||
|
||||
@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(() -> renderDashboard(response.json));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
contentLoaded = false;
|
||||
replaceContent(BossUi.buildEmptyCard(this, "自动进化加载失败:" + 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");
|
||||
|
||||
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 自动进化",
|
||||
"最近在学什么、打算怎么改、已经生效了什么",
|
||||
"支持在这里切换 controlled / autonomous,并直接审核待处理提案。"
|
||||
));
|
||||
|
||||
contentRoot.addView(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"当前模式",
|
||||
"autonomous".equals(currentMode) ? "完全自我进化" : "受控自动进化",
|
||||
autoApplyLowRiskRules ? "低风险提案会自动采纳。" : "所有提案都需要人工确认。"
|
||||
));
|
||||
|
||||
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));
|
||||
|
||||
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", "-")
|
||||
+ " · " + proposal.optString("createdAt", "-"),
|
||||
null,
|
||||
null
|
||||
));
|
||||
Button rejectButton = BossUi.buildMiniActionButton(this, "拒绝", false);
|
||||
rejectButton.setOnClickListener(v -> reviewProposal(proposalId, false));
|
||||
Button approveButton = BossUi.buildMiniActionButton(this, "批准", true);
|
||||
approveButton.setOnClickListener(v -> reviewProposal(proposalId, true));
|
||||
contentRoot.addView(BossUi.buildInlineActionRow(this, rejectButton, approveButton));
|
||||
}
|
||||
if (!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", ""),
|
||||
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", "直接创建"),
|
||||
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(() -> {
|
||||
showMessage("已切到 " + ("autonomous".equals(mode) ? "完全自我进化" : "受控自动进化"));
|
||||
setResult(RESULT_OK);
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("切换失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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(() -> {
|
||||
showMessage(approve ? "提案已批准" : "提案已拒绝");
|
||||
setResult(RESULT_OK);
|
||||
reload();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage((approve ? "批准失败:" : "拒绝失败:") + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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:
|
||||
|
||||
@@ -19,6 +19,8 @@ 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_evolution", "主 Agent 自动进化", "查看进化信号、提案与自动采纳规则"),
|
||||
new MeMenuItem("security", "账号与安全", "修改登录密码、设备安全与身份校验"),
|
||||
new MeMenuItem("settings", "设置", "默认首页、提醒方式与危险操作确认"),
|
||||
new MeMenuItem("ops", "运维与修复", "查看运维会话、修复回放与 standby 切换"),
|
||||
|
||||
@@ -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"));
|
||||
|
||||
@@ -40,7 +40,7 @@ public class BossUiRootSurfaceTest {
|
||||
ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot");
|
||||
|
||||
LinearLayout content = ReflectionHelpers.getField(activity, "screenContent");
|
||||
assertEquals("我的页应是资料头 + 6 条菜单", 7, content.getChildCount());
|
||||
assertEquals("我的页应是资料头 + 8 条菜单", 9, content.getChildCount());
|
||||
|
||||
View header = content.getChildAt(0);
|
||||
assertEquals("资料头不应保留浮层卡片感", 0f, header.getElevation(), 0.01f);
|
||||
@@ -49,6 +49,8 @@ public class BossUiRootSurfaceTest {
|
||||
assertTrue(viewTreeContainsText(header, "最高管理员"));
|
||||
assertTrue(viewTreeContainsText(header, "主控账号已启用安全保护"));
|
||||
|
||||
assertTrue(viewTreeContainsText(content, "主 Agent 提示词 / 记忆"));
|
||||
assertTrue(viewTreeContainsText(content, "主 Agent 自动进化"));
|
||||
assertTrue(viewTreeContainsText(content, "账号与安全"));
|
||||
assertTrue(viewTreeContainsText(content, "设置"));
|
||||
assertTrue(viewTreeContainsText(content, "运维与修复"));
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
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);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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_evolution", items[1].key);
|
||||
assertEquals("主 Agent 自动进化", items[1].title);
|
||||
}
|
||||
}
|
||||
@@ -192,7 +192,7 @@ public class WechatSurfaceMapperTest {
|
||||
@Test
|
||||
public void rootMeMenuTitles_matchLegacyWechatMenuWithOpsEntry() throws Exception {
|
||||
assertArrayEquals(
|
||||
new String[]{"账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
|
||||
new String[]{"主 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 自动进化", "账号与安全", "设置", "运维与修复", "AI 账号", "技能", "关于"},
|
||||
WechatSurfaceMapper.rootMeMenuTitles()
|
||||
);
|
||||
}
|
||||
@@ -380,15 +380,18 @@ 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(8, items.length);
|
||||
assertEquals("master_agent_prompt", items[0].key);
|
||||
assertEquals("主 Agent 提示词 / 记忆", items[0].title);
|
||||
assertEquals("master_agent_evolution", items[1].key);
|
||||
assertEquals("主 Agent 自动进化", items[1].title);
|
||||
assertEquals("security", items[2].key);
|
||||
assertEquals("settings", items[3].key);
|
||||
assertEquals("ops", items[4].key);
|
||||
assertEquals("运维与修复", items[4].title);
|
||||
assertEquals("ai_accounts", items[5].key);
|
||||
assertEquals("skills", items[6].key);
|
||||
assertEquals("about", items[7].key);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
- `android/app/src/main/java/com/hyzq/boss/MainActivity.java`:原生入口 Activity
|
||||
- `android/app/src/main/java/com/hyzq/boss/BossApiClient.java`:原生 API 客户端
|
||||
- `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java`:原生聊天优先项目页,只保留目标/版本轻入口
|
||||
- `android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java`:原生主 Agent 自动进化页,可查看信号/提案/规则并切换模式
|
||||
- `android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java`:原生微信式会话信息页,支持线程改名和发起群聊
|
||||
- `android/app/src/main/java/com/hyzq/boss/GroupInfoActivity.java`:原生群资料页,支持群名修改与成员查看
|
||||
- `android/app/src/main/java/com/hyzq/boss/GroupCreateActivity.java`:原生独立群聊创建页
|
||||
@@ -156,6 +157,7 @@
|
||||
- `我的` 根页当前保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
|
||||
- `我的 > 主 Agent 提示词 / 记忆` 当前可编辑管理员全局主提示词、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 项目记忆
|
||||
- `我的 > 主 Agent 自动进化` 当前可查看进化信号、待审批提案、已生效规则,并切换 `controlled / autonomous`
|
||||
- 原生 Android 也已接上 `我的 > 主 Agent 自动进化` 与 `主 Agent 会话右上角 ... > 自动进化` 两个入口
|
||||
- `我的 > AI 账号` 必须可查看和切换 `主 GPT / 备用 GPT / API 容灾`
|
||||
- `我的 > 技能` 必须按绑定设备展示 Skill,并支持一键复制调用语句
|
||||
- `设备` 页当前只允许出现生产设备,旧演示脏数据不能回流到正式视图
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
- 当前原生活动页:
|
||||
- `MainActivity`
|
||||
- `ProjectDetailActivity`
|
||||
- `MasterAgentEvolutionActivity`
|
||||
- `ConversationInfoActivity`
|
||||
- `ThreadStatusActivity`
|
||||
- `GroupInfoActivity`
|
||||
@@ -74,7 +75,7 @@
|
||||
- 保留版本与 OTA 操作
|
||||
- 当前已补上 OTA 下载进度、失败重试、安装授权提示和返回关于页后的本地状态恢复
|
||||
- 当前 `我的` 根页:
|
||||
- 保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
|
||||
- 保留 `主 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 路由
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Boss 当前运行与部署状态
|
||||
|
||||
更新时间:`2026-04-03`
|
||||
更新时间:`2026-04-16`
|
||||
|
||||
## 1. 本地状态
|
||||
|
||||
@@ -136,6 +136,7 @@ cd /Users/kris/code/boss
|
||||
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择:Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型
|
||||
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看和编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖
|
||||
- 当前 `我的 > 主 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` 备用账号,不再把失败日志直接原样回给用户
|
||||
|
||||
Reference in New Issue
Block a user