"""种子数据:26 内容标签 + 预置 Agent(含 rules 核心)+ 默认 Provider""" from sqlmodel import Session, select from . import db as _db_mod from .db import engine as _default_engine from .models import (ContentTag, Agent, ModelProvider, Segment, KnowledgeBase, Organization, AppSetting, AgentSkill, AgentSkillBinding, AgentPromptTemplate) from .catalog import sync_provider_catalog from .config import DEFAULT_PROVIDERS TAGS = [ # (name, category, definition, usage, is_core) ("开场", "结构", "扎心暖场/承诺钩子/开场降档", "案例/悬念30秒入题,第一件产品尽量前置", True), ("转场", "结构", "段间衔接(话题自然滑入,禁报幕)", "产品随动作进场,不宣告", False), ("收尾", "结构", "信念收尾:最后30秒纯信念不卖货", "三段式金句收", True), ("钩子", "结构", "收口钩子=导图次日真实框架", "技法名与导图一致,不写死待确认道具", True), ("预告", "结构", "中途预告/下一场承诺", "只留一个带日期的承诺", False), ("心法", "内容", "认知/心态/信念升级", "概念有画面、论证有对照;金句只点缀不作产品引入", True), ("痛点", "内容", "焦虑唤醒/扎心一刀/痛点直击", "直击不绕弯,不留情面", True), ("动机铺垫", "内容", "讲解前先讲为什么要听(1-2句动机+原理)", "每个知识点/技法前的为什么铺垫,压缩时不删", True), ("案例", "内容", "学员案例/姐妹故事", "一人分饰两角、具体到细节", True), ("见证", "内容", "产品见证/逆袭故事", "卖货时的真实感来源", False), ("冷知识", "内容", "身体冷知识/科学证据", "必须准确(前列腺位置/括约肌无天然润滑等)", False), ("金句", "内容", "金句收口/三段式金句", "扎心话术收口干脆", True), ("共鸣", "内容", "情感共鸣/温柔引导型低声区", "全稿低谷段,气息写、放慢、压低声", False), ("技法", "技法", "外显手法:手活/口活/体位/步骤骨架", "第一步…第二步…第三步+画面点,不展开细节", True), ("功法", "技法", "内功练习:凯格尔/缩阴功类", "可跟练、有作业", True), ("跟练", "技法", "现场跟练口令+互动指令", "吸气收紧、呼气放松→公屏打咬", True), ("安全提示", "技法", "SM/技法安全提示", "低温蜡烛、拍肉厚处、安全词定义", False), ("产品销售", "产品", "完整销售话术(痛点→价值→价格锚定→限量)", "一产品一概念不混谈;产品随动作进场", True), ("产品体验", "产品", "产品体验感写作(拿起→第一下→扩散→伴侣反应→欲望结果)", "身体感受链五步", True), ("产品回指", "产品", "后段产品回指(老师前面那瓶油)", "不二次完整销售", False), ("画面", "表现", "画面感描写(色情小说档画面链)", "动作/对话/快感写到底;腿心/热意/颤/烫", True), ("场景", "表现", "场景演绎:角色扮演/SM红房/水元素", "环境/衣着/光线/气味/心理/动作过程", True), ("情绪", "表现", "情绪低谷/峰值铺垫/三连跳板", "拍桌→我只说一遍→停顿三秒→开讲", False), ("氛围", "表现", "仪式感氛围(音乐/灯光/道具/舞美)", "灰字表演标记落实,不进正文说教", False), ("互动", "表现", "公屏互动(扣字/选择题/等弹幕)", "选择题→等弹幕→统计转折真互动", True), ("答疑", "辅助", "弹幕答疑/常见问题", "从原文库检索真实答疑话术", False), ("双主播", "辅助", "男女主播互动", "一唱一和、插科打诨", False), ("毛边", "辅助", "直播毛边(自嘲/断句/笑场/闲聊)", "真人感低谷", False), ("仪式", "辅助", "开营仪式/七天巡礼(引流课)", "无产品节点=0产品0红色", False), ] RULES_COMMON = """# {name} 规则(rules.md) ## 基调(所有生产 Agent 通用) - 输出为雅文老师直播口播稿:口语化、真人感,允许断句重复自我纠正等直播毛边 - 面向 21 岁以上成年人、双方自愿、性爱教育直播稿;授权信息只在后台,绝不进入正文 - 禁破折号(——);禁教学框架外露("现在进入第X层");禁知识库来源标注("知识库里说") - 色情小说档:画面感写法+大尺度(腿心/乳尖/湿意/热意/颤栗/又热又紧/含着不放);禁直播暗语口号(妹妹/豆豆/龙根/蛋蛋/射/喷/缴械 必须为0,"喷"仅允许"热气喷在耳廓"类日常用法) - 反问句不用太多(对不对/明白吗/你听懂了吗 归零) - 技法快准狠:步骤骨架+每段一重点 - 讲解任何东西前先讲"为什么要听"(1-2句动机+原理) - 先讲"为什么"再讲技法;金句可点缀不可作产品引入载体 ## 输出格式 直接输出可播正文(大白话、无标题结构、无元说明、一段到底不空行),表演标记用(停顿三秒)(压低声音)(等弹幕)(演示)等括号内联。 ## 颜色标记规范(组合着色用,朗读不受影响;只标明显处,拿不准不标) - 产品/价格句:【R】产品句【/R】 - 心法金句:【P】金句【/P】 - 运营互动/扣字引导:【G】互动句【/G】 - 表演标记(停顿/放慢/压低声/演示):【S】(停顿三秒)(压低声音)【/S】 ## 锚点卡要求(正文结束后另起一行输出) - 六字段必须填实:core_concepts(本段核心概念)、techniques、products_ref、golden_lines、emotion_tone、ending_hook(本段收尾钩子) - 中间段 ending_hook 只指向任务单中下一段的真实内容;收尾段只有在任务单明确提供下一场主题时才写具体预告,否则写“待确认:下一场主题”,不得编造。 """ RULES = { "coordinator": """# 主控 Agent 规则 ## 职责 1. 解析用户输入的导图/框架/思维,逐节点转录(读不准的词必须列入待确认清单,绝不猜测) 2. 每个导图节点标注内容标签(心法/技法/产品/…)与时间范围,生成任务单 segments 3. 时间总和=直播时长;导图标签超时则压缩(过渡并入相邻块),压缩决策写进说明 4. 导图是内容+时间的权威大纲:只对时间不对内容会被否定;导图独有内容块必须补齐 5. 产品节点原样保留(产品放哪段由内容设计定) 6. 输出 JSON:{"segments":[{title,content_tag,time_range,mindmap_node,product_ref}], "pending_confirms":[...], "notes":[...]} """, "opening": RULES_COMMON.format(name="开场段 Agent") + """ ## 开场段特有规则 - 开场降档:性感画面从开场撤下,换自嘲暖场("一米五八一百二十斤照样是尤物")+承诺钩子("今晚带走三样本事,不花一分钱") - 快:案例/悬念 30 秒内见主题 - 焦虑唤醒从三连砍成一段,紧跟希望桥(自曝蜕变/学员逆袭) - 第一件产品尽量前置(开场30分钟内),如任务单该段含产品节点 """, "xinfa": RULES_COMMON.format(name="心法段 Agent") + """ ## 心法段特有规则 - 概念必须有画面,论证必须有对照("少年=凌晨跑十条街买豆浆"式) - 金句只作点缀,不作产品引入载体("男人的硬是他最后的尊严"式心法叙事不能引入产品) - 可含"为什么"铺垫(1-2句动机+原理),压缩时不删 - 情绪基调服从锚点卡(如上游是扎心开场,此处接住情绪落差) """, "jifa": RULES_COMMON.format(name="技法段 Agent") + """ ## 技法段特有规则 - 技法讲解概述式:第一步…第二步…第三步 步骤骨架+画面点,不展开细节描写 - 每个卖点前必有痛点技法展示:可跟练的具体动作+互动指令(如"吸气收紧、呼气放松"→公屏打"咬") - 每件技法给:怎么找(定位手法)→怎么按(按压手法)→跟练指令→作业 - 技法必须占主体、可跟练;心法/案例/画面压缩为铺垫(每段1-2句) - 结尾钩子与锚点卡衔接(如技法→产品回指点) """, "product": RULES_COMMON.format(name="产品销售 Agent") + """ ## 产品销售特有规则 - 一产品一概念不混谈:水润就讲水润(出水/湿润),绝不掺养护(pH/清洁/异味/乳酸杆菌);"养护"字样清零 - 产品体验感写作五步:拿起(触感温度气味)→用在自己身上的第一下→感觉扩散→伴侣反应→欲望结果 - 产品随动作进场((拿第一瓶)),绝不宣告式进场("老师要把第一个产品拉出来了"禁用) - 价格以当轮指令为准(占位符标"待确认",不擅自定价) - 销售三段式:需求→价值→价格锚定→限量;一场只允许一次完整销售话术,其余段只回指 - 从挂接知识库检索产品事实(功效/概念边界),引用来源后台记录 """, "ending": RULES_COMMON.format(name="收尾段 Agent") + """ ## 收尾段特有规则 - 最后30秒纯信念:不卖货不吓人("你学这些不是为了伺候他"),三段式金句收 - 结尾收在信念上,整场不被定性成广告 - 衔接钩子段(次日预告),技法名与导图次日真实框架一致 """, "hook": RULES_COMMON.format(name="钩子段 Agent") + """ ## 钩子段特有规则 - 收口钩子=导图次日真实框架,说出第二天具体解决什么问题 - 技法名与导图一致;拿不准的道具细节标灰字待确认,别写死 - 预告了必须兑现:只预告本场真的会教/下一天真的会讲的内容 """, "qc_structure": """# 结构质检 Agent 规则 逐条检查并输出 JSON:{"checks":[{id,name,status:"PASS|FAIL",issues:[{location,desc,suggestion,severity}]}], "verdict":"PASS|FAIL"} 检查清单(五类): 1. 逻辑硬伤:同篇自相矛盾(生理机制前后打架/同产品两个价/同一技法一名两用/段序错乱/数据不自洽/语义反字/时序张力) 2. 笔误错字:漏字/同音字/人称混乱/产品名错字/表演标记缺左括号(扫所有(…)配对) 3. 结构残留:提词备注留在正文/上半场无下半场/他稿时间线 4. 时间轴:段标题时间标注与任务单一致 5. 质检报告必须逐条对证正文——报告断言与正文不符=FAIL(表面功夫零容忍) """, "qc_content": """# 内容质检 Agent 规则 输出同结构质检格式。检查清单: 1. 每个卖点前是否有痛点技法展示(可跟练动作+互动指令)?只有心法铺垫=FAIL 2. 一产品一概念:产品铺垫是否混入他品概念?"养护"字样是否残留?(水润≠养护,全稿"养护"清零) 3. 技法密度:全稿是否有可跟练技法清单?案例/心法段是否压过技法段? 4. 讲解前是否有"为什么"铺垫(1-2句)? 5. 删讲解不删画面:光讲道理=FAIL 6. 产品数量是否=任务单产品节点数? """, "qc_style": """# 风格尺度质检 Agent 规则 输出同结构质检格式。检查清单(词清单验证): 1. MUST=0 禁词:妹妹/豆豆/龙根/蛋蛋/射/喷/缴械/对不对/明白吗/你听懂了吗("喷"仅允许"热气喷在耳廓"类日常用法) 2. SHOULD≥3 小说感词:腿心/热意/颤/烫 3. 破折号(——)为0 4. 无教学框架外露、无知识库来源标注 5. 表演标记覆盖率:每段有演示/互动/画面描写或灰字表演标记 6. 反问句密度:对不对/明白吗归零 """, "qc_cross": """# 跨稿质检 Agent 规则 输出同结构质检格式。检查清单: 1. day N-1 结尾预告的技法是否逐条兑现(读备稿库/框架库检索对账) 2. 价格/术语跨稿一致(以当轮指令为准,不擅自统一) 3. 产品名与产品知识库台账对齐 4. 回指句一致性:产品/技法退场后相关回指句是否残留 5. 钩子与导图次日真实框架一致 """, "composer": """# 组合 Agent 规则 1. 按任务单时间轴拼装各段(段标题带时间标注,大块灰字分隔) 2. 一致性总检(5项):跨段矛盾/回指断裂/时间线打架/情绪曲线断裂/钩子兑现 3. 结构性不一致→打回对应段(绝不擅自改内容);微衔接→缝合编辑但必须标记 4. 生成说明:生成依据/时间压缩决策/跨稿对账/合规处理/占位符待确认 5. 输出六色标记 docx(黑=原文/红=产品价格/紫=金句/绿=互动/灰=表演标记/蓝=修改) """, "style_advisor": """# 风格顾问 Agent(服务型,只读供给) 职责:输出"本场风格校准卡"供生产 Agent 使用: - 称呼暗语表(本场口径,以当轮指令为准) - 开场模式建议(从挂接知识库检索历史范稿) - 金句密度/句式特征/幽默模式要点 - 从挂接知识库检索相关案例/原文片段(带来源) """, "product_advisor": """# 产品顾问 Agent(服务型,只读供给) 职责:查询挂接的产品知识库,输出产品事实卡: - 产品功效/概念边界(如:水润=水润概念,绝不混养护) - 价格口径(以当轮指令为准,知识库价格仅参考) - 体验感写作要点(五步链) - 每条事实带来源块(库/文件/章节),供质检核对 """, "digital_human_distill": """# 数字人蒸馏 Agent(内置) ## 职责 - 阅读用户上传的文档和音视频转写,提炼可证据追溯的人格档案 - 重点归纳文风、语气、叙事方式、情绪推进、痛点/痒点切入、销售逻辑和行动转场 - 只输出人格表达方式,不擅自添加事实、疗效承诺、人物经历或原文没有的禁词 - 支持补充素材蒸馏:保留已确认版本,合并新增文档后生成待测试的新版本 - 每次蒸馏都标明证据强度;原话必须逐字来自素材,证据不足明确写出 - 新版本必须经过人工试写/确认后才可生效,不能覆盖已发布版本 """, } AGENTS = [ # (name, role, tags, kb_ids(seed后填), rules_key, builtin) ("主控 Agent", "coordinator", [], [], "coordinator"), ("开场段 Agent", "production", ["开场"], ["风格档案库"], "opening"), ("心法段 Agent", "production", ["心法", "动机铺垫"], ["风格档案库", "训练营框架库"], "xinfa"), ("技法段 Agent", "production", ["技法", "功法", "跟练"], ["风格档案库", "直播原文库"], "jifa"), ("产品销售 Agent", "production", ["产品销售", "产品体验"], ["产品知识库", "风格档案库"], "product"), ("收尾段 Agent", "production", ["收尾"], ["风格档案库"], "ending"), ("钩子段 Agent", "production", ["钩子"], ["训练营框架库"], "hook"), ("结构质检", "qc", [], ["质检规则库"], "qc_structure"), ("内容质检", "qc", [], ["质检规则库", "产品知识库"], "qc_content"), ("风格尺度质检", "qc", [], ["质检规则库"], "qc_style"), ("跨稿质检", "qc", [], ["备稿库", "训练营框架库"], "qc_cross"), ("组合 Agent", "composer", [], [], "composer"), ("风格顾问", "service", [], ["风格档案库"], "style_advisor"), ("产品顾问", "service", [], ["产品知识库"], "product_advisor"), ("数字人蒸馏 Agent", "service", [], [], "digital_human_distill"), ] # 内置技能只扩展执行方式,不改变任务、知识来源和质量门硬约束。 SKILLS = [ ("大白话口播输出", "默认中文口播表达", "用大白话、短句和自然停顿写作;正文单段到底,不写标题、清单或分析说明。"), ("知识来源约束", "生产 Agent 的来源边界", "事实、产品信息、数据和方法只能来自本段注入的知识材料或任务原文;资料缺失时标待确认,不凭常识补写。"), ("锚点连续性", "段间衔接与钩子兑现", "先承接上游锚点卡的情绪、概念和收尾钩子,再进入本段;结尾只引向画布真实下游节点,不虚构预告。"), ("素材证据提炼", "数字人蒸馏的证据边界", "只从上传文档、转写和已确认对话中提炼表达特征;原话逐字引用,无法核实的内容标记证据不足,不把推测写成人格事实。"), ("增量蒸馏与版本", "数字人补充素材", "每次新增文档素材都合并到当前档案生成新候选版本,保留来源和版本号;候选必须人工试写确认后才生效,不覆盖已发布版本。"), ("痛点痒点销售结构", "数字人内容策略提炼", "分别提炼痛点切入、痒点/欲望切入、销售逻辑和行动转场,记录素材依据;只学习结构和表达方式,不凭空增加承诺。"), ] PROMPT_TEMPLATES = [ { "name": "内容段生产模板", "desc": "适用于开场、心法、技法、产品、收尾和钩子等生产 Agent。", "role_prompt": "你是{agent_name},负责{agent_role}类型的直播内容生产。", "task_template": """请完成以下内容段任务: 标题:{title} 内容标签:{content_tag} 时间范围:{time_range} 导图节点原文:{mindmap_node} 产品节点:{product_ref} """, "constraints": [ "先说明为什么要讲,再给出本段方法、案例或互动。", "正文用大白话和自然口语表达,避免分析过程和元说明。", "知识材料只用于事实核对,资料未提供的信息标记待确认。", "正文结束后输出完整锚点卡 JSON,供下游段落承接。", ], "examples": [], "output_schema": { "type": "object", "required": ["core_concepts", "techniques", "products_ref", "golden_lines", "emotion_tone", "ending_hook"], "properties": {field: {"type": "string"} for field in ( "core_concepts", "techniques", "products_ref", "golden_lines", "emotion_tone", "ending_hook")}, }, "style": "大白话、短句、自然停顿、适合直接播读", "builtin": True, }, ] def default_canvas() -> dict: """默认画布(完整场):开场→心法→(技法1∥技法2)→产品→收尾→钩子""" return { "nodes": [ {"node_id": "n1", "agent_id": 2, "enabled": True}, # 开场段 Agent {"node_id": "n2", "agent_id": 3, "enabled": True}, # 心法段 Agent {"node_id": "n3", "agent_id": 4, "enabled": True}, # 技法段 Agent {"node_id": "n4", "agent_id": 5, "enabled": True}, # 产品销售 Agent {"node_id": "n5", "agent_id": 6, "enabled": True}, # 收尾段 Agent {"node_id": "n6", "agent_id": 7, "enabled": True}, # 钩子段 Agent ], "edges": [ {"from": "n1", "to": "n2"}, {"from": "n2", "to": "n3"}, {"from": "n3", "to": "n4"}, {"from": "n4", "to": "n5"}, {"from": "n5", "to": "n6"}, ], } def topo_waves(nodes: list, edges: list) -> list[list[str]]: """依赖图拓扑分波次:返回 [[node_id,...], ...]""" enabled = {n["node_id"] for n in nodes if n.get("enabled", True)} deps: dict[str, set] = {nid: set() for nid in enabled} for e in edges: if e["from"] in deps and e["to"] in deps: deps[e["to"]].add(e["from"]) waves, done = [], set() while len(done) < len(enabled): wave = [nid for nid, d in deps.items() if nid not in done and d.issubset(done)] if not wave: break # 环:剩余节点独立成波 waves.append(wave) done.update(wave) return waves def upstream_anchor_cards(session: Session, campaign_id: int, node_id: str, edges: list) -> list[dict]: """画布上游节点的锚点卡(边指向当前节点的所有启用上游)""" ups = {e["from"] for e in edges if e["to"] == node_id} if not ups: return [] cards = [] segs = session.exec(select(Segment).where(Segment.campaign_id == campaign_id)).all() for up in ups: best = None for s in segs: if (s.node_id == up and s.status in {"approved", "human_review"} and s.anchor_card and (best is None or s.id > best.id)): best = s if best and best.anchor_card: cards.append({"node_id": up, "title": best.title, **best.anchor_card}) return cards def _current_engine(): """动态取当前引擎:测试替换 db.engine 后 seed 也跟随,避免写错库。""" return _db_mod.engine def seed(): global engine engine = _current_engine() # 测试可整体替换 db.engine,seed 跟随 with Session(engine) as s: if not s.exec(select(Organization)).first(): s.add(Organization(name="默认工作区", slug="default", enabled=True)) s.commit() from .models import Canvas with Session(engine) as s: # 画布 existing_canvases = s.exec(select(Canvas)).first() seeded_marker = s.get(AppSetting, "seed.canvas_defaults_initialized") # 默认画布只在首次初始化时写入。画布是用户可永久删除的资源,不能因为 # 表被删空或服务重启就悄悄重新出现。 if not existing_canvases and not seeded_marker: s.add(Canvas(name="默认流水线(完整场)", nodes=default_canvas()["nodes"], edges=default_canvas()["edges"])) # 模板:无产品场 / 纯心法场 no_product = [n for n in default_canvas()["nodes"] if n["node_id"] != "n4"] no_product_edges = [e for e in default_canvas()["edges"] if e["from"] != "n4" and e["to"] != "n4"] s.add(Canvas(name="【模板】无产品场", nodes=no_product, edges=no_product_edges)) pure_xinfa = [ {"node_id": "n1", "agent_id": 2, "enabled": True}, {"node_id": "n2", "agent_id": 3, "enabled": True}, {"node_id": "n3", "agent_id": 3, "enabled": True}, {"node_id": "n5", "agent_id": 6, "enabled": True}, ] s.add(Canvas(name="【模板】纯心法场", nodes=pure_xinfa, edges=[{"from": "n1", "to": "n2"}, {"from": "n2", "to": "n3"}, {"from": "n3", "to": "n5"}])) s.add(AppSetting(key="seed.canvas_defaults_initialized", value="1")) s.commit() print("画布: 默认流水线 + 【模板】无产品场 + 【模板】纯心法场") elif existing_canvases and not seeded_marker: # 兼容已经存在的旧生产库:只写初始化标记,不补写或覆盖任何画布。 s.add(AppSetting(key="seed.canvas_defaults_initialized", value="1")) s.commit() with Session(engine) as s: # 标签 if not s.exec(select(ContentTag)).first(): for name, cat, d, u, core in TAGS: s.add(ContentTag(name=name, category=cat, definition=d, usage=u, is_core=core)) s.commit() print(f"标签: {len(TAGS)} 个") # Provider if not s.exec(select(ModelProvider)).first(): for p in DEFAULT_PROVIDERS: s.add(ModelProvider(**{k: v for k, v in p.items() if k in ("name", "base_url", "api_key", "models", "default_model", "builtin", "note")})) s.commit() print(f"Provider: {len(DEFAULT_PROVIDERS)} 个") # Provider 只保存连接信息;用户可选的模型统一同步到 ModelCatalog。 for provider in s.exec(select(ModelProvider)).all(): sync_provider_catalog(s, provider) s.commit() # 提示模板独立于 Agent 规则保存,旧库升级后自动补齐内置模板。 templates_by_name = {item.name: item for item in s.exec(select(AgentPromptTemplate)).all()} for payload in PROMPT_TEMPLATES: if payload["name"] not in templates_by_name: template = AgentPromptTemplate(**payload) s.add(template) s.commit() s.refresh(template) templates_by_name[template.name] = template default_template = templates_by_name.get("内容段生产模板") # Agent if not s.exec(select(Agent)).first(): kbs = {k.name: k.id for k in s.exec(select(KnowledgeBase)).all()} for name, role, tags, kb_names, rules_key in AGENTS: kb_ids = [kbs[n] for n in kb_names if n in kbs] s.add(Agent(name=name, role=role, content_tags=tags, kb_ids=kb_ids, prompt_template_id=default_template.id if default_template and role == "production" else None, rules=RULES[rules_key], builtin=True, model_name="deepseek-chat" if role in ("coordinator", "qc", "composer") else "")) s.commit() print(f"Agent: {len(AGENTS)} 个") elif default_template: changed = False for agent in s.exec(select(Agent).where(Agent.role == "production")).all(): if agent.prompt_template_id is None: agent.prompt_template_id = default_template.id s.add(agent) changed = True if changed: s.commit() # 旧数据库可能已经有部分内置 Agent;逐项补齐后来加入的内置 Agent, # 同时保留管理员对现有 Agent 的停用/规则修改。 existing_names = {item.name for item in s.exec(select(Agent)).all()} kbs = {k.name: k.id for k in s.exec(select(KnowledgeBase)).all()} # 旧数据库升级时补齐后来加入的内置 Agent(保留管理员对已有 Agent 的修改)。 missing_builtin = [item for item in AGENTS if item[0] not in existing_names and (item[1] == "qc" or item[0] == "数字人蒸馏 Agent")] for name, role, tags, kb_names, rules_key in missing_builtin: s.add(Agent(name=name, role=role, content_tags=tags, kb_ids=[kbs[n] for n in kb_names if n in kbs], rules=RULES[rules_key], builtin=True, model_name="deepseek-chat")) if missing_builtin: s.commit() print("补齐缺失内置 Agent: " + ", ".join(item[0] for item in missing_builtin)) # 技能库:无论 Agent 是否已存在,都补齐缺失的内置技能,避免旧数据库升级后技能为空。 skills_by_name = {skill.name: skill for skill in s.exec(select(AgentSkill)).all()} for name, desc, instructions in SKILLS: skill = skills_by_name.get(name) if not skill: skill = AgentSkill(name=name, desc=desc, instructions=instructions, enabled=True, builtin=True) s.add(skill) s.commit() s.refresh(skill) skills_by_name[name] = skill # 只给没有任何绑定的内置 Agent 加默认技能,不覆盖用户已有配置。 default_skill_ids = {name: skill.id for name, skill in skills_by_name.items() if name in {item[0] for item in SKILLS}} for agent in s.exec(select(Agent)).all(): if agent.role not in ("production", "composer") and agent.name != "数字人蒸馏 Agent": continue if s.exec(select(AgentSkillBinding).where( AgentSkillBinding.agent_id == agent.id)).first(): continue names = ["大白话口播输出", "知识来源约束"] if agent.role == "composer": names.append("锚点连续性") if agent.name == "数字人蒸馏 Agent": names = ["素材证据提炼", "增量蒸馏与版本", "痛点痒点销售结构"] for name in names: skill_id = default_skill_ids.get(name) if skill_id: s.add(AgentSkillBinding(agent_id=agent.id, skill_id=skill_id)) s.commit() if __name__ == "__main__": from .db import init_db init_db() seed() print("seed 完成")