From f0aec4cd7ffa4644afa92e1522f7bda4f9731292 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=90=89=E9=91=AB=E5=8D=87?= Date: Thu, 11 Jun 2026 11:37:33 +0800 Subject: [PATCH] =?UTF-8?q?feat(square):=20=E6=96=B0=E5=A2=9E=E5=8F=91?= =?UTF-8?q?=E5=B8=83=E5=B9=BF=E5=9C=BA=E5=8A=9F=E8=83=BD=EF=BC=8C=E6=99=BA?= =?UTF-8?q?=E8=83=BD=E4=BD=93/=E6=8A=80=E8=83=BD=E6=8C=89=E5=B9=BF?= =?UTF-8?q?=E5=9C=BA=E5=88=86=E7=B1=BB=E5=B1=95=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 后端新增 PublishSquare/PublishSquaresSettings 配置与读写接口 - agents/skills 增加 square_id 字段及对应迁移脚本 - 前端新增发布广场设置页、技能转移选择器,画廊按广场分 Tab 展示 - lead_agent prompt 及若干持久化层适配调整 Co-Authored-By: Claude Opus 4.8 (1M context) --- batch-distill/distill_people.py | 37 +- .../workspace/agent-info-sidebar.tsx | 48 +- .../components/workspace/agent-welcome.tsx | 12 +- .../workspace/agents/agent-card.tsx | 223 +++++---- .../workspace/agents/agent-gallery.tsx | 69 ++- .../agents/skill-transfer-selector.tsx | 429 ++++++++++++++++++ .../src/components/workspace/input-box.tsx | 21 +- .../settings/appearance-settings-page.tsx | 3 + .../settings/skill-settings-page.tsx | 90 +++- frontend-web/src/core/agents/api.ts | 3 + frontend-web/src/core/agents/hooks.ts | 5 + frontend-web/src/core/agents/types.ts | 47 +- frontend-web/src/core/skills/api.ts | 9 +- frontend-web/src/core/skills/hooks.ts | 4 +- frontend-web/src/core/skills/type.ts | 2 + frontend-web/src/core/system-settings/api.ts | 46 ++ .../src/core/system-settings/hooks.ts | 29 ++ frontend-web/src/pages/AgentChatPage.tsx | 1 + .../src/pages/PublishSquaresSettingsPage.tsx | 193 ++++++++ frontend-web/src/pages/WorkspaceRoutes.tsx | 13 + offline-backend-20260512/backend/CLAUDE.md | 6 + .../backend/app/gateway/routers/agents.py | 52 ++- .../backend/app/gateway/routers/skills.py | 30 +- .../app/gateway/routers/system_settings.py | 86 ++++ .../deerflow/agents/lead_agent/agent.py | 39 +- .../deerflow/agents/lead_agent/prompt.py | 121 +++++ .../harness/deerflow/config/agents_config.py | 32 +- .../deerflow/config/system_settings.py | 70 +++ .../harness/deerflow/knowledge/exporter.py | 14 +- .../deerflow/persistence/agents/base.py | 2 + .../deerflow/persistence/agents/memory.py | 19 +- .../deerflow/persistence/agents/model.py | 40 +- .../deerflow/persistence/agents/sql.py | 17 +- .../20260611_01_agents_skills_square_id.py | 74 +++ .../deerflow/persistence/skills/memory.py | 5 + .../deerflow/persistence/skills/model.py | 4 + .../deerflow/persistence/skills/sql.py | 5 + .../scripts/sql/column_additions_mysql.sql | 11 +- .../backend/tests/test_agents_backend.py | 22 +- 39 files changed, 1707 insertions(+), 226 deletions(-) create mode 100644 frontend-web/src/components/workspace/agents/skill-transfer-selector.tsx create mode 100644 frontend-web/src/pages/PublishSquaresSettingsPage.tsx create mode 100644 offline-backend-20260512/backend/packages/harness/deerflow/persistence/migrations/versions/20260611_01_agents_skills_square_id.py diff --git a/batch-distill/distill_people.py b/batch-distill/distill_people.py index d5c4d171..a57d9609 100644 --- a/batch-distill/distill_people.py +++ b/batch-distill/distill_people.py @@ -320,12 +320,36 @@ def _extract_text(payload) -> str: def _clean_markdown(text: str) -> str: - """去掉 agent 可能套上的 ```markdown ... ``` 围栏。""" + """去掉 agent 可能套上的 ```markdown ... ``` 围栏,并规整 YAML frontmatter。""" t = text.strip() m = re.match(r"^```(?:markdown|md)?\s*\n(.*)\n```$", t, re.S) if m: t = m.group(1).strip() - return t + return _normalize_frontmatter(t) + + +def _normalize_frontmatter(text: str) -> str: + """修复模型常见的 frontmatter 跑偏,让 YAML 能正常解析。 + + 模型经常把字段名写成 markdown 加粗(``**name:**``)、加空行、留行尾双空格, + 这些都会让 frontmatter 解析失败。这里把开头 ``---`` 到下一个 ``---`` 之间的内容 + 规整为干净的 ``key: value`` 形式。正文(第二个 ``---`` 之后)保持原样。 + """ + if not text.startswith("---"): + return text + # 拆出 frontmatter 块:开头 --- 与下一行单独的 --- 之间 + m = re.match(r"^---[ \t]*\n(.*?)\n---[ \t]*\n?(.*)$", text, re.S) + if not m: + return text + body_fm, rest = m.group(1), m.group(2) + out_lines = [] + for line in body_fm.splitlines(): + s = line.rstrip() # 去行尾双空格 + s = s.replace("**", "") # 去 markdown 加粗标记 + if not s.strip(): + continue # 丢掉空行 + out_lines.append(s) + return "---\n" + "\n".join(out_lines) + "\n---\n\n" + rest.lstrip("\n") # ================================ 名单读取 ================================ @@ -454,6 +478,13 @@ def main(): if not read_people(excel): sys.exit(f"名单为空或不存在:{excel}\n请放 people.xlsx(第一列写人名)。") + # --redo:一次性清掉名单里这些人的旧记录,让他们重做一遍; + # 之后本次会话内跑成功的会正常计为「已完成」,循环才会往后推进 + # (否则每轮 pending() 都把已完成的人当未完成,永远卡在第 1 个人) + if args.redo: + for p in read_people(excel): + manifest.pop(p["name"], None) + client = DeerFlowClient(BASE_URL) if not client.login(): sys.exit("登录失败。请检查 BASE_URL 是否指向正在运行的服务、口令是否正确。") @@ -469,7 +500,7 @@ def main(): out = [] for p in people: r = manifest.get(p["name"], {}) - done = (not args.redo) and r.get("status") == "succeeded" and (OUTPUT_DIR / f"{slugify(p['name'])}.md").exists() + done = r.get("status") == "succeeded" and (OUTPUT_DIR / f"{slugify(p['name'])}.md").exists() if not done: out.append(p) return out diff --git a/frontend-web/src/components/workspace/agent-info-sidebar.tsx b/frontend-web/src/components/workspace/agent-info-sidebar.tsx index b9f03351..71facd9f 100644 --- a/frontend-web/src/components/workspace/agent-info-sidebar.tsx +++ b/frontend-web/src/components/workspace/agent-info-sidebar.tsx @@ -13,11 +13,40 @@ import { useMemo, useState } from "react"; import { Button } from "@/components/ui/button"; import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog"; import { Tooltip } from "@/components/workspace/tooltip"; -import { type Agent } from "@/core/agents"; +import { type Agent, normalizeAgentQuestions } from "@/core/agents"; import { useSkills } from "@/core/skills/hooks"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; +/** + * Turn the admin-authored SOUL.md markdown into a clean, readable profile blurb + * for the narrow sidebar. The sidebar is a profile card, not a document view — + * so we strip markdown syntax (headings like `###`, emphasis `**`/`*`, inline + * code, list bullets, blockquotes) rather than rendering it, while keeping the + * paragraph/line structure intact. + */ +function cleanProfileText(raw: string): string { + return raw + .split("\n") + .map((line) => + line + // Drop heading markers (`#`, `##`, `### …`) — the main "###不要出现" fix. + .replace(/^\s{0,3}#{1,6}\s*/, "") + // Blockquote markers. + .replace(/^\s{0,3}>\s?/, "") + // List bullets `- ` / `* ` / `+ ` → a bullet glyph. + .replace(/^(\s*)[-*+]\s+/, "$1• ") + // Bold / italic / inline-code markers. + .replace(/(\*\*|__)(.*?)\1/g, "$2") + .replace(/(\*|_)(.*?)\1/g, "$2") + .replace(/`([^`]+)`/g, "$1"), + ) + .join("\n") + // Collapse 3+ blank lines into a single blank line. + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + /** * Collapsible right-hand "智能体简介" panel on the agent chat page. It is a * read-only window onto the agent's profile — avatar, name, slogan/description, @@ -50,7 +79,9 @@ export function AgentInfoSidebar({ const description = agent?.description ? desensitize(agent.description) : undefined; - const soul = agent?.soul?.trim() ? desensitize(agent.soul) : undefined; + const soul = agent?.soul?.trim() + ? cleanProfileText(desensitize(agent.soul)) + : undefined; // Map a stored skill name to its Chinese display label (falls back to name). const skillLabel = useMemo(() => { @@ -62,7 +93,7 @@ export function AgentInfoSidebar({ }, [skills]); const questions = useMemo( - () => (agent?.common_questions ?? []).filter((q) => q.trim().length > 0), + () => normalizeAgentQuestions(agent?.common_questions), [agent?.common_questions], ); @@ -116,10 +147,11 @@ export function AgentInfoSidebar({ )} - {/* Soul — the agent's long-form personality blurb. */} + {/* Description — the agent's long-form blurb, sourced from SOUL.md but + stripped of markdown syntax so it reads as plain profile copy. */} {soul && (
-

Soul

+

描述

{soul}

@@ -165,14 +197,14 @@ export function AgentInfoSidebar({ key={index} type="button" disabled={disabled} - onClick={() => onAskQuestion(question)} - title={question} + onClick={() => onAskQuestion(question.prompt)} + title={desensitize(question.prompt)} className={cn( "border-border/60 bg-background/60 hover:bg-accent hover:border-border w-full rounded-xl border px-3 py-2.5 text-left text-sm leading-relaxed transition-colors", "disabled:cursor-not-allowed disabled:opacity-50", )} > - {desensitize(question)} + {desensitize(question.title)} ))} diff --git a/frontend-web/src/components/workspace/agent-welcome.tsx b/frontend-web/src/components/workspace/agent-welcome.tsx index 84f53aaa..66578cea 100644 --- a/frontend-web/src/components/workspace/agent-welcome.tsx +++ b/frontend-web/src/components/workspace/agent-welcome.tsx @@ -2,7 +2,7 @@ import { BotIcon } from "lucide-react"; -import { type Agent } from "@/core/agents"; +import { type Agent, normalizeAgentQuestions } from "@/core/agents"; import { desensitize } from "@/lib/desensitize"; import { cn } from "@/lib/utils"; @@ -30,9 +30,7 @@ export function AgentWelcome({ const description = agent?.description ? desensitize(agent.description) : undefined; - const questions = (agent?.common_questions ?? []).filter( - (q) => q.trim().length > 0, - ); + const questions = normalizeAgentQuestions(agent?.common_questions); return (
onAskQuestion(question)} - title={question} + onClick={() => onAskQuestion(question.prompt)} + title={desensitize(question.prompt)} className={cn( "border-border/70 bg-card/60 hover:bg-accent hover:border-border max-w-[280px] truncate rounded-full border px-4 py-2 text-sm transition-colors", "disabled:cursor-not-allowed disabled:opacity-50", )} > - {desensitize(question)} + {desensitize(question.title)} ))}
diff --git a/frontend-web/src/components/workspace/agents/agent-card.tsx b/frontend-web/src/components/workspace/agents/agent-card.tsx index 427710cb..b506826a 100644 --- a/frontend-web/src/components/workspace/agents/agent-card.tsx +++ b/frontend-web/src/components/workspace/agents/agent-card.tsx @@ -4,12 +4,10 @@ import { BotIcon, CopyIcon, GlobeIcon, - InfoIcon, LockIcon, MessageSquareIcon, PinIcon, PlusIcon, - SearchIcon, ShieldIcon, SparklesIcon, SquarePenIcon, @@ -49,12 +47,22 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; +import { usePublishSquares } from "@/core/system-settings/hooks"; import { SkillDetailDialog } from "@/components/workspace/skills/skill-detail-dialog"; +import { SkillTransferSelector } from "@/components/workspace/agents/skill-transfer-selector"; import { TagChipList } from "@/components/workspace/tags/tag-chip"; import { TagPickerDialog } from "@/components/workspace/tags/tag-picker-dialog"; import { + normalizeAgentQuestions, useAgent, useCreateAgent, useDeleteAgent, @@ -65,7 +73,7 @@ import { useUpdateAgent, } from "@/core/agents"; import { getAgent } from "@/core/agents/api"; -import type { Agent } from "@/core/agents"; +import type { Agent, AgentQuestion } from "@/core/agents"; import { getStoredAuth } from "@/core/auth"; import { useI18n } from "@/core/i18n/hooks"; import { useSkills } from "@/core/skills/hooks"; @@ -210,17 +218,18 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar const [editDescription, setEditDescription] = useState(agent.description ?? ""); const [editSoul, setEditSoul] = useState(agent.soul ?? ""); // Common questions (常用问题) are authored here in the edit dialog and read - // back read-only on the agent chat page. Keep one trailing blank row so there - // is always an empty input to type the next question into. - const [editCommonQuestions, setEditCommonQuestions] = useState( - agent.common_questions ?? [], + // back read-only on the agent chat page. Each row has a short title (shown on + // the chip) and the full prompt sent when clicked. + const [editCommonQuestions, setEditCommonQuestions] = useState( + () => normalizeAgentQuestions(agent.common_questions), ); const [editPublished, setEditPublished] = useState(agent.published); const [editIsPrivate, setEditIsPrivate] = useState(agent.is_private); + const [editSquareId, setEditSquareId] = useState(agent.square_id ?? ""); + const { data: publishSquares } = usePublishSquares(); const [selectedSkills, setSelectedSkills] = useState( agent.skills ?? [], ); - const [skillSearch, setSkillSearch] = useState(""); const [detailSkillName, setDetailSkillName] = useState(null); // The /api/agents list response intentionally omits `soul` (perf optimization @@ -257,15 +266,16 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar setEditDescription(agent.description ?? ""); setEditPublished(agent.published); setEditIsPrivate(agent.is_private); + setEditSquareId(agent.square_id ?? ""); setSelectedSkills(agent.skills ?? []); - setEditCommonQuestions(agent.common_questions ?? []); - setSkillSearch(""); + setEditCommonQuestions(normalizeAgentQuestions(agent.common_questions)); }, [ agent.common_questions, agent.description, agent.is_private, agent.name, agent.published, + agent.square_id, agent.skills, editOpen, ]); @@ -279,7 +289,7 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar // include `soul`) keep working. if (agentDetail) { setEditSoul(agentDetail.soul ?? ""); - setEditCommonQuestions(agentDetail.common_questions ?? []); + setEditCommonQuestions(normalizeAgentQuestions(agentDetail.common_questions)); } else { setEditSoul(agent.soul ?? ""); } @@ -355,10 +365,14 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar // backend will see, regardless of any stale state in editPublished. published: editIsPrivate ? false : editPublished, is_private: editIsPrivate, + square_id: editSquareId, skills: selectedSkills, common_questions: editCommonQuestions - .map((q) => q.trim()) - .filter((q) => q.length > 0), + .map((q) => ({ + title: q.title.trim() || q.prompt.trim(), + prompt: q.prompt.trim() || q.title.trim(), + })) + .filter((q) => q.prompt.length > 0), }, }); toast.success("智能体已更新"); @@ -448,34 +462,11 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar } } - function toggleSkill(skillName: string) { - setSelectedSkills((prev) => { - if (prev.includes(skillName)) { - return prev.filter((name) => name !== skillName); - } - return [...prev, skillName]; - }); - } - const enabledSkills = useMemo( () => skills.filter((skill) => skill.enabled), [skills], ); - // Search over English name, Chinese name (name_zh) and description, so both - // 中英文 work. Already-selected skills stay searchable too (selection is - // independent of this filter). - const filteredSkills = useMemo(() => { - const q = skillSearch.trim().toLowerCase(); - if (!q) return enabledSkills; - return enabledSkills.filter( - (skill) => - skill.name.toLowerCase().includes(q) || - (skill.name_zh ?? "").toLowerCase().includes(q) || - (skill.description ?? "").toLowerCase().includes(q), - ); - }, [enabledSkills, skillSearch]); - // Status badges (内置 / 我的 / 来自他人 / 已发布 / 私密 / 首页推荐) — shared by // both the card header and the compact list row. const statusBadges = ( @@ -873,65 +864,13 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar 已选 {selectedSkills.length} 个 - {enabledSkills.length > 0 && ( -
- - setSkillSearch(e.currentTarget.value)} - className="h-8 pl-7 text-xs" - /> -
- )} -
- {isSkillsLoading ? ( -

技能加载中...

- ) : enabledSkills.length === 0 ? ( -

- 当前没有可用技能 -

- ) : filteredSkills.length === 0 ? ( -

没有匹配的技能

- ) : ( -
- {filteredSkills.map((skill) => { - const checked = selectedSkills.includes(skill.name); - return ( -
- - -
- ); - })} -
- )} -
+

不选任何技能会保存为无技能(skills = [])。

@@ -954,32 +893,53 @@ export function AgentCard({ agent, layout = "card", onPublisherClick }: AgentCar

这些问题会在该智能体的首页与简介栏作为推荐问题展示,点击即可一键提问。

-
- {editCommonQuestions.map((value, index) => ( -
- + {editCommonQuestions.map((item, index) => ( +
+
+ { + const next = [...editCommonQuestions]; + next[index] = { + ...next[index]!, + title: e.currentTarget.value, + }; + setEditCommonQuestions(next); + }} + className="h-8 text-sm font-medium" + /> + +
+