feat: restore wechat-style root shell

This commit is contained in:
kris
2026-03-27 01:58:34 +08:00
parent 17300c49ea
commit ce8dcad41c
11 changed files with 278 additions and 157 deletions

View File

@@ -4,6 +4,8 @@ import android.content.ClipData;
import android.content.ClipboardManager;
import android.content.Context;
import android.graphics.Typeface;
import android.text.TextUtils;
import android.view.Gravity;
import android.view.View;
import android.widget.Button;
import android.widget.EditText;
@@ -16,6 +18,94 @@ import androidx.annotation.Nullable;
public final class BossUi {
private BossUi() {}
public static LinearLayout buildListRow(
Context context,
String title,
@Nullable String subtitle,
@Nullable String meta,
@Nullable String badge,
@Nullable View.OnClickListener listener
) {
LinearLayout row = new LinearLayout(context);
row.setOrientation(LinearLayout.HORIZONTAL);
row.setGravity(Gravity.CENTER_VERTICAL);
LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(
LinearLayout.LayoutParams.MATCH_PARENT,
LinearLayout.LayoutParams.WRAP_CONTENT
);
params.bottomMargin = dp(context, 1);
row.setLayoutParams(params);
row.setPadding(dp(context, 16), dp(context, 14), dp(context, 16), dp(context, 14));
row.setBackgroundResource(R.drawable.bg_list_row);
if (listener != null) {
row.setClickable(true);
row.setFocusable(true);
row.setOnClickListener(listener);
}
LinearLayout textWrap = new LinearLayout(context);
textWrap.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textWrap.setLayoutParams(textParams);
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
textWrap.addView(titleView);
if (!TextUtils.isEmpty(subtitle)) {
TextView subtitleView = new TextView(context);
subtitleView.setText(subtitle);
subtitleView.setTextSize(14);
subtitleView.setTextColor(context.getColor(R.color.boss_text_muted));
subtitleView.setPadding(0, dp(context, 4), 0, 0);
textWrap.addView(subtitleView);
}
if (!TextUtils.isEmpty(meta)) {
TextView metaView = new TextView(context);
metaView.setText(meta);
metaView.setTextSize(12);
metaView.setTextColor(context.getColor(R.color.boss_text_soft));
metaView.setPadding(0, dp(context, 6), 0, 0);
textWrap.addView(metaView);
}
row.addView(textWrap);
LinearLayout accessoryWrap = new LinearLayout(context);
accessoryWrap.setOrientation(LinearLayout.VERTICAL);
accessoryWrap.setGravity(Gravity.END | Gravity.CENTER_VERTICAL);
if (!TextUtils.isEmpty(badge)) {
TextView badgeView = new TextView(context);
badgeView.setText(badge);
badgeView.setTextSize(12);
badgeView.setTextColor(context.getColor(R.color.boss_green));
badgeView.setBackgroundResource(R.drawable.bg_tab_active);
badgeView.setPadding(dp(context, 8), dp(context, 2), dp(context, 8), dp(context, 2));
accessoryWrap.addView(badgeView);
}
if (listener != null) {
TextView arrowView = new TextView(context);
arrowView.setText("");
arrowView.setTextSize(18);
arrowView.setTextColor(context.getColor(R.color.boss_text_soft));
arrowView.setPadding(dp(context, 10), dp(context, 4), 0, 0);
accessoryWrap.addView(arrowView);
}
row.addView(accessoryWrap);
return row;
}
public static LinearLayout buildCard(Context context, String title, String body, String meta) {
return buildCard(context, title, body, meta, null);
}
@@ -74,42 +164,7 @@ public final class BossUi {
@Nullable String badge,
View.OnClickListener listener
) {
LinearLayout row = buildCard(context, title, description, badge == null ? "点击进入" : badge, listener);
row.setOrientation(LinearLayout.HORIZONTAL);
row.removeAllViews();
LinearLayout textWrap = new LinearLayout(context);
textWrap.setOrientation(LinearLayout.VERTICAL);
LinearLayout.LayoutParams textParams = new LinearLayout.LayoutParams(
0,
LinearLayout.LayoutParams.WRAP_CONTENT,
1f
);
textWrap.setLayoutParams(textParams);
TextView titleView = new TextView(context);
titleView.setText(title);
titleView.setTextSize(17);
titleView.setTypeface(Typeface.DEFAULT_BOLD);
titleView.setTextColor(context.getColor(R.color.boss_text_primary));
TextView descView = new TextView(context);
descView.setText(description);
descView.setTextSize(13);
descView.setTextColor(context.getColor(R.color.boss_text_muted));
descView.setPadding(0, dp(context, 6), 0, 0);
textWrap.addView(titleView);
textWrap.addView(descView);
TextView accessory = new TextView(context);
accessory.setText(badge == null ? "" : badge + " ");
accessory.setTextSize(13);
accessory.setTextColor(context.getColor(R.color.boss_green));
row.addView(textWrap);
row.addView(accessory);
return row;
return buildListRow(context, title, description, null, badge, listener);
}
public static LinearLayout buildEmptyCard(Context context, String text) {

View File

@@ -108,6 +108,11 @@ public class MainActivity extends AppCompatActivity {
tabMe = findViewById(R.id.tab_me);
screenRefresh = findViewById(R.id.screen_refresh);
screenContent = findViewById(R.id.screen_content);
String[] rootTabs = WechatSurfaceMapper.rootTabLabels();
tabConversations.setText(rootTabs[0]);
tabDevices.setText(rootTabs[1]);
tabMe.setText(rootTabs[2]);
}
private void bindActions() {
@@ -277,16 +282,16 @@ public class MainActivity extends AppCompatActivity {
switch (activeTab) {
case "devices":
updateHeader("设备", "只展示当前正式接入生产链路的设备。");
updateHeader("设备", "生产设备列表");
renderDevicesRoot();
break;
case "me":
updateHeader("我的", "账号、安全、技能、运维、OTA 都从这里进入。");
updateHeader("我的", "账号与应用设置");
renderMeRoot();
break;
case "conversations":
default:
updateHeader("会话", "原生会话列表直接消费 /api/v1/conversations。");
updateHeader("会话", "最近消息");
renderConversationsRoot();
break;
}
@@ -304,19 +309,12 @@ public class MainActivity extends AppCompatActivity {
}
private void styleTab(Button button, boolean active) {
button.setBackgroundResource(active ? R.drawable.bg_primary_button : R.drawable.bg_secondary_button);
button.setTextColor(getColor(active ? R.color.boss_surface : R.color.boss_green));
button.setBackgroundResource(active ? R.drawable.bg_tab_active : R.drawable.bg_tab_inactive);
button.setTextColor(getColor(active ? R.color.boss_green : R.color.boss_text_muted));
}
private void renderConversationsRoot() {
screenContent.removeAllViews();
screenContent.addView(BossUi.buildCard(
this,
"会话首页",
"当前原生首页会直接进入项目详情、目标、版本、转发与线程预算详情。",
conversationsData == null ? "正在等待数据" : "会话数 " + conversationsData.length()
));
if (conversationsData == null || conversationsData.length() == 0) {
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有会话数据。"));
return;
@@ -326,41 +324,35 @@ public class MainActivity extends AppCompatActivity {
JSONObject item = conversationsData.optJSONObject(i);
if (item == null) continue;
String projectId = item.optString("projectId", "");
String title = item.optString("projectTitle", "未命名会话");
StringBuilder body = new StringBuilder(item.optString("preview", "暂无预览"));
if (item.optInt("activeDeviceCount", 0) > 0) {
body.append("\n设备 ").append(item.optString("deviceNamesPreview", "未标注"));
}
JSONObject budget = item.optJSONObject("contextBudgetIndicator");
String meta = "风险 " + item.optString("riskLevel", "unknown")
+ " · 未读 " + item.optInt("unreadCount", 0)
+ " · " + item.optString("latestReplyLabel", "-");
if (budget != null && budget.optBoolean("visible", false)) {
meta = meta + " · 预算 " + budget.optInt("percent", 0) + "%";
}
screenContent.addView(BossUi.buildCard(this, title, body.toString(), meta, v -> {
WechatSurfaceMapper.ConversationRow row = WechatSurfaceMapper.toConversationRow(item);
String badge = row.unreadCount > 0 ? String.valueOf(row.unreadCount) : null;
screenContent.addView(BossUi.buildListRow(
this,
row.title.isEmpty() ? "未命名会话" : row.title,
row.preview.isEmpty() ? "暂无预览" : row.preview,
row.timeLabel.isEmpty() ? null : row.timeLabel,
badge,
v -> {
if (projectId.isEmpty()) {
showMessage("缺少 projectId");
return;
}
openProject(projectId, title);
openProject(projectId, row.title.isEmpty() ? "未命名会话" : row.title);
}));
}
}
private void renderDevicesRoot() {
screenContent.removeAllViews();
screenContent.addView(BossUi.buildCard(
screenContent.addView(BossUi.buildListRow(
this,
"设备首页",
"设备详情、技能清单和配对草稿都改为原生页。",
devicesData == null ? "正在等待数据" : "设备数 " + devicesData.length()
"添加设备",
"通过配对码接入新的生产设备",
null,
null,
v -> startActivity(new Intent(this, DeviceEnrollmentActivity.class))
));
Button addDeviceButton = BossUi.buildPrimaryButton(this, "添加设备");
addDeviceButton.setOnClickListener(v -> startActivity(new Intent(this, DeviceEnrollmentActivity.class)));
screenContent.addView(addDeviceButton);
if (devicesData == null || devicesData.length() == 0) {
screenContent.addView(BossUi.buildEmptyCard(this, "当前没有接入设备。"));
return;
@@ -370,18 +362,14 @@ public class MainActivity extends AppCompatActivity {
JSONObject item = devicesData.optJSONObject(i);
if (item == null) continue;
String deviceId = item.optString("id", "");
String title = item.optString("name", "未命名设备");
String body = item.optString("note", item.optString("endpoint", "暂无设备说明"));
String meta = "状态 " + item.optString("status", "unknown")
+ " · 账号 " + item.optString("account", "-")
+ " · 5h " + item.optInt("quota5h", 0)
+ " · 7d " + item.optInt("quota7d", 0);
screenContent.addView(BossUi.buildCard(this, title, body, meta, v -> {
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 -> {
if (deviceId.isEmpty()) {
showMessage("缺少 deviceId");
return;
}
openDevice(deviceId, title);
openDevice(deviceId, row.title);
}));
}
}
@@ -395,55 +383,24 @@ public class MainActivity extends AppCompatActivity {
? apiClient.getAccountLabel()
: sessionData.optString("account", apiClient.getAccountLabel());
String expiresAt = sessionData == null ? "-" : sessionData.optString("expiresAt", "-");
screenContent.addView(BossUi.buildCard(
screenContent.addView(BossUi.buildListRow(
this,
displayName,
"账号 " + account + "\n当前原生客户端已覆盖会话 / 设备 / 我的一级导航。",
"会话到期 " + expiresAt
"账号 " + account,
"会话到期 " + expiresAt,
null,
null
));
screenContent.addView(BossUi.buildMenuRow(
this,
"账号与安全",
"查看当前会话、登录模式和退出登录。",
null,
v -> startActivity(new Intent(this, SecurityActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"设置",
"实时刷新、风险徽标和默认首页。",
null,
v -> startActivity(new Intent(this, SettingsActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"运维与修复",
"查看故障、repair ticket、审计请求和能力注册表。",
null,
v -> startActivity(new Intent(this, OpsCenterActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"AI 账号",
"管理主 GPT、备用 GPT、Master Codex Node 与 API 容灾。",
null,
v -> startActivity(new Intent(this, AiAccountsActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"技能",
"按绑定设备查看 Skill并一键复制调用语句。",
null,
v -> startActivity(new Intent(this, SkillInventoryActivity.class))
));
screenContent.addView(BossUi.buildMenuRow(
this,
"关于",
"查看版本、OTA 状态和当前绑定节点。",
otaData == null ? null : otaData.optBoolean("hasOta", false) ? "OTA" : null,
v -> startActivity(new Intent(this, AboutActivity.class))
));
for (String title : WechatSurfaceMapper.rootMeMenuTitles()) {
screenContent.addView(BossUi.buildMenuRow(
this,
title,
meDescriptionFor(title),
meBadgeFor(title),
v -> openMeEntry(title)
));
}
if (otaData != null) {
JSONObject availableRelease = otaData.optJSONObject("availableRelease");
@@ -502,4 +459,53 @@ public class MainActivity extends AppCompatActivity {
private void showMessage(String text) {
Toast.makeText(this, text, Toast.LENGTH_SHORT).show();
}
private void openMeEntry(String title) {
Intent intent;
switch (title) {
case "账号与安全":
intent = new Intent(this, SecurityActivity.class);
break;
case "AI 账号":
intent = new Intent(this, AiAccountsActivity.class);
break;
case "设置":
intent = new Intent(this, SettingsActivity.class);
break;
case "技能":
intent = new Intent(this, SkillInventoryActivity.class);
break;
case "关于":
intent = new Intent(this, AboutActivity.class);
break;
default:
showMessage("暂未接入:" + title);
return;
}
startActivity(intent);
}
private String meDescriptionFor(String title) {
switch (title) {
case "账号与安全":
return "查看当前会话、登录模式和退出登录";
case "AI 账号":
return "管理主 GPT、备用 GPT 与 API 容灾";
case "设置":
return "调整默认首页和刷新偏好";
case "技能":
return "按绑定设备查看 Skill 清单";
case "关于":
return "查看版本、OTA 状态和当前绑定节点";
default:
return "";
}
}
private @Nullable String meBadgeFor(String title) {
if ("关于".equals(title) && otaData != null && otaData.optBoolean("hasOta", false)) {
return "OTA";
}
return null;
}
}

View File

@@ -6,6 +6,12 @@ import java.util.Arrays;
import java.util.List;
public final class WechatSurfaceMapper {
private static final List<String> ROOT_TAB_LABELS = Arrays.asList(
"会话",
"设备",
"我的"
);
private static final List<String> ROOT_ME_MENU_TITLES = Arrays.asList(
"账号与安全",
"AI 账号",
@@ -40,6 +46,10 @@ public final class WechatSurfaceMapper {
);
}
public static String[] rootTabLabels() {
return ROOT_TAB_LABELS.toArray(new String[0]);
}
public static String[] rootMeMenuTitles() {
return ROOT_ME_MENU_TITLES.toArray(new String[0]);
}

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/boss_bg_app" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<solid android:color="@color/boss_surface" />
<stroke
android:width="1dp"
android:color="@color/boss_divider" />
</shape>
</item>
</selector>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="#1407C160" />
<corners android:radius="18dp" />
</shape>

View File

@@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="rectangle">
<solid android:color="@color/boss_surface" />
<corners android:radius="18dp" />
</shape>

View File

@@ -2,7 +2,7 @@
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_app_gradient">
android:background="@color/boss_bg_app">
<ScrollView
android:id="@+id/login_panel"
@@ -16,18 +16,18 @@
android:gravity="center_horizontal"
android:orientation="vertical"
android:paddingLeft="24dp"
android:paddingTop="72dp"
android:paddingTop="96dp"
android:paddingRight="24dp"
android:paddingBottom="32dp">
<TextView
android:layout_width="72dp"
android:layout_height="72dp"
android:background="@drawable/bg_primary_button"
android:layout_width="80dp"
android:layout_height="80dp"
android:background="@drawable/bg_tab_active"
android:gravity="center"
android:text="B"
android:textColor="@color/boss_surface"
android:textSize="30sp"
android:textColor="@color/boss_green"
android:textSize="32sp"
android:textStyle="bold" />
<TextView
@@ -54,7 +54,7 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="28dp"
android:background="@drawable/bg_card"
android:background="@drawable/bg_list_row"
android:orientation="vertical"
android:padding="20dp">
@@ -109,12 +109,13 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/boss_surface"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="20dp"
android:paddingTop="18dp"
android:paddingTop="16dp"
android:paddingRight="20dp"
android:paddingBottom="16dp">
android:paddingBottom="14dp">
<Button
android:id="@+id/back_button"
@@ -144,7 +145,7 @@
android:layout_height="wrap_content"
android:text="会话"
android:textColor="@color/boss_text_primary"
android:textSize="24sp"
android:textSize="22sp"
android:textStyle="bold" />
<TextView
@@ -163,9 +164,9 @@
android:layout_height="wrap_content"
android:background="@drawable/bg_secondary_button"
android:paddingLeft="16dp"
android:paddingTop="10dp"
android:paddingTop="9dp"
android:paddingRight="16dp"
android:paddingBottom="10dp"
android:paddingBottom="9dp"
android:text="刷新"
android:textAllCaps="false"
android:textColor="@color/boss_green"
@@ -192,10 +193,9 @@
android:id="@+id/screen_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/boss_panel"
android:orientation="vertical"
android:paddingLeft="20dp"
android:paddingTop="8dp"
android:paddingRight="20dp"
android:paddingBottom="88dp" />
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>
@@ -203,11 +203,13 @@
<LinearLayout
android:layout_width="match_parent"
android:layout_height="76dp"
android:layout_height="72dp"
android:background="@color/boss_surface"
android:elevation="10dp"
android:gravity="center"
android:orientation="horizontal"
android:paddingTop="8dp"
android:paddingBottom="8dp"
android:paddingLeft="12dp"
android:paddingRight="12dp">
@@ -217,10 +219,10 @@
android:layout_height="48dp"
android:layout_marginRight="6dp"
android:layout_weight="1"
android:background="@drawable/bg_primary_button"
android:background="@drawable/bg_tab_active"
android:text="会话"
android:textAllCaps="false"
android:textColor="@color/boss_surface"
android:textColor="@color/boss_green"
android:textStyle="bold" />
<Button
@@ -230,10 +232,10 @@
android:layout_marginLeft="6dp"
android:layout_marginRight="6dp"
android:layout_weight="1"
android:background="@drawable/bg_secondary_button"
android:background="@drawable/bg_tab_inactive"
android:text="设备"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textColor="@color/boss_text_muted"
android:textStyle="bold" />
<Button
@@ -242,10 +244,10 @@
android:layout_height="48dp"
android:layout_marginLeft="6dp"
android:layout_weight="1"
android:background="@drawable/bg_secondary_button"
android:background="@drawable/bg_tab_inactive"
android:text="我的"
android:textAllCaps="false"
android:textColor="@color/boss_green"
android:textColor="@color/boss_text_muted"
android:textStyle="bold" />
</LinearLayout>
</LinearLayout>

View File

@@ -2,12 +2,13 @@
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/bg_app_gradient"
android:background="@color/boss_bg_app"
android:orientation="vertical">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/boss_surface"
android:gravity="center_vertical"
android:orientation="horizontal"
android:paddingLeft="16dp"
@@ -74,13 +75,13 @@
android:id="@+id/screen_refresh_button"
android:layout_width="wrap_content"
android:layout_height="40dp"
android:background="@drawable/bg_primary_button"
android:background="@drawable/bg_secondary_button"
android:minWidth="0dp"
android:paddingLeft="14dp"
android:paddingRight="14dp"
android:text="刷新"
android:textAllCaps="false"
android:textColor="@color/boss_surface"
android:textColor="@color/boss_green"
android:textStyle="bold" />
</LinearLayout>
@@ -99,10 +100,9 @@
android:id="@+id/screen_content"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/boss_panel"
android:orientation="vertical"
android:paddingLeft="18dp"
android:paddingTop="6dp"
android:paddingRight="18dp"
android:paddingTop="8dp"
android:paddingBottom="24dp" />
</ScrollView>
</androidx.swiperefreshlayout.widget.SwipeRefreshLayout>

View File

@@ -3,11 +3,15 @@
<color name="boss_green">#07C160</color>
<color name="boss_green_dark">#04984B</color>
<color name="boss_surface">#FFFFFFFF</color>
<color name="boss_bg_start">#FFF1F6EE</color>
<color name="boss_bg_end">#FFE3F0E3</color>
<color name="boss_card_stroke">#1A0F1B12</color>
<color name="boss_bg_start">#FFF7F7F7</color>
<color name="boss_bg_end">#FFF7F7F7</color>
<color name="boss_bg_app">#FFF7F7F7</color>
<color name="boss_panel">#FFFFFFFF</color>
<color name="boss_card_stroke">#14000000</color>
<color name="boss_divider">#FFEAEAEA</color>
<color name="boss_text_primary">#FF111111</color>
<color name="boss_text_muted">#FF5F6B63</color>
<color name="boss_text_soft">#FF8E8E93</color>
<color name="colorPrimary">@color/boss_green</color>
<color name="colorPrimaryDark">@color/boss_green_dark</color>
<color name="colorAccent">@color/boss_green</color>

View File

@@ -6,17 +6,17 @@
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
<item name="android:windowBackground">@drawable/bg_app_gradient</item>
<item name="android:windowBackground">@color/boss_bg_app</item>
</style>
<style name="AppTheme.NoActionBar" parent="Theme.AppCompat.DayNight.NoActionBar">
<item name="windowActionBar">false</item>
<item name="windowNoTitle">true</item>
<item name="android:windowBackground">@drawable/bg_app_gradient</item>
<item name="android:windowBackground">@color/boss_bg_app</item>
</style>
<style name="AppTheme.NoActionBarLaunch" parent="AppTheme.NoActionBar">
<item name="android:windowBackground">@drawable/bg_app_gradient</item>
<item name="android:windowBackground">@color/boss_bg_app</item>
</style>
</resources>

View File

@@ -48,6 +48,22 @@ public class WechatSurfaceMapperTest {
);
}
@Test
public void rootTabOrder_isWechatStyle() throws Exception {
assertArrayEquals(
new String[]{"会话", "设备", "我的"},
WechatSurfaceMapper.rootTabLabels()
);
}
@Test
public void mainPage_doesNotExposeOpsEntry() throws Exception {
assertArrayEquals(
new String[]{"账号与安全", "AI 账号", "设置", "技能", "关于"},
WechatSurfaceMapper.rootMeMenuTitles()
);
}
@Test
public void projectQuickActions_keepOnlyGoalsAndVersions() throws Exception {
assertArrayEquals(