diff --git a/_skills_src/ai-skills b/_skills_src/ai-skills new file mode 160000 index 00000000..cde1c838 --- /dev/null +++ b/_skills_src/ai-skills @@ -0,0 +1 @@ +Subproject commit cde1c8385ead64df501c9b214e63d7ba9b283ff9 diff --git a/_skills_src/anthropics-skills b/_skills_src/anthropics-skills new file mode 160000 index 00000000..57546260 --- /dev/null +++ b/_skills_src/anthropics-skills @@ -0,0 +1 @@ +Subproject commit 57546260929473d4e0d1c1bb75297be2fdfa1949 diff --git a/_skills_src/awesome-claude-skills b/_skills_src/awesome-claude-skills new file mode 160000 index 00000000..92568c1e --- /dev/null +++ b/_skills_src/awesome-claude-skills @@ -0,0 +1 @@ +Subproject commit 92568c1edaff1bde5371154f036d959346c145a8 diff --git a/_skills_src/ce-kit b/_skills_src/ce-kit new file mode 160000 index 00000000..d4973c48 --- /dev/null +++ b/_skills_src/ce-kit @@ -0,0 +1 @@ +Subproject commit d4973c48270c66f4a4751131ec84b9dbb03669dc diff --git a/_skills_src/csm b/_skills_src/csm new file mode 160000 index 00000000..3fa16a94 --- /dev/null +++ b/_skills_src/csm @@ -0,0 +1 @@ +Subproject commit 3fa16a94e0aba3509e4d4b318ca175d3191b5b7d diff --git a/_skills_src/superpowers b/_skills_src/superpowers new file mode 160000 index 00000000..6fd45076 --- /dev/null +++ b/_skills_src/superpowers @@ -0,0 +1 @@ +Subproject commit 6fd4507659784c351abbd2bc264c7162cfd386dc diff --git a/_skills_src/tapestry-skills b/_skills_src/tapestry-skills new file mode 160000 index 00000000..80e1dc56 --- /dev/null +++ b/_skills_src/tapestry-skills @@ -0,0 +1 @@ +Subproject commit 80e1dc56df74d1cb849ad649c7ead9756e7929bb diff --git a/frontend-web/docs/embed-public-pages-theme.md b/frontend-web/docs/embed-public-pages-theme.md new file mode 100644 index 00000000..6a4c8c69 --- /dev/null +++ b/frontend-web/docs/embed-public-pages-theme.md @@ -0,0 +1,154 @@ +# 公开页 · iframe 嵌入外观对接说明(主题 / 背景色) + +本文档面向**嵌入方(宿主系统)**,说明如何在用 ` + + + + + + +``` + +--- + +## 3. postMessage(嵌入后动态切换) + +适用于宿主站点支持用户切主题、需要 iframe 实时跟随的场景。向 iframe 的 `contentWindow` 发消息即可,**无需等待子页面回执**(监听器在应用挂载时注册)。 + +### 3.1 消息格式 + +```js +const frame = document.getElementById("deerflow-frame"); + +// 切整体明暗主题(theme 取值同 2.2) +frame.contentWindow.postMessage({ type: "deerflow:set-theme", theme: "dark" }, "*"); + +// 改背景色 / 文字色(字段名与 URL 参数一致,bg/background、text/fg 均可) +frame.contentWindow.postMessage({ type: "deerflow:set-bg", bg: "#001529", text: "#fff" }, "*"); +// 兼容别名:type 也可写 "deerflow:set-background" +``` + +说明: + +- 两类消息相互独立,可只发其一;`set-bg` 里 `bg`、`text` 也可只传其一,未传的字段保持当前值。 +- 非法颜色值 / 不认识的 `theme` 取值会被忽略,不会清空已有设置。 +- targetOrigin 建议按需收紧(写 DeerFlow 站点的 origin 而非 `"*"`)。 + +### 3.2 宿主主题联动示例 + +```js +function syncDeerflowTheme(isDark) { + const win = document.getElementById("deerflow-frame").contentWindow; + win.postMessage({ type: "deerflow:set-theme", theme: isDark ? "dark" : "light" }, "*"); + win.postMessage( + { type: "deerflow:set-bg", bg: isDark ? "#001529" : "#ffffff" }, + "*", + ); +} + +// iframe 首次加载用 URL 参数兜底,之后切换走 postMessage +myThemeStore.subscribe((theme) => syncDeerflowTheme(theme === "dark")); +``` + +--- + +## 4. 优先级与默认行为 + +同一项配置多处传入时,按以下优先级取值(高 → 低): + +1. **postMessage** 动态设置的值 +2. hash 路由内的 URL 参数(`#/路由?theme=...`) +3. `#` 之前的 URL 参数(`?theme=...#/路由`) +4. 都没传 → DeerFlow 默认配色(浅色主题 + 页面自带 Tailwind 配色) + +其他保证: + +- 外部主题只影响**当前 iframe 的渲染**,不写 localStorage——不会污染用户直接访问 DeerFlow 主站时自己选择的主题。 +- `bg`/`text` 是内联样式,作用在页面根容器上,优先级高于主题变量;与 `theme` 叠加时以 `bg`/`text` 为准。 + +--- + +## 5. 常见问题排查 + +| 现象 | 排查 | +|------|------| +| `theme=dark` 不生效 | 确认路由属于第 1 节列出的公开页;确认参数拼在 hash 路由后(`/#/share/xx?theme=dark`),而不是写成了 `/#/share/xx#theme=dark` | +| `bg=#fff` 不生效 | URL 里 `#` 没编码会被浏览器截断,必须写 `%23fff`;或直接省略 `#` 写 `bg=fff` | +| postMessage 无反应 | 确认消息体是对象且 `type` 拼写为 `deerflow:set-theme` / `deerflow:set-bg`;确认在 iframe `load` 之后发送 | +| 颜色被忽略 | 检查是否符合 2.3 白名单格式(如 `linear-gradient(...)` 等复杂值不支持) | +| 主题切了但个别区域颜色没变 | 该区域可能使用了固定配色;把页面路由与截图反馈给 DeerFlow 维护方补充 `dark:` 样式 | diff --git a/frontend-web/src/App.tsx b/frontend-web/src/App.tsx index e2c8695c..6ead2099 100644 --- a/frontend-web/src/App.tsx +++ b/frontend-web/src/App.tsx @@ -10,6 +10,12 @@ import { detectLocale } from "@/core/i18n/locale"; import { resolveForcedThemeForPath } from "@/core/page-layout/chats-iframe-route"; import { ChatProvider } from "@/contexts/ChatContext"; import { ThemeProvider } from "@/contexts/ThemeContext"; +import { + resolveEmbedRoundtableTheme, + resolveEmbedTheme, +} from "@/core/embed/params"; +import { usePublicEmbedTheme } from "@/core/embed/background"; +import { readWujieProps } from "@/core/embed/wujie"; import { HashRouter, useLocation } from "react-router-dom"; @@ -27,10 +33,32 @@ import EmbedRoundtablePage from "./pages/EmbedRoundtablePage"; import { ChatRuntime } from "./pages/WorkspaceRoutes"; import { NotFoundPage } from "./pages/NotFoundPage"; +const PUBLIC_EMBED_PREFIXES = ["/share/", "/public/", "/roundtable-share/"]; + function AppProviders({ children }: { children: React.ReactNode }) { const locale = useMemo(() => detectLocale(), []); const location = useLocation(); - const forcedTheme = resolveForcedThemeForPath(location.pathname); + const publicEmbedTheme = usePublicEmbedTheme(); + const forcedTheme = useMemo(() => { + const routeTheme = resolveForcedThemeForPath(location.pathname); + if (routeTheme) return routeTheme; + if (location.pathname === "/embed/roundtable") { + const wp = readWujieProps(); + const raw = + (typeof wp.theme === "string" && wp.theme.trim()) || + new URLSearchParams(location.search).get("theme")?.trim() || + "dark"; + return resolveEmbedRoundtableTheme(raw); + } + if (location.pathname.startsWith("/embed/chats")) { + const raw = new URLSearchParams(location.search).get("theme")?.trim(); + return raw ? resolveEmbedTheme(raw) : undefined; + } + if (PUBLIC_EMBED_PREFIXES.some((p) => location.pathname.startsWith(p))) { + return publicEmbedTheme; + } + return undefined; + }, [location.pathname, location.search, publicEmbedTheme]); return ( - {/* QueryClientProvider must wrap ThemeProvider because the custom - ThemeProvider reads the admin-configured default via useQuery. */} @@ -67,11 +93,8 @@ export function App() { } /> } /> } /> - {/* Public read-only conversation viewer — no authentication. */} } /> - {/* Public read-only user leaderboard publish page — no authentication. */} } /> - {/* Public read-only roundtable share — landing + full result report. */} } @@ -80,7 +103,6 @@ export function App() { path="/roundtable-share/:shareCode/report" element={} /> - {/* Public read-only scheduled-task runs viewer — no authentication. */} } @@ -89,7 +111,6 @@ export function App() { path="/public/scheduled-tasks/:taskId/runs/:runId/fullscreen" element={} /> - {/* Embedded workspace Q&A — no page/workspace sidebars; parent passes message/username/theme */} } /> - {/* Embedded roundtable planning — body only, no sidebars; username login (not wujie ticket) */} } /> } /> } /> @@ -106,4 +126,4 @@ export function App() { ); -} +} diff --git a/frontend-web/src/components/wiki/RelatedNotesDialog.tsx b/frontend-web/src/components/wiki/RelatedNotesDialog.tsx new file mode 100644 index 00000000..2b07940a --- /dev/null +++ b/frontend-web/src/components/wiki/RelatedNotesDialog.tsx @@ -0,0 +1,186 @@ +import { + ChevronLeft, + ChevronRight, + FileText, + Hash, + Loader2, + Search, + Sparkles, +} from "lucide-react"; +import { useEffect, useState } from "react"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { ScrollArea } from "@/components/ui/scroll-area"; +import { useKnowledgeNotes } from "@/core/knowledge/hooks"; +import type { KnowledgeNote } from "@/core/knowledge/types"; + +import { SOURCE_TYPE_LABELS, formatDateTime } from "./wikiUtils"; + +/** What was clicked: a tag (exact filter) or an entity (relation-graph match). */ +export type RelatedNotesFilter = + | { kind: "tag"; value: string } + | { kind: "entity"; value: string }; + +const PAGE_SIZE = 20; + +function useDebounced(value: T, delay = 300): T { + const [debounced, setDebounced] = useState(value); + useEffect(() => { + const id = window.setTimeout(() => setDebounced(value), delay); + return () => window.clearTimeout(id); + }, [value, delay]); + return debounced; +} + +/** + * Click a tag / entity chip → list every other note that shares it. + * Server-side keyword search + pagination so huge graphs stay usable: + * tags use the backend's exact `tag` filter, entities the exact `entity` + * filter (note→entity relations), both combinable with `keyword`. + */ +export function RelatedNotesDialog({ + filter, + currentNoteId, + onOpenChange, + onSelect, +}: { + filter: RelatedNotesFilter | null; + currentNoteId?: string; + onOpenChange: (open: boolean) => void; + onSelect: (noteId: string) => void; +}) { + const [search, setSearch] = useState(""); + const debouncedSearch = useDebounced(search.trim()); + const [page, setPage] = useState(0); + + // New chip or new search term → back to the first page. + useEffect(() => { + setPage(0); + }, [filter?.kind, filter?.value, debouncedSearch]); + // Reset the search box whenever a different chip is opened. + useEffect(() => { + setSearch(""); + }, [filter?.kind, filter?.value]); + + const params = { + ...(filter?.kind === "tag" ? { tag: filter.value } : {}), + ...(filter?.kind === "entity" ? { entity: filter.value } : {}), + keyword: debouncedSearch || undefined, + limit: filter ? PAGE_SIZE : 0, + offset: page * PAGE_SIZE, + }; + + const { data, isLoading, isFetching } = useKnowledgeNotes(params); + const items = (data?.items ?? []).filter( + (n: KnowledgeNote) => n.id !== currentNoteId, + ); + const total = data?.total ?? 0; + const pageCount = Math.max(1, Math.ceil(total / PAGE_SIZE)); + + return ( + + + + + {filter?.kind === "entity" ? ( + + ) : ( + + )} + {filter?.value} + + + {filter?.kind === "entity" + ? "提到该实体的关联知识" + : "使用该标签的关联知识"} + {total > 0 ? ` · 共 ${total} 条` : ""} + + + +
+ + setSearch(e.target.value)} + placeholder="在关联知识中搜索标题/内容..." + className="h-9 pl-8" + /> +
+ + + {isLoading || (isFetching && items.length === 0) ? ( +
+ 加载中... +
+ ) : items.length === 0 ? ( +
+ {debouncedSearch ? "没有匹配的关联知识。" : "暂无其他关联知识。"} +
+ ) : ( +
    + {items.map((note) => ( +
  • + +
  • + ))} +
+ )} +
+ + {pageCount > 1 ? ( +
+ + 第 {page + 1} / {pageCount} 页 + +
+ + +
+
+ ) : null} +
+
+ ); +} diff --git a/frontend-web/src/components/wiki/WikiContent.tsx b/frontend-web/src/components/wiki/WikiContent.tsx index 38d4705f..354f190b 100644 --- a/frontend-web/src/components/wiki/WikiContent.tsx +++ b/frontend-web/src/components/wiki/WikiContent.tsx @@ -25,6 +25,10 @@ import { DocumentToolbar } from "./DocumentToolbar"; import { FloatingToc } from "./FloatingToc"; import { MarkdownViewer } from "./MarkdownViewer"; import { RelatedDocs } from "./RelatedDocs"; +import { + RelatedNotesDialog, + type RelatedNotesFilter, +} from "./RelatedNotesDialog"; import { SOURCE_TYPE_LABELS, STATUS_LABELS, @@ -91,6 +95,10 @@ export function WikiContent({ const [draftContent, setDraftContent] = useState(note.content_md); const [draftStatus, setDraftStatus] = useState(note.status); const [activeHeadingId, setActiveHeadingId] = useState(null); + // Tag / entity chip clicked → show notes that share it. + const [relatedFilter, setRelatedFilter] = useState( + null, + ); // Pull the backend-injected `## Related` wikilinks out of the body so they // render as a structured module; editing still works on the full source. @@ -270,12 +278,15 @@ export function WikiContent({ 创建:{formatDateTime(note.created_at).slice(0, 10)} ) : null} {note.tags.map((tag) => ( - setRelatedFilter({ kind: "tag", value: tag })} + className="inline-flex h-6 cursor-pointer items-center rounded-md bg-rose-50 px-2 text-xs font-medium text-rose-500 transition-colors hover:bg-rose-100 hover:text-rose-600 dark:bg-rose-500/10 dark:text-rose-300 dark:hover:bg-rose-500/20" > #{tag} - + ))} @@ -287,11 +298,15 @@ export function WikiContent({ {note.entities.map((ent) => { const meta = entityMeta(ent.entity_type); return ( - + setRelatedFilter({ kind: "entity", value: ent.name }) + } className={cn( - "inline-flex items-center gap-1.5 rounded-full border py-1 pl-2 pr-1 text-xs font-medium", + "inline-flex cursor-pointer items-center gap-1.5 rounded-full border py-1 pl-2 pr-1 text-xs font-medium transition-opacity hover:opacity-80", meta.className, )} > @@ -300,7 +315,7 @@ export function WikiContent({ {meta.label} - + ); })} @@ -373,6 +388,18 @@ export function WikiContent({ + + { + if (!open) setRelatedFilter(null); + }} + onSelect={(id) => { + setRelatedFilter(null); + navigate(`/page/workspace/knowledge/${id}`); + }} + /> ); } diff --git a/frontend-web/src/components/wiki/WikiHeader.tsx b/frontend-web/src/components/wiki/WikiHeader.tsx index a82c391d..b278ad49 100644 --- a/frontend-web/src/components/wiki/WikiHeader.tsx +++ b/frontend-web/src/components/wiki/WikiHeader.tsx @@ -1,5 +1,4 @@ import { - BookOpen, Copy, Menu, Network, @@ -41,7 +40,7 @@ function HeaderButton({ ); } -/** Light 56px wiki header: logo + product name on the left, action entries on the right. */ +/** Light 56px wiki header: plain page title on the left, action entries on the right. */ export function WikiHeader({ onToggleSidebar, onImportFile, @@ -68,14 +67,8 @@ export function WikiHeader({ > -
- -
-
-
- 问答梳理 -
-
LLM Wiki · 问答梳理中心
+
+ 问答梳理
diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index d977872c..21c710f4 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -3,6 +3,7 @@ import { BotIcon, CopyIcon, + EyeOffIcon, GlobeIcon, LockIcon, MessageSquareIcon, @@ -529,7 +530,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar 来自他人 )} - {isOwned && agent.published && !agent.is_private && ( + {(isOwned || isAdmin) && !isBuiltin && agent.published && !agent.is_private && ( )} + {(isOwned || isAdmin) && !isBuiltin && !agent.published && !agent.is_private && ( + + + 未发布 + + )} {agent.is_private && ( - line - .replace(/^\s{0,3}#{1,6}\s*/, "") - .replace(/^\s{0,3}>\s?/, "") - .replace(/^(\s*)[-*+]\s+/, "$1• ") - .replace(/(\*\*|__)(.*?)\1/g, "$2") - .replace(/(\*|_)(.*?)\1/g, "$2") - .replace(/`([^`]+)`/g, "$1"), - ) - .join("\n") - .replace(/\n{3,}/g, "\n\n") - .trim(); -} /** Local edit-mode draft — committed in one `updateAgent` call on 保存. */ interface AgentDraft { @@ -94,6 +75,7 @@ interface AgentDraft { skills: string[]; published: boolean; featured: boolean; + templateName: string; } function draftFromAgent(agent: Agent): AgentDraft { @@ -105,6 +87,7 @@ function draftFromAgent(agent: Agent): AgentDraft { skills: agent.skills ?? [], published: agent.published, featured: agent.featured_order != null, + templateName: agent.template_name ?? "", }; } @@ -176,18 +159,22 @@ export function AgentDetailPanel({ const setFeatured = useSetAgentFeatured(); const { data: publishSquares } = usePublishSquares(); const { skills, isLoading: isSkillsLoading } = useSkills(); - // Same pool the edit dialog offers: only globally enabled skills are - // whitelist candidates. - const enabledPool = useMemo( - () => skills.filter((skill) => skill.enabled), - [skills], - ); // Permission model mirrors agent-card: admins manage the full catalog, // owners manage their own agents, everyone else is read-only. const storedAuth = useMemo(() => getStoredAuth(), []); const currentUserId = storedAuth?.user_id ?? null; const isAdmin = storedAuth?.system_role === "admin"; + + // 可配置的技能限定为「内置 + 自己的 + 已发布到广场的」, + // 其他用户未发布的自定义技能不可选(管理员例外)。 + const enabledPool = useMemo( + () => + skills.filter((skill) => + isAgentSkillCandidate(skill, currentUserId, isAdmin), + ), + [skills, currentUserId, isAdmin], + ); const isOwned = !!agent && !agent.builtin && @@ -199,14 +186,30 @@ export function AgentDetailPanel({ const isSaving = updateAgent.isPending || setFeatured.isPending; + // 对话生成文件(md/txt 等 artifact)并在右侧展开预览时,自动收起智能体 + // 信息面板给预览让位;新文件落地时同样收起。编辑中不打断用户,用户随时 + // 可手动重新展开。 + const artifactsCtx = useArtifactsOptional(); + const artifactCount = artifactsCtx?.artifacts.length ?? 0; + const artifactPanelVisible = !!artifactsCtx?.open && artifactCount > 0; + const prevArtifactPanelVisible = useRef(false); + const prevArtifactCount = useRef(0); + useEffect(() => { + const opened = artifactPanelVisible && !prevArtifactPanelVisible.current; + const grew = + artifactPanelVisible && artifactCount > prevArtifactCount.current; + prevArtifactPanelVisible.current = artifactPanelVisible; + prevArtifactCount.current = artifactCount; + if ((opened || grew) && !editing) { + setCollapsed(true); + } + }, [artifactPanelVisible, artifactCount, editing]); + const displayName = desensitize(agent?.name ?? agentName); const questions = useMemo( () => normalizeAgentQuestions(agent?.common_questions), [agent?.common_questions], ); - const soulPreview = agent?.soul?.trim() - ? cleanProfileText(desensitize(agent.soul)) - : undefined; function beginEdit() { if (!agent || !canEdit) return; @@ -280,6 +283,8 @@ export function AgentDetailPanel({ if (draft.description !== (agent.description ?? "")) request.description = draft.description; if (draft.soul !== (agent.soul ?? "")) request.soul = draft.soul; + if (draft.templateName.trim() !== (agent.template_name ?? "")) + request.template_name = draft.templateName.trim(); if (nextPublished !== agent.published) request.published = nextPublished; if (JSON.stringify(draft.skills) !== JSON.stringify(agent.skills ?? [])) request.skills = draft.skills; @@ -422,14 +427,7 @@ export function AgentDetailPanel({ ) : (
- @@ -528,9 +526,31 @@ export function AgentDetailPanel({ )}
+ {/* 入库模板名 — view mode shows a hint row when configured. */} + {!editing && agent.template_name?.trim() && ( +
+ + 入库模板:{desensitize(agent.template_name)} +
+ )} + {/* Publish / featured toggles — only meaningful in edit mode. */} {editing && draft && (
+
+

入库模板名

+ + setDraft({ ...draft, templateName: e.currentTarget.value }) + } + placeholder="例如:临时策略库(留空表示不支持入库)" + className="h-8 text-sm" + /> +

+ 配置后,对话生成的 Markdown 文件可一键发布到知识入库接口。 +

+

公开发布

@@ -564,41 +584,31 @@ export function AgentDetailPanel({ )} - {/* ── SOUL / 人设 ────────────────────────────────────────────────── */} - {(editing || soulPreview) && ( -
} - title="人设(SOUL)" - > - {editing && draft ? ( -