feat: harden agent onboarding and device import flows

This commit is contained in:
kris
2026-03-31 05:18:58 +08:00
parent 4aed93e90c
commit f417fe1955
18 changed files with 975 additions and 132 deletions

View File

@@ -78,28 +78,61 @@ public class AiAccountsActivity extends BossScreenActivity {
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
if (activeIdentity == null) {
return BossUi.buildWechatMenuRow(
LinearLayout empty = new LinearLayout(this);
empty.setOrientation(LinearLayout.VERTICAL);
empty.addView(BossUi.buildWechatMenuRow(
this,
"当前主控身份",
"当前没有可用账号。",
"请先新增或启用一个账号。",
null,
null
);
));
return empty;
}
String subtitle = activeIdentity.optString("label", "AI 账号")
+ " · " + activeIdentity.optString("displayName", "-");
String meta = activeIdentity.optString("roleLabel", "-")
+ " · " + activeIdentity.optString("providerLabel", "-")
+ " · " + activeIdentity.optString("statusLabel", "-");
return BossUi.buildWechatMenuRow(
String note = activeIdentity.optString("note", "");
String activeAccountId = activeIdentity.optString("accountId", "");
boolean canGenerate = activeIdentity.optBoolean("canGenerate", false);
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildWechatMenuRow(
this,
"当前主控身份",
subtitle,
meta,
null,
activeIdentity.optBoolean("isEnvironmentFallback") ? "环境" : "当前",
null
);
));
if (!note.isEmpty()) {
card.addView(BossUi.buildWechatMenuRow(
this,
"主控状态",
note,
activeIdentity.optString("switchReason", ""),
null,
null
));
}
if (!activeAccountId.isEmpty()) {
Button validate = BossUi.buildMiniActionButton(this, "校验主控", false);
validate.setOnClickListener(v -> validateAccount(activeAccountId));
Button testMasterAgent = BossUi.buildMiniActionButton(this, "测试主 Agent 对话", canGenerate);
testMasterAgent.setEnabled(canGenerate);
testMasterAgent.setOnClickListener(v -> openMasterAgentConversation());
card.addView(BossUi.buildInlineActionRow(this, validate, testMasterAgent));
}
return card;
}
private LinearLayout buildAccountsSection(@Nullable JSONArray accounts) {
@@ -564,10 +597,18 @@ public class AiAccountsActivity extends BossScreenActivity {
}
private void validateAccount(JSONObject account) {
validateAccount(account.optString("accountId"));
}
private void validateAccount(String accountId) {
if (accountId == null || accountId.trim().isEmpty()) {
showMessage("当前账号没有可用的账号 ID暂时无法校验。");
return;
}
setRefreshing(true);
executor.execute(() -> {
try {
BossApiClient.ApiResponse response = apiClient.validateAccount(account.optString("accountId"));
BossApiClient.ApiResponse response = apiClient.validateAccount(accountId.trim());
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
showMessage(response.message());
@@ -582,6 +623,13 @@ public class AiAccountsActivity extends BossScreenActivity {
});
}
private void openMasterAgentConversation() {
Intent intent = new Intent(this, ProjectDetailActivity.class);
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent");
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
startActivity(intent);
}
private void confirmDeleteAccount(JSONObject account) {
new AlertDialog.Builder(this)
.setTitle("删除 AI 账号")

View File

@@ -81,7 +81,7 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
runOnUiThread(() -> {
JSONObject enrollment = response.json.optJSONObject("enrollment");
JSONObject device = response.json.optJSONObject("device");
android.widget.Button importButton = BossUi.buildSecondaryButton(this, "继续导入项目");
android.widget.Button importButton = BossUi.buildSecondaryButton(this, "继续导入线程");
importButton.setOnClickListener(v -> openImportDraft(device));
replaceContent(
BossUi.buildSoftPanel(
@@ -90,8 +90,9 @@ public class DeviceEnrollmentActivity extends BossScreenActivity {
"设备 " + (device == null ? "-" : device.optString("name", "-"))
+ "\npairingCode " + (enrollment == null ? "-" : enrollment.optString("pairingCode", "-"))
+ "\ntoken " + (enrollment == null ? "-" : enrollment.optString("token", "-")),
enrollment == null ? "ready" : enrollment.optString("status", "ready")
+ " · 到期 " + enrollment.optString("expiresAt", "-")
(enrollment == null ? "ready" : enrollment.optString("status", "ready"))
+ " · 到期 " + (enrollment == null ? "-" : enrollment.optString("expiresAt", "-"))
+ "\n下一步打开导入草稿勾选线程后生成导入建议。"
),
importButton
);

View File

@@ -9,8 +9,10 @@ import androidx.annotation.Nullable;
import org.json.JSONArray;
import org.json.JSONObject;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
public class DeviceImportDraftActivity extends BossScreenActivity {
@@ -79,10 +81,10 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
appendContent(BossUi.buildSoftPanel(
this,
"导入 Codex 项目",
(deviceName == null ? "当前设备" : deviceName) + "\n勾选要暴露到会话首页的项目和线程",
(deviceName == null ? "当前设备" : deviceName) + "\n勾选线程,再生成导入建议,最后应用导入",
draft == null
? "等待设备完成首次 heartbeat"
: "候选 " + (draft.optJSONArray("candidates") == null ? 0 : draft.optJSONArray("candidates").length()) + " · 状态 " + draft.optString("status", "-")
: "状态 " + resolveStatusTitle(draft)
));
if (draft == null) {
@@ -98,6 +100,22 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
return;
}
int recommendedCount = 0;
for (int i = 0; i < candidates.length(); i++) {
JSONObject candidate = candidates.optJSONObject(i);
if (candidate != null && candidate.optBoolean("suggestedImport", false)) {
recommendedCount += 1;
}
}
appendContent(BossUi.buildCard(
this,
resolveStatusTitle(draft),
resolveStatusBody(draft, resolution),
"候选 " + candidates.length()
+ " · 已选 " + selectedCandidateIds.size()
+ " · 推荐 " + recommendedCount
));
Map<String, JSONArray> grouped = new LinkedHashMap<>();
for (int i = 0; i < candidates.length(); i++) {
JSONObject candidate = candidates.optJSONObject(i);
@@ -133,7 +151,9 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
candidate.optString("threadDisplayName", "未命名线程"),
"最近活跃:" + candidate.optString("lastActiveAt", "-"),
null,
selectedState ? "已选" : (candidate.optBoolean("suggestedImport", false) ? "推荐" : null),
selectedState
? (candidate.optBoolean("suggestedImport", false) ? "已选 · 推荐导入" : "已选")
: (candidate.optBoolean("suggestedImport", false) ? "推荐导入" : null),
v -> toggleSelection(candidateId)
));
}
@@ -163,6 +183,16 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
}
}
JSONArray appliedProjectNames = draft.optJSONArray("appliedProjectNames");
if (appliedProjectNames != null && appliedProjectNames.length() > 0) {
appendContent(BossUi.buildCard(
this,
"应用结果",
"已导入 " + appliedProjectNames.length() + " 个线程:" + joinNames(appliedProjectNames) + "",
"这些线程现在会出现在会话首页。"
));
}
Button reviewButton = BossUi.buildMiniActionButton(this, "生成导入建议", true);
reviewButton.setEnabled(!selectedCandidateIds.isEmpty());
reviewButton.setOnClickListener(v -> reviewSelection());
@@ -177,6 +207,67 @@ public class DeviceImportDraftActivity extends BossScreenActivity {
setRefreshing(false);
}
private String resolveStatusTitle(@Nullable JSONObject draft) {
if (draft == null) {
return "等待导入草稿";
}
String status = draft.optString("status", "");
if ("pending_candidates".equals(status)) {
return "等待候选线程";
}
if ("pending_selection".equals(status)) {
return "等待勾选";
}
if ("pending_resolution".equals(status)) {
return "建议生成中";
}
if ("resolved".equals(status)) {
return "建议已生成";
}
if ("applied".equals(status)) {
return "已导入";
}
return "导入草稿";
}
private String resolveStatusBody(@Nullable JSONObject draft, @Nullable JSONObject resolution) {
if (draft == null) {
return "先让设备完成首次 heartbeat 并上报候选线程,导入草稿就会出现在这里。";
}
String status = draft.optString("status", "");
if ("pending_candidates".equals(status)) {
return "设备已经就绪,等 heartbeat 带回线程候选后,就可以开始勾选。";
}
if ("pending_selection".equals(status)) {
return "先勾选想导入的线程,再生成导入建议。";
}
if ("pending_resolution".equals(status)) {
return "勾选已保存,接下来会生成导入建议。";
}
if ("resolved".equals(status)) {
return resolution == null ? "可以先看建议,再点应用导入。" : resolution.optString("summary", "可以先看建议,再点应用导入。");
}
if ("applied".equals(status)) {
JSONArray appliedProjectNames = draft.optJSONArray("appliedProjectNames");
if (appliedProjectNames != null && appliedProjectNames.length() > 0) {
return "已导入 " + appliedProjectNames.length() + " 个线程:" + joinNames(appliedProjectNames) + "";
}
return "导入已完成,线程已经落到会话首页。";
}
return "先勾选线程,再生成导入建议,最后应用导入。";
}
private String joinNames(JSONArray values) {
List<String> names = new ArrayList<>();
for (int i = 0; i < values.length(); i++) {
String value = values.optString(i, "");
if (!value.isEmpty()) {
names.add(value);
}
}
return String.join("", names);
}
private void toggleSelection(String candidateId) {
if (candidateId == null || candidateId.isEmpty()) {
return;

View File

@@ -180,7 +180,8 @@ public class OpenAiOnboardingActivity extends BossScreenActivity {
if (!response.ok()) throw new IllegalStateException(response.message());
runOnUiThread(() -> {
setResult(RESULT_OK);
showPostLoginActions();
setRefreshing(false);
showPostLoginActions(response.json);
});
} catch (Exception error) {
runOnUiThread(() -> {
@@ -194,12 +195,34 @@ public class OpenAiOnboardingActivity extends BossScreenActivity {
});
}
private void showPostLoginActions() {
private void showPostLoginActions(JSONObject responseJson) {
JSONObject activeIdentity = responseJson == null ? null : responseJson.optJSONObject("activeIdentity");
StringBuilder message = new StringBuilder();
if (activeIdentity != null) {
String statusLabel = activeIdentity.optString("statusLabel", "");
String note = activeIdentity.optString("note", "");
message.append("当前主控:")
.append(activeIdentity.optString("label", "OpenAI 平台账号"))
.append(" · ")
.append(activeIdentity.optString("displayName", ""))
.append('\n')
.append("状态:")
.append(statusLabel.isEmpty() ? "可用" : statusLabel);
if (!note.isEmpty()) {
message.append('\n').append(note);
}
} else {
message.append("OpenAI 平台账号已登录,并设为当前主控。");
}
new AlertDialog.Builder(this)
.setTitle("OpenAI 平台账号已登录")
.setMessage("已经设为当前主控。现在可以直接测试主 Agent 对话。")
.setPositiveButton("测试主 Agent 对话", (dialog, which) -> openMasterAgentConversation())
.setNegativeButton("稍后再说", (dialog, which) -> finish())
.setMessage(message.toString() + "\n\n你现在可以直接测试主 Agent 对话,确认当前主控链路是否可用")
.setPositiveButton("测试主 Agent 对话", (dialog, which) -> {
openMasterAgentConversation();
finish();
})
.setNegativeButton("返回账号页", (dialog, which) -> finish())
.setOnDismissListener(dialog -> {
if (!isFinishing()) {
finish();
@@ -213,6 +236,5 @@ public class OpenAiOnboardingActivity extends BossScreenActivity {
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent");
intent.putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent");
startActivity(intent);
finish();
}
}