Fix stale conversation sync labels on mobile

This commit is contained in:
kris
2026-04-07 13:35:52 +08:00
parent a43bb92f3c
commit 6153e94000
7 changed files with 76 additions and 4 deletions

View File

@@ -667,6 +667,10 @@ public class BossApiClient {
connection.setDoInput(true);
connection.setRequestProperty("Accept", "application/json");
connection.setRequestProperty("x-boss-native-app", "1");
if ("GET".equals(method)) {
connection.setRequestProperty("Cache-Control", "no-cache, no-store, max-age=0");
connection.setRequestProperty("Pragma", "no-cache");
}
String cookie = getSessionCookie();
if (!cookie.isEmpty()) {

View File

@@ -51,6 +51,8 @@ public class BossApiClientDispatchPlansTest {
assertEquals("GET", connection.requestMethodValue);
assertEquals(12000, connection.connectTimeoutValue);
assertEquals(30000, connection.readTimeoutValue);
assertEquals("no-cache, no-store, max-age=0", connection.getRequestProperty("Cache-Control"));
assertEquals("no-cache", connection.getRequestProperty("Pragma"));
}
@Test
@@ -67,6 +69,18 @@ public class BossApiClientDispatchPlansTest {
assertEquals(30000, connection.readTimeoutValue);
}
@Test
public void getConversationHomeSendsNoCacheHeadersToAvoidStaleMobileFeed() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/conversations/home"));
RecordingBossApiClient apiClient = new RecordingBossApiClient(connection);
BossApiClient.ApiResponse response = apiClient.getConversationHome();
assertEquals(200, response.statusCode);
assertEquals("no-cache, no-store, max-age=0", connection.getRequestProperty("Cache-Control"));
assertEquals("no-cache", connection.getRequestProperty("Pragma"));
}
@Test
public void confirmDispatchPlanWritesApprovedTargetProjectIds() throws Exception {
RecordingConnection connection = new RecordingConnection(new URL("https://boss.hyzq.net/api/v1/projects/p1/dispatch-plans/plan-1/confirm"));

View File

@@ -1,9 +1,9 @@
{
"fileName": "boss-android-v2.5.11-release.apk",
"urlPath": "/api/v1/user/ota/package",
"sizeBytes": 3351549,
"updatedAt": "2026-04-06T05:25:05Z",
"sha256": "67a66ea104696d639d576738cdef4ef485efb76d12d63b1b138e59dab091ef00",
"sizeBytes": 3351826,
"updatedAt": "2026-04-07T05:34:00Z",
"sha256": "2ea5fd0b33304f04b3f8f0ba66ec8f3ff40e2e8699c1d989de71ba08c9ab7e77",
"versionName": "2.5.11",
"versionCode": 24,
"buildFlavor": "release"

View File

@@ -180,6 +180,20 @@ export function formatTimestampLabel(value?: string, fallback = "刚刚") {
return shanghaiDayFormatter.format(date);
}
const STALE_CONTEXT_SYNC_LABEL = "待同步";
const STALE_CONTEXT_REPLY_THRESHOLD_MS = 7 * 24 * 60 * 60_000;
function formatConversationLatestReplyLabel(value: string, hasVisibleContext: boolean) {
if (hasVisibleContext && value.includes("T")) {
const date = new Date(value);
const diff = Date.now() - date.getTime();
if (!Number.isNaN(date.getTime()) && diff >= STALE_CONTEXT_REPLY_THRESHOLD_MS) {
return STALE_CONTEXT_SYNC_LABEL;
}
}
return formatTimestampLabel(value);
}
function compareSnapshots(a: ThreadContextSnapshot, b: ThreadContextSnapshot) {
if (a.mustFinishBeforeCompaction !== b.mustFinishBeforeCompaction) {
return a.mustFinishBeforeCompaction ? -1 : 1;
@@ -386,7 +400,10 @@ function buildConversationItem(state: BossState, project: Project): Conversation
topPinnedLabel,
manualPinned: Boolean(project.pinned && !project.systemPinned),
latestReplyAt: latestConversationActivityAt,
latestReplyLabel: formatTimestampLabel(latestConversationActivityAt),
latestReplyLabel: formatConversationLatestReplyLabel(
latestConversationActivityAt,
Boolean(topThread),
),
unreadCount: project.unreadCount,
riskLevel: project.riskLevel,
activeDeviceCount: devices.length,

View File

@@ -818,6 +818,43 @@ test("conversation items prefer latest observed codex activity over stale last m
assert.equal(thread?.latestReplyLabel, formatTimestampLabel("2026-04-04T11:48:00+08:00"));
});
test("conversation items mark stale context-backed timestamps as waiting for sync", async () => {
await setup();
const state = await readState();
state.projects = state.projects.filter((project) => project.id === "master-agent");
state.projects.push({
...buildImportedThreadProject(
"mac-studio",
"stale-context-project",
"Boss",
"boss",
"上下文老化线程",
"thread-stale",
"2026-03-25T10:58:00+08:00",
),
preview: "老上下文还在挂着",
unreadCount: 1,
});
state.threadContextSnapshots.push(
buildThreadContextSnapshot(
"stale-context-project",
"thread-stale",
"上下文老化线程",
"watch",
false,
58,
"2026-03-25T10:58:00+08:00",
"mac-studio",
),
);
const item = getConversationHomeItems(state).find((entry) => entry.projectId === "stale-context-project");
assert.ok(item, "expected stale context conversation");
assert.equal(item?.contextBudgetIndicator.visible, true);
assert.equal(item?.latestReplyLabel, "待同步");
});
test("default seeded conversations no longer expose Boss 移动控制台", async () => {
await setup();
const state = await readState();