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" + /> + +
+