Compare commits
6 Commits
codex/mast
...
codex/mast
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36a2cd8dfd | ||
|
|
e8304faebc | ||
|
|
4bedf75dc2 | ||
|
|
363f732e29 | ||
|
|
f0490de180 | ||
|
|
504d112218 |
@@ -106,7 +106,7 @@ Android APK:
|
||||
- Android 真机无线调试如果要尽量稳定,优先使用“同一局域网 + 初次 USB 启用后执行 `adb tcpip 5555` + `adb connect <phone-ip>:5555`”这条链路;它通常比只依赖系统“无线调试配对码”更稳
|
||||
- Android 系统层面对“无线调试”没有真正的永久不掉线开关;重启手机、切 Wi‑Fi、切热点、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 节点`
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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 "当前没有任何提示词内容。";
|
||||
|
||||
@@ -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,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 切换"),
|
||||
|
||||
@@ -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("我的页应是资料头 + 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, "运维与修复"));
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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,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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -54,6 +54,7 @@
|
||||
- `android/app/src/main/java/com/hyzq/boss/MainActivity.java`:原生入口 Activity
|
||||
- `android/app/src/main/java/com/hyzq/boss/BossApiClient.java`:原生 API 客户端
|
||||
- `android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java`:原生聊天优先项目页,只保留目标/版本轻入口
|
||||
- `android/app/src/main/java/com/hyzq/boss/MasterAgentEvolutionActivity.java`:原生主 Agent 自动进化页,可查看信号/提案/规则并切换模式
|
||||
- `android/app/src/main/java/com/hyzq/boss/ConversationInfoActivity.java`:原生微信式会话信息页,支持线程改名和发起群聊
|
||||
- `android/app/src/main/java/com/hyzq/boss/GroupInfoActivity.java`:原生群资料页,支持群名修改与成员查看
|
||||
- `android/app/src/main/java/com/hyzq/boss/GroupCreateActivity.java`:原生独立群聊创建页
|
||||
@@ -155,6 +156,8 @@
|
||||
- `版本迭代记录` 只读,由主 Agent 汇总
|
||||
- `我的` 根页当前保留 `账号与安全 / 设置 / 运维与修复 / AI 账号 / 技能 / 关于`
|
||||
- `我的 > 主 Agent 提示词 / 记忆` 当前可编辑管理员全局主提示词、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 项目记忆
|
||||
- `我的 > 主 Agent 自动进化` 当前可查看进化信号、待审批提案、已生效规则,并切换 `controlled / autonomous`
|
||||
- 原生 Android 也已接上 `我的 > 主 Agent 自动进化` 与 `主 Agent 会话右上角 ... > 自动进化` 两个入口
|
||||
- `我的 > AI 账号` 必须可查看和切换 `主 GPT / 备用 GPT / API 容灾`
|
||||
- `我的 > 技能` 必须按绑定设备展示 Skill,并支持一键复制调用语句
|
||||
- `设备` 页当前只允许出现生产设备,旧演示脏数据不能回流到正式视图
|
||||
|
||||
@@ -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 路由
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Boss 当前运行与部署状态
|
||||
|
||||
更新时间:`2026-04-03`
|
||||
更新时间:`2026-04-16`
|
||||
|
||||
## 1. 本地状态
|
||||
|
||||
@@ -134,7 +134,9 @@ cd /Users/kris/code/boss
|
||||
- 当前 `AI 账号` 页顶部会直接展示“当前主控身份”,并提供 `校验主控 / 测试主 Agent 对话` 两个入口,切换主控后不必再手动退回会话页验证
|
||||
- 当前阿里百炼备用链已完成一次真实线上闭环验证:手动切到 `aliyun-qwen-backup` 后,`POST /api/v1/projects/master-agent/messages` 会返回 `queued`,并已实际回流 `阿里备用链正常。` 到 `master-agent` 会话
|
||||
- 当前 `我的 > AI 账号` 已把阿里百炼备用模型切成预设选择:Web 和原生 Android 都支持直接切换 `qwen3.5-plus / qwen3.5-flash`,只有预设不适用时才需要填写自定义模型
|
||||
- 当前 `我的 > 主 Agent 提示词 / 记忆` 页面已接通:管理员全局主提示词只读展示、用户主提示词、当前对话附加提示词,以及用户通用记忆 / 跨项目项目记忆都可以在 Web 端查看和编辑;当前对话设置按登录账号隔离,管理员全局主提示词不可覆盖
|
||||
- 当前 `我的` 根页已经拆出 `主 Agent 提示词 / 主 Agent 记忆 / 全局接管 / 主 Agent 自动进化` 四个独立入口;提示词页用于管理员全局主提示词、用户主提示词、当前对话附加提示词与执行后端设置,记忆页用于用户通用记忆和项目记忆的查看、编辑与归档
|
||||
- 当前 `我的 > 主 Agent 自动进化` 页面已接通:Web `/me/master-agent/evolution` 可查看最近信号、待审批提案和已生效规则,并允许管理员切换 `controlled / autonomous`、批准或拒绝提案
|
||||
- 当前原生 Android 也已接通 `主 Agent 自动进化`:`我的` 根页可直接进入,`master-agent` 会话右上角 `...` 菜单也可直达;页面支持查看最近信号、待审批提案、已生效规则。管理员可切换 `controlled / autonomous` 并批准/拒绝提案,非管理员显示只读视角
|
||||
- 当前 Web 端 `master-agent` 会话页右上角也已补齐微信式三点菜单,支持直接进入 `提示词 / 模型 / 推理强度 / 记忆 / 刷新`
|
||||
- 当前 `approval_required` 群聊在 Web 端已统一用单一状态快照驱动:如果存在新的待确认推荐,会自动折叠旧的拒绝态;如果上次推荐已拒绝,会明确展示“重新生成新的推荐”的恢复入口
|
||||
- 当前如果主控身份还是 `Master Codex Node`,但该节点离线或执行立即失败,主 Agent 会优先尝试已配置的 `OpenAI API / 阿里百炼 Qwen` 备用账号,不再把失败日志直接原样回给用户
|
||||
@@ -164,6 +166,8 @@ cd /Users/kris/code/boss
|
||||
- 主 Agent 单聊当前已改成“快速入队 + 异步回流”:`POST /api/v1/projects/master-agent/messages` 会先返回 `masterReplyState + task`,真实回复随后再回写消息账本
|
||||
- 当前对话级 `agentControls` 已经生效:`master-agent` 会话支持 `modelOverride / reasoningEffortOverride` 强制覆盖,也支持 `fastModelOverride / fastReasoningEffortOverride / smartModelOverride / smartReasoningEffortOverride` 这组策略默认值;主 Agent 普通对话默认按 fast 档选模型,深度任务可按 smart 档选模型,手动强制覆盖仍然优先级最高
|
||||
- 当前主 Agent 自动进化引擎已接入第一阶段共享内核:`masterAgentEvolutionConfig / Signals / Proposals / Rules / RunLogs` 已进入状态模型,`GET /api/v1/master-agent/evolution` 可查看进化状态,`POST /api/v1/master-agent/evolution/config` 可在 `controlled / autonomous` 间切换,提案支持 approve / reject;`controlled` 只生成待审批提案,`autonomous` 可自动采纳低风险 `memory_patch / routing_preference_patch / fast_path_rule`
|
||||
- 当前 `codex/master-agent-autonomous-evolution` 分支把默认 evolution mode 改成 `autonomous`,新初始化状态会默认开启低风险自动采纳;`codex/master-agent-controlled-evolution` 保持 `controlled` 默认值
|
||||
- 当前主 Agent 会在 fast path 判断前捕获状态类问题信号;重复短问句会沉淀为 `repeated_question`,显式后端回退会附带 `fallbackToBackendId`,在 autonomous 模式下可自动写回 `backendOverride`
|
||||
- 当前 `group_dispatch_plan / device_import_resolution / attachment_analysis` 三类深度任务已经会把 `smart*` 策略下发到任务队列,并随任务持久化 `executionModel / executionReasoningEffort`;local-agent 执行这类任务时会优先吃任务级模型,不再只依赖本机固定默认模型
|
||||
- 当前对话级 `agentControls` 也已支持 `backendOverride`:`master-agent` 会话可在 `Claw Runtime` 或 `Hermes Runtime` 可用时显式选择 `claw-runtime / hermes-runtime`,普通单线程会话当前只开放 `hermes-runtime`;不可用时保存接口会直接拒绝,并返回人类可读原因
|
||||
- 原生 Android 当前会把 `master-agent` 的等待态保留在消息流里:发送后常驻显示“主 Agent 思考中”,超时后改成“主 Agent 回复超时 + 重试等待”,收到新回复后会自动清掉,不再只靠 toast 提示
|
||||
|
||||
@@ -12,5 +12,10 @@ export async function GET(request: NextRequest) {
|
||||
}
|
||||
|
||||
const dashboard = await getMasterAgentEvolutionDashboard();
|
||||
return jsonNoStore({ ok: true, ...dashboard });
|
||||
return jsonNoStore({
|
||||
ok: true,
|
||||
canManage: session.role === "highest_admin",
|
||||
role: session.role,
|
||||
...dashboard,
|
||||
});
|
||||
}
|
||||
|
||||
36
src/app/me/master-agent/evolution/page.tsx
Normal file
36
src/app/me/master-agent/evolution/page.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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="附件与存储"
|
||||
|
||||
237
src/components/master-agent-evolution-client.tsx
Normal file
237
src/components/master-agent-evolution-client.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -1537,8 +1537,8 @@ const initialState: BossState = {
|
||||
userMasterPrompts: [],
|
||||
masterAgentMemories: [],
|
||||
masterAgentEvolutionConfig: {
|
||||
mode: "controlled",
|
||||
autoApplyLowRiskRules: false,
|
||||
mode: "autonomous",
|
||||
autoApplyLowRiskRules: true,
|
||||
updatedAt: nowIso(),
|
||||
},
|
||||
masterAgentEvolutionSignals: [],
|
||||
|
||||
@@ -1726,10 +1726,6 @@ type MasterAgentFastIntentContext = {
|
||||
effectiveDeepTaskPolicy: ReturnType<typeof resolveMasterAgentModelPolicy>;
|
||||
};
|
||||
|
||||
function buildMasterAgentRuntimeBackendLabel(context: MasterAgentFastIntentContext) {
|
||||
return context.agentControls?.backendOverride?.trim() || "master-codex-node";
|
||||
}
|
||||
|
||||
function getMasterAgentRuntimeDevice(context: MasterAgentFastIntentContext) {
|
||||
const deviceId =
|
||||
context.runtime.account.nodeId?.trim() ||
|
||||
@@ -1952,7 +1948,7 @@ async function appendFastPathError(
|
||||
}
|
||||
|
||||
async function tryRecordMasterAgentEvolutionSignal(input: {
|
||||
kind: "fast_path_candidate" | "user_correction" | "backend_fallback";
|
||||
kind: "fast_path_candidate" | "repeated_question" | "user_correction" | "backend_fallback";
|
||||
account: string;
|
||||
requestText: string;
|
||||
replyText?: string;
|
||||
@@ -1972,6 +1968,47 @@ async function tryRecordMasterAgentEvolutionSignal(input: {
|
||||
}
|
||||
}
|
||||
|
||||
function isMasterAgentEvolutionStatusLikeRequest(requestText: string) {
|
||||
return /(当前|现在|有没有|是否|哪个|什么|在线吗|状态)/i.test(requestText);
|
||||
}
|
||||
|
||||
function normalizeMasterAgentEvolutionQuestion(requestText: string) {
|
||||
return normalizeLexicalText(requestText)
|
||||
.replace(/当前|现在|目前|一下|帮我|请问|请/g, "")
|
||||
.replace(/吗|呢|呀|啊/g, "")
|
||||
.trim();
|
||||
}
|
||||
|
||||
async function isRepeatedMasterAgentStatusQuestion(params: {
|
||||
requestText: string;
|
||||
requestMessageId?: string;
|
||||
}) {
|
||||
const normalizedRequest = normalizeMasterAgentEvolutionQuestion(params.requestText);
|
||||
if (!normalizedRequest) {
|
||||
return false;
|
||||
}
|
||||
const state = await readState();
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
if (!masterProject) {
|
||||
return false;
|
||||
}
|
||||
const priorUserMessages = [...masterProject.messages]
|
||||
.reverse()
|
||||
.filter((message) => message.sender === "user" && message.id !== params.requestMessageId)
|
||||
.slice(0, 8);
|
||||
return priorUserMessages.some((message) => {
|
||||
const normalizedBody = normalizeMasterAgentEvolutionQuestion(message.body);
|
||||
if (!normalizedBody) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
normalizedBody === normalizedRequest ||
|
||||
normalizedBody.includes(normalizedRequest) ||
|
||||
normalizedRequest.includes(normalizedBody)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
function buildModelSummaryReply(context: MasterAgentFastIntentContext, requestText: string) {
|
||||
const normalized = normalizeLexicalText(requestText);
|
||||
const manualModel = context.agentControls?.modelOverride?.trim() || "";
|
||||
@@ -3250,6 +3287,22 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
currentSessionExpiresAt?: string;
|
||||
mode?: "wait" | "enqueue";
|
||||
}) {
|
||||
if (isMasterAgentEvolutionStatusLikeRequest(params.requestText)) {
|
||||
const repeatedQuestion = await isRepeatedMasterAgentStatusQuestion({
|
||||
requestText: params.requestText,
|
||||
requestMessageId: params.requestMessageId,
|
||||
});
|
||||
await tryRecordMasterAgentEvolutionSignal({
|
||||
kind: repeatedQuestion ? "repeated_question" : "fast_path_candidate",
|
||||
account: params.requestedByAccount,
|
||||
requestText: params.requestText,
|
||||
metadataJson: {
|
||||
source: "replyToMasterAgentUserMessage.pre_fast_intent",
|
||||
repeatedQuestion,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const fastIntentResult = await tryHandleMasterAgentFastIntent({
|
||||
requestText: params.requestText,
|
||||
requestedByAccount: params.requestedByAccount,
|
||||
@@ -3258,17 +3311,6 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
return fastIntentResult;
|
||||
}
|
||||
|
||||
if (/(当前|现在|有没有|是否|哪个|什么|在线吗|状态)/i.test(params.requestText)) {
|
||||
await tryRecordMasterAgentEvolutionSignal({
|
||||
kind: "fast_path_candidate",
|
||||
account: params.requestedByAccount,
|
||||
requestText: params.requestText,
|
||||
metadataJson: {
|
||||
source: "replyToMasterAgentUserMessage.pre_slow_path",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const runtime = await getMasterAgentRuntimeAccount();
|
||||
|
||||
if (!runtime?.account) {
|
||||
@@ -3311,6 +3353,22 @@ export async function replyToMasterAgentUserMessage(params: {
|
||||
};
|
||||
const selectedBackend = await selectExecutionBackend(backendSelectionInput);
|
||||
const backendChoices = listExecutionBackendChoices(backendSelectionInput);
|
||||
const requestedBackendId = executionConfig.agentControls?.backendOverride?.trim() || "";
|
||||
if (
|
||||
requestedBackendId &&
|
||||
selectedBackend.backendId !== requestedBackendId &&
|
||||
(selectedBackend.backendId === CLAW_BACKEND_ID || selectedBackend.backendId === HERMES_BACKEND_ID)
|
||||
) {
|
||||
await tryRecordMasterAgentEvolutionSignal({
|
||||
kind: "backend_fallback",
|
||||
account: params.requestedByAccount,
|
||||
requestText: params.requestText,
|
||||
metadataJson: {
|
||||
requestedBackendId,
|
||||
fallbackToBackendId: selectedBackend.backendId,
|
||||
},
|
||||
});
|
||||
}
|
||||
const agentControls = executionConfig.agentControls;
|
||||
const masterExecutionPrompt = buildMasterCodexNodePrompt(
|
||||
state,
|
||||
|
||||
@@ -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: "提示词",
|
||||
|
||||
@@ -31,6 +31,7 @@ function inferProposalFromSignal(input: {
|
||||
projectId: string;
|
||||
requestText: string;
|
||||
signalId: string;
|
||||
metadataJson?: Record<string, unknown>;
|
||||
}): Omit<
|
||||
Parameters<typeof createMasterAgentEvolutionProposalInState>[0],
|
||||
"status"
|
||||
@@ -76,6 +77,11 @@ function inferProposalFromSignal(input: {
|
||||
}
|
||||
|
||||
if (input.kind === "backend_fallback") {
|
||||
const fallbackToBackendId =
|
||||
input.metadataJson?.fallbackToBackendId === "hermes-runtime" ||
|
||||
input.metadataJson?.fallbackToBackendId === "claw-runtime"
|
||||
? input.metadataJson.fallbackToBackendId
|
||||
: undefined;
|
||||
return {
|
||||
proposalType: "routing_preference_patch",
|
||||
account: input.account,
|
||||
@@ -84,9 +90,10 @@ function inferProposalFromSignal(input: {
|
||||
summary: "检测到后端回退,建议后续优先选择最近稳定的可用后端。",
|
||||
patchJson: {
|
||||
backendPreference: "prefer_available_runtime",
|
||||
backendOverride: fallbackToBackendId,
|
||||
},
|
||||
sourceSignalIds: [input.signalId],
|
||||
confidence: 0.72,
|
||||
confidence: fallbackToBackendId ? 0.8 : 0.72,
|
||||
riskLevel: "low",
|
||||
};
|
||||
}
|
||||
@@ -193,6 +200,7 @@ export async function recordMasterAgentEvolutionSignal(input: {
|
||||
projectId: signal.projectId,
|
||||
requestText: signal.requestText,
|
||||
signalId: signal.signalId,
|
||||
metadataJson: signal.metadataJson,
|
||||
});
|
||||
if (!proposalInput) {
|
||||
return { signal, proposal: null };
|
||||
|
||||
@@ -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",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 专属菜单", () => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
|
||||
let runtimeRoot = "";
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
|
||||
let recordMasterAgentEvolutionSignal: (typeof import("../src/lib/master-agent-evolution"))["recordMasterAgentEvolutionSignal"];
|
||||
let listMasterAgentEvolutionProposals: (typeof import("../src/lib/master-agent-evolution"))["listMasterAgentEvolutionProposals"];
|
||||
let setMasterAgentEvolutionMode: (typeof import("../src/lib/master-agent-evolution"))["setMasterAgentEvolutionMode"];
|
||||
@@ -22,6 +23,7 @@ async function setup() {
|
||||
import("../src/lib/master-agent-evolution.ts"),
|
||||
]);
|
||||
readState = data.readState;
|
||||
getProjectAgentControls = data.getProjectAgentControls;
|
||||
recordMasterAgentEvolutionSignal = evolution.recordMasterAgentEvolutionSignal;
|
||||
listMasterAgentEvolutionProposals = evolution.listMasterAgentEvolutionProposals;
|
||||
setMasterAgentEvolutionMode = evolution.setMasterAgentEvolutionMode;
|
||||
@@ -40,6 +42,8 @@ test.beforeEach(async () => {
|
||||
});
|
||||
|
||||
test("recording repeated deterministic questions creates a pending fast_path_rule proposal in controlled mode", async () => {
|
||||
await setMasterAgentEvolutionMode("controlled");
|
||||
|
||||
const result = await recordMasterAgentEvolutionSignal({
|
||||
kind: "repeated_question",
|
||||
projectId: "master-agent",
|
||||
@@ -71,3 +75,25 @@ test("autonomous mode auto applies low risk fast path proposals as evolution rul
|
||||
assert.equal(state.masterAgentEvolutionRules.length, 1);
|
||||
assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "fast_path_rule");
|
||||
});
|
||||
|
||||
test("autonomous mode auto applies backend fallback proposals into master-agent backend override", async () => {
|
||||
await setMasterAgentEvolutionMode("autonomous");
|
||||
|
||||
const result = await recordMasterAgentEvolutionSignal({
|
||||
kind: "backend_fallback",
|
||||
projectId: "master-agent",
|
||||
account: "17600003315",
|
||||
requestText: "Hermes 不可用时自动回退到 Claw",
|
||||
metadataJson: {
|
||||
failedBackendId: "hermes-runtime",
|
||||
fallbackToBackendId: "claw-runtime",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.proposal?.status, "auto_applied");
|
||||
const state = await readState();
|
||||
const controls = await getProjectAgentControls("master-agent", "17600003315");
|
||||
assert.equal(state.masterAgentEvolutionRules.length, 1);
|
||||
assert.equal(state.masterAgentEvolutionRules[0]?.ruleType, "routing_preference_patch");
|
||||
assert.equal(controls?.backendOverride ?? null, "claw-runtime");
|
||||
});
|
||||
|
||||
@@ -73,8 +73,9 @@ test("GET /api/v1/master-agent/evolution returns config proposals and rules", as
|
||||
await createAdminRequest("http://127.0.0.1:3000/api/v1/master-agent/evolution"),
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
const payload = await response.json() as { ok: boolean; proposals: unknown[]; rules: unknown[] };
|
||||
const payload = await response.json() as { ok: boolean; canManage?: boolean; proposals: unknown[]; rules: unknown[] };
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.canManage, true);
|
||||
assert.ok(Array.isArray(payload.proposals));
|
||||
assert.ok(Array.isArray(payload.rules));
|
||||
});
|
||||
|
||||
@@ -32,10 +32,10 @@ test.beforeEach(async () => {
|
||||
await mkdir(runtimeRoot, { recursive: true });
|
||||
});
|
||||
|
||||
test("boss state 初始化时包含 master agent evolution 默认配置与空集合", async () => {
|
||||
test("boss state 初始化时包含 autonomous 默认配置与空集合", async () => {
|
||||
const state = await readState();
|
||||
assert.equal(state.masterAgentEvolutionConfig.mode, "controlled");
|
||||
assert.equal(state.masterAgentEvolutionConfig.autoApplyLowRiskRules, false);
|
||||
assert.equal(state.masterAgentEvolutionConfig.mode, "autonomous");
|
||||
assert.equal(state.masterAgentEvolutionConfig.autoApplyLowRiskRules, true);
|
||||
assert.deepEqual(state.masterAgentEvolutionSignals, []);
|
||||
assert.deepEqual(state.masterAgentEvolutionProposals, []);
|
||||
assert.deepEqual(state.masterAgentEvolutionRules, []);
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user