Merge branch 'main' into feat/skills-remote-install
This commit is contained in:
commit
43a23e2e88
@ -30,7 +30,7 @@
|
||||
Clone the repository and initialize the project:
|
||||
|
||||
```bash
|
||||
git clone --branch v0.6.1 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
git clone --branch v0.6.2 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
cd Yuxi
|
||||
|
||||
# Linux/macOS
|
||||
|
||||
@ -90,7 +90,7 @@
|
||||
克隆代码,并初始化
|
||||
|
||||
```
|
||||
git clone --branch v0.6.1 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
git clone --branch v0.6.2 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
cd Yuxi
|
||||
|
||||
# Linux/macOS
|
||||
|
||||
@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "yuxi"
|
||||
version = "0.6.2.beta"
|
||||
version = "0.6.2"
|
||||
description = "Yuxi 智能知识库与知识图谱平台核心后端包"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.14"
|
||||
|
||||
@ -293,8 +293,8 @@ class ProvisionerSandboxBackend(BaseSandbox):
|
||||
new_str=new_string,
|
||||
replace_mode=replace_mode,
|
||||
)
|
||||
if not result.data.success:
|
||||
return EditResult(error=result.data.message or f"Error editing file '{file_path}'")
|
||||
if not result.success:
|
||||
return EditResult(error=result.message or f"Error editing file '{file_path}'")
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return EditResult(error=f"Error editing file: {exc}")
|
||||
|
||||
|
||||
@ -89,6 +89,7 @@ def tool(
|
||||
)
|
||||
|
||||
# 自动收集工具实例
|
||||
tool_obj.handle_tool_error = True
|
||||
_all_tool_instances.append(tool_obj)
|
||||
|
||||
return tool_obj
|
||||
|
||||
@ -62,6 +62,9 @@ class KnowledgeBaseManager:
|
||||
|
||||
# 为每种使用中的知识库类型创建实例并加载元数据
|
||||
for kb_type in kb_types_in_use:
|
||||
if not KnowledgeBaseFactory.is_type_supported(kb_type):
|
||||
logger.warning(f"[InitializeKB] Skip initialization for unsupported knowledge base type: {kb_type}")
|
||||
continue
|
||||
try:
|
||||
kb_instance = self._get_or_create_kb_instance(kb_type)
|
||||
# 让 KB 实例自行加载元数据
|
||||
@ -180,6 +183,9 @@ class KnowledgeBaseManager:
|
||||
metadata_reloaded_types: set[str] = set()
|
||||
for row in rows:
|
||||
kb_type = row.kb_type or "lightrag"
|
||||
if not KnowledgeBaseFactory.is_type_supported(kb_type):
|
||||
logger.warning(f"Skip database {row.db_id} due to unsupported knowledge base type: {kb_type}")
|
||||
continue
|
||||
kb_instance = self._get_or_create_kb_instance(kb_type)
|
||||
db_info = kb_instance.get_database_info(row.db_id, include_files=False)
|
||||
if not db_info and kb_type not in metadata_reloaded_types:
|
||||
|
||||
@ -275,6 +275,8 @@ async def get_mcp_tools(
|
||||
if tool.metadata is None:
|
||||
tool.metadata = {}
|
||||
tool.metadata["id"] = unique_id
|
||||
# 开启错误处理,防止工具调用抛出 ToolException 时击穿服务
|
||||
tool.handle_tool_error = True
|
||||
all_processed_tools.append(tool)
|
||||
|
||||
if cache:
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "yuxi-workspace"
|
||||
version = "0.6.2.beta"
|
||||
version = "0.6.2"
|
||||
description = "基于大模型的智能知识库与知识图谱智能体开发平台,融合了 RAG 技术与知识图谱技术,基于 LangGraph v1 + Vue.js + FastAPI + LightRAG 架构构建"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.12,<3.14"
|
||||
|
||||
@ -17,7 +17,7 @@ from yuxi.services.mcp_service import (
|
||||
)
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils import logger
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||||
|
||||
mcp = APIRouter(prefix="/system/mcp-servers", tags=["mcp"])
|
||||
|
||||
@ -80,13 +80,29 @@ async def get_server_or_404(db: AsyncSession, name: str):
|
||||
|
||||
@mcp.get("")
|
||||
async def get_mcp_servers(
|
||||
current_user: User = Depends(get_admin_user),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取所有 MCP 服务器配置"""
|
||||
"""获取所有 MCP 服务器配置(普通用户仅获取脱敏的基础信息)"""
|
||||
try:
|
||||
servers = await get_all_mcp_servers(db)
|
||||
return {"success": True, "data": [s.to_dict() for s in servers]}
|
||||
if current_user.role in ["admin", "superadmin"]:
|
||||
return {"success": True, "data": [s.to_dict() for s in servers]}
|
||||
else:
|
||||
# NOTE: 针对普通用户采用高安全显式白名单字段准入投影,使用 getattr 兼容 Mock
|
||||
# 仿真对象和历史数据,避免未来新增敏感字段或审计信息越权泄露
|
||||
data = []
|
||||
for s in servers:
|
||||
data.append(
|
||||
{
|
||||
"name": getattr(s, "name", ""),
|
||||
"description": getattr(s, "description", None),
|
||||
"icon": getattr(s, "icon", None),
|
||||
"enabled": bool(getattr(s, "enabled", True)),
|
||||
"tags": getattr(s, "tags", None) or [],
|
||||
}
|
||||
)
|
||||
return {"success": True, "data": data}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to get MCP servers: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
@ -9,12 +9,11 @@ from fastapi.responses import FileResponse
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||||
from yuxi.services.remote_skill_install_service import (
|
||||
install_remote_skill,
|
||||
install_remote_skills_batch,
|
||||
list_remote_skills,
|
||||
search_remote_skills,
|
||||
)
|
||||
from yuxi.services.skill_service import (
|
||||
BuiltinSkillUpdateConflictError,
|
||||
@ -96,13 +95,29 @@ def _cleanup_export_file(path: str) -> None:
|
||||
|
||||
@skills.get("")
|
||||
async def list_skills_route(
|
||||
_current_user: User = Depends(get_admin_user),
|
||||
current_user: User = Depends(get_required_user),
|
||||
db: AsyncSession = Depends(get_db),
|
||||
):
|
||||
"""获取技能列表(管理员可读)。"""
|
||||
"""获取技能列表(普通用户仅获取白名单脱敏数据,管理员可读完整元数据)。"""
|
||||
try:
|
||||
items = await list_skills(db)
|
||||
return {"success": True, "data": [item.to_dict() for item in items]}
|
||||
|
||||
# NOTE: 针对管理员与常规登录用户分流返回,防止物理目录结构(dir_path)与系统审计信息越权暴露给常规用户
|
||||
if current_user.role in ["admin", "superadmin"]:
|
||||
return {"success": True, "data": [item.to_dict() for item in items]}
|
||||
|
||||
safe_data = []
|
||||
for item in items:
|
||||
safe_data.append(
|
||||
{
|
||||
"slug": item.slug,
|
||||
"name": item.name,
|
||||
"description": item.description,
|
||||
"version": item.version,
|
||||
"is_builtin": item.is_builtin,
|
||||
}
|
||||
)
|
||||
return {"success": True, "data": safe_data}
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to list skills: {e}")
|
||||
raise HTTPException(status_code=500, detail="获取技能列表失败")
|
||||
@ -263,9 +278,7 @@ async def install_remote_skill_route(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to install remote skill '{payload.skill}' from '{payload.source}': {e}"
|
||||
)
|
||||
logger.error(f"Failed to install remote skill '{payload.skill}' from '{payload.source}': {e}")
|
||||
raise HTTPException(status_code=500, detail="安装远程 skill 失败")
|
||||
|
||||
|
||||
@ -295,9 +308,7 @@ async def install_remote_skills_batch_route(
|
||||
except HTTPException:
|
||||
raise
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to install remote skills batch from '{payload.source}': {e}"
|
||||
)
|
||||
logger.error(f"Failed to install remote skills batch from '{payload.source}': {e}")
|
||||
raise HTTPException(status_code=500, detail="批量安装远程 skills 失败")
|
||||
|
||||
|
||||
|
||||
@ -4,11 +4,11 @@ from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from server.routers.mcp_router import mcp
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
|
||||
|
||||
def _build_app() -> FastAPI:
|
||||
def _build_app(*, allow_admin: bool = True) -> FastAPI:
|
||||
app = FastAPI()
|
||||
app.include_router(mcp, prefix="/api")
|
||||
|
||||
@ -16,6 +16,10 @@ def _build_app() -> FastAPI:
|
||||
return None
|
||||
|
||||
async def fake_admin_user():
|
||||
if not allow_admin:
|
||||
from fastapi import HTTPException
|
||||
|
||||
raise HTTPException(status_code=403, detail="需要管理员权限")
|
||||
return User(
|
||||
username="admin",
|
||||
user_id="admin",
|
||||
@ -23,8 +27,17 @@ def _build_app() -> FastAPI:
|
||||
role="admin",
|
||||
)
|
||||
|
||||
async def fake_required_user():
|
||||
return User(
|
||||
username="admin" if allow_admin else "user",
|
||||
user_id="admin" if allow_admin else "user",
|
||||
password_hash="x",
|
||||
role="admin" if allow_admin else "user",
|
||||
)
|
||||
|
||||
app.dependency_overrides[get_db] = fake_db
|
||||
app.dependency_overrides[get_admin_user] = fake_admin_user
|
||||
app.dependency_overrides[get_required_user] = fake_required_user
|
||||
return app
|
||||
|
||||
|
||||
@ -65,3 +78,58 @@ def test_update_mcp_server_status_not_found(monkeypatch):
|
||||
client = TestClient(_build_app())
|
||||
resp = client.put("/api/system/mcp-servers/missing/status", json={"enabled": True})
|
||||
assert resp.status_code == 404, resp.text
|
||||
|
||||
|
||||
def test_get_mcp_servers_normal_user_is_stripped(monkeypatch):
|
||||
class DummyServer:
|
||||
def __init__(self):
|
||||
self.name = "test-mcp"
|
||||
self.description = "test mcp description"
|
||||
self.transport = "stdio"
|
||||
self.url = "http://localhost:8000"
|
||||
self.command = "python"
|
||||
self.args = ["-m", "mcp"]
|
||||
self.env = {"API_KEY": "secret"}
|
||||
self.headers = {"Auth": "Bearer secret"}
|
||||
self.enabled = 1
|
||||
|
||||
def to_dict(self):
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"transport": self.transport,
|
||||
"url": self.url,
|
||||
"command": self.command,
|
||||
"args": self.args,
|
||||
"env": self.env,
|
||||
"headers": self.headers,
|
||||
"enabled": bool(self.enabled),
|
||||
}
|
||||
|
||||
async def fake_get_all_mcp_servers(db):
|
||||
return [DummyServer()]
|
||||
|
||||
monkeypatch.setattr("server.routers.mcp_router.get_all_mcp_servers", fake_get_all_mcp_servers)
|
||||
|
||||
# 1. 管理员请求,应该返回全部字段
|
||||
client_admin = TestClient(_build_app(allow_admin=True))
|
||||
resp_admin = client_admin.get("/api/system/mcp-servers")
|
||||
assert resp_admin.status_code == 200
|
||||
data_admin = resp_admin.json()["data"][0]
|
||||
assert data_admin["url"] == "http://localhost:8000"
|
||||
assert data_admin["command"] == "python"
|
||||
assert data_admin["env"] == {"API_KEY": "secret"}
|
||||
|
||||
# 2. 普通用户请求,敏感字段及一切非安全白名单字段应该被彻底脱敏
|
||||
client_user = TestClient(_build_app(allow_admin=False))
|
||||
resp_user = client_user.get("/api/system/mcp-servers")
|
||||
assert resp_user.status_code == 200
|
||||
data_user = resp_user.json()["data"][0]
|
||||
assert "url" not in data_user
|
||||
assert "command" not in data_user
|
||||
assert "env" not in data_user
|
||||
assert "headers" not in data_user
|
||||
assert "transport" not in data_user # NOTE: 进一步验证连 transport 等配置层元数据也一并过滤
|
||||
assert data_user["name"] == "test-mcp"
|
||||
assert data_user["description"] == "test mcp description"
|
||||
assert data_user["enabled"] is True
|
||||
|
||||
@ -4,7 +4,7 @@ from fastapi import FastAPI, HTTPException
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from server.routers.skill_router import skills
|
||||
from server.utils.auth_middleware import get_admin_user, get_db
|
||||
from server.utils.auth_middleware import get_admin_user, get_db, get_required_user
|
||||
from yuxi.storage.postgres.models_business import Skill, User
|
||||
|
||||
|
||||
@ -25,8 +25,17 @@ def _build_app(*, allow_admin: bool = True) -> FastAPI:
|
||||
role="admin",
|
||||
)
|
||||
|
||||
async def fake_required_user():
|
||||
return User(
|
||||
username="admin" if allow_admin else "user",
|
||||
user_id="admin" if allow_admin else "user",
|
||||
password_hash="x",
|
||||
role="admin" if allow_admin else "user",
|
||||
)
|
||||
|
||||
app.dependency_overrides[get_db] = fake_db
|
||||
app.dependency_overrides[get_admin_user] = fake_admin_user
|
||||
app.dependency_overrides[get_required_user] = fake_required_user
|
||||
return app
|
||||
|
||||
|
||||
@ -240,3 +249,33 @@ def test_install_remote_skill_route(monkeypatch):
|
||||
assert captured["source"] == "anthropics/skills"
|
||||
assert captured["skill"] == "frontend-design"
|
||||
assert captured["created_by"] == "admin"
|
||||
|
||||
|
||||
def test_list_skills_route_normal_user_success(monkeypatch):
|
||||
async def fake_list_skills(_db):
|
||||
return [
|
||||
Skill(
|
||||
slug="test-skill",
|
||||
name="test-skill-name",
|
||||
description="test skill description",
|
||||
dir_path="skills/test-skill",
|
||||
)
|
||||
]
|
||||
|
||||
monkeypatch.setattr("server.routers.skill_router.list_skills", fake_list_skills)
|
||||
|
||||
# 普通用户应该也能成功获取列表,但返回的字段应被安全白名单投影过滤
|
||||
app = _build_app(allow_admin=False)
|
||||
client = TestClient(app)
|
||||
resp = client.get("/api/system/skills")
|
||||
assert resp.status_code == 200, resp.text
|
||||
payload = resp.json()
|
||||
assert payload["success"] is True
|
||||
skill_data = payload["data"][0]
|
||||
assert skill_data["slug"] == "test-skill"
|
||||
assert skill_data["name"] == "test-skill-name"
|
||||
# NOTE: 验证敏感字段如 dir_path、created_by 以及其它元数据已全部被白名单机制过滤,不发生越权泄露
|
||||
assert "dir_path" not in skill_data
|
||||
assert "created_by" not in skill_data
|
||||
assert "updated_by" not in skill_data
|
||||
assert "content_hash" not in skill_data
|
||||
|
||||
@ -112,3 +112,27 @@ async def test_get_tools_from_all_servers_loads_names_from_db_once(monkeypatch):
|
||||
("alpha", server_configs),
|
||||
("beta", server_configs),
|
||||
]
|
||||
|
||||
|
||||
async def test_get_mcp_tools_sets_handle_tool_error(monkeypatch):
|
||||
mcp_service.clear_mcp_cache()
|
||||
|
||||
config = {"transport": "stdio", "command": "demo-tool", "disabled_tools": []}
|
||||
|
||||
async def fake_get_enabled_mcp_server_config(server_name: str, db=None):
|
||||
del db
|
||||
return config
|
||||
|
||||
async def fake_get_mcp_client(server_configs):
|
||||
tool = SimpleNamespace(name="demo_tool", metadata={})
|
||||
return _FakeClient([tool])
|
||||
|
||||
monkeypatch.setattr(mcp_service, "get_enabled_mcp_server_config", fake_get_enabled_mcp_server_config)
|
||||
monkeypatch.setattr(mcp_service, "get_mcp_client", fake_get_mcp_client)
|
||||
|
||||
tools = await mcp_service.get_mcp_tools("demo")
|
||||
assert len(tools) == 1
|
||||
assert tools[0].handle_tool_error is True
|
||||
|
||||
mcp_service.clear_mcp_cache()
|
||||
|
||||
|
||||
18
backend/test/unit/toolkits/test_tool_registry.py
Normal file
18
backend/test/unit/toolkits/test_tool_registry.py
Normal file
@ -0,0 +1,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from yuxi.agents.toolkits.registry import tool
|
||||
|
||||
|
||||
def test_tool_decorator_sets_handle_tool_error():
|
||||
"""测试通过 @tool 装饰器注册的工具是否自动设置了 handle_tool_error 为 True"""
|
||||
|
||||
@tool(
|
||||
category="test",
|
||||
display_name="测试工具",
|
||||
description="这是一个单元测试工具",
|
||||
)
|
||||
def my_test_tool(arg: str) -> str:
|
||||
return f"hello {arg}"
|
||||
|
||||
assert my_test_tool.name == "my_test_tool"
|
||||
assert my_test_tool.handle_tool_error is True
|
||||
@ -5639,7 +5639,7 @@ wheels = [
|
||||
|
||||
[[package]]
|
||||
name = "yuxi"
|
||||
version = "0.6.2b0"
|
||||
version = "0.6.2"
|
||||
source = { editable = "package" }
|
||||
dependencies = [
|
||||
{ name = "agent-sandbox" },
|
||||
@ -5798,7 +5798,7 @@ requires-dist = [
|
||||
|
||||
[[package]]
|
||||
name = "yuxi-workspace"
|
||||
version = "0.6.2b0"
|
||||
version = "0.6.2"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "arq" },
|
||||
|
||||
@ -30,7 +30,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/api.Dockerfile
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2}
|
||||
container_name: api-prod
|
||||
working_dir: /app
|
||||
networks:
|
||||
@ -45,7 +45,7 @@ services:
|
||||
environment:
|
||||
<<: *api-worker-env
|
||||
# endregion api_envs
|
||||
command: uv run --no-dev uvicorn server.main:app --host 0.0.0.0 --port 5050
|
||||
command: uv run --no-sync --no-dev uvicorn server.main:app --host 0.0.0.0 --port 5050
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:5050/api/system/health || exit 1"]
|
||||
@ -67,7 +67,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/api.Dockerfile
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2}
|
||||
container_name: worker-prod
|
||||
working_dir: /app
|
||||
networks:
|
||||
@ -80,7 +80,7 @@ services:
|
||||
- .env.prod
|
||||
environment:
|
||||
<<: *api-worker-env
|
||||
command: uv run --no-dev arq server.worker_main.WorkerSettings
|
||||
command: uv run --no-sync --no-dev arq server.worker_main.WorkerSettings
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
@ -143,7 +143,7 @@ services:
|
||||
context: .
|
||||
dockerfile: docker/web.Dockerfile
|
||||
target: production
|
||||
image: yuxi-web:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-web:${YUXI_VERSION:-0.6.2}
|
||||
container_name: web-prod
|
||||
ports:
|
||||
- "80:80"
|
||||
|
||||
@ -38,7 +38,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/api.Dockerfile
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2}
|
||||
container_name: api-dev
|
||||
working_dir: /app
|
||||
volumes:
|
||||
@ -64,7 +64,7 @@ services:
|
||||
environment:
|
||||
<<: *api-worker-env
|
||||
# endregion api_envs
|
||||
command: uv run --no-dev uvicorn server.main:app --host 0.0.0.0 --port 5050 --reload --reload-dir /app/server --reload-dir /app/package
|
||||
command: uv run --no-sync --no-dev uvicorn server.main:app --host 0.0.0.0 --port 5050 --reload --reload-dir /app/server --reload-dir /app/package
|
||||
restart: unless-stopped
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "curl -f http://localhost:5050/api/system/health || exit 1"]
|
||||
@ -86,7 +86,7 @@ services:
|
||||
build:
|
||||
context: .
|
||||
dockerfile: docker/api.Dockerfile
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-api:${YUXI_VERSION:-0.6.2}
|
||||
container_name: worker-dev
|
||||
working_dir: /app
|
||||
volumes:
|
||||
@ -109,7 +109,7 @@ services:
|
||||
- .env
|
||||
environment:
|
||||
<<: *api-worker-env
|
||||
command: uv run --no-dev arq server.worker_main.WorkerSettings
|
||||
command: watchfiles --filter python "arq server.worker_main.WorkerSettings" /app/server /app/package
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
postgres:
|
||||
@ -125,7 +125,7 @@ services:
|
||||
build:
|
||||
context: ./docker/sandbox_provisioner
|
||||
dockerfile: Dockerfile
|
||||
image: yuxi-sandbox-provisioner:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-sandbox-provisioner:${YUXI_VERSION:-0.6.2}
|
||||
container_name: sandbox-provisioner
|
||||
volumes:
|
||||
- ./saves:/app/saves
|
||||
@ -179,7 +179,7 @@ services:
|
||||
context: .
|
||||
dockerfile: docker/web.Dockerfile
|
||||
target: development
|
||||
image: yuxi-web:${YUXI_VERSION:-0.6.2.beta}
|
||||
image: yuxi-web:${YUXI_VERSION:-0.6.2}
|
||||
container_name: web-dev
|
||||
volumes:
|
||||
- ./web/src:/app/src
|
||||
|
||||
@ -2,6 +2,36 @@
|
||||
|
||||
本页用于记录各版本发布说明(新增、修复与破坏性变更)。
|
||||
|
||||
## v0.6.2 (2026-05-22)
|
||||
|
||||
### 新增
|
||||
|
||||
- 新增个人工作区预览与管理:提供独立于对话 thread 的用户级 workspace API,并增加“工作区”页面,用于浏览、预览、编辑、上传、下载、删除个人 workspace 文件;默认创建 `agents/AGENTS.md`,并在 Agent 执行时将其内容追加到系统提示词。
|
||||
- 新增独立模型配置模块:增加 `model_providers` 表、独立管理接口和“模型配置”页面,支持 provider 基础信息、远端候选模型、enabled models 配置和手动添加模型能力。
|
||||
- 新增远程 Skill 批量安装能力:后端新增 `install_remote_skills_batch()` 与 `POST /remote/install-batch`,前端补充批处理安装 API 和 UI 逻辑。
|
||||
|
||||
### 优化
|
||||
|
||||
- 下放扩展管理权限:普通管理员现在可进入扩展管理并完整管理 Tools、MCP、SubAgent、Skills;同步放开 Skill 管理接口权限并补充权限测试。
|
||||
- 调整 Agent 知识库默认选择:未显式配置知识库时默认启用当前用户可访问的全部知识库,显式保存空列表仍表示不启用知识库。
|
||||
- 优化评估基准自动生成:仅支持 commonrag/Milvus 知识库,默认参考 chunks 数量改为 1;多 chunk 场景复用知识库向量检索选择相似 chunks,不再对全量 chunks 重新计算 embedding。
|
||||
- 优化 Agent 输入框文件 mention:用户级 workspace 文件候选改为从独立 workspace API 递归加载,不再依赖 active thread;插入时仍转换为 `/home/gem/user-data/workspace/` 沙盒虚拟路径。
|
||||
- 调整知识库思维导图后端结构:将思维导图路由文件重命名为知识库语义更明确的 router,并把文件列表整理、提示词构建、AI JSON 解析等纯逻辑下沉到知识库 utils。
|
||||
- 收敛知识库评估后端结构:将评估指标、单题评估、答案生成提示词和自动基准生成算法下沉到 `knowledge/eval`,`EvaluationService` 保留任务、文件和持久化编排职责。
|
||||
- 扩展管理界面交互逻辑重构:MCP / Subagents / Skills 从“左侧边栏 + 右侧详情面板”调整为“卡片式网格布局 + 路由跳转二级页面”,工具标签页改为卡片网格布局 + 弹窗详情。
|
||||
- 统一卡片样式:`ExtensionCard` 新增 `tags` prop 并复用于知识库列表页,知识库列表改用 `ExtensionCard` + `ExtensionCardGrid` 替代原有自定义卡片。
|
||||
- 调整应用主导航:`AppLayout` 升级为默认展开的侧边栏,保留折叠态图标导航,并统一导航项、任务中心、GitHub、用户信息的图标与文字对齐。
|
||||
- 合并智能体对话导航:移除 `AgentChatComponent` 内部聊天侧边栏,将新建对话入口和对话历史移动到 `AppLayout` 主侧边栏,并通过共享线程 store 统一管理。
|
||||
- 统一前端 Markdown 预览渲染:新增共享 `MarkdownPreview` 组件与 `markdown_preview` 渲染工具,替换 Agent 消息、文件预览、知识库 chunk、任务工具结果、聊天导出等场景中的旧预览实现。
|
||||
|
||||
### 修复
|
||||
|
||||
- 修复聊天中普通用户 `@` 提及出不来技能和 MCP 列表的问题:放宽技能列表与 MCP 服务器列表读取接口至已登录用户,并对普通用户请求的 MCP 列表进行敏感连接参数脱敏。
|
||||
- 修复知识库文档入库状态回退:当已解析文件缺失 `markdown_file` 解析产物时,索引流程会将文件状态恢复为未解析,便于重新解析。
|
||||
- 修复附件上传后未立即刷新 mention 候选的问题。
|
||||
- 加固 JWT 鉴权安全:移除历史默认密钥回退,初始化脚本支持生成并持久化 `JWT_SECRET_KEY` 与 `YUXI_INSTANCE_ID`,签发和验证令牌时校验 `iss/aud`,并拒绝已删除或登录锁定用户继续使用旧令牌访问系统。
|
||||
- 修复模型配置路由请求模型未接收 `embedding_base_url` / `rerank_base_url` 导致前端已填写仍被后端校验拦截的问题。
|
||||
|
||||
## v0.6.1 (2026-04-24)
|
||||
|
||||
### 新增
|
||||
|
||||
@ -34,26 +34,9 @@
|
||||
|
||||
## 版本记录
|
||||
|
||||
### 0.6.2 开发记录
|
||||
### 0.6.3 开发记录
|
||||
|
||||
<!-- 0.6.2 的内容请放在这里 -->
|
||||
- 下放扩展管理权限:普通管理员现在可进入扩展管理并完整管理 Tools、MCP、SubAgent、Skills;同步放开 Skill 管理接口权限并补充权限测试。
|
||||
- 调整 Agent 知识库默认选择:未显式配置知识库时默认启用当前用户可访问的全部知识库,显式保存空列表仍表示不启用知识库。
|
||||
- 移除知识库沙盒文件系统映射:不再通过 `/home/gem/kbs` 暴露知识库文件树,Agent 继续使用 `query_kb` 与 `open_kb_document` 访问知识库内容。
|
||||
- 优化评估基准自动生成:仅支持 commonrag/Milvus 知识库,默认参考 chunks 数量改为 1;多 chunk 场景复用知识库向量检索选择相似 chunks,不再对全量 chunks 重新计算 embedding,并移除前端 Embedding 模型选择。
|
||||
- 修复知识库文档入库状态回退:当已解析文件缺失 `markdown_file` 解析产物时,索引流程会将文件状态恢复为未解析,便于重新解析而不是停留在索引失败。
|
||||
- 优化 `@` 文件 mention 候选搜索:前端废弃全量递归遍历,改为后端 `/api/mention/search` 接口 + Redis `ormsgpack` 二进制缓存(TTL 60s,上限 10 万条);扫描加宽度/深度/黑名单三重剪枝防卡死;搜索结果按文件名/前缀/路径加权排序,最多返回 50 条;前端加防抖(250ms)+ `AbortController` 防竞态,高亮渲染使用 DOMPurify 防 XSS。
|
||||
- 调整知识库思维导图后端结构:将思维导图路由文件重命名为知识库语义更明确的 router,并把文件列表整理、提示词构建、AI JSON 解析等纯逻辑下沉到知识库 utils。
|
||||
- 收敛知识库评估后端结构:将评估指标、单题评估、答案生成提示词和自动基准生成算法下沉到 `knowledge/eval`,`EvaluationService` 保留任务、文件和持久化编排职责。
|
||||
- 新增个人工作区预览与管理:提供独立于对话 thread 的用户级 workspace API,并增加“工作区”页面,用于浏览个人 workspace 文件、预览 Markdown/文本/代码/图片/PDF;支持新建文件夹、上传文件、下载文件、删除文件/文件夹和多选删除;工作区预览支持 Markdown/TXT 在右侧预览框内切换编辑并保存,其他格式和非工作区预览默认只读;知识库与团队空间入口先展示到占位层级;默认创建 `agents/AGENTS.md`,并在 Agent 执行时将其内容追加到系统提示词。
|
||||
- 加固 JWT 鉴权安全:移除历史默认密钥回退,初始化脚本支持生成并持久化 `JWT_SECRET_KEY` 与 `YUXI_INSTANCE_ID`,签发和验证令牌时校验 `iss/aud`,并在鉴权阶段拒绝已删除或登录锁定用户继续使用旧令牌访问系统。
|
||||
- 扩展管理界面交互逻辑重构:将 MCP / Subagents / Skills 三个标签页从「左侧边栏 + 右侧详情面板」布局重构为「卡片式网格布局 + 路由跳转二级页面」布局,工具标签页改为卡片网格布局 + 弹窗详情(保持弹窗内容不变)。新增共享组件 `ExtensionCard`、`ExtensionCardGrid`、`ExtensionToolbar`、`ExtensionDetailLayout`,详情页(`McpDetailView`、`SubagentDetailView`、`SkillDetailView`)使用居中宽度限制,路由规划为 `/extensions/mcp/:name`、`/extensions/subagent/:name`、`/extensions/skill/:slug`。
|
||||
- 统一卡片样式:`ExtensionCard` 新增 `tags` prop 支持传入 `[{label, color}]` 数组,内部使用 `<a-tag bordered=false size=small>` 渲染,与知识库卡片标签风格统一;知识库列表页 `DataBaseView` 改用 `ExtensionCard` + `ExtensionCardGrid` 替代原有自定义卡片,移除冗余 card 样式。
|
||||
- 调整应用主导航:`AppLayout` 从默认窄栏升级为默认展开的侧边栏,保留折叠态图标导航;侧边栏样式收敛为 14px 文本 + 18px 图标的标准紧凑密度,并统一导航项、任务中心、GitHub、用户信息的图标与文字对齐。折叠态改为仅通过显式按钮展开,避免空白区域误触发。
|
||||
- 合并智能体对话导航:移除 `AgentChatComponent` 内部聊天侧边栏,将新建对话入口和对话历史移动到 `AppLayout` 主侧边栏,并通过共享线程 store 统一管理历史列表、当前线程、重命名、删除、置顶和分页加载。
|
||||
- 新增独立模型配置模块:增加 `model_providers` 表、独立管理接口和”模型配置”页面,支持 provider 基础信息、可配置模型列表端点、远端候选模型、`enabled_models` 的早期配置验证;启动时会补齐内置 provider 模板,`provider_type` 暂统一默认为 `openai`,该模块暂不接入现有运行时模型选择逻辑。远端模型加载默认使用 `/models` 获取 chat/通用模型,provider 声明 `embedding` 能力时使用 `/embeddings/models` 获取 embedding 候选,rerank 模型列表端点按供应商文档显式配置后加载;修复路由请求模型未接收 `embedding_base_url`/`rerank_base_url` 导致前端已填写仍被后端校验拦截的问题。补充手动添加模型能力:`enabled_models[i]` 新增可选 `source: "manual"|"remote"` 字段(默认 `remote`),管理员可通过”+ 手动添加”入口录入远端清单未覆盖的模型(典型:自部署 embedding/rerank),手动模型在前端跳过”远端不存在”的 stale 警告并显示「手动」标签;type 选项受 `provider.capabilities` 约束,后端在 `_normalize_payload` 与 `update_provider_config` 双层一致性校验中拦截越权写入。
|
||||
- 统一前端 Markdown 预览渲染:新增共享 `MarkdownPreview` 组件与 `markdown_preview` 渲染工具,替换 Agent 消息、文件预览、知识库 chunk、任务工具结果、聊天导出等场景中的旧 `md-editor-v3/marked` 预览;支持 KaTeX、任务列表、frontmatter 卡片、Shiki 代码高亮、DOMPurify 清洗和浅层渲染缓存,并抽取 HTML 转义与代码语言归一化工具。Skill 详情页复用 `AgentFilePreview`,统一文件预览、编辑、保存和全屏交互。
|
||||
- 优化远程 Skill 批量安装:`remote_skill_install_service.py` 新增 `install_remote_skills_batch()`,利用 `npx skills add --skill A --skill B --skill C` 原生多 skill 支持,将安装 N 个 skill 的仓库克隆次数从 2N 降至 1;配套新增路由 `POST /remote/install-batch`、前端 `installRemoteSkillsBatch()` API 方法和批处理 UI 逻辑
|
||||
<!-- 0.6.3 的内容请放在这里 -->
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -20,7 +20,7 @@
|
||||
|
||||
```bash
|
||||
# 克隆最新版本
|
||||
git clone --branch v0.6.1 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
git clone --branch v0.6.2 --depth 1 https://github.com/xerrors/Yuxi.git
|
||||
cd Yuxi
|
||||
```
|
||||
|
||||
|
||||
@ -45,7 +45,7 @@ if [ ! -f "$PYPROJECT_FILE" ]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
CURRENT_VERSION=$(grep -E '^version\s*=\s*"' "$PYPROJECT_FILE" | head -1 | sed -E 's/^version\s*=\s*"([^"]+)".*/\1/')
|
||||
CURRENT_VERSION=$(grep -E '^version[[:space:]]*=[[:space:]]*"' "$PYPROJECT_FILE" | head -1 | sed -E 's/^version[[:space:]]*=[[:space:]]*"([^"]+)".*/\1/')
|
||||
|
||||
if [ -z "$CURRENT_VERSION" ]; then
|
||||
echo "错误: 无法从 ${PYPROJECT_FILE} 读取当前版本号"
|
||||
@ -67,6 +67,7 @@ echo " - docker-compose.prod.yml"
|
||||
echo " - backend/uv.lock"
|
||||
if [ "$DEV_MODE" = false ]; then
|
||||
echo " - README.md"
|
||||
echo " - README.en.md"
|
||||
echo " - docs/intro/quick-start.md"
|
||||
fi
|
||||
echo ""
|
||||
@ -80,32 +81,32 @@ fi
|
||||
# 1. 更新 Python 包版本 (backend/package/pyproject.toml)
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "→ 更新 backend/package/pyproject.toml"
|
||||
sed -i -E "s/^version = \"[^\"]+\"/version = \"${NEW_VERSION}\"/" \
|
||||
perl -pi -e "s/^version = \"[^\"]+\"/version = \"${NEW_VERSION}\"/" \
|
||||
"${PROJECT_ROOT}/backend/package/pyproject.toml"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2. 更新后端工作区版本 (backend/pyproject.toml)
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "→ 更新 backend/pyproject.toml"
|
||||
sed -i -E "s/^version = \"[^\"]+\"/version = \"${NEW_VERSION}\"/" \
|
||||
perl -pi -e "s/^version = \"[^\"]+\"/version = \"${NEW_VERSION}\"/" \
|
||||
"${PROJECT_ROOT}/backend/pyproject.toml"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 3. 更新前端版本 (web/package.json)
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "→ 更新 web/package.json"
|
||||
sed -i -E "s/\"version\": \"[^\"]+\"/\"version\": \"${NEW_VERSION}\"/" \
|
||||
perl -pi -e "s/\"version\": \"[^\"]+\"/\"version\": \"${NEW_VERSION}\"/" \
|
||||
"${PROJECT_ROOT}/web/package.json"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 4. 更新 Docker Compose 镜像标签默认值
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "→ 更新 docker-compose.yml"
|
||||
sed -i -E "s/\\\$\{YUXI_VERSION:-[^}]+\}/\${YUXI_VERSION:-${NEW_VERSION}}/g" \
|
||||
perl -pi -e "s/\\\$\\{YUXI_VERSION:-[^}]+\\}/\\\${YUXI_VERSION:-${NEW_VERSION}}/g" \
|
||||
"${PROJECT_ROOT}/docker-compose.yml"
|
||||
|
||||
echo "→ 更新 docker-compose.prod.yml"
|
||||
sed -i -E "s/\\\$\{YUXI_VERSION:-[^}]+\}/\${YUXI_VERSION:-${NEW_VERSION}}/g" \
|
||||
perl -pi -e "s/\\\$\\{YUXI_VERSION:-[^}]+\\}/\\\${YUXI_VERSION:-${NEW_VERSION}}/g" \
|
||||
"${PROJECT_ROOT}/docker-compose.prod.yml"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ -113,10 +114,10 @@ sed -i -E "s/\\\$\{YUXI_VERSION:-[^}]+\}/\${YUXI_VERSION:-${NEW_VERSION}}/g" \
|
||||
# -----------------------------------------------------------------------------
|
||||
echo "→ 更新 backend/uv.lock"
|
||||
# yuxi 包版本
|
||||
sed -i -E "/^name = \"yuxi\"$/{n;s/^version = \"[^\"]+\"/version = \"${NEW_VERSION}\"/;}" \
|
||||
perl -0pi -e "s/(^name = \"yuxi\"\nversion = \")[^\"]+/\${1}${NEW_VERSION}/m" \
|
||||
"${PROJECT_ROOT}/backend/uv.lock"
|
||||
# yuxi-workspace 版本
|
||||
sed -i -E "/^name = \"yuxi-workspace\"$/{n;s/^version = \"[^\"]+\"/version = \"${NEW_VERSION}\"/;}" \
|
||||
perl -0pi -e "s/(^name = \"yuxi-workspace\"\nversion = \")[^\"]+/\${1}${NEW_VERSION}/m" \
|
||||
"${PROJECT_ROOT}/backend/uv.lock"
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
@ -126,14 +127,18 @@ sed -i -E "/^name = \"yuxi-workspace\"$/{n;s/^version = \"[^\"]+\"/version = \"$
|
||||
# 发布历史记录(如 [2026/04/01] v0.6.1 版本发布)不修改,保持为历史版本记录
|
||||
if [ "$DEV_MODE" = false ]; then
|
||||
echo "→ 更新 README.md"
|
||||
sed -i -E "s/(git clone --branch v)[0-9]+\.[0-9]+\.[0-9]+/\1${NEW_VERSION}/g" \
|
||||
perl -pi -e "s/(git clone --branch v)[0-9]+\\.[0-9]+\\.[0-9]+/\${1}${NEW_VERSION}/g" \
|
||||
"${PROJECT_ROOT}/README.md"
|
||||
|
||||
echo "→ 更新 README.en.md"
|
||||
perl -pi -e "s/(git clone --branch v)[0-9]+\\.[0-9]+\\.[0-9]+/\${1}${NEW_VERSION}/g" \
|
||||
"${PROJECT_ROOT}/README.en.md"
|
||||
|
||||
echo "→ 更新 docs/intro/quick-start.md"
|
||||
sed -i -E "s/(git clone --branch v)[0-9]+\.[0-9]+\.[0-9]+/\1${NEW_VERSION}/g" \
|
||||
perl -pi -e "s/(git clone --branch v)[0-9]+\\.[0-9]+\\.[0-9]+/\${1}${NEW_VERSION}/g" \
|
||||
"${PROJECT_ROOT}/docs/intro/quick-start.md"
|
||||
else
|
||||
echo "→ dev 模式,跳过 README.md 和 docs/intro/quick-start.md 的分支版本更新"
|
||||
echo "→ dev 模式,跳过 README.md、README.en.md 和 docs/intro/quick-start.md 的分支版本更新"
|
||||
fi
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "yuxi-web",
|
||||
"version": "0.6.2.beta",
|
||||
"version": "0.6.2",
|
||||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
import { apiGet, apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
|
||||
/**
|
||||
* MCP 服务器管理 API 模块
|
||||
@ -16,7 +16,7 @@ const BASE_URL = '/api/system/mcp-servers'
|
||||
* @returns {Promise} - 服务器列表
|
||||
*/
|
||||
export const getMcpServers = async () => {
|
||||
return apiAdminGet(BASE_URL)
|
||||
return apiGet(BASE_URL)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import { apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
import { apiGet, apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
|
||||
const BASE_URL = '/api/system/skills'
|
||||
|
||||
export const listSkills = async () => {
|
||||
return apiAdminGet(BASE_URL)
|
||||
return apiGet(BASE_URL)
|
||||
}
|
||||
|
||||
export const importSkillZip = async (file) => {
|
||||
|
||||
@ -1614,10 +1614,10 @@ const hasVisibleAssistantBody = (message) => {
|
||||
const { content, reasoningContent } = extractAssistantMessageBody(message)
|
||||
return Boolean(
|
||||
content ||
|
||||
reasoningContent ||
|
||||
message.error_type ||
|
||||
message.extra_metadata?.error_type ||
|
||||
message.isStoppedByUser
|
||||
reasoningContent ||
|
||||
message.error_type ||
|
||||
message.extra_metadata?.error_type ||
|
||||
message.isStoppedByUser
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
Loading…
Reference in New Issue
Block a user