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
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",

View File

@ -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}"

View File

@ -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):

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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:

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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
)

View File

@ -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():

View File

@ -1,3 +1,3 @@
from .milvus_graph_service import MilvusGraphService
__all__ = ["MilvusGraphService"]
__all__ = ["MilvusGraphService"]

View File

@ -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

View File

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

View File

@ -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

View File

@ -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

View File

@ -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"}

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.datetime_utils import utc_isoformat
_DROPPED_PROCESSING_PARAM_KEYS = {
"_preprocessed_map",
"auto_index",

View File

@ -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,

View File

@ -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

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.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,

View File

@ -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

View File

@ -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"
# 登录页路径

View File

@ -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

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.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

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.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

View File

@ -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

View File

@ -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()

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_knowledge import Base as KnowledgeBase
from yuxi.utils import logger
from yuxi.utils.singleton import SingletonMeta
# 合并两个 Base

View File

@ -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"

View File

@ -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')

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 USER_BASE_URL = '/api/skills'

View File

@ -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>

View File

@ -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()

View File

@ -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()
}

View File

@ -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" />

View File

@ -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>

View File

@ -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 })

View File

@ -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>

View File

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

View File

@ -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 {

View File

@ -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>

View File

@ -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)

View File

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

View File

@ -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>

View File

@ -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

View File

@ -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"

View File

@ -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 {

View File

@ -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)

View File

@ -59,4 +59,4 @@ const handlePanelSave = (config) => {
const handleCancel = () => {
emit('update:modelValue', false)
}
</script>
</script>

View File

@ -256,4 +256,4 @@ defineExpose({ save, resetToDefaults, loadQueryParams })
border-radius: 6px;
}
}
</style>
</style>

View File

@ -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'

View File

@ -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">

View File

@ -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;
}

View File

@ -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">

View File

@ -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;

View File

@ -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()

View File

@ -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
}

View File

@ -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>

View File

@ -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%;

View File

@ -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%;

View File

@ -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>

View File

@ -204,4 +204,4 @@ const sharedDatabases = computed(() =>
font-size: 12px;
line-height: 1.5;
}
</style>
</style>

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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) => {

View File

@ -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

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 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

View File

@ -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))

View File

@ -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 || [] : []
}

View File

@ -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 = () => {