fix: preserve user chat messages after ai onboarding

This commit is contained in:
kris
2026-03-31 16:30:21 +08:00
parent 70494fc15b
commit 71d0979292
4 changed files with 223 additions and 4 deletions

View File

@@ -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)

View File

@@ -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;
}

View File

@@ -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(

View File

@@ -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<String, Object> values = new HashMap<>();
@Override
public Map<String, ?> 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<String> getStringSet(String key, Set<String> defValues) {
Object value = values.get(key);
return value instanceof Set ? (Set<String>) 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<String, Object> 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<String> 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<String, Object> 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) {}
}
}