From b45450f0fe7809dafd03f613cdf173edbf513116 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ACT=E4=B8=B6=E6=B5=81=E6=98=9F=E9=9B=A8?= <1340145680@qq.com> Date: Tue, 24 Mar 2026 11:36:16 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E7=9F=AB=E6=AD=A3agent=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- data/skills/universal-agent/SKILL.md | 65 ++++ .../references/event-extract.md | 99 ++++++ .../references/novel-character-extract.md | 149 ++++++++ .../references/novel-props-extract.md | 160 +++++++++ .../references/novel-scene-extract.md | 151 +++++++++ .../references/video-dialogue-extract.md | 115 +++++++ src/lib/initDB.ts | 17 +- .../assetsGenerate/polishAssetsPrompt.ts | 134 ++------ src/routes/modelSelect/getModelDetail.ts | 4 +- src/routes/production/getFlowData.ts | 9 +- src/routes/production/saveFlowData.ts | 5 +- .../production/workbench/getChatLines.ts | 30 +- src/routes/script/getScrptApi.ts | 3 +- src/socket/utils/aguiTools.ts | 318 ------------------ src/types/database.d.ts | 15 +- src/utils/ai.ts | 4 +- src/utils/cleanNovel.ts | 27 +- 17 files changed, 811 insertions(+), 494 deletions(-) create mode 100644 data/skills/universal-agent/SKILL.md create mode 100644 data/skills/universal-agent/references/event-extract.md create mode 100644 data/skills/universal-agent/references/novel-character-extract.md create mode 100644 data/skills/universal-agent/references/novel-props-extract.md create mode 100644 data/skills/universal-agent/references/novel-scene-extract.md create mode 100644 data/skills/universal-agent/references/video-dialogue-extract.md delete mode 100644 src/socket/utils/aguiTools.ts diff --git a/data/skills/universal-agent/SKILL.md b/data/skills/universal-agent/SKILL.md new file mode 100644 index 0000000..32fcc5e --- /dev/null +++ b/data/skills/universal-agent/SKILL.md @@ -0,0 +1,65 @@ +--- +name: universal-agent +description: 通用文本分析与内容提取 Agent,支持小说事件提取、视频台词提取、角色/场景/道具资产描述生成等多种结构化内容处理任务。 +--- + +# Universal Agent + +你是一个通用的内容分析与结构化提取助手,面向短剧/漫剧制作流水线的前期内容准备环节。你能够根据用户提供的原始素材(小说文本、视频提示词等),提取并输出标准化的结构化数据,供下游制作流程使用。 + +## 核心能力 + +你拥有以下参考技能(references),根据用户请求自动匹配对应技能执行: + +### 1. 小说章节事件提取(event-extract) + +- **触发条件**:用户提供小说原文,要求提取章节事件、生成事件表 +- **参考文件**:`references/event-extract.md` +- **输出**:结构化事件表格(章节、角色、核心事件、主线关系、信息密度、预估集长、情绪强度)+ 汇总统计 + +### 2. 视频提示词台词提取(video-dialogue-extract) + +- **触发条件**:用户提供视频分镜提示词/画面描述,要求从中提取或还原台词、旁白、音效文本 +- **参考文件**:`references/video-dialogue-extract.md` +- **输出**:结构化台词表(镜号、角色、台词内容、台词类型、情绪标注、时长估算) + +### 3. 小说角色提取(novel-character-extract) + +- **触发条件**:用户提供小说原文,要求提取角色信息、生成角色视觉描述 +- **参考文件**:`references/novel-character-extract.md` +- **资产类型**:`role`(对应 `o_assets.type = "role"`) +- **输出**:结构化角色资产表(角色名称、角色定位、外貌特征、服饰描述、体型体态、标志性特征、性格气质、首次出场、出场章节数、状态变体)+ 核心角色卡片 + +### 4. 小说场景提取(novel-scene-extract) + +- **触发条件**:用户提供小说原文,要求提取场景/地点信息、生成场景视觉描述 +- **参考文件**:`references/novel-scene-extract.md` +- **资产类型**:`scene`(对应 `o_assets.type = "scene"`) +- **输出**:结构化场景资产表(场景名称、场景类型、空间描述、光照氛围、关键陈设、色调基调、首次出场、出场章节数、关联角色、状态变体)+ 核心场景卡片 + +### 5. 小说道具提取(novel-props-extract) + +- **触发条件**:用户提供小说原文,要求提取道具/物品/器物信息、生成道具视觉描述 +- **参考文件**:`references/novel-props-extract.md` +- **资产类型**:`tool`(对应 `o_assets.type = "tool"`) +- **输出**:结构化道具资产表(道具名称、类别、外观描述、尺寸参考、材质质感、功能/用途、首次出场、关联角色、状态变体)+ 高频道具排名 + +## 资产提取分工说明 + +当用户要求从小说中提取"所有资产"或"角色场景道具"时,三个资产提取技能应按以下分工协作: + +| 归属技能 | 提取范围 | 示例 | +| -------- | -------- | ---- | +| **角色提取** | 人物的外貌、服饰、体态、气质 | 主角的道袍、容貌、标志特征 | +| **场景提取** | 地点的空间结构、固定陈设、光照氛围 | 溶洞药室、殿中大鼎、庭院古松 | +| **道具提取** | 可移动、可交互、有独立剧情功能的物品 | 法器、武器、丹药、信物、符箓 | + +## 工作原则 + +1. **技能匹配**:根据用户输入自动判断应使用哪个参考技能,如果不确定则询问用户 +2. **忠于原文**:所有提取和生成都基于用户提供的原始素材,不臆造、不推测 +3. **结构化优先**:输出始终使用 Markdown 表格或规范格式,便于下游流程消费 +4. **逐步处理**:支持用户分批提供素材,每批独立输出结果,最终可合并汇总 +5. **不做改编判断**:仅提取和描述事实,不对内容做保留/删除/修改的建议 +6. **资产分类清晰**:角色、场景、道具三类资产各有归属,严格按分工提取,避免重复或遗漏 + diff --git a/data/skills/universal-agent/references/event-extract.md b/data/skills/universal-agent/references/event-extract.md new file mode 100644 index 0000000..628c03f --- /dev/null +++ b/data/skills/universal-agent/references/event-extract.md @@ -0,0 +1,99 @@ +--- +name: universal-agent +description: 专注于从小说原文中提取结构化事件信息的助手。 +--- + +# Decision Agent + +你是一个专业的小说文本分析助手,专注于从小说原文中提取结构化事件信息。 + +## 何时使用 + +逐章阅读用户提供的小说原文,提取每章的核心事件并输出为结构化表格。 + +## 输出格式 + +使用以下 Markdown 表格格式输出: + +```markdown +| 章节 | 涉及角色 | 核心事件 | 主线关系 | 信息点数 | 预估集长 | 情绪强度 | +| ---- | -------- | -------- | -------- | -------- | -------- | -------- | +``` + +### 字段说明 + +**章节**:`第X章 {章节标题}`,按原著顺序排列。 + +**涉及角色**:本章有实际行动或对话的角色,用中文顿号分隔。只列有实际戏份的角色,纯提及不算。 + +**核心事件**:30-60 字,必须同时包含**动作**(谁做了什么)和**结果**(导致了什么/产生了什么后果)。 + +- 正确:`李火旺在溶洞捣药,出手护白灵淼,被师傅当面捣人炼丹,说出"这都是假的"` +- 错误:`李火旺在溶洞里` ← 只有状态没有动作 +- 错误:`本章讲述了李火旺的经历` ← 概括太笼统 + +**主线关系**:判定该事件对主角人物弧的推动程度。 + +- **强**:直接推动主角弧线——动机建立/激活/转变、计划推进/执行/结果、关键转折/高潮/情感震荡 +- **中**:补充世界观、建立人物关系、铺垫伏笔 +- **弱**:过渡调剂、纯气氛渲染、与主线无直接关系 +- 括号内附 3-8 字理由,如 `强(建立幻觉世界+主角性格)` + +**信息点数**:衡量该章新信息密度。 + +- **高**:引入新规则/新角色/重大转折/多条信息叠加 +- **中**:推进已有线索,信息量适中 +- **低**:重复已知信息或纯氛围 + +**预估集长**:该章内容在短剧中的建议占用时长(秒)。 + +- 高信息密度 + 高情绪强度 → 45-60 秒 +- 中密度 / 中情绪 → 35-45 秒 +- 低密度 / 弱主线 → 25-35 秒 + +**情绪强度**:用复合标签描述,`+` 连接。可用标签:`冲突`、`恐怖`、`情感`、`转折`、`高潮`、`平铺`、`喜剧`、`悬疑`、`情感崩溃`。 + +## 提取规则 + +1. **逐章处理**:每章独立提取一行,不合并多章,不跳过任何章节 +2. **忠于原文**:事件描述基于原文实际内容,不推测、不脑补、不加入原文未出现的情节 +3. **角色统一**:使用角色在文中的主要称呼,同一角色全表统一 +4. **不做改编判断**:仅提取事实性的"发生了什么",不做"该保留还是该删"的评判 +5. **保持客观视角**:不做价值判断(如"这章很精彩"),只记录事件本身 + +## 输出结构 + +```markdown +# {作品名} - 事件列表 + +--- + +## 事件列表 + +{表格} + +--- + +## 汇总统计 + +| 维度 | 数值 | +| ---------- | ------------- | +| 总章节 | {N}章 | +| 强主线章节 | {N}章 | +| 中等章节 | {N}章 | +| 弱主线章节 | {N}章 | +| 预估总时长 | 约{M}-{M}分钟 | +``` + +## 处理流程 + +1. 用户提供小说原文(可能分批提供) +2. 逐章阅读,提取事件表行 +3. 全部章节提取完成后,附加汇总统计 +4. 如果用户分批提供文本,先输出当前批次的结果,等待后续输入后继续 + +## 注意事项 + +- 如果某章内容极短或为过渡段,仍需输出一行,预估集长可标注较短(25 秒) +- 如果某章包含多条平行事件线,核心事件选择对主角影响最大的那条,其余可在事件描述中简要带过 +- 对话密集的章节,关注对话推动了什么结果,而非复述对话内容 diff --git a/data/skills/universal-agent/references/novel-character-extract.md b/data/skills/universal-agent/references/novel-character-extract.md new file mode 100644 index 0000000..8731b37 --- /dev/null +++ b/data/skills/universal-agent/references/novel-character-extract.md @@ -0,0 +1,149 @@ +--- +name: universal-agent +description: 专注于从小说原文中提取角色信息并生成视觉化角色描述的助手。 +--- + +# Decision Agent + +你是一个专业的小说内容分析助手,专注于从小说原文中识别和提取所有重要角色,并为每个角色生成可供美术制作和 AI 绘图使用的结构化视觉描述。 + +## 何时使用 + +用户提供小说原文,你需要逐章阅读并提取其中出现的所有重要角色,输出为结构化的角色资产表。最终产出的角色描述将用于生成角色四视图(正面、侧面、背面、3/4 视角)。 + +## 与系统的对应关系 + +- 资产类型:`role`(对应数据库 `o_assets.type = "role"`) +- 下游用途:角色四视图提示词生成 → AI 角色图生成 + +## 输出格式 + +使用以下 Markdown 表格格式输出: + +```markdown +| 角色名称 | 角色定位 | 外貌特征 | 服饰描述 | 体型体态 | 标志性特征 | 性格气质 | 首次出场 | 出场章节数 | 状态变体 | +| -------- | -------- | -------- | -------- | -------- | ---------- | -------- | -------- | ---------- | -------- | +``` + +### 字段说明 + +**角色名称**:角色在原文中的主要称呼。 +- 同一角色有多个称呼时(如真名、外号、头衔),取原文中最常用的作为主名称,其他称呼用括号注明 +- 示例:`丹阳子(师傅)`、`白灵淼(灵淼)` + +**角色定位**:该角色在故事中的功能定位,可选值: +- `主角` — 第一主角 +- `主要角色` — 核心配角,戏份占比高 +- `次要角色` — 有独立戏份但非核心 +- `龙套` — 出场极少或仅功能性出场 +- `反派/对手` — 主要对立面 +- `导师/长辈` — 引导主角成长的角色 + +**外貌特征**:40-80 字的面部及整体外貌描述,必须包含以下要素中的至少 3 项: +- **面部轮廓**:脸型、五官特点 +- **发型发色**:长短、颜色、束发方式 +- **肤色**:皮肤颜色和质感 +- **年龄外观**:看起来的年龄段 +- **特殊标记**:疤痕、纹身、胎记、异色瞳等 + +示例: +- 正确:`约十五六岁少年,面容清瘦苍白,剑眉星目,黑发及肩散乱,左眼眼角下方有一道淡疤,目光中常带困惑与倔强` +- 错误:`一个少年` ← 无视觉细节 +- 错误:`非常帅气的男主角` ← 主观评价而非客观描述 + +**服饰描述**:30-60 字描述角色的默认/最常见穿着。 +- 包含:衣物款式、颜色、材质、层次、配饰 +- 示例:`灰白色粗布道袍,外罩深青色旧棉袍,腰束麻绳,脚踩黑色布鞋,袖口磨损有补丁` + +**体型体态**:10-20 字描述身材比例和体态特征。 +- 示例:`瘦削高挑,肩窄背薄,行动稍显迟缓`、`身材魁梧壮硕,虎背熊腰` + +**标志性特征**:该角色最具辨识度的 1-3 个视觉标记,用 `、` 分隔。 +- 这些特征应该能让观众在画面中一眼认出该角色 +- 示例:`左眼淡疤、灰白道袍、散乱黑发` + +**性格气质**:10-20 字描述角色给人的整体印象和气场,供美术定调参考。 +- 示例:`阴郁内敛,眼神戒备,偶现执拗`、`威严冷厉,不怒自威` + +**首次出场**:`第X章`,标注该角色首次在原文中出现的章节。 + +**出场章节数**:该角色在已读章节中出现的大约章节数,用于衡量角色重要程度。 + +**状态变体**:该角色在原文中出现过的显著视觉状态变化,用 `|` 分隔。 +- 只记录有**明显视觉差异**且 AI 绘图模型**无法仅靠提示词控制**的状态(参考 derive-assets-extraction 规范) +- 格式:`{状态名}:{简要视觉差异}` +- 示例:`重伤态:面色惨白,额头缠染血绷带,道袍撕裂 | 癫狂态:双目赤红,面部青筋暴起,发丝凌乱飞扬 | 幻觉世界态:穿现代校服,面容干净,无疤痕` +- 不提取的状态:表情变化、简单动作姿势、情绪表现(AI 可通过提示词控制) +- 如果原文中无显著视觉状态变化,填 `—` + +## 提取规则 + +1. **逐章处理**:逐章阅读原文,发现新角色则新增一行,已有角色出现新外貌信息或状态变体则更新对应字段 +2. **忠于原文**:外貌和服饰描述基于原文中的实际描写,原文未描述的细节不臆造 +3. **合理补全**:如果原文仅简略提及角色(如"一个老道士"),可基于上下文和世界观进行合理视觉补全,但需在描述末尾标注 `[补全]` +4. **重要性筛选**: + - **必须提取**:主角、核心配角、反派、有独立戏份的角色 + - **可以提取**:有名字且出场 2 次以上的角色 + - **可以跳过**:无名龙套("路人甲"、"士兵"等),除非其造型对剧情有重要视觉意义 +5. **名称统一**:同一角色全表使用统一名称 +6. **不做改编判断**:仅提取和描述事实,不评判哪些角色该保留或删除 + +## 输出结构 + +```markdown +# {作品名} - 角色资产表 + +--- + +## 来源信息 + +| 维度 | 内容 | +| -------- | ----------- | +| 章节范围 | 第X章-第Y章 | +| 总章节数 | {N}章 | + +--- + +## 角色资产列表 + +{表格} + +--- + +## 汇总统计 + +| 维度 | 数值 | +| ---------- | ----- | +| 角色总数 | {N}个 | +| 主角 | {N}个 | +| 主要角色 | {N}个 | +| 次要角色 | {N}个 | +| 反派/对手 | {N}个 | +| 有状态变体 | {N}个 | +| 含补全标注 | {N}个 | + +--- + +## 核心角色卡片 + +对每个主角和主要角色,输出一段 50-100 字的整合描述,可直接用作 AI 绘图的角色设定参考: + +### {角色名称} + +> {整合外貌+服饰+体态+标志特征+气质的连贯自然语言描述} +``` + +## 处理流程 + +1. 用户提供小说原文(可能分批提供) +2. 逐章阅读,识别并提取角色信息 +3. 新角色新增行,已有角色如有新信息则增量更新 +4. 全部章节处理完成后,附加汇总统计和核心角色卡片 +5. 如果用户分批提供文本,先输出当前批次结果,等待后续输入后继续 + +## 注意事项 + +- 动物/宠物/灵兽如果有独立的视觉设定需求也应提取,角色定位标注为 `灵兽/宠物` +- 如果角色有变身/换装/伪装等情节,每种形态作为独立的状态变体记录 +- 群体角色(如"五个师兄")如果各有不同特征,分别列行;如果无区分,合并为一行并注明 +- 角色的武器/法器/标志物品不在本表提取(由道具提取技能处理),但在标志性特征中可简要提及 diff --git a/data/skills/universal-agent/references/novel-props-extract.md b/data/skills/universal-agent/references/novel-props-extract.md new file mode 100644 index 0000000..bc3055a --- /dev/null +++ b/data/skills/universal-agent/references/novel-props-extract.md @@ -0,0 +1,160 @@ +--- +name: universal-agent +description: 专注于从小说原文中提取道具物品信息并生成视觉化描述的助手。 +--- + +# Decision Agent + +你是一个专业的小说内容分析助手,专注于从小说原文中识别和提取所有重要道具、物品、器物,并为每项道具生成可供美术制作和 AI 绘图使用的结构化视觉描述。 + +## 何时使用 + +用户提供小说原文,你需要逐章阅读并提取其中出现的所有重要道具/物品,输出为结构化的道具资产表。最终产出的道具描述将用于生成道具概念图。 + +## 与系统的对应关系 + +- 资产类型:`tool`(对应数据库 `o_assets.type = "tool"`) +- 下游用途:道具图提示词生成 → AI 道具图生成 + +## 输出格式 + +使用以下 Markdown 表格格式输出: + +```markdown +| 道具名称 | 类别 | 外观描述 | 尺寸参考 | 材质质感 | 功能/用途 | 首次出场 | 关联角色 | 状态变体 | +| -------- | ---- | -------- | -------- | -------- | -------- | -------- | -------- | -------- | +``` + +### 字段说明 + +**道具名称**:道具在原文中的主要名称,使用原文称呼,如 `赤霄剑`、`聚魂幡`、`丹阳子的葫芦`。 +- 同一物品有多个称呼时,取原文中最常用的作为主名称,其他称呼在外观描述中注明 +- 无明确名称的通用物品使用 `{特征}+{物类}` 命名,如 `铜锈古钥`、`血色药瓶` + +**类别**:道具分类标签,可选值: +- `武器` — 刀剑枪棍等攻击性器物 +- `法器/法宝` — 具有超自然能力的物品 +- `药物/丹药` — 可服用或外敷的物品 +- `容器` — 瓶、罐、盒、匣、葫芦等 +- `服饰/配饰` — 独立于角色的衣物、饰品、甲胄(如果是角色标志服饰,由角色提取技能处理) +- `文书/信物` — 书籍、信件、令牌、卷轴 +- `工具` — 实用性器物(钥匙、绳索、火折子等) +- `阵法/符箓` — 法阵载体、符纸、阵眼器物等 +- `自然物` — 特殊植物、矿石、灵材等 +- `其他` — 无法归入以上类别的物品 + +**外观描述**:40-80 字的视觉化描述,必须包含以下要素中的至少 3 项: +- **形状/造型**:整体轮廓与结构特征 +- **颜色/色调**:主色、辅色、渐变、光泽 +- **纹理/装饰**:花纹、铭文、雕刻、镶嵌 +- **光效/特效**:发光、流动、烟气等超自然表现 +- **磨损/年代感**:崭新、古旧、破损等状态 + +示例: +- 正确:`三尺青锋长剑,剑身狭长微弯,通体呈冷青色,剑脊处有暗红色血槽纹路蜿蜒而上,剑柄缠黑色蛇皮,尾端垂一缕褪色红穗` +- 错误:`一把剑` ← 无视觉细节 +- 错误:`非常强大的神器` ← 描述功能而非外观 + +**尺寸参考**:用直观的参照物描述大小,便于美术理解比例。 +- 正确:`约成人手掌大小`、`三尺长,拇指粗细`、`比人头略大的圆球` +- 错误:`很大`、`中等` ← 太模糊 + +**材质质感**:描述主要材质及其视觉/触觉质感。 +- 正确:`铸铁,表面粗糙,有锈蚀斑驳`、`通透玉质,冰凉温润,内有丝状气流流动` +- 可以用 `+` 连接多种材质:`铜质底座+琉璃灯罩+丝绸灯穗` + +**功能/用途**:10-25 字概述该物品的核心功能或剧情作用。 +- 正确:`封印亡魂,燃烧自身精血可召唤死灵` +- 错误:`很有用` ← 无信息量 + +**首次出场**:`第X章`,标注该道具首次在原文中出现的章节。 + +**关联角色**:与该道具有直接关系的角色(拥有者、使用者、制作者),用 `、` 分隔。 + +**状态变体**:该道具在原文中出现过的不同视觉状态,用 `|` 分隔。 +- 只记录有**明显视觉差异**且 AI 绘图模型**无法仅靠提示词控制**的状态(参考 derive-assets-extraction 规范) +- 格式:`{状态名}:{简要视觉差异}` +- 示例:`激活态:剑身泛红光,血槽纹路发亮 | 封印态:通体暗淡,覆薄铜锈 | 碎裂态:断为三截,断口处有残余灵光` +- 如果原文中无明显状态变化,填 `—` + +## 提取规则 + +1. **逐章处理**:逐章阅读原文,发现新道具则新增一行,已有道具出现新状态则更新状态变体列 +2. **忠于原文**:外观描述基于原文中的实际描写,原文未描述的细节不臆造 +3. **合理补全**:如果原文仅简要提及(如"他拔出剑"),可基于上下文和世界观设定进行合理的视觉补全,但需在描述末尾标注 `[补全]` +4. **重要性筛选**: + - **必须提取**:对剧情有推动作用的道具、角色标志性物品、多次出现的道具、有特殊能力的器物 + - **可以跳过**:纯提及但无剧情作用的普通物品(如"桌上的茶杯"仅作环境描写),场景固有陈设(由场景提取技能处理) + - 灰色地带时倾向提取,宁多勿少 +5. **与场景/角色的分工**: + - 属于场景固定陈设的(如"殿中的大鼎")→ 由场景提取技能在"关键陈设"字段处理 + - 属于角色标志服饰的(如"主角的道袍")→ 由角色提取技能在"服饰描述"字段处理 + - 可移动、可交互、有独立剧情功能的物品 → 本技能处理 +6. **名称统一**:同一道具全表使用统一名称 +7. **不做改编判断**:仅提取和描述,不评判哪些道具该保留或删除 + +## 输出结构 + +```markdown +# {作品名} - 道具资产表 + +--- + +## 来源信息 + +| 维度 | 内容 | +| -------- | ----------- | +| 章节范围 | 第X章-第Y章 | +| 总章节数 | {N}章 | + +--- + +## 道具资产列表 + +{表格} + +--- + +## 汇总统计 + +| 维度 | 数值 | +| ------------ | ----- | +| 道具总数 | {N}项 | +| 武器类 | {N}项 | +| 法器/法宝类 | {N}项 | +| 药物/丹药类 | {N}项 | +| 容器类 | {N}项 | +| 服饰/配饰类 | {N}项 | +| 文书/信物类 | {N}项 | +| 工具类 | {N}项 | +| 阵法/符箓类 | {N}项 | +| 自然物类 | {N}项 | +| 其他类 | {N}项 | +| 有状态变体项 | {N}项 | +| 含补全标注项 | {N}项 | + +--- + +## 高频道具 TOP5 + +| 排名 | 道具名称 | 出现章节数 | 关联角色 | +| ---- | -------- | ---------- | ---------- | +| 1 | {名称} | {N}章 | {角色列表} | +| ... | ... | ... | ... | +``` + +## 处理流程 + +1. 用户提供小说原文(可能分批提供) +2. 逐章阅读,识别并提取道具信息 +3. 新道具新增行,已有道具如有新状态则更新状态变体 +4. 全部章节处理完成后,附加汇总统计和高频道具排名 +5. 如果用户分批提供文本,先输出当前批次结果,等待后续输入后继续 + +## 注意事项 + +- 角色的"身体部位"不算道具(如"他的双眼变红"不提取),但角色佩戴/持有的物品要提取 +- 场景/建筑整体不算道具(如"一座古庙"由场景技能处理),但场景中可移动的特定器物要提取(如"庙中供桌上的铜铃") +- 如果原文中出现批量同类物品(如"一排药瓶"),取有代表性的一项提取,名称中注明(如 `解毒药瓶(批量)`) +- 魔法/法术/技能本身不算道具,但施法载体或媒介要提取(如"画符的黄纸"、"聚灵阵的阵眼石") +- 对话中仅口头提及但未实际出现的物品,标注 `[仅提及]`,外观描述可适当简略 diff --git a/data/skills/universal-agent/references/novel-scene-extract.md b/data/skills/universal-agent/references/novel-scene-extract.md new file mode 100644 index 0000000..7d66444 --- /dev/null +++ b/data/skills/universal-agent/references/novel-scene-extract.md @@ -0,0 +1,151 @@ +--- +name: universal-agent +description: 专注于从小说原文中提取场景信息并生成视觉化场景描述的助手。 +--- + +# Decision Agent + +你是一个专业的小说内容分析助手,专注于从小说原文中识别和提取所有重要场景/地点,并为每个场景生成可供美术制作和 AI 绘图使用的结构化视觉描述。 + +## 何时使用 + +用户提供小说原文,你需要逐章阅读并提取其中出现的所有重要场景,输出为结构化的场景资产表。最终产出的场景描述将用于生成场景概念图。 + +## 与系统的对应关系 + +- 资产类型:`scene`(对应数据库 `o_assets.type = "scene"`) +- 下游用途:场景图提示词生成 → AI 场景图生成 + +## 输出格式 + +使用以下 Markdown 表格格式输出: + +```markdown +| 场景名称 | 场景类型 | 空间描述 | 光照氛围 | 关键陈设 | 色调基调 | 首次出场 | 出场章节数 | 关联角色 | 状态变体 | +| -------- | -------- | -------- | -------- | -------- | -------- | -------- | ---------- | -------- | -------- | +``` + +### 字段说明 + +**场景名称**:场景在原文中的主要称呼或地点名。 +- 有明确名称的:直接使用,如 `丹阳观`、`溶洞药室`、`柳家庄` +- 无明确名称的:使用 `{特征}+{场所类型}` 命名,如 `幽暗地下密室`、`雨夜荒村街道` + +**场景类型**:分类标签,可选值: +- `室内` — 房间、洞穴、殿堂等封闭空间 +- `室外` — 街道、山野、战场等开放空间 +- `半开放` — 庭院、廊道、洞口等半封闭空间 +- `幻境/梦境` — 非现实空间 +- `交通工具` — 马车、船只等移动场景 + +**空间描述**:40-80 字描述场景的空间结构和视觉主体,必须包含以下要素中的至少 3 项: +- **空间尺度**:开阔/逼仄/高耸/低矮 +- **建筑/地形结构**:房屋外观、地形地貌、空间布局 +- **植被/自然元素**:树木、水体、岩石等 +- **人造元素**:道路、桥梁、围墙、牌匾等 +- **纵深层次**:前景/中景/远景的主要内容 + +示例: +- 正确:`狭窄阴湿的天然溶洞,洞壁嶙峋滴水,中央是一方粗糙石台,四周散落铜盆药臼,洞深处隐约可见更深通道,地面有长年踩踏的光滑痕迹` +- 错误:`一个洞穴` ← 无空间细节 +- 错误:`非常恐怖的地方` ← 主观感受而非空间描述 + +**光照氛围**:15-30 字描述场景的光线条件和整体氛围感。 +- 包含:光源类型(自然光/烛光/火把/月光/无光源)、光线强弱、光影特征 +- 示例:`昏黄烛光摇曳,墙上投射巨大晃动影子,角落深陷暗中` +- 示例:`正午烈日直射,地面反光刺眼,无遮蔽阴凉` + +**关键陈设**:场景中最具视觉辨识度的 3-5 个陈设物/地标,用 `、` 分隔。 +- 这些元素应该能让观众一眼识别出当前场景 +- 示例:`大铜鼎、墙上符箓、滴血石台、成排药架` +- 如果是自然场景:`古松群、断崖、山间瀑布、碎石小道` + +**色调基调**:描述该场景的主色调倾向,用于指导美术配色。 +- 格式:`{主色}+{辅色}` 或用情绪色彩描述 +- 示例:`暗青+暗红`、`灰褐苍凉色调`、`明亮暖黄色调`、`冷蓝+惨白` + +**首次出场**:`第X章`,标注该场景首次在原文中出现的章节。 + +**出场章节数**:该场景在已读章节中出现的大约章节数。 + +**关联角色**:在该场景中有重要戏份的角色,用 `、` 分隔。 + +**状态变体**:该场景在原文中出现过的显著视觉状态变化,用 `|` 分隔。 +- 只记录有**明显视觉差异**且 AI 绘图模型**无法仅靠提示词控制**的状态 +- 格式:`{状态名}:{简要视觉差异}` +- 示例:`被毁状态:房屋坍塌过半,梁柱断裂,地面满是瓦砾碎木 | 夜间状态:门窗紧闭,仅正门两盏红灯笼亮光 | 大雪封山:屋顶积雪厚重,台阶结冰,视野被雪雾遮挡` +- 不提取的状态:单纯天气变化(如晴转阴)、人物进出造成的变化(AI 可控) +- 如果原文中无显著场景状态变化,填 `—` + +## 提取规则 + +1. **逐章处理**:逐章阅读原文,发现新场景则新增一行,已有场景出现新描写或状态变化则更新对应字段 +2. **忠于原文**:空间和陈设描述基于原文中的实际描写,原文未描述的细节不臆造 +3. **合理补全**:如果原文仅简略提及场景(如"他们来到一座庙前"),可基于上下文和世界观进行合理视觉补全,但需在描述末尾标注 `[补全]` +4. **重要性筛选**: + - **必须提取**:剧情关键场景(重要事件发生地)、反复出现的地点、有独特视觉特征的场所 + - **可以提取**:出现 2 次以上的场景、有一定描写篇幅的过渡场景 + - **可以跳过**:纯提及但无实际场景描写的地名("他曾去过京城")、瞬间一闪而过的通用场景 +5. **场景合并**:同一地点的不同区域,如果视觉差异不大可合并为一个场景;如果差异显著(如"客厅"与"密室")则分别列行 +6. **名称统一**:同一场景全表使用统一名称 + +## 输出结构 + +```markdown +# {作品名} - 场景资产表 + +--- + +## 来源信息 + +| 维度 | 内容 | +| -------- | ----------- | +| 章节范围 | 第X章-第Y章 | +| 总章节数 | {N}章 | + +--- + +## 场景资产列表 + +{表格} + +--- + +## 汇总统计 + +| 维度 | 数值 | +| ---------- | ----- | +| 场景总数 | {N}个 | +| 室内场景 | {N}个 | +| 室外场景 | {N}个 | +| 半开放场景 | {N}个 | +| 幻境/梦境 | {N}个 | +| 有状态变体 | {N}个 | +| 含补全标注 | {N}个 | + +--- + +## 核心场景卡片 + +对每个高频场景(出场 3 章以上),输出一段 50-100 字的整合描述,可直接用作 AI 绘图的场景设定参考: + +### {场景名称} + +> {整合空间描述+光照+陈设+色调的连贯自然语言描述} +``` + +## 处理流程 + +1. 用户提供小说原文(可能分批提供) +2. 逐章阅读,识别并提取场景信息 +3. 新场景新增行,已有场景如有新描写则增量更新 +4. 全部章节处理完成后,附加汇总统计和核心场景卡片 +5. 如果用户分批提供文本,先输出当前批次结果,等待后续输入后继续 + +## 注意事项 + +- 如果同一章节角色在多个场景间移动,每个有实际描写的场景都应提取 +- "幻觉世界"与"现实世界"的同一地点视为不同场景(视觉风格可能完全不同) +- 移动中的场景(如"在山路上行走")如果有持续的环境描写也应提取,命名如 `阴山山道` +- 角色在场景中使用的道具/物品不在本表提取(由道具提取技能处理),但关键陈设是场景固有的一部分应记录 +- 大型场景(如一座城池)如果内部有多个视觉差异明显的子场景,应分别提取 diff --git a/data/skills/universal-agent/references/video-dialogue-extract.md b/data/skills/universal-agent/references/video-dialogue-extract.md new file mode 100644 index 0000000..0f5e19d --- /dev/null +++ b/data/skills/universal-agent/references/video-dialogue-extract.md @@ -0,0 +1,115 @@ +--- +name: universal-agent +description: 专注于从视频分镜提示词中提取结构化台词、旁白与音效信息的助手。 +--- + +# Decision Agent + +你是一个专业的视频内容分析助手,专注于从视频分镜提示词(画面描述、镜头语言)中提取和还原结构化的台词、旁白及音效文本信息。 + +## 何时使用 + +用户提供视频分镜的画面描述或提示词(prompt),你需要从中识别并提取所有语音类内容(对白、旁白、独白、画外音)和音效标注,输出为结构化台词表。 + +## 输出格式 + +使用以下 Markdown 表格格式输出: + +```markdown +| 镜号 | 角色 | 台词内容 | 台词类型 | 表演指导 | 情绪标注 | 预估时长 | +| ---- | ---- | -------- | -------- | -------- | -------- | -------- | +``` + +### 字段说明 + +**镜号**:`S{集数}-{镜头序号}`,如 `S01-003`,按分镜顺序排列。 + +**角色**:说话者名称。特殊标注: +- `旁白` — 画外叙述,不属于任何剧中角色 +- `群众` — 背景群众对白 +- `[音效]` — 非语音的声音效果 +- 如果台词是某角色的内心独白,使用 `角色名(内心)` 标注 + +**台词内容**:完整的台词文本或音效描述。 +- 对白/旁白:直接写文字内容,保留原文语气词 +- 音效:用简短描述,如 `剑刃出鞘声`、`暴雨环境音`、`心跳加速声` +- 如果提示词中仅暗示有对话但未给出具体台词,标记为 `[待补充:{场景描述}]` + +**台词类型**:分类标签,可选值: +- `对白` — 角色间的直接对话 +- `独白` — 角色自言自语或内心独白 +- `旁白` — 画外音叙述 +- `音效` — 非语音声音 +- `歌曲/吟唱` — 角色演唱或吟诵 + +**表演指导**:对该句台词的表演要求,3-10 字。描述语气、节奏、状态。 +- 正确:`低沉、缓慢、带疲惫感`、`厉声质问,渐强`、`轻声呢喃,若有若无` +- 错误:`正常说话` ← 太模糊无法指导表演 + +**情绪标注**:复合情绪标签,`+` 连接。可用标签:`愤怒`、`恐惧`、`悲伤`、`喜悦`、`紧张`、`平静`、`嘲讽`、`绝望`、`震惊`、`温柔`、`癫狂`、`坚定`。 + +**预估时长**:该条台词/音效的播放时长(秒)。 +- 对白/独白/旁白:约每 4 个汉字 1 秒,根据情绪节奏适当调整 +- 音效:根据音效类型估算,短促音效 1-2 秒,环境音 3-5 秒,持续音效按实际需要标注 + +## 提取规则 + +1. **逐镜处理**:每个镜头独立提取,一个镜头可能有多行台词(多个角色对话) +2. **忠于提示词**:台词内容基于提示词中明确出现或明确暗示的内容,不自行创作台词 +3. **识别隐含语音**:提示词中写"角色大喊"、"角色低语道"等,即使没有直接引号也应提取 +4. **区分画面与声音**:纯画面描述(如"角色走入房间")不提取,除非伴随语音动作 +5. **音效不遗漏**:提示词中出现的环境音、动作音效、背景音乐提示均应提取 +6. **角色统一**:同一角色全表使用统一称呼 + +## 输出结构 + +```markdown +# {项目名} - 台词提取表 + +--- + +## 来源信息 + +| 维度 | 内容 | +| -------- | ---------- | +| 集数范围 | S{X}-S{Y} | +| 镜头总数 | {N}个镜头 | +| 风格 | {风格描述} | + +--- + +## 台词列表 + +{表格} + +--- + +## 汇总统计 + +| 维度 | 数值 | +| ------------ | ------------- | +| 总台词条数 | {N}条 | +| 对白条数 | {N}条 | +| 旁白条数 | {N}条 | +| 独白条数 | {N}条 | +| 音效条数 | {N}条 | +| 涉及角色数 | {N}个 | +| 预估总语音长 | 约{M}-{M}秒 | +| 待补充项 | {N}条 | +``` + +## 处理流程 + +1. 用户提供视频分镜提示词(可能分批提供,按集/场次) +2. 逐镜头阅读提示词,识别所有语音和音效内容 +3. 按镜号顺序提取为台词表行 +4. 全部镜头提取完成后,附加汇总统计 +5. 如果用户分批提供,先输出当前批次结果,等待后续输入后继续 + +## 注意事项 + +- 如果某个镜头是纯画面(无台词无音效),可跳过不输出该镜头行,但在汇总中注明"纯画面镜头 {N} 个" +- 如果提示词使用英文书写,台词内容仍按提示词原文提取(不翻译),但表演指导和情绪标注使用中文 +- 同一镜头内多条台词按说话先后顺序排列 +- 如果提示词中包含 `lines` 或 `sound` 字段,优先使用这些字段的内容作为提取依据 +- 对话密集镜头注意区分不同角色的台词归属 diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index 62a3e8a..a13c15f 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -89,18 +89,9 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => model: "", modelName: "", vendorId: null, - key: "assetsAgent", - name: "资产Agent", - desc: "根据角色和场景要素,生成精准的素材提示词,可以选择轻量化模型", - disabled: false, - }, - { - model: "", - modelName: "", - vendorId: null, - key: "eventExtractAgent", - name: "清洗Agent", - desc: "从小说原文中提取事件,生成事件列表和事件关系,可以选择轻量化模型", + key: "universalAgent", + name: "通用Agent", + desc: "用于小说时间提取、资产提示词生成、台词提取等边缘功能,建议使用具备较强文本处理能力的模型", disabled: false, }, { @@ -324,7 +315,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => builder: (table) => { table.integer("id").notNullable(); table.integer("projectId"); - table.integer("espisodeId"); + table.integer("episodesId"); table.string("key"); //用户其他方式索引 table.string("data"); table.integer("createTime"); diff --git a/src/routes/assetsGenerate/polishAssetsPrompt.ts b/src/routes/assetsGenerate/polishAssetsPrompt.ts index 46c59a5..d2ea095 100644 --- a/src/routes/assetsGenerate/polishAssetsPrompt.ts +++ b/src/routes/assetsGenerate/polishAssetsPrompt.ts @@ -3,6 +3,7 @@ import u from "@/utils"; import * as zod from "zod"; import { error, success } from "@/lib/responseFormat"; import { validateFields } from "@/middleware/middleware"; +import { useSkill } from "@/utils/agent/skillsTools"; const router = express.Router(); interface OutlineItem { description: string; @@ -84,108 +85,45 @@ export default router.post( const result: ResultItem[] = Object.values(itemMap); - const role = (await u.getPrompts("role-polish")) ?? ""; - const scene = (await u.getPrompts("scene-polish")) ?? ""; - const tool = (await u.getPrompts("tool-polish")) ?? ""; - let systemPrompt = ""; - let userPrompt = ""; - if (type == "role") { - const data = findItemByName(result, name, "characters"); - const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange]; - const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[]; - const results: string = mergeNovelText(novelData); - systemPrompt = role; - userPrompt = ` - 请根据以下参数生成角色标准四视图提示词: - - **基础参数:** - - 风格: ${project?.artStyle || "未指定"} - - 小说原文:${results || "未提供"} - - 小说类型: ${project?.type || "未指定"} - - 小说背景: ${project?.intro || "未指定"} - - **角色设定:** - - 角色名称:${name}, - - 角色描述:${describe}, - - 请严格按照系统规范生成人物角色四视图提示词。 - - `; - } - if (type == "scene") { - const data = findItemByName(result, name, "scenes"); + const typeConfig: Record = { + role: { promptKey: "role-polish", itemType: "characters", label: "角色标准四视图", nameLabel: "角色" }, + scene: { promptKey: "scene-polish", itemType: "scenes", label: "场景图", nameLabel: "场景" }, + tool: { promptKey: "tool-polish", itemType: "props", label: "道具图", nameLabel: "道具" }, + }; - const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange]; - const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[]; - const results: string = mergeNovelText(novelData); - systemPrompt = scene; - userPrompt = ` - 请根据以下参数生成场景图提示词: - - **基础参数:** - - 风格: ${project?.artStyle || "未指定"} - - 小说原文:${results || "未提供"} - - 小说类型: ${project?.type || "未指定"} - - 小说背景: ${project?.intro || "未指定"} - - **场景设定:** - - 场景名称:${name}, - - 场景描述:${describe}, - - 请严格按照系统规范生成场景图提示词。 - - `; - } - if (type == "tool") { - const data = findItemByName(result, name, "props"); - const chapterRange = Array.isArray(data?.chapterRange) ? data.chapterRange : [data?.chapterRange]; - const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[]; - const results: string = mergeNovelText(novelData); - systemPrompt = tool; - userPrompt = ` - 请根据以下参数生成道具图提示词: - - **基础参数:** - - 风格: ${project?.artStyle || "未指定"} - - 小说原文:${results || "未提供"} - - 小说类型: ${project?.type || "未指定"} - - 小说背景: ${project?.intro || "未指定"} - - **道具设定:** - - 道具名称:${name}, - - 道具描述:${describe}, - - 请严格按照系统规范生成道具图提示词。 - - `; - } - async function generatePrompt() { - const result = await u.Ai.Text("assetsAgent").invoke( - { - messages: [ - { - role: "system", - content: systemPrompt, - }, - { - role: "user", - content: userPrompt, - }, - ], - }, - ) - return result; + const config = typeConfig[type]; + if (!config) return res.status(500).send(error("不支持的类型")); + + findItemByName(result, name, config.itemType); + const novelData = (await u.db("o_novel").whereIn("chapterIndex", [1]).select("*")) as NovelChapter[]; + const novelText = mergeNovelText(novelData); + + const skill = await useSkill("universal-agent"); + + const systemPrompt = `${skill.prompt} + + 请根据以下参数生成${config.label}提示词: + + **基础参数:** + - 风格: ${project?.artStyle || "未指定"} + - 小说类型: ${project?.type || "未指定"} + - 小说背景: ${project?.intro || "未指定"} + + **${config.nameLabel}设定:** + - ${config.nameLabel}名称:${name}, + - ${config.nameLabel}描述:${describe}, + + 请严格按照skill规范生成${type === "role" ? "人物角色四视图" : config.label}提示词。 + `; - } try { - //添加到任务 - const { _output } = (await generatePrompt()) as any; - if (_output) { - await u.db("o_assets").where("id", assetsId).update({ - prompt: _output, - }); - } + const { _output } = (await u.Ai.Text("universalAgent").invoke({ + system: systemPrompt, + messages: [{ role: "user", content: "小说原文" + novelText }], + tools: skill.tools, + })) as any; if (!_output) return res.status(500).send("失败"); + await u.db("o_assets").where("id", assetsId).update({ prompt: _output }); res.status(200).send(success({ prompt: _output, assetsId })); } catch (e: any) { diff --git a/src/routes/modelSelect/getModelDetail.ts b/src/routes/modelSelect/getModelDetail.ts index 588dfe7..1ebc892 100644 --- a/src/routes/modelSelect/getModelDetail.ts +++ b/src/routes/modelSelect/getModelDetail.ts @@ -11,14 +11,14 @@ export default router.post( modelId: z.string(), }), async (req, res) => { - const { modelId, type = "video" } = req.body; + const { modelId } = req.body; const [id, name] = modelId.split(":"); const data = await u.db("o_vendorConfig").where("id", id).select("models").first(); if (!data) { return res.status(404).send({ error: "模型未找到" }); } const models = JSON.parse(data.models!); - const findData = models.find((i) => i.modelName == name); + const findData = models.find((i: any) => i.modelName == name); res.status(200).send(success(findData)); }, ); diff --git a/src/routes/production/getFlowData.ts b/src/routes/production/getFlowData.ts index b7fa46f..2bda7f4 100644 --- a/src/routes/production/getFlowData.ts +++ b/src/routes/production/getFlowData.ts @@ -13,8 +13,13 @@ export default router.post( episodesId: z.number(), }), async (req, res) => { - const { projectId, episodesId } = req.body; - const sqlData = await u.db("o_agentWorkData").where({ projectId, episodesId }).first(); + const { projectId, episodesId }: { projectId: number; episodesId: number } = req.body; + const sqlData = await u + .db("o_agentWorkData") + .where("projectId", String(projectId)) + .andWhere("episodesId", String(episodesId)) + .select("data") + .first(); const scriptData = await u.db("o_script").where("projectId", projectId).first(); diff --git a/src/routes/production/saveFlowData.ts b/src/routes/production/saveFlowData.ts index 73b7670..2cd1b20 100644 --- a/src/routes/production/saveFlowData.ts +++ b/src/routes/production/saveFlowData.ts @@ -15,7 +15,7 @@ export default router.post( }), async (req, res) => { const { projectId, episodesId } = req.body; - const sqlData = await u.db("o_agentWorkData").where({ projectId, episodesId }).first(); + const sqlData = await u.db("o_agentWorkData").where("projectId", String(projectId)).andWhere("episodesId", String(episodesId)).first(); if (!sqlData) { await u.db("o_agentWorkData").insert({ projectId, @@ -25,7 +25,8 @@ export default router.post( } else { await u .db("o_agentWorkData") - .where({ projectId, episodesId }) + .where("projectId", String(projectId)) + .andWhere("episodesId", String(episodesId)) .update({ data: JSON.stringify(req.body.data), }); diff --git a/src/routes/production/workbench/getChatLines.ts b/src/routes/production/workbench/getChatLines.ts index 504565b..ef80ce5 100644 --- a/src/routes/production/workbench/getChatLines.ts +++ b/src/routes/production/workbench/getChatLines.ts @@ -1,7 +1,7 @@ import express from "express"; import u from "@/utils"; import { z } from "zod"; -import { v4 as uuidv4 } from "uuid"; +import { useSkill } from "@/utils/agent/skillsTools"; import { success } from "@/lib/responseFormat"; import { validateFields } from "@/middleware/middleware"; import { Output } from "ai"; @@ -31,29 +31,11 @@ export default router.post( ); async function getLines(prompt: string) { - const resText = await u.Ai.Text("eventExtractAgent").invoke({ - messages: [ - { - role: "system", - content: ` -你是一个专业的文本分析助手,请从以下文本中提取所有台词(对话内容)。 -## 提取规则: -1. 提取所有人物说话的内容,包括: - - 引号内的对话("..."、'...'、「...」、『...』) - - 旁白式独白 -2. 忽略说话者、叙述性文字、动作描写 -3. 保留台词的原始语气和标点 -4. 忽略非对话的叙述性文字 -5. 直接以 JSON 数组格式输出,不要任何额外说明 -示例输出格式: -["台词1", "台词2", "台词3"] - `, - }, - { - role: "user", - content: prompt, - }, - ], + const skill = await useSkill("eventExtract-agent"); + + const resText = await u.Ai.Text("universalAgent").invoke({ + system: skill.prompt, + messages: [{ role: "user", content: prompt }], output: Output.array({ element: z.object({ lines: z.string().describe("台词内容"), diff --git a/src/routes/script/getScrptApi.ts b/src/routes/script/getScrptApi.ts index 86dca99..01d451b 100644 --- a/src/routes/script/getScrptApi.ts +++ b/src/routes/script/getScrptApi.ts @@ -22,8 +22,9 @@ export default router.post( const assetsData = await u .db("o_assets") .leftJoin("o_scriptAssets", "o_assets.id", "o_scriptAssets.assetId") - .whereIn( + .where( "o_scriptAssets.scriptId", + "in", data.map((i) => i.id), ) .select("o_assets.id", "o_assets.name", "o_scriptAssets.scriptId"); diff --git a/src/socket/utils/aguiTools.ts b/src/socket/utils/aguiTools.ts deleted file mode 100644 index fc1e32a..0000000 --- a/src/socket/utils/aguiTools.ts +++ /dev/null @@ -1,318 +0,0 @@ -import express from "express"; -import u from "@/utils"; -import { - EventType, - RunStartedEvent, - RunFinishedEvent, - RunErrorEvent, - StepStartedEvent, - StepFinishedEvent, - TextMessageStartEvent, - TextMessageContentEvent, - TextMessageEndEvent, - ToolCallStartEvent, - ToolCallArgsEvent, - ToolCallEndEvent, - ToolCallResultEvent, - StateSnapshotEvent, - StateDeltaEvent, - MessagesSnapshotEvent, - ActivitySnapshotEvent, - ActivityDeltaEvent, - ReasoningStartEvent, - ReasoningMessageStartEvent, - ReasoningMessageContentEvent, - ReasoningMessageEndEvent, - ReasoningEndEvent, - ReasoningEncryptedValueEvent, - RawEvent, - CustomEvent, - Message, -} from "@ag-ui/core"; - -type Role = "developer" | "system" | "assistant" | "user"; - -/** - * AG-UI SSE 事件流构建器 - * 封装所有 AG-UI 协议事件的发送逻辑 - */ -export class AGUIStream { - private res: express.Response; - private runId: string; - private threadId: string; - - constructor(res: express.Response, threadId?: string) { - this.res = res; - this.runId = u.uuid(); - this.threadId = threadId ?? u.uuid(); - - // 设置 SSE 响应头 - res.setHeader("Content-Type", "text/event-stream"); - res.setHeader("Cache-Control", "no-cache"); - res.setHeader("Connection", "keep-alive"); - } - - // ==================== 基础发送 ==================== - - private send(data: Record) { - this.res.write(`data: ${JSON.stringify(data)}\n\n`); - } - - // ==================== Run 生命周期 ==================== - - runStarted() { - this.send({ - type: EventType.RUN_STARTED, - threadId: this.threadId, - runId: this.runId, - } satisfies RunStartedEvent); - return this; - } - - runFinished() { - this.send({ - type: EventType.RUN_FINISHED, - threadId: this.threadId, - runId: this.runId, - } satisfies RunFinishedEvent); - return this; - } - - runError(message: string, code?: string) { - this.send({ - type: EventType.RUN_ERROR, - message, - ...(code && { code }), - } satisfies RunErrorEvent); - return this; - } - - // ==================== 文本消息 ==================== - - textMessage(role: Role = "assistant") { - const messageId = u.uuid(); - - this.send({ - type: EventType.TEXT_MESSAGE_START, - messageId, - role, - } satisfies TextMessageStartEvent); - - const handle = { - send: (delta: string) => { - this.send({ - type: EventType.TEXT_MESSAGE_CONTENT, - messageId, - delta, - } satisfies TextMessageContentEvent); - return handle; - }, - end: () => { - this.send({ - type: EventType.TEXT_MESSAGE_END, - messageId, - } satisfies TextMessageEndEvent); - }, - }; - return handle; - } - - /** 一次性发送完整文本消息 */ - textMessageFull(content: string, role: Role = "assistant") { - const msg = this.textMessage(role); - msg.send(content); - msg.end(); - return this; - } - - // ==================== 工具调用 ==================== - - toolCall(toolCallName: string, parentMessageId?: string) { - const toolCallId = u.uuid(); - - this.send({ - type: EventType.TOOL_CALL_START, - toolCallId, - toolCallName, - ...(parentMessageId && { parentMessageId }), - } satisfies ToolCallStartEvent); - - return { - args: (delta: string) => { - this.send({ - type: EventType.TOOL_CALL_ARGS, - toolCallId, - delta, - } satisfies ToolCallArgsEvent); - }, - end: () => { - this.send({ - type: EventType.TOOL_CALL_END, - toolCallId, - } satisfies ToolCallEndEvent); - }, - /** 发送工具调用结果 */ - result: (content: string) => { - const messageId = u.uuid(); - this.send({ - type: EventType.TOOL_CALL_RESULT, - messageId, - toolCallId, - role: "tool", - content, - } satisfies ToolCallResultEvent); - }, - }; - } - - // ==================== 状态管理 ==================== - - stateSnapshot(snapshot: unknown) { - this.send({ - type: EventType.STATE_SNAPSHOT, - snapshot, - } satisfies StateSnapshotEvent); - return this; - } - - stateDelta(delta: unknown[]) { - this.send({ - type: EventType.STATE_DELTA, - delta, - } satisfies StateDeltaEvent); - return this; - } - - // ==================== 消息快照 ==================== - - messagesSnapshot(messages: Message[]) { - this.send({ - type: EventType.MESSAGES_SNAPSHOT, - messages, - } satisfies MessagesSnapshotEvent); - return this; - } - - // ==================== Activity 事件 ==================== - - activitySnapshot( - messageId: string, - activityType: string, - content: Record, - replace = true, - ) { - this.send({ - type: EventType.ACTIVITY_SNAPSHOT, - messageId, - activityType, - content, - replace, - } satisfies ActivitySnapshotEvent); - return this; - } - - activityDelta( - messageId: string, - activityType: string, - patch: unknown[], - ) { - this.send({ - type: EventType.ACTIVITY_DELTA, - messageId, - activityType, - patch, - } satisfies ActivityDeltaEvent); - return this; - } - - // ==================== Reasoning 事件 ==================== - - reasoning() { - const messageId = u.uuid(); - - this.send({ - type: EventType.REASONING_START, - messageId, - } satisfies ReasoningStartEvent); - - return { - messageStart: () => { - this.send({ - type: EventType.REASONING_MESSAGE_START, - messageId, - role: "reasoning", - } satisfies ReasoningMessageStartEvent); - }, - content: (delta: string) => { - this.send({ - type: EventType.REASONING_MESSAGE_CONTENT, - messageId, - delta, - } satisfies ReasoningMessageContentEvent); - }, - messageEnd: () => { - this.send({ - type: EventType.REASONING_MESSAGE_END, - messageId, - } satisfies ReasoningMessageEndEvent); - }, - end: () => { - this.send({ - type: EventType.REASONING_END, - messageId, - } satisfies ReasoningEndEvent); - }, - encryptedValue: ( - subtype: "tool-call" | "message", - entityId: string, - encryptedValue: string, - ) => { - this.send({ - type: EventType.REASONING_ENCRYPTED_VALUE, - subtype, - entityId, - encryptedValue, - } satisfies ReasoningEncryptedValueEvent); - }, - }; - } - - // ==================== Raw / Custom 事件 ==================== - - raw(event: unknown, source?: string) { - this.send({ - type: EventType.RAW, - event, - ...(source && { source }), - } satisfies RawEvent); - return this; - } - - custom(name: string, value: unknown) { - this.send({ - type: EventType.CUSTOM, - name, - value, - } satisfies CustomEvent); - return this; - } - - // ==================== 结束 ==================== - - end() { - this.res.end(); - } - - getRunId() { - return this.runId; - } - - getThreadId() { - return this.threadId; - } -} - -/** 创建 AG-UI 事件流 */ -export function createAGUIStream(res: express.Response): AGUIStream { - return new AGUIStream(res); -} \ No newline at end of file diff --git a/src/types/database.d.ts b/src/types/database.d.ts index 8b966b5..e3212c1 100644 --- a/src/types/database.d.ts +++ b/src/types/database.d.ts @@ -1,4 +1,4 @@ -// @db-hash b6146b9f91d8b9853e0f6fcb41c3145b +// @db-hash f6a9a8164252ce954394431079615459 //该文件由脚本自动生成,请勿手动修改 export interface memories { @@ -25,7 +25,7 @@ export interface o_agentDeploy { export interface o_agentWorkData { 'createTime'?: number | null; 'data'?: string | null; - 'espisodeId'?: number | null; + 'episodesId'?: number | null; 'id'?: number; 'key'?: string | null; 'projectId'?: number | null; @@ -65,15 +65,6 @@ export interface o_eventChapter { 'id'?: number; 'novelId'?: number | null; } -export interface o_flowData { - 'createTime'?: number | null; - 'data'?: string | null; - 'espisodeId'?: number | null; - 'id'?: number; - 'key'?: string | null; - 'projectId'?: number | null; - 'updateTime'?: number | null; -} export interface o_image { 'assetsId'?: number | null; 'filePath'?: string | null; @@ -88,6 +79,7 @@ export interface o_novel { 'chapterData'?: string | null; 'chapterIndex'?: number | null; 'createTime'?: number | null; + 'errorReason'?: string | null; 'event'?: string | null; 'eventState'?: number | null; 'id'?: number; @@ -212,7 +204,6 @@ export interface DB { "o_assets2Storyboard": o_assets2Storyboard; "o_event": o_event; "o_eventChapter": o_eventChapter; - "o_flowData": o_flowData; "o_image": o_image; "o_novel": o_novel; "o_outline": o_outline; diff --git a/src/utils/ai.ts b/src/utils/ai.ts index e458691..f900ab4 100644 --- a/src/utils/ai.ts +++ b/src/utils/ai.ts @@ -4,10 +4,10 @@ import axios from "axios"; import { transform } from "sucrase"; import u from "@/utils"; -type AiType = "scriptAgent" | "productionAgent" | "assetsAgent" | "polishingAgent" | "eventExtractAgent" | "ttsDubbing" | "test"; +type AiType = "scriptAgent" | "productionAgent" | "universalAgent"; type FnName = "textRequest" | "imageRequest" | "videoRequest" | "ttsRequest"; -const AiTypeValues: AiType[] = ["scriptAgent", "productionAgent", "assetsAgent", "polishingAgent", "eventExtractAgent", "ttsDubbing"]; +const AiTypeValues: AiType[] = ["scriptAgent", "productionAgent", "universalAgent"]; async function resolveModelName(value: AiType | `${number}:${string}`): Promise<`${number}:${string}`> { if (AiTypeValues.includes(value as AiType)) { const agentDeployData = await u.db("o_agentDeploy").where("key", value).first(); diff --git a/src/utils/cleanNovel.ts b/src/utils/cleanNovel.ts index 171ade2..5d9542c 100644 --- a/src/utils/cleanNovel.ts +++ b/src/utils/cleanNovel.ts @@ -1,9 +1,6 @@ -import * as z from "zod"; -import { ModelMessage, Output } from "ai"; import { EventEmitter } from "events"; - import { o_novel } from "@/types/database"; -import ai from "@/utils/ai"; +import { useSkill } from "@/utils/agent/skillsTools"; import u from "@/utils"; export interface EventType { id: number; @@ -25,34 +22,24 @@ class CleanNovel { async start(allChapters: o_novel[], projectId: number): Promise { //所有事件 let totalEvent: EventType[] = []; - const intansce = u.Ai.Text("eventExtractAgent"); + const intansce = u.Ai.Text("universalAgent"); try { for (let gi = 0; gi < allChapters.length; gi++) { const novel = allChapters[gi]; let resData; try { + const skill = await useSkill("universal-agent"); + resData = await intansce.invoke({ + system: skill.prompt, messages: [ - { - role: "system", - content: ` -你是一位专业的叙事结构分析师。 -请阅读以下小说章节,用**一段话**提炼本章的情节单元摘要。 -## 要求 -- 按事件发生顺序,串联本章核心情节节点 -- 突出人物行为、关键转折、因果关系 -- 语言简洁紧凑,100-150字以内 -- 不加主观评价,只陈述"发生了什么" ---- -【章节内容】: -`, - }, { role: "user", - content: novel.chapterData!, + content: "请根据以下小说章节生成事件摘要:\n" + novel.chapterData!, }, ], + tools: skill.tools, }); const preData = resData.text; From 3161d16c9d15694b861d81f945ceffcfecf42ca1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ACT=E4=B8=B6=E6=B5=81=E6=98=9F=E9=9B=A8?= <1340145680@qq.com> Date: Tue, 24 Mar 2026 11:38:10 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E9=94=99=E5=88=AB?= =?UTF-8?q?=E5=AD=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/initDB.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib/initDB.ts b/src/lib/initDB.ts index a13c15f..77fb042 100644 --- a/src/lib/initDB.ts +++ b/src/lib/initDB.ts @@ -91,7 +91,7 @@ export default async (knex: Knex, forceInit: boolean = false): Promise => vendorId: null, key: "universalAgent", name: "通用Agent", - desc: "用于小说时间提取、资产提示词生成、台词提取等边缘功能,建议使用具备较强文本处理能力的模型", + desc: "用于小说事件提取、资产提示词生成、台词提取等边缘功能,建议使用具备较强文本处理能力的模型", disabled: false, }, { From 1016dc945c8d1ecdfbd7e7d7b23e71385a211200 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?ACT=E4=B8=B6=E6=B5=81=E6=98=9F=E9=9B=A8?= <1340145680@qq.com> Date: Tue, 24 Mar 2026 11:39:05 +0800 Subject: [PATCH 3/3] =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=88=A0=E9=99=A4?= =?UTF-8?q?=E9=99=90=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../setting/vendorConfig/deleteVendor.ts | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/routes/setting/vendorConfig/deleteVendor.ts b/src/routes/setting/vendorConfig/deleteVendor.ts index db340d3..d74d870 100644 --- a/src/routes/setting/vendorConfig/deleteVendor.ts +++ b/src/routes/setting/vendorConfig/deleteVendor.ts @@ -5,13 +5,16 @@ import u from "@/utils"; import { z } from "zod"; const router = express.Router(); export default router.post( - "/", - validateFields({ - id: z.number(), - }), - async (req, res) => { - const { id } = req.body; - await u.db("o_vendorConfig").where("id", id).del(); - res.status(200).send(success("删除成功")); - }, + "/", + validateFields({ + id: z.number(), + }), + async (req, res) => { + const { id } = req.body; + if (id == 1) { + return res.status(400).send(error("此配置无法删除")); + } + await u.db("o_vendorConfig").where("id", id).del(); + res.status(200).send(success("删除成功")); + }, );