Merge remote-tracking branch 'origin/master' into ji

# Conflicts:
#	frontend-web/src/components/workspace/settings/memory-settings-page.tsx
#	frontend-web/src/core/auth/sync.tsx
This commit is contained in:
吉鑫升 committed 2026-06-04 17:49:26 +08:00
commit c1336ad5b3
21 files changed
+877 -181

No files matched your search

@@ -767,7 +767,10 @@ export function ArtifactFilePreview({
return;
}
const blob = new Blob([content ?? ""], { type: "text/html" });
// 必须带 charset=utf-8:blob: URL 的 Content-Type 不含 charset 时,iframe 会按
// 系统默认编码(中文 Windows 常为 GBK/Latin-1)解码,模型生成的 HTML 若没写
// <meta charset> 就会中文乱码。与下方下载/新窗口打开的 blobType 保持一致。
const blob = new Blob([content ?? ""], { type: "text/html;charset=utf-8" });
const url = URL.createObjectURL(blob);
setHtmlPreviewUrl(url);
@@ -1,6 +1,6 @@
"use client";
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon } from "lucide-react";
import { PenLineIcon, PlusIcon, SearchIcon, Trash2Icon, Heart, BookOpen, Map, Zap, Target, ShieldAlert } from "lucide-react";
import { useMemo, useState } from "react";
import { toast } from "sonner";
@@ -464,23 +464,41 @@ const HISTORY_LABELS: Record<string, string> = {
longTermBackground: "长期背景",
};
const CATEGORY_COLORS: Record<string, string> = {
preference: "bg-blue-100 text-blue-700",
knowledge: "bg-green-100 text-green-700",
context: "bg-yellow-100 text-yellow-700",
behavior: "bg-purple-100 text-purple-700",
goal: "bg-orange-100 text-orange-700",
correction: "bg-red-100 text-red-700",
const CATEGORY_LABELS: Record<string, Record<string, string>> = {
zh: {
preference: "偏好",
knowledge: "知识",
context: "背景",
behavior: "行为",
goal: "目标",
correction: "纠偏",
},
en: {
preference: "Preference",
knowledge: "Knowledge",
context: "Context",
behavior: "Behavior",
goal: "Goal",
correction: "Correction",
},
};
/** 事实分类的中文标签。 */
const CATEGORY_LABELS: Record<string, string> = {
preference: "偏好",
knowledge: "知识",
context: "背景",
behavior: "行为",
goal: "目标",
correction: "纠正",
const CATEGORY_COLORS: Record<string, string> = {
preference: "bg-blue-50 text-blue-700 border border-blue-200/50 dark:bg-blue-950/30 dark:text-blue-400 dark:border-blue-900/30",
knowledge: "bg-green-50 text-green-700 border border-green-200/50 dark:bg-green-950/30 dark:text-green-400 dark:border-green-900/30",
context: "bg-amber-50 text-amber-700 border border-amber-200/50 dark:bg-amber-950/30 dark:text-amber-400 dark:border-amber-900/30",
behavior: "bg-purple-50 text-purple-700 border border-purple-200/50 dark:bg-purple-950/30 dark:text-purple-400 dark:border-purple-900/30",
goal: "bg-orange-50 text-orange-700 border border-orange-200/50 dark:bg-orange-950/30 dark:text-orange-400 dark:border-orange-900/30",
correction: "bg-red-50 text-red-700 border border-red-200/50 dark:bg-red-950/30 dark:text-red-400 dark:border-red-900/30",
};
const CATEGORY_ICONS: Record<string, React.ElementType> = {
preference: Heart,
knowledge: BookOpen,
context: Map,
behavior: Zap,
goal: Target,
correction: ShieldAlert,
};
/** V1 记忆只读视图:展示 memory.json 的全部字段,Tab 布局。 */
@@ -494,6 +512,9 @@ function MemoryV1View({
memory_usage: string;
};
}) {
const { locale } = useI18n();
const isZh = locale.startsWith("zh");
const labelMap = isZh ? CATEGORY_LABELS.zh : CATEGORY_LABELS.en;
const ctxEntries = Object.entries(memory.user_context ?? {}).filter(([, v]) => v);
const historyEntries = Object.entries(memory.history ?? {}).filter(([, v]) => v);
@@ -504,9 +525,8 @@ function MemoryV1View({
V1 模式:记忆由系统在对话结束后自动提取,无需手动管理。
</div>
<Tabs defaultValue="profile">
<Tabs defaultValue="facts">
<TabsList>
<TabsTrigger value="profile">用户画像 &amp; 历史</TabsTrigger>
<TabsTrigger value="facts">
事实记录
{memory.facts.length > 0 && (
@@ -515,8 +535,81 @@ function MemoryV1View({
</span>
)}
</TabsTrigger>
<TabsTrigger value="profile">用户画像 &amp; 历史</TabsTrigger>
</TabsList>
{/* 事实记录 Tab */}
<TabsContent value="facts" className="mt-3">
<div className="rounded-lg border p-4 space-y-3 dark:border-[#213D5E] dark:bg-[#022248]">
<div className="flex items-center justify-between">
<h3 className="text-base font-medium">事实记录</h3>
<span className="text-xs text-muted-foreground">{memory.memory_usage}</span>
</div>
{memory.facts.length === 0 ? (
<div className="rounded-md border border-dashed p-3 text-sm text-muted-foreground ">
暂无事实记录,对话后系统会自动提取。
</div>
) : (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-5 pt-2">
{memory.facts.map((fact) => {
const categoryLabel = labelMap[fact.category] ?? fact.category;
const Icon = CATEGORY_ICONS[fact.category] ?? PlusIcon;
return (
<div
key={fact.id}
className="group relative flex flex-col justify-between rounded-xl border border-border/40 bg-gradient-to-br from-card to-card/50 hover:from-card hover:to-card hover:border-primary/30 hover:shadow-md hover:-translate-y-0.5 transition-all duration-300 p-5 space-y-4 dark:border-[#213D5E]/40 dark:from-[#022248]/40 dark:to-[#022248]/10 dark:hover:from-[#022248]/60 dark:hover:to-[#022248]/30 overflow-hidden"
>
{/* Subtle top highlight */}
<div className="absolute inset-x-0 top-0 h-px bg-gradient-to-r from-transparent via-primary/10 to-transparent opacity-0 group-hover:opacity-100 transition-opacity duration-300" />
<div className="space-y-4">
<div className="flex items-center justify-between gap-2">
<span
className={`inline-flex items-center gap-1.5 rounded-full px-2.5 py-1 text-xs font-medium border shadow-sm ${
CATEGORY_COLORS[fact.category] ?? "bg-gray-100 text-gray-700 border-gray-200"
}`}
>
<Icon className="w-3.5 h-3.5" />
{categoryLabel}
</span>
<div className="flex flex-col items-end gap-1">
<span className="text-[11px] text-muted-foreground font-semibold tracking-wider">
{Math.round(fact.confidence * 100)}%
</span>
{/* Confidence Progress Bar */}
<div className="h-1 w-12 rounded-full bg-muted overflow-hidden">
<div
className="h-full rounded-full bg-primary transition-all duration-500"
style={{ width: `${Math.round(fact.confidence * 100)}%` }}
/>
</div>
</div>
</div>
<p className="text-[14px] text-foreground/90 leading-relaxed whitespace-pre-wrap [overflow-wrap:anywhere]">
{desensitize(fact.content)}
</p>
</div>
{fact.sourceError && (
<div className="mt-auto pt-2">
<div className="rounded-lg bg-red-50/80 dark:bg-red-950/30 border border-red-100 dark:border-red-900/50 p-3 text-xs text-red-600 dark:text-red-400 shadow-sm">
<div className="flex items-center gap-1.5 font-semibold mb-1">
<ShieldAlert className="w-3.5 h-3.5" />
<span>避免</span>
</div>
<p className="leading-relaxed opacity-90">{desensitize(fact.sourceError)}</p>
</div>
</div>
)}
</div>
);
})}
</div>
)}
</div>
</TabsContent>
{/* 用户画像 & 历史摘要 Tab */}
<TabsContent value="profile" className="mt-3 space-y-4">
{/* 用户画像 (userContext) */}
@@ -557,49 +650,6 @@ function MemoryV1View({
)}
</div>
</TabsContent>
{/* 事实记录 Tab */}
<TabsContent value="facts" className="mt-3">
<div className="rounded-lg border p-4 space-y-3 dark:border-[#213D5E] dark:bg-[#022248]">
<div className="flex items-center justify-between">
<h3 className="text-base font-medium">事实记录</h3>
<span className="text-xs text-muted-foreground">{memory.memory_usage}</span>
</div>
{memory.facts.length === 0 ? (
<div className="rounded-md border border-dashed p-3 text-sm text-muted-foreground ">
暂无事实记录,对话后系统会自动提取。
</div>
) : (
<div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
{memory.facts.map((fact) => (
<div
key={fact.id}
className="flex flex-col gap-2 rounded-lg border p-3 shadow-sm dark:border-[#213D5E]"
>
<div className="flex items-center justify-between gap-2">
<span
className={`rounded px-1.5 py-0.5 text-xs font-medium ${
CATEGORY_COLORS[fact.category] ?? "bg-gray-100 text-gray-700"
}`}
>
{CATEGORY_LABELS[fact.category] ?? fact.category}
</span>
<span className="shrink-0 text-xs font-medium text-muted-foreground">
置信度 {Math.round(fact.confidence * 100)}%
</span>
</div>
<p className="text-sm whitespace-pre-wrap [overflow-wrap:anywhere]">{desensitize(fact.content)}</p>
{fact.sourceError && (
<p className="rounded bg-red-50 px-2 py-1 text-xs text-red-600 dark:bg-[#311629] dark:text-[#EE212C]">
避免:{desensitize(fact.sourceError)}
</p>
)}
</div>
))}
</div>
)}
</div>
</TabsContent>
</Tabs>
</div>
);
@@ -29,6 +29,13 @@ import { toast } from "sonner";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
@@ -45,6 +52,7 @@ import {
EmptyMedia,
EmptyTitle,
} from "@/components/ui/empty";
import {
Item,
ItemActions,
@@ -358,6 +366,8 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
{!isLoading && !error && filteredSkills.length === 0 && (
<EmptySkill onCreateSkill={handleCreateSkill} />
)}
{/* —— 原纵向列表布局(保留备查)——
{!isLoading && !error &&
filteredSkills.length > 0 &&
pagedSkills.map((skill) => (
@@ -367,7 +377,21 @@ function SkillSettingsList({ onClose }: { onClose?: () => void }) {
currentUserId={currentUserId}
isAdmin={isAdmin}
/>
))}
))} */}
{/* card */}
{!isLoading && !error && filteredSkills.length > 0 && (
<div className="grid grid-cols-1 items-stretch gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
{pagedSkills.map((skill) => (
<SkillRow
key={skill.name}
skill={skill}
currentUserId={currentUserId}
isAdmin={isAdmin}
className="h-full"
/>
))}
</div>
)}
{!isLoading && !error && totalPages > 1 && (
<div className="flex items-center justify-end gap-2 pt-1">
<Button
@@ -420,10 +444,12 @@ export function SkillRow({
skill,
currentUserId,
isAdmin,
className,
}: {
skill: Skill;
currentUserId: string | null;
isAdmin: boolean;
className?: string;
}) {
const { t } = useI18n();
const setPublished = useSetSkillPublished();
@@ -509,6 +535,8 @@ export function SkillRow({
return (
<>
{/* —— 原 Item 列表行布局(保留备查)——
<Item className="w-full dark:border-[#213D5E] dark:bg-[#02244C] dark:text-white" variant="outline">
<ItemContent
className="cursor-pointer"
@@ -676,7 +704,205 @@ export function SkillRow({
</Button>
)}
</ItemActions>
</Item>
</Item> */}
<Card
className={cn(
"group flex h-full min-w-0 cursor-pointer flex-col transition-shadow hover:shadow-md dark:!border-[#213D5E] dark:bg-[#02244C] dark:text-white",
className,
)}
onClick={() => setDetailOpen(true)}
>
<CardHeader className="min-w-0 flex flex-col gap-0">
<div className="flex min-w-0 items-center gap-2">
<div className="bg-primary/10 text-primary flex h-9 w-9 shrink-0 items-center justify-center rounded-lg">
<SparklesIcon className="h-5 w-5" />
</div>
<div className="flex min-w-0 flex-1 flex-wrap items-center gap-2 overflow-hidden">
<CardTitle
className="min-w-0 flex-1 overflow-hidden text-base text-ellipsis whitespace-nowrap"
title={desensitize(skill.name)}
>
{desensitize(skill.name)}
</CardTitle>
<Badge variant="secondary" className="shrink-0 text-xs">
{isBuiltin
? t.settings.skills.badgeBuiltin
: skill.category}
</Badge>
</div>
</div>
<div className="mt-4 mb-1.5 flex min-h-5 flex-wrap items-center gap-1">
{isOwned && (
<Badge
variant="default"
className="bg-emerald-500/15 text-emerald-700 hover:bg-emerald-500/15 dark:text-emerald-300 text-xs"
>
{t.settings.skills.badgeMine}
</Badge>
)}
{isFromOthers && (
<Badge
variant="outline"
className="border-blue-500/30 text-blue-700 dark:text-blue-300 text-xs"
title={
skill.owner_user_id
? t.settings.skills.badgeFromOthersTitle.replace(
"{user_id}",
skill.owner_user_id,
)
: undefined
}
>
{t.settings.skills.badgeFromOthers}
</Badge>
)}
{isOwned && skill.published && (
<Badge
variant="outline"
className="border-sky-500/30 text-sky-700 dark:text-sky-300 text-xs"
title={t.settings.skills.badgePublishedTitle}
>
<GlobeIcon className="size-3" />
{t.settings.skills.badgePublished}
</Badge>
)}
{isCustom && !isBuiltin && skill.source === "agent" && (
<Badge
variant="outline"
className="border-cyan-500/30 text-cyan-700 dark:text-cyan-300 text-xs"
title={t.settings.skills.badgeSourceAgentTitle}
>
{t.settings.skills.badgeSourceAgent}
</Badge>
)}
{isCustom && !isBuiltin && skill.source === "upload" && (
<Badge
variant="outline"
className="border-zinc-400/40 text-zinc-600 dark:text-zinc-400 text-xs"
title={t.settings.skills.badgeSourceUploadTitle}
>
{t.settings.skills.badgeSourceUpload}
</Badge>
)}
{skill.pinned && (
<Badge
variant="outline"
className="border-purple-500/30 text-purple-700 dark:text-purple-300 text-xs"
title={t.settings.skills.badgePinnedTitle}
>
{t.settings.skills.badgePinned}
</Badge>
)}
</div>
<div className="mt-1.5 min-h-10 mb-3">
{skill.description ? (
<CardDescription className="line-clamp-2 text-sm">
{desensitize(skill.description)}
</CardDescription>
) : null}
</div>
<div className="text-muted-foreground mt-0.5 min-h-4 text-xs leading-tight">
{skill.use_count !== undefined && skill.use_count > 0
? t.settings.skills.useCountInline.replace(
"{count}",
String(skill.use_count),
)
: t.settings.skills.useCountInlineZero}
</div>
<div
className={cn(
"min-w-0 max-w-full overflow-hidden pb-0",
(skill.tags?.length ?? 0) > 0 ? "pt-2.5" : "min-h-4",
)}
>
{(skill.tags?.length ?? 0) > 0 && (
<TagChipList tags={skill.tags!} className="w-full" />
)}
</div>
</CardHeader>
<CardFooter
className="cursor-default py-0 pt-1.5"
onClick={(e) => e.stopPropagation()}
>
<div className="flex w-full flex-wrap items-center justify-end gap-1">
{isAdmin && (
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
onClick={() => setTagPickerOpen(true)}
title={t.adminTags.manageTags}
>
<TagsIcon className="h-3.5 w-3.5" />
</Button>
)}
{canManage && (
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
disabled={setPinned.isPending}
onClick={() => void handleTogglePinned()}
title={
skill.pinned
? t.settings.skills.unpinAction
: t.settings.skills.pinAction
}
>
{skill.pinned ? (
<PinOffIcon className="h-3.5 w-3.5" />
) : (
<PinIcon className="h-3.5 w-3.5" />
)}
</Button>
)}
{canTogglePublish && (
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
disabled={setPublished.isPending}
onClick={() => void handleTogglePublished(!skill.published)}
title={
skill.published
? t.settings.skills.unpublishAction
: t.settings.skills.publishAction
}
>
{skill.published ? (
<LockIcon className="h-3.5 w-3.5" />
) : (
<GlobeIcon className="h-3.5 w-3.5" />
)}
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-8 w-8 shrink-0"
disabled={duplicateSkillMutation.isPending}
onClick={handleOpenDuplicate}
title="复制为新技能"
>
<CopyIcon className="h-3.5 w-3.5" />
</Button>
{canManage && (
<Button
size="icon"
variant="ghost"
className="text-destructive hover:text-destructive h-8 w-8 shrink-0"
onClick={() => setConfirmDeleteOpen(true)}
title={t.settings.skills.deleteAction}
>
<Trash2Icon className="h-3.5 w-3.5" />
</Button>
)}
</div>
</CardFooter>
</Card>
<Dialog open={duplicateOpen} onOpenChange={setDuplicateOpen}>
<DialogContent>
<DialogHeader>
@@ -17,20 +17,22 @@ export function TagChip({
className?: string;
}) {
const color = resolveTagColor(tag.color);
const label = desensitize(tag.name);
return (
<span
className={cn(
"inline-flex items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium",
"inline-flex max-w-full min-w-0 items-center gap-1 rounded-full border px-2 py-0.5 text-xs font-medium",
color.chip,
className,
)}
title={label}
>
{desensitize(tag.name)}
<span className="min-w-0 truncate">{label}</span>
{onRemove && (
<button
type="button"
onClick={onRemove}
className="hover:opacity-70"
className="shrink-0 hover:opacity-70"
aria-label={`remove ${tag.name}`}
>
<XIcon className="size-3" />
@@ -50,7 +52,7 @@ export function TagChipList({
}) {
if (!tags || tags.length === 0) return null;
return (
<div className={cn("flex flex-wrap gap-1", className)}>
<div className={cn("flex min-w-0 max-w-full flex-wrap gap-1", className)}>
{tags.map((tag) => (
<TagChip key={tag.name} tag={tag} />
))}