新增子 Agent 与技能系统管理:
All checks were successful
Deploy Noval to K3s / deploy (push) Successful in 54s

- **子 Agent 支持**:
  - 新增 `dispatch_subagent` 工具,支持分配特定任务给子 Agent,由其独立操作。
  - 前端新增 `ChatSubAgent` 组件,支持子 Agent 任务状态展示(运行中/已完成)、工具调用进度及输出预览等。

- **技能管理扩展**:
  - 新增 Skill 编辑器页面(`SkillEditorView.vue`),支持创建、编辑、删除技能。
  - 完善技能工具权限分组选择功能,按类型灵活分配工具。

- **工具增强**:
  - 新增工具接口:`get_outline_detail`、`get_character_detail`、`get_chapter_detail`,用于获取详情内容。
  - 优化部分工具返回数据,仅输出基础信息以减少上下文体积。

- 后端添加 `SPECIAL_TOOLS` 支持,增强 `dispatch_subagent` 的特殊处理能力。

同时优化部分前后端交互,提升代码维护性与可读性。
This commit is contained in:
2026-03-23 02:23:22 +08:00
parent b39377e487
commit 0525c8217e
12 changed files with 2028 additions and 209 deletions

View File

@@ -34,12 +34,23 @@ TOOL_DEFINITIONS: list[dict] = [
},
{
"name": "list_outlines",
"description": "列出当前小说的所有大纲节点,返回每个节点的 id、排序摘要和详情",
"description": "列出当前小说的所有大纲节点(仅返回 id、排序摘要标题,不含详情)。如需查看某节点的详情内容,请使用 get_outline_detail 工具。",
"parameters": {
"type": "object",
"properties": {},
},
},
{
"name": "get_outline_detail",
"description": "查看指定大纲节点的完整详情内容",
"parameters": {
"type": "object",
"properties": {
"outline_id": {"type": "integer", "description": "大纲节点 ID"},
},
"required": ["outline_id"],
},
},
{
"name": "create_outline",
"description": "为当前小说创建一个新的大纲节点",
@@ -118,12 +129,23 @@ TOOL_DEFINITIONS: list[dict] = [
# ── 角色工具 ──
{
"name": "list_characters",
"description": "列出当前小说的所有角色,返回每个角色的 id、名称和描述",
"description": "列出当前小说的所有角色(仅返回 id 和名称,不含描述详情)。如需查看某角色的完整描述,请使用 get_character_detail 工具。",
"parameters": {
"type": "object",
"properties": {},
},
},
{
"name": "get_character_detail",
"description": "查看指定角色的完整描述信息",
"parameters": {
"type": "object",
"properties": {
"character_id": {"type": "integer", "description": "角色 ID"},
},
"required": ["character_id"],
},
},
{
"name": "create_character",
"description": "为当前小说创建一个新角色",
@@ -163,12 +185,23 @@ TOOL_DEFINITIONS: list[dict] = [
# ── 章节工具 ──
{
"name": "list_chapters",
"description": "列出当前小说的所有章节,按排序顺序返回每个章节的 id、标题和内容摘要",
"description": "列出当前小说的所有章节(仅返回 id、排序和标题不含正文。如需查看某章节的完整正文请使用 get_chapter_detail 工具。",
"parameters": {
"type": "object",
"properties": {},
},
},
{
"name": "get_chapter_detail",
"description": "查看指定章节的完整正文内容",
"parameters": {
"type": "object",
"properties": {
"chapter_id": {"type": "integer", "description": "章节 ID"},
},
"required": ["chapter_id"],
},
},
{
"name": "create_chapter",
"description": "为当前小说创建一个新章节",
@@ -227,8 +260,23 @@ TOOL_DEFINITIONS: list[dict] = [
"required": ["file_id"],
},
},
{
"name": "dispatch_subagent",
"description": "派遣一个子 Agent 执行特定任务。子 Agent 拥有独立的系统提示词和工具集(由 Skill 定义),适合将复杂任务拆分为子任务分别处理。",
"parameters": {
"type": "object",
"properties": {
"skill_id": {"type": "integer", "description": "要使用的 Skill ID从可用 Skill 列表中选择)"},
"task": {"type": "string", "description": "交给子 Agent 的具体任务描述"},
},
"required": ["skill_id", "task"],
},
},
]
# dispatch_subagent 不由 execute_tool 处理,它在 chat.py 中特殊处理
SPECIAL_TOOLS = {"dispatch_subagent"}
def _filter_defs(allowed: list[str] | None) -> list[dict]:
"""根据白名单过滤工具定义"""
@@ -275,6 +323,7 @@ def execute_tool(name: str, arguments: dict, novel_id: int) -> str:
"get_novel_info": _get_novel_info,
"update_novel_info": _update_novel_info,
"list_outlines": _list_outlines,
"get_outline_detail": _get_outline_detail,
"create_outline": _create_outline,
"update_outline": _update_outline,
"delete_outline": _delete_outline,
@@ -282,10 +331,12 @@ def execute_tool(name: str, arguments: dict, novel_id: int) -> str:
"get_world_setting": _get_world_setting,
"save_world_setting": _save_world_setting,
"list_characters": _list_characters,
"get_character_detail": _get_character_detail,
"create_character": _create_character,
"update_character": _update_character,
"delete_character": _delete_character,
"list_chapters": _list_chapters,
"get_chapter_detail": _get_chapter_detail,
"create_chapter": _create_chapter,
"update_chapter": _update_chapter,
"delete_chapter": _delete_chapter,
@@ -339,13 +390,12 @@ def _update_novel_info(session: Session, novel_id: int, args: dict) -> str:
def _list_outlines(session: Session, novel_id: int, _args: dict) -> str:
# 查询顶级节点
"""列表仅返回 id + 排序 + 摘要标题(不含 detail节省上下文。"""
top_items = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id.is_(None)) # type: ignore
.order_by(Outline.sort_order)
).all()
# 查询所有子节点,按 parent_id 分组
all_children = session.exec(
select(Outline)
.where(Outline.novel_id == novel_id, Outline.parent_id.isnot(None)) # type: ignore
@@ -357,17 +407,28 @@ def _list_outlines(session: Session, novel_id: int, _args: dict) -> str:
result = []
for o in top_items:
node = {"id": o.id, "sort_order": o.sort_order, "summary": o.summary, "detail": o.detail}
node: dict = {"id": o.id, "sort_order": o.sort_order, "summary": o.summary}
kids = children_map.get(o.id, [])
if kids:
node["children"] = [
{"id": c.id, "sort_order": c.sort_order, "summary": c.summary, "detail": c.detail, "parent_id": c.parent_id}
{"id": c.id, "sort_order": c.sort_order, "summary": c.summary, "parent_id": c.parent_id}
for c in kids
]
result.append(node)
return json.dumps({"outlines": result, "total": len(result)}, ensure_ascii=False)
def _get_outline_detail(session: Session, novel_id: int, args: dict) -> str:
outline = session.get(Outline, args["outline_id"])
if not outline or outline.novel_id != novel_id:
return json.dumps({"error": "大纲不存在"}, ensure_ascii=False)
return json.dumps({
"id": outline.id, "sort_order": outline.sort_order,
"summary": outline.summary, "detail": outline.detail,
"parent_id": outline.parent_id,
}, ensure_ascii=False)
def _create_outline(session: Session, novel_id: int, args: dict) -> str:
parent_id = args.get("parent_id")
@@ -498,18 +559,25 @@ def _save_world_setting(session: Session, novel_id: int, args: dict) -> str:
def _list_characters(session: Session, novel_id: int, _args: dict) -> str:
"""列表仅返回 id + 名称(不含 description节省上下文。"""
items = session.exec(
select(Character)
.where(Character.novel_id == novel_id)
.order_by(Character.created_at)
).all()
result = [
{"id": c.id, "name": c.name, "description": c.description}
for c in items
]
result = [{"id": c.id, "name": c.name} for c in items]
return json.dumps({"characters": result, "total": len(result)}, ensure_ascii=False)
def _get_character_detail(session: Session, novel_id: int, args: dict) -> str:
char = session.get(Character, args["character_id"])
if not char or char.novel_id != novel_id:
return json.dumps({"error": "角色不存在"}, ensure_ascii=False)
return json.dumps({
"id": char.id, "name": char.name, "description": char.description,
}, ensure_ascii=False)
def _create_character(session: Session, novel_id: int, args: dict) -> str:
name = args["name"].strip()
if not name or len(name) > 100:
@@ -564,23 +632,26 @@ def _delete_character(session: Session, novel_id: int, args: dict) -> str:
def _list_chapters(session: Session, novel_id: int, _args: dict) -> str:
"""列表仅返回 id + 排序 + 标题(不含正文),节省上下文。"""
items = session.exec(
select(Chapter)
.where(Chapter.novel_id == novel_id)
.order_by(Chapter.sort_order)
).all()
result = [
{
"id": c.id,
"sort_order": c.sort_order,
"title": c.title,
"content_preview": c.content[:100] + "" if len(c.content) > 100 else c.content,
}
for c in items
]
result = [{"id": c.id, "sort_order": c.sort_order, "title": c.title} for c in items]
return json.dumps({"chapters": result, "total": len(result)}, ensure_ascii=False)
def _get_chapter_detail(session: Session, novel_id: int, args: dict) -> str:
chapter = session.get(Chapter, args["chapter_id"])
if not chapter or chapter.novel_id != novel_id:
return json.dumps({"error": "章节不存在"}, ensure_ascii=False)
return json.dumps({
"id": chapter.id, "sort_order": chapter.sort_order,
"title": chapter.title, "content": chapter.content,
}, ensure_ascii=False)
def _create_chapter(session: Session, novel_id: int, args: dict) -> str:
title = args["title"].strip()
if not title or len(title) > 200: