Merge remote-tracking branch 'origin/main' into feat/agent-v2

# Conflicts:
#	api/controllers/console/app/agent_drive_inspector.py
#	api/migrations/versions/2026_06_18_2300-b2515f9d4c2a_agent_drive_skill_metadata_refactor.py
#	api/openapi/markdown/console-openapi.md
#	api/services/agent/skill_standardize_service.py
#	api/services/agent_drive_service.py
#	api/tests/unit_tests/controllers/console/app/test_agent_drive_inspector.py
#	api/tests/unit_tests/services/agent/test_skill_standardize_service.py
#	packages/contracts/generated/api/console/agent/orpc.gen.ts
#	packages/contracts/generated/api/console/agent/types.gen.ts
#	packages/contracts/generated/api/console/agent/zod.gen.ts
#	packages/contracts/generated/api/console/apps/orpc.gen.ts
#	packages/contracts/generated/api/console/apps/types.gen.ts
#	packages/contracts/generated/api/console/apps/zod.gen.ts
This commit is contained in:
Yanli 盐粒
2026-06-22 16:43:54 +08:00
215 changed files with 6267 additions and 3468 deletions

View File

@ -36,6 +36,7 @@ from extensions.ext_database import db
from fields.agent_fields import (
AgentConfigSnapshotDetailResponse,
AgentConfigSnapshotListResponse,
AgentConfigSnapshotRestoreResponse,
AgentInviteOptionsResponse,
AgentLogListResponse,
AgentLogMessageListResponse,
@ -223,6 +224,7 @@ register_response_schema_models(
AgentAppPartial,
AgentConfigSnapshotDetailResponse,
AgentConfigSnapshotListResponse,
AgentConfigSnapshotRestoreResponse,
AgentInviteOptionsResponse,
AgentLogListResponse,
AgentLogMessageListResponse,
@ -649,3 +651,24 @@ class AgentRosterVersionDetailApi(Resource):
version_id=str(version_id),
),
)
@console_ns.route("/agent/<uuid:agent_id>/versions/<uuid:version_id>/restore")
class AgentRosterVersionRestoreApi(Resource):
@console_ns.response(200, "Agent version restored", console_ns.models[AgentConfigSnapshotRestoreResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@edit_permission_required
@with_current_user
@with_current_tenant_id
def post(self, tenant_id: str, current_user: Account, agent_id: UUID, version_id: UUID):
return dump_response(
AgentConfigSnapshotRestoreResponse,
_agent_roster_service().restore_agent_version(
tenant_id=tenant_id,
agent_id=str(agent_id),
version_id=str(version_id),
account_id=current_user.id,
),
)

View File

@ -10,8 +10,12 @@ backend — drive data lives in the API's own DB/storage, served straight from
from __future__ import annotations
import json
from collections.abc import Mapping
from typing import Any
from uuid import UUID
from flask import Response
from flask_restx import Resource
from pydantic import BaseModel, Field
@ -49,6 +53,10 @@ class AgentDriveFileByAgentQuery(BaseModel):
key: str = Field(min_length=1, description="Drive key, e.g. tender-analyzer/SKILL.md")
class AgentDriveSkillInspectQuery(BaseModel):
node_id: str | None = Field(default=None, description="Workflow node ID (workflow composer variant)")
class AgentDriveItemResponse(ResponseModel):
key: str
size: int | None = None
@ -80,6 +88,39 @@ class AgentDriveSkillListResponse(ResponseModel):
items: list[AgentDriveSkillItemResponse] = Field(default_factory=list)
class AgentDriveSkillFileResponse(ResponseModel):
path: str
name: str
type: str
drive_key: str | None = None
available_in_drive: bool
class AgentDriveSkillMarkdownResponse(ResponseModel):
key: str
size: int | None = None
truncated: bool
binary: bool
text: str | None = None
class AgentDriveSkillInspectResponse(ResponseModel):
path: str
skill_md_key: str
archive_key: str | None = None
name: str
description: str
size: int | None = None
mime_type: str | None = None
hash: str | None = None
created_at: int | None = None
source: str
files: list[AgentDriveSkillFileResponse] = Field(default_factory=list)
file_tree: list[dict[str, Any]] = Field(default_factory=list)
skill_md: AgentDriveSkillMarkdownResponse
warnings: list[str] = Field(default_factory=list)
class AgentDrivePreviewResponse(ResponseModel):
key: str
size: int | None = None
@ -97,6 +138,7 @@ register_response_schema_models(
AgentDriveDownloadResponse,
AgentDriveListResponse,
AgentDrivePreviewResponse,
AgentDriveSkillInspectResponse,
AgentDriveSkillListResponse,
)
@ -118,6 +160,13 @@ def _handle(exc: AgentDriveError) -> tuple[dict[str, object], int]:
return {"code": exc.code, "message": exc.message}, exc.status_code
def _json_response(data: Mapping[str, Any]):
return Response(
response=json.dumps(data, ensure_ascii=False, separators=(",", ":")),
content_type="application/json; charset=utf-8",
)
_WORKFLOW_APP_MODES = [AppMode.WORKFLOW, AppMode.ADVANCED_CHAT]
@ -160,6 +209,30 @@ class AgentDriveSkillListByAgentApi(Resource):
return {"items": items}
@console_ns.route("/agent/<uuid:agent_id>/drive/skills/<path:skill_path>/inspect")
class AgentDriveSkillInspectByAgentApi(Resource):
@console_ns.doc("inspect_agent_drive_skill_by_agent")
@console_ns.doc(description="Inspect one drive-backed skill for slash-menu hover/detail UI")
@console_ns.doc(params={"agent_id": "Agent ID", "skill_path": "Skill path/slug, e.g. tender-analyzer"})
@console_ns.response(200, "Drive skill inspect view", console_ns.models[AgentDriveSkillInspectResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@with_current_tenant_id
def get(self, tenant_id: str, agent_id: UUID, skill_path: str):
resolve_agent_app_model(tenant_id=tenant_id, agent_id=agent_id)
try:
return _json_response(
AgentDriveService().inspect_skill(
tenant_id=tenant_id,
agent_id=str(agent_id),
skill_path=skill_path,
)
)
except AgentDriveError as exc:
return _handle(exc)
@console_ns.route("/agent/<uuid:agent_id>/drive/files/preview")
class AgentDrivePreviewByAgentApi(Resource):
@console_ns.doc("preview_agent_drive_file_by_agent")
@ -245,6 +318,39 @@ class AgentDriveSkillListApi(Resource):
return {"items": items}
@console_ns.route("/apps/<uuid:app_id>/agent/drive/skills/<path:skill_path>/inspect")
class AgentDriveSkillInspectApi(Resource):
@console_ns.doc("inspect_agent_drive_skill")
@console_ns.doc(description="Inspect one drive-backed skill for slash-menu hover/detail UI")
@console_ns.doc(
params={
"app_id": "Application ID",
"skill_path": "Skill path/slug, e.g. tender-analyzer",
**query_params_from_model(AgentDriveSkillInspectQuery),
}
)
@console_ns.response(200, "Drive skill inspect view", console_ns.models[AgentDriveSkillInspectResponse.__name__])
@setup_required
@login_required
@account_initialization_required
@get_app_model(mode=_WORKFLOW_APP_MODES)
def get(self, app_model: App, skill_path: str):
query = query_params_from_request(AgentDriveSkillInspectQuery)
agent_id = _resolve_agent_id(app_model, query.node_id)
if not agent_id:
return _agent_not_bound()
try:
return _json_response(
AgentDriveService().inspect_skill(
tenant_id=app_model.tenant_id,
agent_id=agent_id,
skill_path=skill_path,
)
)
except AgentDriveError as exc:
return _handle(exc)
@console_ns.route("/apps/<uuid:app_id>/agent/drive/files/preview")
class AgentDrivePreviewApi(Resource):
@console_ns.doc("preview_agent_drive_file")
@ -295,6 +401,8 @@ __all__ = [
"AgentDriveListByAgentApi",
"AgentDrivePreviewApi",
"AgentDrivePreviewByAgentApi",
"AgentDriveSkillInspectApi",
"AgentDriveSkillInspectByAgentApi",
"AgentDriveSkillListApi",
"AgentDriveSkillListByAgentApi",
]

View File

@ -211,7 +211,7 @@ def _legacy_workspace_roles(
name=role_name,
description="",
is_builtin=True,
permission_keys=list(_LEGACY_ROLE_PERMISSION_KEYS[role_name]),
permission_keys=list(dict.fromkeys(_LEGACY_ROLE_PERMISSION_KEYS[role_name])),
role_tag="owner" if role_name == "owner" else "",
)
for role_name in ("owner", "admin", "editor", "normal", "dataset_operator")
@ -244,11 +244,6 @@ def _legacy_workspace_roles(
)
# ---------------------------------------------------------------------------
# Permission catalogs.
# ---------------------------------------------------------------------------
@console_ns.route("/workspaces/current/rbac/role-permissions/catalog")
class RBACWorkspaceCatalogApi(Resource):
@login_required
@ -375,30 +370,6 @@ class RBACRoleCopyApi(Resource):
return _dump(role), 201
@console_ns.route("/workspaces/current/rbac/roles/<uuid:role_id>/members")
class RBACRoleMembersApi(Resource):
@login_required
@rbac_permission_required(
RBACResourceScope.WORKSPACE, RBACPermission.WORKSPACE_ROLE_MANAGE, resource_required=False
)
@console_ns.response(200, "Success", console_ns.models[_RBACRoleAccountList.__name__])
def get(self, role_id):
tenant_id, account_id = _current_ids()
return _dump(
svc.RBACService.Roles.members(
tenant_id,
account_id,
str(role_id),
options=_pagination_options(),
)
)
# ---------------------------------------------------------------------------
# Access policies (tenant-level permission sets).
# ---------------------------------------------------------------------------
class _AccessPolicyCreateRequest(BaseModel):
name: str
resource_type: svc.RBACResourceType
@ -788,11 +759,6 @@ class RBACDatasetMemberBindingsApi(Resource):
return {"result": "success"}
# ---------------------------------------------------------------------------
# Workspace-level access (Settings > Access Rules).
# ---------------------------------------------------------------------------
@console_ns.route("/workspaces/current/rbac/workspace/apps/access-policy")
class RBACWorkspaceAppMatrixApi(Resource):
@login_required