android: simplify wechat device and me surfaces

This commit is contained in:
kris
2026-03-27 13:20:46 +08:00
parent 05e26afbf1
commit ff56617fdb
9 changed files with 155 additions and 200 deletions

View File

@@ -10,8 +10,6 @@ import android.os.Build;
import android.os.Bundle;
import android.os.Environment;
import android.provider.Settings;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
@@ -40,7 +38,7 @@ public class AboutActivity extends BossScreenActivity {
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("关于 / OTA", "原生版本中心");
configureScreen("关于", "版本与更新");
IntentFilter filter = new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE);
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(otaDownloadReceiver, filter, Context.RECEIVER_NOT_EXPORTED);
@@ -89,51 +87,58 @@ public class AboutActivity extends BossScreenActivity {
replaceContent();
otaPayload = ota;
if (user != null) {
appendContent(BossUi.buildCard(
appendContent(BossUi.buildListRow(
this,
"当前版本",
user.optString("version", "-")
+ "\n当前账号" + user.optString("account", "-")
+ "\n绑定 Codex" + user.optString("boundCodexNodeLabel", "未绑定"),
session == null ? "-" : "会话到期 " + session.optString("expiresAt", "-")
+ " · 账号 " + user.optString("account", "-"),
"绑定 Codex " + user.optString("boundCodexNodeLabel", "未绑定")
+ (session == null ? "" : " · 会话到期 " + session.optString("expiresAt", "-")),
null,
null
));
}
JSONObject availableRelease = ota.optJSONObject("availableRelease");
String otaBody = availableRelease == null
String otaSubtitle = availableRelease == null
? "当前已经是最新版本。"
: availableRelease.optString("version", "未知版本")
+ "\n" + availableRelease.optString("summary", "暂无摘要")
+ "\n文件" + availableRelease.optString("packageFileName", "-");
appendContent(BossUi.buildCard(
: "可用版本 " + availableRelease.optString("version", "未知版本");
String otaMeta = availableRelease == null
? "当前版本 " + ota.optString("currentVersion", "-")
: availableRelease.optString("summary", "暂无摘要")
+ " · 文件 " + availableRelease.optString("packageFileName", "-");
appendContent(BossUi.buildListRow(
this,
"OTA 状态",
otaBody,
"当前版本 " + ota.optString("currentVersion", "-")
otaSubtitle,
otaMeta,
availableRelease == null ? null : "NEW",
null
));
LinearLayout actionCard = BossUi.buildCard(this, "OTA 操作", "可在原生页直接检查更新、登记 OTA 并下载 APK。", "当前接口:/api/v1/user/ota");
Button check = BossUi.buildPrimaryButton(this, "检查更新");
check.setOnClickListener(v -> performOtaAction("check"));
actionCard.addView(check);
Button apply = BossUi.buildSecondaryButton(this, "登记应用 OTA");
apply.setOnClickListener(v -> performOtaAction("apply"));
actionCard.addView(apply);
Button download = BossUi.buildSecondaryButton(this, "应用内下载 APK");
download.setOnClickListener(v -> downloadLatestApk());
actionCard.addView(download);
appendContent(actionCard);
appendContent(BossUi.buildMenuRow(this, "检查更新", "拉取最新 OTA 状态", null, v -> performOtaAction("check")));
appendContent(BossUi.buildMenuRow(this, "登记应用 OTA", "把当前已应用版本写回服务端", null, v -> performOtaAction("apply")));
appendContent(BossUi.buildMenuRow(this, "应用内下载 APK", "下载最新安装包并拉起系统安装器", null, v -> downloadLatestApk()));
appendContent(BossUi.buildMenuRow(
this,
WechatSurfaceMapper.advancedEntryTitle(),
"进入运维对话、审计与修复入口",
null,
v -> startActivity(new Intent(this, OpsCenterActivity.class))
));
JSONArray logs = ota.optJSONArray("logs");
if (logs != null) {
for (int i = 0; i < logs.length(); i++) {
JSONObject log = logs.optJSONObject(i);
if (log == null) continue;
appendContent(BossUi.buildCard(
appendContent(BossUi.buildListRow(
this,
log.optString("version", "OTA"),
log.optString("summary", ""),
log.optString("status", "-") + " · " + log.optString("createdAt", "-")
log.optString("status", "-") + " · " + log.optString("createdAt", "-"),
null,
null
));
}
}

View File

@@ -1,7 +1,6 @@
package com.hyzq.boss;
import android.os.Bundle;
import android.view.View;
import android.widget.ArrayAdapter;
import android.widget.Button;
import android.widget.LinearLayout;
@@ -20,14 +19,12 @@ public class AiAccountsActivity extends BossScreenActivity {
private static final String[] PROVIDER_VALUES = {"master_codex_node", "openai_api"};
private static final String[] PROVIDER_LABELS = {"Master Codex Node", "OpenAI API"};
private LinearLayout accountList;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
configureScreen("AI 账号", "主 GPT / 备用 GPT / API 容灾");
setHeaderAction("新增", v -> openAccountEditor(null, null));
replaceContent(buildIntroCard(), buildAccountListShell());
replaceContent();
reload();
}
@@ -48,56 +45,46 @@ public class AiAccountsActivity extends BossScreenActivity {
});
}
private LinearLayout buildIntroCard() {
return BossUi.buildCard(
this,
"账号说明",
"当前页面管理 Boss 的主控 AI 账号。主链路优先使用已绑定电脑上的 Master Codex NodeAPI 容灾在同页可补充配置。",
"支持新增、编辑、激活、校验和删除"
);
}
private LinearLayout buildAccountListShell() {
LinearLayout wrapper = new LinearLayout(this);
wrapper.setOrientation(LinearLayout.VERTICAL);
accountList = new LinearLayout(this);
accountList.setOrientation(LinearLayout.VERTICAL);
wrapper.addView(accountList);
return wrapper;
}
private void renderAccounts(JSONObject payload) {
JSONArray accounts = payload.optJSONArray("accounts");
JSONObject activeIdentity = payload.optJSONObject("activeIdentity");
JSONArray switchHistory = payload.optJSONArray("switchHistory");
accountList.removeAllViews();
replaceContent(buildIntroCard(), buildActiveIdentityCard(activeIdentity), buildAccountsSection(accounts), buildSwitchHistoryCard(switchHistory));
replaceContent();
appendContent(BossUi.buildListRow(
this,
"账号管理",
"管理主 GPT、备用 GPT 与 API 容灾。",
"支持新增、编辑、激活、校验和删除。",
null,
null
));
appendContent(buildActiveIdentityCard(activeIdentity));
appendContent(buildAccountsSection(accounts));
setRefreshing(false);
}
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
String body = activeIdentity == null
? "当前没有可用的主控身份。"
: activeIdentity.optString("label", "AI 账号")
+ "\n" + activeIdentity.optString("displayName", "-")
+ "\n" + activeIdentity.optString("providerLabel", "-")
+ (activeIdentity.optString("nodeLabel").isEmpty() ? "" : "\n节点" + activeIdentity.optString("nodeLabel"));
String meta = activeIdentity == null
? "请先配置一个可用账号"
: activeIdentity.optString("roleLabel", "-") + " · " + activeIdentity.optString("statusLabel", "-");
return BossUi.buildCard(this, "当前主控身份", body, meta);
if (activeIdentity == null) {
return BossUi.buildListRow(this, "当前主控身份", "当前没有可用账号。", "请先新增或启用一个账号。", null, null);
}
String subtitle = activeIdentity.optString("label", "AI 账号")
+ " · " + activeIdentity.optString("displayName", "-");
String meta = activeIdentity.optString("roleLabel", "-")
+ " · " + activeIdentity.optString("providerLabel", "-")
+ " · " + activeIdentity.optString("statusLabel", "-");
return BossUi.buildListRow(this, "当前主控身份", subtitle, meta, "当前", null);
}
private LinearLayout buildAccountsSection(@Nullable JSONArray accounts) {
LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL);
section.addView(BossUi.buildCard(
section.addView(BossUi.buildListRow(
this,
"账号列表",
accounts == null || accounts.length() == 0 ? "当前还没有 AI 账号。" : "击卡片可编辑,按钮可激活 / 校验 / 删除。",
"当前 API/api/v1/accounts"
accounts == null || accounts.length() == 0 ? "当前还没有 AI 账号。" : "可编辑,按钮可激活、校验或删除。",
"当前 API/api/v1/accounts",
null,
null
));
if (accounts == null || accounts.length() == 0) {
@@ -118,20 +105,25 @@ public class AiAccountsActivity extends BossScreenActivity {
String meta = account.optString("roleLabel", "-")
+ " · " + account.optString("providerLabel", "-")
+ " · " + statusLabel
+ (account.optBoolean("isActive") ? " · 当前主控" : "")
+ (account.optBoolean("apiKeyConfigured") ? " · 已配置 Key" : "");
String body = account.optString("displayName", "-")
+ "\n账号" + account.optString("accountIdentifier", "-")
+ (account.optString("nodeLabel").isEmpty() ? "" : "\n节点" + account.optString("nodeLabel"))
+ (account.optString("loginStatusNote").isEmpty() ? "" : "\n" + account.optString("loginStatusNote"));
StringBuilder subtitle = new StringBuilder(account.optString("displayName", "-"));
if (!account.optString("accountIdentifier").isEmpty()) {
subtitle.append(" · ").append(account.optString("accountIdentifier", "-"));
}
if (!account.optString("nodeLabel").isEmpty()) {
subtitle.append(" · ").append(account.optString("nodeLabel", "-"));
}
LinearLayout card = BossUi.buildCard(
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildListRow(
this,
account.optString("label", "未命名账号"),
body,
subtitle.toString(),
meta,
account.optBoolean("isActive") ? "当前" : null,
v -> openAccountEditor(account, null)
);
));
Button activate = BossUi.buildPrimaryButton(this, account.optBoolean("isActive") ? "已激活" : "设为当前主控");
activate.setEnabled(!account.optBoolean("isActive"));
@@ -153,33 +145,6 @@ public class AiAccountsActivity extends BossScreenActivity {
return card;
}
private LinearLayout buildSwitchHistoryCard(@Nullable JSONArray switchHistory) {
LinearLayout section = new LinearLayout(this);
section.setOrientation(LinearLayout.VERTICAL);
section.addView(BossUi.buildCard(
this,
"切换历史",
switchHistory == null || switchHistory.length() == 0 ? "当前没有切换记录。" : "最近切换记录会保留 40 条。",
"用于追踪主控身份变化"
));
if (switchHistory == null || switchHistory.length() == 0) {
section.addView(BossUi.buildEmptyCard(this, "当前没有 AI 账号切换历史。"));
return section;
}
for (int i = 0; i < switchHistory.length(); i++) {
JSONObject record = switchHistory.optJSONObject(i);
if (record == null) continue;
String body = "" + record.optString("fromLabel", "")
+ "\n到 " + record.optString("toLabel", "-")
+ "\n原因" + record.optString("reason", "-");
String meta = record.optString("role", "-") + " · " + record.optString("switchedAt", "-");
section.addView(BossUi.buildCard(this, "切换记录", body, meta));
}
return section;
}
private void openAccountEditor(@Nullable JSONObject existing, @Nullable String apiKeyHint) {
final android.widget.EditText labelInput = BossUi.buildInput(this, "标签,例如 主 GPT", false);
final android.widget.EditText displayNameInput = BossUi.buildInput(this, "显示名称", false);

View File

@@ -2,7 +2,6 @@ package com.hyzq.boss;
import android.content.Intent;
import android.os.Bundle;
import android.widget.Button;
import android.widget.LinearLayout;
import androidx.annotation.Nullable;
@@ -24,7 +23,7 @@ public class DeviceDetailActivity extends BossScreenActivity {
deviceId = getIntent().getStringExtra(EXTRA_DEVICE_ID);
deviceName = getIntent().getStringExtra(EXTRA_DEVICE_NAME);
configureScreen(deviceName == null ? "设备详情" : deviceName, "原生设备详情");
setHeaderAction("编辑", v -> openEditDialog());
hideHeaderAction();
reload();
}
@@ -48,8 +47,6 @@ public class DeviceDetailActivity extends BossScreenActivity {
private void renderDevice(JSONObject payload) {
JSONObject workspace = payload.optJSONObject("workspace");
JSONObject device = workspace == null ? null : workspace.optJSONObject("selectedDevice");
JSONArray relatedThreads = workspace == null ? null : workspace.optJSONArray("relatedThreads");
JSONObject enrollment = workspace == null ? null : workspace.optJSONObject("activeEnrollment");
replaceContent();
if (device == null) {
@@ -60,53 +57,38 @@ public class DeviceDetailActivity extends BossScreenActivity {
deviceName = device.optString("name", deviceId);
configureScreen(deviceName, device.optString("endpoint", "设备详情"));
appendContent(BossUi.buildCard(
appendContent(BossUi.buildListRow(
this,
device.optString("name", "设备"),
device.optString("note", "暂无备注"),
"状态 " + device.optString("status", "unknown")
+ " · 账号 " + device.optString("account", "-")
+ " · 5h " + device.optInt("quota5h", 0)
+ " · 7d " + device.optInt("quota7d", 0)
buildDeviceSubtitle(device),
buildDeviceMeta(device),
null,
null
));
Button skillsButton = BossUi.buildPrimaryButton(this, "查看技能");
skillsButton.setOnClickListener(v -> openSkills());
appendContent(skillsButton);
if (relatedThreads != null && relatedThreads.length() > 0) {
for (int i = 0; i < relatedThreads.length(); i++) {
JSONObject thread = relatedThreads.optJSONObject(i);
if (thread == null) continue;
appendContent(BossUi.buildCard(
this,
thread.optString("title", "线程"),
thread.optString("summary", ""),
thread.optString("workerId", "-")
+ " · " + thread.optInt("contextBudgetRemainingPct", 0) + "%"
+ " · " + thread.optString("contextBudgetLevel", "safe"),
v -> openThread(thread.optString("threadId"))
));
}
}
if (enrollment != null) {
appendContent(BossUi.buildCard(
this,
"当前绑定草稿",
"pairingCode " + enrollment.optString("pairingCode", "-")
+ "\ntoken " + enrollment.optString("token", "-"),
enrollment.optString("status", "ready")
+ " · 到期 " + enrollment.optString("expiresAt", "-")
));
}
appendContent(BossUi.buildMenuRow(this, "查看技能", "查看当前设备同步的 Skill 清单", null, v -> openSkills()));
appendContent(BossUi.buildMenuRow(this, "编辑", "修改设备名称、备注和项目列表", null, v -> openEditDialog()));
setRefreshing(false);
}
private void openThread(String threadId) {
Intent intent = new Intent(this, ThreadDetailActivity.class);
intent.putExtra(ThreadDetailActivity.EXTRA_THREAD_ID, threadId);
startActivity(intent);
private String buildDeviceSubtitle(JSONObject device) {
String note = device.optString("note", "");
if (!note.isEmpty()) {
return note;
}
return "状态 " + device.optString("status", "unknown") + " · 账号 " + device.optString("account", "-");
}
private @Nullable String buildDeviceMeta(JSONObject device) {
String endpoint = device.optString("endpoint", "");
if (!endpoint.isEmpty()) {
return endpoint;
}
JSONArray projects = device.optJSONArray("projects");
if (projects == null || projects.length() == 0) {
return null;
}
return "项目 " + joinArray(projects);
}
private void openSkills() {

View File

@@ -344,12 +344,11 @@ public class MainActivity extends AppCompatActivity {
private void renderDevicesRoot() {
screenContent.removeAllViews();
screenContent.addView(BossUi.buildListRow(
screenContent.addView(BossUi.buildMenuRow(
this,
"添加设备",
"通过配对码接入新的生产设备",
null,
null,
v -> startActivity(new Intent(this, DeviceEnrollmentActivity.class))
));
@@ -363,8 +362,7 @@ public class MainActivity extends AppCompatActivity {
if (item == null) continue;
String deviceId = item.optString("id", "");
WechatSurfaceMapper.DeviceRow row = WechatSurfaceMapper.toDeviceRow(item);
String meta = "5h " + item.optInt("quota5h", 0) + " · 7d " + item.optInt("quota7d", 0);
screenContent.addView(BossUi.buildListRow(this, row.title, row.subtitle, meta, null, v -> {
screenContent.addView(BossUi.buildListRow(this, row.title, row.subtitle, null, null, v -> {
if (deviceId.isEmpty()) {
showMessage("缺少 deviceId");
return;
@@ -382,12 +380,11 @@ public class MainActivity extends AppCompatActivity {
String account = sessionData == null
? apiClient.getAccountLabel()
: sessionData.optString("account", apiClient.getAccountLabel());
String expiresAt = sessionData == null ? "-" : sessionData.optString("expiresAt", "-");
screenContent.addView(BossUi.buildListRow(
this,
displayName,
"账号 " + account,
"会话到期 " + expiresAt,
null,
null,
null
));
@@ -402,16 +399,6 @@ public class MainActivity extends AppCompatActivity {
));
}
if (otaData != null) {
JSONObject availableRelease = otaData.optJSONObject("availableRelease");
String body = "当前版本 " + otaData.optString("currentVersion", "-");
String meta = availableRelease == null
? "当前没有待安装版本"
: "可用版本 " + availableRelease.optString("version", "-")
+ " · 文件 " + availableRelease.optString("packageFileName", "-");
screenContent.addView(BossUi.buildCard(this, "OTA 状态", body, meta));
}
Button logoutButton = BossUi.buildSecondaryButton(this, "退出登录");
logoutButton.setOnClickListener(v -> logout());
screenContent.addView(logoutButton);
@@ -488,15 +475,15 @@ public class MainActivity extends AppCompatActivity {
private String meDescriptionFor(String title) {
switch (title) {
case "账号与安全":
return "查看当前会话登录模式和退出登录";
return "查看当前会话登录安全";
case "AI 账号":
return "管理主 GPT、备用 GPT 与 API 容灾";
case "设置":
return "调整默认首页和刷新偏好";
return "调整默认首页和提醒偏好";
case "技能":
return "绑定设备查看 Skill 清单";
return "按设备查看 Skill 清单";
case "关于":
return "查看版本、OTA 状态和当前绑定节点";
return "查看版本、更新与高级入口";
default:
return "";
}

View File

@@ -33,33 +33,34 @@ public class SecurityActivity extends BossScreenActivity {
}
private void renderSecurity(@Nullable JSONObject session) {
replaceContent(
BossUi.buildCard(
this,
"当前登录模式",
"当前登录页已临时切到免验证模式,点击登录会直接创建最高管理员会话",
"后续如收口认证,再切回账号密码 / 验证码登录。"
)
);
replaceContent();
appendContent(BossUi.buildListRow(
this,
"当前登录模式",
"当前客户端仍使用快速进入模式",
"需要更严格认证,再切回账号密码验证码登录。",
null,
null
));
if (session != null) {
appendContent(BossUi.buildCard(
appendContent(BossUi.buildListRow(
this,
"当前会话",
"账号 " + session.optString("account", "-")
+ "\n角色 " + session.optString("role", "-")
+ "\n登录方式 " + session.optString("loginMethod", "-"),
"到期 " + session.optString("expiresAt", "-")
+ " · " + session.optString("role", "-"),
"登录方式 " + session.optString("loginMethod", "-")
+ " · 到期 " + session.optString("expiresAt", "-"),
null,
null
));
}
android.widget.Button devicesButton = BossUi.buildPrimaryButton(this, "打开设备页");
devicesButton.setOnClickListener(v -> {
appendContent(BossUi.buildMenuRow(this, "打开设备页", "查看已绑定设备与状态", null, v -> {
Intent intent = new Intent(this, MainActivity.class);
intent.putExtra(MainActivity.EXTRA_INITIAL_TAB, "devices");
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
startActivity(intent);
});
appendContent(devicesButton);
}));
android.widget.Button logoutButton = BossUi.buildSecondaryButton(this, "退出登录");
logoutButton.setOnClickListener(v -> logout());

View File

@@ -43,14 +43,14 @@ public class SettingsActivity extends BossScreenActivity {
}
private void buildForm() {
replaceContent(
BossUi.buildCard(
this,
"设置说明",
"当前设置会持久化到 data/boss-state.json下一线程接手不会丢失",
"原生设置页直接走 /api/v1/settings"
)
);
replaceContent(BossUi.buildListRow(
this,
"偏好设置",
"调整默认首页和提醒行为。",
"保存后会直接写入 /api/v1/settings",
null,
null
));
liveUpdatesSwitch = new SwitchCompat(this);
liveUpdatesSwitch.setText("启用实时刷新");
@@ -69,12 +69,12 @@ public class SettingsActivity extends BossScreenActivity {
);
preferredEntrySpinner.setAdapter(adapter);
LinearLayout card = BossUi.buildCard(this, "交互偏好", "切换默认首页与提醒行为", "保存后立即生效");
card.addView(liveUpdatesSwitch);
card.addView(riskBadgesSwitch);
card.addView(confirmActionsSwitch);
card.addView(preferredEntrySpinner);
appendContent(card);
LinearLayout form = BossUi.buildCard(this, "交互偏好", "切换默认首页与提醒开关", "保存后立即生效");
form.addView(liveUpdatesSwitch);
form.addView(riskBadgesSwitch);
form.addView(confirmActionsSwitch);
form.addView(preferredEntrySpinner);
appendContent(form);
}
private void populate(@Nullable JSONObject settings) {

View File

@@ -65,11 +65,13 @@ public class SkillInventoryActivity extends BossScreenActivity {
if (device != null) {
deviceName = device.optString("name", deviceId);
configureScreen("技能", deviceName);
appendContent(BossUi.buildCard(
appendContent(BossUi.buildListRow(
this,
deviceName,
"当前页按设备查看 Skill 清单。",
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。"
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。",
null,
null
));
}
@@ -81,13 +83,17 @@ public class SkillInventoryActivity extends BossScreenActivity {
for (int i = 0; i < skills.length(); i++) {
JSONObject skill = skills.optJSONObject(i);
if (skill == null) continue;
LinearLayout card = BossUi.buildCard(
LinearLayout card = new LinearLayout(this);
card.setOrientation(LinearLayout.VERTICAL);
card.addView(BossUi.buildListRow(
this,
skill.optString("name", "未命名 Skill"),
skill.optString("description", "未提供说明"),
skill.optString("category", "-")
+ " · " + skill.optString("updatedAt", "-")
);
+ " · " + skill.optString("updatedAt", "-"),
null,
null
));
Button copyInvocation = BossUi.buildPrimaryButton(this, "复制调用语句");
copyInvocation.setOnClickListener(v -> BossUi.copyText(this, "Skill 调用", skill.optString("invocation", "")));
card.addView(copyInvocation);

View File

@@ -60,6 +60,10 @@ public final class WechatSurfaceMapper {
return ROOT_ME_MENU_TITLES.toArray(new String[0]);
}
public static String advancedEntryTitle() {
return "高级与调试";
}
public static String[] projectQuickActions() {
return PROJECT_QUICK_ACTIONS.toArray(new String[0]);
}

View File

@@ -77,6 +77,11 @@ public class WechatSurfaceMapperTest {
);
}
@Test
public void advancedEntryTitle_movesOpsOutOfMainMePage() throws Exception {
assertEquals("高级与调试", WechatSurfaceMapper.advancedEntryTitle());
}
@Test
public void projectQuickActions_keepOnlyGoalsAndVersions() throws Exception {
assertArrayEquals(