feat: 新增环境配置管理和接口编辑增强
- 新增环境配置(EnvironmentConfig)的增删改查和激活功能 - API 接口支持 description、request_params、response_params 字段 - 接口管理页面支持抽屉内编辑(替代跳转编辑页) - 新增版本管理页面和工作流列表页面 - catalog_snapshot 返回 environments 数据 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+218
-1
@@ -25,6 +25,7 @@ from sqlalchemy.orm import Session
|
||||
from .database import Base, SessionLocal, engine, get_db
|
||||
from .models import (
|
||||
ApiDefinition,
|
||||
EnvironmentConfig,
|
||||
FolderEntry,
|
||||
McpToolConfig,
|
||||
MockDataset,
|
||||
@@ -77,6 +78,9 @@ from .schemas import (
|
||||
SshShortcutOut,
|
||||
SshShortcutUpdate,
|
||||
RunWorkflowRequest,
|
||||
EnvironmentConfigCreate,
|
||||
EnvironmentConfigOut,
|
||||
EnvironmentConfigUpdate,
|
||||
UserApiKeyCreate,
|
||||
UserApiKeyCreateResponse,
|
||||
UserApiKeyOut,
|
||||
@@ -287,6 +291,9 @@ MCP_MUTATION_TOOLS = frozenset(
|
||||
"ssh_script_upsert",
|
||||
"workflow_batch_create",
|
||||
"workflow_batch_update",
|
||||
"env_upsert",
|
||||
"env_activate",
|
||||
"env_delete",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -614,7 +621,7 @@ def _delete_ssh_script_files(row: SshScript) -> None:
|
||||
MCP_TOOL_SPECS = [
|
||||
{
|
||||
"name": "api_upsert",
|
||||
"description": "Create or update one API definition.",
|
||||
"description": "Create or update one API definition (supports description and request/response param schema).",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"required": ["name", "method", "url"],
|
||||
@@ -624,11 +631,38 @@ MCP_TOOL_SPECS = [
|
||||
"folder_path": {"type": "string"},
|
||||
"method": {"type": "string"},
|
||||
"url": {"type": "string"},
|
||||
"description": {"type": "string", "description": "API description / notes"},
|
||||
"headers": {"type": "object"},
|
||||
"body": {"type": "object"},
|
||||
"query": {"type": "object"},
|
||||
"path_params": {"type": "object"},
|
||||
"timeout_seconds": {"type": "number"},
|
||||
"request_params": {
|
||||
"type": "array",
|
||||
"description": "Request parameter schema",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"type": {"type": "string", "enum": ["string", "integer", "number", "boolean", "object", "array"]},
|
||||
"required": {"type": "boolean"},
|
||||
"description": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
"response_params": {
|
||||
"type": "array",
|
||||
"description": "Response parameter schema",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {"type": "string"},
|
||||
"type": {"type": "string", "enum": ["string", "integer", "number", "boolean", "object", "array"]},
|
||||
"required": {"type": "boolean"},
|
||||
"description": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -966,6 +1000,44 @@ MCP_TOOL_SPECS = [
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "env_list",
|
||||
"description": "List all environment configurations (base_url + auth_token versions).",
|
||||
"input_schema": {"type": "object", "properties": {}},
|
||||
},
|
||||
{
|
||||
"name": "env_upsert",
|
||||
"description": "Create or update an environment configuration (base_url, auth_token). Requires sto- API Key.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"required": ["name", "base_url"],
|
||||
"properties": {
|
||||
"env_id": {"type": "integer", "description": "Pass to update existing; omit to create new"},
|
||||
"name": {"type": "string"},
|
||||
"base_url": {"type": "string"},
|
||||
"auth_token": {"type": "string"},
|
||||
"description": {"type": "string"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "env_activate",
|
||||
"description": "Set an environment as the active version. Archives the previously active one. Requires sto- API Key.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"required": ["env_id"],
|
||||
"properties": {"env_id": {"type": "integer"}},
|
||||
},
|
||||
},
|
||||
{
|
||||
"name": "env_delete",
|
||||
"description": "Delete an environment configuration. Requires sto- API Key.",
|
||||
"input_schema": {
|
||||
"type": "object",
|
||||
"required": ["env_id"],
|
||||
"properties": {"env_id": {"type": "integer"}},
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
|
||||
@@ -1968,6 +2040,7 @@ async def _dispatch_mcp_tool(
|
||||
if not _is_superadmin(actor):
|
||||
batch_query = batch_query.filter(WorkflowBatch.creator_id == actor.id)
|
||||
batch_rows = batch_query.order_by(WorkflowBatch.id.desc()).limit(50).all()
|
||||
env_rows = _scope_query_by_user(db.query(EnvironmentConfig), EnvironmentConfig, actor).order_by(EnvironmentConfig.id.desc()).all()
|
||||
data = {
|
||||
"apis": apis,
|
||||
"workflows": workflows,
|
||||
@@ -1975,6 +2048,7 @@ async def _dispatch_mcp_tool(
|
||||
"workflow_batches": [_model_to_batch_dict(row) for row in batch_rows],
|
||||
"mcp_tools": configs,
|
||||
"ssh_tree": ssh_tree,
|
||||
"environments": [EnvironmentConfigOut.model_validate(row).model_dump() for row in env_rows],
|
||||
"folders": {
|
||||
"apis": list_folders(target="apis", db=db, user=actor)["folders"],
|
||||
"workflows": list_folders(target="workflows", db=db, user=actor)["folders"],
|
||||
@@ -2055,6 +2129,67 @@ async def _dispatch_mcp_tool(
|
||||
data = await run_script_collect(profile, script, password, timeout_seconds=timeout_seconds)
|
||||
return McpInvokeResponse(ok=bool(data.get("ok")), tool=tool, data=data)
|
||||
|
||||
if tool == "env_list":
|
||||
rows = _scope_query_by_user(db.query(EnvironmentConfig), EnvironmentConfig, actor).order_by(EnvironmentConfig.id.desc()).all()
|
||||
items = [EnvironmentConfigOut.model_validate(row).model_dump() for row in rows]
|
||||
return McpInvokeResponse(ok=True, tool=tool, data={"items": items})
|
||||
|
||||
if tool == "env_upsert":
|
||||
env_id = args.get("env_id")
|
||||
if env_id:
|
||||
row = db.query(EnvironmentConfig).filter(EnvironmentConfig.id == int(env_id)).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="environment not found")
|
||||
_assert_owner_or_superadmin(actor, row)
|
||||
row.name = str(args.get("name", row.name)).strip()
|
||||
row.base_url = str(args.get("base_url", row.base_url)).strip()
|
||||
row.auth_token = str(args.get("auth_token", row.auth_token)).strip()
|
||||
row.description = str(args.get("description", row.description)).strip()
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
else:
|
||||
row = EnvironmentConfig(
|
||||
name=str(args["name"]).strip(),
|
||||
base_url=str(args.get("base_url", "")).strip(),
|
||||
auth_token=str(args.get("auth_token", "")).strip(),
|
||||
description=str(args.get("description", "")).strip(),
|
||||
status="draft",
|
||||
creator_id=actor.id,
|
||||
creator_name=actor.display_name or actor.username,
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
data = EnvironmentConfigOut.model_validate(row).model_dump()
|
||||
return McpInvokeResponse(ok=True, tool=tool, data=data)
|
||||
|
||||
if tool == "env_activate":
|
||||
env_id = int(args["env_id"])
|
||||
row = db.query(EnvironmentConfig).filter(EnvironmentConfig.id == env_id).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="environment not found")
|
||||
_assert_owner_or_superadmin(actor, row)
|
||||
active_rows = _scope_query_by_user(
|
||||
db.query(EnvironmentConfig).filter(EnvironmentConfig.status == "active"), EnvironmentConfig, actor
|
||||
).all()
|
||||
for active_row in active_rows:
|
||||
active_row.status = "archived"
|
||||
row.status = "active"
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
data = EnvironmentConfigOut.model_validate(row).model_dump()
|
||||
return McpInvokeResponse(ok=True, tool=tool, data=data)
|
||||
|
||||
if tool == "env_delete":
|
||||
env_id = int(args["env_id"])
|
||||
row = db.query(EnvironmentConfig).filter(EnvironmentConfig.id == env_id).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="environment not found")
|
||||
_assert_owner_or_superadmin(actor, row)
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return McpInvokeResponse(ok=True, tool=tool, data={"deleted": env_id})
|
||||
|
||||
raise HTTPException(status_code=404, detail=f"unknown mcp tool: {tool}")
|
||||
except HTTPException as exc:
|
||||
return McpInvokeResponse(ok=False, tool=tool, error=str(exc.detail))
|
||||
@@ -2386,6 +2521,9 @@ def create_api(payload: ApiCreate, db: Session = Depends(get_db), user: User = D
|
||||
"query": payload.query,
|
||||
"path_params": payload.path_params,
|
||||
"timeout_seconds": payload.timeout_seconds,
|
||||
"description": payload.description,
|
||||
"request_params": [p.model_dump() for p in payload.request_params],
|
||||
"response_params": [p.model_dump() for p in payload.response_params],
|
||||
}
|
||||
row = ApiDefinition(
|
||||
name=payload.name,
|
||||
@@ -2426,6 +2564,9 @@ def update_api(api_id: int, payload: ApiUpdate, db: Session = Depends(get_db), u
|
||||
"query": payload.query,
|
||||
"path_params": payload.path_params,
|
||||
"timeout_seconds": payload.timeout_seconds,
|
||||
"description": payload.description,
|
||||
"request_params": [p.model_dump() for p in payload.request_params],
|
||||
"response_params": [p.model_dump() for p in payload.response_params],
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
@@ -3131,6 +3272,82 @@ def upsert_mcp_tool(payload: McpToolCreate, db: Session = Depends(get_db), user:
|
||||
return model_to_mcp_out(row)
|
||||
|
||||
|
||||
@app.get("/api/environments", response_model=list[EnvironmentConfigOut])
|
||||
def list_environments(db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
rows = _scope_query_by_user(db.query(EnvironmentConfig), EnvironmentConfig, user).order_by(EnvironmentConfig.id.desc()).all()
|
||||
return [EnvironmentConfigOut.model_validate(row) for row in rows]
|
||||
|
||||
|
||||
@app.post("/api/environments", response_model=EnvironmentConfigOut)
|
||||
def create_environment(payload: EnvironmentConfigCreate, db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
row = EnvironmentConfig(
|
||||
name=payload.name.strip(),
|
||||
base_url=payload.base_url.strip(),
|
||||
auth_token=payload.auth_token.strip(),
|
||||
description=payload.description.strip(),
|
||||
status="draft",
|
||||
creator_id=user.id,
|
||||
creator_name=user.display_name or user.username,
|
||||
)
|
||||
db.add(row)
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return EnvironmentConfigOut.model_validate(row)
|
||||
|
||||
|
||||
@app.put("/api/environments/{env_id}", response_model=EnvironmentConfigOut)
|
||||
def update_environment(env_id: int, payload: EnvironmentConfigUpdate, db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
row = db.query(EnvironmentConfig).filter(EnvironmentConfig.id == env_id).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="environment not found")
|
||||
_assert_owner_or_superadmin(user, row)
|
||||
row.name = payload.name.strip()
|
||||
row.base_url = payload.base_url.strip()
|
||||
row.auth_token = payload.auth_token.strip()
|
||||
row.description = payload.description.strip()
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return EnvironmentConfigOut.model_validate(row)
|
||||
|
||||
|
||||
@app.post("/api/environments/{env_id}/activate")
|
||||
def activate_environment(env_id: int, db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
row = db.query(EnvironmentConfig).filter(EnvironmentConfig.id == env_id).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="environment not found")
|
||||
_assert_owner_or_superadmin(user, row)
|
||||
active_rows = _scope_query_by_user(
|
||||
db.query(EnvironmentConfig).filter(EnvironmentConfig.status == "active"), EnvironmentConfig, user
|
||||
).all()
|
||||
for active_row in active_rows:
|
||||
active_row.status = "archived"
|
||||
row.status = "active"
|
||||
db.commit()
|
||||
db.refresh(row)
|
||||
return EnvironmentConfigOut.model_validate(row)
|
||||
|
||||
|
||||
@app.delete("/api/environments/{env_id}")
|
||||
def delete_environment(env_id: int, db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
row = db.query(EnvironmentConfig).filter(EnvironmentConfig.id == env_id).first()
|
||||
if not row:
|
||||
raise HTTPException(status_code=404, detail="environment not found")
|
||||
_assert_owner_or_superadmin(user, row)
|
||||
db.delete(row)
|
||||
db.commit()
|
||||
return {"ok": True}
|
||||
|
||||
|
||||
@app.get("/api/environments/active", response_model=EnvironmentConfigOut | None)
|
||||
def get_active_environment(db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
row = _scope_query_by_user(
|
||||
db.query(EnvironmentConfig).filter(EnvironmentConfig.status == "active"), EnvironmentConfig, user
|
||||
).first()
|
||||
if not row:
|
||||
return None
|
||||
return EnvironmentConfigOut.model_validate(row)
|
||||
|
||||
|
||||
@app.get("/api/ssh-profiles", response_model=list[SshProfileOut])
|
||||
def list_ssh_profiles(db: Session = Depends(get_db), user: User = Depends(_get_current_user)):
|
||||
rows = _scope_query_by_user(db.query(SshProfile), SshProfile, user).order_by(SshProfile.id.desc()).all()
|
||||
|
||||
@@ -217,3 +217,18 @@ class WorkflowRun(Base):
|
||||
summary_json = Column(Text, nullable=False, default="{}")
|
||||
payload_json = Column(Text, nullable=False, default="{}")
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False, index=True)
|
||||
|
||||
|
||||
class EnvironmentConfig(Base):
|
||||
__tablename__ = "environment_configs"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
name = Column(String(128), nullable=False, index=True)
|
||||
base_url = Column(String(512), nullable=False, default="")
|
||||
auth_token = Column(String(512), nullable=False, default="")
|
||||
description = Column(Text, nullable=False, default="")
|
||||
status = Column(String(16), nullable=False, default="draft") # active | draft | archived
|
||||
creator_id = Column(Integer, nullable=False, default=1, index=True)
|
||||
creator_name = Column(String(64), nullable=False, default="admin")
|
||||
created_at = Column(DateTime(timezone=True), server_default=func.now(), nullable=False)
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now(), nullable=False)
|
||||
|
||||
@@ -3,16 +3,26 @@ from typing import Any, Literal
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ApiParamField(BaseModel):
|
||||
name: str = ""
|
||||
type: str = "string"
|
||||
required: bool = False
|
||||
description: str = ""
|
||||
|
||||
|
||||
class ApiBase(BaseModel):
|
||||
name: str
|
||||
folder_path: str = ""
|
||||
method: str = "GET"
|
||||
url: str
|
||||
description: str = ""
|
||||
headers: dict[str, Any] = Field(default_factory=dict)
|
||||
body: dict[str, Any] = Field(default_factory=dict)
|
||||
query: dict[str, Any] = Field(default_factory=dict)
|
||||
path_params: dict[str, Any] = Field(default_factory=dict)
|
||||
timeout_seconds: float = 10.0
|
||||
request_params: list[ApiParamField] = Field(default_factory=list)
|
||||
response_params: list[ApiParamField] = Field(default_factory=list)
|
||||
|
||||
|
||||
class ApiCreate(ApiBase):
|
||||
@@ -387,6 +397,33 @@ class FolderMoveRequest(BaseModel):
|
||||
to_path: str
|
||||
|
||||
|
||||
class EnvironmentConfigBase(BaseModel):
|
||||
name: str
|
||||
base_url: str = ""
|
||||
auth_token: str = ""
|
||||
description: str = ""
|
||||
|
||||
|
||||
class EnvironmentConfigCreate(EnvironmentConfigBase):
|
||||
pass
|
||||
|
||||
|
||||
class EnvironmentConfigUpdate(EnvironmentConfigBase):
|
||||
pass
|
||||
|
||||
|
||||
class EnvironmentConfigOut(EnvironmentConfigBase):
|
||||
id: int
|
||||
status: str = "draft"
|
||||
creator_id: int = 0
|
||||
creator_name: str = ""
|
||||
created_at: Any = None
|
||||
updated_at: Any = None
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
|
||||
|
||||
class McpInvokeRequest(BaseModel):
|
||||
tool: str
|
||||
arguments: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
Reference in New Issue
Block a user