fix: make lint && make format
This commit is contained in:
parent
b76d0d663e
commit
8b6229d230
@ -434,9 +434,6 @@ class KubernetesProvisionerBackend:
|
||||
if discovered is not None:
|
||||
return discovered
|
||||
|
||||
pod_name = self._pod_name(sandbox_id)
|
||||
service_name = self._service_name(sandbox_id)
|
||||
|
||||
try:
|
||||
self._core_api.create_namespaced_pod(
|
||||
namespace=self._namespace,
|
||||
|
||||
@ -144,4 +144,4 @@ if __name__ == "__main__":
|
||||
port=5050,
|
||||
reload=True,
|
||||
reload_dirs=["server", "src"],
|
||||
)
|
||||
)
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
"""ARQ worker entrypoint."""
|
||||
|
||||
import asyncio
|
||||
import os
|
||||
import sys
|
||||
@ -11,4 +12,4 @@ if sys.platform == "win32":
|
||||
|
||||
from src.services.run_worker import WorkerSettings
|
||||
|
||||
__all__ = ["WorkerSettings"]
|
||||
__all__ = ["WorkerSettings"]
|
||||
|
||||
@ -2,8 +2,8 @@ from __future__ import annotations
|
||||
|
||||
from deepagents.backends import CompositeBackend
|
||||
|
||||
from src.sandbox import ProvisionerSandboxBackend
|
||||
from src.agents.common.middlewares.skills_middleware import normalize_selected_skills
|
||||
from src.sandbox import ProvisionerSandboxBackend
|
||||
|
||||
from .skills_backend import SelectedSkillsReadonlyBackend
|
||||
|
||||
|
||||
@ -1,59 +0,0 @@
|
||||
"""
|
||||
MinIO Backend for Deep Agents
|
||||
|
||||
基于 S3_backend 适配的 MinIO 后端实现。
|
||||
使用环境变量配置 MinIO 连接,专用于 Agent 状态存储。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
|
||||
from src.utils.S3_backend import S3Backend, S3Config
|
||||
|
||||
__all__ = ["MinIOBackend", "get_minio_backend"]
|
||||
|
||||
|
||||
def get_minio_backend(runtime) -> MinIOBackend:
|
||||
thread_id = getattr(runtime, "config", {}).get("configurable", {}).get("thread_id")
|
||||
"""获取 MinIO 后端实例(单例)"""
|
||||
return MinIOBackend(thread_id)
|
||||
|
||||
|
||||
class MinIOBackend(S3Backend):
|
||||
"""
|
||||
基于 S3Backend 的 MinIO 后端。
|
||||
|
||||
使用环境变量配置:
|
||||
- MINIO_URI: MinIO 端点地址(默认: http://milvus-minio:9000)
|
||||
- MINIO_ACCESS_KEY: 访问密钥(默认: minioadmin)
|
||||
- MINIO_SECRET_KEY: 密钥(默认: minioadmin)
|
||||
|
||||
默认配置:
|
||||
- bucket: "state-bucket"
|
||||
- prefix: "threads"
|
||||
"""
|
||||
|
||||
def __init__(self, thread_id: str) -> None:
|
||||
endpoint = os.getenv("MINIO_URI") or "http://milvus-minio:9000"
|
||||
access_key = os.getenv("MINIO_ACCESS_KEY") or "minioadmin"
|
||||
secret_key = os.getenv("MINIO_SECRET_KEY") or "minioadmin"
|
||||
|
||||
|
||||
# 从 endpoint 提取 region(MinIO 通常用 us-east-1)
|
||||
region = os.getenv("MINIO_REGION") or "us-east-1"
|
||||
|
||||
config = S3Config(
|
||||
bucket="agent-state-bucket",
|
||||
prefix=f"threads/{thread_id}",
|
||||
region=region,
|
||||
endpoint_url=endpoint,
|
||||
access_key_id=access_key,
|
||||
secret_access_key=secret_key,
|
||||
use_ssl=endpoint.startswith("https://"),
|
||||
max_pool_connections=50,
|
||||
connect_timeout=5.0,
|
||||
read_timeout=30.0,
|
||||
max_retries=3,
|
||||
)
|
||||
super().__init__(config)
|
||||
@ -118,6 +118,7 @@ def ask_user_question(
|
||||
if isinstance(options, str):
|
||||
try:
|
||||
import json
|
||||
|
||||
options = json.loads(options)
|
||||
logger.debug(f"Parsed string options to list: {options}")
|
||||
except Exception as e:
|
||||
@ -128,6 +129,7 @@ def ask_user_question(
|
||||
if isinstance(questions, str):
|
||||
try:
|
||||
import json
|
||||
|
||||
questions = json.loads(questions)
|
||||
logger.debug(f"Parsed string questions to list: {questions}")
|
||||
except Exception as e:
|
||||
|
||||
@ -78,9 +78,7 @@ class Config(BaseModel):
|
||||
# Sandbox 配置
|
||||
# ============================================================
|
||||
sandbox_provider: str = Field(default="provisioner", description="沙箱提供者")
|
||||
sandbox_provisioner_url: str = Field(
|
||||
default="http://sandbox-provisioner:8002", description="沙箱服务地址"
|
||||
)
|
||||
sandbox_provisioner_url: str = Field(default="http://sandbox-provisioner:8002", description="沙箱服务地址")
|
||||
sandbox_virtual_path_prefix: str = Field(default="/mnt/user-data", description="沙箱虚拟路径前缀")
|
||||
sandbox_exec_timeout_seconds: int = Field(default=180, description="沙箱执行超时时间(秒)")
|
||||
sandbox_max_output_bytes: int = Field(default=262144, description="沙箱最大输出字节数")
|
||||
@ -247,18 +245,12 @@ class Config(BaseModel):
|
||||
self.valuable_model_provider = [k for k, v in self.model_provider_status.items() if v]
|
||||
|
||||
# 处理 Sandbox 配置
|
||||
self.sandbox_provider = (
|
||||
os.getenv("SANDBOX_PROVIDER") or self.sandbox_provider or "provisioner"
|
||||
).strip()
|
||||
self.sandbox_provider = (os.getenv("SANDBOX_PROVIDER") or self.sandbox_provider or "provisioner").strip()
|
||||
self.sandbox_provisioner_url = (
|
||||
os.getenv("SANDBOX_PROVISIONER_URL")
|
||||
or self.sandbox_provisioner_url
|
||||
or "http://sandbox-provisioner:8002"
|
||||
os.getenv("SANDBOX_PROVISIONER_URL") or self.sandbox_provisioner_url or "http://sandbox-provisioner:8002"
|
||||
).strip()
|
||||
self.sandbox_virtual_path_prefix = (
|
||||
os.getenv("SANDBOX_VIRTUAL_PATH_PREFIX")
|
||||
or self.sandbox_virtual_path_prefix
|
||||
or "/mnt/user-data"
|
||||
os.getenv("SANDBOX_VIRTUAL_PATH_PREFIX") or self.sandbox_virtual_path_prefix or "/mnt/user-data"
|
||||
).strip()
|
||||
self.sandbox_exec_timeout_seconds = int(
|
||||
os.getenv("SANDBOX_EXEC_TIMEOUT_SECONDS") or self.sandbox_exec_timeout_seconds or 180
|
||||
@ -267,9 +259,7 @@ class Config(BaseModel):
|
||||
os.getenv("SANDBOX_MAX_OUTPUT_BYTES") or self.sandbox_max_output_bytes or 262144
|
||||
)
|
||||
self.sandbox_keepalive_interval_seconds = int(
|
||||
os.getenv("SANDBOX_KEEPALIVE_INTERVAL_SECONDS")
|
||||
or self.sandbox_keepalive_interval_seconds
|
||||
or 30
|
||||
os.getenv("SANDBOX_KEEPALIVE_INTERVAL_SECONDS") or self.sandbox_keepalive_interval_seconds or 30
|
||||
)
|
||||
|
||||
# 验证 Sandbox 配置
|
||||
|
||||
@ -4,8 +4,8 @@ import base64
|
||||
from pathlib import PurePosixPath
|
||||
from typing import Any
|
||||
|
||||
from deepagents.backends.sandbox import BaseSandbox
|
||||
from deepagents.backends.protocol import ExecuteResponse, FileDownloadResponse, FileUploadResponse
|
||||
from deepagents.backends.sandbox import BaseSandbox
|
||||
|
||||
from src import config as conf
|
||||
from src.utils.logging_config import logger
|
||||
@ -156,10 +156,7 @@ class ProvisionerSandboxBackend(BaseSandbox):
|
||||
if not selected_lines:
|
||||
return ""
|
||||
|
||||
return "\n".join(
|
||||
f"{start + idx + 1:6d}\t{line}"
|
||||
for idx, line in enumerate(selected_lines)
|
||||
)
|
||||
return "\n".join(f"{start + idx + 1:6d}\t{line}" for idx, line in enumerate(selected_lines))
|
||||
|
||||
def execute(self, command: str) -> ExecuteResponse:
|
||||
try:
|
||||
|
||||
@ -136,8 +136,7 @@ class ProvisionerSandboxProvider:
|
||||
self._client.delete(connection.sandbox_id)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
logger.warning(
|
||||
f"Failed to release sandbox {connection.sandbox_id} "
|
||||
f"for thread {connection.thread_id}: {exc}"
|
||||
f"Failed to release sandbox {connection.sandbox_id} for thread {connection.thread_id}: {exc}"
|
||||
)
|
||||
|
||||
|
||||
|
||||
@ -36,10 +36,7 @@ class ProvisionerClient:
|
||||
json={"sandbox_id": sandbox_id, "thread_id": thread_id},
|
||||
)
|
||||
if response.status_code >= 400:
|
||||
raise RuntimeError(
|
||||
f"failed to create sandbox {sandbox_id}: "
|
||||
f"{response.status_code} {response.text}"
|
||||
)
|
||||
raise RuntimeError(f"failed to create sandbox {sandbox_id}: {response.status_code} {response.text}")
|
||||
payload = response.json()
|
||||
return SandboxRecord(
|
||||
sandbox_id=payload["sandbox_id"],
|
||||
@ -52,10 +49,7 @@ class ProvisionerClient:
|
||||
if response.status_code == 404:
|
||||
return None
|
||||
if response.status_code >= 400:
|
||||
raise RuntimeError(
|
||||
f"failed to discover sandbox {sandbox_id}: "
|
||||
f"{response.status_code} {response.text}"
|
||||
)
|
||||
raise RuntimeError(f"failed to discover sandbox {sandbox_id}: {response.status_code} {response.text}")
|
||||
payload = response.json()
|
||||
return SandboxRecord(
|
||||
sandbox_id=payload["sandbox_id"],
|
||||
@ -68,17 +62,11 @@ class ProvisionerClient:
|
||||
if response.status_code == 404:
|
||||
return False
|
||||
if response.status_code >= 400:
|
||||
raise RuntimeError(
|
||||
f"failed to touch sandbox {sandbox_id}: "
|
||||
f"{response.status_code} {response.text}"
|
||||
)
|
||||
raise RuntimeError(f"failed to touch sandbox {sandbox_id}: {response.status_code} {response.text}")
|
||||
return True
|
||||
|
||||
def delete(self, sandbox_id: str) -> None:
|
||||
response = self._request("DELETE", f"/api/sandboxes/{sandbox_id}")
|
||||
if response.status_code in {200, 404}:
|
||||
return
|
||||
raise RuntimeError(
|
||||
f"failed to delete sandbox {sandbox_id}: "
|
||||
f"{response.status_code} {response.text}"
|
||||
)
|
||||
raise RuntimeError(f"failed to delete sandbox {sandbox_id}: {response.status_code} {response.text}")
|
||||
|
||||
@ -9,7 +9,8 @@ from typing import Any
|
||||
from langchain.messages import AIMessage, AIMessageChunk, HumanMessage
|
||||
from langgraph.types import Command
|
||||
|
||||
from src import config as conf, knowledge_base
|
||||
from src import config as conf
|
||||
from src import knowledge_base
|
||||
from src.agents import agent_manager
|
||||
from src.plugins.guard import content_guard
|
||||
from src.repositories.agent_config_repository import AgentConfigRepository
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import shlex
|
||||
import shlex
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
@ -78,8 +78,8 @@ class PostgresManager(metaclass=SingletonMeta):
|
||||
# 创建 LangGraph 专属连接池
|
||||
self.langgraph_pool = AsyncConnectionPool(
|
||||
conninfo=langgraph_db_url,
|
||||
max_size=10, # 根据你的 Agent 并发情况设置,通常 5-10 足够了
|
||||
kwargs={"autocommit": True} # LangGraph Checkpoint 强依赖 autocommit
|
||||
max_size=10, # 根据你的 Agent 并发情况设置,通常 5-10 足够了
|
||||
kwargs={"autocommit": True}, # LangGraph Checkpoint 强依赖 autocommit
|
||||
)
|
||||
|
||||
self._initialized = True
|
||||
|
||||
@ -1,486 +0,0 @@
|
||||
"""
|
||||
Deep Agents Remote Backends
|
||||
|
||||
S3 backend implementations for LangChain's Deep Agents.
|
||||
Supports any S3-compatible storage (AWS S3, MinIO, etc.)
|
||||
with connection pooling for optimal performance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import fnmatch
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
from contextlib import asynccontextmanager
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import PurePosixPath
|
||||
from typing import TYPE_CHECKING, Any, AsyncIterator, Coroutine
|
||||
|
||||
import aioboto3
|
||||
import wcmatch.glob as wcglob
|
||||
from botocore.config import Config as BotoConfig
|
||||
from botocore.exceptions import ClientError
|
||||
from deepagents.backends.protocol import (
|
||||
BackendProtocol,
|
||||
EditResult,
|
||||
FileDownloadResponse,
|
||||
FileInfo,
|
||||
FileUploadResponse,
|
||||
GrepMatch,
|
||||
WriteResult,
|
||||
)
|
||||
from deepagents.backends.utils import (
|
||||
check_empty_content,
|
||||
format_content_with_line_numbers,
|
||||
perform_string_replacement,
|
||||
)
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from types_aiobotocore_s3 import S3Client
|
||||
|
||||
__all__ = ["S3Backend", "S3Config"]
|
||||
|
||||
|
||||
def run_async_safely[T](coroutine: Coroutine[Any, Any, T], timeout: float | None = None) -> T:
|
||||
try:
|
||||
asyncio.get_running_loop()
|
||||
except RuntimeError:
|
||||
return asyncio.run(coroutine)
|
||||
|
||||
result: dict[str, T] = {}
|
||||
error: dict[str, Exception] = {}
|
||||
|
||||
def _run() -> None:
|
||||
try:
|
||||
result["value"] = asyncio.run(coroutine)
|
||||
except Exception as exc: # noqa: BLE001
|
||||
error["value"] = exc
|
||||
|
||||
thread = threading.Thread(target=_run, daemon=True)
|
||||
thread.start()
|
||||
thread.join(timeout)
|
||||
|
||||
if thread.is_alive():
|
||||
raise TimeoutError("Timed out while waiting for coroutine result")
|
||||
if "value" in error:
|
||||
raise error["value"]
|
||||
|
||||
return result["value"]
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# S3 Backend (S3-compatible: AWS S3, MinIO, etc.)
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@dataclass
|
||||
class S3Config:
|
||||
"""Configuration for S3-compatible storage."""
|
||||
|
||||
bucket: str
|
||||
prefix: str = ""
|
||||
region: str = "us-east-1"
|
||||
endpoint_url: str | None = None
|
||||
access_key_id: str | None = None
|
||||
secret_access_key: str | None = None
|
||||
use_ssl: bool = True
|
||||
max_pool_connections: int = 50
|
||||
connect_timeout: float = 5.0
|
||||
read_timeout: float = 30.0
|
||||
max_retries: int = 3
|
||||
|
||||
|
||||
class S3Backend(BackendProtocol):
|
||||
"""
|
||||
S3-compatible backend for Deep Agents file operations.
|
||||
|
||||
Supports AWS S3, MinIO, and any S3-compatible object storage.
|
||||
All operations are async-native using aioboto3.
|
||||
|
||||
Files are stored as objects with paths mapping to S3 keys.
|
||||
Content is stored as JSON with the structure:
|
||||
{"content": [...lines], "created_at": "...", "modified_at": "..."}
|
||||
"""
|
||||
|
||||
def __init__(self, config: S3Config) -> None:
|
||||
self._config = config
|
||||
self._prefix = config.prefix.strip("/")
|
||||
if self._prefix:
|
||||
self._prefix += "/"
|
||||
|
||||
self._boto_config = BotoConfig(
|
||||
region_name=config.region,
|
||||
signature_version="s3v4",
|
||||
retries={"max_attempts": config.max_retries, "mode": "adaptive"},
|
||||
max_pool_connections=config.max_pool_connections,
|
||||
connect_timeout=config.connect_timeout,
|
||||
read_timeout=config.read_timeout,
|
||||
)
|
||||
|
||||
session_kwargs: dict[str, Any] = {}
|
||||
if config.access_key_id:
|
||||
session_kwargs["aws_access_key_id"] = config.access_key_id
|
||||
if config.secret_access_key:
|
||||
session_kwargs["aws_secret_access_key"] = config.secret_access_key
|
||||
|
||||
self._session = aioboto3.Session(**session_kwargs)
|
||||
self._bucket = config.bucket
|
||||
|
||||
def _s3_key(self, path: str) -> str:
|
||||
"""Convert virtual path to S3 key."""
|
||||
clean = path.lstrip("/")
|
||||
return f"{self._prefix}{clean}"
|
||||
|
||||
def _virtual_path(self, key: str) -> str:
|
||||
"""Convert S3 key to virtual path."""
|
||||
if self._prefix and key.startswith(self._prefix):
|
||||
key = key[len(self._prefix) :]
|
||||
return "/" + key.lstrip("/")
|
||||
|
||||
@asynccontextmanager
|
||||
async def _client(self) -> AsyncIterator["S3Client"]:
|
||||
"""Get S3 client context."""
|
||||
async with self._session.client(
|
||||
"s3",
|
||||
config=self._boto_config,
|
||||
endpoint_url=self._config.endpoint_url,
|
||||
use_ssl=self._config.use_ssl,
|
||||
) as client:
|
||||
yield client
|
||||
|
||||
async def _get_file_data(self, path: str) -> dict[str, Any] | None:
|
||||
"""Get file data dict from S3."""
|
||||
key = self._s3_key(path)
|
||||
try:
|
||||
async with self._client() as client:
|
||||
response = await client.get_object(Bucket=self._bucket, Key=key)
|
||||
async with response["Body"] as stream:
|
||||
content = await stream.read()
|
||||
return json.loads(content.decode("utf-8"))
|
||||
except ClientError as e:
|
||||
if e.response["Error"]["Code"] == "NoSuchKey":
|
||||
return None
|
||||
raise
|
||||
|
||||
async def _put_file_data(
|
||||
self, path: str, data: dict[str, Any], *, update_modified: bool = True
|
||||
) -> None:
|
||||
"""Put file data dict to S3."""
|
||||
key = self._s3_key(path)
|
||||
if update_modified:
|
||||
data["modified_at"] = datetime.now(timezone.utc).isoformat()
|
||||
content = json.dumps(data).encode("utf-8")
|
||||
async with self._client() as client:
|
||||
await client.put_object(
|
||||
Bucket=self._bucket,
|
||||
Key=key,
|
||||
Body=content,
|
||||
ContentType="application/json",
|
||||
)
|
||||
|
||||
async def _exists(self, path: str) -> bool:
|
||||
"""Check if file exists in S3."""
|
||||
key = self._s3_key(path)
|
||||
try:
|
||||
async with self._client() as client:
|
||||
await client.head_object(Bucket=self._bucket, Key=key)
|
||||
return True
|
||||
except ClientError as e:
|
||||
if e.response["Error"]["Code"] == "404":
|
||||
return False
|
||||
raise
|
||||
|
||||
async def _list_keys(self, prefix: str = "") -> list[dict[str, Any]]:
|
||||
"""List all keys with a prefix."""
|
||||
full_prefix = self._s3_key(prefix)
|
||||
results: list[dict[str, Any]] = []
|
||||
async with self._client() as client:
|
||||
paginator = client.get_paginator("list_objects_v2")
|
||||
async for page in paginator.paginate(
|
||||
Bucket=self._bucket, Prefix=full_prefix
|
||||
):
|
||||
for obj in page.get("Contents", []):
|
||||
results.append(obj)
|
||||
return results
|
||||
|
||||
# -------------------------------------------------------------------------
|
||||
# BackendProtocol Implementation
|
||||
# -------------------------------------------------------------------------
|
||||
|
||||
def ls_info(self, path: str) -> list[FileInfo]:
|
||||
"""Sync wrapper for als_info."""
|
||||
return run_async_safely(self.als_info(path))
|
||||
|
||||
async def als_info(self, path: str) -> list[FileInfo]:
|
||||
"""List files in a directory."""
|
||||
prefix = path.lstrip("/")
|
||||
if prefix and not prefix.endswith("/"):
|
||||
prefix += "/"
|
||||
|
||||
objects = await self._list_keys(prefix)
|
||||
results: list[FileInfo] = []
|
||||
seen_dirs: set[str] = set()
|
||||
|
||||
for obj in objects:
|
||||
key = obj["Key"]
|
||||
vpath = self._virtual_path(key)
|
||||
|
||||
# Check if this is a direct child or nested
|
||||
rel = vpath[len("/" + prefix) :] if prefix else vpath[1:]
|
||||
if "/" in rel:
|
||||
# This is in a subdirectory, add the directory entry
|
||||
dir_name = rel.split("/")[0]
|
||||
dir_path = "/" + prefix + dir_name + "/"
|
||||
if dir_path not in seen_dirs:
|
||||
seen_dirs.add(dir_path)
|
||||
results.append({"path": dir_path, "is_dir": True})
|
||||
else:
|
||||
# Direct file
|
||||
results.append(
|
||||
{
|
||||
"path": vpath,
|
||||
"is_dir": False,
|
||||
"size": obj.get("Size", 0),
|
||||
"modified_at": obj["LastModified"].isoformat()
|
||||
if "LastModified" in obj
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
results.sort(key=lambda x: x.get("path", ""))
|
||||
return results
|
||||
|
||||
def read(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
||||
"""Sync wrapper for aread."""
|
||||
return run_async_safely(
|
||||
self.aread(file_path, offset, limit)
|
||||
)
|
||||
|
||||
async def aread(self, file_path: str, offset: int = 0, limit: int = 2000) -> str:
|
||||
"""Read file content with line numbers."""
|
||||
data = await self._get_file_data(file_path)
|
||||
if data is None:
|
||||
return f"Error: File '{file_path}' not found"
|
||||
|
||||
lines = data.get("content", [])
|
||||
if not lines:
|
||||
empty_msg = check_empty_content("")
|
||||
if empty_msg:
|
||||
return empty_msg
|
||||
|
||||
if offset >= len(lines):
|
||||
return f"Error: Line offset {offset} exceeds file length ({len(lines)} lines)"
|
||||
|
||||
selected = lines[offset : offset + limit]
|
||||
return format_content_with_line_numbers(selected, start_line=offset + 1)
|
||||
|
||||
def write(self, file_path: str, content: str) -> WriteResult:
|
||||
"""Sync wrapper for awrite."""
|
||||
return run_async_safely(
|
||||
self.awrite(file_path, content)
|
||||
)
|
||||
|
||||
async def awrite(self, file_path: str, content: str) -> WriteResult:
|
||||
"""Create a new file."""
|
||||
if await self._exists(file_path):
|
||||
return WriteResult(
|
||||
error=f"Cannot write to {file_path} because it already exists. "
|
||||
"Read and then make an edit, or write to a new path."
|
||||
)
|
||||
|
||||
now = datetime.now(timezone.utc).isoformat()
|
||||
data = {
|
||||
"content": content.splitlines(),
|
||||
"created_at": now,
|
||||
"modified_at": now,
|
||||
}
|
||||
try:
|
||||
await self._put_file_data(file_path, data, update_modified=False)
|
||||
return WriteResult(path=file_path, files_update=None)
|
||||
except Exception as e:
|
||||
return WriteResult(error=f"Error writing file '{file_path}': {e}")
|
||||
|
||||
def edit(
|
||||
self,
|
||||
file_path: str,
|
||||
old_string: str,
|
||||
new_string: str,
|
||||
replace_all: bool = False,
|
||||
) -> EditResult:
|
||||
"""Sync wrapper for aedit."""
|
||||
return run_async_safely(
|
||||
self.aedit(file_path, old_string, new_string, replace_all)
|
||||
)
|
||||
|
||||
async def aedit(
|
||||
self,
|
||||
file_path: str,
|
||||
old_string: str,
|
||||
new_string: str,
|
||||
replace_all: bool = False,
|
||||
) -> EditResult:
|
||||
"""Edit file by replacing strings."""
|
||||
data = await self._get_file_data(file_path)
|
||||
if data is None:
|
||||
return EditResult(error=f"Error: File '{file_path}' not found")
|
||||
|
||||
content = "\n".join(data.get("content", []))
|
||||
result = perform_string_replacement(content, old_string, new_string, replace_all)
|
||||
|
||||
if isinstance(result, str):
|
||||
return EditResult(error=result)
|
||||
|
||||
new_content, occurrences = result
|
||||
data["content"] = new_content.splitlines()
|
||||
|
||||
try:
|
||||
await self._put_file_data(file_path, data)
|
||||
return EditResult(
|
||||
path=file_path, files_update=None, occurrences=int(occurrences)
|
||||
)
|
||||
except Exception as e:
|
||||
return EditResult(error=f"Error editing file '{file_path}': {e}")
|
||||
|
||||
def grep_raw(
|
||||
self, pattern: str, path: str | None = None, glob: str | None = None
|
||||
) -> list[GrepMatch] | str:
|
||||
"""Sync wrapper for agrep_raw."""
|
||||
return run_async_safely(
|
||||
self.agrep_raw(pattern, path, glob)
|
||||
)
|
||||
|
||||
async def agrep_raw(
|
||||
self, pattern: str, path: str | None = None, glob: str | None = None
|
||||
) -> list[GrepMatch] | str:
|
||||
"""Search for pattern in files."""
|
||||
try:
|
||||
regex = re.compile(pattern)
|
||||
except re.error as e:
|
||||
return f"Invalid regex pattern: {e}"
|
||||
|
||||
search_prefix = (path or "/").lstrip("/")
|
||||
objects = await self._list_keys(search_prefix)
|
||||
matches: list[GrepMatch] = []
|
||||
|
||||
for obj in objects:
|
||||
vpath = self._virtual_path(obj["Key"])
|
||||
filename = PurePosixPath(vpath).name
|
||||
|
||||
if glob and not wcglob.globmatch(filename, glob, flags=wcglob.BRACE):
|
||||
continue
|
||||
|
||||
data = await self._get_file_data(vpath)
|
||||
if data is None:
|
||||
continue
|
||||
|
||||
for line_num, line in enumerate(data.get("content", []), 1):
|
||||
if regex.search(line):
|
||||
matches.append({"path": vpath, "line": line_num, "text": line})
|
||||
|
||||
return matches
|
||||
|
||||
def glob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
||||
"""Sync wrapper for aglob_info."""
|
||||
return run_async_safely(
|
||||
self.aglob_info(pattern, path)
|
||||
)
|
||||
|
||||
async def aglob_info(self, pattern: str, path: str = "/") -> list[FileInfo]:
|
||||
"""Find files matching a glob pattern."""
|
||||
search_prefix = path.lstrip("/")
|
||||
objects = await self._list_keys(search_prefix)
|
||||
results: list[FileInfo] = []
|
||||
|
||||
for obj in objects:
|
||||
vpath = self._virtual_path(obj["Key"])
|
||||
rel_path = vpath[len(path) :].lstrip("/") if path != "/" else vpath[1:]
|
||||
|
||||
if fnmatch.fnmatch(rel_path, pattern) or fnmatch.fnmatch(vpath, pattern):
|
||||
results.append(
|
||||
{
|
||||
"path": vpath,
|
||||
"is_dir": False,
|
||||
"size": obj.get("Size", 0),
|
||||
"modified_at": obj["LastModified"].isoformat()
|
||||
if "LastModified" in obj
|
||||
else None,
|
||||
}
|
||||
)
|
||||
|
||||
results.sort(key=lambda x: x.get("path", ""))
|
||||
return results
|
||||
|
||||
def upload_files(self, files: list[tuple[str, bytes]]) -> list[FileUploadResponse]:
|
||||
"""Sync wrapper for aupload_files."""
|
||||
return run_async_safely(self.aupload_files(files))
|
||||
|
||||
async def aupload_files(
|
||||
self, files: list[tuple[str, bytes]]
|
||||
) -> list[FileUploadResponse]:
|
||||
"""Upload multiple files."""
|
||||
responses: list[FileUploadResponse] = []
|
||||
async with self._client() as client:
|
||||
for path, content in files:
|
||||
try:
|
||||
key = self._s3_key(path)
|
||||
await client.put_object(
|
||||
Bucket=self._bucket, Key=key, Body=content
|
||||
)
|
||||
responses.append(FileUploadResponse(path=path, error=None))
|
||||
except ClientError as e:
|
||||
code = e.response["Error"]["Code"]
|
||||
if code == "AccessDenied":
|
||||
responses.append(
|
||||
FileUploadResponse(path=path, error="permission_denied")
|
||||
)
|
||||
else:
|
||||
responses.append(
|
||||
FileUploadResponse(path=path, error="invalid_path")
|
||||
)
|
||||
except Exception:
|
||||
responses.append(
|
||||
FileUploadResponse(path=path, error="invalid_path")
|
||||
)
|
||||
return responses
|
||||
|
||||
def download_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
||||
"""Sync wrapper for adownload_files."""
|
||||
return run_async_safely(self.adownload_files(paths))
|
||||
|
||||
async def adownload_files(self, paths: list[str]) -> list[FileDownloadResponse]:
|
||||
"""Download multiple files."""
|
||||
responses: list[FileDownloadResponse] = []
|
||||
async with self._client() as client:
|
||||
for path in paths:
|
||||
try:
|
||||
key = self._s3_key(path)
|
||||
response = await client.get_object(Bucket=self._bucket, Key=key)
|
||||
async with response["Body"] as stream:
|
||||
content = await stream.read()
|
||||
responses.append(
|
||||
FileDownloadResponse(path=path, content=content, error=None)
|
||||
)
|
||||
except ClientError as e:
|
||||
code = e.response["Error"]["Code"]
|
||||
if code == "NoSuchKey":
|
||||
responses.append(
|
||||
FileDownloadResponse(
|
||||
path=path, content=None, error="file_not_found"
|
||||
)
|
||||
)
|
||||
elif code == "AccessDenied":
|
||||
responses.append(
|
||||
FileDownloadResponse(
|
||||
path=path, content=None, error="permission_denied"
|
||||
)
|
||||
)
|
||||
else:
|
||||
responses.append(
|
||||
FileDownloadResponse(
|
||||
path=path, content=None, error="invalid_path"
|
||||
)
|
||||
)
|
||||
return responses
|
||||
182
uv.lock
182
uv.lock
@ -29,6 +29,56 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7e/46/02ac5e262d4af18054b3e922b2baedbb2a03289ee792162de60a865defc5/accelerate-1.13.0-py3-none-any.whl", hash = "sha256:cf1a3efb96c18f7b152eb0fa7490f3710b19c3f395699358f08decca2b8b62e0", size = 383744, upload-time = "2026-03-04T19:34:10.313Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-sandbox"
|
||||
version = "0.0.26"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "httpx", extra = ["socks"] },
|
||||
{ name = "pydantic" },
|
||||
{ name = "volcengine-python-sdk" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/3f/68/931c61b5e28ee344b5c2301bf56739f7fc90767da007c9e44a9b0a1ea937/agent_sandbox-0.0.26.tar.gz", hash = "sha256:67ec87e58794d017f6be321fc46913d76e84453d0cfe426a5f4b9bf8f5bd619b", size = 98635, upload-time = "2026-03-02T08:47:24.853Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/26/b5/2748e86137ee757b749d2fb7f5504e039f780c9346be798870ae01ee1559/agent_sandbox-0.0.26-py2.py3-none-any.whl", hash = "sha256:6421fc4eb6144f10ddfdd2668c6644e132056eaeb6ab89d76a0b2e4bcf327a0a", size = 216489, upload-time = "2026-03-02T08:47:23.351Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aioboto3"
|
||||
version = "15.5.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiobotocore", extra = ["boto3"] },
|
||||
{ name = "aiofiles" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/a2/01/92e9ab00f36e2899315f49eefcd5b4685fbb19016c7f19a9edf06da80bb0/aioboto3-15.5.0.tar.gz", hash = "sha256:ea8d8787d315594842fbfcf2c4dce3bac2ad61be275bc8584b2ce9a3402a6979", size = 255069, upload-time = "2025-10-30T13:37:16.122Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e5/3e/e8f5b665bca646d43b916763c901e00a07e40f7746c9128bdc912a089424/aioboto3-15.5.0-py3-none-any.whl", hash = "sha256:cc880c4d6a8481dd7e05da89f41c384dbd841454fc1998ae25ca9c39201437a6", size = 35913, upload-time = "2025-10-30T13:37:14.549Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiobotocore"
|
||||
version = "2.25.1"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "aiohttp" },
|
||||
{ name = "aioitertools" },
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "multidict" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "wrapt" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/94/2e4ec48cf1abb89971cb2612d86f979a6240520f0a659b53a43116d344dc/aiobotocore-2.25.1.tar.gz", hash = "sha256:ea9be739bfd7ece8864f072ec99bb9ed5c7e78ebb2b0b15f29781fbe02daedbc", size = 120560, upload-time = "2025-10-28T22:33:21.787Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/2a/d275ec4ce5cd0096665043995a7d76f5d0524853c76a3d04656de49f8808/aiobotocore-2.25.1-py3-none-any.whl", hash = "sha256:eb6daebe3cbef5b39a0bb2a97cffbe9c7cb46b2fcc399ad141f369f3c2134b1f", size = 86039, upload-time = "2025-10-28T22:33:19.949Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
boto3 = [
|
||||
{ name = "boto3" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiofiles"
|
||||
version = "25.1.0"
|
||||
@ -98,6 +148,15 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/4e/f1/ab0395f8a79933577cdd996dd2f9aa6014af9535f65dddcf88204682fe62/aiohttp-3.13.3-cp313-cp313-win_amd64.whl", hash = "sha256:693781c45a4033d31d4187d2436f5ac701e7bbfe5df40d917736108c1cc7436e", size = 453899, upload-time = "2026-01-03T17:31:15.958Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aioitertools"
|
||||
version = "0.13.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/fd/3c/53c4a17a05fb9ea2313ee1777ff53f5e001aefd5cc85aa2f4c2d982e1e38/aioitertools-0.13.0.tar.gz", hash = "sha256:620bd241acc0bbb9ec819f1ab215866871b4bbd1f73836a55f799200ee86950c", size = 19322, upload-time = "2025-11-06T22:17:07.609Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/10/a1/510b0a7fadc6f43a6ce50152e69dbd86415240835868bb0bd9b5b88b1e06/aioitertools-0.13.0-py3-none-any.whl", hash = "sha256:0be0292b856f08dfac90e31f4739432f4cb6d7520ab9eb73e143f4f2fa5259be", size = 24182, upload-time = "2025-11-06T22:17:06.502Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.4.0"
|
||||
@ -334,6 +393,34 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/95/c1/84fc6811122f54b20de2e5afb312ee07a3a47a328755587d1e505475239b/blockbuster-1.5.26-py3-none-any.whl", hash = "sha256:f8e53fb2dd4b6c6ec2f04907ddbd063ca7cd1ef587d24448ef4e50e81e3a79bb", size = 13226, upload-time = "2025-12-05T10:43:48.778Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "boto3"
|
||||
version = "1.40.61"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
{ name = "jmespath" },
|
||||
{ name = "s3transfer" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ed/f9/6ef8feb52c3cce5ec3967a535a6114b57ac7949fd166b0f3090c2b06e4e5/boto3-1.40.61.tar.gz", hash = "sha256:d6c56277251adf6c2bdd25249feae625abe4966831676689ff23b4694dea5b12", size = 111535, upload-time = "2025-10-28T19:26:57.247Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/61/24/3bf865b07d15fea85b63504856e137029b6acbc73762496064219cdb265d/boto3-1.40.61-py3-none-any.whl", hash = "sha256:6b9c57b2a922b5d8c17766e29ed792586a818098efe84def27c8f582b33f898c", size = 139321, upload-time = "2025-10-28T19:26:55.007Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "botocore"
|
||||
version = "1.40.61"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "jmespath" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/28/a3/81d3a47c2dbfd76f185d3b894f2ad01a75096c006a2dd91f237dca182188/botocore-1.40.61.tar.gz", hash = "sha256:a2487ad69b090f9cccd64cf07c7021cd80ee9c0655ad974f87045b02f3ef52cd", size = 14393956, upload-time = "2025-10-28T19:26:46.108Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/38/c5/f6ce561004db45f0b847c2cd9b19c67c6bf348a82018a48cb718be6b58b0/botocore-1.40.61-py3-none-any.whl", hash = "sha256:17ebae412692fd4824f99cde0f08d50126dc97954008e5ba2b522eb049238aa7", size = 14055973, upload-time = "2025-10-28T19:26:42.15Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bracex"
|
||||
version = "2.6"
|
||||
@ -1408,6 +1495,11 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
socks = [
|
||||
{ name = "socksio" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "httpx-sse"
|
||||
version = "0.4.3"
|
||||
@ -1530,6 +1622,15 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/67/8a/a342b2f0251f3dac4ca17618265d93bf244a2a4d089126e81e4c1056ac50/jiter-0.13.0-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7bb00b6d26db67a05fe3e12c76edc75f32077fb51deed13822dc648fa373bc19", size = 343768, upload-time = "2026-02-02T12:37:55.055Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "jmespath"
|
||||
version = "1.1.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/d3/59/322338183ecda247fb5d1763a6cbe46eff7222eaeebafd9fa65d4bf5cb11/jmespath-1.1.0.tar.gz", hash = "sha256:472c87d80f36026ae83c6ddd0f1d05d4e510134ed462851fd5f754c8c3cbb88d", size = 27377, upload-time = "2026-01-22T16:35:26.279Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "joblib"
|
||||
version = "1.5.3"
|
||||
@ -3302,6 +3403,43 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c8/5b/181e2e3becb7672b502f0ed7f16ed7352aca7c109cfb94cf3878a9186db9/psycopg-3.3.3-py3-none-any.whl", hash = "sha256:f96525a72bcfade6584ab17e89de415ff360748c766f0106959144dcbb38c698", size = 212768, upload-time = "2026-02-18T16:46:27.365Z" },
|
||||
]
|
||||
|
||||
[package.optional-dependencies]
|
||||
binary = [
|
||||
{ name = "psycopg-binary", marker = "implementation_name != 'pypy'" },
|
||||
]
|
||||
pool = [
|
||||
{ name = "psycopg-pool" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-binary"
|
||||
version = "3.3.3"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/90/15/021be5c0cbc5b7c1ab46e91cc3434eb42569f79a0592e67b8d25e66d844d/psycopg_binary-3.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6698dbab5bcef8fdb570fc9d35fd9ac52041771bfcfe6fd0fc5f5c4e36f1e99d", size = 4591170, upload-time = "2026-02-18T16:48:55.594Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f1/54/a60211c346c9a2f8c6b272b5f2bbe21f6e11800ce7f61e99ba75cf8b63e1/psycopg_binary-3.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:329ff393441e75f10b673ae99ab45276887993d49e65f141da20d915c05aafd8", size = 4670009, upload-time = "2026-02-18T16:49:03.608Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c1/53/ac7c18671347c553362aadbf65f92786eef9540676ca24114cc02f5be405/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:eb072949b8ebf4082ae24289a2b0fd724da9adc8f22743409d6fd718ddb379df", size = 5469735, upload-time = "2026-02-18T16:49:10.128Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/c3/4f4e040902b82a344eff1c736cde2f2720f127fe939c7e7565706f96dd44/psycopg_binary-3.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:263a24f39f26e19ed7fc982d7859a36f17841b05bebad3eb47bb9cd2dd785351", size = 5152919, upload-time = "2026-02-18T16:49:16.335Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/0c/e7/d929679c6a5c212bcf738806c7c89f5b3d0919f2e1685a0e08d6ff877945/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5152d50798c2fa5bd9b68ec68eb68a1b71b95126c1d70adaa1a08cd5eefdc23d", size = 6738785, upload-time = "2026-02-18T16:49:22.687Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/69/b0/09703aeb69a9443d232d7b5318d58742e8ca51ff79f90ffe6b88f1db45e7/psycopg_binary-3.3.3-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9d6a1e56dd267848edb824dbeb08cf5bac649e02ee0b03ba883ba3f4f0bd54f2", size = 4979008, upload-time = "2026-02-18T16:49:27.313Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/cc/a6/e662558b793c6e13a7473b970fee327d635270e41eded3090ef14045a6a5/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73eaaf4bb04709f545606c1db2f65f4000e8a04cdbf3e00d165a23004692093e", size = 4508255, upload-time = "2026-02-18T16:49:31.575Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/5f/7f/0f8b2e1d5e0093921b6f324a948a5c740c1447fbb45e97acaf50241d0f39/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:162e5675efb4704192411eaf8e00d07f7960b679cd3306e7efb120bb8d9456cc", size = 4189166, upload-time = "2026-02-18T16:49:35.801Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/92/ec/ce2e91c33bc8d10b00c87e2f6b0fb570641a6a60042d6a9ae35658a3a797/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:fab6b5e37715885c69f5d091f6ff229be71e235f272ebaa35158d5a46fd548a0", size = 3924544, upload-time = "2026-02-18T16:49:41.129Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/c5/2f/7718141485f73a924205af60041c392938852aa447a94c8cbd222ff389a1/psycopg_binary-3.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a4aab31bd6d1057f287c96c0effca3a25584eb9cc702f282ecb96ded7814e830", size = 4235297, upload-time = "2026-02-18T16:49:46.726Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/57/f9/1add717e2643a003bbde31b1b220172e64fbc0cb09f06429820c9173f7fc/psycopg_binary-3.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:59aa31fe11a0e1d1bcc2ce37ed35fe2ac84cd65bb9036d049b1a1c39064d0f14", size = 3547659, upload-time = "2026-02-18T16:49:52.999Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/03/0a/cac9fdf1df16a269ba0e5f0f06cac61f826c94cadb39df028cdfe19d3a33/psycopg_binary-3.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:05f32239aec25c5fb15f7948cffdc2dc0dac098e48b80a140e4ba32b572a2e7d", size = 4590414, upload-time = "2026-02-18T16:50:01.441Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/c0/d8f8508fbf440edbc0099b1abff33003cd80c9e66eb3a1e78834e3fb4fb9/psycopg_binary-3.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c84f9d214f2d1de2fafebc17fa68ac3f6561a59e291553dfc45ad299f4898c1", size = 4669021, upload-time = "2026-02-18T16:50:08.803Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/04/05/097016b77e343b4568feddf12c72171fc513acef9a4214d21b9478569068/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e77957d2ba17cada11be09a5066d93026cdb61ada7c8893101d7fe1c6e1f3925", size = 5467453, upload-time = "2026-02-18T16:50:14.985Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/91/23/73244e5feb55b5ca109cede6e97f32ef45189f0fdac4c80d75c99862729d/psycopg_binary-3.3.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:42961609ac07c232a427da7c87a468d3c82fee6762c220f38e37cfdacb2b178d", size = 5151135, upload-time = "2026-02-18T16:50:24.82Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/11/49/5309473b9803b207682095201d8708bbc7842ddf3f192488a69204e36455/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ae07a3114313dd91fce686cab2f4c44af094398519af0e0f854bc707e1aeedf1", size = 6737315, upload-time = "2026-02-18T16:50:35.106Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/d4/5d/03abe74ef34d460b33c4d9662bf6ec1dd38888324323c1a1752133c10377/psycopg_binary-3.3.3-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d257c58d7b36a621dcce1d01476ad8b60f12d80eb1406aee4cf796f88b2ae482", size = 4979783, upload-time = "2026-02-18T16:50:42.067Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f0/6c/3fbf8e604e15f2f3752900434046c00c90bb8764305a1b81112bff30ba24/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:07c7211f9327d522c9c47560cae00a4ecf6687f4e02d779d035dd3177b41cb12", size = 4509023, upload-time = "2026-02-18T16:50:50.116Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/9c/6b/1a06b43b7c7af756c80b67eac8bfaa51d77e68635a8a8d246e4f0bb7604a/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:8e7e9eca9b363dbedeceeadd8be97149d2499081f3c52d141d7cd1f395a91f83", size = 4185874, upload-time = "2026-02-18T16:50:55.97Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2b/d3/bf49e3dcaadba510170c8d111e5e69e5ae3f981c1554c5bb71c75ce354bb/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:cb85b1d5702877c16f28d7b92ba030c1f49ebcc9b87d03d8c10bf45a2f1c7508", size = 3925668, upload-time = "2026-02-18T16:51:03.299Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/92/0aac830ed6a944fe334404e1687a074e4215630725753f0e3e9a9a595b62/psycopg_binary-3.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4d4606c84d04b80f9138d72f1e28c6c02dc5ae0c7b8f3f8aaf89c681ce1cd1b1", size = 4234973, upload-time = "2026-02-18T16:51:09.097Z" },
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/2e/96/102244653ee5a143ece5afe33f00f52fe64e389dfce8dbc87580c6d70d3d/psycopg_binary-3.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:74eae563166ebf74e8d950ff359be037b85723d99ca83f57d9b244a871d6c13b", size = 3551342, upload-time = "2026-02-18T16:51:13.892Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "psycopg-pool"
|
||||
version = "3.3.0"
|
||||
@ -4199,6 +4337,18 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/7f/d0/578c47dd68152ddddddf31cd7fc67dc30b7cdf639a86275fda821b0d9d98/ruff-0.15.6-py3-none-win_arm64.whl", hash = "sha256:c34de3dd0b0ba203be50ae70f5910b17188556630e2178fd7d79fc030eb0d837", size = 11060497, upload-time = "2026-03-12T23:05:25.968Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.14.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "botocore" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/62/74/8d69dcb7a9efe8baa2046891735e5dfe433ad558ae23d9e3c14c633d1d58/s3transfer-0.14.0.tar.gz", hash = "sha256:eff12264e7c8b4985074ccce27a3b38a485bb7f7422cc8046fee9be4983e4125", size = 151547, upload-time = "2025-09-09T19:23:31.089Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/48/f0/ae7ca09223a81a1d890b2557186ea015f6e0502e9b8cb8e1813f1d8cfa4e/s3transfer-0.14.0-py3-none-any.whl", hash = "sha256:ea3b790c7077558ed1f02a3072fb3cb992bbbd253392f4b6e9e8976941c7d456", size = 85712, upload-time = "2025-09-09T19:23:30.041Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "safetensors"
|
||||
version = "0.7.0"
|
||||
@ -4366,6 +4516,15 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "socksio"
|
||||
version = "1.0.0"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055, upload-time = "2020-04-17T15:50:34.664Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763, upload-time = "2020-04-17T15:50:31.878Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "soupsieve"
|
||||
version = "2.8.3"
|
||||
@ -5172,6 +5331,21 @@ wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/f8/ba/d69adbe699b768f6b29a5eec7b47dd610bd17a69de51b251126a801369ea/uvloop-0.22.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1f38ec5e3f18c8a10ded09742f7fb8de0108796eb673f30ce7762ce1b8550cad", size = 4239051, upload-time = "2025-10-16T22:16:43.224Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "volcengine-python-sdk"
|
||||
version = "5.0.19"
|
||||
source = { registry = "https://pypi.tuna.tsinghua.edu.cn/simple" }
|
||||
dependencies = [
|
||||
{ name = "certifi" },
|
||||
{ name = "python-dateutil" },
|
||||
{ name = "six" },
|
||||
{ name = "urllib3" },
|
||||
]
|
||||
sdist = { url = "https://pypi.tuna.tsinghua.edu.cn/packages/ea/a3/ebd474d14d7b63d9e3c14fc7afa8c8fb816d0d1e295e69f0f2c3a7799c08/volcengine_python_sdk-5.0.19.tar.gz", hash = "sha256:4154e5d1d13603d146c3b972d82433287c8b9e7f8a5f94210280e0ef2875e659", size = 8277541, upload-time = "2026-03-20T09:44:50.835Z" }
|
||||
wheels = [
|
||||
{ url = "https://pypi.tuna.tsinghua.edu.cn/packages/34/84/dd6713358185122ada9341ce44d57cb4c7fedf8a4374334cb6ff7d60b74a/volcengine_python_sdk-5.0.19-py2.py3-none-any.whl", hash = "sha256:95cd4e473d9f59423ee1cbcbe37e0f543448f133a3c1c540d2c6eaf83b526ed2", size = 32545589, upload-time = "2026-03-20T09:44:42.391Z" },
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "wasabi"
|
||||
version = "1.1.3"
|
||||
@ -5490,6 +5664,8 @@ name = "yuxi-know"
|
||||
version = "0.5.3"
|
||||
source = { virtual = "." }
|
||||
dependencies = [
|
||||
{ name = "agent-sandbox" },
|
||||
{ name = "aioboto3" },
|
||||
{ name = "aiofiles" },
|
||||
{ name = "aiohttp" },
|
||||
{ name = "aiosqlite" },
|
||||
@ -5529,6 +5705,7 @@ dependencies = [
|
||||
{ name = "openai" },
|
||||
{ name = "opencv-python-headless" },
|
||||
{ name = "pillow" },
|
||||
{ name = "psycopg", extra = ["binary", "pool"] },
|
||||
{ name = "pyjwt" },
|
||||
{ name = "pymilvus" },
|
||||
{ name = "pymupdf" },
|
||||
@ -5556,6 +5733,7 @@ dependencies = [
|
||||
{ name = "typer" },
|
||||
{ name = "unstructured" },
|
||||
{ name = "uvicorn", extra = ["standard"] },
|
||||
{ name = "wcmatch" },
|
||||
]
|
||||
|
||||
[package.dev-dependencies]
|
||||
@ -5571,6 +5749,8 @@ test = [
|
||||
|
||||
[package.metadata]
|
||||
requires-dist = [
|
||||
{ name = "agent-sandbox", specifier = ">=0.0.26" },
|
||||
{ name = "aioboto3", specifier = ">=13.0.0" },
|
||||
{ name = "aiofiles", specifier = ">=24.1.0" },
|
||||
{ name = "aiohttp", specifier = ">=3.9.0" },
|
||||
{ name = "aiosqlite", specifier = ">=0.20.0" },
|
||||
@ -5610,6 +5790,7 @@ requires-dist = [
|
||||
{ name = "openai", specifier = ">=1.109" },
|
||||
{ name = "opencv-python-headless", specifier = ">=4.11.0.86" },
|
||||
{ name = "pillow", specifier = ">=10.5.0" },
|
||||
{ name = "psycopg", extras = ["binary", "pool"], specifier = ">=3.3.3" },
|
||||
{ name = "pyjwt", specifier = ">=2.8.0" },
|
||||
{ name = "pymilvus", specifier = ">=2.5.8" },
|
||||
{ name = "pymupdf", specifier = ">=1.25.5" },
|
||||
@ -5635,6 +5816,7 @@ requires-dist = [
|
||||
{ name = "typer", specifier = ">=0.16.0" },
|
||||
{ name = "unstructured", specifier = ">=0.17.2" },
|
||||
{ name = "uvicorn", extras = ["standard"], specifier = ">=0.34.2" },
|
||||
{ name = "wcmatch", specifier = ">=8.0.0" },
|
||||
]
|
||||
|
||||
[package.metadata.requires-dev]
|
||||
|
||||
@ -174,7 +174,7 @@ export const agentApi = {
|
||||
/**
|
||||
* 恢复被人工审批中断的对话(流式响应)
|
||||
* @param {string} agentId - 智能体ID
|
||||
* @param {Object} data - 恢复数据 { thread_id, answer: { question_id: answer }, approved }
|
||||
* @param {Object} data - 恢复数据 { thread_id, answer: { question_id: answer }, approved }
|
||||
* @param {Object} options - 可选参数(signal, headers等)
|
||||
* @returns {Promise} - 恢复响应流
|
||||
*/
|
||||
@ -351,7 +351,9 @@ export const threadApi = {
|
||||
* @returns {Promise}
|
||||
*/
|
||||
listThreadFiles: (threadId, path = '/mnt/user-data', recursive = false) =>
|
||||
apiGet(`/api/chat/thread/${threadId}/files?path=${encodeURIComponent(path)}&recursive=${recursive}`),
|
||||
apiGet(
|
||||
`/api/chat/thread/${threadId}/files?path=${encodeURIComponent(path)}&recursive=${recursive}`
|
||||
),
|
||||
|
||||
/**
|
||||
* 读取线程文本文件内容(分页)
|
||||
|
||||
@ -1201,8 +1201,8 @@ const startRunStream = async (threadId, runId, afterSeq = '0') => {
|
||||
}
|
||||
|
||||
const approvalStatuses = ['ask_user_question_required', 'human_approval_required']
|
||||
const isApprovalEvent = approvalStatuses.includes(event) ||
|
||||
approvalStatuses.includes(payload?.chunk?.status)
|
||||
const isApprovalEvent =
|
||||
approvalStatuses.includes(event) || approvalStatuses.includes(payload?.chunk?.status)
|
||||
|
||||
if (isApprovalEvent) {
|
||||
const approvalChunk = payload?.chunk || { status: event, thread_id: threadId }
|
||||
@ -1216,9 +1216,11 @@ const startRunStream = async (threadId, runId, afterSeq = '0') => {
|
||||
ts.activeRunId = null
|
||||
ts.lastRetryableJobTry = null
|
||||
clearActiveRunSnapshot(threadId)
|
||||
fetchThreadMessages({ agentId: currentAgentId.value, threadId, delay: 200 }).finally(() => {
|
||||
handleAgentStateRefresh(threadId)
|
||||
})
|
||||
fetchThreadMessages({ agentId: currentAgentId.value, threadId, delay: 200 }).finally(
|
||||
() => {
|
||||
handleAgentStateRefresh(threadId)
|
||||
}
|
||||
)
|
||||
} else if (ts.activeRunId === runId) {
|
||||
window.setTimeout(() => {
|
||||
if (ts.activeRunId === runId && !ts.runStreamAbortController) {
|
||||
|
||||
@ -1,12 +1,7 @@
|
||||
<template>
|
||||
<div class="attachment-panel">
|
||||
<label class="attachment-upload" :class="{ disabled: disabled || isUploading }">
|
||||
<input
|
||||
type="file"
|
||||
multiple
|
||||
:disabled="disabled || isUploading"
|
||||
@change="handleFileChange"
|
||||
/>
|
||||
<input type="file" multiple :disabled="disabled || isUploading" @change="handleFileChange" />
|
||||
<Paperclip size="14" />
|
||||
<span>{{ isUploading ? '上传中…' : '添加附件' }}</span>
|
||||
</label>
|
||||
|
||||
@ -2,12 +2,7 @@
|
||||
<div class="file-table-container">
|
||||
<div class="panel-header">
|
||||
<div class="upload-btn-group">
|
||||
<a-button
|
||||
type="primary"
|
||||
size="small"
|
||||
class="upload-btn"
|
||||
@click="showAddFilesModal()"
|
||||
>
|
||||
<a-button type="primary" size="small" class="upload-btn" @click="showAddFilesModal()">
|
||||
<FileUp size="14" />
|
||||
上传
|
||||
</a-button>
|
||||
|
||||
@ -20,9 +20,7 @@
|
||||
|
||||
<div v-if="activeQuestion" class="question-block">
|
||||
<div class="approval-header">
|
||||
<h4>
|
||||
{{ activeQuestionIndex + 1 }}. {{ activeQuestion.question }}
|
||||
</h4>
|
||||
<h4>{{ activeQuestionIndex + 1 }}. {{ activeQuestion.question }}</h4>
|
||||
</div>
|
||||
|
||||
<div v-if="activeQuestion.operation" class="approval-operation">
|
||||
@ -77,7 +75,11 @@
|
||||
|
||||
<div class="approval-actions">
|
||||
<button class="btn btn-reject" @click="handleCancel" :disabled="isProcessing">取消</button>
|
||||
<button class="btn btn-approve" @click="handlePrimaryAction" :disabled="isPrimaryButtonDisabled">
|
||||
<button
|
||||
class="btn btn-approve"
|
||||
@click="handlePrimaryAction"
|
||||
:disabled="isPrimaryButtonDisabled"
|
||||
>
|
||||
{{ primaryButtonText }}
|
||||
</button>
|
||||
</div>
|
||||
@ -92,7 +94,11 @@
|
||||
|
||||
<script setup>
|
||||
import { computed, ref, watch } from 'vue'
|
||||
import { isOtherOption, normalizeQuestions, DEFAULT_OTHER_OPTION_VALUE } from '@/utils/questionUtils'
|
||||
import {
|
||||
isOtherOption,
|
||||
normalizeQuestions,
|
||||
DEFAULT_OTHER_OPTION_VALUE
|
||||
} from '@/utils/questionUtils'
|
||||
|
||||
const props = defineProps({
|
||||
visible: { type: Boolean, default: false },
|
||||
|
||||
@ -127,7 +127,9 @@ export function useAgentStreamHandler({
|
||||
currentAgentId: unref(currentAgentId),
|
||||
hasAgentState: !!chunk.agent_state,
|
||||
todoCount: Array.isArray(chunk.agent_state?.todos) ? chunk.agent_state.todos.length : 0,
|
||||
uploadCount: Array.isArray(chunk.agent_state?.uploads) ? chunk.agent_state.uploads.length : 0
|
||||
uploadCount: Array.isArray(chunk.agent_state?.uploads)
|
||||
? chunk.agent_state.uploads.length
|
||||
: 0
|
||||
})
|
||||
if (chunk.agent_state) {
|
||||
console.log(`${debugPrefix}[agent_state_apply]`, {
|
||||
|
||||
@ -28,8 +28,10 @@ const parseApprovedDecision = (answer) => {
|
||||
if (typeof value === 'boolean') return value
|
||||
if (typeof value === 'string') {
|
||||
const normalized = value.trim().toLowerCase()
|
||||
if (normalized === 'approve' || normalized === 'approved' || normalized === 'true') return true
|
||||
if (normalized === 'reject' || normalized === 'rejected' || normalized === 'false') return false
|
||||
if (normalized === 'approve' || normalized === 'approved' || normalized === 'true')
|
||||
return true
|
||||
if (normalized === 'reject' || normalized === 'rejected' || normalized === 'false')
|
||||
return false
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@ -9,8 +9,12 @@ const DEFAULT_OTHER_OPTION_VALUE = '__other__'
|
||||
*/
|
||||
export const isOtherOption = (option) => {
|
||||
if (!option || typeof option !== 'object') return false
|
||||
const label = String(option.label || '').trim().toLowerCase()
|
||||
const value = String(option.value || '').trim().toLowerCase()
|
||||
const label = String(option.label || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
const value = String(option.value || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
|
||||
return (
|
||||
value === DEFAULT_OTHER_OPTION_VALUE ||
|
||||
@ -53,14 +57,16 @@ export const normalizeQuestions = (rawQuestions) => {
|
||||
const question = String(item.question || '').trim()
|
||||
if (!question) return null
|
||||
|
||||
const questionId = String(item.questionId || item.question_id || '').trim() || `q-${index + 1}`
|
||||
const questionId =
|
||||
String(item.questionId || item.question_id || '').trim() || `q-${index + 1}`
|
||||
const operation = String(item.operation || '').trim()
|
||||
const allowOther = Boolean(item.allowOther ?? item.allow_other ?? true)
|
||||
const baseOptions = normalizeOptions(item.options || [])
|
||||
const hasOtherOption = baseOptions.some((option) => isOtherOption(option))
|
||||
const options = allowOther && !hasOtherOption
|
||||
? [...baseOptions, { label: '其他', value: DEFAULT_OTHER_OPTION_VALUE }]
|
||||
: baseOptions
|
||||
const options =
|
||||
allowOther && !hasOtherOption
|
||||
? [...baseOptions, { label: '其他', value: DEFAULT_OTHER_OPTION_VALUE }]
|
||||
: baseOptions
|
||||
|
||||
return {
|
||||
questionId,
|
||||
|
||||
@ -103,7 +103,11 @@
|
||||
/>
|
||||
|
||||
<!-- 反馈模态框 -->
|
||||
<FeedbackModalComponent v-if="userStore.isAdmin" ref="feedbackModal" :agent-id="selectedAgentId" />
|
||||
<FeedbackModalComponent
|
||||
v-if="userStore.isAdmin"
|
||||
ref="feedbackModal"
|
||||
:agent-id="selectedAgentId"
|
||||
/>
|
||||
|
||||
<!-- 自定义更多菜单 -->
|
||||
<Teleport to="body">
|
||||
@ -186,7 +190,10 @@ const syncSelectedAgentFromRoute = async () => {
|
||||
const routeAgentExists = (agents.value || []).some((agent) => agent.id === routeAgentId)
|
||||
if (!routeAgentExists) {
|
||||
if (selectedAgentId.value) {
|
||||
await router.replace({ name: 'AgentCompWithId', params: { agent_id: selectedAgentId.value } })
|
||||
await router.replace({
|
||||
name: 'AgentCompWithId',
|
||||
params: { agent_id: selectedAgentId.value }
|
||||
})
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user