- **文件管理**: - 支持文件上传、解析和删除。 - 限制支持格式(txt, md, docx, pdf),最大 10MB。 - 提供前端上传区、文件列表及后端解析服务。 - **技能管理**: - 新增技能的创建、更新及删除功能。 - 支持分配自定义提示词和工具权限。 - 提供技能列表及分组工具选择交互界面。 同时调整相关 API 和前端组件,优化协作体验。
This commit is contained in:
@@ -6,7 +6,8 @@ from datetime import datetime, timezone
|
||||
from sqlmodel import Session, select, func
|
||||
|
||||
from ..database import engine
|
||||
from ..models import Novel, Outline, WorldSetting, Character, Chapter
|
||||
from ..models import Novel, Outline, WorldSetting, Character, Chapter, NovelFile
|
||||
from ..database import DATA_DIR
|
||||
|
||||
|
||||
# ── 工具定义(provider 无关格式)──
|
||||
@@ -204,25 +205,66 @@ TOOL_DEFINITIONS: list[dict] = [
|
||||
"required": ["chapter_id"],
|
||||
},
|
||||
},
|
||||
# ── 文件工具 ──
|
||||
{
|
||||
"name": "list_files",
|
||||
"description": "列出当前小说关联的所有上传文件,返回文件名、大小和纯文本字符数",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "read_file",
|
||||
"description": "读取指定上传文件的纯文本内容。支持分段读取(offset/limit 按字符数),适合大文件。返回文本片段和总字符数。",
|
||||
"parameters": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"file_id": {"type": "integer", "description": "文件 ID(从 list_files 获取)"},
|
||||
"offset": {"type": "integer", "description": "起始字符位置(默认 0)"},
|
||||
"limit": {"type": "integer", "description": "读取字符数(默认 5000,最大 10000)"},
|
||||
},
|
||||
"required": ["file_id"],
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
def get_openai_tools() -> list[dict]:
|
||||
def _filter_defs(allowed: list[str] | None) -> list[dict]:
|
||||
"""根据白名单过滤工具定义"""
|
||||
if not allowed:
|
||||
return TOOL_DEFINITIONS
|
||||
return [t for t in TOOL_DEFINITIONS if t["name"] in allowed]
|
||||
|
||||
|
||||
def get_openai_tools(allowed: list[str] | None = None) -> list[dict]:
|
||||
"""转换为 OpenAI function calling 格式"""
|
||||
return [
|
||||
{"type": "function", "function": t}
|
||||
for t in TOOL_DEFINITIONS
|
||||
for t in _filter_defs(allowed)
|
||||
]
|
||||
|
||||
|
||||
def get_anthropic_tools() -> list[dict]:
|
||||
def get_anthropic_tools(allowed: list[str] | None = None) -> list[dict]:
|
||||
"""转换为 Anthropic tool_use 格式"""
|
||||
return [
|
||||
{"name": t["name"], "description": t["description"], "input_schema": t["parameters"]}
|
||||
for t in TOOL_DEFINITIONS
|
||||
for t in _filter_defs(allowed)
|
||||
]
|
||||
|
||||
|
||||
def get_tools_description(allowed: list[str] | None = None) -> str:
|
||||
"""动态生成工具能力描述文本(用于系统提示词)"""
|
||||
defs = _filter_defs(allowed)
|
||||
if not defs:
|
||||
return ""
|
||||
lines = ["你拥有以下工具能力,可以直接操作小说数据:"]
|
||||
for t in defs:
|
||||
lines.append(f"- {t['name']}: {t['description']}")
|
||||
lines.append("\n请根据用户需求主动使用工具,不要仅给出建议而不执行。")
|
||||
return "\n".join(lines)
|
||||
|
||||
|
||||
# ── 工具执行 ──
|
||||
|
||||
|
||||
@@ -247,6 +289,8 @@ def execute_tool(name: str, arguments: dict, novel_id: int) -> str:
|
||||
"create_chapter": _create_chapter,
|
||||
"update_chapter": _update_chapter,
|
||||
"delete_chapter": _delete_chapter,
|
||||
"list_files": _list_files,
|
||||
"read_file": _read_file,
|
||||
}
|
||||
handler = handlers.get(name)
|
||||
if not handler:
|
||||
@@ -593,3 +637,53 @@ def _delete_chapter(session: Session, novel_id: int, args: dict) -> str:
|
||||
session.delete(chapter)
|
||||
session.commit()
|
||||
return json.dumps({"deleted": True, "id": args["chapter_id"]}, ensure_ascii=False)
|
||||
|
||||
|
||||
# ── 文件处理函数 ──
|
||||
|
||||
|
||||
def _list_files(session: Session, novel_id: int, _args: dict) -> str:
|
||||
items = session.exec(
|
||||
select(NovelFile)
|
||||
.where(NovelFile.novel_id == novel_id)
|
||||
.order_by(NovelFile.created_at.desc())
|
||||
).all()
|
||||
result = [
|
||||
{
|
||||
"id": f.id,
|
||||
"filename": f.filename,
|
||||
"file_size": f.file_size,
|
||||
"text_length": f.text_length,
|
||||
"created_at": f.created_at.isoformat() if f.created_at else None,
|
||||
}
|
||||
for f in items
|
||||
]
|
||||
return json.dumps({"files": result, "total": len(result)}, ensure_ascii=False)
|
||||
|
||||
|
||||
def _read_file(session: Session, novel_id: int, args: dict) -> str:
|
||||
file_id = args["file_id"]
|
||||
offset = max(0, args.get("offset", 0))
|
||||
limit = min(10000, max(1, args.get("limit", 5000)))
|
||||
|
||||
novel_file = session.get(NovelFile, file_id)
|
||||
if not novel_file or novel_file.novel_id != novel_id:
|
||||
return json.dumps({"error": "文件不存在"}, ensure_ascii=False)
|
||||
|
||||
# 读取预解析的纯文本缓存
|
||||
text_path = DATA_DIR / "uploads" / str(novel_id) / f"{novel_file.stored_name}.txt"
|
||||
if not text_path.exists():
|
||||
return json.dumps({"error": "文件纯文本缓存不存在,可能需要重新上传"}, ensure_ascii=False)
|
||||
|
||||
full_text = text_path.read_text(encoding="utf-8")
|
||||
total_length = len(full_text)
|
||||
segment = full_text[offset:offset + limit]
|
||||
|
||||
return json.dumps({
|
||||
"filename": novel_file.filename,
|
||||
"text": segment,
|
||||
"offset": offset,
|
||||
"limit": limit,
|
||||
"total_length": total_length,
|
||||
"has_more": offset + limit < total_length,
|
||||
}, ensure_ascii=False)
|
||||
|
||||
Reference in New Issue
Block a user