PMFlow/frontend/src/pages/model-routing-config/ModelRoutingConfigPage.tsx

235 lines
11 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 { useEffect, useMemo, useState } from "react";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { Settings2 } from "lucide-react";
import { modelRoutingApi, type ModelRoutingItem } from "@/api/modelRouting";
import { ConfigSubpageHeader } from "@/components/ai-config/ConfigSubpageHeader";
import { FeatureIntroGuide } from "@/components/ai-config/FeatureIntroGuide";
import { Button } from "@/components/ui/button";
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table";
import { formatBeijingDateTime } from "@/lib/datetime";
import { toZhRoutingSource, toZhTierAlias } from "@/lib/display";
const TIER_OPTIONS = ["low", "high"] as const;
const FUNCTION_USAGE_DESCRIPTIONS: Record<string, string> = {
adaptive_rag_answer_deep: "复杂问题的深度回答阶段:会做更细的推理和更完整的结论组织。",
adaptive_rag_answer_normal: "常规问答阶段:基于已检索到的信息快速生成可读回复。",
feishu_humanized_reply: "对外回复润色阶段:把结果改写成飞书里更自然、易读的表达。",
rag_db_project_semantic_retrieval: "知识检索阶段:从项目库里找最相关的历史上下文与证据。",
rag_intent_classification: "意图识别阶段:先判断这是哪类问题,再决定后续处理路径。",
signal_planner_analyze: "任务规划阶段:把需求拆成可执行步骤、产物和责任边界。",
classify_intent_ai_pmo_kernel: "轻量意图分流:快速判断是否进入 PMO 内核处理链路。",
classify_intent_complex: "复杂意图判别:识别高复杂度请求,决定是否升级到高层模型。",
};
function getFunctionUsageHint(functionName: string): string {
return FUNCTION_USAGE_DESCRIPTIONS[functionName] || "通用路由节点:用于该函数调用阶段的高低层选择。";
}
function describeLastExecution(item: ModelRoutingItem): string {
const last = item.last_execution;
if (!last) return "暂无执行记录";
return `${last.planned_tier} -> ${last.executed_tier}`;
}
function describeUpgrade(item: ModelRoutingItem): string {
const last = item.last_execution;
if (!last) return "-";
return last.planned_tier !== last.executed_tier ? "是" : "否";
}
export default function ModelRoutingConfigPage() {
const queryClient = useQueryClient();
const [selectedFunction, setSelectedFunction] = useState("signal_planner_analyze");
const [tierAlias, setTierAlias] = useState<(typeof TIER_OPTIONS)[number]>("high");
const routingQuery = useQuery({
queryKey: ["model-routing-functions"],
queryFn: () => modelRoutingApi.listFunctions(),
});
const items = routingQuery.data || [];
const current = useMemo(
() => items.find((item) => item.function_name === selectedFunction) || null,
[items, selectedFunction]
);
useEffect(() => {
const fallback: ModelRoutingItem | null = current || items[0] || null;
if (!fallback) return;
if (!current && fallback.function_name) {
setSelectedFunction(fallback.function_name);
}
const nextTier = TIER_OPTIONS.includes(fallback.tier_alias as (typeof TIER_OPTIONS)[number])
? (fallback.tier_alias as (typeof TIER_OPTIONS)[number])
: "high";
setTierAlias(nextTier);
}, [current, items]);
const updateMutation = useMutation({
mutationFn: () =>
modelRoutingApi.updateFunction(selectedFunction, {
tier_alias: tierAlias,
}),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["model-routing-functions"] });
},
});
const routingErrorText = routingQuery.error instanceof Error ? routingQuery.error.message : "请检查后端服务。";
return (
<div className="space-y-6">
<ConfigSubpageHeader
title="高级路由详情"
description="高级排障时按函数选择快速/深度槽位;团队日常策略请优先使用 AI 路由与供应商控制台。"
icon={<Settings2 className="h-5 w-5" />}
tone="blue"
meta={<span>{routingQuery.isError ? "读取失败" : `已加载 ${items.length} 条路由`}</span>}
/>
<FeatureIntroGuide
featureId="model-routing-config"
title="高级路由详情"
purpose="给高级排障保留函数级快速/深度槽位覆盖能力。"
whenToUse="只有当控制台诊断已定位到具体函数,并需要临时切换槽位时使用。"
steps={[
"在右侧选中目标函数,左侧会自动回填当前逻辑 tier。",
"只调整高层或低层,点“保存路由”。",
"触发一次该功能调用,观察最近一次执行是否出现低层升级到高层。",
]}
outputs={[
"函数级 tier override",
"命中规则与最近执行结果",
]}
/>
<div className="grid gap-6 xl:grid-cols-[360px,1fr]">
<Card className="border-slate-200/80 bg-white/90 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/85">
<CardHeader>
<CardTitle className="text-base">编辑器</CardTitle>
<CardDescription>选择函数并更新逻辑 tier。</CardDescription>
</CardHeader>
<CardContent className="space-y-3">
<Input
value={selectedFunction}
onChange={(e) => setSelectedFunction(e.target.value.trim())}
placeholder="function_name"
/>
<p className="text-xs text-slate-500 dark:text-slate-400">{getFunctionUsageHint(selectedFunction)}</p>
<select
value={tierAlias}
onChange={(e) => setTierAlias(e.target.value as (typeof TIER_OPTIONS)[number])}
className="h-10 rounded-md border border-input bg-background px-3 text-sm"
>
{TIER_OPTIONS.map((item) => (
<option key={item} value={item}>
{toZhTierAlias(item)}
</option>
))}
</select>
<div className="rounded-xl border border-slate-200/80 bg-slate-50/80 p-3 text-xs text-slate-600 dark:border-slate-800 dark:bg-slate-950/50 dark:text-slate-300">
真实 provider / model 只存在于配置真源,不在调用视图直接展示。
</div>
<Button
onClick={() => updateMutation.mutate()}
disabled={updateMutation.isPending || !selectedFunction}
className="w-full bg-blue-600 text-white hover:bg-blue-700"
>
{updateMutation.isPending ? "保存中..." : "保存路由"}
</Button>
{updateMutation.isError ? (
<p className="text-xs text-rose-600 dark:text-rose-300">
保存失败:{updateMutation.error instanceof Error ? updateMutation.error.message : "请重试"}
</p>
) : null}
</CardContent>
</Card>
<Card className="border-slate-200/80 bg-white/90 shadow-sm backdrop-blur-sm dark:border-slate-800 dark:bg-slate-900/85">
<CardHeader>
<CardTitle className="text-base">生效路由</CardTitle>
<CardDescription>选中行即当前编辑目标。</CardDescription>
</CardHeader>
<CardContent>
{routingQuery.isLoading ? (
<p className="text-sm text-slate-500 dark:text-slate-400">加载中...</p>
) : routingQuery.isError ? (
<div className="rounded-xl border border-rose-200/70 bg-rose-50/80 p-4 dark:border-rose-900/40 dark:bg-rose-950/30">
<p className="text-sm font-medium text-rose-700 dark:text-rose-200">路由读取失败:{routingErrorText}</p>
<Button
className="mt-3"
variant="outline"
size="sm"
onClick={() => void routingQuery.refetch()}
disabled={routingQuery.isFetching}
>
{routingQuery.isFetching ? "重试中..." : "重试"}
</Button>
</div>
) : items.length === 0 ? (
<p className="text-sm text-slate-500 dark:text-slate-400">暂无路由配置。</p>
) : (
<div className="overflow-x-auto rounded-xl border border-slate-200/90 dark:border-slate-700">
<Table>
<TableHeader>
<TableRow>
<TableHead>函数</TableHead>
<TableHead>逻辑层</TableHead>
<TableHead>配置来源</TableHead>
<TableHead>规则</TableHead>
<TableHead>最近执行</TableHead>
<TableHead>升级</TableHead>
<TableHead className="text-right">操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{items.map((item) => (
<TableRow
key={item.function_name}
className={
item.function_name === selectedFunction
? "bg-blue-50/80 dark:bg-blue-950/35"
: "hover:bg-slate-50/70 dark:hover:bg-slate-800/60"
}
>
<TableCell>
<div className="font-medium">{item.function_name}</div>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{getFunctionUsageHint(item.function_name)}
</div>
</TableCell>
<TableCell>{toZhTierAlias(item.tier_alias)}</TableCell>
<TableCell>{toZhRoutingSource(item.source)}</TableCell>
<TableCell>
<div className="font-medium">{item.rule_name || "来自配置真源"}</div>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{typeof item.rule_priority === "number" ? `priority ${item.rule_priority}` : "tier default"}
</div>
</TableCell>
<TableCell>
<div className="font-medium">{describeLastExecution(item)}</div>
<div className="mt-1 text-xs text-slate-500 dark:text-slate-400">
{item.last_execution?.created_at ? formatBeijingDateTime(item.last_execution.created_at) : "-"}
</div>
</TableCell>
<TableCell>{describeUpgrade(item)}</TableCell>
<TableCell className="text-right">
<Button size="sm" variant="outline" onClick={() => setSelectedFunction(item.function_name)}>
编辑
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
)}
</CardContent>
</Card>
</div>
</div>
);
}