From 2488956f36d9f3f43eb2e992277061e5ea2a2cd3 Mon Sep 17 00:00:00 2001 From: ericwyuan Date: Tue, 1 Sep 2026 15:41:28 +0800 Subject: [PATCH] =?UTF-8?q?fix(ai):=20=E6=8A=98=E5=8F=A0=E6=80=81=E5=8F=AA?= =?UTF-8?q?=E6=98=BE=E7=A4=BA=E4=B8=80=E5=8F=A5=E8=AF=9D=EF=BC=8C=E9=82=A3?= =?UTF-8?q?=E5=8F=A5=E8=AF=9D=E5=BE=97=E8=AF=B4=E6=B8=85=E6=98=AF=E5=93=AA?= =?UTF-8?q?=E4=B8=AA=E6=8C=87=E6=A0=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 精简版的卡片正文就是第一条 highlight,标题不渲染。于是趋势页折叠起来是 「395 天内由 4376.0分 到 4905.0分(改善)」——什么的 4376 分?身体成分、 运动详情同样。 每个 builder 的第一条 detail 现在都自带主语,并加了不变量测试:detail 必须 包含它自己的 title(纯汇总性标题除外)。 Co-Authored-By: Claude Opus 5 --- backend/services/coach.py | 4 ++++ backend/services/scopes.py | 13 ++++++++----- backend/tests/test_coach.py | 20 ++++++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/backend/services/coach.py b/backend/services/coach.py index a915c2c..0661d67 100644 --- a/backend/services/coach.py +++ b/backend/services/coach.py @@ -495,6 +495,10 @@ def rule_scope_insight(context): return { # The first highlight leads and is then dropped from the list: showing # it in both places printed the same sentence twice. + # + # This is why every builder's first `detail` has to read on its own — + # here the `title` beside it is not rendered, and "395 天内由 4376 到 + # 4905(改善)" without naming 耐力分 is a sentence about nothing. "headline": highlights[0]["detail"] if highlights else None, "points": [dict(h) for h in highlights[1:6]], "actions": [], diff --git a/backend/services/scopes.py b/backend/services/scopes.py index e06cc63..94b69fd 100644 --- a/backend/services/scopes.py +++ b/backend/services/scopes.py @@ -280,7 +280,7 @@ def build_trends(user_id, subject=None): highlights = [{ "title": t["label"], "detail": ( - f"{t['days']} 天内由 {t['firstMean']}{t['unit']} 到 " + f"{t['label']} {t['days']} 天内由 {t['firstMean']}{t['unit']} 到 " f"{t['lastMean']}{t['unit']}({t['direction']})," f"斜率约 {t['slopePer30d']}{t['unit']}/30 天。" ), @@ -332,7 +332,7 @@ def build_daily(user_id, subject=None): "detail": f"{d['label']} {d['value']}{d['unit']},偏离基线 " f"{abs(d['z']):.1f} 个标准差。", } for d in notable[:4]] or [ - {"title": date, "detail": "这一天各项指标都在常态范围内。"} + {"title": date, "detail": f"{date} 各项指标都在常态范围内。"} ] return date, { @@ -370,7 +370,8 @@ def build_body(user_id, subject=None): continue highlights.append({ "title": label, - "detail": f"{first['date']} {a}{unit} → {last['date']} {b}{unit}" + "detail": f"{label}从 {first['date']} 的 {a}{unit} 到 " + f"{last['date']} 的 {b}{unit}" f"({'+' if b >= a else ''}{round(b - a, 1)}{unit})。", }) elif series: @@ -522,9 +523,11 @@ def build_activity(user_id, subject=None): total_zone = sum(z.get("seconds") or 0 for z in zones) minutes = round((base.get("duration") or summary.get("duration") or 0) / 60) + sport = (base.get("activity_type") or (detail or {}).get("activityType") + or "运动") highlights = [{ - "title": base.get("activity_type") or (detail or {}).get("activityType") or "运动", - "detail": f"{minutes} 分钟" + "title": sport, + "detail": f"{sport} {minutes} 分钟" + (f",{round((base.get('distance') or 0) / 1000, 2)} km" if base.get("distance") else "") + (f",平均心率 {base.get('heart_rate_average')}" diff --git a/backend/tests/test_coach.py b/backend/tests/test_coach.py index 5fdb6c2..a8175a5 100644 --- a/backend/tests/test_coach.py +++ b/backend/tests/test_coach.py @@ -1053,3 +1053,23 @@ class TestSupersede: statuses = {r["subject"]: r["status"] for r in analysis_svc.query_all( "SELECT subject, status FROM ai_jobs WHERE kind = 'trends'")} assert statuses.get("2026-08-01") == "running" + + +class TestHighlightsReadAlone: + """The first highlight becomes the collapsed card's whole text, with no + title beside it — so it has to name what it is about.""" + + def test_every_first_highlight_names_its_subject(self, month): + for name in scopes.SCOPES: + built = scopes.build(month["id"], name) + if not built: + continue + first = built[1]["highlights"][0] + title = first["title"] + # Titles that are already summaries rather than names of a metric. + if title in ("整体", "当前预测") or title.startswith("近 "): + continue + assert title in first["detail"], ( + f"{name}: 折叠态只显示 detail,但它没提到 {title!r}: " + f"{first['detail']!r}" + )