From 74113b3e6e4f676bfa47ddefe7b8e6732b7a0c2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 12 Jun 2026 21:20:33 +0800 Subject: [PATCH 01/30] =?UTF-8?q?feat:=20=E6=99=BA=E8=83=BD=E4=BD=93?= =?UTF-8?q?=E4=B8=8E=E6=8A=80=E8=83=BD=E5=88=97=E8=A1=A8=E6=98=BE=E7=A4=BA?= =?UTF-8?q?=E5=88=9B=E5=BB=BA=E6=97=B6=E9=97=B4=E5=AD=97=E6=AE=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增共享 formatDateTime 工具(YYYY-MM-DD HH:mm) - agent-card 卡片/列表视图在发布者下方显示创建时间 - skill 设置页 metaNodes 显示创建时间 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../components/workspace/agents/agent-card.tsx | 15 +++++++++++++++ .../workspace/settings/skill-settings-page.tsx | 9 +++++++++ frontend-web/src/core/utils/datetime.ts | 15 +++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index 21c710f4..cfd7e8c2 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -2,6 +2,7 @@ import { BotIcon, + ClockIcon, CopyIcon, EyeOffIcon, GlobeIcon, @@ -78,6 +79,7 @@ import type { Agent, AgentQuestion, UpdateAgentRequest } from "@/core/agents"; import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; import { useSkills } from "@/core/skills/hooks"; +import { formatDateTime } from "@/core/utils/datetime"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; @@ -605,6 +607,17 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar ) : null; + const createdAtText = formatDateTime(agent.created_at); + const createdAtNode = createdAtText ? ( +
+ + 创建时间:{createdAtText} +
+ ) : null; + // 技能英文名 → 中文名映射,用于把智能体的技能徽章显示为中文(agent.skills 只存英文名)。 const skillNameZhMap = useMemo(() => { const map = new Map(); @@ -820,6 +833,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar )} {publisherNode &&
{publisherNode}
} + {createdAtNode &&
{createdAtNode}
} {(agent.tags?.length ?? 0) > 0 && ( @@ -866,6 +880,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar

)} {publisherNode &&
{publisherNode}
} + {createdAtNode &&
{createdAtNode}
}
{showActionRow && actionButtons} 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 916c83ca..9c16d7e9 100644 --- a/frontend-web/src/components/workspace/settings/skill-settings-page.tsx +++ b/frontend-web/src/components/workspace/settings/skill-settings-page.tsx @@ -86,6 +86,7 @@ import { } from "@/core/skills/hooks"; import { usePublishSquares, useViewMode } from "@/core/system-settings/hooks"; import type { Skill } from "@/core/skills/type"; +import { formatDateTime } from "@/core/utils/datetime"; import { env } from "@/env"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; @@ -848,6 +849,14 @@ export function SkillRow({ : t.settings.skills.useCountInlineZero}
)} + {formatDateTime(skill.created_at) && ( +
+ + + 创建时间:{formatDateTime(skill.created_at)} + +
+ )} {(skill.tags?.length ?? 0) > 0 && ( )} diff --git a/frontend-web/src/core/utils/datetime.ts b/frontend-web/src/core/utils/datetime.ts index 3ec7203d..fea6af99 100644 --- a/frontend-web/src/core/utils/datetime.ts +++ b/frontend-web/src/core/utils/datetime.ts @@ -25,3 +25,18 @@ export function formatTimeAgo(date: Date | string | number, locale?: Locale) { locale: getDateFnsLocale(effectiveLocale), }); } + +/** + * Format a created/updated timestamp as `YYYY-MM-DD HH:mm`. + * Returns an empty string for missing or unparseable values. + */ +export function formatDateTime(value?: Date | string | number | null): string { + if (value === null || value === undefined || value === "") return ""; + const date = value instanceof Date ? value : new Date(value); + if (Number.isNaN(date.getTime())) return ""; + const pad = (n: number) => String(n).padStart(2, "0"); + return ( + `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` + + `${pad(date.getHours())}:${pad(date.getMinutes())}` + ); +} From 21e53310f8a4c1a7aebf44e5b4a12877cf0451fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Fri, 12 Jun 2026 21:49:32 +0800 Subject: [PATCH 02/30] =?UTF-8?q?fix:=20HTML=20=E9=A2=84=E8=A7=88=E9=94=9A?= =?UTF-8?q?=E7=82=B9=E7=82=B9=E5=87=BB=E5=90=8C=E6=AD=A5=20hash=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=A4=9A=E9=A1=B5=E9=9D=A2=E6=8A=80=E8=83=BD?= =?UTF-8?q?=E5=AD=90=E9=A1=B5=E9=9D=A2=E5=88=87=E6=8D=A2=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 预览 iframe 注入的锚点拦截脚本只 preventDefault + scrollIntoView, URL hash 从未改变,html-multipage 技能基于 CSS :target 的子页面 切换永远不命中,子页面 section 保持隐藏,点击无效。改为拦截后 同步 location.hash 触发 :target 切页,仅当目标元素已渲染时再平滑滚动。 Co-Authored-By: Claude Fable 5 --- frontend-web/src/core/utils/markdown.ts | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/frontend-web/src/core/utils/markdown.ts b/frontend-web/src/core/utils/markdown.ts index 96060708..4ab819ce 100644 --- a/frontend-web/src/core/utils/markdown.ts +++ b/frontend-web/src/core/utils/markdown.ts @@ -100,9 +100,16 @@ function candidateTextFromHref(href: string): string { /** * Injects a small runtime script into generated HTML so that clicking an - * in-page anchor (``) scrolls within the iframe instead of - * navigating it. The preview iframe loads from a sandboxed `blob:` URL, where a - * fragment navigation blanks the frame; intercepting the click avoids that. + * in-page anchor (``) works inside the preview iframe. + * + * The hash MUST really change on click: multi-page documents (the + * html-multipage skill) show/hide their sub-page `
` + * blocks with CSS `:target` rules, which only fire on a fragment navigation. + * Intercepting with preventDefault + scrollIntoView alone leaves the hash + * untouched, so sub-pages stay `display:none` and clicks look dead. The + * handler therefore syncs `location.hash` (a same-document navigation — + * safe in the sandboxed blob: frame) and only smooth-scrolls afterwards when + * the target is actually rendered. */ export function injectInPageAnchorHandler(html: string): string { const script = ``; if (/<\/body>/i.test(html)) { From 2f414c6e08e8c1014a85c05b1d4893c9f18bb6df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Tue, 16 Jun 2026 09:19:15 +0800 Subject: [PATCH 03/30] =?UTF-8?q?fix:=20HTML=20=E9=A2=84=E8=A7=88=E5=AD=90?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=E6=94=B9=E7=94=A8=20JS=20=E5=88=87=E6=8D=A2?= =?UTF-8?q?=EF=BC=8C=E9=81=BF=E5=85=8D=E6=B2=99=E7=AE=B1=20blob=20?= =?UTF-8?q?=E5=AF=BC=E8=88=AA=E8=A2=AB=E6=8B=A6=E6=88=AA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上一版通过设置 location.hash 触发 CSS :target 切页,但沙箱 blob iframe 禁止导航到自身 blob URL,浏览器报 Not allowed to load local resource,且 :target 仅在真实片段导航时更新,注定失效。改为完全 不动 hash,用 JS 模拟 html-multipage 技能的 .page/.pages 显示隐藏 (内联 display 覆盖类级 CSS),普通目录锚点仍平滑滚动。 Co-Authored-By: Claude Fable 5 --- frontend-web/src/core/utils/markdown.ts | 52 +++++++++++++++---------- 1 file changed, 31 insertions(+), 21 deletions(-) diff --git a/frontend-web/src/core/utils/markdown.ts b/frontend-web/src/core/utils/markdown.ts index 4ab819ce..c5b80575 100644 --- a/frontend-web/src/core/utils/markdown.ts +++ b/frontend-web/src/core/utils/markdown.ts @@ -102,14 +102,18 @@ function candidateTextFromHref(href: string): string { * Injects a small runtime script into generated HTML so that clicking an * in-page anchor (``) works inside the preview iframe. * - * The hash MUST really change on click: multi-page documents (the - * html-multipage skill) show/hide their sub-page `
` - * blocks with CSS `:target` rules, which only fire on a fragment navigation. - * Intercepting with preventDefault + scrollIntoView alone leaves the hash - * untouched, so sub-pages stay `display:none` and clicks look dead. The - * handler therefore syncs `location.hash` (a same-document navigation — - * safe in the sandboxed blob: frame) and only smooth-scrolls afterwards when - * the target is actually rendered. + * The preview loads from a sandboxed `blob:` URL (opaque origin). Such a frame + * is FORBIDDEN from navigating to its own `blob:` URL, so changing the hash + * throws "Not allowed to load local resource: blob:…#page-x". And because CSS + * `:target` only updates on a real fragment navigation, the html-multipage + * skill's `:target`-based sub-page switching can never fire here. + * + * So we never touch the hash. Instead the handler emulates the skill's + * show/hide in JS: a "page" is a `.page` inside a `.pages` container; clicking + * a link to one shows it and hides its siblings (mirroring the + * `.page:target` + `~ #page-home` rules via inline `display`, which outranks + * the skill's class-level CSS). Plain in-page anchors (TOC headings, etc.) just + * smooth-scroll as before. */ export function injectInPageAnchorHandler(html: string): string { const script = ``; if (/<\/body>/i.test(html)) { From 3855c30eddebc7ad1a39e9db2550d8ed76904390 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Tue, 16 Jun 2026 10:49:36 +0800 Subject: [PATCH 04/30] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=B2=97?= =?UTF-8?q?=E4=BD=8D=E7=AE=A1=E7=90=86=E3=80=81=E6=8F=90=E7=A4=BA=E8=AF=8D?= =?UTF-8?q?=E6=A8=A1=E6=9D=BF=E4=B8=8E=E8=B5=84=E6=BA=90=E6=A0=87=E7=AD=BE?= =?UTF-8?q?=EF=BC=8C=E5=AE=8C=E5=96=84=E7=94=A8=E6=88=B7=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E4=B8=8E=E6=8E=A8=E8=8D=90=E9=97=AE=E9=A2=98=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../admin/position-management-page.tsx | 479 ++++++++++++++++++ .../workspace/admin/prompt-templates-page.tsx | 258 ++++++++++ .../workspace/admin/tag-management-page.tsx | 11 +- .../src/components/workspace/input-box.tsx | 48 +- .../settings/skill-settings-page.tsx | 13 +- .../workspace/tags/resource-tag-dialog.tsx | 98 ++++ frontend-web/src/core/custom-prompts/index.ts | 91 ++++ .../src/core/page-layout/sidebar-menu.ts | 15 + frontend-web/src/core/positions/api.ts | 123 +++++ frontend-web/src/core/positions/hooks.ts | 124 +++++ frontend-web/src/core/positions/index.ts | 3 + frontend-web/src/core/positions/types.ts | 86 ++++ .../src/core/recommended-questions/api.ts | 6 +- .../src/core/recommended-questions/hooks.ts | 12 +- frontend-web/src/core/skills/api.ts | 3 + frontend-web/src/core/skills/hooks.ts | 5 +- frontend-web/src/core/tags/types.ts | 24 +- frontend-web/src/core/threads/hooks.ts | 17 + frontend-web/src/pages/PositionsPage.tsx | 5 + .../src/pages/PromptTemplatesPage.tsx | 5 + .../src/pages/RecommendedQuestionsPage.tsx | 28 +- frontend-web/src/pages/StrategyRoutes.tsx | 22 + frontend-web/src/pages/WorkspaceRoutes.tsx | 26 + offline-backend-20260512/backend/CLAUDE.md | 59 ++- .../backend/app/gateway/app.py | 4 + .../backend/app/gateway/deps.py | 24 + .../backend/app/gateway/routers/auth.py | 19 + .../backend/app/gateway/routers/positions.py | 311 ++++++++++++ .../gateway/routers/recommended_questions.py | 29 +- .../backend/app/gateway/routers/skills.py | 14 + .../app/gateway/routers/user_prompts.py | 108 ++++ .../backend/docs/summarization.md | 4 +- .../middlewares/summarization_middleware.py | 91 +++- .../deerflow/persistence/agents/model.py | 9 +- .../versions/20260616_01_positions.py | 108 ++++ .../deerflow/persistence/models/__init__.py | 3 + .../persistence/positions/__init__.py | 26 + .../deerflow/persistence/positions/base.py | 112 ++++ .../deerflow/persistence/positions/memory.py | 143 ++++++ .../deerflow/persistence/positions/model.py | 73 +++ .../deerflow/persistence/positions/sql.py | 196 +++++++ .../persistence/scheduled_tasks/model.py | 5 + .../harness/deerflow/persistence/tags/base.py | 11 + .../deerflow/persistence/tags/memory.py | 12 + .../deerflow/persistence/tags/model.py | 4 +- .../harness/deerflow/persistence/tags/sql.py | 12 + .../deerflow/persistence/user/model.py | 5 + .../persistence/user_prompts/model.py | 15 +- .../deerflow/persistence/user_prompts/sql.py | 63 ++- .../harness/deerflow/services/__init__.py | 1 + .../deerflow/services/position_sync.py | 220 ++++++++ .../scripts/sql/column_additions_mysql.sql | 17 + 52 files changed, 3143 insertions(+), 57 deletions(-) create mode 100644 frontend-web/src/components/workspace/admin/position-management-page.tsx create mode 100644 frontend-web/src/components/workspace/admin/prompt-templates-page.tsx create mode 100644 frontend-web/src/components/workspace/tags/resource-tag-dialog.tsx create mode 100644 frontend-web/src/core/positions/api.ts create mode 100644 frontend-web/src/core/positions/hooks.ts create mode 100644 frontend-web/src/core/positions/index.ts create mode 100644 frontend-web/src/core/positions/types.ts create mode 100644 frontend-web/src/pages/PositionsPage.tsx create mode 100644 frontend-web/src/pages/PromptTemplatesPage.tsx create mode 100644 offline-backend-20260512/backend/app/gateway/routers/positions.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260616_01_positions.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/positions/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/positions/base.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/positions/memory.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/positions/model.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/positions/sql.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/services/__init__.py create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/services/position_sync.py diff --git a/frontend-web/src/components/workspace/admin/position-management-page.tsx b/frontend-web/src/components/workspace/admin/position-management-page.tsx new file mode 100644 index 00000000..07ed6e18 --- /dev/null +++ b/frontend-web/src/components/workspace/admin/position-management-page.tsx @@ -0,0 +1,479 @@ +"use client"; + +import { + CheckIcon, + Loader2, + PencilIcon, + PlusIcon, + RefreshCwIcon, + Trash2Icon, + UserPlusIcon, + UsersIcon, +} from "lucide-react"; +import { useEffect, useMemo, useState } from "react"; +import { toast } from "sonner"; + +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; +import { resolveTagColor } from "@/components/workspace/tags/tag-colors"; +import { + POSITION_RESOURCE_HINT, + POSITION_RESOURCE_LABELS, + POSITION_RESOURCE_TYPES, + resourceTypeToTagType, + useAddMembers, + useCreatePosition, + useDeletePosition, + usePositionMembers, + usePositionTagBindings, + usePositionUsers, + usePositions, + useRemoveMembers, + useResyncPosition, + useSetTagBindings, + useUpdatePosition, + type Position, + type PositionResourceType, + type TagBindings, +} from "@/core/positions"; +import { useTags } from "@/core/tags"; +import { cn } from "@/lib/utils"; + +function errMsg(e: unknown): string { + return String(e instanceof Error ? e.message : e); +} + +/** Inline multi-select chip row for the tags of one resource type. */ +function TagMultiSelect({ + resourceType, + value, + onChange, +}: { + resourceType: PositionResourceType; + value: string[]; + onChange: (next: string[]) => void; +}) { + const { data: tags, isLoading } = useTags(resourceTypeToTagType(resourceType)); + const toggle = (id: string) => + onChange(value.includes(id) ? value.filter((x) => x !== id) : [...value, id]); + + if (isLoading) { + return ; + } + if (!tags || tags.length === 0) { + return ( + + 暂无{POSITION_RESOURCE_LABELS[resourceType]}标签,请先到「标签管理」创建。 + + ); + } + return ( +
+ {tags.map((tag) => { + const checked = value.includes(tag.id); + const color = resolveTagColor(tag.color); + return ( + + ); + })} +
+ ); +} + +function PositionFormDialog({ + open, + onOpenChange, + editing, +}: { + open: boolean; + onOpenChange: (open: boolean) => void; + editing: Position | null; +}) { + const createPosition = useCreatePosition(); + const updatePosition = useUpdatePosition(); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [isDefault, setIsDefault] = useState(false); + + const [syncedFor, setSyncedFor] = useState(null); + const syncKey = `${open}:${editing?.id ?? "new"}`; + if (open && syncedFor !== syncKey) { + setName(editing?.name ?? ""); + setDescription(editing?.description ?? ""); + setIsDefault(editing?.is_default ?? false); + setSyncedFor(syncKey); + } + if (!open && syncedFor !== null) setSyncedFor(null); + + const pending = createPosition.isPending || updatePosition.isPending; + const handleSave = async () => { + const trimmed = name.trim(); + if (!trimmed) return; + try { + if (editing) { + await updatePosition.mutateAsync({ id: editing.id, input: { name: trimmed, description, is_default: isDefault } }); + } else { + await createPosition.mutateAsync({ name: trimmed, description, is_default: isDefault }); + } + onOpenChange(false); + } catch (e) { + toast.error(errMsg(e)); + } + }; + + return ( + + + + {editing ? "编辑岗位" : "新增岗位"} + +
+
+ + setName(e.target.value)} placeholder="如:理赔专员" maxLength={191} /> +
+
+ +