From 71d097929229554515dff6dff742e1616dbe6dc3 Mon Sep 17 00:00:00 2001 From: kris Date: Tue, 31 Mar 2026 16:30:21 +0800 Subject: [PATCH] fix: preserve user chat messages after ai onboarding --- .../java/com/hyzq/boss/BossApiClient.java | 24 ++- .../com/hyzq/boss/ProjectDetailActivity.java | 8 +- .../boss/BossApiClientDispatchPlansTest.java | 21 +++ .../boss/ProjectDetailActivityUiTest.java | 174 ++++++++++++++++++ 4 files changed, 223 insertions(+), 4 deletions(-) diff --git a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java index d2b3940..2e8e9bc 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java +++ b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java @@ -701,8 +701,10 @@ public class BossApiClient { void rememberIdentity(JSONObject json) { if (json == null) return; - JSONObject session = json.optJSONObject("session"); - JSONObject source = session != null ? session : json; + JSONObject source = resolveSessionIdentitySource(json); + if (source == null) { + return; + } SharedPreferences.Editor editor = prefs.edit(); String restoreToken = source.optString("restoreToken", ""); @@ -723,6 +725,24 @@ public class BossApiClient { editor.apply(); } + @Nullable + private JSONObject resolveSessionIdentitySource(JSONObject json) { + JSONObject session = json.optJSONObject("session"); + if (session != null) { + return session; + } + if ( + json.has("restoreToken") + || json.has("account") + || json.has("role") + || json.has("expiresAt") + || json.has("sessionCookie") + ) { + return json; + } + return null; + } + private void clearSession() { prefs.edit() .remove(KEY_SESSION_COOKIE) diff --git a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java index 9a56e2d..c68cd77 100644 --- a/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java +++ b/android/app/src/main/java/com/hyzq/boss/ProjectDetailActivity.java @@ -718,11 +718,12 @@ public class ProjectDetailActivity extends BossScreenActivity { private View buildMessageView(JSONObject message) { String messageId = message.optString("id", ""); + String sender = message.optString("sender", ""); String senderLabel = message.optString("senderLabel", "消息"); String body = message.optString("body", ""); String meta = formatMessageTime(message.optString("sentAt", "")); String kind = message.optString("kind", ""); - boolean outgoing = isOutgoingMessage(senderLabel); + boolean outgoing = isOutgoingMessage(senderLabel, sender); View messageView; View.OnClickListener messagePrimaryClick = null; @@ -1293,7 +1294,10 @@ public class ProjectDetailActivity extends BossScreenActivity { return remainingScroll <= BossUi.dp(this, 96); } - private boolean isOutgoingMessage(String senderLabel) { + private boolean isOutgoingMessage(String senderLabel, @Nullable String sender) { + if ("user".equals(sender)) { + return true; + } if (TextUtils.isEmpty(senderLabel)) { return false; } diff --git a/android/app/src/test/java/com/hyzq/boss/BossApiClientDispatchPlansTest.java b/android/app/src/test/java/com/hyzq/boss/BossApiClientDispatchPlansTest.java index 3818e61..eeedb54 100644 --- a/android/app/src/test/java/com/hyzq/boss/BossApiClientDispatchPlansTest.java +++ b/android/app/src/test/java/com/hyzq/boss/BossApiClientDispatchPlansTest.java @@ -119,6 +119,27 @@ public class BossApiClientDispatchPlansTest { ); } + @Test + public void rememberIdentityDoesNotOverwriteSessionIdentityFromAiAccountOnboardingResponse() throws Exception { + InMemorySharedPreferences prefs = new InMemorySharedPreferences(); + prefs.edit() + .putString("account", "17600003315") + .putString("display_name", "Boss 超级管理员") + .apply(); + BossApiClient apiClient = new BossApiClient(prefs, "https://boss.hyzq.net"); + + JSONObject onboardingResponse = new JSONObject() + .put("ok", true) + .put("accountId", "openai-api-primary") + .put("displayName", "OpenAI 平台账号") + .put("message", "OpenAI 平台账号已登录,并设为当前主控。"); + + apiClient.rememberIdentity(onboardingResponse); + + assertEquals("17600003315", apiClient.getAccountLabel()); + assertEquals("Boss 超级管理员", apiClient.getDisplayName()); + } + @Test public void onboardMasterNodeFallsBackToGenericAccountCreationWhenDedicatedRouteMissing() throws Exception { RecordingConnection dedicated = new RecordingConnection( diff --git a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java index 585a98e..d313738 100644 --- a/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java +++ b/android/app/src/test/java/com/hyzq/boss/ProjectDetailActivityUiTest.java @@ -6,6 +6,7 @@ import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertNotNull; import android.content.Intent; +import android.content.SharedPreferences; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; @@ -24,6 +25,11 @@ import org.robolectric.annotation.Config; import org.robolectric.shadows.ShadowDialog; import org.robolectric.util.ReflectionHelpers; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; + @RunWith(RobolectricTestRunner.class) @Config(sdk = 34) public class ProjectDetailActivityUiTest { @@ -191,6 +197,41 @@ public class ProjectDetailActivityUiTest { assertFalse(viewTreeContainsText(attachmentView, "已分析")); } + @Test + public void userMessageRemainsOutgoingWhenAiAccountDisplayNameDiffers() throws Exception { + Intent intent = new Intent() + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_ID, "master-agent") + .putExtra(ProjectDetailActivity.EXTRA_PROJECT_NAME, "主 Agent"); + TestProjectDetailActivity activity = Robolectric + .buildActivity(TestProjectDetailActivity.class, intent) + .setup() + .get(); + + InMemorySharedPreferences prefs = new InMemorySharedPreferences(); + prefs.edit() + .putString("account", "17600003315") + .putString("display_name", "OpenAI 平台账号") + .apply(); + ReflectionHelpers.setField(activity, "apiClient", new BossApiClient(prefs, "https://boss.hyzq.net")); + + JSONObject message = new JSONObject() + .put("id", "msg-user-1") + .put("sender", "user") + .put("senderLabel", "Boss 超级管理员") + .put("body", "请只回复一句:聊天链路自检正常。") + .put("kind", "text") + .put("sentAt", "2026-03-31T10:26:00.000Z"); + + View messageView = ReflectionHelpers.callInstanceMethod( + activity, + "buildMessageView", + ReflectionHelpers.ClassParameter.from(JSONObject.class, message) + ); + + assertTrue(viewTreeContainsText(messageView, "10:26")); + assertFalse(viewTreeContainsText(messageView, "Boss 超级管理员 · 10:26")); + } + @Test public void outgoingAttachmentMetaPrefersTimeOnly() throws Exception { Intent intent = new Intent() @@ -384,4 +425,137 @@ public class ProjectDetailActivityUiTest { return false; } } + + private static final class InMemorySharedPreferences implements SharedPreferences { + private final Map values = new HashMap<>(); + + @Override + public Map getAll() { + return Collections.unmodifiableMap(values); + } + + @Override + public String getString(String key, String defValue) { + Object value = values.get(key); + return value instanceof String ? (String) value : defValue; + } + + @Override + public Set getStringSet(String key, Set defValues) { + Object value = values.get(key); + return value instanceof Set ? (Set) 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() { + private final Map staged = new HashMap<>(); + private boolean clearRequested = false; + + @Override + public Editor putString(String key, String value) { + staged.put(key, value); + return this; + } + + @Override + public Editor putStringSet(String key, Set value) { + staged.put(key, value); + return this; + } + + @Override + public Editor putInt(String key, int value) { + staged.put(key, value); + return this; + } + + @Override + public Editor putLong(String key, long value) { + staged.put(key, value); + return this; + } + + @Override + public Editor putFloat(String key, float value) { + staged.put(key, value); + return this; + } + + @Override + public Editor putBoolean(String key, boolean value) { + staged.put(key, value); + return this; + } + + @Override + public Editor remove(String key) { + staged.put(key, null); + return this; + } + + @Override + public Editor clear() { + clearRequested = true; + staged.clear(); + return this; + } + + @Override + public boolean commit() { + apply(); + return true; + } + + @Override + public void apply() { + if (clearRequested) { + values.clear(); + } + for (Map.Entry entry : staged.entrySet()) { + if (entry.getValue() == null) { + values.remove(entry.getKey()); + } else { + values.put(entry.getKey(), entry.getValue()); + } + } + } + }; + } + + @Override + public void registerOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {} + + @Override + public void unregisterOnSharedPreferenceChangeListener(OnSharedPreferenceChangeListener listener) {} + } }