feat: add master-agent prompts and memory management
This commit is contained in:
@@ -48,6 +48,8 @@
|
||||
<activity android:name=".SettingsActivity" android:exported="false" />
|
||||
<activity android:name=".AiAccountsActivity" android:exported="false" />
|
||||
<activity android:name=".OpenAiOnboardingActivity" android:exported="false" />
|
||||
<activity android:name=".MasterAgentPromptActivity" android:exported="false" />
|
||||
<activity android:name=".MasterAgentMemoryActivity" android:exported="false" />
|
||||
<activity android:name=".OpsCenterActivity" android:exported="false" />
|
||||
<activity android:name=".AboutActivity" android:exported="false" />
|
||||
|
||||
|
||||
@@ -113,6 +113,43 @@ public class BossApiClient {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
|
||||
}
|
||||
|
||||
public ApiResponse updateProjectAgentControls(
|
||||
String projectId,
|
||||
@Nullable String modelOverride,
|
||||
@Nullable String reasoningEffortOverride,
|
||||
@Nullable String promptOverride
|
||||
) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("modelOverride", modelOverride == null ? JSONObject.NULL : modelOverride);
|
||||
payload.put("reasoningEffortOverride", reasoningEffortOverride == null ? JSONObject.NULL : reasoningEffortOverride);
|
||||
payload.put("promptOverride", promptOverride == null ? JSONObject.NULL : promptOverride);
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/agent-controls", payload);
|
||||
}
|
||||
|
||||
public ApiResponse getMasterAgentPromptProfile(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/prompt-profile", null);
|
||||
}
|
||||
|
||||
public ApiResponse updateMasterAgentPromptProfile(String projectId, JSONObject payload) throws IOException, JSONException {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/prompt-profile", payload);
|
||||
}
|
||||
|
||||
public ApiResponse getMasterAgentMemories(String projectId) throws IOException, JSONException {
|
||||
return requestWithRestore("GET", "/api/v1/projects/" + encode(projectId) + "/memories", null);
|
||||
}
|
||||
|
||||
public ApiResponse createMasterAgentMemory(String projectId, JSONObject payload) throws IOException, JSONException {
|
||||
return requestWithRestore("POST", "/api/v1/projects/" + encode(projectId) + "/memories", payload);
|
||||
}
|
||||
|
||||
public ApiResponse updateMasterAgentMemory(String projectId, String memoryId, JSONObject payload) throws IOException, JSONException {
|
||||
return requestWithRestore("PATCH", "/api/v1/projects/" + encode(projectId) + "/memories/" + encode(memoryId), payload);
|
||||
}
|
||||
|
||||
public ApiResponse deleteMasterAgentMemory(String projectId, String memoryId) throws IOException, JSONException {
|
||||
return requestWithRestore("DELETE", "/api/v1/projects/" + encode(projectId) + "/memories/" + encode(memoryId), null);
|
||||
}
|
||||
|
||||
public ApiResponse confirmDispatchPlan(String projectId, String planId, JSONArray approvedTargetProjectIds) throws IOException, JSONException {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put(
|
||||
|
||||
@@ -0,0 +1,398 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.DialogInterface;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.Spinner;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AlertDialog;
|
||||
|
||||
import org.json.JSONArray;
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class MasterAgentMemoryActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
|
||||
private static final String[] MEMORY_SCOPE_VALUES = {"global", "project"};
|
||||
private static final String[] MEMORY_SCOPE_LABELS = {"我的通用记忆", "当前项目记忆"};
|
||||
private static final String[] MEMORY_TYPE_VALUES = {
|
||||
"user_preference",
|
||||
"project_progress",
|
||||
"decision",
|
||||
"risk",
|
||||
"blocking_issue",
|
||||
"research_note",
|
||||
"workflow_rule"
|
||||
};
|
||||
private static final String[] MEMORY_TYPE_LABELS = {
|
||||
"用户偏好",
|
||||
"项目进度",
|
||||
"决策",
|
||||
"风险",
|
||||
"阻塞",
|
||||
"调研结论",
|
||||
"工作规则"
|
||||
};
|
||||
|
||||
private String projectId;
|
||||
private String projectName;
|
||||
private boolean contentLoaded;
|
||||
private @Nullable JSONObject globalMemoriesPayload;
|
||||
private @Nullable JSONObject projectMemoriesPayload;
|
||||
private @Nullable JSONArray globalMemoryItems;
|
||||
private @Nullable JSONArray projectMemoryItems;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("记忆", projectName == null ? "主 Agent 记忆" : projectName);
|
||||
setHeaderAction("新增", v -> openMemoryEditor(null));
|
||||
updateSaveAvailability();
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "缺少 projectId。"));
|
||||
setRefreshing(false);
|
||||
contentLoaded = false;
|
||||
updateSaveAvailability();
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.getMasterAgentMemories(projectId);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> renderMemories(response.json));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
contentLoaded = false;
|
||||
updateSaveAvailability();
|
||||
replaceContent(BossUi.buildEmptyCard(this, "记忆加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderMemories(JSONObject payload) {
|
||||
JSONObject memories = payload.optJSONObject("memories");
|
||||
globalMemoriesPayload = memories == null ? null : memories.optJSONObject("global");
|
||||
projectMemoriesPayload = memories == null ? null : memories.optJSONObject("project");
|
||||
globalMemoryItems = extractMemoryItems(memories, "global");
|
||||
projectMemoryItems = extractMemoryItems(memories, "project");
|
||||
|
||||
replaceContent();
|
||||
appendContent(BossUi.buildSimpleProfileHeader(
|
||||
this,
|
||||
projectName == null ? "主 Agent" : projectName,
|
||||
"自动沉淀 / 手动维护",
|
||||
"项目记忆默认挂到当前项目,通用记忆属于当前用户。"
|
||||
));
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"记忆说明",
|
||||
"主 Agent 会自动沉淀长期有用的信息。你也可以在这里手动新增、编辑或删除。",
|
||||
"底层是结构化存储,前台展示为轻量卡片。"
|
||||
));
|
||||
|
||||
renderSection(
|
||||
"我的通用记忆",
|
||||
globalMemoryItems,
|
||||
"当前没有通用记忆。"
|
||||
);
|
||||
renderSection(
|
||||
"当前项目记忆",
|
||||
projectMemoryItems,
|
||||
"当前项目还没有沉淀记忆。"
|
||||
);
|
||||
|
||||
contentLoaded = true;
|
||||
updateSaveAvailability();
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private void renderSection(String title, @Nullable JSONArray items, String emptyText) {
|
||||
int count = items == null ? 0 : items.length();
|
||||
appendContent(BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
title,
|
||||
count <= 0 ? emptyText : "共 " + count + " 条",
|
||||
null,
|
||||
null,
|
||||
null
|
||||
));
|
||||
if (items == null || items.length() == 0) {
|
||||
appendContent(BossUi.buildEmptyCard(this, emptyText));
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < items.length(); i++) {
|
||||
JSONObject memory = items.optJSONObject(i);
|
||||
if (memory == null) continue;
|
||||
appendContent(buildMemoryRow(memory));
|
||||
}
|
||||
}
|
||||
|
||||
private @Nullable JSONArray extractMemoryItems(@Nullable JSONObject memories, String key) {
|
||||
if (memories == null || !memories.has(key)) {
|
||||
return null;
|
||||
}
|
||||
Object value = memories.opt(key);
|
||||
if (value instanceof JSONArray) {
|
||||
return (JSONArray) value;
|
||||
}
|
||||
if (value instanceof JSONObject) {
|
||||
return ((JSONObject) value).optJSONArray("items");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private LinearLayout buildMemoryRow(JSONObject memory) {
|
||||
String scope = memory.optString("scope", "global");
|
||||
String type = memory.optString("memoryType", "user_preference");
|
||||
String title = memory.optString("title", "未命名记忆");
|
||||
String content = memory.optString("content", "");
|
||||
String tags = joinTags(memory.optJSONArray("tags"));
|
||||
String meta = memory.optString("updatedAt", memory.optString("createdAt", ""));
|
||||
if (!TextUtils.isEmpty(tags)) {
|
||||
meta = TextUtils.isEmpty(meta) ? tags : meta + " · " + tags;
|
||||
}
|
||||
String badge = "project".equals(scope) ? "当前项目" : "全局";
|
||||
String subtitle = memoryTypeLabel(type) + (TextUtils.isEmpty(content) ? "" : " · " + content);
|
||||
return BossUi.buildWechatMenuRow(
|
||||
this,
|
||||
title,
|
||||
subtitle,
|
||||
meta,
|
||||
badge,
|
||||
v -> openMemoryEditor(memory)
|
||||
);
|
||||
}
|
||||
|
||||
private void openMemoryEditor(@Nullable JSONObject memory) {
|
||||
LinearLayout form = new LinearLayout(this);
|
||||
form.setOrientation(LinearLayout.VERTICAL);
|
||||
|
||||
final Spinner scopeSpinner = new Spinner(this);
|
||||
ArrayAdapter<String> scopeAdapter = new ArrayAdapter<>(
|
||||
this,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
MEMORY_SCOPE_LABELS
|
||||
);
|
||||
scopeSpinner.setAdapter(scopeAdapter);
|
||||
|
||||
final Spinner typeSpinner = new Spinner(this);
|
||||
ArrayAdapter<String> typeAdapter = new ArrayAdapter<>(
|
||||
this,
|
||||
android.R.layout.simple_spinner_dropdown_item,
|
||||
MEMORY_TYPE_LABELS
|
||||
);
|
||||
typeSpinner.setAdapter(typeAdapter);
|
||||
|
||||
final EditText titleInput = BossUi.buildInput(this, "记忆标题", false);
|
||||
final EditText contentInput = BossUi.buildInput(this, "记忆内容", true);
|
||||
final EditText tagsInput = BossUi.buildInput(this, "标签,逗号分隔", false);
|
||||
contentInput.setMinLines(6);
|
||||
|
||||
if (memory != null) {
|
||||
titleInput.setText(memory.optString("title", ""));
|
||||
contentInput.setText(memory.optString("content", ""));
|
||||
tagsInput.setText(joinTags(memory.optJSONArray("tags")));
|
||||
scopeSpinner.setSelection("project".equals(memory.optString("scope", "global")) ? 1 : 0);
|
||||
typeSpinner.setSelection(memoryTypeIndex(memory.optString("memoryType", "user_preference")));
|
||||
} else {
|
||||
scopeSpinner.setSelection(0);
|
||||
typeSpinner.setSelection(0);
|
||||
}
|
||||
|
||||
form.addView(BossUi.buildFormCell(this, "作用域", "决定是用户通用记忆还是当前项目记忆。", scopeSpinner));
|
||||
form.addView(BossUi.buildFormCell(this, "标题", "一句话说明这条记忆。", titleInput));
|
||||
form.addView(BossUi.buildFormCell(this, "内容", "主 Agent 读取时会使用这段内容。", contentInput));
|
||||
form.addView(BossUi.buildFormCell(this, "类型", "帮助主 Agent 决定优先级与使用场景。", typeSpinner));
|
||||
form.addView(BossUi.buildFormCell(this, "标签", "以逗号分隔,便于后续检索和归档。", tagsInput));
|
||||
|
||||
AlertDialog.Builder builder = new AlertDialog.Builder(this)
|
||||
.setTitle(memory == null ? "新增记忆" : "编辑记忆")
|
||||
.setView(form)
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("保存", (dialog, which) -> saveMemory(
|
||||
memory,
|
||||
MEMORY_SCOPE_VALUES[scopeSpinner.getSelectedItemPosition()],
|
||||
titleInput.getText() == null ? "" : titleInput.getText().toString(),
|
||||
contentInput.getText() == null ? "" : contentInput.getText().toString(),
|
||||
MEMORY_TYPE_VALUES[typeSpinner.getSelectedItemPosition()],
|
||||
tagsInput.getText() == null ? "" : tagsInput.getText().toString()
|
||||
));
|
||||
|
||||
if (memory != null) {
|
||||
builder.setNeutralButton("删除", (dialog, which) -> confirmDeleteMemory(memory));
|
||||
}
|
||||
|
||||
builder.show();
|
||||
}
|
||||
|
||||
private void confirmDeleteMemory(JSONObject memory) {
|
||||
final String memoryId = memory.optString("memoryId", "");
|
||||
if (memoryId.isEmpty()) {
|
||||
showMessage("缺少 memoryId");
|
||||
return;
|
||||
}
|
||||
new AlertDialog.Builder(this)
|
||||
.setTitle("删除记忆")
|
||||
.setMessage("确定删除这条记忆吗?")
|
||||
.setNegativeButton("取消", null)
|
||||
.setPositiveButton("删除", (dialog, which) -> deleteMemory(memoryId))
|
||||
.show();
|
||||
}
|
||||
|
||||
private void saveMemory(
|
||||
@Nullable JSONObject existingMemory,
|
||||
String scope,
|
||||
String title,
|
||||
String content,
|
||||
String memoryType,
|
||||
String tagsText
|
||||
) {
|
||||
if (!contentLoaded && existingMemory == null) {
|
||||
showMessage("记忆尚未加载完成,请先刷新成功后再保存。");
|
||||
return;
|
||||
}
|
||||
final String normalizedTitle = title == null ? "" : title.trim();
|
||||
final String normalizedContent = content == null ? "" : content.trim();
|
||||
if (normalizedTitle.isEmpty()) {
|
||||
showMessage("记忆标题不能为空");
|
||||
return;
|
||||
}
|
||||
if (normalizedContent.isEmpty()) {
|
||||
showMessage("记忆内容不能为空");
|
||||
return;
|
||||
}
|
||||
final JSONArray tags = parseTags(tagsText);
|
||||
final boolean projectScope = "project".equals(scope);
|
||||
final String memoryId = existingMemory == null ? "" : existingMemory.optString("memoryId", "");
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("scope", scope);
|
||||
if (projectScope) {
|
||||
payload.put("projectId", projectId);
|
||||
}
|
||||
payload.put("title", normalizedTitle);
|
||||
payload.put("content", normalizedContent);
|
||||
payload.put("memoryType", memoryType);
|
||||
payload.put("tags", tags);
|
||||
if (existingMemory != null && existingMemory.has("sourceMessageId")) {
|
||||
payload.put("sourceMessageId", existingMemory.optString("sourceMessageId", ""));
|
||||
}
|
||||
|
||||
BossApiClient.ApiResponse response = memoryId.isEmpty()
|
||||
? apiClient.createMasterAgentMemory(projectId, payload)
|
||||
: apiClient.updateMasterAgentMemory(projectId, memoryId, payload);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
showMessage("记忆已保存");
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("记忆保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void deleteMemory(String memoryId) {
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.deleteMasterAgentMemory(projectId, memoryId);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
showMessage("记忆已删除");
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("记忆删除失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private int memoryTypeIndex(String memoryType) {
|
||||
for (int i = 0; i < MEMORY_TYPE_VALUES.length; i++) {
|
||||
if (MEMORY_TYPE_VALUES[i].equals(memoryType)) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
private JSONArray parseTags(String rawTags) {
|
||||
JSONArray tags = new JSONArray();
|
||||
if (rawTags == null) {
|
||||
return tags;
|
||||
}
|
||||
String[] parts = rawTags.split("[,,]");
|
||||
for (String part : parts) {
|
||||
String tag = part == null ? "" : part.trim();
|
||||
if (!tag.isEmpty()) {
|
||||
tags.put(tag);
|
||||
}
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
private String joinTags(@Nullable JSONArray tags) {
|
||||
if (tags == null || tags.length() == 0) {
|
||||
return "";
|
||||
}
|
||||
StringBuilder builder = new StringBuilder();
|
||||
for (int i = 0; i < tags.length(); i++) {
|
||||
String tag = tags.optString(i, "").trim();
|
||||
if (tag.isEmpty()) continue;
|
||||
if (builder.length() > 0) {
|
||||
builder.append(" · ");
|
||||
}
|
||||
builder.append(tag);
|
||||
}
|
||||
return builder.toString();
|
||||
}
|
||||
|
||||
private String memoryTypeLabel(String memoryType) {
|
||||
for (int i = 0; i < MEMORY_TYPE_VALUES.length; i++) {
|
||||
if (MEMORY_TYPE_VALUES[i].equals(memoryType)) {
|
||||
return MEMORY_TYPE_LABELS[i];
|
||||
}
|
||||
}
|
||||
return memoryType;
|
||||
}
|
||||
|
||||
private void updateSaveAvailability() {
|
||||
if (headerActionButton != null) {
|
||||
headerActionButton.setEnabled(contentLoaded);
|
||||
headerActionButton.setAlpha(contentLoaded ? 1f : 0.45f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package com.hyzq.boss;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import org.json.JSONObject;
|
||||
|
||||
public class MasterAgentPromptActivity extends BossScreenActivity {
|
||||
public static final String EXTRA_PROJECT_ID = "project_id";
|
||||
public static final String EXTRA_PROJECT_NAME = "project_name";
|
||||
|
||||
private String projectId;
|
||||
private String projectName;
|
||||
private boolean contentLoaded;
|
||||
private @Nullable JSONObject promptPolicy;
|
||||
private @Nullable JSONObject userPrompt;
|
||||
private @Nullable JSONObject projectControls;
|
||||
private @Nullable String adminPromptText;
|
||||
private @Nullable String userPromptText;
|
||||
private @Nullable String projectPromptOverrideText;
|
||||
private EditText userPromptInput;
|
||||
private EditText projectPromptInput;
|
||||
|
||||
@Override
|
||||
protected void onCreate(@Nullable Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
projectId = getIntent().getStringExtra(EXTRA_PROJECT_ID);
|
||||
projectName = getIntent().getStringExtra(EXTRA_PROJECT_NAME);
|
||||
configureScreen("提示词", projectName == null ? "主 Agent 提示词分层" : projectName);
|
||||
setHeaderAction("保存", v -> savePromptProfile());
|
||||
updateSaveAvailability();
|
||||
reload();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void reload() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
replaceContent(BossUi.buildEmptyCard(this, "缺少 projectId。"));
|
||||
setRefreshing(false);
|
||||
contentLoaded = false;
|
||||
updateSaveAvailability();
|
||||
return;
|
||||
}
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
BossApiClient.ApiResponse response = apiClient.getMasterAgentPromptProfile(projectId);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> renderPromptProfile(response.json));
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
contentLoaded = false;
|
||||
updateSaveAvailability();
|
||||
replaceContent(BossUi.buildEmptyCard(this, "提示词加载失败:" + error.getMessage()));
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void renderPromptProfile(JSONObject payload) {
|
||||
promptPolicy = payload.optJSONObject("promptPolicy");
|
||||
userPrompt = payload.optJSONObject("userPrompt");
|
||||
projectControls = payload.optJSONObject("projectControls");
|
||||
adminPromptText = promptPolicy == null ? null : promptPolicy.optString("globalPrompt", "");
|
||||
userPromptText = userPrompt == null ? "" : userPrompt.optString("content", "");
|
||||
projectPromptOverrideText = payload.optString(
|
||||
"projectPromptOverride",
|
||||
projectControls == null ? "" : projectControls.optString("promptOverride", "")
|
||||
);
|
||||
|
||||
replaceContent();
|
||||
appendContent(BossUi.buildSimpleProfileHeader(
|
||||
this,
|
||||
projectName == null ? "主 Agent" : projectName,
|
||||
"管理员全局主提示词 + 用户私有主提示词 + 当前对话提示词",
|
||||
"管理员提示词不可覆盖,用户可编辑自己的主提示词和当前对话覆盖。"
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"管理员全局主提示词",
|
||||
TextUtils.isEmpty(adminPromptText) ? "暂无全局主提示词。" : adminPromptText,
|
||||
"只读 · 由管理员 Web 后台配置 · 不可覆盖"
|
||||
));
|
||||
|
||||
userPromptInput = BossUi.buildInput(this, "编辑当前用户的主 Agent 提示词", true);
|
||||
userPromptInput.setText(TextUtils.isEmpty(userPromptText) ? "" : userPromptText);
|
||||
userPromptInput.setMinLines(8);
|
||||
userPromptInput.setText(userPromptText == null ? "" : userPromptText);
|
||||
appendContent(BossUi.buildFormCell(
|
||||
this,
|
||||
"用户私有主提示词",
|
||||
"仅影响当前登录用户的主 Agent 对话。",
|
||||
userPromptInput
|
||||
));
|
||||
|
||||
projectPromptInput = BossUi.buildInput(this, "编辑当前对话附加提示词", true);
|
||||
projectPromptInput.setMinLines(8);
|
||||
projectPromptInput.setText(projectPromptOverrideText == null ? "" : projectPromptOverrideText);
|
||||
appendContent(BossUi.buildFormCell(
|
||||
this,
|
||||
"当前对话提示词",
|
||||
"只对当前 master-agent 会话生效。",
|
||||
projectPromptInput
|
||||
));
|
||||
|
||||
appendContent(BossUi.buildSoftPanel(
|
||||
this,
|
||||
"合成预览",
|
||||
buildPreviewText(),
|
||||
"保存后会立即影响主 Agent 回复。"
|
||||
));
|
||||
|
||||
contentLoaded = true;
|
||||
updateSaveAvailability();
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private String buildPreviewText() {
|
||||
StringBuilder builder = new StringBuilder();
|
||||
if (!TextUtils.isEmpty(adminPromptText)) {
|
||||
builder.append("【管理员全局主提示词】\n").append(adminPromptText).append("\n\n");
|
||||
}
|
||||
String userText = userPromptInput == null ? userPromptText : userPromptInput.getText().toString();
|
||||
if (!TextUtils.isEmpty(userText)) {
|
||||
builder.append("【用户私有主提示词】\n").append(userText).append("\n\n");
|
||||
}
|
||||
String projectText = projectPromptInput == null ? projectPromptOverrideText : projectPromptInput.getText().toString();
|
||||
if (!TextUtils.isEmpty(projectText)) {
|
||||
builder.append("【当前对话提示词】\n").append(projectText).append("\n\n");
|
||||
}
|
||||
if (builder.length() == 0) {
|
||||
return "当前没有任何提示词内容。";
|
||||
}
|
||||
return builder.toString().trim();
|
||||
}
|
||||
|
||||
private void savePromptProfile() {
|
||||
if (!contentLoaded) {
|
||||
showMessage("提示词尚未加载完成,请先刷新成功后再保存。");
|
||||
return;
|
||||
}
|
||||
final String userContent = userPromptInput == null ? "" : userPromptInput.getText().toString();
|
||||
final String promptOverride = projectPromptInput == null ? "" : projectPromptInput.getText().toString();
|
||||
setRefreshing(true);
|
||||
executor.execute(() -> {
|
||||
try {
|
||||
JSONObject payload = new JSONObject();
|
||||
payload.put("userPromptContent", userContent);
|
||||
payload.put("promptOverride", promptOverride);
|
||||
BossApiClient.ApiResponse response = apiClient.updateMasterAgentPromptProfile(projectId, payload);
|
||||
if (!response.ok()) {
|
||||
throw new IllegalStateException(response.message());
|
||||
}
|
||||
runOnUiThread(() -> {
|
||||
showMessage("提示词已保存");
|
||||
setResult(RESULT_OK);
|
||||
finish();
|
||||
});
|
||||
} catch (Exception error) {
|
||||
runOnUiThread(() -> {
|
||||
setRefreshing(false);
|
||||
showMessage("提示词保存失败:" + error.getMessage());
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private void updateSaveAvailability() {
|
||||
if (headerActionButton != null) {
|
||||
headerActionButton.setEnabled(contentLoaded);
|
||||
headerActionButton.setAlpha(contentLoaded ? 1f : 0.45f);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -74,6 +74,8 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
private @Nullable JSONObject currentRejectedDispatchPlan;
|
||||
private ProjectChatUiState.SelectionState selectionState = ProjectChatUiState.emptySelection();
|
||||
private ActivityResultLauncher<Intent> conversationInfoLauncher;
|
||||
private ActivityResultLauncher<Intent> masterAgentPromptLauncher;
|
||||
private ActivityResultLauncher<Intent> masterAgentMemoryLauncher;
|
||||
private ActivityResultLauncher<Intent> forwardTargetLauncher;
|
||||
private ActivityResultLauncher<String> imagePickerLauncher;
|
||||
private ActivityResultLauncher<String> videoPickerLauncher;
|
||||
@@ -164,6 +166,22 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
reload();
|
||||
}
|
||||
);
|
||||
masterAgentPromptLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
reload(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
masterAgentMemoryLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
if (result.getResultCode() == RESULT_OK) {
|
||||
reload(true);
|
||||
}
|
||||
}
|
||||
);
|
||||
forwardTargetLauncher = registerForActivityResult(
|
||||
new ActivityResultContracts.StartActivityForResult(),
|
||||
result -> {
|
||||
@@ -627,12 +645,34 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
conversationInfoLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void openMasterAgentPromptProfile() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, MasterAgentPromptActivity.class);
|
||||
intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_ID, projectId);
|
||||
intent.putExtra(MasterAgentPromptActivity.EXTRA_PROJECT_NAME, initialProjectName);
|
||||
masterAgentPromptLauncher.launch(intent);
|
||||
}
|
||||
|
||||
private void openMasterAgentMemories() {
|
||||
if (projectId == null || projectId.isEmpty()) {
|
||||
showMessage("缺少 projectId");
|
||||
return;
|
||||
}
|
||||
Intent intent = new Intent(this, MasterAgentMemoryActivity.class);
|
||||
intent.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID, projectId);
|
||||
intent.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME, initialProjectName);
|
||||
masterAgentMemoryLauncher.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();
|
||||
@@ -641,9 +681,15 @@ public class ProjectDetailActivity extends BossScreenActivity {
|
||||
showMasterAgentReasoningPicker();
|
||||
break;
|
||||
case 2:
|
||||
openConversationInfo();
|
||||
openMasterAgentPromptProfile();
|
||||
break;
|
||||
case 3:
|
||||
openMasterAgentMemories();
|
||||
break;
|
||||
case 4:
|
||||
openConversationInfo();
|
||||
break;
|
||||
case 5:
|
||||
reload(true);
|
||||
break;
|
||||
default:
|
||||
|
||||
@@ -109,6 +109,85 @@ public class BossApiClientDispatchPlansTest {
|
||||
assertEquals("{\"modelOverride\":\"gpt-5.4\",\"reasoningEffortOverride\":\"high\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateProjectAgentControlsWritesPromptOverrideWhenProvided() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/agent-controls"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.updateProjectAgentControls("master-agent", "gpt-5.4", "high", "当前对话提示词");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/agent-controls", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"modelOverride\":\"gpt-5.4\",\"reasoningEffortOverride\":\"high\",\"promptOverride\":\"当前对话提示词\"}",
|
||||
connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMasterAgentPromptProfileUsesScopedEndpoint() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/prompt-profile"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.getMasterAgentPromptProfile("master-agent");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/prompt-profile", apiClient.lastPath);
|
||||
assertEquals("GET", connection.requestMethodValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void updateMasterAgentPromptProfileWritesUserPromptAndOverride() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/prompt-profile"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("userPromptContent", "用户私有主提示词")
|
||||
.put("promptOverride", "当前对话提示词");
|
||||
BossApiClient.ApiResponse response = apiClient.updateMasterAgentPromptProfile("master-agent", payload);
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/prompt-profile", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals("{\"userPromptContent\":\"用户私有主提示词\",\"promptOverride\":\"当前对话提示词\"}", connection.requestBody());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void getMasterAgentMemoriesUsesScopedEndpoint() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
BossApiClient.ApiResponse response = apiClient.getMasterAgentMemories("master-agent");
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/memories", apiClient.lastPath);
|
||||
assertEquals("GET", connection.requestMethodValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void createMasterAgentMemoryWritesStructuredPayload() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"));
|
||||
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
|
||||
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("scope", "project")
|
||||
.put("projectId", "boss-console")
|
||||
.put("title", "项目目标")
|
||||
.put("content", "把会话页收成微信式列表")
|
||||
.put("memoryType", "project_progress")
|
||||
.put("tags", new JSONArray().put("ui").put("progress"));
|
||||
BossApiClient.ApiResponse response = apiClient.createMasterAgentMemory("master-agent", payload);
|
||||
|
||||
assertEquals(200, response.statusCode);
|
||||
assertEquals("/api/v1/projects/master-agent/memories", apiClient.lastPath);
|
||||
assertEquals("POST", connection.requestMethodValue);
|
||||
assertEquals(
|
||||
"{\"scope\":\"project\",\"projectId\":\"boss-console\",\"title\":\"项目目标\",\"content\":\"把会话页收成微信式列表\",\"memoryType\":\"project_progress\",\"tags\":[\"ui\",\"progress\"]}",
|
||||
connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void sendProjectMessageUsesQueueFriendlyReadTimeoutForMasterAgent() throws Exception {
|
||||
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/master-agent/messages"));
|
||||
|
||||
@@ -0,0 +1,419 @@
|
||||
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.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class MasterAgentMemoryActivityTest {
|
||||
@Test
|
||||
public void renderMemoriesShowsGlobalAndProjectSections() throws Exception {
|
||||
TestMasterAgentMemoryActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentMemoryActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
|
||||
JSONObject globalMemory = new JSONObject()
|
||||
.put("memoryId", "mem-global")
|
||||
.put("scope", "global")
|
||||
.put("title", "偏好")
|
||||
.put("content", "优先中文回复")
|
||||
.put("memoryType", "user_preference")
|
||||
.put("tags", new JSONArray().put("ui"));
|
||||
JSONObject projectMemory = new JSONObject()
|
||||
.put("memoryId", "mem-project")
|
||||
.put("scope", "project")
|
||||
.put("projectId", "master-agent")
|
||||
.put("title", "项目进度")
|
||||
.put("content", "主 Agent 对话链已接通")
|
||||
.put("memoryType", "project_progress")
|
||||
.put("tags", new JSONArray().put("progress"));
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("memories", new JSONObject()
|
||||
.put("global", new JSONObject().put("items", new JSONArray().put(globalMemory)))
|
||||
.put("project", new JSONObject().put("items", new JSONArray().put(projectMemory))));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderMemories",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "我的通用记忆"));
|
||||
assertTrue(viewTreeContainsText(content, "当前项目记忆"));
|
||||
assertTrue(viewTreeContainsText(content, "优先中文回复"));
|
||||
JSONObject memories = payload.getJSONObject("memories");
|
||||
JSONArray globalMemoryItems = (JSONArray) ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"extractMemoryItems",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, memories),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "global")
|
||||
);
|
||||
JSONArray projectMemoryItems = (JSONArray) ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"extractMemoryItems",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, memories),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "project")
|
||||
);
|
||||
assertEquals(1, globalMemoryItems.length());
|
||||
assertEquals(1, projectMemoryItems.length());
|
||||
assertEquals("偏好", globalMemoryItems.getJSONObject(0).getString("title"));
|
||||
assertEquals("项目进度", projectMemoryItems.getJSONObject(0).getString("title"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void saveMemoryWritesStructuredCreatePayload() throws Exception {
|
||||
TestMasterAgentMemoryActivity activity = Robolectric
|
||||
.buildActivity(
|
||||
TestMasterAgentMemoryActivity.class,
|
||||
new Intent()
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_ID, "master-agent")
|
||||
.putExtra(MasterAgentMemoryActivity.EXTRA_PROJECT_NAME, "主 Agent")
|
||||
)
|
||||
.setup()
|
||||
.get();
|
||||
ReflectionHelpers.setField(activity, "contentLoaded", true);
|
||||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/projects/master-agent/memories"),
|
||||
200,
|
||||
"{\"ok\":true}",
|
||||
"{\"ok\":false,\"message\":\"MEMORY_SAVE_FAILED\"}"
|
||||
)
|
||||
));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
ReflectionHelpers.setField(activity, "projectId", "master-agent");
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"saveMemory",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, null),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "project"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "项目目标"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "把会话页收成微信式列表"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "project_progress"),
|
||||
ReflectionHelpers.ClassParameter.from(String.class, "ui,progress")
|
||||
);
|
||||
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals(
|
||||
"{\"scope\":\"project\",\"projectId\":\"master-agent\",\"title\":\"项目目标\",\"content\":\"把会话页收成微信式列表\",\"memoryType\":\"project_progress\",\"tags\":[\"ui\",\"progress\"]}",
|
||||
((ScriptedBossApiClient) ReflectionHelpers.getField(activity, "apiClient")).connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static final class TestMasterAgentMemoryActivity extends MasterAgentMemoryActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render synthetic payloads directly.
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DirectExecutorService extends AbstractExecutorService {
|
||||
private boolean shutdown;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
shutdown = true;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
command.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||||
private final Map<String, RecordingConnection> connections;
|
||||
private final RecordingConnection connection;
|
||||
|
||||
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return new HashMap<>(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Set<String> getStringSet(String key, java.util.Set<String> defValues) {
|
||||
Object value = values.get(key);
|
||||
if (!(value instanceof java.util.Set)) {
|
||||
return defValues;
|
||||
}
|
||||
// noinspection unchecked
|
||||
return (java.util.Set<String>) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Integer ? (Integer) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Long ? (Long) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Float ? (Float) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override public Editor putString(String key, String value) { values.put(key, value); return this; }
|
||||
@Override public Editor putStringSet(String key, java.util.Set<String> value) { values.put(key, value); return this; }
|
||||
@Override public Editor putInt(String key, int value) { values.put(key, value); return this; }
|
||||
@Override public Editor putLong(String key, long value) { values.put(key, value); return this; }
|
||||
@Override public Editor putFloat(String key, float value) { values.put(key, value); return this; }
|
||||
@Override public Editor putBoolean(String key, boolean value) { values.put(key, value); return this; }
|
||||
@Override public Editor remove(String key) { values.remove(key); return this; }
|
||||
@Override public Editor clear() { values.clear(); return this; }
|
||||
@Override public boolean commit() { return true; }
|
||||
@Override public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
@Override public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
|
||||
ScriptedBossApiClient(RecordingConnection connection) {
|
||||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||||
this.connection = connection;
|
||||
this.connections = new HashMap<>();
|
||||
this.connections.put(connection.getURL().getPath(), connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) {
|
||||
RecordingConnection scripted = connections.get(path);
|
||||
if (scripted == null) {
|
||||
throw new IllegalStateException("Missing scripted connection for " + path);
|
||||
}
|
||||
return scripted;
|
||||
}
|
||||
|
||||
@Override
|
||||
String encode(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
void rememberIdentity(JSONObject json) {
|
||||
// JVM 单测不需要落 Android 侧身份缓存。
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return new HashMap<>(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public java.util.Set<String> getStringSet(String key, java.util.Set<String> defValues) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof java.util.Set ? (java.util.Set<String>) value : defValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Integer ? (Integer) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Long ? (Long) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Float ? (Float) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override
|
||||
public Editor putString(String key, String value) { values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putStringSet(String key, java.util.Set<String> values) { InMemorySharedPreferences.this.values.put(key, values); return this; }
|
||||
@Override
|
||||
public Editor putInt(String key, int value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putLong(String key, long value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putFloat(String key, float value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putBoolean(String key, boolean value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor remove(String key) { InMemorySharedPreferences.this.values.remove(key); return this; }
|
||||
@Override
|
||||
public Editor clear() { InMemorySharedPreferences.this.values.clear(); return this; }
|
||||
@Override
|
||||
public boolean commit() { return true; }
|
||||
@Override
|
||||
public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||||
private final int responseCodeValue;
|
||||
private final String responseBody;
|
||||
private final String errorBody;
|
||||
private String requestMethodValue = "GET";
|
||||
|
||||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||||
super(url);
|
||||
this.responseCodeValue = responseCodeValue;
|
||||
this.responseBody = responseBody;
|
||||
this.errorBody = errorBody;
|
||||
}
|
||||
|
||||
@Override public void disconnect() {}
|
||||
@Override public boolean usingProxy() { return false; }
|
||||
@Override public void connect() {}
|
||||
@Override public void setRequestMethod(String method) throws ProtocolException { requestMethodValue = method; }
|
||||
@Override public void setRequestProperty(String key, String value) { requestHeaders.put(key, value); }
|
||||
@Override public OutputStream getOutputStream() { return requestBody; }
|
||||
@Override public int getResponseCode() { return responseCodeValue; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); }
|
||||
@Override public InputStream getErrorStream() {
|
||||
if (responseCodeValue < 400) {
|
||||
return null;
|
||||
}
|
||||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String requestBody() { return requestBody.toString(StandardCharsets.UTF_8); }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
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.EditText;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.json.JSONObject;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.robolectric.Robolectric;
|
||||
import org.robolectric.RobolectricTestRunner;
|
||||
import org.robolectric.annotation.Config;
|
||||
import org.robolectric.util.ReflectionHelpers;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.ProtocolException;
|
||||
import java.net.URL;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.AbstractExecutorService;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
@RunWith(RobolectricTestRunner.class)
|
||||
@Config(sdk = 34)
|
||||
public class MasterAgentPromptActivityTest {
|
||||
@Test
|
||||
public void renderPromptProfileShowsAdminUserAndConversationLayers() 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", "当前对话提示词"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderPromptProfile",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
View content = activity.findViewById(R.id.screen_content);
|
||||
assertTrue(viewTreeContainsText(content, "管理员全局主提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "全局主提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "用户私有主提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "当前对话提示词"));
|
||||
assertTrue(viewTreeContainsText(content, "合成预览"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void savePromptProfileWritesBothEditableLayers() 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();
|
||||
ReflectionHelpers.setField(activity, "contentLoaded", true);
|
||||
ReflectionHelpers.setField(activity, "apiClient", new ScriptedBossApiClient(
|
||||
new RecordingConnection(
|
||||
new URL("https://boss.hyzq.net/api/v1/projects/master-agent/prompt-profile"),
|
||||
200,
|
||||
"{\"ok\":true,\"promptPolicy\":{\"globalPrompt\":\"全局主提示词\"},\"userPrompt\":{\"content\":\"用户私有主提示词\"},\"projectControls\":{\"promptOverride\":\"当前对话提示词\"}}",
|
||||
"{\"ok\":false,\"message\":\"PROMPT_SAVE_FAILED\"}"
|
||||
)
|
||||
));
|
||||
ReflectionHelpers.setField(activity, "executor", new DirectExecutorService());
|
||||
JSONObject payload = new JSONObject()
|
||||
.put("promptPolicy", new JSONObject().put("globalPrompt", "全局主提示词"))
|
||||
.put("userPrompt", new JSONObject().put("content", "用户私有主提示词"))
|
||||
.put("projectControls", new JSONObject().put("promptOverride", "当前对话提示词"));
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(
|
||||
activity,
|
||||
"renderPromptProfile",
|
||||
ReflectionHelpers.ClassParameter.from(JSONObject.class, payload)
|
||||
);
|
||||
|
||||
EditText userInput = ReflectionHelpers.getField(activity, "userPromptInput");
|
||||
EditText conversationInput = ReflectionHelpers.getField(activity, "projectPromptInput");
|
||||
userInput.setText("更新后的用户提示词");
|
||||
conversationInput.setText("更新后的对话提示词");
|
||||
|
||||
ReflectionHelpers.callInstanceMethod(activity, "savePromptProfile");
|
||||
org.robolectric.Shadows.shadowOf(android.os.Looper.getMainLooper()).idle();
|
||||
|
||||
assertEquals(
|
||||
"{\"userPromptContent\":\"更新后的用户提示词\",\"promptOverride\":\"更新后的对话提示词\"}",
|
||||
((ScriptedBossApiClient) ReflectionHelpers.getField(activity, "apiClient")).connection.requestBody()
|
||||
);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
private static final class TestMasterAgentPromptActivity extends MasterAgentPromptActivity {
|
||||
@Override
|
||||
protected void reload() {
|
||||
// Tests render synthetic payloads directly.
|
||||
}
|
||||
}
|
||||
|
||||
private static final class DirectExecutorService extends AbstractExecutorService {
|
||||
private boolean shutdown;
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
shutdown = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<Runnable> shutdownNow() {
|
||||
shutdown = true;
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isShutdown() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isTerminated() {
|
||||
return shutdown;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean awaitTermination(long timeout, TimeUnit unit) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void execute(Runnable command) {
|
||||
command.run();
|
||||
}
|
||||
}
|
||||
|
||||
private static final class ScriptedBossApiClient extends BossApiClient {
|
||||
private final Map<String, RecordingConnection> connections;
|
||||
private final RecordingConnection connection;
|
||||
|
||||
ScriptedBossApiClient(RecordingConnection connection) {
|
||||
super(new InMemorySharedPreferences(), "https://boss.hyzq.net");
|
||||
this.connection = connection;
|
||||
this.connections = new HashMap<>();
|
||||
this.connections.put(connection.getURL().getPath(), connection);
|
||||
}
|
||||
|
||||
@Override
|
||||
HttpURLConnection openConnection(String path) {
|
||||
RecordingConnection scripted = connections.get(path);
|
||||
if (scripted == null) {
|
||||
throw new IllegalStateException("Missing scripted connection for " + path);
|
||||
}
|
||||
return scripted;
|
||||
}
|
||||
|
||||
@Override
|
||||
String encode(String value) {
|
||||
return value;
|
||||
}
|
||||
|
||||
@Override
|
||||
void rememberIdentity(JSONObject json) {
|
||||
// JVM 单测不需要落 Android 侧身份缓存。
|
||||
}
|
||||
}
|
||||
|
||||
private static final class InMemorySharedPreferences implements android.content.SharedPreferences {
|
||||
private final Map<String, Object> values = new HashMap<>();
|
||||
|
||||
@Override
|
||||
public Map<String, ?> getAll() {
|
||||
return new HashMap<>(values);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getString(String key, String defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof String ? (String) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getStringSet(String key, Set<String> defValues) {
|
||||
Object value = values.get(key);
|
||||
if (!(value instanceof Set)) {
|
||||
return defValues;
|
||||
}
|
||||
// noinspection unchecked
|
||||
return (Set<String>) value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getInt(String key, int defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Integer ? (Integer) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getLong(String key, long defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Long ? (Long) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getFloat(String key, float defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Float ? (Float) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean getBoolean(String key, boolean defValue) {
|
||||
Object value = values.get(key);
|
||||
return value instanceof Boolean ? (Boolean) value : defValue;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean contains(String key) {
|
||||
return values.containsKey(key);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Editor edit() {
|
||||
return new Editor() {
|
||||
@Override
|
||||
public Editor putString(String key, String value) { values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putStringSet(String key, java.util.Set<String> values) { InMemorySharedPreferences.this.values.put(key, values); return this; }
|
||||
@Override
|
||||
public Editor putInt(String key, int value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putLong(String key, long value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putFloat(String key, float value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor putBoolean(String key, boolean value) { InMemorySharedPreferences.this.values.put(key, value); return this; }
|
||||
@Override
|
||||
public Editor remove(String key) { InMemorySharedPreferences.this.values.remove(key); return this; }
|
||||
@Override
|
||||
public Editor clear() { InMemorySharedPreferences.this.values.clear(); return this; }
|
||||
@Override
|
||||
public boolean commit() { return true; }
|
||||
@Override
|
||||
public void apply() {}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
|
||||
@Override
|
||||
public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {}
|
||||
}
|
||||
|
||||
private static final class RecordingConnection extends HttpURLConnection {
|
||||
private final ByteArrayOutputStream requestBody = new ByteArrayOutputStream();
|
||||
private final Map<String, String> requestHeaders = new HashMap<>();
|
||||
private final int responseCodeValue;
|
||||
private final String responseBody;
|
||||
private final String errorBody;
|
||||
private String requestMethodValue = "GET";
|
||||
|
||||
RecordingConnection(URL url, int responseCodeValue, String responseBody, String errorBody) {
|
||||
super(url);
|
||||
this.responseCodeValue = responseCodeValue;
|
||||
this.responseBody = responseBody;
|
||||
this.errorBody = errorBody;
|
||||
}
|
||||
|
||||
@Override public void disconnect() {}
|
||||
@Override public boolean usingProxy() { return false; }
|
||||
@Override public void connect() {}
|
||||
@Override public void setRequestMethod(String method) throws ProtocolException { requestMethodValue = method; }
|
||||
@Override public void setRequestProperty(String key, String value) { requestHeaders.put(key, value); }
|
||||
@Override public OutputStream getOutputStream() { return requestBody; }
|
||||
@Override public int getResponseCode() { return responseCodeValue; }
|
||||
@Override public InputStream getInputStream() { return new ByteArrayInputStream(responseBody.getBytes(StandardCharsets.UTF_8)); }
|
||||
@Override public InputStream getErrorStream() {
|
||||
if (responseCodeValue < 400) {
|
||||
return null;
|
||||
}
|
||||
return new ByteArrayInputStream(errorBody.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
String requestBody() { return requestBody.toString(StandardCharsets.UTF_8); }
|
||||
}
|
||||
}
|
||||
@@ -44,8 +44,10 @@ public class ProjectDetailActivityMasterAgentMenuTest {
|
||||
|
||||
assertMenuItem(listView, 0, "模型");
|
||||
assertMenuItem(listView, 1, "推理强度");
|
||||
assertMenuItem(listView, 2, "会话信息");
|
||||
assertMenuItem(listView, 3, "刷新");
|
||||
assertMenuItem(listView, 2, "提示词");
|
||||
assertMenuItem(listView, 3, "记忆");
|
||||
assertMenuItem(listView, 4, "会话信息");
|
||||
assertMenuItem(listView, 5, "刷新");
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
Reference in New Issue
Block a user