diff --git a/CHANGELOG.md b/CHANGELOG.md index ba5352e8..5e21d697 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 印度占星 Skill 更新日志 +## 2026-10-07 — 新对话里换模型不再报「云端同步失败」(未上线) + +- 新建对话还没发第一句时换模型,以前会报「云端同步失败」,其实模型已经换好。现在只在本机记下所选模型,发第一句时一起保存,不再报错(BUG-1259)。 + ## 2026-10-07 — 生时校正改从对话里开始,做完回到原来的对话(未上线) - 首页不再有「生时校正」按钮(BUG-1247)。在对话里说「你说的那年其实没发生」「我出生时间是我妈估的」这类话时,agent 会在回答下面放一张卡,写着大约几题、几点;只是不认同某个解释时不会提。给家人档案问时会说暂不支持(BUG-1248)。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index 03ad0fb2..730cbb70 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -16904,7 +16904,7 @@ ## BUG-1259 | 新对话里还没发第一句就换模型,提示「云端同步失败」 -- 状态:investigating(根因已确认,未修) +- 状态:fixed-pending-verify(真机复核前不标 resolved) - 首次发现 / 最近更新:2026-10-07 / 2026-10-07 - 来源:产品 iPhone 真机截图(staging 首页,新对话未发消息时切到 gpt-5.6-luna)。 - 影响面:`use-session-management.ts` 的 `selectSessionModel` → `patchSessionModel` → `PATCH /api/sessions/{id}`;首页 / 新建的空对话。 @@ -16912,6 +16912,8 @@ - 触发条件:会话还没发过第一句(本地未落库的空咨询)时切换模型。 - 根因:BUG-989(2026-09-21)把「新建对话」改成只在本地创建、第一问时才 `POST /api/sessions` 落库;`selectSessionModel` 没跟着改,仍对这个还不存在于服务器的会话发 `PATCH`,路由查不到行返回 404「聊天记录不存在或已被删除」,前端包装成「云端同步失败」。本地选择本身已生效(第一问落库时 `model_id` 取自本地会话),报错是多余的,但会让用户以为没切成功。 - 复发自:无;BUG-989 漏改的调用点。BUG-989 的源码合同只锁了「新建无 POST / send 时 create」,没覆盖换模型。 -- 修复:待定(候选:未落库会话换模型只改本地,不发 PATCH;第一问落库时带上所选模型——现有 create 已带)。 +- 修复:`selectSessionModel` 在本地更新后,若会话仍是未落库的空咨询(与发送路径同一判据 `isUnsavedEmptyConsultation` 且不在 `cloudCreatedIds`),清掉该会话的同步失败标记后直接返回,不发 `PATCH`。第一问落库的 create 本来就写 `model_id: session.modelId`,所选模型随落库生效。已落库会话照旧同步。启动 / 回访时的模型下线回退(`applyWarmCatalog`)早已跳过未落库会话,未改。 +- 验证:`session-list-filter.test.ts` 新增 1 条源码合同(守卫在任何 PATCH 之前、清失败标记并 return、create 写入 `model_id`);去掉修复后该测试失败、恢复后通过。全量 4988 项 fail 24,与 `11333460` 逐条一致;tsc 0、lint 0 error;build `/` Static,首屏 gzip 568,343 → 568,362 B。 +- 防复发:上述测试;以后凡是对会话发 PATCH 的入口,先判断会话是否已落库(BUG-989 的「第一问才落库」)。 - 相关记录:BUG-989、BUG-928。 -- 修复版本:— +- 修复版本:待发布 diff --git a/frontend/src/hooks/use-session-management.ts b/frontend/src/hooks/use-session-management.ts index 9b0c3f11..2e9b63c9 100644 --- a/frontend/src/hooks/use-session-management.ts +++ b/frontend/src/hooks/use-session-management.ts @@ -506,6 +506,14 @@ export function useSessionManagement(params: SessionManagementParams) { if (!retryingFailedSync) updateSession(activeSession.id, () => nextSession); setRequestError(null); setComposerNotice(""); + // BUG-1259: a new chat is saved on its first send (BUG-989) and that create + // already carries the session's model; there is no row to patch yet, so + // the choice stays local instead of reporting a sync failure. + if (isUnsavedEmptyConsultation(nextSession) && !cloudCreatedIds.current.has(nextSession.id)) { + modelSelectionVersions.current.delete(nextSession.id); + modelSyncFailures.current.delete(nextSession.id); + return; + } try { await modelPersistence.current.enqueue(nextSession.id, () => persistSessionModelSelection( diff --git a/frontend/tests/session-list-filter.test.ts b/frontend/tests/session-list-filter.test.ts index a38875db..aeb5de28 100644 --- a/frontend/tests/session-list-filter.test.ts +++ b/frontend/tests/session-list-filter.test.ts @@ -131,3 +131,22 @@ test("new chat stays local until the first send and does not reuse a draft row", assert.match(persistBlock, /setRequestError/); assert.doesNotMatch(persistBlock, /setDraft\(/); }); + +test("switching the model on a chat not saved yet stays local and never patches (BUG-1259)", () => { + // BUG-989 saves a new chat on its first send; a PATCH before that hits no + // row (404) and was shown as 「云端同步失败」. + const start = management.indexOf("async function selectSessionModel"); + const end = management.indexOf("\n }\n", start); + assert.ok(start >= 0 && end > start); + const body = management.slice(start, end); + const guard = body.indexOf("if (isUnsavedEmptyConsultation(nextSession) && !cloudCreatedIds.current.has(nextSession.id))"); + const patch = body.indexOf("patchSessionModel("); + assert.ok(guard >= 0, "the unsaved-chat guard exists"); + assert.ok(patch > guard, "the guard runs before any PATCH"); + const guarded = body.slice(guard, body.indexOf("}", guard)); + assert.match(guarded, /modelSyncFailures\.current\.delete\(nextSession\.id\)/); + assert.match(guarded, /return;/); + // The local choice is what the first-send create writes. + const create = management.slice(management.indexOf("const values = mode === \"create\""), management.indexOf(": {", management.indexOf("const values = mode === \"create\"") + 40)); + assert.match(create, /model_id: session\.modelId/); +});