style: code format
This commit is contained in:
parent
31a7fc52d0
commit
b6240a1b4f
@ -4,6 +4,9 @@ from yuxi.agents.base import BaseAgent
|
||||
# 从 buildin 模块导入 agent_manager
|
||||
from yuxi.agents.context import BaseContext
|
||||
|
||||
# MCP - Agent 层统一入口(自动过滤 disabled_tools)
|
||||
from yuxi.agents.mcp.service import get_enabled_mcp_tools
|
||||
|
||||
# Model utilities - 模型加载
|
||||
from yuxi.agents.models import load_chat_model
|
||||
from yuxi.agents.state import BaseState
|
||||
@ -11,9 +14,6 @@ from yuxi.agents.state import BaseState
|
||||
# Tools - 核心工具函数
|
||||
from yuxi.agents.toolkits.utils import get_tool_info
|
||||
|
||||
# MCP - Agent 层统一入口(自动过滤 disabled_tools)
|
||||
from yuxi.agents.mcp.service import get_enabled_mcp_tools
|
||||
|
||||
__all__ = [
|
||||
# Base classes
|
||||
"BaseAgent",
|
||||
|
||||
@ -33,7 +33,6 @@ from yuxi.utils.paths import (
|
||||
|
||||
from .provider import get_sandbox_provider, sandbox_id_for_thread
|
||||
|
||||
|
||||
_USER_DATA_ROOT = "/" + VIRTUAL_PATH_PREFIX.strip("/")
|
||||
_WORKSPACE_ROOT = f"{_USER_DATA_ROOT}/{WORKSPACE_DIR_NAME}"
|
||||
_UPLOADS_ROOT = f"{_USER_DATA_ROOT}/{UPLOADS_DIR_NAME}"
|
||||
|
||||
@ -3,9 +3,9 @@ import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
|
||||
from yuxi.utils.singleton import SingletonMeta
|
||||
from yuxi.agents.base import BaseAgent
|
||||
from yuxi.utils import logger
|
||||
from yuxi.utils.singleton import SingletonMeta
|
||||
|
||||
|
||||
class AgentManager(metaclass=SingletonMeta):
|
||||
|
||||
@ -17,8 +17,8 @@ from yuxi.agents.middlewares import (
|
||||
)
|
||||
from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
|
||||
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware
|
||||
from yuxi.agents.toolkits.buildin.tools import _create_tavily_search
|
||||
from yuxi.agents.subagents.service import build_subagent_middleware_specs, get_subagents_from_slugs
|
||||
from yuxi.agents.toolkits.buildin.tools import _create_tavily_search
|
||||
from yuxi.agents.toolkits.service import resolve_configured_runtime_tools
|
||||
from yuxi.utils import logger
|
||||
from yuxi.utils.datetime_utils import shanghai_now
|
||||
|
||||
@ -17,6 +17,7 @@ from typing import Any, cast
|
||||
from langchain_mcp_adapters.client import MultiServerMCPClient
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.storage.postgres.models_business import MCPServer
|
||||
from yuxi.utils import logger
|
||||
|
||||
|
||||
@ -14,10 +14,10 @@ from langchain.tools.tool_node import ToolCallRequest
|
||||
from langgraph.types import Command
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.agents.toolkits import get_all_tool_instances
|
||||
from yuxi.agents.skills.repository import SkillRepository
|
||||
from yuxi.agents.mcp.service import get_enabled_mcp_tools
|
||||
from yuxi.agents.skills.repository import SkillRepository
|
||||
from yuxi.agents.skills.service import is_valid_skill_slug, list_accessible_skills, normalize_string_list
|
||||
from yuxi.agents.toolkits import get_all_tool_instances
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ from pathlib import Path
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.agents.skills.service import import_skill_dir, is_valid_skill_slug
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
||||
@ -16,9 +16,10 @@ from typing import Any
|
||||
import yaml
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi import config as sys_config
|
||||
from yuxi.agents.skills.repository import SkillRepository
|
||||
from yuxi.agents.mcp.service import get_enabled_mcp_server_slugs
|
||||
from yuxi.agents.skills.repository import SkillRepository
|
||||
from yuxi.storage.postgres.models_business import Skill, User
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
|
||||
@ -8,6 +8,7 @@ from typing import Any
|
||||
from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.agents.subagents.repository import SubAgentRepository
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.storage.postgres.models_business import SubAgent
|
||||
|
||||
@ -10,7 +10,6 @@ from langgraph.prebuilt.tool_node import ToolRuntime
|
||||
from langgraph.types import Command, interrupt
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from yuxi import config
|
||||
from yuxi.agents.toolkits.registry import ToolExtraMetadata, _all_tool_instances, _extra_registry, tool
|
||||
from yuxi.storage.minio import aupload_file_to_minio
|
||||
from yuxi.utils import logger
|
||||
|
||||
@ -4,11 +4,10 @@ import inspect
|
||||
from typing import Any
|
||||
|
||||
from langgraph.prebuilt.tool_node import ToolRuntime
|
||||
|
||||
from yuxi.agents.toolkits.registry import tool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from yuxi import knowledge_base
|
||||
from yuxi.agents.toolkits.registry import tool
|
||||
from yuxi.knowledge.base import KnowledgeBase
|
||||
from yuxi.knowledge.schemas import (
|
||||
FindInputSchema,
|
||||
@ -226,11 +225,7 @@ async def query_kb(kb_id: str, query_text: str, file_name: str | None = None, ru
|
||||
else:
|
||||
result = retriever(query_text, **kwargs)
|
||||
|
||||
if (
|
||||
isinstance(result, dict)
|
||||
and result.get("kb_id") == target_kb_id
|
||||
and isinstance(result.get("results"), list)
|
||||
):
|
||||
if isinstance(result, dict) and result.get("kb_id") == target_kb_id and isinstance(result.get("results"), list):
|
||||
return SearchOutputSchema(**result).model_dump()
|
||||
return KnowledgeBase.build_search_output(target_kb_id, result)
|
||||
|
||||
|
||||
@ -97,7 +97,9 @@ class Config(BaseModel):
|
||||
self.sandbox_exec_timeout_seconds = int(
|
||||
os.getenv("SANDBOX_EXEC_TIMEOUT_SECONDS") or self.sandbox_exec_timeout_seconds or 180
|
||||
)
|
||||
self.sandbox_max_output_bytes = int(os.getenv("SANDBOX_MAX_OUTPUT_BYTES") or self.sandbox_max_output_bytes or 262144)
|
||||
self.sandbox_max_output_bytes = int(
|
||||
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
|
||||
)
|
||||
|
||||
@ -33,6 +33,7 @@ CHUNK_PRESET_DESCRIPTIONS: dict[str, str] = {
|
||||
CHUNK_ENGINE_VERSION = "ragflow_like_v1"
|
||||
GENERAL_INTERNAL_PARSER_ID = "naive"
|
||||
|
||||
|
||||
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
|
||||
result = deepcopy(base)
|
||||
for key, value in (override or {}).items():
|
||||
|
||||
@ -1,3 +1,3 @@
|
||||
from .milvus_graph_service import MilvusGraphService
|
||||
|
||||
__all__ = ["MilvusGraphService"]
|
||||
__all__ = ["MilvusGraphService"]
|
||||
|
||||
@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
from yuxi.knowledge.graphs.graph_utils import normalize_entity_name
|
||||
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ from yuxi.models.chat import select_model
|
||||
|
||||
from .base import GraphExtractor
|
||||
|
||||
|
||||
DEFAULT_TRIPLE_EXTRACTION_PROMPT = """请从下面文本中抽取实体和实体关系,返回严格 JSON,不要输出解释。
|
||||
JSON 格式:
|
||||
{
|
||||
|
||||
@ -15,6 +15,9 @@ from yuxi.knowledge.graphs.graph_utils import (
|
||||
normalize_entity_name,
|
||||
)
|
||||
from yuxi.knowledge.graphs.milvus_graph_vector_store import MilvusGraphVectorStore
|
||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||
from yuxi.repositories.knowledge_chunk_repository import KnowledgeChunkRepository
|
||||
from yuxi.repositories.knowledge_graph_repository import KnowledgeGraphRepository
|
||||
from yuxi.storage.neo4j import (
|
||||
Neo4jConnectionManager,
|
||||
get_shared_neo4j_connection,
|
||||
@ -22,9 +25,6 @@ from yuxi.storage.neo4j import (
|
||||
neo4j_write,
|
||||
safe_neo4j_label,
|
||||
)
|
||||
from yuxi.repositories.knowledge_base_repository import KnowledgeBaseRepository
|
||||
from yuxi.repositories.knowledge_chunk_repository import KnowledgeChunkRepository
|
||||
from yuxi.repositories.knowledge_graph_repository import KnowledgeGraphRepository
|
||||
from yuxi.utils import logger
|
||||
from yuxi.utils.datetime_utils import utc_isoformat
|
||||
|
||||
|
||||
@ -22,10 +22,10 @@ from pymilvus import (
|
||||
|
||||
from yuxi.knowledge.base import FileStatus, KnowledgeBase
|
||||
from yuxi.knowledge.chunking.ragflow_like.dispatcher import chunk_markdown
|
||||
from yuxi.knowledge.utils.kb_utils import resolve_processing_params
|
||||
from yuxi.repositories.knowledge_chunk_repository import KnowledgeChunkRepository
|
||||
from yuxi.models.providers.cache import model_cache
|
||||
from yuxi.knowledge.parser.unified import Parser
|
||||
from yuxi.knowledge.utils.kb_utils import resolve_processing_params
|
||||
from yuxi.models.providers.cache import model_cache
|
||||
from yuxi.repositories.knowledge_chunk_repository import KnowledgeChunkRepository
|
||||
from yuxi.utils import hashstr, logger
|
||||
from yuxi.utils.datetime_utils import utc_isoformat
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils import logger
|
||||
from yuxi.utils.datetime_utils import utc_isoformat
|
||||
|
||||
|
||||
DEFAULT_SHARE_CONFIG = {"access_level": "global", "department_ids": [], "user_uids": []}
|
||||
ACCESS_LEVELS = {"global", "department", "user"}
|
||||
|
||||
|
||||
@ -5,7 +5,6 @@ from yuxi.knowledge.chunking.ragflow_like.presets import resolve_chunk_processin
|
||||
from yuxi.utils import hashstr, logger
|
||||
from yuxi.utils.datetime_utils import utc_isoformat
|
||||
|
||||
|
||||
_DROPPED_PROCESSING_PARAM_KEYS = {
|
||||
"_preprocessed_map",
|
||||
"auto_index",
|
||||
|
||||
@ -7,6 +7,7 @@ from typing import Any
|
||||
|
||||
import httpx
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from yuxi.models.providers.builtin import BUILTIN_PROVIDERS
|
||||
from yuxi.models.providers.repository import (
|
||||
create_model_provider,
|
||||
|
||||
@ -34,8 +34,6 @@ class MessageFeedbackRepository:
|
||||
"""检查用户是否已对消息反馈"""
|
||||
async with pg_manager.get_async_session_context() as session:
|
||||
result = await session.execute(
|
||||
select(MessageFeedback.id).where(
|
||||
MessageFeedback.message_id == message_id, MessageFeedback.uid == uid
|
||||
)
|
||||
select(MessageFeedback.id).where(MessageFeedback.message_id == message_id, MessageFeedback.uid == uid)
|
||||
)
|
||||
return result.scalar_one_or_none() is not None
|
||||
|
||||
@ -12,7 +12,6 @@ from yuxi.agents.backends.sandbox.paths import sandbox_workspace_agents_prompt_f
|
||||
from yuxi.agents.buildin import agent_manager
|
||||
from yuxi.agents.context import normalize_agent_context_config
|
||||
from yuxi.agents.state import AgentStatePayload
|
||||
from yuxi.utils.guard import content_guard
|
||||
from yuxi.repositories.agent_repository import AgentRepository
|
||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||
from yuxi.services.conversation_service import serialize_attachment
|
||||
@ -24,6 +23,7 @@ from yuxi.services.langfuse_service import (
|
||||
)
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.storage.postgres.models_business import Agent, User
|
||||
from yuxi.utils.guard import content_guard
|
||||
from yuxi.utils.logging_config import logger
|
||||
from yuxi.utils.question_utils import (
|
||||
normalize_questions as _normalize_interrupt_questions,
|
||||
|
||||
@ -16,12 +16,12 @@ from yuxi.knowledge.parser import Parser
|
||||
from yuxi.repositories.agent_repository import AgentRepository
|
||||
from yuxi.repositories.conversation_repository import ConversationRepository
|
||||
from yuxi.services.mention_search_service import invalidate_mention_cache
|
||||
from yuxi.utils.upload_utils import read_upload_with_limit, write_upload_to_path
|
||||
from yuxi.storage.minio import StorageError, get_minio_client
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils.datetime_utils import utc_isoformat
|
||||
from yuxi.utils.logging_config import logger
|
||||
from yuxi.utils.paths import VIRTUAL_PATH_UPLOADS
|
||||
from yuxi.utils.upload_utils import read_upload_with_limit, write_upload_to_path
|
||||
|
||||
ATTACHMENT_ALLOWED_EXTENSIONS: tuple[str, ...] = ()
|
||||
MAX_ATTACHMENT_SIZE_BYTES = 5 * 1024 * 1024 # 5 MB
|
||||
|
||||
@ -18,13 +18,12 @@ from pydantic import BaseModel, Field
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import IntegrityError
|
||||
from yuxi.repositories.user_repository import UserRepository
|
||||
from yuxi.services.operation_log_service import log_operation
|
||||
from yuxi.storage.postgres.models_business import Department, User
|
||||
from yuxi.utils.auth_utils import AuthUtils
|
||||
from yuxi.utils.datetime_utils import utc_now_naive
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
from yuxi.utils.auth_utils import AuthUtils
|
||||
from yuxi.services.operation_log_service import log_operation
|
||||
|
||||
# 前端 OIDC 回调路由路径(与 web/src/router/index.js 中的路由保持一致)
|
||||
FRONTEND_CALLBACK_PATH = "/auth/oidc/callback"
|
||||
# 登录页路径
|
||||
|
||||
@ -10,16 +10,16 @@ from dataclasses import dataclass, field
|
||||
|
||||
from sqlalchemy import select
|
||||
from sqlalchemy.exc import OperationalError
|
||||
from yuxi.agents.mcp.service import ensure_builtin_mcp_servers_in_db
|
||||
from yuxi.agents.skills.service import init_builtin_skills
|
||||
from yuxi.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository
|
||||
from yuxi.services.chat_service import stream_agent_chat, stream_agent_resume
|
||||
from yuxi.agents.mcp.service import ensure_builtin_mcp_servers_in_db
|
||||
from yuxi.services.run_queue_service import (
|
||||
append_run_stream_event,
|
||||
clear_cancel_signal,
|
||||
has_cancel_signal,
|
||||
wait_for_cancel_signal,
|
||||
)
|
||||
from yuxi.agents.skills.service import init_builtin_skills
|
||||
from yuxi.storage.postgres.manager import pg_manager
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils.logging_config import logger
|
||||
|
||||
@ -26,10 +26,18 @@ from yuxi.services.agent_runtime_service import resolve_thread_agent_runtime_con
|
||||
from yuxi.services.file_preview import detect_preview_type
|
||||
from yuxi.services.workspace_service import (
|
||||
create_workspace_directory as create_workspace_directory_entry,
|
||||
)
|
||||
from yuxi.services.workspace_service import (
|
||||
delete_workspace_path,
|
||||
download_workspace_file as download_workspace_file_response,
|
||||
list_workspace_tree,
|
||||
)
|
||||
from yuxi.services.workspace_service import (
|
||||
download_workspace_file as download_workspace_file_response,
|
||||
)
|
||||
from yuxi.services.workspace_service import (
|
||||
read_workspace_file_content as read_workspace_file_content_response,
|
||||
)
|
||||
from yuxi.services.workspace_service import (
|
||||
upload_workspace_file as upload_workspace_file_entry,
|
||||
)
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
|
||||
@ -14,10 +14,10 @@ from fastapi.responses import FileResponse, StreamingResponse
|
||||
from yuxi.agents.backends.sandbox.paths import _global_user_data_dir, ensure_workspace_default_files
|
||||
from yuxi.services.file_preview import detect_preview_type
|
||||
from yuxi.services.mention_search_service import invalidate_workspace_mention_cache
|
||||
from yuxi.utils.upload_utils import MAX_UPLOAD_SIZE_BYTES, write_upload_to_buffer
|
||||
from yuxi.storage.postgres.models_business import User
|
||||
from yuxi.utils.datetime_utils import utc_isoformat_from_timestamp
|
||||
from yuxi.utils.paths import VIRTUAL_PATH_WORKSPACE, WORKSPACE_DIR_NAME
|
||||
from yuxi.utils.upload_utils import MAX_UPLOAD_SIZE_BYTES, write_upload_to_buffer
|
||||
|
||||
EDITABLE_WORKSPACE_SUFFIXES = {".md", ".markdown", ".mdx", ".txt"}
|
||||
MAX_WORKSPACE_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_BYTES
|
||||
|
||||
@ -7,7 +7,6 @@ import os
|
||||
import uuid
|
||||
|
||||
from fastapi import UploadFile
|
||||
|
||||
from yuxi.utils.upload_utils import read_upload_with_limit
|
||||
|
||||
from .client import aupload_file_to_minio
|
||||
|
||||
@ -6,10 +6,10 @@ import threading
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from neo4j import GraphDatabase as GD
|
||||
|
||||
from yuxi.utils import logger
|
||||
|
||||
from neo4j import GraphDatabase as GD
|
||||
|
||||
_SAFE_NEO4J_LABEL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
|
||||
_shared_neo4j_connection: Neo4jConnectionManager | None = None
|
||||
_shared_neo4j_connection_lock = threading.Lock()
|
||||
|
||||
@ -11,7 +11,6 @@ from sqlalchemy.orm import declarative_base
|
||||
from yuxi.storage.postgres.models_business import Base as BusinessBase
|
||||
from yuxi.storage.postgres.models_knowledge import Base as KnowledgeBase
|
||||
from yuxi.utils import logger
|
||||
|
||||
from yuxi.utils.singleton import SingletonMeta
|
||||
|
||||
# 合并两个 Base
|
||||
|
||||
@ -6,6 +6,7 @@ from typing import Any
|
||||
import jwt
|
||||
from argon2 import PasswordHasher
|
||||
from argon2.exceptions import InvalidHash, VerificationError, VerifyMismatchError
|
||||
|
||||
from yuxi.utils.datetime_utils import utc_now
|
||||
|
||||
JWT_ALGORITHM = "HS256"
|
||||
|
||||
@ -6,7 +6,13 @@ export const graphApi = {
|
||||
},
|
||||
|
||||
getSubgraph: async (params) => {
|
||||
const { kb_id, node_label = '*', max_depth = 2, max_nodes = 100, exclude_chunk = false } = params
|
||||
const {
|
||||
kb_id,
|
||||
node_label = '*',
|
||||
max_depth = 2,
|
||||
max_nodes = 100,
|
||||
exclude_chunk = false
|
||||
} = params
|
||||
|
||||
if (!kb_id) {
|
||||
throw new Error('kb_id is required')
|
||||
|
||||
@ -1,4 +1,12 @@
|
||||
import { apiGet, apiPost, apiDelete, apiAdminGet, apiAdminPost, apiAdminPut, apiAdminDelete } from './base'
|
||||
import {
|
||||
apiGet,
|
||||
apiPost,
|
||||
apiDelete,
|
||||
apiAdminGet,
|
||||
apiAdminPost,
|
||||
apiAdminPut,
|
||||
apiAdminDelete
|
||||
} from './base'
|
||||
|
||||
const BASE_URL = '/api/system/skills'
|
||||
const USER_BASE_URL = '/api/skills'
|
||||
|
||||
@ -64,7 +64,12 @@
|
||||
@keydown.esc.stop.prevent="cancelField"
|
||||
@blur="cancelField"
|
||||
/>
|
||||
<button v-else type="button" class="editable-value" @click="startFieldEdit('phone_number')">
|
||||
<button
|
||||
v-else
|
||||
type="button"
|
||||
class="editable-value"
|
||||
@click="startFieldEdit('phone_number')"
|
||||
>
|
||||
{{ userStore.phoneNumber || '未设置' }}
|
||||
</button>
|
||||
</div>
|
||||
@ -289,11 +294,7 @@ watch(
|
||||
}
|
||||
)
|
||||
|
||||
watch(
|
||||
() => [userStore.username, userStore.phoneNumber],
|
||||
syncProfileDraft,
|
||||
{ immediate: true }
|
||||
)
|
||||
watch(() => [userStore.username, userStore.phoneNumber], syncProfileDraft, { immediate: true })
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
|
||||
@ -1,7 +1,12 @@
|
||||
<template>
|
||||
<section v-if="normalizedArtifacts.length" class="artifacts-list">
|
||||
<div v-for="file in normalizedArtifacts" :key="file.path" class="artifact-card">
|
||||
<button type="button" class="item-main" :title="`打开 ${file.name}`" @click="openPreview(file)">
|
||||
<button
|
||||
type="button"
|
||||
class="item-main"
|
||||
:title="`打开 ${file.name}`"
|
||||
@click="openPreview(file)"
|
||||
>
|
||||
<component
|
||||
:is="getFileIcon(file.path)"
|
||||
class="item-icon"
|
||||
@ -81,7 +86,10 @@ const parseDownloadFilename = (contentDisposition) => {
|
||||
}
|
||||
|
||||
const getFileMetaLabel = (path) => {
|
||||
const filename = String(path || '').split('/').pop() || ''
|
||||
const filename =
|
||||
String(path || '')
|
||||
.split('/')
|
||||
.pop() || ''
|
||||
if (!filename.includes('.')) return '交付文件'
|
||||
|
||||
const extension = filename.split('.').pop()
|
||||
|
||||
@ -386,7 +386,8 @@ const setPanelRatioForViewMode = () => {
|
||||
|
||||
const showFilePanel = (mode = 'tree') => {
|
||||
sideActive.value = 'file'
|
||||
agentPanelViewMode.value = mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
|
||||
agentPanelViewMode.value =
|
||||
mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
|
||||
setPanelRatioForViewMode()
|
||||
}
|
||||
|
||||
|
||||
@ -16,9 +16,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="env-tip">
|
||||
保存后仅对新建沙盒生效,已运行沙盒不会热更新。
|
||||
</div>
|
||||
<div class="env-tip">保存后仅对新建沙盒生效,已运行沙盒不会热更新。</div>
|
||||
|
||||
<a-spin :spinning="loading">
|
||||
<McpEnvEditor :modelValue="draftEnv" @update:modelValue="updateDraftEnv" />
|
||||
|
||||
@ -94,7 +94,10 @@
|
||||
class="file-content"
|
||||
:class="[
|
||||
contentClass,
|
||||
{ 'is-iframe-preview': file?.previewType === 'pdf' || (isHtmlFile && htmlPreviewMode === 'render') }
|
||||
{
|
||||
'is-iframe-preview':
|
||||
file?.previewType === 'pdf' || (isHtmlFile && htmlPreviewMode === 'render')
|
||||
}
|
||||
]"
|
||||
>
|
||||
<div v-if="canEdit && editMode === 'edit'" class="edit-floating-actions">
|
||||
@ -110,12 +113,7 @@
|
||||
<span v-if="saving">保存中...</span>
|
||||
<span v-else>保存</span>
|
||||
</button>
|
||||
<button
|
||||
class="edit-floating-btn"
|
||||
:disabled="saving"
|
||||
@click="cancelEdit"
|
||||
title="取消"
|
||||
>
|
||||
<button class="edit-floating-btn" :disabled="saving" @click="cancelEdit" title="取消">
|
||||
<X :size="16" />
|
||||
<span>取消</span>
|
||||
</button>
|
||||
|
||||
@ -54,7 +54,10 @@
|
||||
</div>
|
||||
|
||||
<div class="tab-content">
|
||||
<div class="files-display" :class="{ 'has-preview': hasActivePreview, 'with-tree': treePaneVisible }">
|
||||
<div
|
||||
class="files-display"
|
||||
:class="{ 'has-preview': hasActivePreview, 'with-tree': treePaneVisible }"
|
||||
>
|
||||
<div v-if="hasActivePreview" class="preview-pane">
|
||||
<AgentFilePreview
|
||||
v-if="currentFile"
|
||||
@ -619,16 +622,19 @@ onUnmounted(() => {
|
||||
revokeCurrentPreviewUrl()
|
||||
})
|
||||
|
||||
watch(() => props.threadId, (threadId) => {
|
||||
if (threadId) {
|
||||
refreshFileSystem()
|
||||
} else {
|
||||
dynamicTreeData.value = []
|
||||
expandedKeys.value = []
|
||||
selectedKeys.value = []
|
||||
filesystemError.value = ''
|
||||
watch(
|
||||
() => props.threadId,
|
||||
(threadId) => {
|
||||
if (threadId) {
|
||||
refreshFileSystem()
|
||||
} else {
|
||||
dynamicTreeData.value = []
|
||||
expandedKeys.value = []
|
||||
selectedKeys.value = []
|
||||
filesystemError.value = ''
|
||||
}
|
||||
}
|
||||
})
|
||||
)
|
||||
|
||||
watch([() => props.threadId, () => props.activePreviewPath], loadActivePreview, { immediate: true })
|
||||
|
||||
|
||||
@ -21,11 +21,7 @@
|
||||
class="config-alert"
|
||||
/>
|
||||
<!-- 统一显示所有配置项 -->
|
||||
<a-empty
|
||||
v-if="isCurrentSegmentEmpty"
|
||||
description="暂无配置项"
|
||||
class="config-empty"
|
||||
/>
|
||||
<a-empty v-if="isCurrentSegmentEmpty" description="暂无配置项" class="config-empty" />
|
||||
<template v-for="(value, key) in filteredConfigurableItems" :key="key">
|
||||
<a-form-item :label="getConfigLabel(key, value)" :name="key" class="config-item">
|
||||
<p v-if="value.description" class="config-description">{{ value.description }}</p>
|
||||
|
||||
@ -175,7 +175,6 @@ const presetDescription = computed(() => getChunkPresetDescription(effectivePres
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
|
||||
.param-description {
|
||||
font-size: 12px;
|
||||
color: var(--gray-400);
|
||||
|
||||
@ -247,11 +247,7 @@ const renameChat = async (chatId) => {
|
||||
}
|
||||
|
||||
.actions-mask {
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
transparent,
|
||||
var(--gray-50)
|
||||
);
|
||||
background: linear-gradient(to right, transparent, var(--gray-50));
|
||||
}
|
||||
|
||||
.more-btn {
|
||||
|
||||
@ -13,7 +13,9 @@
|
||||
title="刷新"
|
||||
class="refresh-btn lucide-icon-btn"
|
||||
>
|
||||
<template #icon><RefreshCw :size="16" :class="{ spin: departmentManagement.refreshing }" /></template>
|
||||
<template #icon
|
||||
><RefreshCw :size="16" :class="{ spin: departmentManagement.refreshing }"
|
||||
/></template>
|
||||
</a-button>
|
||||
<a-button type="primary" @click="showAddDepartmentModal" class="add-btn lucide-icon-btn">
|
||||
<template #icon><Plus :size="16" /></template>
|
||||
|
||||
@ -100,7 +100,9 @@ const fetchV2Models = async () => {
|
||||
|
||||
const checkV2ModelStatuses = async () => {
|
||||
try {
|
||||
const models = Object.values(v2Models.value).flatMap((providerData) => providerData.models || [])
|
||||
const models = Object.values(v2Models.value).flatMap(
|
||||
(providerData) => providerData.models || []
|
||||
)
|
||||
await checkV2Statuses(models)
|
||||
} catch (error) {
|
||||
console.error('检查 embedding 模型状态失败:', error)
|
||||
|
||||
@ -259,9 +259,7 @@
|
||||
</span>
|
||||
<span
|
||||
>已选择
|
||||
{{
|
||||
selectedWorkspacePaths.length
|
||||
}}
|
||||
{{ selectedWorkspacePaths.length }}
|
||||
个文件,注意上传会扁平化上传,不保留文件层级结构</span
|
||||
>
|
||||
</div>
|
||||
|
||||
@ -11,7 +11,11 @@
|
||||
<div :class="rowClass(item.data?.label)">
|
||||
<span class="detail-label">名称</span>
|
||||
<span class="detail-value">
|
||||
<DetailValue :value="item.data?.label" field-key="__label__" :expanded-keys="expandedKeys" />
|
||||
<DetailValue
|
||||
:value="item.data?.label"
|
||||
field-key="__label__"
|
||||
:expanded-keys="expandedKeys"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<div class="detail-row">
|
||||
@ -22,12 +26,18 @@
|
||||
<div class="detail-row">
|
||||
<span class="detail-label">标签</span>
|
||||
<span class="detail-value">
|
||||
<a-tag v-for="tag in item.data.original.labels" :key="tag" size="small">{{ tag }}</a-tag>
|
||||
<a-tag v-for="tag in item.data.original.labels" :key="tag" size="small">{{
|
||||
tag
|
||||
}}</a-tag>
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="item.data?.original?.properties">
|
||||
<div v-for="(value, key) in item.data.original.properties" :key="key" :class="rowClass(value)">
|
||||
<div
|
||||
v-for="(value, key) in item.data.original.properties"
|
||||
:key="key"
|
||||
:class="rowClass(value)"
|
||||
>
|
||||
<span class="detail-label">{{ key }}</span>
|
||||
<span class="detail-value">
|
||||
<DetailValue :value="value" :field-key="key" :expanded-keys="expandedKeys" />
|
||||
@ -39,11 +49,19 @@
|
||||
<div :class="rowClass(item.data?.label)">
|
||||
<span class="detail-label">类型</span>
|
||||
<span class="detail-value">
|
||||
<DetailValue :value="item.data?.label" field-key="__type__" :expanded-keys="expandedKeys" />
|
||||
<DetailValue
|
||||
:value="item.data?.label"
|
||||
field-key="__type__"
|
||||
:expanded-keys="expandedKeys"
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
<template v-if="item.data?.original?.properties">
|
||||
<div v-for="(value, key) in filteredEdgeProperties" :key="key" :class="rowClass(value)">
|
||||
<div
|
||||
v-for="(value, key) in filteredEdgeProperties"
|
||||
:key="key"
|
||||
:class="rowClass(value)"
|
||||
>
|
||||
<span class="detail-label">{{ key }}</span>
|
||||
<span class="detail-value">
|
||||
<DetailValue :value="value" :field-key="key" :expanded-keys="expandedKeys" />
|
||||
@ -76,15 +94,35 @@ const DetailValue = defineComponent({
|
||||
if (typeof v !== 'string') return String(v ?? '')
|
||||
if (v.length <= TRUNCATE_LIMIT) return v
|
||||
if (props.expandedKeys.has(props.fieldKey)) {
|
||||
return [v, h('a', {
|
||||
class: 'expand-link',
|
||||
onClick: (e) => { e.preventDefault(); props.expandedKeys.delete(props.fieldKey) }
|
||||
}, ' 收起')]
|
||||
return [
|
||||
v,
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'expand-link',
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
props.expandedKeys.delete(props.fieldKey)
|
||||
}
|
||||
},
|
||||
' 收起'
|
||||
)
|
||||
]
|
||||
}
|
||||
return [v.slice(0, TRUNCATE_LIMIT) + '...', h('a', {
|
||||
class: 'expand-link',
|
||||
onClick: (e) => { e.preventDefault(); props.expandedKeys.add(props.fieldKey) }
|
||||
}, '展开')]
|
||||
return [
|
||||
v.slice(0, TRUNCATE_LIMIT) + '...',
|
||||
h(
|
||||
'a',
|
||||
{
|
||||
class: 'expand-link',
|
||||
onClick: (e) => {
|
||||
e.preventDefault()
|
||||
props.expandedKeys.add(props.fieldKey)
|
||||
}
|
||||
},
|
||||
'展开'
|
||||
)
|
||||
]
|
||||
}
|
||||
}
|
||||
})
|
||||
@ -99,9 +137,12 @@ defineEmits(['close'])
|
||||
|
||||
const expandedKeys = reactive(new Set())
|
||||
|
||||
watch(() => props.item, () => {
|
||||
expandedKeys.clear()
|
||||
})
|
||||
watch(
|
||||
() => props.item,
|
||||
() => {
|
||||
expandedKeys.clear()
|
||||
}
|
||||
)
|
||||
|
||||
const isOverThreshold = (value) => typeof value === 'string' && value.length > STACK_THRESHOLD
|
||||
|
||||
@ -239,4 +280,4 @@ const filteredEdgeProperties = computed(() => {
|
||||
transform: translateX(-20px);
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -99,7 +99,11 @@
|
||||
<div
|
||||
v-for="(item, index) in mentionItems.knowledgeBases"
|
||||
:key="'kb-' + item.value"
|
||||
:class="['mention-item', 'resource-item', { active: isItemSelected('knowledge', index) }]"
|
||||
:class="[
|
||||
'mention-item',
|
||||
'resource-item',
|
||||
{ active: isItemSelected('knowledge', index) }
|
||||
]"
|
||||
@click="insertMention(item)"
|
||||
>
|
||||
<div class="resource-name">
|
||||
@ -204,7 +208,11 @@
|
||||
<div
|
||||
v-for="(item, index) in mentionItems.subagents"
|
||||
:key="'subagent-' + item.value"
|
||||
:class="['mention-item', 'resource-item', { active: isItemSelected('subagent', index) }]"
|
||||
:class="[
|
||||
'mention-item',
|
||||
'resource-item',
|
||||
{ active: isItemSelected('subagent', index) }
|
||||
]"
|
||||
@click="insertMention(item)"
|
||||
>
|
||||
<div class="resource-name">
|
||||
@ -411,7 +419,10 @@ const getRawNodeLength = (node) => {
|
||||
if (isTextNode(node)) return node.textContent?.length || 0
|
||||
if (isMentionNode(node)) return node.dataset.mentionRaw?.length || 0
|
||||
if (isLineBreakNode(node)) return 1
|
||||
return Array.from(node.childNodes || []).reduce((total, child) => total + getRawNodeLength(child), 0)
|
||||
return Array.from(node.childNodes || []).reduce(
|
||||
(total, child) => total + getRawNodeLength(child),
|
||||
0
|
||||
)
|
||||
}
|
||||
|
||||
const serializeEditorNode = (node) => {
|
||||
@ -462,7 +473,11 @@ const createEditorMentionElement = (segment) => {
|
||||
|
||||
const label = document.createElement('span')
|
||||
label.className = 'mention-ref-label'
|
||||
label.textContent = getMentionDisplayLabel(segment.type, segment.value, mentionDisplayLabels.value)
|
||||
label.textContent = getMentionDisplayLabel(
|
||||
segment.type,
|
||||
segment.value,
|
||||
mentionDisplayLabels.value
|
||||
)
|
||||
token.appendChild(label)
|
||||
|
||||
return token
|
||||
@ -475,7 +490,9 @@ const renderEditorContent = (raw = '') => {
|
||||
editor.replaceChildren()
|
||||
parseMentionText(raw).forEach((segment) => {
|
||||
editor.appendChild(
|
||||
segment.kind === 'text' ? document.createTextNode(segment.text) : createEditorMentionElement(segment)
|
||||
segment.kind === 'text'
|
||||
? document.createTextNode(segment.text)
|
||||
: createEditorMentionElement(segment)
|
||||
)
|
||||
})
|
||||
lastSyncedEditorValue = String(raw || '')
|
||||
@ -549,7 +566,10 @@ const getDomPointForRawOffset = (rawOffset) => {
|
||||
let remaining = offset
|
||||
|
||||
const pointBeforeNode = (node) => ({ node: node.parentNode || editor, offset: childIndex(node) })
|
||||
const pointAfterNode = (node) => ({ node: node.parentNode || editor, offset: childIndex(node) + 1 })
|
||||
const pointAfterNode = (node) => ({
|
||||
node: node.parentNode || editor,
|
||||
offset: childIndex(node) + 1
|
||||
})
|
||||
|
||||
const visit = (node) => {
|
||||
if (!node) return null
|
||||
@ -872,7 +892,12 @@ const insertMention = (item) => {
|
||||
|
||||
const mentionValue = item.insertValue || item.value
|
||||
const mentionText = `${formatMentionToken(item.type, mentionValue)} `
|
||||
const newValue = replaceRawRange(currentValue, activeMention.start, activeMention.end, mentionText)
|
||||
const newValue = replaceRawRange(
|
||||
currentValue,
|
||||
activeMention.start,
|
||||
activeMention.end,
|
||||
mentionText
|
||||
)
|
||||
const newCursorPos = activeMention.start + mentionText.length
|
||||
|
||||
mentionPopupVisible.value = false
|
||||
|
||||
@ -1,5 +1,10 @@
|
||||
<template>
|
||||
<a-dropdown trigger="click" :open="dropdownOpen" :disabled="props.disabled" @open-change="handleOpenChange">
|
||||
<a-dropdown
|
||||
trigger="click"
|
||||
:open="dropdownOpen"
|
||||
:disabled="props.disabled"
|
||||
@open-change="handleOpenChange"
|
||||
>
|
||||
<div class="model-select" :class="modelSelectClasses" @click.prevent.stop @mousedown.stop>
|
||||
<div class="model-select-content">
|
||||
<div class="model-info">
|
||||
@ -30,7 +35,12 @@
|
||||
<template #overlay>
|
||||
<div class="model-dropdown" @click.stop>
|
||||
<div class="model-search">
|
||||
<a-input v-model:value="modelSearchKeyword" placeholder="搜索模型" allow-clear @keydown.stop>
|
||||
<a-input
|
||||
v-model:value="modelSearchKeyword"
|
||||
placeholder="搜索模型"
|
||||
allow-clear
|
||||
@keydown.stop
|
||||
>
|
||||
<template #suffix>
|
||||
<button
|
||||
:disabled="props.disabled || state.refreshingCache"
|
||||
@ -46,7 +56,9 @@
|
||||
</div>
|
||||
<a-menu class="scrollable-menu">
|
||||
<a-menu-item v-if="loadingV2Models" key="loading" disabled>加载中...</a-menu-item>
|
||||
<a-menu-item v-else-if="!hasFilteredModels" key="empty" disabled>暂无匹配模型</a-menu-item>
|
||||
<a-menu-item v-else-if="!hasFilteredModels" key="empty" disabled
|
||||
>暂无匹配模型</a-menu-item
|
||||
>
|
||||
<template v-else>
|
||||
<a-menu-item-group
|
||||
v-for="(providerData, providerId) in filteredV2Models"
|
||||
|
||||
@ -139,10 +139,7 @@
|
||||
</div>
|
||||
|
||||
<div v-else class="suggestions-empty">
|
||||
<button
|
||||
class="suggestion-row"
|
||||
@click="() => generateSampleQuestions(false)"
|
||||
>
|
||||
<button class="suggestion-row" @click="() => generateSampleQuestions(false)">
|
||||
<RefreshCw class="suggestion-icon" />
|
||||
<span class="suggestion-text">生成示例问题</span>
|
||||
</button>
|
||||
@ -515,7 +512,9 @@ defineExpose({
|
||||
border-radius: 6px;
|
||||
color: var(--gray-800);
|
||||
font-size: 13px;
|
||||
span { font-weight: 500; }
|
||||
span {
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
|
||||
.clear-results-btn {
|
||||
|
||||
@ -150,7 +150,9 @@
|
||||
status="active"
|
||||
:show-info="false"
|
||||
/>
|
||||
<span class="run-progress-message">{{ getRunProgressMessage(latestEvaluation) }}</span>
|
||||
<span class="run-progress-message">{{
|
||||
getRunProgressMessage(latestEvaluation)
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -221,7 +223,11 @@
|
||||
ok-text="确定"
|
||||
cancel-text="取消"
|
||||
>
|
||||
<a href="" class="history-action-link history-action-link-danger" @click.prevent>
|
||||
<a
|
||||
href=""
|
||||
class="history-action-link history-action-link-danger"
|
||||
@click.prevent
|
||||
>
|
||||
删除
|
||||
</a>
|
||||
</a-popconfirm>
|
||||
@ -668,15 +674,11 @@ const loadResultsWithPagination = async () => {
|
||||
|
||||
try {
|
||||
resultsLoading.value = true
|
||||
const response = await evaluationApi.getRunResults(
|
||||
props.kbId,
|
||||
selectedResult.value.run_id,
|
||||
{
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
errorOnly: showErrorsOnly.value
|
||||
}
|
||||
)
|
||||
const response = await evaluationApi.getRunResults(props.kbId, selectedResult.value.run_id, {
|
||||
page: currentPage.value,
|
||||
pageSize: pageSize.value,
|
||||
errorOnly: showErrorsOnly.value
|
||||
})
|
||||
|
||||
if (response.message === 'success' && response.data) {
|
||||
const resultData = response.data
|
||||
@ -773,7 +775,8 @@ const onDatasetChanged = (datasetId) => {
|
||||
}
|
||||
}
|
||||
|
||||
const hasRunningEvaluation = () => evaluationHistory.value.some((record) => record.status === 'running')
|
||||
const hasRunningEvaluation = () =>
|
||||
evaluationHistory.value.some((record) => record.status === 'running')
|
||||
|
||||
const stopEvaluationRefresh = () => {
|
||||
if (evaluationRefreshTimer) {
|
||||
@ -1035,7 +1038,8 @@ const formatRunDuration = (record) => {
|
||||
}
|
||||
|
||||
const getRunProgress = (record) => {
|
||||
if (isFiniteNumber(record?.progress)) return Math.max(0, Math.min(Math.round(record.progress), 100))
|
||||
if (isFiniteNumber(record?.progress))
|
||||
return Math.max(0, Math.min(Math.round(record.progress), 100))
|
||||
|
||||
const total = Number(record?.total_items || 0)
|
||||
if (!total) return 0
|
||||
@ -1051,7 +1055,9 @@ const getRunProgressMessage = (record) => {
|
||||
}
|
||||
|
||||
const formatLatestRingValue = (record) =>
|
||||
record?.status === 'running' ? `${getRunProgress(record)}%` : formatPercent(record?.overall_score, 0)
|
||||
record?.status === 'running'
|
||||
? `${getRunProgress(record)}%`
|
||||
: formatPercent(record?.overall_score, 0)
|
||||
|
||||
const getLatestRingClass = (record) =>
|
||||
record?.status === 'running' ? 'score-running' : getScoreLevelClass(record?.overall_score)
|
||||
|
||||
@ -59,4 +59,4 @@ const handlePanelSave = (config) => {
|
||||
const handleCancel = () => {
|
||||
emit('update:modelValue', false)
|
||||
}
|
||||
</script>
|
||||
</script>
|
||||
|
||||
@ -256,4 +256,4 @@ defineExpose({ save, resetToDefaults, loadQueryParams })
|
||||
border-radius: 6px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -166,7 +166,6 @@
|
||||
<div v-show="activeTab === 'department'" v-if="userStore.isSuperAdmin">
|
||||
<DepartmentManagementComponent />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -176,7 +175,16 @@
|
||||
<script setup>
|
||||
import { computed, onMounted, ref, watch } from 'vue'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { CircleUser, ExternalLink, Settings, Star, SquareTerminal, User, Users, X } from 'lucide-vue-next'
|
||||
import {
|
||||
CircleUser,
|
||||
ExternalLink,
|
||||
Settings,
|
||||
Star,
|
||||
SquareTerminal,
|
||||
User,
|
||||
Users,
|
||||
X
|
||||
} from 'lucide-vue-next'
|
||||
import AccountSettingsComponent from '@/components/AccountSettingsComponent.vue'
|
||||
import AgentEnvSettingsCard from '@/components/AgentEnvSettingsCard.vue'
|
||||
import BasicSettingsSection from '@/components/BasicSettingsSection.vue'
|
||||
|
||||
@ -29,7 +29,11 @@
|
||||
class="card-action"
|
||||
@click.stop
|
||||
>
|
||||
<a-dropdown :trigger="['click']" placement="bottomRight" overlay-class-name="share-selection-popover">
|
||||
<a-dropdown
|
||||
:trigger="['click']"
|
||||
placement="bottomRight"
|
||||
overlay-class-name="share-selection-popover"
|
||||
>
|
||||
<a-button
|
||||
size="small"
|
||||
class="select-action lucide-icon-btn"
|
||||
@ -66,18 +70,44 @@
|
||||
:aria-checked="isSelected(option.value, item.value)"
|
||||
:tabindex="item.disabled ? -1 : 0"
|
||||
class="selection-item"
|
||||
:class="{ selected: isSelected(option.value, item.value), locked: item.disabled }"
|
||||
:class="{
|
||||
selected: isSelected(option.value, item.value),
|
||||
locked: item.disabled
|
||||
}"
|
||||
@mousedown.stop
|
||||
@click.stop="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))"
|
||||
@keydown.enter.prevent="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))"
|
||||
@keydown.space.prevent="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))"
|
||||
@click.stop="
|
||||
!item.disabled &&
|
||||
toggleSelection(
|
||||
option.value,
|
||||
item.value,
|
||||
!isSelected(option.value, item.value)
|
||||
)
|
||||
"
|
||||
@keydown.enter.prevent="
|
||||
!item.disabled &&
|
||||
toggleSelection(
|
||||
option.value,
|
||||
item.value,
|
||||
!isSelected(option.value, item.value)
|
||||
)
|
||||
"
|
||||
@keydown.space.prevent="
|
||||
!item.disabled &&
|
||||
toggleSelection(
|
||||
option.value,
|
||||
item.value,
|
||||
!isSelected(option.value, item.value)
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="selection-item-content">
|
||||
<a-checkbox
|
||||
:checked="isSelected(option.value, item.value)"
|
||||
:disabled="item.disabled"
|
||||
@click.stop
|
||||
@change="toggleSelection(option.value, item.value, $event.target.checked)"
|
||||
@change="
|
||||
toggleSelection(option.value, item.value, $event.target.checked)
|
||||
"
|
||||
/>
|
||||
<span class="selection-label">{{ item.label }}</span>
|
||||
</span>
|
||||
@ -94,7 +124,13 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<a-alert v-if="disabled && disabledReason" type="info" show-icon class="share-disabled-alert" :message="disabledReason" />
|
||||
<a-alert
|
||||
v-if="disabled && disabledReason"
|
||||
type="info"
|
||||
show-icon
|
||||
class="share-disabled-alert"
|
||||
:message="disabledReason"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -179,11 +215,15 @@ const currentDepartmentId = computed(() => {
|
||||
|
||||
const currentUserUid = computed(() => userStore.uid || '')
|
||||
const normalizedAllowedAccessLevels = computed(() => {
|
||||
const allowed = props.allowedAccessLevels.filter((level) => ['global', 'department', 'user'].includes(level))
|
||||
const allowed = props.allowedAccessLevels.filter((level) =>
|
||||
['global', 'department', 'user'].includes(level)
|
||||
)
|
||||
return allowed.length ? allowed : ['global']
|
||||
})
|
||||
const shareModeOptions = computed(() =>
|
||||
baseShareModeOptions.filter((option) => normalizedAllowedAccessLevels.value.includes(option.value))
|
||||
baseShareModeOptions.filter((option) =>
|
||||
normalizedAllowedAccessLevels.value.includes(option.value)
|
||||
)
|
||||
)
|
||||
|
||||
const departmentOptions = computed(() =>
|
||||
@ -246,7 +286,9 @@ const normalizeActiveConfig = () => {
|
||||
|
||||
const initConfig = () => {
|
||||
syncingFromProps.value = true
|
||||
const requestedAccessLevel = ['global', 'department', 'user'].includes(props.modelValue?.access_level)
|
||||
const requestedAccessLevel = ['global', 'department', 'user'].includes(
|
||||
props.modelValue?.access_level
|
||||
)
|
||||
? props.modelValue.access_level
|
||||
: 'global'
|
||||
config.access_level = normalizedAllowedAccessLevels.value.includes(requestedAccessLevel)
|
||||
@ -263,7 +305,8 @@ const initConfig = () => {
|
||||
const emitConfig = () => {
|
||||
emit('update:modelValue', {
|
||||
access_level: config.access_level,
|
||||
department_ids: config.access_level === 'department' ? normalizeDepartmentIds(config.department_ids) : [],
|
||||
department_ids:
|
||||
config.access_level === 'department' ? normalizeDepartmentIds(config.department_ids) : [],
|
||||
user_uids: config.access_level === 'user' ? normalizeUserUids(config.user_uids) : []
|
||||
})
|
||||
}
|
||||
@ -314,7 +357,9 @@ const toggleSelection = (accessLevel, value, checked) => {
|
||||
}
|
||||
|
||||
const uid = String(value)
|
||||
const selected = checked ? [...config.user_uids, uid] : config.user_uids.filter((item) => item !== uid)
|
||||
const selected = checked
|
||||
? [...config.user_uids, uid]
|
||||
: config.user_uids.filter((item) => item !== uid)
|
||||
config.user_uids = normalizeUserUids(selected)
|
||||
ensureCurrentUser()
|
||||
}
|
||||
@ -553,7 +598,6 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
<style lang="less">
|
||||
|
||||
@ -61,7 +61,6 @@
|
||||
</a-dropdown>
|
||||
<a-button v-else-if="showButton" type="primary" @click="goToLogin"> 登录 </a-button>
|
||||
|
||||
|
||||
<!-- 调试面板 Modal -->
|
||||
<DebugComponent v-model:show="showDebug" />
|
||||
</div>
|
||||
@ -316,7 +315,6 @@ const openProfile = () => {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
:deep(.ant-dropdown-menu) {
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
@ -109,7 +109,10 @@
|
||||
<a-input v-model:value="editForm.slug" disabled />
|
||||
</a-form-item>
|
||||
<a-form-item label="MCP 名称" required class="form-item">
|
||||
<a-input v-model:value="editForm.name" placeholder="请输入 MCP 展示名称" />
|
||||
<a-input
|
||||
v-model:value="editForm.name"
|
||||
placeholder="请输入 MCP 展示名称"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="传输类型" required class="form-item">
|
||||
<a-select v-model:value="editForm.transport">
|
||||
|
||||
@ -54,14 +54,23 @@
|
||||
</template>
|
||||
</PageShoulder>
|
||||
|
||||
<div v-if="filteredInstalledSkills.length === 0" class="extension-card-grid-empty-state skill-empty-state">
|
||||
<div
|
||||
v-if="filteredInstalledSkills.length === 0"
|
||||
class="extension-card-grid-empty-state skill-empty-state"
|
||||
>
|
||||
<div class="skill-empty-card">
|
||||
<div class="skill-empty-icon">
|
||||
<BookMarked :size="22" />
|
||||
</div>
|
||||
<div class="skill-empty-title">{{ searchQuery ? '没有匹配的 Skill' : '还没有添加 Skill' }}</div>
|
||||
<div class="skill-empty-title">
|
||||
{{ searchQuery ? '没有匹配的 Skill' : '还没有添加 Skill' }}
|
||||
</div>
|
||||
<div class="skill-empty-desc">
|
||||
{{ searchQuery ? '换个关键词试试,或清空搜索条件。' : '可以从远程仓库安装,或上传本地 Skill 文件。' }}
|
||||
{{
|
||||
searchQuery
|
||||
? '换个关键词试试,或清空搜索条件。'
|
||||
: '可以从远程仓库安装,或上传本地 Skill 文件。'
|
||||
}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -409,7 +418,9 @@
|
||||
</div>
|
||||
</div>
|
||||
<a-tag v-if="item.success === false" color="red">解析失败</a-tag>
|
||||
<a-tag v-else color="blue">{{ sourceTypeLabel(item.source_type || pendingDraft.source_type) }}</a-tag>
|
||||
<a-tag v-else color="blue">{{
|
||||
sourceTypeLabel(item.source_type || pendingDraft.source_type)
|
||||
}}</a-tag>
|
||||
</div>
|
||||
</div>
|
||||
<div class="draft-share-title">生效范围</div>
|
||||
@ -488,7 +499,9 @@ const installedSkillCards = computed(() =>
|
||||
sourceType,
|
||||
sourceLabel: sourceTypeLabel(sourceType),
|
||||
status:
|
||||
skill.enabled === false ? { label: '已禁用', level: 'default' } : { label: '已启用', level: 'success' }
|
||||
skill.enabled === false
|
||||
? { label: '已禁用', level: 'default' }
|
||||
: { label: '已启用', level: 'success' }
|
||||
}
|
||||
})
|
||||
)
|
||||
@ -727,7 +740,9 @@ const normalizePendingDraft = (draftPayload) => {
|
||||
const openDraftConfirmation = async (draftPayload) => {
|
||||
const draft = normalizePendingDraft(draftPayload)
|
||||
if (!draft.draft_ids.length || !draft.items.some((item) => item.success !== false)) {
|
||||
await Promise.allSettled(draft.draft_ids.map((draftId) => skillApi.discardSkillInstallDraft(draftId)))
|
||||
await Promise.allSettled(
|
||||
draft.draft_ids.map((draftId) => skillApi.discardSkillInstallDraft(draftId))
|
||||
)
|
||||
message.error('没有可添加的 Skill')
|
||||
return false
|
||||
}
|
||||
@ -1006,7 +1021,6 @@ defineExpose({
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
|
||||
.card-wrapper {
|
||||
position: relative;
|
||||
|
||||
@ -1174,7 +1188,6 @@ defineExpose({
|
||||
padding: 4px 0 8px 0;
|
||||
}
|
||||
|
||||
|
||||
.skills-list-section {
|
||||
margin-top: 12px;
|
||||
border: 1px solid var(--gray-150);
|
||||
@ -1310,7 +1323,6 @@ defineExpose({
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.modal-footer-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
@ -190,7 +190,9 @@
|
||||
<div class="dependency-title-block">
|
||||
<div class="dependency-title-row">
|
||||
<h4>{{ group.title }}</h4>
|
||||
<span class="dependency-count">已选择 {{ getDependencyValues(group).length }} 项</span>
|
||||
<span class="dependency-count"
|
||||
>已选择 {{ getDependencyValues(group).length }} 项</span
|
||||
>
|
||||
</div>
|
||||
<p>{{ group.description }}</p>
|
||||
</div>
|
||||
@ -220,7 +222,10 @@
|
||||
@mousedown.stop
|
||||
@click.stop
|
||||
/>
|
||||
<div v-if="getFilteredDependencyOptions(group).length" class="selection-list">
|
||||
<div
|
||||
v-if="getFilteredDependencyOptions(group).length"
|
||||
class="selection-list"
|
||||
>
|
||||
<div
|
||||
v-for="option in getFilteredDependencyOptions(group)"
|
||||
:key="option.value"
|
||||
@ -230,15 +235,35 @@
|
||||
class="selection-item"
|
||||
:class="{ selected: isDependencySelected(group, option.value) }"
|
||||
@mousedown.stop
|
||||
@click.stop="toggleDependency(group, option.value, !isDependencySelected(group, option.value))"
|
||||
@keydown.enter.prevent="toggleDependency(group, option.value, !isDependencySelected(group, option.value))"
|
||||
@keydown.space.prevent="toggleDependency(group, option.value, !isDependencySelected(group, option.value))"
|
||||
@click.stop="
|
||||
toggleDependency(
|
||||
group,
|
||||
option.value,
|
||||
!isDependencySelected(group, option.value)
|
||||
)
|
||||
"
|
||||
@keydown.enter.prevent="
|
||||
toggleDependency(
|
||||
group,
|
||||
option.value,
|
||||
!isDependencySelected(group, option.value)
|
||||
)
|
||||
"
|
||||
@keydown.space.prevent="
|
||||
toggleDependency(
|
||||
group,
|
||||
option.value,
|
||||
!isDependencySelected(group, option.value)
|
||||
)
|
||||
"
|
||||
>
|
||||
<span class="selection-item-content">
|
||||
<a-checkbox
|
||||
:checked="isDependencySelected(group, option.value)"
|
||||
@click.stop
|
||||
@change="toggleDependency(group, option.value, $event.target.checked)"
|
||||
@change="
|
||||
toggleDependency(group, option.value, $event.target.checked)
|
||||
"
|
||||
/>
|
||||
<span class="selection-label">{{ option.label }}</span>
|
||||
</span>
|
||||
@ -250,7 +275,9 @@
|
||||
</div>
|
||||
</template>
|
||||
</a-dropdown>
|
||||
<a-button v-else size="small" disabled class="dependency-action-btn">系统维护</a-button>
|
||||
<a-button v-else size="small" disabled class="dependency-action-btn"
|
||||
>系统维护</a-button
|
||||
>
|
||||
</div>
|
||||
|
||||
<div v-if="getDependencyValues(group).length" class="dependency-chip-list">
|
||||
@ -388,7 +415,9 @@ const selectedFilePreview = computed(() => ({
|
||||
|
||||
const toolDependencyOptions = computed(() =>
|
||||
(dependencyOptions.tools || []).map((i) =>
|
||||
typeof i === 'object' ? { label: i.name || i.slug, value: i.slug || i.id } : { label: i, value: i }
|
||||
typeof i === 'object'
|
||||
? { label: i.name || i.slug, value: i.slug || i.id }
|
||||
: { label: i, value: i }
|
||||
)
|
||||
)
|
||||
const mcpDependencyOptions = computed(() =>
|
||||
@ -441,7 +470,9 @@ const getDependencyOptionLabel = (group, value) => {
|
||||
}
|
||||
|
||||
const getFilteredDependencyOptions = (group) => {
|
||||
const keyword = String(dependencySearch[group.key] || '').trim().toLowerCase()
|
||||
const keyword = String(dependencySearch[group.key] || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!keyword) return group.options
|
||||
return group.options.filter((option) => {
|
||||
const label = String(option.label || '').toLowerCase()
|
||||
|
||||
@ -67,7 +67,10 @@
|
||||
>
|
||||
<a-form layout="vertical" class="extension-form">
|
||||
<a-form-item label="标识" required class="form-item">
|
||||
<a-input v-model:value="form.slug" placeholder="请输入 SubAgent 稳定标识,如 research-agent" />
|
||||
<a-input
|
||||
v-model:value="form.slug"
|
||||
placeholder="请输入 SubAgent 稳定标识,如 research-agent"
|
||||
/>
|
||||
</a-form-item>
|
||||
<a-form-item label="名称" required class="form-item">
|
||||
<a-input v-model:value="form.name" placeholder="请输入 SubAgent 展示名称" />
|
||||
@ -166,7 +169,14 @@ const navigateToDetail = (agent) => {
|
||||
}
|
||||
|
||||
const handleSubagentAdd = () => {
|
||||
Object.assign(form, { slug: '', name: '', description: '', system_prompt: '', tools: [], model: '' })
|
||||
Object.assign(form, {
|
||||
slug: '',
|
||||
name: '',
|
||||
description: '',
|
||||
system_prompt: '',
|
||||
tools: [],
|
||||
model: ''
|
||||
})
|
||||
formModalVisible.value = true
|
||||
}
|
||||
|
||||
|
||||
@ -82,11 +82,7 @@
|
||||
</a-form-item>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-form-item
|
||||
name="neighbors_count"
|
||||
:labelCol="{ span: 24 }"
|
||||
:wrapperCol="{ span: 24 }"
|
||||
>
|
||||
<a-form-item name="neighbors_count" :labelCol="{ span: 24 }" :wrapperCol="{ span: 24 }">
|
||||
<template #label>
|
||||
<span class="field-label-with-help">
|
||||
候选 Chunk 数量
|
||||
@ -168,7 +164,12 @@
|
||||
</div>
|
||||
<div class="footer-actions">
|
||||
<a-button :disabled="generating" @click="handleCancel">取消</a-button>
|
||||
<a-button type="primary" :loading="generating" :disabled="generating" @click="handleGenerate">
|
||||
<a-button
|
||||
type="primary"
|
||||
:loading="generating"
|
||||
:disabled="generating"
|
||||
@click="handleGenerate"
|
||||
>
|
||||
确定
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
@ -76,9 +76,15 @@ const filteredAgents = computed(() => {
|
||||
const filtered = keyword
|
||||
? list.filter(
|
||||
(agent) =>
|
||||
String(agent.name || '').toLowerCase().includes(keyword) ||
|
||||
String(agent.id || '').toLowerCase().includes(keyword) ||
|
||||
String(agent.backend_id || '').toLowerCase().includes(keyword)
|
||||
String(agent.name || '')
|
||||
.toLowerCase()
|
||||
.includes(keyword) ||
|
||||
String(agent.id || '')
|
||||
.toLowerCase()
|
||||
.includes(keyword) ||
|
||||
String(agent.backend_id || '')
|
||||
.toLowerCase()
|
||||
.includes(keyword)
|
||||
)
|
||||
: list
|
||||
return [...filtered].sort((a, b) => {
|
||||
@ -128,7 +134,9 @@ const agentPreviewIcon = computed(() => agentForm.icon?.trim() || defaultAgentIc
|
||||
const selectedBackendOption = computed(() =>
|
||||
agentBackendOptions.value.find((backend) => backend.value === agentForm.backend_id)
|
||||
)
|
||||
const selectedBackendLabel = computed(() => selectedBackendOption.value?.label || agentForm.backend_id || '未选择')
|
||||
const selectedBackendLabel = computed(
|
||||
() => selectedBackendOption.value?.label || agentForm.backend_id || '未选择'
|
||||
)
|
||||
const selectedBackendIcon = computed(() => {
|
||||
const backendText = `${agentForm.backend_id} ${selectedBackendLabel.value}`.toLowerCase()
|
||||
return backendText.includes('deep') || backendText.includes('search') ? Microscope : Bot
|
||||
@ -252,7 +260,9 @@ const saveAgent = async () => {
|
||||
return
|
||||
}
|
||||
|
||||
const validation = canEditAgentShareConfig.value ? agentShareConfigFormRef.value?.validate?.() : null
|
||||
const validation = canEditAgentShareConfig.value
|
||||
? agentShareConfigFormRef.value?.validate?.()
|
||||
: null
|
||||
if (validation && !validation.valid) {
|
||||
agentModalActiveTab.value = 'basic'
|
||||
message.error(validation.message)
|
||||
@ -264,7 +274,10 @@ const saveAgent = async () => {
|
||||
const payload = buildAgentPayload()
|
||||
if (editingAgentId.value) {
|
||||
const validatedConfig = runtimeConfigFormRef.value?.validateAndFilterConfig?.()
|
||||
if (validatedConfig && JSON.stringify(validatedConfig) !== JSON.stringify(agentStore.agentConfig)) {
|
||||
if (
|
||||
validatedConfig &&
|
||||
JSON.stringify(validatedConfig) !== JSON.stringify(agentStore.agentConfig)
|
||||
) {
|
||||
agentStore.updateAgentConfig(validatedConfig)
|
||||
}
|
||||
if (agentStore.hasConfigChanges) {
|
||||
@ -320,73 +333,73 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="agent-manage-panel">
|
||||
<PageShoulder v-model:search="searchQuery" search-placeholder="搜索智能体...">
|
||||
<template #actions>
|
||||
<a-button type="primary" class="lucide-icon-btn" @click="openCreateAgentModal">
|
||||
<Plus :size="14" />
|
||||
新增智能体
|
||||
</a-button>
|
||||
<a-button class="lucide-icon-btn" @click="loadAgents" :loading="agentLoading">
|
||||
<RefreshCw :size="14" :class="{ spinning: agentLoading }" />
|
||||
</a-button>
|
||||
</template>
|
||||
</PageShoulder>
|
||||
<PageShoulder v-model:search="searchQuery" search-placeholder="搜索智能体...">
|
||||
<template #actions>
|
||||
<a-button type="primary" class="lucide-icon-btn" @click="openCreateAgentModal">
|
||||
<Plus :size="14" />
|
||||
新增智能体
|
||||
</a-button>
|
||||
<a-button class="lucide-icon-btn" @click="loadAgents" :loading="agentLoading">
|
||||
<RefreshCw :size="14" :class="{ spinning: agentLoading }" />
|
||||
</a-button>
|
||||
</template>
|
||||
</PageShoulder>
|
||||
|
||||
<ExtensionCardGrid :min-width="320">
|
||||
<InfoCard
|
||||
v-for="agent in filteredAgents"
|
||||
:key="agent.id"
|
||||
:title="agent.name"
|
||||
:subtitle="agent.slug || agent.id"
|
||||
:description="agent.description || '暂无描述'"
|
||||
:default-icon="Bot"
|
||||
:tags="agent.backend_id ? [{ name: agent.backend_id, color: 'blue' }] : []"
|
||||
class="config-card agent-card"
|
||||
@click="canManageAgent(agent) && openEditAgentModal(agent)"
|
||||
>
|
||||
<template #icon>
|
||||
<img
|
||||
class="agent-card-icon-image"
|
||||
:src="getAgentIconSrc(agent)"
|
||||
:alt="`${agent.name || '智能体'}图标`"
|
||||
/>
|
||||
</template>
|
||||
<ExtensionCardGrid :min-width="320">
|
||||
<InfoCard
|
||||
v-for="agent in filteredAgents"
|
||||
:key="agent.id"
|
||||
:title="agent.name"
|
||||
:subtitle="agent.slug || agent.id"
|
||||
:description="agent.description || '暂无描述'"
|
||||
:default-icon="Bot"
|
||||
:tags="agent.backend_id ? [{ name: agent.backend_id, color: 'blue' }] : []"
|
||||
class="config-card agent-card"
|
||||
@click="canManageAgent(agent) && openEditAgentModal(agent)"
|
||||
>
|
||||
<template #icon>
|
||||
<img
|
||||
class="agent-card-icon-image"
|
||||
:src="getAgentIconSrc(agent)"
|
||||
:alt="`${agent.name || '智能体'}图标`"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<template #status>
|
||||
<a-dropdown v-if="canManageAgent(agent)" :trigger="['click']" placement="bottomRight">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="edit" @click.stop="openEditAgentModal(agent)">
|
||||
<span class="agent-card-menu-item">
|
||||
<Edit3 :size="14" />
|
||||
编辑智能体
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
key="delete"
|
||||
:disabled="isBuiltinAgent(agent)"
|
||||
@click.stop="deleteAgent(agent)"
|
||||
>
|
||||
<span class="agent-card-menu-item" :class="{ danger: !isBuiltinAgent(agent) }">
|
||||
<Trash2 :size="14" />
|
||||
删除智能体
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="agent-card-menu-trigger"
|
||||
aria-label="智能体操作"
|
||||
@click.stop
|
||||
<template #status>
|
||||
<a-dropdown v-if="canManageAgent(agent)" :trigger="['click']" placement="bottomRight">
|
||||
<template #overlay>
|
||||
<a-menu>
|
||||
<a-menu-item key="edit" @click.stop="openEditAgentModal(agent)">
|
||||
<span class="agent-card-menu-item">
|
||||
<Edit3 :size="14" />
|
||||
编辑智能体
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item
|
||||
key="delete"
|
||||
:disabled="isBuiltinAgent(agent)"
|
||||
@click.stop="deleteAgent(agent)"
|
||||
>
|
||||
<MoreVertical :size="16" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
<span class="agent-card-menu-item" :class="{ danger: !isBuiltinAgent(agent) }">
|
||||
<Trash2 :size="14" />
|
||||
删除智能体
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</ExtensionCardGrid>
|
||||
<a-button
|
||||
type="text"
|
||||
size="small"
|
||||
class="agent-card-menu-trigger"
|
||||
aria-label="智能体操作"
|
||||
@click.stop
|
||||
>
|
||||
<MoreVertical :size="16" />
|
||||
</a-button>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</ExtensionCardGrid>
|
||||
|
||||
<!-- Agent Edit Modal -->
|
||||
<a-modal
|
||||
@ -420,7 +433,10 @@ defineExpose({
|
||||
<component :is="item.icon" :size="16" />
|
||||
<span>{{ item.label }}</span>
|
||||
</span>
|
||||
<span v-if="item.key === 'model' && agentStore.hasConfigChanges" class="nav-dirty-dot" />
|
||||
<span
|
||||
v-if="item.key === 'model' && agentStore.hasConfigChanges"
|
||||
class="nav-dirty-dot"
|
||||
/>
|
||||
</button>
|
||||
</aside>
|
||||
|
||||
@ -460,10 +476,16 @@ defineExpose({
|
||||
placeholder="标识可选,留空自动生成"
|
||||
aria-label="智能体标识"
|
||||
/>
|
||||
<span v-else class="agent-inline-slug">{{ agentForm.slug || editingAgentId }}</span>
|
||||
<span v-else class="agent-inline-slug">{{
|
||||
agentForm.slug || editingAgentId
|
||||
}}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="agent-backend-summary" :class="{ editable: !editingAgentId }" aria-label="智能体后端">
|
||||
<div
|
||||
class="agent-backend-summary"
|
||||
:class="{ editable: !editingAgentId }"
|
||||
aria-label="智能体后端"
|
||||
>
|
||||
<span class="agent-backend-icon">
|
||||
<component :is="selectedBackendIcon" :size="16" />
|
||||
</span>
|
||||
@ -521,7 +543,6 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="less" scoped>
|
||||
.agent-manage-panel {
|
||||
height: 100%;
|
||||
|
||||
@ -619,54 +619,54 @@ defineExpose({
|
||||
|
||||
<template>
|
||||
<div class="model-provider-manage-panel">
|
||||
<PageShoulder v-model:search="searchQuery" search-placeholder="搜索供应商...">
|
||||
<template #actions>
|
||||
<a-button type="primary" class="lucide-icon-btn" @click="openCreateProviderModal">
|
||||
<Plus :size="14" />
|
||||
新增供应商
|
||||
</a-button>
|
||||
<a-button class="lucide-icon-btn" @click="loadProviders" :loading="loading">
|
||||
<RefreshCw :size="14" :class="{ spinning: loading }" />
|
||||
</a-button>
|
||||
</template>
|
||||
</PageShoulder>
|
||||
<PageShoulder v-model:search="searchQuery" search-placeholder="搜索供应商...">
|
||||
<template #actions>
|
||||
<a-button type="primary" class="lucide-icon-btn" @click="openCreateProviderModal">
|
||||
<Plus :size="14" />
|
||||
新增供应商
|
||||
</a-button>
|
||||
<a-button class="lucide-icon-btn" @click="loadProviders" :loading="loading">
|
||||
<RefreshCw :size="14" :class="{ spinning: loading }" />
|
||||
</a-button>
|
||||
</template>
|
||||
</PageShoulder>
|
||||
|
||||
<ExtensionCardGrid :min-width="320">
|
||||
<InfoCard
|
||||
v-for="provider in filteredProviders"
|
||||
:key="provider.provider_id"
|
||||
:title="provider.display_name"
|
||||
:subtitle="provider.provider_id"
|
||||
:disabled="!provider.is_enabled"
|
||||
:default-icon="Globe"
|
||||
:info="getProviderInfo(provider)"
|
||||
:status="getProviderStatus(provider)"
|
||||
@click="openEditProviderModal(provider)"
|
||||
>
|
||||
<template #icon>
|
||||
<img
|
||||
v-if="getProviderIcon(provider)"
|
||||
:src="getIconUrl(getProviderIcon(provider))"
|
||||
:alt="provider.display_name"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="view-models-btn" type="button" @click.stop="openModelsModal(provider)">
|
||||
<Settings2 :size="14" />
|
||||
管理模型
|
||||
</button>
|
||||
<a-tooltip title="编辑供应商">
|
||||
<a-button
|
||||
size="small"
|
||||
class="lucide-icon-btn"
|
||||
@click.stop="openEditProviderModal(provider)"
|
||||
>
|
||||
<Edit3 :size="14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</ExtensionCardGrid>
|
||||
<ExtensionCardGrid :min-width="320">
|
||||
<InfoCard
|
||||
v-for="provider in filteredProviders"
|
||||
:key="provider.provider_id"
|
||||
:title="provider.display_name"
|
||||
:subtitle="provider.provider_id"
|
||||
:disabled="!provider.is_enabled"
|
||||
:default-icon="Globe"
|
||||
:info="getProviderInfo(provider)"
|
||||
:status="getProviderStatus(provider)"
|
||||
@click="openEditProviderModal(provider)"
|
||||
>
|
||||
<template #icon>
|
||||
<img
|
||||
v-if="getProviderIcon(provider)"
|
||||
:src="getIconUrl(getProviderIcon(provider))"
|
||||
:alt="provider.display_name"
|
||||
/>
|
||||
</template>
|
||||
<template #footer>
|
||||
<button class="view-models-btn" type="button" @click.stop="openModelsModal(provider)">
|
||||
<Settings2 :size="14" />
|
||||
管理模型
|
||||
</button>
|
||||
<a-tooltip title="编辑供应商">
|
||||
<a-button
|
||||
size="small"
|
||||
class="lucide-icon-btn"
|
||||
@click.stop="openEditProviderModal(provider)"
|
||||
>
|
||||
<Edit3 :size="14" />
|
||||
</a-button>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
</InfoCard>
|
||||
</ExtensionCardGrid>
|
||||
|
||||
<!-- Provider Edit Modal -->
|
||||
<a-modal
|
||||
@ -906,8 +906,16 @@ defineExpose({
|
||||
size="small"
|
||||
class="model-test-button"
|
||||
:title="getModelTestTitle(currentProviderForModels.provider_id, model)"
|
||||
:loading="modelTestLoadingBySpec[buildModelSpec(currentProviderForModels.provider_id, model.id)]"
|
||||
:disabled="modelTestLoadingBySpec[buildModelSpec(currentProviderForModels.provider_id, model.id)]"
|
||||
:loading="
|
||||
modelTestLoadingBySpec[
|
||||
buildModelSpec(currentProviderForModels.provider_id, model.id)
|
||||
]
|
||||
"
|
||||
:disabled="
|
||||
modelTestLoadingBySpec[
|
||||
buildModelSpec(currentProviderForModels.provider_id, model.id)
|
||||
]
|
||||
"
|
||||
@click="testModelConnection(currentProviderForModels.provider_id, model)"
|
||||
>
|
||||
测试
|
||||
@ -1065,7 +1073,6 @@ defineExpose({
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
<style lang="less" scoped>
|
||||
.model-provider-manage-panel {
|
||||
height: 100%;
|
||||
|
||||
@ -44,7 +44,11 @@
|
||||
</div>
|
||||
|
||||
<div class="file-table" role="table" aria-label="工作区文件列表">
|
||||
<div class="file-row table-head" :class="{ 'selection-enabled': effectiveSelectionMode }" role="row">
|
||||
<div
|
||||
class="file-row table-head"
|
||||
:class="{ 'selection-enabled': effectiveSelectionMode }"
|
||||
role="row"
|
||||
>
|
||||
<span v-if="effectiveSelectionMode" class="selection-cell">
|
||||
<a-checkbox
|
||||
:checked="allSelected"
|
||||
@ -115,7 +119,12 @@
|
||||
<span>下载</span>
|
||||
</span>
|
||||
</a-menu-item>
|
||||
<a-menu-item v-if="!readonly" key="delete" danger @click="$emit('delete-entry', entry)">
|
||||
<a-menu-item
|
||||
v-if="!readonly"
|
||||
key="delete"
|
||||
danger
|
||||
@click="$emit('delete-entry', entry)"
|
||||
>
|
||||
<span class="menu-item-content">
|
||||
<Trash2 :size="14" />
|
||||
<span>删除</span>
|
||||
|
||||
@ -204,4 +204,4 @@ const sharedDatabases = computed(() =>
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
</style>
|
||||
</style>
|
||||
|
||||
@ -2,7 +2,6 @@ import { message } from 'ant-design-vue'
|
||||
import { handleChatError } from '@/utils/errorHandler'
|
||||
import { unref } from 'vue'
|
||||
|
||||
|
||||
export function useAgentStreamHandler({
|
||||
getThreadState,
|
||||
processApprovalInStream,
|
||||
@ -68,7 +67,6 @@ export function useAgentStreamHandler({
|
||||
threadState.isStreaming = false
|
||||
threadState.replyLoadingVisible = false
|
||||
threadState.pendingRequestId = null
|
||||
|
||||
}
|
||||
return true
|
||||
|
||||
|
||||
@ -278,7 +278,7 @@ provide('settingsModal', {
|
||||
:to="item.path"
|
||||
v-show="!item.hidden"
|
||||
class="nav-item"
|
||||
:class="{ active: isNavItemActive(item)}"
|
||||
:class="{ active: isNavItemActive(item) }"
|
||||
:active-class="item.action ? '' : 'active'"
|
||||
@click.stop
|
||||
>
|
||||
@ -805,5 +805,4 @@ div.header,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
</style>
|
||||
|
||||
@ -6,7 +6,9 @@ import { handleChatError } from '@/utils/errorHandler'
|
||||
|
||||
function normalizeAgent(agent) {
|
||||
const agentId = agent?.agent_id || agent?.slug || agent?.id
|
||||
return agentId ? { ...agent, id: agentId, agent_id: agentId, slug: agent?.slug || agentId } : agent
|
||||
return agentId
|
||||
? { ...agent, id: agentId, agent_id: agentId, slug: agent?.slug || agentId }
|
||||
: agent
|
||||
}
|
||||
|
||||
export const BUILTIN_AGENT_ID = 'default-chatbot'
|
||||
@ -55,7 +57,9 @@ export const useAgentStore = defineStore(
|
||||
|
||||
const selectedAgent = computed(() => {
|
||||
const agentId = selectedAgentId.value
|
||||
return agentId ? agentDetails.value[agentId] || agents.value.find((a) => a.id === agentId) || null : null
|
||||
return agentId
|
||||
? agentDetails.value[agentId] || agents.value.find((a) => a.id === agentId) || null
|
||||
: null
|
||||
})
|
||||
|
||||
const agentsList = computed(() => agents.value)
|
||||
@ -131,7 +135,9 @@ export const useAgentStore = defineStore(
|
||||
function applyConfigDefaults(loadedConfig, configItems = configurableItems.value) {
|
||||
const items = { ...configItems }
|
||||
Object.keys(items).forEach((key) => {
|
||||
const item = items[key]?.x_oap_ui_config ? { ...items[key], ...items[key].x_oap_ui_config } : items[key]
|
||||
const item = items[key]?.x_oap_ui_config
|
||||
? { ...items[key], ...items[key].x_oap_ui_config }
|
||||
: items[key]
|
||||
const isDefaultAllList = isDefaultAllAgentResourceKind(item?.kind)
|
||||
if (loadedConfig[key] === undefined || (loadedConfig[key] === null && !isDefaultAllList)) {
|
||||
if (item.default !== undefined) loadedConfig[key] = item.default
|
||||
@ -177,7 +183,10 @@ export const useAgentStore = defineStore(
|
||||
isLoadingConfig.value = true
|
||||
try {
|
||||
const detail = await fetchAgentDetail(agentId)
|
||||
const loadedConfig = applyConfigDefaults(extractContext(detail), detail?.configurable_items || {})
|
||||
const loadedConfig = applyConfigDefaults(
|
||||
extractContext(detail),
|
||||
detail?.configurable_items || {}
|
||||
)
|
||||
selectedAgentId.value = agentId
|
||||
agentConfig.value = loadedConfig
|
||||
originalAgentConfig.value = { ...loadedConfig }
|
||||
@ -211,7 +220,10 @@ export const useAgentStore = defineStore(
|
||||
const created = normalizeAgent(response.agent)
|
||||
if (created?.id) {
|
||||
agentDetails.value[created.id] = created
|
||||
agents.value = sortAgents([created, ...agents.value.filter((item) => item.id !== created.id)])
|
||||
agents.value = sortAgents([
|
||||
created,
|
||||
...agents.value.filter((item) => item.id !== created.id)
|
||||
])
|
||||
await selectAgent(created.id)
|
||||
}
|
||||
return created
|
||||
|
||||
@ -22,7 +22,15 @@ export const getAgentConfigOptions = (item) => (Array.isArray(item?.options) ? i
|
||||
|
||||
export const getAgentConfigOptionValue = (option) => {
|
||||
if (typeof option !== 'object' || option === null) return option
|
||||
return option.key || option.id || option.value || option.name || option.db_id || option.slug || option.label
|
||||
return (
|
||||
option.key ||
|
||||
option.id ||
|
||||
option.value ||
|
||||
option.name ||
|
||||
option.db_id ||
|
||||
option.slug ||
|
||||
option.label
|
||||
)
|
||||
}
|
||||
|
||||
export const getAgentConfigOptionLabel = (option) => {
|
||||
|
||||
@ -74,9 +74,12 @@ const FILE_ICON_CONFIG = {
|
||||
}
|
||||
|
||||
const getFileIconConfig = (filename) => {
|
||||
const normalizedName = String(filename || '').trim().toLowerCase()
|
||||
const normalizedName = String(filename || '')
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
if (!normalizedName) return DEFAULT_FILE_ICON
|
||||
if (normalizedName.startsWith('http://') || normalizedName.startsWith('https://')) return LINK_FILE_ICON
|
||||
if (normalizedName.startsWith('http://') || normalizedName.startsWith('https://'))
|
||||
return LINK_FILE_ICON
|
||||
|
||||
const cleanName = normalizedName.split(/[?#]/)[0]
|
||||
const extension = cleanName.includes('.') ? cleanName.split('.').pop() : cleanName
|
||||
|
||||
@ -4,7 +4,8 @@ import { Database, DatabaseZap } from 'lucide-vue-next'
|
||||
const ICON_BASE = 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons'
|
||||
|
||||
const createBrandIcon = (url) => {
|
||||
const Icon = ({ size = 20 }) => h('img', { src: url, style: { width: size + 'px', height: size + 'px' } })
|
||||
const Icon = ({ size = 20 }) =>
|
||||
h('img', { src: url, style: { width: size + 'px', height: size + 'px' } })
|
||||
Icon.inheritAttrs = false
|
||||
return Icon
|
||||
}
|
||||
@ -44,7 +45,9 @@ export const getKbTypeColor = (type) => {
|
||||
const READ_ONLY_KB_TYPES = new Set(['dify', 'notion'])
|
||||
|
||||
export const isReadOnlyDatabase = (database, kbTypes = {}) => {
|
||||
const kbType = (typeof database === 'string' ? database : database?.kb_type || 'milvus').toLowerCase()
|
||||
const kbType = (
|
||||
typeof database === 'string' ? database : database?.kb_type || 'milvus'
|
||||
).toLowerCase()
|
||||
|
||||
if (database?.supports_documents !== undefined) {
|
||||
return database.supports_documents === false
|
||||
|
||||
@ -9,9 +9,15 @@ export const mentionTypePrefixMap = {
|
||||
}
|
||||
|
||||
const mentionTypePattern = Object.values(mentionTypePrefixMap).join('|')
|
||||
const mentionTokenRegex = new RegExp(`@(${mentionTypePattern}):(?:"((?:\\\\.|[^"\\\\])*)"|(\\S+))`, 'g')
|
||||
const mentionTokenRegex = new RegExp(
|
||||
`@(${mentionTypePattern}):(?:"((?:\\\\.|[^"\\\\])*)"|(\\S+))`,
|
||||
'g'
|
||||
)
|
||||
|
||||
const quoteMentionValue = (value) => String(value ?? '').replace(/\\/g, '\\\\').replace(/"/g, '\\"')
|
||||
const quoteMentionValue = (value) =>
|
||||
String(value ?? '')
|
||||
.replace(/\\/g, '\\\\')
|
||||
.replace(/"/g, '\\"')
|
||||
const unquoteMentionValue = (value) => String(value ?? '').replace(/\\(["\\])/g, '$1')
|
||||
|
||||
export const formatMentionToken = (type, value) => {
|
||||
@ -87,7 +93,6 @@ export const buildMentionDisplayLabels = (mention = {}) => {
|
||||
setMentionLabel(labels, 'knowledge', kb?.kb_id, label)
|
||||
setMentionLabel(labels, 'knowledge', kb?.value, label)
|
||||
})
|
||||
|
||||
;(mention.mcps || []).forEach((mcp) => {
|
||||
const label = mcp?.name || mcp?.label || mcp?.slug || mcp?.id || mcp?.value || ''
|
||||
setMentionLabel(labels, 'mcp', mcp?.slug, label)
|
||||
@ -95,7 +100,6 @@ export const buildMentionDisplayLabels = (mention = {}) => {
|
||||
setMentionLabel(labels, 'mcp', mcp?.value, label)
|
||||
setMentionLabel(labels, 'mcp', mcp?.name, label)
|
||||
})
|
||||
|
||||
;(mention.skills || []).forEach((skill) => {
|
||||
const label = skill?.name || skill?.label || skill?.slug || skill?.id || skill?.value || ''
|
||||
setMentionLabel(labels, 'skill', skill?.slug, label)
|
||||
@ -103,7 +107,6 @@ export const buildMentionDisplayLabels = (mention = {}) => {
|
||||
setMentionLabel(labels, 'skill', skill?.value, label)
|
||||
setMentionLabel(labels, 'skill', skill?.name, label)
|
||||
})
|
||||
|
||||
;(mention.subagents || []).forEach((subagent) => {
|
||||
const label = subagent?.name || subagent?.label || subagent?.id || subagent?.value || ''
|
||||
setMentionLabel(labels, 'subagent', subagent?.id, label)
|
||||
@ -153,7 +156,12 @@ export const replaceRawRange = (text = '', start = 0, end = start, replacement =
|
||||
return value.slice(0, safeStart) + replacement + value.slice(safeEnd)
|
||||
}
|
||||
|
||||
export const expandMentionDeletionRange = (text = '', start = 0, end = start, direction = 'backward') => {
|
||||
export const expandMentionDeletionRange = (
|
||||
text = '',
|
||||
start = 0,
|
||||
end = start,
|
||||
direction = 'backward'
|
||||
) => {
|
||||
const value = String(text || '')
|
||||
const safeStart = Math.max(0, Math.min(start, value.length))
|
||||
const safeEnd = Math.max(safeStart, Math.min(end, value.length))
|
||||
|
||||
@ -25,7 +25,11 @@
|
||||
</a-select>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-button type="primary" :disabled="!kbTypes.length" @click="state.openNewDatabaseModel = true">
|
||||
<a-button
|
||||
type="primary"
|
||||
:disabled="!kbTypes.length"
|
||||
@click="state.openNewDatabaseModel = true"
|
||||
>
|
||||
<PlusOutlined /> 新建知识库
|
||||
</a-button>
|
||||
</template>
|
||||
@ -100,7 +104,8 @@
|
||||
class="form-section compact-section"
|
||||
>
|
||||
<h3 class="section-title">
|
||||
{{ field.label || field.key }}<span v-if="field.required" class="required-mark">*</span>
|
||||
{{ field.label || field.key
|
||||
}}<span v-if="field.required" class="required-mark">*</span>
|
||||
</h3>
|
||||
<a-input-password
|
||||
v-if="field.type === 'password'"
|
||||
@ -150,7 +155,11 @@
|
||||
<!-- 共享配置 -->
|
||||
<div class="form-section compact-section">
|
||||
<h3 class="section-title">共享设置</h3>
|
||||
<ShareConfigForm ref="shareConfigFormRef" v-model="shareConfig" :auto-select-user-dept="true" />
|
||||
<ShareConfigForm
|
||||
ref="shareConfigFormRef"
|
||||
v-model="shareConfig"
|
||||
:auto-select-user-dept="true"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<template #footer>
|
||||
@ -305,9 +314,7 @@ const orderedKbTypes = computed(() => supportedKbTypes.value)
|
||||
|
||||
const selectedKbTypeInfo = computed(() => supportedKbTypes.value[newDatabase.kb_type] || null)
|
||||
|
||||
const createParamOptions = computed(
|
||||
() => selectedKbTypeInfo.value?.create_params?.options || []
|
||||
)
|
||||
const createParamOptions = computed(() => selectedKbTypeInfo.value?.create_params?.options || [])
|
||||
|
||||
const getKbTypeDescription = (typeInfo) => typeInfo?.description || ''
|
||||
|
||||
@ -401,13 +408,15 @@ const buildRequestData = () => {
|
||||
}
|
||||
|
||||
if (selectedKbTypeInfo.value?.requires_embedding_model) {
|
||||
requestData.embedding_model_spec = newDatabase.embedding_model_spec || configStore.config.embed_model
|
||||
requestData.embedding_model_spec =
|
||||
newDatabase.embedding_model_spec || configStore.config.embed_model
|
||||
requestData.additional_params.chunk_preset_id = newDatabase.chunk_preset_id || 'general'
|
||||
}
|
||||
|
||||
requestData.share_config = {
|
||||
access_level: shareConfig.value.access_level,
|
||||
department_ids: shareConfig.value.access_level === 'department' ? shareConfig.value.department_ids || [] : [],
|
||||
department_ids:
|
||||
shareConfig.value.access_level === 'department' ? shareConfig.value.department_ids || [] : [],
|
||||
user_uids: shareConfig.value.access_level === 'user' ? shareConfig.value.user_uids || [] : []
|
||||
}
|
||||
|
||||
|
||||
@ -249,7 +249,11 @@ const isBinaryPreview = (previewType) => previewType === 'image' || previewType
|
||||
const createBinaryPreviewUrl = async (entry, response) => {
|
||||
const downloadResponse =
|
||||
entry.source === 'knowledge'
|
||||
? await downloadWorkspaceKnowledgeFile(entry.kb_id, entry.file_id, response.variant || 'original')
|
||||
? await downloadWorkspaceKnowledgeFile(
|
||||
entry.kb_id,
|
||||
entry.file_id,
|
||||
response.variant || 'original'
|
||||
)
|
||||
: await downloadWorkspaceFile(entry.path)
|
||||
const blob = await downloadResponse.blob()
|
||||
return window.URL.createObjectURL(blob)
|
||||
@ -485,7 +489,11 @@ const selectKnowledgePath = async (path) => {
|
||||
const targetIndex = knowledgeBreadcrumbItems.value.findIndex((item) => item.path === path)
|
||||
if (targetIndex < 0) return
|
||||
const breadcrumbs = knowledgeBreadcrumbItems.value.slice(0, targetIndex + 1)
|
||||
await loadKnowledgeEntries(selectedDatabase.value, breadcrumbs.at(-1)?.parentId || null, breadcrumbs)
|
||||
await loadKnowledgeEntries(
|
||||
selectedDatabase.value,
|
||||
breadcrumbs.at(-1)?.parentId || null,
|
||||
breadcrumbs
|
||||
)
|
||||
}
|
||||
|
||||
const handleSelectListPath = async (path) => {
|
||||
@ -542,9 +550,15 @@ const handleSelectEntry = async (entry) => {
|
||||
const handleSwitchKnowledgeVariant = async (variant) => {
|
||||
const entry = selectedEntry.value
|
||||
if (!entry || entry.source !== 'knowledge' || !entry.kb_id || !entry.file_id) return
|
||||
if (previewFile.value?.variant === variant || previewFile.value?.previewVariant === variant) return
|
||||
if (previewFile.value?.variant === variant || previewFile.value?.previewVariant === variant)
|
||||
return
|
||||
|
||||
await loadKnowledgePreview(entry, variant, previewFile.value || entry, KNOWLEDGE_PREVIEW_SWITCH_MESSAGES)
|
||||
await loadKnowledgePreview(
|
||||
entry,
|
||||
variant,
|
||||
previewFile.value || entry,
|
||||
KNOWLEDGE_PREVIEW_SWITCH_MESSAGES
|
||||
)
|
||||
}
|
||||
|
||||
const closePreview = () => {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user