From 96e0d695c088073201bee16c8053cdefc9bf2308 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Wed, 10 Jun 2026 11:23:01 +0800 Subject: [PATCH] =?UTF-8?q?feat(skills,knowledge):=20=E4=B8=AD=E6=96=87?= =?UTF-8?q?=E5=90=8D=E8=A1=A5=E5=85=A8=E4=B8=8E=E7=9F=A5=E8=AF=86=E5=BA=93?= =?UTF-8?q?=E6=B2=89=E6=B7=80=E6=94=AF=E6=8C=81=E5=8F=AF=E9=80=89=E5=A4=A7?= =?UTF-8?q?=E6=A8=A1=E5=9E=8B=E5=B9=B6=E6=98=8E=E7=A1=AE=E5=A4=A7=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E6=8A=A5=E9=94=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 技能中文名: - 单个「AI 生成」与「一键补充中文名」均可在请求体 model_name 选择大模型 - 一键补充改为仅管理员开放 - _generate_skill_name_zh 在模型自身失败(构建/超时/API 报错)时抛 SkillNameZhModelError,与模型返回无效名称区分;单个接口返回 502「大模型报错,无法补全技能中文名」,批量在 model_error 中带回代表性报错 知识库沉淀: - 对话保存到知识库 / 批量导入均新增可选 model_name(透传至 capture_thread → extract_thread_llm_multi) - 批量返回 llm_fallback 计数;模型异常时仍按既有逻辑回退规则抽取并落草稿, 前端明确提示「已改用规则抽取」(保存按钮保持后台异步) 前端新增 components/workspace/model-name-select.tsx 复用下拉(默认=系统配置模型) Co-Authored-By: Claude Opus 4.8 (1M context) --- .../workspace/knowledge-save-trigger.tsx | 34 +++-- .../workspace/model-name-select.tsx | 59 +++++++++ .../settings/skill-settings-page.tsx | 59 ++++++--- .../workspace/skills/skill-detail-dialog.tsx | 18 ++- frontend-web/src/core/knowledge/types.ts | 13 +- frontend-web/src/core/skills/api.ts | 21 ++- frontend-web/src/core/skills/hooks.ts | 7 +- frontend-web/src/pages/KnowledgeBasePage.tsx | 26 +++- .../backend/app/gateway/routers/knowledge.py | 15 ++- .../backend/app/gateway/routers/skills.py | 120 ++++++++++++------ .../harness/deerflow/knowledge/service.py | 6 +- 11 files changed, 295 insertions(+), 83 deletions(-) create mode 100644 frontend-web/src/components/workspace/model-name-select.tsx diff --git a/frontend-web/src/components/workspace/knowledge-save-trigger.tsx b/frontend-web/src/components/workspace/knowledge-save-trigger.tsx index e6bbcfec..17cd62ba 100644 --- a/frontend-web/src/components/workspace/knowledge-save-trigger.tsx +++ b/frontend-web/src/components/workspace/knowledge-save-trigger.tsx @@ -1,17 +1,21 @@ "use client"; import { BookMarked } from "lucide-react"; +import { useState } from "react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { useKnowledgeActions } from "@/core/knowledge/hooks"; +import { ModelNameSelect } from "./model-name-select"; import { useThread } from "./messages/context"; import { Tooltip } from "./tooltip"; export function KnowledgeSaveTrigger({ threadId }: { threadId: string }) { const { thread } = useThread(); const { ingestThread } = useKnowledgeActions(); + // Optional model override for the extraction (null = configured default). + const [model, setModel] = useState(null); if (thread.messages.length === 0) { return null; @@ -23,7 +27,7 @@ export function KnowledgeSaveTrigger({ threadId }: { threadId: string }) { try { await ingestThread.mutateAsync({ threadId, - payload: { background: true }, + payload: { background: true, model_name: model }, }); toast.success("已加入知识库保存队列,正在后台整理…"); } catch (error) { @@ -32,16 +36,24 @@ export function KnowledgeSaveTrigger({ threadId }: { threadId: string }) { }; return ( - - - + triggerClassName="h-8 w-[8.5rem]" + /> + + + + ); } diff --git a/frontend-web/src/components/workspace/model-name-select.tsx b/frontend-web/src/components/workspace/model-name-select.tsx new file mode 100644 index 00000000..327aaa63 --- /dev/null +++ b/frontend-web/src/components/workspace/model-name-select.tsx @@ -0,0 +1,59 @@ +"use client"; + +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { useModels } from "@/core/models/hooks"; +import { cn } from "@/lib/utils"; + +/** Sentinel for "use the backend's default/configured model" (no override). */ +const DEFAULT_VALUE = "__default__"; + +/** + * Compact dropdown to optionally pick an LLM model for a one-off action + * (skill-name generation, knowledge ingestion, …). The value is the model + * `name`; `null` means "use the configured default" (no override sent). + */ +export function ModelNameSelect({ + value, + onChange, + disabled, + className, + triggerClassName, + defaultLabel = "默认模型", +}: { + value: string | null; + onChange: (modelName: string | null) => void; + disabled?: boolean; + className?: string; + triggerClassName?: string; + defaultLabel?: string; +}) { + const { models } = useModels(); + return ( + + ); +} diff --git a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx index 7dc278ec..c43b2d1e 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -57,6 +57,7 @@ import { import { Input } from "@/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs"; +import { ModelNameSelect } from "@/components/workspace/model-name-select"; import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog"; import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; @@ -163,6 +164,8 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { const [viewMode, setViewMode] = useViewMode("skill"); const reconcileMutation = useReconcileCustomSkills(); const fillNamesMutation = useGenerateMissingSkillNameZh(); + // Optional model override for the admin "一键补充中文名" action (null = default). + const [fillModel, setFillModel] = useState(null); // Name + tag filtering is resolved server-side via the /api/skills query. const { skills, isLoading, error } = useSkills({ search, tagIds, sort }); @@ -249,13 +252,18 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { const handleFillNames = async () => { try { - const result = await fillNamesMutation.mutateAsync(); + const result = await fillNamesMutation.mutateAsync(fillModel); const okCount = result.generated.length; const failCount = result.failed.length; if (okCount === 0 && failCount === 0) { toast.info("所有技能均已配置中文名称"); } else if (failCount === 0) { toast.success(`已为 ${okCount} 个技能补充中文名称`); + } else if (result.model_error) { + // Failures came from the model itself — say so explicitly. + toast.warning( + `已补充 ${okCount} 个,${failCount} 个因大模型报错失败:${result.model_error}`, + ); } else { toast.warning(`已补充 ${okCount} 个,${failCount} 个生成失败,可稍后重试或手动填写`); } @@ -354,24 +362,37 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) { ? t.settings.skills.rescanSkillsRunning : t.settings.skills.rescanSkills} - + {isAdmin && ( + <> + + + + )}