style: align native me surfaces with wechat ui

This commit is contained in:
kris
2026-03-30 12:26:14 +08:00
parent 038c2bd088
commit 9c15c30a41
18 changed files with 120 additions and 70 deletions

View File

@@ -90,7 +90,7 @@ Android APK
- 已生成 Android debug APK`android/app/build/outputs/apk/debug/app-debug.apk`
- 已生成 Android signed release APK`android/app/build/outputs/apk/release/app-release.apk`
- `npm run apk:release` 还会额外产出带版本号的文件:`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk`
- 当前最新 release 构建版本:`2.5.3``versionCode=16`
- 当前最新 release 构建版本:`2.5.4``versionCode=17`
- 当前 APK 已切到原生 Android 客户端:`MainActivity + BossApiClient + 原生 XML 布局`
- 当前原生活动页已经覆盖会话首页、项目详情、项目目标、版本记录、会话信息、群资料、发起群聊、消息转发、线程详情、设备详情、添加设备、账号与安全、设置、AI 账号、技能、运维中心、关于
- 当前原生一级体验已回退到微信式交互:`会话 / 设备 / 我的` 固定底部 tab会话首页是简单聊天列表`主 Agent / 审计对话` 以普通置顶会话样式排在最前;项目详情页是聊天优先,只保留 `项目目标 / 版本记录` 两个轻入口
@@ -117,7 +117,7 @@ Android APK
- `2.5.0` 已补齐聊天附件主链:原生聊天框左侧 `+` 会打开底部抽屉,支持图片 / 视频 / 文件发送;默认走服务器文件存储,`我的 > 附件与存储` 可切到阿里 OSS 私有桶;附件消息已支持下载 / 打开、手动分析、自动分析状态,以及带 task token 的主 Agent 附件分析链接
- `2.5.1` 继续收口微信式原生 UI聊天页普通态顶部已隐藏刷新按钮只保留右上角“信息”发起群聊页顶部说明和选择区已压成更轻的会话式密度候选线程继续复用微信式会话卡片
- `2.5.2` 继续补齐深层原生页:`项目目标 / 版本迭代记录 / 会话信息 / 群资料` 已进一步向设计图收口;附件消息卡片的分析状态和动作文案也压成了更轻的微信式层级
- `2.5.3` 继续压缩原生聊天与建群页面:`发起群聊` 页已收成轻量头部 + hint pill + 一层操作区;聊天页里自己发出的消息顶部元信息已收成只显示时间,不再重复 `你 · 时间`
- `2.5.4` 已把 `我的` 根页收口成微信式资料区 + 白底菜单列表,并同步把 `设置 / 账号与安全 / AI 账号 / 技能 / 运维与修复` 的顶部说明从重 `soft panel` 降成轻量列表说明
## 本地启动

View File

@@ -36,8 +36,8 @@ android {
applicationId "com.hyzq.boss"
minSdkVersion rootProject.ext.minSdkVersion
targetSdkVersion rootProject.ext.targetSdkVersion
versionCode 16
versionName "2.5.3"
versionCode 17
versionName "2.5.4"
buildConfigField "String", "BOSS_API_BASE_URL", "\"https://boss.hyzq.net\""
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -49,11 +49,13 @@ public class AiAccountsActivity extends BossScreenActivity {
JSONArray accounts = payload.optJSONArray("accounts");
JSONObject activeIdentity = payload.optJSONObject("activeIdentity");
replaceContent();
appendContent(BossUi.buildSoftPanel(
appendContent(BossUi.buildWechatMenuRow(
this,
"AI 账号",
"这里统一管理主 GPT、备用 GPT 与 API 容灾账号。",
"轻点条目可编辑,按钮可切换、校验或删除。"
"轻点条目可编辑,按钮可切换、校验或删除。",
null,
null
));
appendContent(buildActiveIdentityCard(activeIdentity));
appendContent(buildAccountsSection(accounts));
@@ -62,17 +64,27 @@ public class AiAccountsActivity extends BossScreenActivity {
private LinearLayout buildActiveIdentityCard(@Nullable JSONObject activeIdentity) {
if (activeIdentity == null) {
return BossUi.buildSoftPanel(this, "当前主控身份", "当前没有可用账号。", "请先新增或启用一个账号。");
return BossUi.buildWechatMenuRow(
this,
"当前主控身份",
"当前没有可用账号。",
"请先新增或启用一个账号。",
null,
null
);
}
String body = activeIdentity.optString("label", "AI 账号")
+ " · " + activeIdentity.optString("displayName", "-")
+ "\n" + activeIdentity.optString("roleLabel", "-")
+ " · " + activeIdentity.optString("providerLabel", "-");
return BossUi.buildSoftPanel(
String subtitle = activeIdentity.optString("label", "AI 账号")
+ " · " + activeIdentity.optString("displayName", "-");
String meta = activeIdentity.optString("roleLabel", "-")
+ " · " + activeIdentity.optString("providerLabel", "-")
+ " · " + activeIdentity.optString("statusLabel", "-");
return BossUi.buildWechatMenuRow(
this,
"当前主控身份",
body,
activeIdentity.optString("statusLabel", "-")
subtitle,
meta,
null,
null
);
}

View File

@@ -47,6 +47,28 @@ public final class BossUi {
COMPACT_ICON
}
public static String formatRoleLabel(@Nullable String rawRole) {
if (TextUtils.isEmpty(rawRole)) {
return "";
}
switch (rawRole) {
case "highest_admin":
return "最高管理员";
case "admin":
return "管理员";
case "member":
return "成员";
case "primary":
return "主 GPT";
case "backup":
return "备用 GPT";
case "api_fallback":
return "API 容灾";
default:
return rawRole;
}
}
public static void applyTopActionButtonStyle(Context context, Button button, TopActionButtonStyle style) {
if (style == TopActionButtonStyle.COMPACT_ICON) {
button.setBackgroundResource(R.drawable.bg_secondary_button);
@@ -232,7 +254,11 @@ public final class BossUi {
@Nullable String badge,
@Nullable View.OnClickListener listener
) {
return buildListRow(context, title, subtitle, meta, badge, listener);
LinearLayout row = buildListRow(context, title, subtitle, meta, badge, listener);
row.setBackgroundColor(Color.WHITE);
row.setElevation(0f);
row.setPadding(dp(context, 18), dp(context, 15), dp(context, 18), dp(context, 15));
return row;
}
public static LinearLayout buildSimpleProfileHeader(
@@ -248,23 +274,21 @@ public final class BossUi {
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 12);
params.rightMargin = dp(context, 12);
params.bottomMargin = dp(context, 12);
params.bottomMargin = dp(context, 10);
card.setLayoutParams(params);
card.setPadding(dp(context, 16), dp(context, 16), dp(context, 16), dp(context, 16));
card.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
card.setElevation(dp(context, 1));
card.setPadding(dp(context, 20), dp(context, 18), dp(context, 20), dp(context, 18));
card.setBackgroundColor(Color.WHITE);
card.setElevation(0f);
TextView avatar = new TextView(context);
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(dp(context, 64), dp(context, 64));
LinearLayout.LayoutParams avatarParams = new LinearLayout.LayoutParams(dp(context, 70), dp(context, 70));
avatar.setLayoutParams(avatarParams);
avatar.setGravity(Gravity.CENTER);
avatar.setText(firstLetter(name));
avatar.setTextSize(28);
avatar.setTextSize(30);
avatar.setTypeface(Typeface.DEFAULT_BOLD);
avatar.setTextColor(context.getColor(R.color.boss_green));
avatar.setBackground(createRoundedBackground(Color.parseColor("#DFF3E8"), dp(context, 18)));
avatar.setBackground(createRoundedBackground(Color.parseColor("#DFF3E8"), dp(context, 35)));
card.addView(avatar);
LinearLayout textWrap = new LinearLayout(context);
@@ -279,16 +303,16 @@ public final class BossUi {
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(name) ? "我的" : name);
titleView.setTextSize(17);
titleView.setTextSize(22);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
textWrap.addView(titleView);
TextView subtitleView = new TextView(context);
subtitleView.setText(subtitle);
subtitleView.setTextSize(13);
subtitleView.setTextSize(14);
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
subtitleView.setPadding(0, dp(context, 4), 0, 0);
subtitleView.setPadding(0, dp(context, 5), 0, 0);
textWrap.addView(subtitleView);
if (!TextUtils.isEmpty(detail)) {
@@ -296,7 +320,7 @@ public final class BossUi {
detailView.setText(detail);
detailView.setTextSize(12);
detailView.setTextColor(context.getColor(R.color.boss_text_soft));
detailView.setPadding(0, dp(context, 6), 0, 0);
detailView.setPadding(0, dp(context, 5), 0, 0);
textWrap.addView(detailView);
}
@@ -543,13 +567,11 @@ public final class BossUi {
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.leftMargin = dp(context, 12);
params.rightMargin = dp(context, 12);
params.bottomMargin = dp(context, 12);
params.bottomMargin = dp(context, 1);
card.setLayoutParams(params);
card.setPadding(dp(context, 14), dp(context, 14), dp(context, 14), dp(context, 14));
card.setBackground(createRoundedBackground(Color.WHITE, dp(context, 18)));
card.setElevation(dp(context, 1));
card.setPadding(dp(context, 16), dp(context, 12), dp(context, 16), dp(context, 12));
card.setBackgroundColor(Color.WHITE);
card.setElevation(0f);
if (listener != null) {
card.setClickable(true);
card.setFocusable(true);
@@ -566,12 +588,12 @@ public final class BossUi {
1f
);
centerParams.leftMargin = dp(context, 12);
centerParams.rightMargin = dp(context, 8);
centerParams.rightMargin = dp(context, 10);
centerColumn.setLayoutParams(centerParams);
TextView titleView = new TextView(context);
titleView.setText(TextUtils.isEmpty(row.threadTitle) ? "未命名会话" : row.threadTitle);
titleView.setTextSize(18);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
titleView.setMaxLines(1);
@@ -591,10 +613,10 @@ public final class BossUi {
TextView previewView = new TextView(context);
previewView.setText(TextUtils.isEmpty(row.lastMessagePreview) ? "暂无消息" : row.lastMessagePreview);
previewView.setTextSize(14);
previewView.setTextSize(13);
previewView.setTextColor(context.getColor(R.color.boss_text_soft));
previewView.setPadding(0, dp(context, 5), 0, 0);
previewView.setMaxLines(2);
previewView.setPadding(0, dp(context, 4), 0, 0);
previewView.setMaxLines(1);
previewView.setEllipsize(TextUtils.TruncateAt.END);
centerColumn.addView(previewView);

View File

@@ -502,11 +502,14 @@ public class MainActivity extends AppCompatActivity {
String account = sessionData == null
? apiClient.getAccountLabel()
: sessionData.optString("account", apiClient.getAccountLabel());
String roleLabel = sessionData == null
? ""
: BossUi.formatRoleLabel(sessionData.optString("roleLabel", sessionData.optString("role", "")));
screenContent.addView(BossUi.buildSimpleProfileHeader(
this,
displayName,
"ChatGPT Plus · 主账号",
"主控账号已启用安全保护 · " + account
account,
(roleLabel.isEmpty() ? "主控账号已启用安全保护" : roleLabel + " · 主控账号已启用安全保护")
));
for (WechatSurfaceMapper.MeMenuItem item : WechatSurfaceMapper.rootMeMenuItems()) {

View File

@@ -50,13 +50,15 @@ public class OpsCenterActivity extends BossScreenActivity {
}
private void renderOpsTab(JSONObject ops) {
contentRoot.addView(BossUi.buildSoftPanel(
contentRoot.addView(BossUi.buildWechatMenuRow(
this,
"巡检状态",
ops.optString("mode", "idle").equals("active")
? "active当前存在风险线程或未关闭运维工单。"
: "idle当前没有高风险工单保持低频巡检。",
"这里只保留修复与验证的轻量入口。"
"这里只保留修复与验证的轻量入口。",
null,
null
));
JSONArray faults = ops.optJSONArray("faults");

View File

@@ -34,18 +34,20 @@ public class SecurityActivity extends BossScreenActivity {
private void renderSecurity(@Nullable JSONObject session) {
replaceContent();
appendContent(BossUi.buildSoftPanel(
appendContent(BossUi.buildWechatMenuRow(
this,
"当前登录模式",
"当前客户端仍使用快速进入模式。",
"需要更严格认证时,再切回账号密码或验证码登录。"
"需要更严格认证时,再切回账号密码或验证码登录。",
null,
null
));
if (session != null) {
appendContent(BossUi.buildWechatMenuRow(
this,
"当前会话",
"账号 " + session.optString("account", "-")
+ " · " + session.optString("role", "-"),
+ " · " + BossUi.formatRoleLabel(session.optString("role", "-")),
"登录方式 " + session.optString("loginMethod", "-")
+ " · 到期 " + session.optString("expiresAt", "-"),
null,

View File

@@ -69,11 +69,13 @@ public class SettingsActivity extends BossScreenActivity {
preferredEntrySpinner.setAdapter(adapter);
}
replaceContent(BossUi.buildSoftPanel(
replaceContent(BossUi.buildWechatMenuRow(
this,
"偏好设置",
"调整默认首页和提醒行为。",
"保存后会直接写入 /api/v1/settings。"
"保存后会直接写入当前账号设置",
null,
null
));
appendContent(BossUi.buildFormCell(this, "实时刷新", "会话、设备和 OTA 状态变化时自动更新", liveUpdatesSwitch));

View File

@@ -119,11 +119,13 @@ public class SkillInventoryActivity extends BossScreenActivity {
if (device != null) {
deviceName = device.optString("name", deviceId);
configureScreen("技能", deviceName);
appendContent(BossUi.buildSoftPanel(
appendContent(BossUi.buildWechatMenuRow(
this,
deviceName,
"当前页按设备查看 Skill 清单。",
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。"
"Skill 由 local-agent 从本机 ~/.codex/skills 扫描并同步。",
null,
null
));
}

View File

@@ -28,7 +28,7 @@ public class BossUiRootSurfaceTest {
new JSONObject()
.put("displayName", "Kris")
.put("account", "17600003315")
.put("role", "最高管理员")
.put("role", "highest_admin")
);
ReflectionHelpers.callInstanceMethod(activity, "renderMeRoot");
@@ -39,7 +39,7 @@ public class BossUiRootSurfaceTest {
assertEquals("资料头不应保留浮层卡片感", 0f, header.getElevation(), 0.01f);
assertTrue(viewTreeContainsText(header, "Kris"));
assertTrue(viewTreeContainsText(header, "17600003315"));
assertTrue(viewTreeContainsText(header, "ChatGPT Plus · 主账号"));
assertTrue(viewTreeContainsText(header, "最高管理员"));
assertTrue(viewTreeContainsText(header, "主控账号已启用安全保护"));
assertTrue(viewTreeContainsText(content, "账号与安全"));
@@ -48,12 +48,17 @@ public class BossUiRootSurfaceTest {
assertTrue(viewTreeContainsText(content, "AI 账号"));
assertTrue(viewTreeContainsText(content, "技能"));
assertTrue(viewTreeContainsText(content, "关于"));
for (int i = 1; i < content.getChildCount(); i += 1) {
View row = content.getChildAt(i);
assertTrue("我的页菜单应整行可点", row.isClickable());
}
}
private static boolean viewTreeContainsText(View root, String expectedText) {
if (root instanceof TextView) {
CharSequence text = ((TextView) root).getText();
if (expectedText.contentEquals(text)) {
if (text != null && text.toString().contains(expectedText)) {
return true;
}
}

View File

@@ -163,7 +163,7 @@
- 邮件:`Postfix + Dovecot`
- Android`AppCompatActivity + 原生 XML 布局 + HttpURLConnection`
- 原生登录恢复:`SharedPreferences + restore token`
- 当前最新原生 APK`2.5.3``versionCode=16`
- 当前最新原生 APK`2.5.4``versionCode=17`
当前不要误判成已经用了:

View File

@@ -121,7 +121,7 @@ cd /Users/kris/code/boss
- 当前已生成 Android debug APK`android/app/build/outputs/apk/debug/app-debug.apk`
- 当前已生成 Android signed release APK`android/app/build/outputs/apk/release/app-release.apk`
- 当前 release 构建还会额外生成带版本号的 APK`android/app/build/outputs/apk/release/boss-android-v{versionName}-release.apk`
- 当前最新 release 构建版本:`2.5.3``versionCode=16`
- 当前最新 release 构建版本:`2.5.4``versionCode=17`
- 当前 release keystore 位于本机 `android/keystores/boss-release.keystore`,签名参数位于 `android/signing/release-signing.properties`
- `2.0.1` 已在本机连接的华为真机上复核通过,修复了 `Theme.SplashScreen` 导致的 `AppCompatActivity` 启动闪退
- `2.1.0` 已把 Web 一级页和主要二级页全部补成原生活动页:`MainActivity / ProjectDetailActivity / ProjectGoalsActivity / ProjectVersionsActivity / ProjectForwardActivity / ThreadDetailActivity / DeviceDetailActivity / DeviceEnrollmentActivity / SkillInventoryActivity / SecurityActivity / SettingsActivity / AiAccountsActivity / OpsCenterActivity / AboutActivity`
@@ -137,8 +137,8 @@ cd /Users/kris/code/boss
- `2.5.1` 已压缩“发起群聊”页首信息密度:来源会话场景只保留一张紧凑摘要卡,选择区改成更短的微信式提示,同时保留会话卡片式候选列表
- `2.5.2` 已继续回退深层原生页:`会话信息 / 群资料` 改为轻量头部信息 + 菜单式入口 + 线程列表;`项目目标 / 版本迭代记录` 也已按设计图改成轻卡片结构,不再使用厚按钮和说明块
- `2.5.2` 已压缩附件消息卡片的状态层级:`待分析` 收成 `可分析``让 AI 分析` 收成 `AI 分析`,有摘要时不再重复显示 `已分析`
- `2.5.3`继续压缩“发起群聊”页:来源会话摘要改成轻量 profile header选择区改成 `选择其他线程 + hint pill`,底部按钮收成同层操作区,候选会话卡片纵向间距进一步压缩
- `2.5.3` 已把聊天页里自己发出的消息顶部元信息收成只显示时间,不再重复 `你 · 时间`,文本消息、附件卡片和聊天记录卡片都会共用这条规则
- `2.5.4``我的` 根页收口成微信式资料区 + 白底菜单列表,会话根页同步改成更扁平的白底聊天列表,不再是厚圆角卡片流
- `2.5.4` 已把 `设置 / 账号与安全 / AI 账号 / 技能 / 运维与修复` 的顶部说明从绿色 `soft panel` 降成轻量列表说明,和会话/设备页统一成同一套微信式产品语言
- 当前附件分析任务已带受控 `task token` 下载链接和文本摘录:本地开发环境会跟随请求 origin 生成链接,生产环境默认走 `https://boss.hyzq.net`
- `2.5.x` 当前已补上会话首页独立建群入口:可以不从单线程聊天内部出发,直接在会话首页右上角 `+` 建立新群聊;同时已把多个原生自定义 top bar 页面统一纳入状态栏安全区处理
- 当前 `local-agent` 已能回写带 `dispatchExecutionId / targetProjectId / targetThreadId / rawThreadReply` 的任务完成载荷,群聊分发执行结果不再只停留在主 Agent 队列

View File

@@ -1,11 +1,11 @@
{
"artifactType": "aab",
"fileName": "boss-android-v2.5.3-release.aab",
"urlPath": "/downloads/boss-android-v2.5.3-release.aab",
"sizeBytes": 2912097,
"updatedAt": "2026-03-29T12:41:56Z",
"sha256": "12b520dba9a9fa3075c374910a595943d3f2a0b9f70a813bdfe8d1546e380d3c",
"versionName": "2.5.3",
"versionCode": 16,
"fileName": "boss-android-v2.5.4-release.aab",
"urlPath": "/downloads/boss-android-v2.5.4-release.aab",
"sizeBytes": 2912332,
"updatedAt": "2026-03-30T04:24:49Z",
"sha256": "75d622b5b48ca5b6005406202ea4b016d64cc7049535e89a38e8cb127ca682e8",
"versionName": "2.5.4",
"versionCode": 17,
"buildFlavor": "release"
}

View File

@@ -1,10 +1,10 @@
{
"fileName": "boss-android-v2.5.3-release.apk",
"fileName": "boss-android-v2.5.4-release.apk",
"urlPath": "/api/v1/user/ota/package",
"sizeBytes": 3088834,
"updatedAt": "2026-03-29T12:41:51Z",
"sha256": "dd44b6e1228966fbd6e83ab53936393024a76e17f7fe996e15442f4f105a435a",
"versionName": "2.5.3",
"versionCode": 16,
"sizeBytes": 3089082,
"updatedAt": "2026-03-30T04:20:41Z",
"sha256": "a76f6a86c0923695188750a8794d1ded9defa5e9f3bf898b24421c9cae435b02",
"versionName": "2.5.4",
"versionCode": 17,
"buildFlavor": "release"
}

Binary file not shown.

Binary file not shown.