@@ -405,11 +405,11 @@ function ResultViewContent({
{result.gap && (
{result.gap.recorded
- ? "这个需求已记下,可在「待完善需求」中查看。"
+ ? "这个需求已记下。"
: "可以点击下方「未解决我的问题」记录需求。"}
)}
- {plan.kind !== "clarify" && (
+ {plan.kind !== "clarify" && !result.conciseCount && (
<>
{result.inventoryBreakdown && display !== "metrics" && (
@@ -849,6 +849,11 @@ function ResultViewContent({
>
)}
+ {result.conciseCount && result.metrics[0]?.value > 0 && (
+
@@ -873,7 +878,7 @@ function ResultViewContent({
来源:{result.source}
- {plan.kind !== "clarify" && (
+ {plan.kind !== "clarify" && (!result.conciseCount || result.metrics[0]?.value > 0) && (
- {!!suggestions.length && (
+ {!result.conciseCount && !!suggestions.length && (
{plan.kind === "clarify" ? "你可以这样继续" : "继续分析"}
{suggestions.slice(0, 4).map((question) => (
diff --git a/src/modules/assets/assistant/ThinkingState.tsx b/src/modules/assets/assistant/ThinkingState.tsx
index c6c7660..bb7e1ef 100644
--- a/src/modules/assets/assistant/ThinkingState.tsx
+++ b/src/modules/assets/assistant/ThinkingState.tsx
@@ -1,7 +1,7 @@
// Adapted from Beautiful UI's MIT-licensed ThinkingState (Steps).
// Uses real server progress rather than the reference's timed demo sequence.
-import { useEffect, useState } from "react";
-import { Check, ChevronDown, LoaderCircle, Sparkles } from "lucide-react";
+import { useEffect, useId, useState } from "react";
+import { Check, ChevronDown, CircleAlert, LoaderCircle, Pause, Sparkles } from "lucide-react";
export function ThinkingState({
steps,
@@ -18,27 +18,30 @@ export function ThinkingState({
stopped?: boolean;
outcome?: string;
}) {
+ const traceId = useId();
const [manualExpanded, setExpanded] = useState
(null);
const [elapsed, setElapsed] = useState(0);
useEffect(() => {
if (!working) return;
const update = () => setElapsed((Date.now() - started) / 1000);
update();
- const timer = window.setInterval(update, 100);
+ const timer = window.setInterval(update, 1000);
return () => window.clearInterval(timer);
}, [working, started]);
- const expanded = manualExpanded ?? false;
+ const expanded = manualExpanded ?? working;
+ const StatusIcon = working ? LoaderCircle : stopped ? Pause : failed ? CircleAlert : Sparkles;
return (
-
+
+ {!steps.length && - {working ? "正在连接分析服务…" : "暂无执行步骤"}
}
{steps.map((step, index) => (
- -
+
-
{working && index === steps.length - 1 ? (
+ ) : index === steps.length - 1 && (failed || stopped) ? (
+
) : (
)}
diff --git a/src/modules/assets/assistant/assistant.css b/src/modules/assets/assistant/assistant.css
index 7a92c45..4bbba8c 100644
--- a/src/modules/assets/assistant/assistant.css
+++ b/src/modules/assets/assistant/assistant.css
@@ -405,9 +405,6 @@
color: var(--aq-muted);
margin-top: 24px;
}
-.aq-mobile-break {
- display: none;
-}
.aq-turns {
max-width: 820px;
margin: 0 auto;
@@ -1124,9 +1121,6 @@
.aq-welcome > p {
font-size: 12px;
}
- .aq-mobile-break {
- display: block;
- }
.aq-welcome-mark {
width: 52px;
height: 52px;
@@ -2014,3 +2008,93 @@
.aq-generated-metrics strong { font-size: 21px; }
.aq-stacked-section { padding: 7px; }
}
+
+/* The full-page assistant owns the mobile viewport, including keyboard resize. */
+@media (max-width: 767px) {
+ .app-shell:has(.aq-page) .app-mobile-nav { display: none; }
+ .app-shell:has(.aq-page) .app-shell-content { padding-bottom: 0; }
+ .aq-shell.aq-page {
+ position: fixed;
+ top: var(--aq-viewport-top, 0px);
+ left: 0;
+ height: var(--aq-viewport-height, 100dvh);
+ max-height: none;
+ margin-top: 0;
+ }
+ .aq-page .aq-sidebar { display: none; }
+ .aq-page .aq-composer-area { flex-shrink: 0; }
+ .aq-page .aq-composer textarea { font-size: 16px; }
+}
+
+/* Quiet, expandable execution traces inspired by Beautiful UI. */
+.aq-thinking {
+ border: 1px solid var(--aq-line);
+ border-radius: 12px;
+ background: var(--aq-bg);
+ padding: 2px 12px;
+ margin-bottom: 16px;
+}
+.aq-thinking-working { background: var(--aq-sidebar); }
+.aq-thinking > button {
+ display: flex;
+ width: 100%;
+ min-height: 44px;
+ margin: 0;
+ padding: 8px 0;
+ gap: 8px;
+ text-align: left;
+ font-size: 13px;
+}
+.aq-thinking > button > svg { flex-shrink: 0; }
+.aq-thinking-title { min-width: 0; overflow-wrap: anywhere; }
+.aq-thinking-meta {
+ display: flex;
+ gap: 10px;
+ margin-left: auto;
+ white-space: nowrap;
+ font-size: 12px;
+ font-variant-numeric: tabular-nums;
+}
+.aq-thinking .aq-time { font-size: 12px; opacity: 1; }
+.aq-trace { margin: 0 0 12px 7px; padding: 0 0 0 18px; }
+.aq-trace li { align-items: flex-start; gap: 10px; padding: 7px 0; line-height: 1.6; overflow-wrap: anywhere; }
+.aq-trace li svg { flex-shrink: 0; margin-top: 3px; }
+.aq-thinking-working .aq-trace li[data-current="true"] { color: var(--aq-ink); }
+.aq-thinking[data-state="failed"] > button > svg { color: var(--aq-danger); }
+.aq-question p { padding: 12px 16px; border-radius: 18px 18px 4px 18px; font-weight: 400; }
+.aq-assistant-label { margin-bottom: 10px; }
+.aq-assistant-label strong { font-size: 13px; }
+.aq-assistant-label > span:last-child { font-size: 12px; }
+.aq-composer { border-radius: 18px; box-shadow: 0 2px 8px #00000006; }
+.aq-composer:focus-within { border-color: var(--aq-accent); box-shadow: 0 0 0 3px var(--aq-soft); }
+.aq-welcome h2 { text-wrap: balance; }
+.aq-prompt-grid > button { transition: background 160ms ease, border-color 160ms ease; }
+.aq-prompt-grid > button:hover { background: var(--aq-sidebar); border-color: var(--aq-accent); }
+.aq-page button:focus-visible { outline: 2px solid var(--aq-accent); outline-offset: 3px; }
+.aq-page button:active:not(:disabled) { opacity: .75; }
+@media (max-width: 767px) {
+ .aq-header { min-height: 64px; height: auto; padding: 8px 12px; }
+ .aq-header > div { gap: 4px; }
+ .aq-header .aq-icon-button { width: 44px; height: 44px; min-width: 44px; }
+ .aq-thinking { margin-left: 0; padding: 2px 10px; }
+ .aq-thinking-meta { gap: 6px; }
+ .aq-question p { max-width: 88%; font-size: 15px; }
+ .aq-welcome { padding: 24px 16px 16px; }
+ .aq-welcome-mark { display: none; }
+ .aq-welcome h2 { margin: 0 0 8px; font-size: 22px; line-height: 1.4; letter-spacing: -.5px; }
+ .aq-welcome > p { margin: 0; font-size: 13px; line-height: 1.5; }
+ .aq-welcome-detail { display: none; }
+ .aq-prompt-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); margin-top: 20px; gap: 8px; }
+ .aq-prompt-grid > button { flex-direction: row; align-items: center; justify-content: flex-start; gap: 8px; min-width: 0; min-height: 48px; padding: 12px; border-radius: 12px; }
+ .aq-prompt-grid > button > div { flex-shrink: 0; margin: 0; color: var(--aq-ink); }
+ .aq-prompt-grid > button > div > svg:last-child { display: none; }
+ .aq-prompt-grid > button strong { font-size: 14px; line-height: 1.5; overflow-wrap: anywhere; }
+ .aq-prompt-grid > button > span { display: none; }
+ .aq-welcome-note { margin-top: 12px; font-size: 12px; line-height: 1.5; flex-wrap: nowrap; }
+ .aq-welcome-note svg { flex-shrink: 0; }
+}
+@media (prefers-reduced-motion: reduce) {
+ .aq-page .aq-spin, .aq-page .aq-shimmer, .aq-page .aq-trace li { animation: none; }
+ .aq-page .aq-shimmer { background: none; color: var(--aq-muted); }
+ .aq-page .aq-trace-collapse { transition: none; }
+}
diff --git a/src/modules/assets/assistant/concise-count.test.ts b/src/modules/assets/assistant/concise-count.test.ts
new file mode 100644
index 0000000..172cd72
--- /dev/null
+++ b/src/modules/assets/assistant/concise-count.test.ts
@@ -0,0 +1,24 @@
+import assert from "node:assert/strict";
+import test from "node:test";
+import { createElement } from "react";
+import { renderToStaticMarkup } from "react-dom/server";
+import { ResultView } from "./ResultView";
+import type { AssistantResult } from "../../../shared/assets/assistant";
+
+test("count answer renders prose without report cards and only offers records for positive counts", () => {
+ for (const count of [0, 7]) {
+ const result: AssistantResult = {
+ plan: {kind:"assets",category:"abnormal",groupBy:"none",view:"chart",filters:{}},
+ conciseCount:true,outcome:"complete",summary:`当前异动车辆为 ${count} 台。`,
+ title:"异动车辆概览",metrics:[{label:"异动车辆",value:count,unit:"台"}],
+ rows:[],groups:[],source:"测试台账",fetchedAt:"2026-09-23T00:00:00Z",suggestions:["查看全部车辆"],
+ };
+ const noop = () => {};
+ const html = renderToStaticMarkup(createElement(ResultView,{result,ask:noop,operate:noop,restore:noop,active:true,activate:noop,subject:null,disabled:false,showCorrection:noop}));
+ assert.match(html,new RegExp(`当前异动车辆为 ${count} 台`));
+ assert.doesNotMatch(html,/aq-primary-metrics|aq-result-card|aq-followups|aq-query-heading/);
+ assert.equal(html.includes("查看车辆明细"),count > 0);
+ assert.equal(html.includes("导出明细"),count > 0);
+ assert.match(html,/统计口径、权限与数据时间/);
+ }
+});
diff --git a/src/modules/ele/EleImportPage.tsx b/src/modules/ele/EleImportPage.tsx
index 92d0c19..e1eeaa1 100644
--- a/src/modules/ele/EleImportPage.tsx
+++ b/src/modules/ele/EleImportPage.tsx
@@ -2,8 +2,6 @@ import { useCallback, useEffect, useRef, useState } from 'react';
import { ArrowLeft, Upload, Zap } from 'lucide-react';
import { fetchJson } from '../../auth/api-client';
import { useAuth } from '../../auth/useAuth';
-import FeedbackFab from '../../components/FeedbackFab';
-import RotatingFooterHint from '../../components/RotatingFooterHint';
import { PageFrame } from '../../components/ui/surface';
import { BatchTable } from './import-page/BatchTable';
import { RecordFilters } from './import-page/RecordFilters';
@@ -168,8 +166,7 @@ export default function EleImportPage() {
-
-
+
);
}
diff --git a/src/modules/energy/ETCView.tsx b/src/modules/energy/ETCView.tsx
index ad9e44b..a91516a 100644
--- a/src/modules/energy/ETCView.tsx
+++ b/src/modules/energy/ETCView.tsx
@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { BadgeCheck, CarFront, FileText, ReceiptText, WalletCards } from 'lucide-react';
import { fetchEtcOverview, type EtcOverviewResponse } from './api';
-import RotatingFooterHint from '../../components/RotatingFooterHint';
import { EmptyState, ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
function formatYuan(value: number) {
@@ -27,7 +26,6 @@ export default function ETCView() {
return (
-
);
}
@@ -46,7 +44,6 @@ export default function ETCView() {
当前统计直接读取 ETC 通行明细与账单台账;金额、应收和已收均不做推算。
-
);
}
diff --git a/src/modules/energy/ElectricDaily.tsx b/src/modules/energy/ElectricDaily.tsx
index 945662d..ac5f6f5 100644
--- a/src/modules/energy/ElectricDaily.tsx
+++ b/src/modules/energy/ElectricDaily.tsx
@@ -4,7 +4,6 @@ import { AnimatePresence, motion } from 'motion/react';
import TrendBadge from './TrendBadge';
import { fetchElectricMonthly } from './api';
import type { CustomerType, DateQuickPick, ElectricMonthGroup } from './types';
-import RotatingFooterHint from '../../components/RotatingFooterHint';
import { EmptyState, ErrorState, LoadingState, MetricTile } from '../../components/ui/surface';
import DailyRangeControls from './daily-range/DailyRangeControls';
import {
@@ -184,7 +183,6 @@ export default function ElectricDaily() {
);
})}
-
);
}
diff --git a/src/modules/energy/ElectricOverview.tsx b/src/modules/energy/ElectricOverview.tsx
index 7fc2799..f9414b1 100644
--- a/src/modules/energy/ElectricOverview.tsx
+++ b/src/modules/energy/ElectricOverview.tsx
@@ -2,7 +2,6 @@ import { useEffect, useState } from 'react';
import { BatteryCharging, Gauge, Wallet, CalendarClock } from 'lucide-react';
import { BarChart, Bar, XAxis, YAxis, ResponsiveContainer, Cell, Tooltip, ReferenceLine } from 'recharts';
import { fetchElectricOverview, type ElectricOverviewResponse } from './api';
-import RotatingFooterHint from '../../components/RotatingFooterHint';
import { ErrorState, LoadingState, MetricTile, SurfaceCard } from '../../components/ui/surface';
function fmtYuan(yuan: number) {
@@ -126,7 +125,7 @@ export default function ElectricOverview() {
-
+
);
}
diff --git a/src/modules/mileage/MileageModule.tsx b/src/modules/mileage/MileageModule.tsx
index 9466e5f..9bd6005 100644
--- a/src/modules/mileage/MileageModule.tsx
+++ b/src/modules/mileage/MileageModule.tsx
@@ -4,7 +4,6 @@ import MonitoringView from './MonitoringView';
import StatisticsView from './StatisticsView';
import DailyReportView from './DailyReportView';
import { useHashSubTab } from '../energy/useHashSubTab';
-import RotatingFooterHint from '../../components/RotatingFooterHint';
import { FadeIn, PageFrame, SegmentedNav } from '../../components/ui/surface';
type MileageSubTab = 'monitoring' | 'statistics' | 'report';
@@ -45,7 +44,7 @@ export default function MileageModule() {
)}
-
+
);
}
diff --git a/src/modules/scheduling/SchedulingModule.tsx b/src/modules/scheduling/SchedulingModule.tsx
index 9dc88a8..a88eaa4 100644
--- a/src/modules/scheduling/SchedulingModule.tsx
+++ b/src/modules/scheduling/SchedulingModule.tsx
@@ -7,7 +7,6 @@ import SuggestionList from './SuggestionList';
import SuggestionDetail from './SuggestionDetail';
import NotificationHistory from './NotificationHistory';
import { exportSuggestionsCsv } from './csv-export';
-import RotatingFooterHint from '../../components/RotatingFooterHint';
import { PageFrame } from '../../components/ui/surface';
import ActiveFilterTags from './scheduling-module/ActiveFilterTags';
import BatchActionBar from './scheduling-module/BatchActionBar';
@@ -309,7 +308,7 @@ export default function SchedulingModule() {
onSubmit={handleBatchSubmit}
/>
)}
-
+
);
}
diff --git a/src/server/routes/asset-assistant/generative.test.ts b/src/server/routes/asset-assistant/generative.test.ts
index 8607e6c..cd75e0f 100644
--- a/src/server/routes/asset-assistant/generative.test.ts
+++ b/src/server/routes/asset-assistant/generative.test.ts
@@ -257,3 +257,15 @@ test("metric references keep one unit when the model appends it in prose", async
await generatePresentation(value,"说明库存",signal());
assert.equal(value.presentation?.blocks[0].text,"库存2 台,其中待交车0台。");
});
+
+test("verified single count bypasses generated report and external request", async (t) => {
+ configure(t);
+ t.mock.method(globalThis, "fetch", async () => { throw new Error("count must not call presentation model"); });
+ const r = result({plan:plan({category:"abnormal",groupBy:"none"}),outcome:"empty",rows:[],groups:[],metrics:[{label:"异动车辆",value:0,unit:"台"}]});
+ await generatePresentation(r,"当前有多少异动车辆?",new AbortController().signal);
+ assert.equal(r.conciseCount,true);
+ assert.equal(r.outcome,"complete");
+ assert.equal(r.presentation,undefined);
+ assert.equal(r.presentationWarning,undefined);
+ assert.match(r.summary,/异动车辆为 0 台/);
+});
diff --git a/src/server/routes/asset-assistant/generative.ts b/src/server/routes/asset-assistant/generative.ts
index ae9ba7f..759be51 100644
--- a/src/server/routes/asset-assistant/generative.ts
+++ b/src/server/routes/asset-assistant/generative.ts
@@ -6,6 +6,7 @@ import type {
ResultPresentation,
} from "../../../shared/assets/assistant.js";
import { validatePlan } from "./model.js";
+import { simplifyCountAnswer } from "./presentation.js";
type Dataset = "groups" | "rows" | "daily" | "comparison";
type Field = { field: string; label: string; type: "number" | "string" | "boolean" };
@@ -158,6 +159,8 @@ function fallback(target: AssistantResult, warning: string) {
}
/** Generate presentation metadata only; query data and authorization are unchanged. */
export async function generatePresentation(result: AssistantResult, question: string, signal: AbortSignal): Promise
{
+ if (signal.aborted) throw signal.reason || new Error("已取消");
+ if (simplifyCountAnswer(result, question)) return;
if (process.env.ASSET_AI_EXTERNAL_ENABLED === "0") return;
if (signal.aborted) throw signal.reason || new Error("已取消");
const targets = targetsFor(result).filter(target => target.result.plan.responseMode !== "vehicle-status");
diff --git a/src/server/routes/asset-assistant/model.test.ts b/src/server/routes/asset-assistant/model.test.ts
index fe3961d..31cf7c9 100644
--- a/src/server/routes/asset-assistant/model.test.ts
+++ b/src/server/routes/asset-assistant/model.test.ts
@@ -158,3 +158,11 @@ test("presentation receives authorized aggregates while vehicle rows stay in the
);
assert.equal(events.filter((e) => e.type === "step").length, 4);
});
+
+test("inventory subsets do not inherit parent inventory metric cards", () => {
+ for (const category of ["abnormal", "pending", "available", "awaiting"] as const) {
+ const r = assetResult(vehicles, {kind:"assets", category, groupBy:"none", view:"chart", filters:{}});
+ assert.equal(r.metrics.length, 1, category);
+ assert.doesNotMatch(r.summary, /其中异动.*待交车/);
+ }
+});
diff --git a/src/server/routes/asset-assistant/model.ts b/src/server/routes/asset-assistant/model.ts
index 63d3595..bc52a1d 100644
--- a/src/server/routes/asset-assistant/model.ts
+++ b/src/server/routes/asset-assistant/model.ts
@@ -335,7 +335,7 @@ export function assetResult(
missingDueDate: "未填写预计完成时间",
}[plan.exception || "overdue"];
let summary = rows.length
- ? `${scope}共有 ${rows.length.toLocaleString()} 台${CATEGORY_LABELS[plan.category]}${plan.exception ? `,均为${exceptionLabel}的异动车辆` : ""}。${abnormal || pending ? `其中异动 ${abnormal} 台、待交车 ${pending} 台,已计入库存。` : ""}${plan.topN ? `下方展示排名前 ${plan.topN} 的分组,总量与占比仍按完整筛选范围计算。` : ""}`
+ ? `${scope}共有 ${rows.length.toLocaleString()} 台${CATEGORY_LABELS[plan.category]}${plan.exception ? `,均为${exceptionLabel}的异动车辆` : ""}。${["all", "inventory"].includes(plan.category) && (abnormal || pending) ? `其中异动 ${abnormal} 台、待交车 ${pending} 台,已计入库存。` : ""}${plan.topN ? `下方展示排名前 ${plan.topN} 的分组,总量与占比仍按完整筛选范围计算。` : ""}`
: `${scope}的${CATEGORY_LABELS[plan.category]}为 0 台,暂无符合条件车辆。`;
let inventoryBreakdown: AssistantResult["inventoryBreakdown"];
if (plan.category === "all") summary = `${scope}共有资产 ${rows.length.toLocaleString()} 台,其中运营 ${operating.toLocaleString()} 台、库存 ${inventory.toLocaleString()} 台,运营率 ${operationRate}%。`;
@@ -405,7 +405,7 @@ export function assetResult(
unit: "%",
},
]
- : [
+ : plan.category === "inventory" ? [
{
label: "其中可运营",
value: selected.filter((v) =>
@@ -420,9 +420,9 @@ export function assetResult(
).length,
unit: "台",
},
- ]),
+ ] : []),
...(plan.analysis === "briefing" ? [{ label: "其中可运营库存", value: selected.filter(v => isInventoryVehicle(v) && ["3", "可运营"].includes(v.operationStatus || "")).length, unit: "台" }, { label: "其中待运营库存", value: selected.filter(v => isInventoryVehicle(v) && ["4", "待运营"].includes(v.operationStatus || "")).length, unit: "台" }] : []),
- ...(!["all", "operating"].includes(plan.category) || plan.analysis === "briefing"
+ ...(plan.category === "inventory" || plan.analysis === "briefing"
? [
{ label: "其中异动", value: abnormal, unit: "台" },
{ label: "其中待交车", value: pending, unit: "台" },
diff --git a/src/server/routes/asset-assistant/presentation.test.ts b/src/server/routes/asset-assistant/presentation.test.ts
index a836682..ae39d34 100644
--- a/src/server/routes/asset-assistant/presentation.test.ts
+++ b/src/server/routes/asset-assistant/presentation.test.ts
@@ -23,3 +23,30 @@ test("opening inventory from a briefing leaves briefing mode and keeps inherited
const next=applyAction(briefing,{type:"category",category:"inventory"},result(briefing));
assert.equal(next.category,"inventory");assert.equal(next.analysis,"distribution");assert.deepEqual(next.filters,briefing.filters);
});
+
+test("simple counts answer directly, including verified zero, while preserving scope", async () => {
+ const { simplifyCountAnswer } = await import("./presentation.js");
+ for (const count of [0, 7]) {
+ const r = result({...plan, category:"abnormal", groupBy:"none", view:"chart"});
+ r.outcome = count ? "complete" : "empty";
+ r.metrics = [{label:"异动车辆",value:count,unit:"台"},{label:"其中待交车",value:0,unit:"台"}];
+ r.scope = resultScope(r.plan,"测试公司");
+ assert.equal(simplifyCountAnswer(r,"当前有多少异动车辆?"),true);
+ assert.match(r.summary,new RegExp(`异动车辆为 ${count} 台`));
+ assert.match(r.summary,/测试公司.*4.5T.*嘉兴市/);
+ assert.equal(r.metrics.length,1);
+ assert.equal(r.outcome,"complete");
+ assert.deepEqual(r.suggestions,[]);
+ }
+});
+
+test("counts never collapse analysis requests, missing metrics or incomplete results", async () => {
+ const { simplifyCountAnswer } = await import("./presentation.js");
+ const make = () => ({...result({...plan,category:"abnormal",groupBy:"none",view:"chart"}), outcome:"complete" as const,metrics:[{label:"异动车辆",value:7,unit:"台"}]});
+ for (const question of ["各区域有多少异动车辆", "异动和待交车分别多少", "异动有多少,占比多少", "有多少异动车辆,查看明细", "分析异动车辆数量变化"]) {
+ assert.equal(simplifyCountAnswer(make(),question),false,question);
+ }
+ for (const override of [{outcome:"partial" as const},{warning:"源数据未接入"},{metrics:[]},{plan:{...make().plan,view:"list" as const}},{plan:{...make().plan,groupBy:"city" as const}}]) {
+ assert.equal(simplifyCountAnswer({...make(),...override},"当前有多少异动车辆?"),false);
+ }
+});
diff --git a/src/server/routes/asset-assistant/presentation.ts b/src/server/routes/asset-assistant/presentation.ts
index 3978c27..ef2d797 100644
--- a/src/server/routes/asset-assistant/presentation.ts
+++ b/src/server/routes/asset-assistant/presentation.ts
@@ -1,5 +1,31 @@
import { CATEGORY_LABELS, GROUP_LABELS, type AssistantResult, type QueryPlan } from "../../../shared/assets/assistant.js";
+/** Only simplify verified, single-scope count queries; never infer zero from missing data. */
+export function simplifyCountAnswer(result: AssistantResult, question: string): boolean {
+ const p = result.plan;
+ const metric = result.metrics.find(m => m.label === CATEGORY_LABELS[p.category] && m.unit === "台");
+ if (result.sections?.length || p.kind !== "assets" || p.responseMode ||
+ p.groupBy !== "none" || p.secondaryGroupBy || p.view !== "chart" ||
+ (p.analysis && p.analysis !== "distribution") || p.comparePrevious || p.topN ||
+ result.warning || result.unanswered?.length || result.gap ||
+ !["complete", "empty"].includes(result.outcome || "") ||
+ !metric || !Number.isFinite(metric.value) || metric.value < 0 ||
+ !/(多少|几台|几辆|数量|总数)/.test(question) ||
+ /(分别|各|分布|占比|比例|运营率|对比|比较|趋势|变化|原因|为什么|分析|明细|清单|列表|表格|图表|排行|最多|最少|以及|和|与|、)/.test(question)) return false;
+ const scope = result.scope || resultScope(p, null);
+ const filters = scope.filters.slice(1);
+ result.conciseCount = true;
+ result.summary = `${scope.company}${filters.length ? `,${filters.join(",")}` : ""},当前${CATEGORY_LABELS[p.category]}为 ${metric.value.toLocaleString()} 台。`;
+ result.metrics = [metric];
+ result.suggestions = [];
+ // A successful zero count is an answer, not a query failure or missing dataset.
+ result.outcome = "complete";
+ delete result.presentation;
+ delete result.presentationStatus;
+ delete result.presentationWarning;
+ return true;
+}
+
export function resultScope(plan: QueryPlan, subject: string | null): NonNullable {
const filters = [
plan.kind === "flow" ? "交退换车业务" : CATEGORY_LABELS[plan.category],
diff --git a/src/shared/assets/assistant.ts b/src/shared/assets/assistant.ts
index c8c182a..89e7e28 100644
--- a/src/shared/assets/assistant.ts
+++ b/src/shared/assets/assistant.ts
@@ -167,6 +167,7 @@ export type DemandGap = {
};
export type AssistantResult = {
plan: QueryPlan;
+ conciseCount?: boolean;
presentation?: ResultPresentation;
presentationStatus?: "generated" | "fallback";
presentationWarning?: string;