style: code format

This commit is contained in:
Wenjie Zhang 2026-05-31 13:40:15 +08:00
parent 31a7fc52d0
commit b6240a1b4f
74 changed files with 643 additions and 342 deletions

View File

@ -4,6 +4,9 @@ from yuxi.agents.base import BaseAgent
# 从 buildin 模块导入 agent_manager # 从 buildin 模块导入 agent_manager
from yuxi.agents.context import BaseContext from yuxi.agents.context import BaseContext
# MCP - Agent 层统一入口(自动过滤 disabled_tools
from yuxi.agents.mcp.service import get_enabled_mcp_tools
# Model utilities - 模型加载 # Model utilities - 模型加载
from yuxi.agents.models import load_chat_model from yuxi.agents.models import load_chat_model
from yuxi.agents.state import BaseState from yuxi.agents.state import BaseState
@ -11,9 +14,6 @@ from yuxi.agents.state import BaseState
# Tools - 核心工具函数 # Tools - 核心工具函数
from yuxi.agents.toolkits.utils import get_tool_info from yuxi.agents.toolkits.utils import get_tool_info
# MCP - Agent 层统一入口(自动过滤 disabled_tools
from yuxi.agents.mcp.service import get_enabled_mcp_tools
__all__ = [ __all__ = [
# Base classes # Base classes
"BaseAgent", "BaseAgent",

View File

@ -33,7 +33,6 @@ from yuxi.utils.paths import (
from .provider import get_sandbox_provider, sandbox_id_for_thread from .provider import get_sandbox_provider, sandbox_id_for_thread
_USER_DATA_ROOT = "/" + VIRTUAL_PATH_PREFIX.strip("/") _USER_DATA_ROOT = "/" + VIRTUAL_PATH_PREFIX.strip("/")
_WORKSPACE_ROOT = f"{_USER_DATA_ROOT}/{WORKSPACE_DIR_NAME}" _WORKSPACE_ROOT = f"{_USER_DATA_ROOT}/{WORKSPACE_DIR_NAME}"
_UPLOADS_ROOT = f"{_USER_DATA_ROOT}/{UPLOADS_DIR_NAME}" _UPLOADS_ROOT = f"{_USER_DATA_ROOT}/{UPLOADS_DIR_NAME}"

View File

@ -3,9 +3,9 @@ import importlib
import inspect import inspect
from pathlib import Path from pathlib import Path
from yuxi.utils.singleton import SingletonMeta
from yuxi.agents.base import BaseAgent from yuxi.agents.base import BaseAgent
from yuxi.utils import logger from yuxi.utils import logger
from yuxi.utils.singleton import SingletonMeta
class AgentManager(metaclass=SingletonMeta): class AgentManager(metaclass=SingletonMeta):

View File

@ -17,8 +17,8 @@ from yuxi.agents.middlewares import (
) )
from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware from yuxi.agents.middlewares.knowledge_base_middleware import KnowledgeBaseMiddleware
from yuxi.agents.middlewares.skills_middleware import SkillsMiddleware 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.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.agents.toolkits.service import resolve_configured_runtime_tools
from yuxi.utils import logger from yuxi.utils import logger
from yuxi.utils.datetime_utils import shanghai_now from yuxi.utils.datetime_utils import shanghai_now

View File

@ -17,6 +17,7 @@ from typing import Any, cast
from langchain_mcp_adapters.client import MultiServerMCPClient from langchain_mcp_adapters.client import MultiServerMCPClient
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.storage.postgres.models_business import MCPServer from yuxi.storage.postgres.models_business import MCPServer
from yuxi.utils import logger from yuxi.utils import logger

View File

@ -14,10 +14,10 @@ from langchain.tools.tool_node import ToolCallRequest
from langgraph.types import Command from langgraph.types import Command
from sqlalchemy.ext.asyncio import AsyncSession 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.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.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.storage.postgres.manager import pg_manager
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger

View File

@ -10,6 +10,7 @@ from pathlib import Path
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.skills.service import import_skill_dir, is_valid_skill_slug from yuxi.agents.skills.service import import_skill_dir, is_valid_skill_slug
if TYPE_CHECKING: if TYPE_CHECKING:

View File

@ -16,9 +16,10 @@ from typing import Any
import yaml import yaml
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from yuxi import config as sys_config 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.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.storage.postgres.models_business import Skill, User
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger

View File

@ -8,6 +8,7 @@ from typing import Any
from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT from deepagents.middleware.subagents import GENERAL_PURPOSE_SUBAGENT
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.agents.subagents.repository import SubAgentRepository from yuxi.agents.subagents.repository import SubAgentRepository
from yuxi.storage.postgres.manager import pg_manager from yuxi.storage.postgres.manager import pg_manager
from yuxi.storage.postgres.models_business import SubAgent from yuxi.storage.postgres.models_business import SubAgent

View File

@ -10,7 +10,6 @@ from langgraph.prebuilt.tool_node import ToolRuntime
from langgraph.types import Command, interrupt from langgraph.types import Command, interrupt
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from yuxi import config
from yuxi.agents.toolkits.registry import ToolExtraMetadata, _all_tool_instances, _extra_registry, tool from yuxi.agents.toolkits.registry import ToolExtraMetadata, _all_tool_instances, _extra_registry, tool
from yuxi.storage.minio import aupload_file_to_minio from yuxi.storage.minio import aupload_file_to_minio
from yuxi.utils import logger from yuxi.utils import logger

View File

@ -4,11 +4,10 @@ import inspect
from typing import Any from typing import Any
from langgraph.prebuilt.tool_node import ToolRuntime from langgraph.prebuilt.tool_node import ToolRuntime
from yuxi.agents.toolkits.registry import tool
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from yuxi import knowledge_base from yuxi import knowledge_base
from yuxi.agents.toolkits.registry import tool
from yuxi.knowledge.base import KnowledgeBase from yuxi.knowledge.base import KnowledgeBase
from yuxi.knowledge.schemas import ( from yuxi.knowledge.schemas import (
FindInputSchema, FindInputSchema,
@ -226,11 +225,7 @@ async def query_kb(kb_id: str, query_text: str, file_name: str | None = None, ru
else: else:
result = retriever(query_text, **kwargs) result = retriever(query_text, **kwargs)
if ( if isinstance(result, dict) and result.get("kb_id") == target_kb_id and isinstance(result.get("results"), list):
isinstance(result, dict)
and result.get("kb_id") == target_kb_id
and isinstance(result.get("results"), list)
):
return SearchOutputSchema(**result).model_dump() return SearchOutputSchema(**result).model_dump()
return KnowledgeBase.build_search_output(target_kb_id, result) return KnowledgeBase.build_search_output(target_kb_id, result)

View File

@ -97,7 +97,9 @@ class Config(BaseModel):
self.sandbox_exec_timeout_seconds = int( self.sandbox_exec_timeout_seconds = int(
os.getenv("SANDBOX_EXEC_TIMEOUT_SECONDS") or self.sandbox_exec_timeout_seconds or 180 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( 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
) )

View File

@ -33,6 +33,7 @@ CHUNK_PRESET_DESCRIPTIONS: dict[str, str] = {
CHUNK_ENGINE_VERSION = "ragflow_like_v1" CHUNK_ENGINE_VERSION = "ragflow_like_v1"
GENERAL_INTERNAL_PARSER_ID = "naive" GENERAL_INTERNAL_PARSER_ID = "naive"
def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]: def deep_merge(base: dict[str, Any], override: dict[str, Any]) -> dict[str, Any]:
result = deepcopy(base) result = deepcopy(base)
for key, value in (override or {}).items(): for key, value in (override or {}).items():

View File

@ -4,7 +4,6 @@ from abc import ABC, abstractmethod
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
from yuxi.knowledge.graphs.graph_utils import normalize_entity_name from yuxi.knowledge.graphs.graph_utils import normalize_entity_name

View File

@ -8,7 +8,6 @@ from yuxi.models.chat import select_model
from .base import GraphExtractor from .base import GraphExtractor
DEFAULT_TRIPLE_EXTRACTION_PROMPT = """请从下面文本中抽取实体和实体关系,返回严格 JSON不要输出解释。 DEFAULT_TRIPLE_EXTRACTION_PROMPT = """请从下面文本中抽取实体和实体关系,返回严格 JSON不要输出解释。
JSON 格式 JSON 格式
{ {

View File

@ -15,6 +15,9 @@ from yuxi.knowledge.graphs.graph_utils import (
normalize_entity_name, normalize_entity_name,
) )
from yuxi.knowledge.graphs.milvus_graph_vector_store import MilvusGraphVectorStore 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 ( from yuxi.storage.neo4j import (
Neo4jConnectionManager, Neo4jConnectionManager,
get_shared_neo4j_connection, get_shared_neo4j_connection,
@ -22,9 +25,6 @@ from yuxi.storage.neo4j import (
neo4j_write, neo4j_write,
safe_neo4j_label, 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 import logger
from yuxi.utils.datetime_utils import utc_isoformat from yuxi.utils.datetime_utils import utc_isoformat

View File

@ -22,10 +22,10 @@ from pymilvus import (
from yuxi.knowledge.base import FileStatus, KnowledgeBase from yuxi.knowledge.base import FileStatus, KnowledgeBase
from yuxi.knowledge.chunking.ragflow_like.dispatcher import chunk_markdown 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.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 import hashstr, logger
from yuxi.utils.datetime_utils import utc_isoformat from yuxi.utils.datetime_utils import utc_isoformat

View File

@ -8,7 +8,6 @@ from yuxi.storage.postgres.models_business import User
from yuxi.utils import logger from yuxi.utils import logger
from yuxi.utils.datetime_utils import utc_isoformat from yuxi.utils.datetime_utils import utc_isoformat
DEFAULT_SHARE_CONFIG = {"access_level": "global", "department_ids": [], "user_uids": []} DEFAULT_SHARE_CONFIG = {"access_level": "global", "department_ids": [], "user_uids": []}
ACCESS_LEVELS = {"global", "department", "user"} ACCESS_LEVELS = {"global", "department", "user"}

View File

@ -5,7 +5,6 @@ from yuxi.knowledge.chunking.ragflow_like.presets import resolve_chunk_processin
from yuxi.utils import hashstr, logger from yuxi.utils import hashstr, logger
from yuxi.utils.datetime_utils import utc_isoformat from yuxi.utils.datetime_utils import utc_isoformat
_DROPPED_PROCESSING_PARAM_KEYS = { _DROPPED_PROCESSING_PARAM_KEYS = {
"_preprocessed_map", "_preprocessed_map",
"auto_index", "auto_index",

View File

@ -7,6 +7,7 @@ from typing import Any
import httpx import httpx
from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.ext.asyncio import AsyncSession
from yuxi.models.providers.builtin import BUILTIN_PROVIDERS from yuxi.models.providers.builtin import BUILTIN_PROVIDERS
from yuxi.models.providers.repository import ( from yuxi.models.providers.repository import (
create_model_provider, create_model_provider,

View File

@ -34,8 +34,6 @@ class MessageFeedbackRepository:
"""检查用户是否已对消息反馈""" """检查用户是否已对消息反馈"""
async with pg_manager.get_async_session_context() as session: async with pg_manager.get_async_session_context() as session:
result = await session.execute( result = await session.execute(
select(MessageFeedback.id).where( select(MessageFeedback.id).where(MessageFeedback.message_id == message_id, MessageFeedback.uid == uid)
MessageFeedback.message_id == message_id, MessageFeedback.uid == uid
)
) )
return result.scalar_one_or_none() is not None return result.scalar_one_or_none() is not None

View File

@ -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.buildin import agent_manager
from yuxi.agents.context import normalize_agent_context_config from yuxi.agents.context import normalize_agent_context_config
from yuxi.agents.state import AgentStatePayload from yuxi.agents.state import AgentStatePayload
from yuxi.utils.guard import content_guard
from yuxi.repositories.agent_repository import AgentRepository from yuxi.repositories.agent_repository import AgentRepository
from yuxi.repositories.conversation_repository import ConversationRepository from yuxi.repositories.conversation_repository import ConversationRepository
from yuxi.services.conversation_service import serialize_attachment 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.manager import pg_manager
from yuxi.storage.postgres.models_business import Agent, User 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.logging_config import logger
from yuxi.utils.question_utils import ( from yuxi.utils.question_utils import (
normalize_questions as _normalize_interrupt_questions, normalize_questions as _normalize_interrupt_questions,

View File

@ -16,12 +16,12 @@ from yuxi.knowledge.parser import Parser
from yuxi.repositories.agent_repository import AgentRepository from yuxi.repositories.agent_repository import AgentRepository
from yuxi.repositories.conversation_repository import ConversationRepository from yuxi.repositories.conversation_repository import ConversationRepository
from yuxi.services.mention_search_service import invalidate_mention_cache 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.minio import StorageError, get_minio_client
from yuxi.storage.postgres.models_business import User from yuxi.storage.postgres.models_business import User
from yuxi.utils.datetime_utils import utc_isoformat from yuxi.utils.datetime_utils import utc_isoformat
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger
from yuxi.utils.paths import VIRTUAL_PATH_UPLOADS 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, ...] = () ATTACHMENT_ALLOWED_EXTENSIONS: tuple[str, ...] = ()
MAX_ATTACHMENT_SIZE_BYTES = 5 * 1024 * 1024 # 5 MB MAX_ATTACHMENT_SIZE_BYTES = 5 * 1024 * 1024 # 5 MB

View File

@ -18,13 +18,12 @@ from pydantic import BaseModel, Field
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
from yuxi.repositories.user_repository import UserRepository 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.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.datetime_utils import utc_now_naive
from yuxi.utils.logging_config import logger 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 中的路由保持一致) # 前端 OIDC 回调路由路径(与 web/src/router/index.js 中的路由保持一致)
FRONTEND_CALLBACK_PATH = "/auth/oidc/callback" FRONTEND_CALLBACK_PATH = "/auth/oidc/callback"
# 登录页路径 # 登录页路径

View File

@ -10,16 +10,16 @@ from dataclasses import dataclass, field
from sqlalchemy import select from sqlalchemy import select
from sqlalchemy.exc import OperationalError 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.repositories.agent_run_repository import TERMINAL_RUN_STATUSES, AgentRunRepository
from yuxi.services.chat_service import stream_agent_chat, stream_agent_resume 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 ( from yuxi.services.run_queue_service import (
append_run_stream_event, append_run_stream_event,
clear_cancel_signal, clear_cancel_signal,
has_cancel_signal, has_cancel_signal,
wait_for_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.manager import pg_manager
from yuxi.storage.postgres.models_business import User from yuxi.storage.postgres.models_business import User
from yuxi.utils.logging_config import logger from yuxi.utils.logging_config import logger

View File

@ -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.file_preview import detect_preview_type
from yuxi.services.workspace_service import ( from yuxi.services.workspace_service import (
create_workspace_directory as create_workspace_directory_entry, create_workspace_directory as create_workspace_directory_entry,
)
from yuxi.services.workspace_service import (
delete_workspace_path, delete_workspace_path,
download_workspace_file as download_workspace_file_response,
list_workspace_tree, 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, read_workspace_file_content as read_workspace_file_content_response,
)
from yuxi.services.workspace_service import (
upload_workspace_file as upload_workspace_file_entry, upload_workspace_file as upload_workspace_file_entry,
) )
from yuxi.storage.postgres.models_business import User from yuxi.storage.postgres.models_business import User

View File

@ -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.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.file_preview import detect_preview_type
from yuxi.services.mention_search_service import invalidate_workspace_mention_cache 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.storage.postgres.models_business import User
from yuxi.utils.datetime_utils import utc_isoformat_from_timestamp from yuxi.utils.datetime_utils import utc_isoformat_from_timestamp
from yuxi.utils.paths import VIRTUAL_PATH_WORKSPACE, WORKSPACE_DIR_NAME 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"} EDITABLE_WORKSPACE_SUFFIXES = {".md", ".markdown", ".mdx", ".txt"}
MAX_WORKSPACE_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_BYTES MAX_WORKSPACE_UPLOAD_SIZE_BYTES = MAX_UPLOAD_SIZE_BYTES

View File

@ -7,7 +7,6 @@ import os
import uuid import uuid
from fastapi import UploadFile from fastapi import UploadFile
from yuxi.utils.upload_utils import read_upload_with_limit from yuxi.utils.upload_utils import read_upload_with_limit
from .client import aupload_file_to_minio from .client import aupload_file_to_minio

View File

@ -6,10 +6,10 @@ import threading
from collections.abc import Callable from collections.abc import Callable
from typing import Any from typing import Any
from neo4j import GraphDatabase as GD
from yuxi.utils import logger 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_]*$") _SAFE_NEO4J_LABEL_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$")
_shared_neo4j_connection: Neo4jConnectionManager | None = None _shared_neo4j_connection: Neo4jConnectionManager | None = None
_shared_neo4j_connection_lock = threading.Lock() _shared_neo4j_connection_lock = threading.Lock()

View File

@ -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_business import Base as BusinessBase
from yuxi.storage.postgres.models_knowledge import Base as KnowledgeBase from yuxi.storage.postgres.models_knowledge import Base as KnowledgeBase
from yuxi.utils import logger from yuxi.utils import logger
from yuxi.utils.singleton import SingletonMeta from yuxi.utils.singleton import SingletonMeta
# 合并两个 Base # 合并两个 Base

View File

@ -6,6 +6,7 @@ from typing import Any
import jwt import jwt
from argon2 import PasswordHasher from argon2 import PasswordHasher
from argon2.exceptions import InvalidHash, VerificationError, VerifyMismatchError from argon2.exceptions import InvalidHash, VerificationError, VerifyMismatchError
from yuxi.utils.datetime_utils import utc_now from yuxi.utils.datetime_utils import utc_now
JWT_ALGORITHM = "HS256" JWT_ALGORITHM = "HS256"

View File

@ -6,7 +6,13 @@ export const graphApi = {
}, },
getSubgraph: async (params) => { 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) { if (!kb_id) {
throw new Error('kb_id is required') throw new Error('kb_id is required')

View File

@ -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 BASE_URL = '/api/system/skills'
const USER_BASE_URL = '/api/skills' const USER_BASE_URL = '/api/skills'

View File

@ -64,7 +64,12 @@
@keydown.esc.stop.prevent="cancelField" @keydown.esc.stop.prevent="cancelField"
@blur="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 || '未设置' }} {{ userStore.phoneNumber || '未设置' }}
</button> </button>
</div> </div>
@ -289,11 +294,7 @@ watch(
} }
) )
watch( watch(() => [userStore.username, userStore.phoneNumber], syncProfileDraft, { immediate: true })
() => [userStore.username, userStore.phoneNumber],
syncProfileDraft,
{ immediate: true }
)
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>

View File

@ -1,7 +1,12 @@
<template> <template>
<section v-if="normalizedArtifacts.length" class="artifacts-list"> <section v-if="normalizedArtifacts.length" class="artifacts-list">
<div v-for="file in normalizedArtifacts" :key="file.path" class="artifact-card"> <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 <component
:is="getFileIcon(file.path)" :is="getFileIcon(file.path)"
class="item-icon" class="item-icon"
@ -81,7 +86,10 @@ const parseDownloadFilename = (contentDisposition) => {
} }
const getFileMetaLabel = (path) => { const getFileMetaLabel = (path) => {
const filename = String(path || '').split('/').pop() || '' const filename =
String(path || '')
.split('/')
.pop() || ''
if (!filename.includes('.')) return '交付文件' if (!filename.includes('.')) return '交付文件'
const extension = filename.split('.').pop() const extension = filename.split('.').pop()

View File

@ -386,7 +386,8 @@ const setPanelRatioForViewMode = () => {
const showFilePanel = (mode = 'tree') => { const showFilePanel = (mode = 'tree') => {
sideActive.value = 'file' sideActive.value = 'file'
agentPanelViewMode.value = mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree' agentPanelViewMode.value =
mode === 'preview' && agentPanelActivePreviewPath.value ? 'preview' : 'tree'
setPanelRatioForViewMode() setPanelRatioForViewMode()
} }

View File

@ -16,9 +16,7 @@
</div> </div>
</div> </div>
<div class="env-tip"> <div class="env-tip">保存后仅对新建沙盒生效已运行沙盒不会热更新</div>
保存后仅对新建沙盒生效已运行沙盒不会热更新
</div>
<a-spin :spinning="loading"> <a-spin :spinning="loading">
<McpEnvEditor :modelValue="draftEnv" @update:modelValue="updateDraftEnv" /> <McpEnvEditor :modelValue="draftEnv" @update:modelValue="updateDraftEnv" />

View File

@ -94,7 +94,10 @@
class="file-content" class="file-content"
:class="[ :class="[
contentClass, 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"> <div v-if="canEdit && editMode === 'edit'" class="edit-floating-actions">
@ -110,12 +113,7 @@
<span v-if="saving">保存中...</span> <span v-if="saving">保存中...</span>
<span v-else>保存</span> <span v-else>保存</span>
</button> </button>
<button <button class="edit-floating-btn" :disabled="saving" @click="cancelEdit" title="取消">
class="edit-floating-btn"
:disabled="saving"
@click="cancelEdit"
title="取消"
>
<X :size="16" /> <X :size="16" />
<span>取消</span> <span>取消</span>
</button> </button>

View File

@ -54,7 +54,10 @@
</div> </div>
<div class="tab-content"> <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"> <div v-if="hasActivePreview" class="preview-pane">
<AgentFilePreview <AgentFilePreview
v-if="currentFile" v-if="currentFile"
@ -619,16 +622,19 @@ onUnmounted(() => {
revokeCurrentPreviewUrl() revokeCurrentPreviewUrl()
}) })
watch(() => props.threadId, (threadId) => { watch(
if (threadId) { () => props.threadId,
refreshFileSystem() (threadId) => {
} else { if (threadId) {
dynamicTreeData.value = [] refreshFileSystem()
expandedKeys.value = [] } else {
selectedKeys.value = [] dynamicTreeData.value = []
filesystemError.value = '' expandedKeys.value = []
selectedKeys.value = []
filesystemError.value = ''
}
} }
}) )
watch([() => props.threadId, () => props.activePreviewPath], loadActivePreview, { immediate: true }) watch([() => props.threadId, () => props.activePreviewPath], loadActivePreview, { immediate: true })

View File

@ -21,11 +21,7 @@
class="config-alert" class="config-alert"
/> />
<!-- 统一显示所有配置项 --> <!-- 统一显示所有配置项 -->
<a-empty <a-empty v-if="isCurrentSegmentEmpty" description="暂无配置项" class="config-empty" />
v-if="isCurrentSegmentEmpty"
description="暂无配置项"
class="config-empty"
/>
<template v-for="(value, key) in filteredConfigurableItems" :key="key"> <template v-for="(value, key) in filteredConfigurableItems" :key="key">
<a-form-item :label="getConfigLabel(key, value)" :name="key" class="config-item"> <a-form-item :label="getConfigLabel(key, value)" :name="key" class="config-item">
<p v-if="value.description" class="config-description">{{ value.description }}</p> <p v-if="value.description" class="config-description">{{ value.description }}</p>

View File

@ -175,7 +175,6 @@ const presetDescription = computed(() => getChunkPresetDescription(effectivePres
margin-bottom: 0; margin-bottom: 0;
} }
.param-description { .param-description {
font-size: 12px; font-size: 12px;
color: var(--gray-400); color: var(--gray-400);

View File

@ -247,11 +247,7 @@ const renameChat = async (chatId) => {
} }
.actions-mask { .actions-mask {
background: linear-gradient( background: linear-gradient(to right, transparent, var(--gray-50));
to right,
transparent,
var(--gray-50)
);
} }
.more-btn { .more-btn {

View File

@ -13,7 +13,9 @@
title="刷新" title="刷新"
class="refresh-btn lucide-icon-btn" 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>
<a-button type="primary" @click="showAddDepartmentModal" class="add-btn lucide-icon-btn"> <a-button type="primary" @click="showAddDepartmentModal" class="add-btn lucide-icon-btn">
<template #icon><Plus :size="16" /></template> <template #icon><Plus :size="16" /></template>

View File

@ -100,7 +100,9 @@ const fetchV2Models = async () => {
const checkV2ModelStatuses = async () => { const checkV2ModelStatuses = async () => {
try { try {
const models = Object.values(v2Models.value).flatMap((providerData) => providerData.models || []) const models = Object.values(v2Models.value).flatMap(
(providerData) => providerData.models || []
)
await checkV2Statuses(models) await checkV2Statuses(models)
} catch (error) { } catch (error) {
console.error('检查 embedding 模型状态失败:', error) console.error('检查 embedding 模型状态失败:', error)

View File

@ -259,9 +259,7 @@
</span> </span>
<span <span
>已选择 >已选择
{{ {{ selectedWorkspacePaths.length }}
selectedWorkspacePaths.length
}}
个文件注意上传会扁平化上传不保留文件层级结构</span 个文件注意上传会扁平化上传不保留文件层级结构</span
> >
</div> </div>

View File

@ -11,7 +11,11 @@
<div :class="rowClass(item.data?.label)"> <div :class="rowClass(item.data?.label)">
<span class="detail-label">名称</span> <span class="detail-label">名称</span>
<span class="detail-value"> <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> </span>
</div> </div>
<div class="detail-row"> <div class="detail-row">
@ -22,12 +26,18 @@
<div class="detail-row"> <div class="detail-row">
<span class="detail-label">标签</span> <span class="detail-label">标签</span>
<span class="detail-value"> <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> </span>
</div> </div>
</template> </template>
<template v-if="item.data?.original?.properties"> <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-label">{{ key }}</span>
<span class="detail-value"> <span class="detail-value">
<DetailValue :value="value" :field-key="key" :expanded-keys="expandedKeys" /> <DetailValue :value="value" :field-key="key" :expanded-keys="expandedKeys" />
@ -39,11 +49,19 @@
<div :class="rowClass(item.data?.label)"> <div :class="rowClass(item.data?.label)">
<span class="detail-label">类型</span> <span class="detail-label">类型</span>
<span class="detail-value"> <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> </span>
</div> </div>
<template v-if="item.data?.original?.properties"> <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-label">{{ key }}</span>
<span class="detail-value"> <span class="detail-value">
<DetailValue :value="value" :field-key="key" :expanded-keys="expandedKeys" /> <DetailValue :value="value" :field-key="key" :expanded-keys="expandedKeys" />
@ -76,15 +94,35 @@ const DetailValue = defineComponent({
if (typeof v !== 'string') return String(v ?? '') if (typeof v !== 'string') return String(v ?? '')
if (v.length <= TRUNCATE_LIMIT) return v if (v.length <= TRUNCATE_LIMIT) return v
if (props.expandedKeys.has(props.fieldKey)) { if (props.expandedKeys.has(props.fieldKey)) {
return [v, h('a', { return [
class: 'expand-link', v,
onClick: (e) => { e.preventDefault(); props.expandedKeys.delete(props.fieldKey) } h(
}, ' 收起')] 'a',
{
class: 'expand-link',
onClick: (e) => {
e.preventDefault()
props.expandedKeys.delete(props.fieldKey)
}
},
' 收起'
)
]
} }
return [v.slice(0, TRUNCATE_LIMIT) + '...', h('a', { return [
class: 'expand-link', v.slice(0, TRUNCATE_LIMIT) + '...',
onClick: (e) => { e.preventDefault(); props.expandedKeys.add(props.fieldKey) } 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()) const expandedKeys = reactive(new Set())
watch(() => props.item, () => { watch(
expandedKeys.clear() () => props.item,
}) () => {
expandedKeys.clear()
}
)
const isOverThreshold = (value) => typeof value === 'string' && value.length > STACK_THRESHOLD const isOverThreshold = (value) => typeof value === 'string' && value.length > STACK_THRESHOLD

View File

@ -99,7 +99,11 @@
<div <div
v-for="(item, index) in mentionItems.knowledgeBases" v-for="(item, index) in mentionItems.knowledgeBases"
:key="'kb-' + item.value" :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)" @click="insertMention(item)"
> >
<div class="resource-name"> <div class="resource-name">
@ -204,7 +208,11 @@
<div <div
v-for="(item, index) in mentionItems.subagents" v-for="(item, index) in mentionItems.subagents"
:key="'subagent-' + item.value" :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)" @click="insertMention(item)"
> >
<div class="resource-name"> <div class="resource-name">
@ -411,7 +419,10 @@ const getRawNodeLength = (node) => {
if (isTextNode(node)) return node.textContent?.length || 0 if (isTextNode(node)) return node.textContent?.length || 0
if (isMentionNode(node)) return node.dataset.mentionRaw?.length || 0 if (isMentionNode(node)) return node.dataset.mentionRaw?.length || 0
if (isLineBreakNode(node)) return 1 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) => { const serializeEditorNode = (node) => {
@ -462,7 +473,11 @@ const createEditorMentionElement = (segment) => {
const label = document.createElement('span') const label = document.createElement('span')
label.className = 'mention-ref-label' 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) token.appendChild(label)
return token return token
@ -475,7 +490,9 @@ const renderEditorContent = (raw = '') => {
editor.replaceChildren() editor.replaceChildren()
parseMentionText(raw).forEach((segment) => { parseMentionText(raw).forEach((segment) => {
editor.appendChild( editor.appendChild(
segment.kind === 'text' ? document.createTextNode(segment.text) : createEditorMentionElement(segment) segment.kind === 'text'
? document.createTextNode(segment.text)
: createEditorMentionElement(segment)
) )
}) })
lastSyncedEditorValue = String(raw || '') lastSyncedEditorValue = String(raw || '')
@ -549,7 +566,10 @@ const getDomPointForRawOffset = (rawOffset) => {
let remaining = offset let remaining = offset
const pointBeforeNode = (node) => ({ node: node.parentNode || editor, offset: childIndex(node) }) 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) => { const visit = (node) => {
if (!node) return null if (!node) return null
@ -872,7 +892,12 @@ const insertMention = (item) => {
const mentionValue = item.insertValue || item.value const mentionValue = item.insertValue || item.value
const mentionText = `${formatMentionToken(item.type, mentionValue)} ` 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 const newCursorPos = activeMention.start + mentionText.length
mentionPopupVisible.value = false mentionPopupVisible.value = false

View File

@ -1,5 +1,10 @@
<template> <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" :class="modelSelectClasses" @click.prevent.stop @mousedown.stop>
<div class="model-select-content"> <div class="model-select-content">
<div class="model-info"> <div class="model-info">
@ -30,7 +35,12 @@
<template #overlay> <template #overlay>
<div class="model-dropdown" @click.stop> <div class="model-dropdown" @click.stop>
<div class="model-search"> <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> <template #suffix>
<button <button
:disabled="props.disabled || state.refreshingCache" :disabled="props.disabled || state.refreshingCache"
@ -46,7 +56,9 @@
</div> </div>
<a-menu class="scrollable-menu"> <a-menu class="scrollable-menu">
<a-menu-item v-if="loadingV2Models" key="loading" disabled>加载中...</a-menu-item> <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> <template v-else>
<a-menu-item-group <a-menu-item-group
v-for="(providerData, providerId) in filteredV2Models" v-for="(providerData, providerId) in filteredV2Models"

View File

@ -139,10 +139,7 @@
</div> </div>
<div v-else class="suggestions-empty"> <div v-else class="suggestions-empty">
<button <button class="suggestion-row" @click="() => generateSampleQuestions(false)">
class="suggestion-row"
@click="() => generateSampleQuestions(false)"
>
<RefreshCw class="suggestion-icon" /> <RefreshCw class="suggestion-icon" />
<span class="suggestion-text">生成示例问题</span> <span class="suggestion-text">生成示例问题</span>
</button> </button>
@ -515,7 +512,9 @@ defineExpose({
border-radius: 6px; border-radius: 6px;
color: var(--gray-800); color: var(--gray-800);
font-size: 13px; font-size: 13px;
span { font-weight: 500; } span {
font-weight: 500;
}
} }
.clear-results-btn { .clear-results-btn {

View File

@ -150,7 +150,9 @@
status="active" status="active"
:show-info="false" :show-info="false"
/> />
<span class="run-progress-message">{{ getRunProgressMessage(latestEvaluation) }}</span> <span class="run-progress-message">{{
getRunProgressMessage(latestEvaluation)
}}</span>
</div> </div>
</div> </div>
</div> </div>
@ -221,7 +223,11 @@
ok-text="确定" ok-text="确定"
cancel-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>
</a-popconfirm> </a-popconfirm>
@ -668,15 +674,11 @@ const loadResultsWithPagination = async () => {
try { try {
resultsLoading.value = true resultsLoading.value = true
const response = await evaluationApi.getRunResults( const response = await evaluationApi.getRunResults(props.kbId, selectedResult.value.run_id, {
props.kbId, page: currentPage.value,
selectedResult.value.run_id, pageSize: pageSize.value,
{ errorOnly: showErrorsOnly.value
page: currentPage.value, })
pageSize: pageSize.value,
errorOnly: showErrorsOnly.value
}
)
if (response.message === 'success' && response.data) { if (response.message === 'success' && response.data) {
const resultData = 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 = () => { const stopEvaluationRefresh = () => {
if (evaluationRefreshTimer) { if (evaluationRefreshTimer) {
@ -1035,7 +1038,8 @@ const formatRunDuration = (record) => {
} }
const getRunProgress = (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) const total = Number(record?.total_items || 0)
if (!total) return 0 if (!total) return 0
@ -1051,7 +1055,9 @@ const getRunProgressMessage = (record) => {
} }
const formatLatestRingValue = (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) => const getLatestRingClass = (record) =>
record?.status === 'running' ? 'score-running' : getScoreLevelClass(record?.overall_score) record?.status === 'running' ? 'score-running' : getScoreLevelClass(record?.overall_score)

View File

@ -166,7 +166,6 @@
<div v-show="activeTab === 'department'" v-if="userStore.isSuperAdmin"> <div v-show="activeTab === 'department'" v-if="userStore.isSuperAdmin">
<DepartmentManagementComponent /> <DepartmentManagementComponent />
</div> </div>
</div> </div>
</div> </div>
</div> </div>
@ -176,7 +175,16 @@
<script setup> <script setup>
import { computed, onMounted, ref, watch } from 'vue' import { computed, onMounted, ref, watch } from 'vue'
import { useUserStore } from '@/stores/user' 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 AccountSettingsComponent from '@/components/AccountSettingsComponent.vue'
import AgentEnvSettingsCard from '@/components/AgentEnvSettingsCard.vue' import AgentEnvSettingsCard from '@/components/AgentEnvSettingsCard.vue'
import BasicSettingsSection from '@/components/BasicSettingsSection.vue' import BasicSettingsSection from '@/components/BasicSettingsSection.vue'

View File

@ -29,7 +29,11 @@
class="card-action" class="card-action"
@click.stop @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 <a-button
size="small" size="small"
class="select-action lucide-icon-btn" class="select-action lucide-icon-btn"
@ -66,18 +70,44 @@
:aria-checked="isSelected(option.value, item.value)" :aria-checked="isSelected(option.value, item.value)"
:tabindex="item.disabled ? -1 : 0" :tabindex="item.disabled ? -1 : 0"
class="selection-item" 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 @mousedown.stop
@click.stop="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))" @click.stop="
@keydown.enter.prevent="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))" !item.disabled &&
@keydown.space.prevent="!item.disabled && toggleSelection(option.value, item.value, !isSelected(option.value, item.value))" 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"> <span class="selection-item-content">
<a-checkbox <a-checkbox
:checked="isSelected(option.value, item.value)" :checked="isSelected(option.value, item.value)"
:disabled="item.disabled" :disabled="item.disabled"
@click.stop @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 class="selection-label">{{ item.label }}</span>
</span> </span>
@ -94,7 +124,13 @@
</div> </div>
</div> </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> </div>
</template> </template>
@ -179,11 +215,15 @@ const currentDepartmentId = computed(() => {
const currentUserUid = computed(() => userStore.uid || '') const currentUserUid = computed(() => userStore.uid || '')
const normalizedAllowedAccessLevels = computed(() => { 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'] return allowed.length ? allowed : ['global']
}) })
const shareModeOptions = computed(() => const shareModeOptions = computed(() =>
baseShareModeOptions.filter((option) => normalizedAllowedAccessLevels.value.includes(option.value)) baseShareModeOptions.filter((option) =>
normalizedAllowedAccessLevels.value.includes(option.value)
)
) )
const departmentOptions = computed(() => const departmentOptions = computed(() =>
@ -246,7 +286,9 @@ const normalizeActiveConfig = () => {
const initConfig = () => { const initConfig = () => {
syncingFromProps.value = true 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 ? props.modelValue.access_level
: 'global' : 'global'
config.access_level = normalizedAllowedAccessLevels.value.includes(requestedAccessLevel) config.access_level = normalizedAllowedAccessLevels.value.includes(requestedAccessLevel)
@ -263,7 +305,8 @@ const initConfig = () => {
const emitConfig = () => { const emitConfig = () => {
emit('update:modelValue', { emit('update:modelValue', {
access_level: config.access_level, 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) : [] user_uids: config.access_level === 'user' ? normalizeUserUids(config.user_uids) : []
}) })
} }
@ -314,7 +357,9 @@ const toggleSelection = (accessLevel, value, checked) => {
} }
const uid = String(value) 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) config.user_uids = normalizeUserUids(selected)
ensureCurrentUser() ensureCurrentUser()
} }
@ -553,7 +598,6 @@ defineExpose({
} }
} }
} }
</style> </style>
<style lang="less"> <style lang="less">

View File

@ -61,7 +61,6 @@
</a-dropdown> </a-dropdown>
<a-button v-else-if="showButton" type="primary" @click="goToLogin"> 登录 </a-button> <a-button v-else-if="showButton" type="primary" @click="goToLogin"> 登录 </a-button>
<!-- 调试面板 Modal --> <!-- 调试面板 Modal -->
<DebugComponent v-model:show="showDebug" /> <DebugComponent v-model:show="showDebug" />
</div> </div>
@ -316,7 +315,6 @@ const openProfile = () => {
} }
} }
:deep(.ant-dropdown-menu) { :deep(.ant-dropdown-menu) {
padding: 8px 0; padding: 8px 0;
} }

View File

@ -109,7 +109,10 @@
<a-input v-model:value="editForm.slug" disabled /> <a-input v-model:value="editForm.slug" disabled />
</a-form-item> </a-form-item>
<a-form-item label="MCP 名称" required class="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>
<a-form-item label="传输类型" required class="form-item"> <a-form-item label="传输类型" required class="form-item">
<a-select v-model:value="editForm.transport"> <a-select v-model:value="editForm.transport">

View File

@ -54,14 +54,23 @@
</template> </template>
</PageShoulder> </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-card">
<div class="skill-empty-icon"> <div class="skill-empty-icon">
<BookMarked :size="22" /> <BookMarked :size="22" />
</div> </div>
<div class="skill-empty-title">{{ searchQuery ? '没有匹配的 Skill' : '还没有添加 Skill' }}</div> <div class="skill-empty-title">
{{ searchQuery ? '没有匹配的 Skill' : '还没有添加 Skill' }}
</div>
<div class="skill-empty-desc"> <div class="skill-empty-desc">
{{ searchQuery ? '换个关键词试试,或清空搜索条件。' : '可以从远程仓库安装,或上传本地 Skill 文件。' }} {{
searchQuery
? '换个关键词试试,或清空搜索条件。'
: '可以从远程仓库安装,或上传本地 Skill 文件。'
}}
</div> </div>
</div> </div>
</div> </div>
@ -409,7 +418,9 @@
</div> </div>
</div> </div>
<a-tag v-if="item.success === false" color="red">解析失败</a-tag> <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> </div>
<div class="draft-share-title">生效范围</div> <div class="draft-share-title">生效范围</div>
@ -488,7 +499,9 @@ const installedSkillCards = computed(() =>
sourceType, sourceType,
sourceLabel: sourceTypeLabel(sourceType), sourceLabel: sourceTypeLabel(sourceType),
status: 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 openDraftConfirmation = async (draftPayload) => {
const draft = normalizePendingDraft(draftPayload) const draft = normalizePendingDraft(draftPayload)
if (!draft.draft_ids.length || !draft.items.some((item) => item.success !== false)) { 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') message.error('没有可添加的 Skill')
return false return false
} }
@ -1006,7 +1021,6 @@ defineExpose({
line-height: 20px; line-height: 20px;
} }
.card-wrapper { .card-wrapper {
position: relative; position: relative;
@ -1174,7 +1188,6 @@ defineExpose({
padding: 4px 0 8px 0; padding: 4px 0 8px 0;
} }
.skills-list-section { .skills-list-section {
margin-top: 12px; margin-top: 12px;
border: 1px solid var(--gray-150); border: 1px solid var(--gray-150);
@ -1310,7 +1323,6 @@ defineExpose({
} }
} }
.modal-footer-actions { .modal-footer-actions {
display: flex; display: flex;
justify-content: flex-end; justify-content: flex-end;

View File

@ -190,7 +190,9 @@
<div class="dependency-title-block"> <div class="dependency-title-block">
<div class="dependency-title-row"> <div class="dependency-title-row">
<h4>{{ group.title }}</h4> <h4>{{ group.title }}</h4>
<span class="dependency-count">已选择 {{ getDependencyValues(group).length }} </span> <span class="dependency-count"
>已选择 {{ getDependencyValues(group).length }} </span
>
</div> </div>
<p>{{ group.description }}</p> <p>{{ group.description }}</p>
</div> </div>
@ -220,7 +222,10 @@
@mousedown.stop @mousedown.stop
@click.stop @click.stop
/> />
<div v-if="getFilteredDependencyOptions(group).length" class="selection-list"> <div
v-if="getFilteredDependencyOptions(group).length"
class="selection-list"
>
<div <div
v-for="option in getFilteredDependencyOptions(group)" v-for="option in getFilteredDependencyOptions(group)"
:key="option.value" :key="option.value"
@ -230,15 +235,35 @@
class="selection-item" class="selection-item"
:class="{ selected: isDependencySelected(group, option.value) }" :class="{ selected: isDependencySelected(group, option.value) }"
@mousedown.stop @mousedown.stop
@click.stop="toggleDependency(group, option.value, !isDependencySelected(group, option.value))" @click.stop="
@keydown.enter.prevent="toggleDependency(group, option.value, !isDependencySelected(group, option.value))" toggleDependency(
@keydown.space.prevent="toggleDependency(group, option.value, !isDependencySelected(group, option.value))" 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"> <span class="selection-item-content">
<a-checkbox <a-checkbox
:checked="isDependencySelected(group, option.value)" :checked="isDependencySelected(group, option.value)"
@click.stop @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 class="selection-label">{{ option.label }}</span>
</span> </span>
@ -250,7 +275,9 @@
</div> </div>
</template> </template>
</a-dropdown> </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>
<div v-if="getDependencyValues(group).length" class="dependency-chip-list"> <div v-if="getDependencyValues(group).length" class="dependency-chip-list">
@ -388,7 +415,9 @@ const selectedFilePreview = computed(() => ({
const toolDependencyOptions = computed(() => const toolDependencyOptions = computed(() =>
(dependencyOptions.tools || []).map((i) => (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(() => const mcpDependencyOptions = computed(() =>
@ -441,7 +470,9 @@ const getDependencyOptionLabel = (group, value) => {
} }
const getFilteredDependencyOptions = (group) => { const getFilteredDependencyOptions = (group) => {
const keyword = String(dependencySearch[group.key] || '').trim().toLowerCase() const keyword = String(dependencySearch[group.key] || '')
.trim()
.toLowerCase()
if (!keyword) return group.options if (!keyword) return group.options
return group.options.filter((option) => { return group.options.filter((option) => {
const label = String(option.label || '').toLowerCase() const label = String(option.label || '').toLowerCase()

View File

@ -67,7 +67,10 @@
> >
<a-form layout="vertical" class="extension-form"> <a-form layout="vertical" class="extension-form">
<a-form-item label="标识" required class="form-item"> <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>
<a-form-item label="名称" required class="form-item"> <a-form-item label="名称" required class="form-item">
<a-input v-model:value="form.name" placeholder="请输入 SubAgent 展示名称" /> <a-input v-model:value="form.name" placeholder="请输入 SubAgent 展示名称" />
@ -166,7 +169,14 @@ const navigateToDetail = (agent) => {
} }
const handleSubagentAdd = () => { 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 formModalVisible.value = true
} }

View File

@ -82,11 +82,7 @@
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item <a-form-item name="neighbors_count" :labelCol="{ span: 24 }" :wrapperCol="{ span: 24 }">
name="neighbors_count"
:labelCol="{ span: 24 }"
:wrapperCol="{ span: 24 }"
>
<template #label> <template #label>
<span class="field-label-with-help"> <span class="field-label-with-help">
候选 Chunk 数量 候选 Chunk 数量
@ -168,7 +164,12 @@
</div> </div>
<div class="footer-actions"> <div class="footer-actions">
<a-button :disabled="generating" @click="handleCancel">取消</a-button> <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> </a-button>
</div> </div>

View File

@ -76,9 +76,15 @@ const filteredAgents = computed(() => {
const filtered = keyword const filtered = keyword
? list.filter( ? list.filter(
(agent) => (agent) =>
String(agent.name || '').toLowerCase().includes(keyword) || String(agent.name || '')
String(agent.id || '').toLowerCase().includes(keyword) || .toLowerCase()
String(agent.backend_id || '').toLowerCase().includes(keyword) .includes(keyword) ||
String(agent.id || '')
.toLowerCase()
.includes(keyword) ||
String(agent.backend_id || '')
.toLowerCase()
.includes(keyword)
) )
: list : list
return [...filtered].sort((a, b) => { return [...filtered].sort((a, b) => {
@ -128,7 +134,9 @@ const agentPreviewIcon = computed(() => agentForm.icon?.trim() || defaultAgentIc
const selectedBackendOption = computed(() => const selectedBackendOption = computed(() =>
agentBackendOptions.value.find((backend) => backend.value === agentForm.backend_id) 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 selectedBackendIcon = computed(() => {
const backendText = `${agentForm.backend_id} ${selectedBackendLabel.value}`.toLowerCase() const backendText = `${agentForm.backend_id} ${selectedBackendLabel.value}`.toLowerCase()
return backendText.includes('deep') || backendText.includes('search') ? Microscope : Bot return backendText.includes('deep') || backendText.includes('search') ? Microscope : Bot
@ -252,7 +260,9 @@ const saveAgent = async () => {
return return
} }
const validation = canEditAgentShareConfig.value ? agentShareConfigFormRef.value?.validate?.() : null const validation = canEditAgentShareConfig.value
? agentShareConfigFormRef.value?.validate?.()
: null
if (validation && !validation.valid) { if (validation && !validation.valid) {
agentModalActiveTab.value = 'basic' agentModalActiveTab.value = 'basic'
message.error(validation.message) message.error(validation.message)
@ -264,7 +274,10 @@ const saveAgent = async () => {
const payload = buildAgentPayload() const payload = buildAgentPayload()
if (editingAgentId.value) { if (editingAgentId.value) {
const validatedConfig = runtimeConfigFormRef.value?.validateAndFilterConfig?.() 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) agentStore.updateAgentConfig(validatedConfig)
} }
if (agentStore.hasConfigChanges) { if (agentStore.hasConfigChanges) {
@ -320,73 +333,73 @@ defineExpose({
<template> <template>
<div class="agent-manage-panel"> <div class="agent-manage-panel">
<PageShoulder v-model:search="searchQuery" search-placeholder="搜索智能体..."> <PageShoulder v-model:search="searchQuery" search-placeholder="搜索智能体...">
<template #actions> <template #actions>
<a-button type="primary" class="lucide-icon-btn" @click="openCreateAgentModal"> <a-button type="primary" class="lucide-icon-btn" @click="openCreateAgentModal">
<Plus :size="14" /> <Plus :size="14" />
新增智能体 新增智能体
</a-button> </a-button>
<a-button class="lucide-icon-btn" @click="loadAgents" :loading="agentLoading"> <a-button class="lucide-icon-btn" @click="loadAgents" :loading="agentLoading">
<RefreshCw :size="14" :class="{ spinning: agentLoading }" /> <RefreshCw :size="14" :class="{ spinning: agentLoading }" />
</a-button> </a-button>
</template> </template>
</PageShoulder> </PageShoulder>
<ExtensionCardGrid :min-width="320"> <ExtensionCardGrid :min-width="320">
<InfoCard <InfoCard
v-for="agent in filteredAgents" v-for="agent in filteredAgents"
:key="agent.id" :key="agent.id"
:title="agent.name" :title="agent.name"
:subtitle="agent.slug || agent.id" :subtitle="agent.slug || agent.id"
:description="agent.description || '暂无描述'" :description="agent.description || '暂无描述'"
:default-icon="Bot" :default-icon="Bot"
:tags="agent.backend_id ? [{ name: agent.backend_id, color: 'blue' }] : []" :tags="agent.backend_id ? [{ name: agent.backend_id, color: 'blue' }] : []"
class="config-card agent-card" class="config-card agent-card"
@click="canManageAgent(agent) && openEditAgentModal(agent)" @click="canManageAgent(agent) && openEditAgentModal(agent)"
> >
<template #icon> <template #icon>
<img <img
class="agent-card-icon-image" class="agent-card-icon-image"
:src="getAgentIconSrc(agent)" :src="getAgentIconSrc(agent)"
:alt="`${agent.name || '智能体'}图标`" :alt="`${agent.name || '智能体'}图标`"
/> />
</template> </template>
<template #status> <template #status>
<a-dropdown v-if="canManageAgent(agent)" :trigger="['click']" placement="bottomRight"> <a-dropdown v-if="canManageAgent(agent)" :trigger="['click']" placement="bottomRight">
<template #overlay> <template #overlay>
<a-menu> <a-menu>
<a-menu-item key="edit" @click.stop="openEditAgentModal(agent)"> <a-menu-item key="edit" @click.stop="openEditAgentModal(agent)">
<span class="agent-card-menu-item"> <span class="agent-card-menu-item">
<Edit3 :size="14" /> <Edit3 :size="14" />
编辑智能体 编辑智能体
</span> </span>
</a-menu-item> </a-menu-item>
<a-menu-item <a-menu-item
key="delete" key="delete"
:disabled="isBuiltinAgent(agent)" :disabled="isBuiltinAgent(agent)"
@click.stop="deleteAgent(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
> >
<MoreVertical :size="16" /> <span class="agent-card-menu-item" :class="{ danger: !isBuiltinAgent(agent) }">
</a-button> <Trash2 :size="14" />
</a-dropdown> 删除智能体
</span>
</a-menu-item>
</a-menu>
</template> </template>
</InfoCard> <a-button
</ExtensionCardGrid> 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 --> <!-- Agent Edit Modal -->
<a-modal <a-modal
@ -420,7 +433,10 @@ defineExpose({
<component :is="item.icon" :size="16" /> <component :is="item.icon" :size="16" />
<span>{{ item.label }}</span> <span>{{ item.label }}</span>
</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> </button>
</aside> </aside>
@ -460,10 +476,16 @@ defineExpose({
placeholder="标识可选,留空自动生成" placeholder="标识可选,留空自动生成"
aria-label="智能体标识" 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> </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"> <span class="agent-backend-icon">
<component :is="selectedBackendIcon" :size="16" /> <component :is="selectedBackendIcon" :size="16" />
</span> </span>
@ -521,7 +543,6 @@ defineExpose({
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.agent-manage-panel { .agent-manage-panel {
height: 100%; height: 100%;

View File

@ -619,54 +619,54 @@ defineExpose({
<template> <template>
<div class="model-provider-manage-panel"> <div class="model-provider-manage-panel">
<PageShoulder v-model:search="searchQuery" search-placeholder="搜索供应商..."> <PageShoulder v-model:search="searchQuery" search-placeholder="搜索供应商...">
<template #actions> <template #actions>
<a-button type="primary" class="lucide-icon-btn" @click="openCreateProviderModal"> <a-button type="primary" class="lucide-icon-btn" @click="openCreateProviderModal">
<Plus :size="14" /> <Plus :size="14" />
新增供应商 新增供应商
</a-button> </a-button>
<a-button class="lucide-icon-btn" @click="loadProviders" :loading="loading"> <a-button class="lucide-icon-btn" @click="loadProviders" :loading="loading">
<RefreshCw :size="14" :class="{ spinning: loading }" /> <RefreshCw :size="14" :class="{ spinning: loading }" />
</a-button> </a-button>
</template> </template>
</PageShoulder> </PageShoulder>
<ExtensionCardGrid :min-width="320"> <ExtensionCardGrid :min-width="320">
<InfoCard <InfoCard
v-for="provider in filteredProviders" v-for="provider in filteredProviders"
:key="provider.provider_id" :key="provider.provider_id"
:title="provider.display_name" :title="provider.display_name"
:subtitle="provider.provider_id" :subtitle="provider.provider_id"
:disabled="!provider.is_enabled" :disabled="!provider.is_enabled"
:default-icon="Globe" :default-icon="Globe"
:info="getProviderInfo(provider)" :info="getProviderInfo(provider)"
:status="getProviderStatus(provider)" :status="getProviderStatus(provider)"
@click="openEditProviderModal(provider)" @click="openEditProviderModal(provider)"
> >
<template #icon> <template #icon>
<img <img
v-if="getProviderIcon(provider)" v-if="getProviderIcon(provider)"
:src="getIconUrl(getProviderIcon(provider))" :src="getIconUrl(getProviderIcon(provider))"
:alt="provider.display_name" :alt="provider.display_name"
/> />
</template> </template>
<template #footer> <template #footer>
<button class="view-models-btn" type="button" @click.stop="openModelsModal(provider)"> <button class="view-models-btn" type="button" @click.stop="openModelsModal(provider)">
<Settings2 :size="14" /> <Settings2 :size="14" />
管理模型 管理模型
</button> </button>
<a-tooltip title="编辑供应商"> <a-tooltip title="编辑供应商">
<a-button <a-button
size="small" size="small"
class="lucide-icon-btn" class="lucide-icon-btn"
@click.stop="openEditProviderModal(provider)" @click.stop="openEditProviderModal(provider)"
> >
<Edit3 :size="14" /> <Edit3 :size="14" />
</a-button> </a-button>
</a-tooltip> </a-tooltip>
</template> </template>
</InfoCard> </InfoCard>
</ExtensionCardGrid> </ExtensionCardGrid>
<!-- Provider Edit Modal --> <!-- Provider Edit Modal -->
<a-modal <a-modal
@ -906,8 +906,16 @@ defineExpose({
size="small" size="small"
class="model-test-button" class="model-test-button"
:title="getModelTestTitle(currentProviderForModels.provider_id, model)" :title="getModelTestTitle(currentProviderForModels.provider_id, model)"
:loading="modelTestLoadingBySpec[buildModelSpec(currentProviderForModels.provider_id, model.id)]" :loading="
:disabled="modelTestLoadingBySpec[buildModelSpec(currentProviderForModels.provider_id, model.id)]" modelTestLoadingBySpec[
buildModelSpec(currentProviderForModels.provider_id, model.id)
]
"
:disabled="
modelTestLoadingBySpec[
buildModelSpec(currentProviderForModels.provider_id, model.id)
]
"
@click="testModelConnection(currentProviderForModels.provider_id, model)" @click="testModelConnection(currentProviderForModels.provider_id, model)"
> >
测试 测试
@ -1065,7 +1073,6 @@ defineExpose({
</div> </div>
</template> </template>
<style lang="less" scoped> <style lang="less" scoped>
.model-provider-manage-panel { .model-provider-manage-panel {
height: 100%; height: 100%;

View File

@ -44,7 +44,11 @@
</div> </div>
<div class="file-table" role="table" aria-label="工作区文件列表"> <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"> <span v-if="effectiveSelectionMode" class="selection-cell">
<a-checkbox <a-checkbox
:checked="allSelected" :checked="allSelected"
@ -115,7 +119,12 @@
<span>下载</span> <span>下载</span>
</span> </span>
</a-menu-item> </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"> <span class="menu-item-content">
<Trash2 :size="14" /> <Trash2 :size="14" />
<span>删除</span> <span>删除</span>

View File

@ -2,7 +2,6 @@ import { message } from 'ant-design-vue'
import { handleChatError } from '@/utils/errorHandler' import { handleChatError } from '@/utils/errorHandler'
import { unref } from 'vue' import { unref } from 'vue'
export function useAgentStreamHandler({ export function useAgentStreamHandler({
getThreadState, getThreadState,
processApprovalInStream, processApprovalInStream,
@ -68,7 +67,6 @@ export function useAgentStreamHandler({
threadState.isStreaming = false threadState.isStreaming = false
threadState.replyLoadingVisible = false threadState.replyLoadingVisible = false
threadState.pendingRequestId = null threadState.pendingRequestId = null
} }
return true return true

View File

@ -278,7 +278,7 @@ provide('settingsModal', {
:to="item.path" :to="item.path"
v-show="!item.hidden" v-show="!item.hidden"
class="nav-item" class="nav-item"
:class="{ active: isNavItemActive(item)}" :class="{ active: isNavItemActive(item) }"
:active-class="item.action ? '' : 'active'" :active-class="item.action ? '' : 'active'"
@click.stop @click.stop
> >
@ -805,5 +805,4 @@ div.header,
} }
} }
} }
</style> </style>

View File

@ -6,7 +6,9 @@ import { handleChatError } from '@/utils/errorHandler'
function normalizeAgent(agent) { function normalizeAgent(agent) {
const agentId = agent?.agent_id || agent?.slug || agent?.id 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' export const BUILTIN_AGENT_ID = 'default-chatbot'
@ -55,7 +57,9 @@ export const useAgentStore = defineStore(
const selectedAgent = computed(() => { const selectedAgent = computed(() => {
const agentId = selectedAgentId.value 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) const agentsList = computed(() => agents.value)
@ -131,7 +135,9 @@ export const useAgentStore = defineStore(
function applyConfigDefaults(loadedConfig, configItems = configurableItems.value) { function applyConfigDefaults(loadedConfig, configItems = configurableItems.value) {
const items = { ...configItems } const items = { ...configItems }
Object.keys(items).forEach((key) => { 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) const isDefaultAllList = isDefaultAllAgentResourceKind(item?.kind)
if (loadedConfig[key] === undefined || (loadedConfig[key] === null && !isDefaultAllList)) { if (loadedConfig[key] === undefined || (loadedConfig[key] === null && !isDefaultAllList)) {
if (item.default !== undefined) loadedConfig[key] = item.default if (item.default !== undefined) loadedConfig[key] = item.default
@ -177,7 +183,10 @@ export const useAgentStore = defineStore(
isLoadingConfig.value = true isLoadingConfig.value = true
try { try {
const detail = await fetchAgentDetail(agentId) const detail = await fetchAgentDetail(agentId)
const loadedConfig = applyConfigDefaults(extractContext(detail), detail?.configurable_items || {}) const loadedConfig = applyConfigDefaults(
extractContext(detail),
detail?.configurable_items || {}
)
selectedAgentId.value = agentId selectedAgentId.value = agentId
agentConfig.value = loadedConfig agentConfig.value = loadedConfig
originalAgentConfig.value = { ...loadedConfig } originalAgentConfig.value = { ...loadedConfig }
@ -211,7 +220,10 @@ export const useAgentStore = defineStore(
const created = normalizeAgent(response.agent) const created = normalizeAgent(response.agent)
if (created?.id) { if (created?.id) {
agentDetails.value[created.id] = created 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) await selectAgent(created.id)
} }
return created return created

View File

@ -22,7 +22,15 @@ export const getAgentConfigOptions = (item) => (Array.isArray(item?.options) ? i
export const getAgentConfigOptionValue = (option) => { export const getAgentConfigOptionValue = (option) => {
if (typeof option !== 'object' || option === null) return 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) => { export const getAgentConfigOptionLabel = (option) => {

View File

@ -74,9 +74,12 @@ const FILE_ICON_CONFIG = {
} }
const getFileIconConfig = (filename) => { const getFileIconConfig = (filename) => {
const normalizedName = String(filename || '').trim().toLowerCase() const normalizedName = String(filename || '')
.trim()
.toLowerCase()
if (!normalizedName) return DEFAULT_FILE_ICON 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 cleanName = normalizedName.split(/[?#]/)[0]
const extension = cleanName.includes('.') ? cleanName.split('.').pop() : cleanName const extension = cleanName.includes('.') ? cleanName.split('.').pop() : cleanName

View File

@ -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 ICON_BASE = 'https://registry.npmmirror.com/@lobehub/icons-static-svg/latest/files/icons'
const createBrandIcon = (url) => { 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 Icon.inheritAttrs = false
return Icon return Icon
} }
@ -44,7 +45,9 @@ export const getKbTypeColor = (type) => {
const READ_ONLY_KB_TYPES = new Set(['dify', 'notion']) const READ_ONLY_KB_TYPES = new Set(['dify', 'notion'])
export const isReadOnlyDatabase = (database, kbTypes = {}) => { 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) { if (database?.supports_documents !== undefined) {
return database.supports_documents === false return database.supports_documents === false

View File

@ -9,9 +9,15 @@ export const mentionTypePrefixMap = {
} }
const mentionTypePattern = Object.values(mentionTypePrefixMap).join('|') 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') const unquoteMentionValue = (value) => String(value ?? '').replace(/\\(["\\])/g, '$1')
export const formatMentionToken = (type, value) => { export const formatMentionToken = (type, value) => {
@ -87,7 +93,6 @@ export const buildMentionDisplayLabels = (mention = {}) => {
setMentionLabel(labels, 'knowledge', kb?.kb_id, label) setMentionLabel(labels, 'knowledge', kb?.kb_id, label)
setMentionLabel(labels, 'knowledge', kb?.value, label) setMentionLabel(labels, 'knowledge', kb?.value, label)
}) })
;(mention.mcps || []).forEach((mcp) => { ;(mention.mcps || []).forEach((mcp) => {
const label = mcp?.name || mcp?.label || mcp?.slug || mcp?.id || mcp?.value || '' const label = mcp?.name || mcp?.label || mcp?.slug || mcp?.id || mcp?.value || ''
setMentionLabel(labels, 'mcp', mcp?.slug, label) setMentionLabel(labels, 'mcp', mcp?.slug, label)
@ -95,7 +100,6 @@ export const buildMentionDisplayLabels = (mention = {}) => {
setMentionLabel(labels, 'mcp', mcp?.value, label) setMentionLabel(labels, 'mcp', mcp?.value, label)
setMentionLabel(labels, 'mcp', mcp?.name, label) setMentionLabel(labels, 'mcp', mcp?.name, label)
}) })
;(mention.skills || []).forEach((skill) => { ;(mention.skills || []).forEach((skill) => {
const label = skill?.name || skill?.label || skill?.slug || skill?.id || skill?.value || '' const label = skill?.name || skill?.label || skill?.slug || skill?.id || skill?.value || ''
setMentionLabel(labels, 'skill', skill?.slug, label) setMentionLabel(labels, 'skill', skill?.slug, label)
@ -103,7 +107,6 @@ export const buildMentionDisplayLabels = (mention = {}) => {
setMentionLabel(labels, 'skill', skill?.value, label) setMentionLabel(labels, 'skill', skill?.value, label)
setMentionLabel(labels, 'skill', skill?.name, label) setMentionLabel(labels, 'skill', skill?.name, label)
}) })
;(mention.subagents || []).forEach((subagent) => { ;(mention.subagents || []).forEach((subagent) => {
const label = subagent?.name || subagent?.label || subagent?.id || subagent?.value || '' const label = subagent?.name || subagent?.label || subagent?.id || subagent?.value || ''
setMentionLabel(labels, 'subagent', subagent?.id, label) 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) 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 value = String(text || '')
const safeStart = Math.max(0, Math.min(start, value.length)) const safeStart = Math.max(0, Math.min(start, value.length))
const safeEnd = Math.max(safeStart, Math.min(end, value.length)) const safeEnd = Math.max(safeStart, Math.min(end, value.length))

View File

@ -25,7 +25,11 @@
</a-select> </a-select>
</template> </template>
<template #actions> <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 /> 新建知识库 <PlusOutlined /> 新建知识库
</a-button> </a-button>
</template> </template>
@ -100,7 +104,8 @@
class="form-section compact-section" class="form-section compact-section"
> >
<h3 class="section-title"> <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> </h3>
<a-input-password <a-input-password
v-if="field.type === 'password'" v-if="field.type === 'password'"
@ -150,7 +155,11 @@
<!-- 共享配置 --> <!-- 共享配置 -->
<div class="form-section compact-section"> <div class="form-section compact-section">
<h3 class="section-title">共享设置</h3> <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>
</div> </div>
<template #footer> <template #footer>
@ -305,9 +314,7 @@ const orderedKbTypes = computed(() => supportedKbTypes.value)
const selectedKbTypeInfo = computed(() => supportedKbTypes.value[newDatabase.kb_type] || null) const selectedKbTypeInfo = computed(() => supportedKbTypes.value[newDatabase.kb_type] || null)
const createParamOptions = computed( const createParamOptions = computed(() => selectedKbTypeInfo.value?.create_params?.options || [])
() => selectedKbTypeInfo.value?.create_params?.options || []
)
const getKbTypeDescription = (typeInfo) => typeInfo?.description || '' const getKbTypeDescription = (typeInfo) => typeInfo?.description || ''
@ -401,13 +408,15 @@ const buildRequestData = () => {
} }
if (selectedKbTypeInfo.value?.requires_embedding_model) { 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.additional_params.chunk_preset_id = newDatabase.chunk_preset_id || 'general'
} }
requestData.share_config = { requestData.share_config = {
access_level: shareConfig.value.access_level, 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 || [] : [] user_uids: shareConfig.value.access_level === 'user' ? shareConfig.value.user_uids || [] : []
} }

View File

@ -249,7 +249,11 @@ const isBinaryPreview = (previewType) => previewType === 'image' || previewType
const createBinaryPreviewUrl = async (entry, response) => { const createBinaryPreviewUrl = async (entry, response) => {
const downloadResponse = const downloadResponse =
entry.source === 'knowledge' 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) : await downloadWorkspaceFile(entry.path)
const blob = await downloadResponse.blob() const blob = await downloadResponse.blob()
return window.URL.createObjectURL(blob) return window.URL.createObjectURL(blob)
@ -485,7 +489,11 @@ const selectKnowledgePath = async (path) => {
const targetIndex = knowledgeBreadcrumbItems.value.findIndex((item) => item.path === path) const targetIndex = knowledgeBreadcrumbItems.value.findIndex((item) => item.path === path)
if (targetIndex < 0) return if (targetIndex < 0) return
const breadcrumbs = knowledgeBreadcrumbItems.value.slice(0, targetIndex + 1) 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) => { const handleSelectListPath = async (path) => {
@ -542,9 +550,15 @@ const handleSelectEntry = async (entry) => {
const handleSwitchKnowledgeVariant = async (variant) => { const handleSwitchKnowledgeVariant = async (variant) => {
const entry = selectedEntry.value const entry = selectedEntry.value
if (!entry || entry.source !== 'knowledge' || !entry.kb_id || !entry.file_id) return 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 = () => { const closePreview = () => {