feat: 接入运维真实数据与三角色权限链路
- 新增 model_deploy 与 model_operations 双库查询,支持模型列表、模型详情、单月监控结果和运维工作台真实接口。 - 关闭运维模块生产 Mock 数据,补充缺表/空数据降级、工作台空状态和首条纵向链路测试。 - 按业务团队、模型团队、管理员权限矩阵接入菜单、页面、操作按钮和后端接口权限校验,支持 business_team 角色。 - 更新工作台布局、深色欢迎卡片、全宽页面适配、顶部回退,以及权限分组展示。 - 新增架构实现基线、周目标完成情况和角色权限矩阵初始化 SQL 文档。
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
import { Fragment, useState } from "react";
|
||||
import { CheckCircle2, Edit3, RotateCcw, Save, Sparkles } from "lucide-react";
|
||||
import { CheckCircle2, Edit3, Save, Sparkles } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
|
||||
import { Button } from "~/components/ui/button";
|
||||
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "~/components/ui/card";
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/components/ui/table";
|
||||
import { Textarea } from "~/components/ui/textarea";
|
||||
import { usePersistentState } from "~/hooks/use-persistent-state";
|
||||
import { FilterSelect, OperationsPageHeader } from "./OperationsUi";
|
||||
import { PROMPTS, PROMPT_REGRESSION, PROMPT_VERSIONS } from "./governanceData";
|
||||
|
||||
type PromptKey = keyof typeof PROMPTS;
|
||||
import { PROMPTS, PROMPT_REGRESSION, PROMPT_VERSIONS, type PromptKey } from "./governanceData";
|
||||
import { useCanOperationsAction } from "./operationsRole";
|
||||
|
||||
function PromptPreview({ text }: { text: string }) {
|
||||
const parts = text.split(/(\{\{[^}]+\}\})/g);
|
||||
@@ -18,30 +16,30 @@ function PromptPreview({ text }: { text: string }) {
|
||||
}
|
||||
|
||||
export default function PromptManagementPage() {
|
||||
const canEdit = useCanOperationsAction("prompts:manage");
|
||||
const canRegression = useCanOperationsAction("prompts:regression");
|
||||
const [promptKey, setPromptKey] = useState<PromptKey>("BC");
|
||||
const [texts, setTexts] = usePersistentState<Record<PromptKey, string>>("a-card-prompt-texts", { A: PROMPTS.A.text, BC: PROMPTS.BC.text });
|
||||
const [texts, setTexts] = useState<Record<PromptKey, string>>({ A: "", BC: "" });
|
||||
const [editing, setEditing] = useState(false);
|
||||
const [versions, setVersions] = usePersistentState("a-card-prompt-versions", PROMPT_VERSIONS);
|
||||
const [versions] = useState(PROMPT_VERSIONS);
|
||||
const prompt = PROMPTS[promptKey];
|
||||
const passed = PROMPT_REGRESSION.filter((item) => item.result === "通过").length;
|
||||
|
||||
const savePrompt = () => {
|
||||
const version = `P${Number(versions[0]?.version.slice(1) ?? 5) + 1}`;
|
||||
setVersions((current) => [{ version, author: "模型团队 李伟", createdAt: "2026-08-31 11:05", current: true, note: "手动保存并提交回归(Mock)" }, ...current.map((item) => ({ ...item, current: false }))]);
|
||||
setEditing(false);
|
||||
toast.success(`已保存为 ${version} 并提交回归评审`);
|
||||
toast.info("Prompt保存接口尚未接入");
|
||||
};
|
||||
|
||||
return (
|
||||
<section className="h-full overflow-auto bg-bg p-6">
|
||||
<div className="mx-auto flex max-w-screen-2xl flex-col gap-6 pb-8">
|
||||
<OperationsPageHeader title="报告 Prompt 管理" description="查看和调整提示词全文,保留版本记录,并通过历史样本回归后生效。" actions={<Button onClick={savePrompt}><Save />保存并提交回归</Button>} />
|
||||
<div className="flex w-full flex-col gap-6 pb-8">
|
||||
<OperationsPageHeader title="报告 Prompt 管理" description="查看和调整提示词全文,保留版本记录,并通过历史样本回归后生效。" actions={canRegression ? <Button onClick={savePrompt}><Save />保存并提交回归</Button> : undefined} />
|
||||
<div className="rounded-xl bg-brand-soft p-4 text-sm leading-6 text-primary">提示词查看与调整属于需求沟通补充项,建议在下一版需求文档中同步固化。平台注入变量由程序取数,不交由大模型计算。</div>
|
||||
|
||||
<div className="grid grid-cols-[minmax(0,1.5fr)_minmax(20rem,0.7fr)] gap-6">
|
||||
<Card>
|
||||
<CardHeader className="border-b border-border"><CardTitle className="flex items-center gap-2"><Sparkles className="size-5 text-primary" />提示词全文</CardTitle><CardDescription>变量占位符必须保持双花括号格式</CardDescription><CardAction className="flex items-end gap-2"><FilterSelect className="w-72" label="报告模板" value={promptKey} allLabel="请选择" options={[{ label: PROMPTS.A.name, value: "A" }, { label: PROMPTS.BC.name, value: "BC" }]} onChange={(value) => { setPromptKey(value as PromptKey); setEditing(false); }} /><Button variant="outline" size="sm" onClick={() => setEditing((value) => !value)}><Edit3 />{editing ? "退出编辑" : "编辑"}</Button></CardAction></CardHeader>
|
||||
<CardHeader className="border-b border-border"><CardTitle className="flex items-center gap-2"><Sparkles className="size-5 text-primary" />提示词全文</CardTitle><CardDescription>变量占位符必须保持双花括号格式</CardDescription><CardAction className="flex items-end gap-2"><FilterSelect className="w-72" label="报告模板" value={promptKey} allLabel="暂无模板" options={Object.entries(PROMPTS).map(([value, item]) => ({ label: item.name, value }))} onChange={(value) => { setPromptKey(value as PromptKey); setEditing(false); }} /><Button variant="outline" size="sm" disabled={!prompt || !canEdit} onClick={() => setEditing((value) => !value)}><Edit3 />{editing ? "退出编辑" : "编辑"}</Button></CardAction></CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
{editing ? <Textarea className="min-h-[36rem] font-mono text-xs leading-6" value={texts[promptKey]} onChange={(event) => setTexts((current) => ({ ...current, [promptKey]: event.target.value }))} /> : <PromptPreview text={texts[promptKey]} />}
|
||||
{prompt ? (editing ? <Textarea className="min-h-[36rem] font-mono text-xs leading-6" value={texts[promptKey]} onChange={(event) => setTexts((current) => ({ ...current, [promptKey]: event.target.value }))} /> : <PromptPreview text={texts[promptKey]} />) : <div className="grid min-h-[36rem] place-items-center rounded-xl border border-dashed border-border text-sm text-muted-foreground">暂无 Prompt 数据</div>}
|
||||
<p className="rounded-xl bg-muted/50 p-4 text-xs leading-5 text-muted-foreground">高亮内容为程序注入变量。等级字段仅用于选择模板,不进入报告正文;Prompt 禁止自行计算指标或臆测业务原因。</p>
|
||||
</CardContent>
|
||||
</Card>
|
||||
@@ -49,7 +47,7 @@ export default function PromptManagementPage() {
|
||||
<div className="space-y-6">
|
||||
<Card>
|
||||
<CardHeader><CardTitle>版本留痕</CardTitle><CardDescription>当前生效与历史版本</CardDescription></CardHeader>
|
||||
<CardContent className="space-y-3">{versions.map((version) => <div className="flex items-center gap-3 rounded-xl border border-border p-4" key={version.version}><b className="text-sm text-foreground">{version.version}</b><span className="min-w-0 flex-1"><small className="block truncate text-xs text-muted-foreground">{version.note}</small><small className="text-3xs text-muted-foreground">{version.author} · {version.createdAt}</small></span>{version.current ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">生效中</span> : <Button variant="outline" size="icon-xs" aria-label={`回滚至 ${version.version}`} onClick={() => { setVersions((current) => current.map((item) => ({ ...item, current: item.version === version.version }))); toast.success(`Prompt 已模拟回滚至 ${version.version}`); }}><RotateCcw /></Button>}</div>)}</CardContent>
|
||||
<CardContent className="space-y-3">{versions.length ? versions.map((version) => <div className="flex items-center gap-3 rounded-xl border border-border p-4" key={version.version}><b className="text-sm text-foreground">{version.version}</b><span className="min-w-0 flex-1"><small className="block truncate text-xs text-muted-foreground">{version.note}</small><small className="text-3xs text-muted-foreground">{version.author} · {version.createdAt}</small></span>{version.current ? <span className="rounded-full bg-success-soft px-2.5 py-1 text-xs text-success-strong">生效中</span> : <span className="rounded-full bg-muted px-2.5 py-1 text-xs text-muted-foreground">历史</span>}</div>) : <div className="py-10 text-center text-sm text-muted-foreground">暂无 Prompt 版本数据</div>}</CardContent>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
|
||||
Reference in New Issue
Block a user