diff --git a/CHANGELOG.md b/CHANGELOG.md index 288683da..6a8763b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,10 @@ # 印度占星 Skill 更新日志 +## 2026-10-28 — 只知道出生时段时,咨询出字更快 + +- 只填了出生时段(没有精确到分钟)的人来咨询时,后台两次计算改成同时进行,每轮约快 0.3~1 秒。回答内容不变。 +- Skill 版本不变(BUG-1473,issue #128)。 + ## 2026-10-28 — 回答出字时页面不再整页重画 - 回答逐字出来时,只有回答那一块在更新;顶栏、输入框和右侧本命盘不再每一帧跟着重画(以前最多每秒 60 次)。长回答时电脑更省力,滚动和点按更跟手。看到的内容不变。 diff --git a/docs/BUG_HISTORY.md b/docs/BUG_HISTORY.md index b15409ef..00fdac09 100644 --- a/docs/BUG_HISTORY.md +++ b/docs/BUG_HISTORY.md @@ -20096,3 +20096,18 @@ - 关联:BUG-1329(侧栏每帧重注册)、BUG-1130。 - 复发自:无 - 修复版本:分支 `codex/home-stream-rerender-20261028`。 + +## BUG-1473 | 按出生时段咨询时,窗口排盘和时段读数排队串行 + +- 状态:resolved(代码 + 回归测试;待部署) +- 首次发现 / 最近更新:2026-10-28 / 2026-10-28(issue #118 代码体检第 11 条 → #128) +- 影响面:只知道出生时段(没有精确出生分钟)的咨询;`frontend/src/mastra/consultation-tools.ts` `executeWindowConsultation`。 +- 现象:这类咨询每轮在出字前多等一段:先等 `/api/declared_window_chart` 回来,再发 `/api/rectification/v5/range_reading`。估每轮 0.3~1 秒。 +- 触发条件:任何按出生时段(declared birth window)走的咨询轮。 +- 根因:两个请求互不依赖,代码却先 `await` 第一个再发第二个。 +- 修复:时段读数先发出、窗口排盘随后发出,两者同时进行,再一起等结果。时段读数失败仍只是没有「分钟敏感主题」,与改前相同;窗口排盘失败时整轮照旧失败,只是此时时段读数已经发出(结果丢弃)。 +- 验证:`frontend/tests/consultation-agentic-runtime.test.ts` 新增两条:两个桩各延时 150 ms,改后总耗时 <260 ms(改前约 300 ms,改前代码上该测试失败),且窗口排盘返回前时段读数已开始;`minuteSensitiveThemes` 与改前一致。时段读数抛错时照常完成、不带分钟敏感主题。同文件既有窗口咨询测试(BUG-957 三条)全过。tsc 0、lint 0。 +- 防复发:上述测试。互不依赖的后端请求同时发。 +- 关联:BUG-957(窗口咨询预计算与缓存)。 +- 复发自:无 +- 修复版本:分支 `codex/window-consult-parallel-20261028`。 diff --git a/frontend/src/mastra/consultation-tools.ts b/frontend/src/mastra/consultation-tools.ts index 7bf15248..6613c877 100644 --- a/frontend/src/mastra/consultation-tools.ts +++ b/frontend/src/mastra/consultation-tools.ts @@ -1620,12 +1620,9 @@ async function executeWindowConsultation( label: "正在比较声明出生窗口内的稳定层", }, }); - const packet = await (ctx.fetchWindowChart ?? fetchDeclaredWindowChart)({ - window: ctx.declaredWindow, - signal: context.abortSignal ?? ctx.abortSignal, - }); - let minuteSensitiveThemes: string[] = []; - try { + // The range reading does not depend on the window chart, so both go out at once + // (issue #128). A failed reading still only drops the minute-sensitive themes. + const sensitiveThemes = (async () => { const reading = await (ctx.runRangeReading ?? runV9RangeReading)({ baselineBirthSnapshot: { birth_date: ctx.declaredWindow.truth.birthDate, @@ -1640,10 +1637,13 @@ async function executeWindowConsultation( }, birthTimeAccuracy: "approximate", }); - minuteSensitiveThemes = [...(reading?.sensitiveThemes ?? [])]; - } catch { - minuteSensitiveThemes = []; - } + return [...(reading?.sensitiveThemes ?? [])]; + })().catch((): string[] => []); + const packet = await (ctx.fetchWindowChart ?? fetchDeclaredWindowChart)({ + window: ctx.declaredWindow, + signal: context.abortSignal ?? ctx.abortSignal, + }); + const minuteSensitiveThemes = await sensitiveThemes; const varyingLagna = Array.isArray(packet.varying_layers.ascendant_signs) && packet.varying_layers.ascendant_signs.length > 1; ctx.state.workflowReceipt = { diff --git a/frontend/tests/consultation-agentic-runtime.test.ts b/frontend/tests/consultation-agentic-runtime.test.ts index 13204d55..89de4d22 100644 --- a/frontend/tests/consultation-agentic-runtime.test.ts +++ b/frontend/tests/consultation-agentic-runtime.test.ts @@ -1636,7 +1636,12 @@ function windowPacket() { }; } -function makeWindowCtx(options: { fetchWindowChart?: () => Promise> } = {}) { +function makeWindowCtx(options: { + fetchWindowChart?: () => Promise>; + runRangeReading?: () => Promise<{ + stableThemes: string[]; sensitiveThemes: string[]; claimBoundary: null; themeSensitivity: Record; window: null; + }>; +} = {}) { const state = createConsultationRuntimeState(); const ctx = createWindowConsultationAgentContext({ userId: "u", @@ -1646,9 +1651,9 @@ function makeWindowCtx(options: { fetchWindowChart?: () => Promise windowPacket()), - runRangeReading: async () => { + runRangeReading: options.runRangeReading ?? (async () => { throw new Error("range-reading-unused-in-this-test"); - }, + }), }); return { ctx, state, tools: createWindowConsultationTools(ctx) }; } @@ -1730,6 +1735,41 @@ test("window precompute and a later tool call share one cache (BUG-957)", async assert.deepEqual(first, second); }); +test("the window chart and the range reading go out together (issue #128)", async () => { + const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); + let rangeStarted = false; + let rangeStartedBeforeChartReturned = false; + const { ctx, state } = makeWindowCtx({ + fetchWindowChart: async () => { + await delay(150); + rangeStartedBeforeChartReturned = rangeStarted; + return windowPacket(); + }, + runRangeReading: async () => { + rangeStarted = true; + await delay(150); + return { stableThemes: [], sensitiveThemes: ["career"], claimBoundary: null, themeSensitivity: {}, window: null }; + }, + }); + const startedAt = Date.now(); + await precomputeWindowConsultation(ctx, { question: "事业如何" }); + const elapsed = Date.now() - startedAt; + assert.equal(rangeStartedBeforeChartReturned, true); + // Serial was 300 ms; together it is about the longer of the two. + assert.ok(elapsed < 260, `took ${elapsed} ms`); + assert.deepEqual(state.workflowReceipt?.minuteSensitiveThemes, ["career"]); + assert.equal(state.consultationToolCompleted, true); +}); + +test("a failed range reading still only drops the minute-sensitive themes (issue #128)", async () => { + const { ctx, state } = makeWindowCtx({ + runRangeReading: async () => { throw new Error("range_unavailable"); }, + }); + await precomputeWindowConsultation(ctx, { question: "事业如何" }); + assert.equal(state.consultationToolCompleted, true); + assert.equal(state.workflowReceipt?.minuteSensitiveThemes, undefined); +}); + test("window precompute failure still degrades when the model writes without a tool (BUG-957)", async () => { const { ctx, state } = makeWindowCtx({ fetchWindowChart: async () => {