feat: Provider model_settings 全链路 + 监管节点工具集 + 重型插件注入 + 前端打磨
- Provider model_settings (Provider+Model 级别参数配置): DB JSONB → API → GSM → AgentFactory.resolve → 三节点 agent.run 注入 - 新增 data/toolset/regulatory_toolset/: 监管节点专属工具(query_workflow_status / query_task_list / send_file) - send_file 从 interactive_toolset 迁移至 regulatory_toolset,interactive 仅保留 approval - mcp_helper 合入 GlobalPluginManager dispatch tools - 前端 Provider 弹窗参数设置区加 JSON 编辑器(model_settings) - 前端 Plugin 页面新增"重型插件"Tab(HeavyPluginList 占位) - .gitignore 精简:去除系统默认项,修复 data/ 子目录追踪 - data/toolset/ 与 data/plugin/ 首次纳入版本控制 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,30 @@
|
||||
# base_toolset
|
||||
|
||||
KiloStar 内置基础工具集。提供文件操作、命令执行、搜索等通用能力,所有 Agent 默认可用。
|
||||
|
||||
## 工具列表
|
||||
|
||||
| 工具 | 说明 |
|
||||
|------|------|
|
||||
| `shell_executor` | 执行 shell 命令,返回 stdout/stderr |
|
||||
| `file_reader` | 读取文件内容(支持按行偏移和行数限制) |
|
||||
| `edit_file` | 按 old_string → new_string 的方式精确替换文件内容 |
|
||||
| `write_file` | 整体写入或覆盖文件 |
|
||||
| `search_file` | 在目录树内按 glob/正则搜索文件名或内容 |
|
||||
| `python_executor` | 在沙箱中运行 Python 代码片段 |
|
||||
| `tavily_search` | 调用 Tavily API 进行联网搜索(需配置 `api_key`) |
|
||||
|
||||
## 配置说明
|
||||
|
||||
`tavily_search` 需要在工具配置中填入 `api_key`,可选参数:
|
||||
|
||||
- `max_results`:返回结果条数,默认 `5`
|
||||
- `search_depth`:`basic` 或 `advanced`
|
||||
- `include_answer`:是否带 LLM 摘要,默认 `true`
|
||||
|
||||
其他工具开箱即用,无需配置。
|
||||
|
||||
## 安全提示
|
||||
|
||||
- `shell_executor` / `python_executor` 会在受限沙箱内执行,但仍建议在受信环境下使用
|
||||
- `edit_file` / `write_file` 会修改本地文件系统,注意权限范围
|
||||
@@ -0,0 +1,17 @@
|
||||
from .shell_executor import shell_executor
|
||||
from .file_reader import file_reader
|
||||
from .edit_file import edit_file
|
||||
from .write_file import write_file
|
||||
from .search_file import search_file
|
||||
from .python_executor import python_executor
|
||||
from .tavily_search import tavily_search
|
||||
|
||||
__all__ = [
|
||||
"shell_executor",
|
||||
"file_reader",
|
||||
"edit_file",
|
||||
"write_file",
|
||||
"search_file",
|
||||
"python_executor",
|
||||
"tavily_search",
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
import os
|
||||
|
||||
|
||||
async def edit_file(
|
||||
file_path: str,
|
||||
old_content: str,
|
||||
new_content: str,
|
||||
) -> str:
|
||||
"""通过查找替换的方式编辑文件内容。
|
||||
|
||||
Args:
|
||||
file_path: 文件的路径
|
||||
old_content: 要被替换的原始内容片段
|
||||
new_content: 替换后的新内容
|
||||
|
||||
Returns:
|
||||
操作结果描述
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
file_path = validate_path(file_path, write=True)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
try:
|
||||
if not os.path.exists(file_path):
|
||||
return f"[Error] 文件不存在: {file_path}"
|
||||
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
content = f.read()
|
||||
|
||||
if old_content not in content:
|
||||
return f"[Error] 未在文件中找到要替换的内容片段"
|
||||
|
||||
new_file_content = content.replace(old_content, new_content, 1)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(new_file_content)
|
||||
|
||||
return f"已成功编辑文件: {file_path}"
|
||||
except Exception as e:
|
||||
return f"[Error] 编辑文件失败: {e}"
|
||||
@@ -0,0 +1,23 @@
|
||||
async def file_reader(file_path: str) -> str:
|
||||
"""读取本地文件的内容。
|
||||
|
||||
Args:
|
||||
file_path: 文件的绝对路径或相对路径
|
||||
|
||||
Returns:
|
||||
文件内容文本,若文件不存在则返回错误信息
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
file_path = validate_path(file_path, write=False)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
try:
|
||||
with open(file_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
except FileNotFoundError:
|
||||
return f"[Error] File not found: {file_path}"
|
||||
except Exception as e:
|
||||
return f"[Error] Failed to read file: {str(e)}"
|
||||
@@ -0,0 +1,68 @@
|
||||
{
|
||||
"name": "基础工具集",
|
||||
"version": "0.1.0",
|
||||
"description": "文件读写、命令执行、Python/搜索等通用能力",
|
||||
"tools": [
|
||||
{
|
||||
"name": "shell_executor",
|
||||
"file": "shell_executor.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "file_reader",
|
||||
"file": "file_reader.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "edit_file",
|
||||
"file": "edit_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "write_file",
|
||||
"file": "write_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "search_file",
|
||||
"file": "search_file.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "python_executor",
|
||||
"file": "python_executor.py",
|
||||
"is_system": true,
|
||||
"action_scope": [],
|
||||
"config_args": {},
|
||||
"category": "system"
|
||||
},
|
||||
{
|
||||
"name": "tavily_search",
|
||||
"file": "tavily_search.py",
|
||||
"is_system": false,
|
||||
"action_scope": ["control_node", "consciousness_node", "regulatory_node"],
|
||||
"config_args": {
|
||||
"api_key": "",
|
||||
"max_results": "5",
|
||||
"search_depth": "basic",
|
||||
"include_answer": "true"
|
||||
},
|
||||
"category": "search"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import asyncio
|
||||
import sys
|
||||
import tempfile
|
||||
import os
|
||||
|
||||
|
||||
async def python_executor(code: str, timeout: int = 30) -> str:
|
||||
"""执行 Python 代码片段并返回输出。
|
||||
|
||||
Args:
|
||||
code: 要执行的 Python 代码
|
||||
timeout: 超时秒数,默认 30 秒
|
||||
|
||||
Returns:
|
||||
代码的标准输出 + 标准错误
|
||||
"""
|
||||
from kilostar.utils.sandbox import (
|
||||
validate_python_code, CodeViolation, get_python_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
code = validate_python_code(code)
|
||||
except CodeViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
timeout = get_python_timeout(timeout)
|
||||
|
||||
tmp_file = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(
|
||||
mode="w", suffix=".py", delete=False, encoding="utf-8"
|
||||
) as f:
|
||||
f.write(code)
|
||||
tmp_file = f.name
|
||||
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
sys.executable, tmp_file,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
output = stdout.decode("utf-8", errors="replace")
|
||||
err_output = stderr.decode("utf-8", errors="replace")
|
||||
result = ""
|
||||
if output:
|
||||
result += output
|
||||
if err_output:
|
||||
result += f"\n[stderr]\n{err_output}"
|
||||
if proc.returncode != 0:
|
||||
result += f"\n[exit code: {proc.returncode}]"
|
||||
return result.strip() or "(no output)"
|
||||
except asyncio.TimeoutError:
|
||||
return f"[Error] Python 代码执行超时({timeout}s)"
|
||||
except Exception as e:
|
||||
return f"[Error] 执行失败: {e}"
|
||||
finally:
|
||||
if tmp_file and os.path.exists(tmp_file):
|
||||
os.unlink(tmp_file)
|
||||
@@ -0,0 +1,55 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
async def search_file(
|
||||
keyword: str,
|
||||
directory: str = ".",
|
||||
file_pattern: str = "*",
|
||||
max_results: int = 20,
|
||||
) -> str:
|
||||
"""在指定目录下递归搜索包含关键字的文件内容。
|
||||
|
||||
Args:
|
||||
keyword: 要搜索的关键字或正则表达式
|
||||
directory: 搜索的根目录,默认当前目录
|
||||
file_pattern: 文件名匹配模式,如 "*.py"
|
||||
max_results: 最大返回结果数
|
||||
|
||||
Returns:
|
||||
匹配的文件名和行内容
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
directory = validate_path(directory, write=False)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
max_results = min(max_results, 100)
|
||||
|
||||
try:
|
||||
grep_args = [
|
||||
"grep", "-rn",
|
||||
f"--include={file_pattern}",
|
||||
"-m", str(max_results),
|
||||
"--", keyword, directory,
|
||||
]
|
||||
proc = await asyncio.create_subprocess_exec(
|
||||
*grep_args,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, _ = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=30
|
||||
)
|
||||
output = stdout.decode("utf-8", errors="replace").strip()
|
||||
if not output:
|
||||
return f"未找到包含 '{keyword}' 的匹配项"
|
||||
lines = output.split("\n")
|
||||
if len(lines) > max_results:
|
||||
output = "\n".join(lines[:max_results])
|
||||
return output
|
||||
except asyncio.TimeoutError:
|
||||
return "[Error] 搜索超时"
|
||||
except Exception as e:
|
||||
return f"[Error] 搜索失败: {e}"
|
||||
@@ -0,0 +1,46 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
async def shell_executor(command: str, timeout: int = 30) -> str:
|
||||
"""在服务器上执行 shell 命令并返回输出。
|
||||
|
||||
Args:
|
||||
command: 要执行的 shell 命令
|
||||
timeout: 超时秒数,默认 30 秒
|
||||
|
||||
Returns:
|
||||
命令的 stdout + stderr 输出
|
||||
"""
|
||||
from kilostar.utils.sandbox import (
|
||||
validate_shell_command, CommandViolation, get_shell_timeout,
|
||||
)
|
||||
|
||||
try:
|
||||
command = validate_shell_command(command)
|
||||
except CommandViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
timeout = get_shell_timeout(timeout)
|
||||
|
||||
try:
|
||||
proc = await asyncio.create_subprocess_shell(
|
||||
command,
|
||||
stdout=asyncio.subprocess.PIPE,
|
||||
stderr=asyncio.subprocess.PIPE,
|
||||
)
|
||||
stdout, stderr = await asyncio.wait_for(
|
||||
proc.communicate(), timeout=timeout
|
||||
)
|
||||
output = stdout.decode("utf-8", errors="replace")
|
||||
err_output = stderr.decode("utf-8", errors="replace")
|
||||
result = ""
|
||||
if output:
|
||||
result += output
|
||||
if err_output:
|
||||
result += f"\n[stderr]\n{err_output}"
|
||||
if proc.returncode != 0:
|
||||
result += f"\n[exit code: {proc.returncode}]"
|
||||
return result.strip() or "(no output)"
|
||||
except asyncio.TimeoutError:
|
||||
return f"[Error] 命令执行超时({timeout}s)"
|
||||
except Exception as e:
|
||||
return f"[Error] 执行失败: {e}"
|
||||
@@ -0,0 +1,78 @@
|
||||
import os
|
||||
from typing import Optional
|
||||
|
||||
from tavily import AsyncTavilyClient
|
||||
|
||||
|
||||
async def _resolve_api_key(explicit: Optional[str]) -> Optional[str]:
|
||||
"""按优先级解析 Tavily API key:显式参数 > GSM 配置 > 环境变量。"""
|
||||
if explicit:
|
||||
return explicit
|
||||
try:
|
||||
from kilostar.core.global_state_machine.gsm_snapshot import fetch_snapshot
|
||||
|
||||
snapshot = await fetch_snapshot()
|
||||
cfg = snapshot.tool_configs.get("tavily_search") or {}
|
||||
if isinstance(cfg, dict) and cfg.get("api_key"):
|
||||
return cfg["api_key"]
|
||||
except Exception:
|
||||
pass
|
||||
return os.environ.get("TAVILY_API_KEY")
|
||||
|
||||
|
||||
async def tavily_search(
|
||||
query: str,
|
||||
max_results: int = 5,
|
||||
search_depth: str = "basic",
|
||||
include_answer: bool = True,
|
||||
api_key: Optional[str] = None,
|
||||
) -> str:
|
||||
"""使用 Tavily 进行网络搜索,获取高质量的网络搜索结果。
|
||||
|
||||
Args:
|
||||
query: 搜索查询内容
|
||||
max_results: 返回的最大结果数量(1-10)
|
||||
search_depth: 搜索深度,"basic" 或 "advanced"
|
||||
include_answer: 是否包含 AI 生成的答案摘要
|
||||
api_key: 可选;不传则按 GSM 配置 → 环境变量顺序解析
|
||||
|
||||
Returns:
|
||||
格式化的搜索结果文本,包含标题、URL、摘要和可选的 AI 答案
|
||||
"""
|
||||
resolved_key = await _resolve_api_key(api_key)
|
||||
if not resolved_key:
|
||||
return (
|
||||
"[Error] Tavily API key 未配置。"
|
||||
"请在 ``/api/v1/resource/tool/config`` 写入或设置环境变量 ``TAVILY_API_KEY``。"
|
||||
)
|
||||
|
||||
try:
|
||||
client = AsyncTavilyClient(api_key=resolved_key)
|
||||
result = await client.search(
|
||||
query=query,
|
||||
max_results=min(max_results, 10),
|
||||
search_depth=search_depth,
|
||||
include_answer=include_answer,
|
||||
)
|
||||
|
||||
lines = []
|
||||
if include_answer and result.get("answer"):
|
||||
lines.append(f"【AI 摘要】{result['answer']}\n")
|
||||
|
||||
results = result.get("results", [])
|
||||
if not results:
|
||||
return "No results found for the query."
|
||||
|
||||
lines.append("【搜索结果】")
|
||||
for i, item in enumerate(results, 1):
|
||||
title = item.get("title", "Untitled")
|
||||
url = item.get("url", "")
|
||||
content = item.get("content", "").strip()
|
||||
lines.append(f"\n{i}. {title}")
|
||||
lines.append(f" URL: {url}")
|
||||
if content:
|
||||
lines.append(f" {content[:300]}{'...' if len(content) > 300 else ''}")
|
||||
|
||||
return "\n".join(lines)
|
||||
except Exception as e:
|
||||
return f"[Error] Tavily search failed: {str(e)}"
|
||||
@@ -0,0 +1,31 @@
|
||||
import os
|
||||
|
||||
|
||||
async def write_file(file_path: str, content: str) -> str:
|
||||
"""将内容写入指定文件(会覆盖已有内容,自动创建目录)。
|
||||
|
||||
Args:
|
||||
file_path: 文件的路径
|
||||
content: 要写入的内容
|
||||
|
||||
Returns:
|
||||
操作结果描述
|
||||
"""
|
||||
from kilostar.utils.sandbox import validate_path, PathViolation
|
||||
|
||||
try:
|
||||
file_path = validate_path(file_path, write=True)
|
||||
except PathViolation as e:
|
||||
return f"[Sandbox] {e}"
|
||||
|
||||
try:
|
||||
dir_path = os.path.dirname(file_path)
|
||||
if dir_path:
|
||||
os.makedirs(dir_path, exist_ok=True)
|
||||
|
||||
with open(file_path, "w", encoding="utf-8") as f:
|
||||
f.write(content)
|
||||
|
||||
return f"已成功写入文件: {file_path}({len(content)} 字符)"
|
||||
except Exception as e:
|
||||
return f"[Error] 写入文件失败: {e}"
|
||||
Reference in New Issue
Block a user