235 lines
11 KiB
TypeScript
235 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|