Files
ln-bi/src/server/routes/asset-assistant/presentation.ts
T
2026-09-28 13:16:12 +08:00

83 lines
5.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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<AssistantResult["scope"]> {
const filters = [
plan.kind === "flow" ? "交退换车业务" : CATEGORY_LABELS[plan.category],
...Object.entries(plan.filters).map(([key, values]) => `${GROUP_LABELS[key as keyof typeof GROUP_LABELS]}:${values.join("、")}`),
...Object.entries(plan.excludeFilters || {}).map(([key, values]) => `排除${GROUP_LABELS[key as keyof typeof GROUP_LABELS]}:${values.join("、")}`),
...(plan.vehicleIds ? [`指定 ${plan.vehicleIds.length} 台车辆`] : []),
...(plan.exception ? [{ overdue: "异动已超预计完成时间", dueSoon: "异动未来7天到期", missingDueDate: "异动未填写预计完成时间" }[plan.exception]] : []),
];
const definitions = plan.responseMode === "vehicle-status" ? ["状态来自当前资产台账,不代表车辆实时位置、行驶或故障状态。", "运营状态、库存归属、异动及待交车分别展示,不互相替代。"] : plan.kind === "flow" ? [
"按运维签章日期统计业务车次,替换交车已计入交车,不能重复相加。",
"按当前账号本人或部门权限及当前筛选条件查询流转记录(全量权限不受此限);车次差额不代表净增车辆,增减本身不能证明经营改善。",
] : [
"库存含可运营、待运营、异动及待交车;异动、待交车是库存子集,不与总库存相加。",
"待运营是运营状态,待交车是交车流程状态,两者不可替代。",
"运营率 = 当前筛选范围内运营车辆数 ÷ 当前筛选范围内车辆总数。",
"车型按吨位归类;4.5T包含普货和冷链,车型细分可拆开查看,与首页分开展示的口径不同。",
];
if (plan.topN) definitions.push(`仅展示前 ${plan.topN} 个分组,指标总量和占比仍按完整筛选范围计算。`);
return {
company: subject || "全部公司(当前账号可见)",
permission: plan.kind === "flow" ? "交退换记录沿用当前账号原有数据权限" : "库存(含待交车、异动)全量可见,非库存车辆按当前账号数据权限查看",
filters,
grouping: plan.kind === "flow" ? "按日" : plan.groupBy === "none" ? "不分组" : `按${GROUP_LABELS[plan.groupBy]}${plan.secondaryGroupBy ? ` × ${GROUP_LABELS[plan.secondaryGroupBy]}` : ""}`,
period: plan.kind === "flow" ? `${plan.start} 至 ${plan.end}(含首尾日期)` : "当前车辆状态(非历史快照)",
definitions,
sourceUpdatedAt: null,
};
}
export function followupSuggestions(result: AssistantResult): string[] {
const p = result.plan;
if (p.responseMode === "vehicle-status") return result.rows.length ? ["查看全部库存"] : ["取消所有筛选"];
if (p.kind === "clarify") return [...new Set(result.suggestions || [])].slice(0, 3);
const suggestions: string[] = [];
if (p.kind === "flow") {
if (!p.comparePrevious) suggestions.push("与上一等长周期对比");
if (p.view !== "list") suggestions.push("查看交退换车明细");
if (p.view !== "table") suggestions.push("按天用表格展示");
return suggestions.slice(0, 3);
}
if (!result.rows.length) {
if (Object.keys(p.filters).length || Object.keys(p.excludeFilters || {}).length || p.exception || p.vehicleIds) suggestions.push("取消所有筛选");
if (p.category !== "all") suggestions.push("查看全部资产");
return suggestions;
}
if (p.analysis === "briefing") suggestions.push("按车型细分查看当前库存");
else {
const next = ["vehicleClass", "region", "city", "model", "department"].find(d => d !== p.groupBy && d !== p.secondaryGroupBy && !p.filters[d as keyof typeof p.filters] && (d !== "vehicleClass" || p.groupBy === "type" || p.filters.type?.includes("4.5T")));
if (next) suggestions.push(`保持当前筛选,按${GROUP_LABELS[next as keyof typeof GROUP_LABELS]}拆分`);
}
if (p.view !== "list") suggestions.push("查看当前范围车辆明细");
if (p.category === "inventory" && !p.exception) suggestions.push("当前库存中待交车有多少");
return suggestions.slice(0, 3);
}