fix(ai): subject 里塞了行数,每轮询一次就新建一个任务
生产上 trends 队列里积了 36 个任务,subject 是 2026-09-01:1033、:1039、 :1044……一路涨。这台账号当时正在补历史,get_summary 的行数每隔几分钟就变, 而我把 len(rows) 写进了 subject——subject 同时是缓存键和任务队列的键,一变 就是一条全新的任务,轮询几次就刷出十几条。 subject 该回答的是「这条解读是关于什么的」,不是「当时有多少行数据」。 数据变化本来就由 fingerprint 负责。 - trends 的 subject 改成快照日期;sleep 用配置的窗口常量而不是实际夜数 (缺一晚也不该换键);challenges 用固定键 - 加了不变量测试:补一天历史数据后 subject 不许变;任何 subject 段都不许 长得像行数 顺带加一层兜底 jobs.supersede():单实例 scope 只该有一个在跑的 subject, 队列里同 kind 的其它 pending 任务是关于已经不存在的快照的,跑完也没人看。 per_item 的 daily / activity 不受影响——它们本来就一天一条、一次运动一条。 兜底不是机制,机制是 subject 稳定;它存在只是因为这次 subject 不稳定,而 36 条任务堆在那里之前没人发现。 顺带按要求把 AiPanel 改成默认精简:只显示标题、来源和一句话结论,点「展开 详细」才出要点/建议/依据,可再收起——和今日晨报卡片一致。这些面板压在本来 就很密的图表页上面,全部默认展开会把真正的数据一次性挤到屏幕外。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -85,6 +85,36 @@
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.aip-detail {
|
||||
border-top: 1px solid var(--border);
|
||||
padding-top: 0.7rem;
|
||||
margin-top: 0.5rem;
|
||||
animation: aip-open 0.26s var(--ease) both;
|
||||
}
|
||||
|
||||
@keyframes aip-open {
|
||||
from { opacity: 0; transform: translateY(-4px); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
/* Full-width, at the foot of the card: the same affordance the 今日 briefing
|
||||
uses, so the two read as one pattern rather than two. */
|
||||
.aip-toggle {
|
||||
display: block;
|
||||
width: 100%;
|
||||
background: none;
|
||||
border: none;
|
||||
border-top: 1px solid var(--border);
|
||||
margin-top: 0.5rem;
|
||||
padding: 0.5rem 0 0.1rem;
|
||||
font-size: 0.76rem;
|
||||
font-weight: 600;
|
||||
color: var(--text-muted);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.aip-toggle:active { color: var(--accent); }
|
||||
|
||||
.aip-point { margin-bottom: 0.55rem; }
|
||||
|
||||
.aip-tag {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useCallback } from 'react';
|
||||
import { useCallback, useState } from 'react';
|
||||
import {
|
||||
apiClient, InsightScope, InsightMeta, ScopeInsight,
|
||||
} from '../services/api';
|
||||
@@ -48,6 +48,13 @@ function Badge({ meta }: { meta: InsightMeta }) {
|
||||
* screen — which opening the screen moves to the front of.
|
||||
*/
|
||||
function AiPanel({ scope, subject, title = 'AI 解读' }: Props) {
|
||||
/* Collapsed by default, the same as the 今日 briefing card. These panels sit
|
||||
on top of screens that are already dense with charts; opening every one of
|
||||
them by default would push the actual data below the fold on every screen
|
||||
at once. The conclusion is always visible — it is the detail behind it
|
||||
that waits to be asked for. */
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const load = useCallback(async () => {
|
||||
const resp = await apiClient.getInsight(scope, { subject });
|
||||
return { data: resp.insight, meta: resp.meta };
|
||||
@@ -71,27 +78,42 @@ function AiPanel({ scope, subject, title = 'AI 解读' }: Props) {
|
||||
|
||||
{data.headline && <p className="aip-headline">{data.headline}</p>}
|
||||
|
||||
{data.points.map((p) => (
|
||||
<div className="aip-point" key={p.title + p.detail}>
|
||||
<span className="aip-tag">{p.title}</span>
|
||||
<p>{p.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
{open && (
|
||||
<div className="aip-detail">
|
||||
{data.points.map((p) => (
|
||||
<div className="aip-point" key={p.title + p.detail}>
|
||||
<span className="aip-tag">{p.title}</span>
|
||||
<p>{p.detail}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{!!data.actions.length && (
|
||||
<ul className="aip-actions">
|
||||
{data.actions.map((a) => <li key={a}>{a}</li>)}
|
||||
</ul>
|
||||
{!!data.actions.length && (
|
||||
<ul className="aip-actions">
|
||||
{data.actions.map((a) => <li key={a}>{a}</li>)}
|
||||
</ul>
|
||||
)}
|
||||
|
||||
{data.caution && <p className="aip-caution">{data.caution}</p>}
|
||||
|
||||
<div className="aip-foot">
|
||||
<button className="aip-link" onClick={refresh} disabled={loading}>
|
||||
{loading ? '重新生成中…' : '重新生成'}
|
||||
</button>
|
||||
<span>{CONFIDENCE_LABEL[data.confidence]}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{data.caution && <p className="aip-caution">{data.caution}</p>}
|
||||
|
||||
<div className="aip-foot">
|
||||
<button className="aip-link" onClick={refresh} disabled={loading}>
|
||||
{loading ? '重新生成中…' : '重新生成'}
|
||||
{/* Nothing to expand into when the reading is a headline on its own. */}
|
||||
{(!!data.points.length || !!data.actions.length || !!data.caution) && (
|
||||
<button
|
||||
className="aip-toggle"
|
||||
onClick={() => setOpen((v) => !v)}
|
||||
aria-expanded={open}
|
||||
>
|
||||
{open ? '收起' : `展开详细(${data.points.length} 项)`}
|
||||
</button>
|
||||
<span>{CONFIDENCE_LABEL[data.confidence]}</span>
|
||||
</div>
|
||||
)}
|
||||
</section>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user