fix(web): keep consultation conclusions across turns and surface cache hits (BUG-555, BUG-556)

Session history was silently clipped to the first 4000 characters of the last 12 messages, so follow-ups could not see timing or audit tables. Keep an append-only tail plus a checkpoint summary, retry overflow in the same request, and expose cache hit rate in admin usage.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Jesse_Chen
2026-09-06 15:16:18 +08:00
co-authored by Cursor
parent 9ce7a374ed
commit bf8ad0d1ff
29 changed files with 1295 additions and 100 deletions
@@ -415,6 +415,7 @@ type Usage = {
outputTokens: number;
costMicrousd: number;
durationMs: number | null;
cacheReadTokens: number | null;
createdAt: string;
};
@@ -452,6 +453,11 @@ export function UsageResource() {
render: (_, item) =>
`${item.inputTokens.toLocaleString()} / ${item.outputTokens.toLocaleString()}`,
},
{
title: "缓存读 tokens",
dataIndex: "cacheReadTokens",
render: (value: number | null) => value == null ? "—" : value.toLocaleString(),
},
{
title: "成本",
dataIndex: "costMicrousd",
@@ -27,7 +27,19 @@ const BANDS: DistributionBand[] = [
type ModelRow = ModelPrice & { modelId: string; status: string; enabled: boolean };
type PricingRow = { featureKey: string; modelTier: string; creditCost: number; status: string; enabled: boolean };
type Product = { code: string; priceCents: number; enabled: boolean; status: string; entitlements: Array<{ featureKey: string; metadata?: { minuteLimit?: number | null; dayLimit?: number | null; billingLimit?: number | null } }> };
type UsagePayload = { data: Array<{ featureKey: string; hasData: boolean; metrics: { costMicrousd: Metric; inputTokens: Metric; outputTokens: Metric; durationMs: Metric } }> };
type CacheModelRow = {
actualModelId: string | null;
runsWithCache: number;
hitRate: number | null;
readTokens: number;
writeTokens: number;
noCacheTokens: number;
cacheShare: number | null;
};
type UsagePayload = {
data: Array<{ featureKey: string; hasData: boolean; metrics: { costMicrousd: Metric; inputTokens: Metric; outputTokens: Metric; durationMs: Metric } }>;
cache?: { days7: CacheModelRow[]; days30: CacheModelRow[] };
};
type LoadState = { models?: ModelRow[]; pricing?: PricingRow[]; products?: Product[]; usage?: UsagePayload; errors: string[] };
@@ -48,6 +60,10 @@ function errorText(error: unknown, fallback: string) {
return error instanceof Error ? error.message : fallback;
}
function formatPercent(value: number | null) {
return value === null || !Number.isFinite(value) ? "—" : `${(value * 100).toFixed(1)}%`;
}
export function PricingSimulator() {
const [state, setState] = useState<LoadState>({ errors: [] });
const [usdToCny, setUsdToCny] = useState<number | null>(null);
@@ -158,6 +174,36 @@ export function PricingSimulator() {
<Col xs={24} md={8}><Statistic title="画像下可承载会员数" value={capacity ?? "—"} /></Col>
</Row>
</Card>
<Card title="缓存命中">
<Typography.Paragraph type="secondary">
`usage_ledger.metadata.cache` = tokens &gt; 0
</Typography.Paragraph>
{([
["近 7 天", state.usage?.cache?.days7 ?? []],
["近 30 天", state.usage?.cache?.days30 ?? []],
] as const).map(([title, rows]) => (
<div key={title} style={{ marginBottom: 16 }}>
<Typography.Text strong>{title}</Typography.Text>
<Table<CacheModelRow>
size="small"
pagination={false}
rowKey={(row, index) => `${title}-${row.actualModelId ?? "none"}-${index}`}
dataSource={rows}
locale={{ emptyText: "暂无缓存数据" }}
columns={[
{ title: "模型", dataIndex: "actualModelId", render: (value) => value ?? "—" },
{ title: "运行数", dataIndex: "runsWithCache" },
{ title: "命中率", dataIndex: "hitRate", render: formatPercent },
{ title: "缓存占比", dataIndex: "cacheShare", render: formatPercent },
{ title: "读", dataIndex: "readTokens", render: (value: number) => value.toLocaleString() },
{ title: "写", dataIndex: "writeTokens", render: (value: number) => value.toLocaleString() },
{ title: "未命中", dataIndex: "noCacheTokens", render: (value: number) => value.toLocaleString() },
]}
/>
</div>
))}
</Card>
</Space>
);
}