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 356708d..0c20a43 100644 --- a/android/app/src/main/java/com/hyzq/boss/BossApiClient.java +++ b/android/app/src/main/java/com/hyzq/boss/BossApiClient.java @@ -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()) { 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 624fae6..c0b587e 100644 --- a/android/app/src/test/java/com/hyzq/boss/BossApiClientDispatchPlansTest.java +++ b/android/app/src/test/java/com/hyzq/boss/BossApiClientDispatchPlansTest.java @@ -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")); diff --git a/public/downloads/boss-android-latest.apk b/public/downloads/boss-android-latest.apk index 5089bc9..fa9a242 100644 Binary files a/public/downloads/boss-android-latest.apk and b/public/downloads/boss-android-latest.apk differ diff --git a/public/downloads/boss-android-latest.json b/public/downloads/boss-android-latest.json index e37a1e1..004caa1 100644 --- a/public/downloads/boss-android-latest.json +++ b/public/downloads/boss-android-latest.json @@ -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" diff --git a/public/downloads/boss-android-v2.5.11-release.apk b/public/downloads/boss-android-v2.5.11-release.apk index 5089bc9..fa9a242 100644 Binary files a/public/downloads/boss-android-v2.5.11-release.apk and b/public/downloads/boss-android-v2.5.11-release.apk differ diff --git a/src/lib/boss-projections.ts b/src/lib/boss-projections.ts index 31cf8cb..c244baf 100644 --- a/src/lib/boss-projections.ts +++ b/src/lib/boss-projections.ts @@ -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, diff --git a/tests/conversation-home-items.test.ts b/tests/conversation-home-items.test.ts index bbba353..204787f 100644 --- a/tests/conversation-home-items.test.ts +++ b/tests/conversation-home-items.test.ts @@ -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();