style(format): 格式化代码

This commit is contained in:
Wenjie Zhang 2026-04-27 02:24:52 +08:00
parent ba7c1299a5
commit ee624513f1
29 changed files with 368 additions and 124 deletions

View File

@ -302,7 +302,9 @@ class LightRagKB(KnowledgeBase):
), ),
) )
async def index_file(self, db_id: str, file_id: str, operator_id: str | None = None, params: dict | None = None) -> dict: async def index_file(
self, db_id: str, file_id: str, operator_id: str | None = None, params: dict | None = None
) -> dict:
""" """
Index parsed file (Status: INDEXING -> INDEXED/ERROR_INDEXING) Index parsed file (Status: INDEXING -> INDEXED/ERROR_INDEXING)
@ -395,7 +397,7 @@ class LightRagKB(KnowledgeBase):
logger.info( logger.info(
f"Indexed file {file_id} into LightRAG with {len(chunks)} chunks, " f"Indexed file {file_id} into LightRAG with {len(chunks)} chunks, "
f"chunk_preset_id={processing_params.get('chunk_preset_id')}" f"chunk_preset_id={params.get('chunk_preset_id')}"
) )
# Update status # Update status

View File

@ -284,7 +284,9 @@ class MilvusKB(KnowledgeBase):
"""将文本分割成块""" """将文本分割成块"""
return chunk_markdown(text, file_id, filename, params) return chunk_markdown(text, file_id, filename, params)
async def index_file(self, db_id: str, file_id: str, operator_id: str | None = None, params: dict | None = None) -> dict: async def index_file(
self, db_id: str, file_id: str, operator_id: str | None = None, params: dict | None = None
) -> dict:
""" """
Index parsed file (Status: INDEXING -> INDEXED/ERROR_INDEXING) Index parsed file (Status: INDEXING -> INDEXED/ERROR_INDEXING)

View File

@ -415,7 +415,9 @@ class KnowledgeBaseManager:
kb_instance = await self._get_kb_for_database(db_id) kb_instance = await self._get_kb_for_database(db_id)
return await kb_instance.parse_file(db_id, file_id, operator_id) return await kb_instance.parse_file(db_id, file_id, operator_id)
async def index_file(self, db_id: str, file_id: str, operator_id: str | None = None, params: dict | None = None) -> dict: async def index_file(
self, db_id: str, file_id: str, operator_id: str | None = None, params: dict | None = None
) -> dict:
"""Index parsed file""" """Index parsed file"""
kb_instance = await self._get_kb_for_database(db_id) kb_instance = await self._get_kb_for_database(db_id)
return await kb_instance.index_file(db_id, file_id, operator_id, params=params) return await kb_instance.index_file(db_id, file_id, operator_id, params=params)

View File

@ -263,6 +263,7 @@ async def save_messages_from_langgraph_state(
elif msg_type == "tool": elif msg_type == "tool":
await _save_tool_message(conv_repo, msg_dict) await _save_tool_message(conv_repo, msg_dict)
def _extract_interrupt_info(state) -> Any | None: def _extract_interrupt_info(state) -> Any | None:
"""从 LangGraph state 中提取中断信息""" """从 LangGraph state 中提取中断信息"""
if hasattr(state, "tasks") and state.tasks: if hasattr(state, "tasks") and state.tasks:
@ -1086,7 +1087,7 @@ async def stream_agent_resume(
except Exception as e: except Exception as e:
logger.error(f"Error saving messages from LangGraph state: {e}") logger.error(f"Error saving messages from LangGraph state: {e}")
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
yield make_chunk(status="warning", message=f"消息保存失败: {e}", meta=meta) yield make_resume_chunk(status="warning", message=f"消息保存失败: {e}", meta=meta)
yield make_resume_chunk(status="finished", meta=meta) yield make_resume_chunk(status="finished", meta=meta)

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.config.builtin_providers import BUILTIN_PROVIDERS
from yuxi.repositories.model_provider_repository import ( from yuxi.repositories.model_provider_repository import (
create_model_provider, create_model_provider,
delete_model_provider, delete_model_provider,
@ -14,7 +15,6 @@ from yuxi.repositories.model_provider_repository import (
list_model_providers, list_model_providers,
update_model_provider, update_model_provider,
) )
from yuxi.config.builtin_providers import BUILTIN_PROVIDERS
from yuxi.services.model_cache import is_v2_spec_format from yuxi.services.model_cache import is_v2_spec_format
from yuxi.storage.postgres.models_business import ModelProvider from yuxi.storage.postgres.models_business import ModelProvider

View File

@ -11,7 +11,8 @@ export default defineConfig({
ignoreDeadLinks: [ ignoreDeadLinks: [
/localhost/, /localhost/,
/CONTRIBUTING$/, /CONTRIBUTING$/,
/docker-compose\.yml$/ /docker-compose\.yml$/,
/^\.\/intro\//
], ],
markdown: { markdown: {
config: (md) => { config: (md) => {

View File

@ -27,7 +27,9 @@
cursor: pointer; cursor: pointer;
padding: 4px 8px; padding: 4px 8px;
border-radius: 6px; border-radius: 6px;
transition: color 0.15s, background 0.15s; transition:
color 0.15s,
background 0.15s;
&:hover { &:hover {
color: var(--gray-700); color: var(--gray-700);
@ -90,4 +92,4 @@
min-height: 0; min-height: 0;
overflow: hidden; overflow: hidden;
} }
} }

View File

@ -86,7 +86,6 @@
} }
} }
// Provider 标签样式 // Provider 标签样式
.provider-tag { .provider-tag {
padding: 2px 4px; padding: 2px 4px;

View File

@ -101,7 +101,12 @@ const emit = defineEmits(['update:value', 'change'])
const v2Models = ref({}) const v2Models = ref({})
const v1ModelStatuses = reactive({}) const v1ModelStatuses = reactive({})
const { statusMap: v2ModelStatuses, getStatusIcon: getV2StatusIcon, getStatusClass: getV2StatusClass, getStatusTooltip: getV2StatusTooltip, checkV2Status, checkV2Statuses } = useModelStatus() const {
getStatusIcon: getV2StatusIcon,
getStatusClass: getV2StatusClass,
getStatusTooltip: getV2StatusTooltip,
checkV2Statuses
} = useModelStatus()
const state = reactive({ const state = reactive({
checkingStatus: false checkingStatus: false
}) })

View File

@ -530,12 +530,33 @@ const deleteDatabase = () => {
background: var(--gray-100); background: var(--gray-100);
color: var(--gray-600); color: var(--gray-600);
&.tag-purple { background: #f3e8ff; color: #7c3aed; } &.tag-purple {
&.tag-blue { background: var(--color-info-50); color: var(--color-info-700); } background: #f3e8ff;
&.tag-red { background: var(--color-error-50); color: var(--color-error-700); } color: #7c3aed;
&.tag-green { background: var(--color-success-50); color: var(--color-success-700); } }
&.tag-gold { background: #fef3c7; color: #d97706; } &.tag-blue {
&.tag-cyan { background: #cffafe; color: #0891b2; } background: var(--color-info-50);
&.tag-orange { background: #fff7ed; color: #ea580c; } color: var(--color-info-700);
}
&.tag-red {
background: var(--color-error-50);
color: var(--color-error-700);
}
&.tag-green {
background: var(--color-success-50);
color: var(--color-success-700);
}
&.tag-gold {
background: #fef3c7;
color: #d97706;
}
&.tag-cyan {
background: #cffafe;
color: #0891b2;
}
&.tag-orange {
background: #fff7ed;
color: #ea580c;
}
} }
</style> </style>

View File

@ -125,7 +125,7 @@ const refreshCache = async () => {
} }
// //
const { statusMap: modelStatusMap, getStatusIcon, getStatusTooltip } = useModelStatus() useModelStatus()
const state = reactive({ const state = reactive({
currentModelStatus: null, currentModelStatus: null,
checkingStatus: false, checkingStatus: false,
@ -197,7 +197,6 @@ const handleSelectV2Model = (spec) => {
<style lang="less" scoped> <style lang="less" scoped>
@import '@/assets/css/model-selector-common.less'; @import '@/assets/css/model-selector-common.less';
// //
.status-check-button { .status-check-button {
font-size: 12px; font-size: 12px;

View File

@ -55,11 +55,7 @@
@click="selectTool(tool)" @click="selectTool(tool)"
> >
<div class="item-header"> <div class="item-header">
<component <component :is="getToolIcon(tool.id) || Wrench" :size="16" class="item-icon" />
:is="getToolIcon(tool.id) || Wrench"
:size="16"
class="item-icon"
/>
<span class="item-name">{{ tool.name }}</span> <span class="item-name">{{ tool.name }}</span>
</div> </div>
<div class="item-details item-details-inline"> <div class="item-details item-details-inline">

View File

@ -12,7 +12,9 @@
</div> </div>
<div class="extension-card-info"> <div class="extension-card-info">
<span class="extension-card-name" :title="title">{{ title }}</span> <span class="extension-card-name" :title="title">{{ title }}</span>
<span v-if="subtitle" class="extension-card-subtitle" :title="subtitle">{{ subtitle }}</span> <span v-if="subtitle" class="extension-card-subtitle" :title="subtitle">{{
subtitle
}}</span>
</div> </div>
<div class="extension-card-status"> <div class="extension-card-status">
<slot name="status" /> <slot name="status" />
@ -23,17 +25,17 @@
class="card-action-btn" class="card-action-btn"
:class="`card-action-btn--${actionVariant || 'primary'}`" :class="`card-action-btn--${actionVariant || 'primary'}`"
@click.stop="$emit('actionClick')" @click.stop="$emit('actionClick')"
>{{ actionLabel }}</button> >
{{ actionLabel }}
</button>
<template v-else-if="status"> <template v-else-if="status">
<span <span
v-if="status.label" v-if="status.label"
class="card-status-tag" class="card-status-tag"
:class="`card-status-tag--${status.level || 'info'}`" :class="`card-status-tag--${status.level || 'info'}`"
>{{ status.label }}</span> >{{ status.label }}</span
<span >
class="card-status-dot" <span class="card-status-dot" :class="`card-status-dot--${statusDotColor}`"></span>
:class="`card-status-dot--${statusDotColor}`"
></span>
</template> </template>
</template> </template>
</div> </div>
@ -52,7 +54,10 @@
</div> </div>
</div> </div>
<div v-if="$slots.tags || (normalizedTags && normalizedTags.length > 0)" class="extension-card-tags"> <div
v-if="$slots.tags || (normalizedTags && normalizedTags.length > 0)"
class="extension-card-tags"
>
<slot name="tags"> <slot name="tags">
<span <span
v-for="(tag, idx) in normalizedTags" v-for="(tag, idx) in normalizedTags"
@ -60,7 +65,8 @@
class="card-tag" class="card-tag"
:class="tag.color ? `tag-${tag.color}` : ''" :class="tag.color ? `tag-${tag.color}` : ''"
:style="tag.bgColor ? { backgroundColor: tag.bgColor } : {}" :style="tag.bgColor ? { backgroundColor: tag.bgColor } : {}"
>{{ tag.name }}</span> >{{ tag.name }}</span
>
</slot> </slot>
</div> </div>
@ -116,7 +122,9 @@ const normalizedTags = computed(() => {
border: 1px solid var(--gray-150); border: 1px solid var(--gray-150);
background: linear-gradient(45deg, var(--gray-0) 0%, var(--gray-25) 100%); background: linear-gradient(45deg, var(--gray-0) 0%, var(--gray-25) 100%);
cursor: pointer; cursor: pointer;
transition: border-color 0.2s ease, box-shadow 0.2s ease; transition:
border-color 0.2s ease,
box-shadow 0.2s ease;
overflow: hidden; overflow: hidden;
&:hover { &:hover {
@ -206,7 +214,6 @@ const normalizedTags = computed(() => {
gap: 6px; gap: 6px;
} }
.info-row { .info-row {
display: flex; display: flex;
align-items: center; align-items: center;
@ -260,13 +267,34 @@ const normalizedTags = computed(() => {
background: var(--gray-100); background: var(--gray-100);
color: var(--gray-600); color: var(--gray-600);
&.tag-purple { background: #f3e8ff; color: #7c3aed; } &.tag-purple {
&.tag-blue { background: var(--color-info-50); color: var(--color-info-700); } background: #f3e8ff;
&.tag-red { background: var(--color-error-50); color: var(--color-error-700); } color: #7c3aed;
&.tag-green { background: var(--color-success-50); color: var(--color-success-700); } }
&.tag-gold { background: #fef3c7; color: #d97706; } &.tag-blue {
&.tag-cyan { background: #cffafe; color: #0891b2; } background: var(--color-info-50);
&.tag-orange { background: #fff7ed; color: #ea580c; } color: var(--color-info-700);
}
&.tag-red {
background: var(--color-error-50);
color: var(--color-error-700);
}
&.tag-green {
background: var(--color-success-50);
color: var(--color-success-700);
}
&.tag-gold {
background: #fef3c7;
color: #d97706;
}
&.tag-cyan {
background: #cffafe;
color: #0891b2;
}
&.tag-orange {
background: #fff7ed;
color: #ea580c;
}
} }
.card-action-btn { .card-action-btn {
@ -282,13 +310,17 @@ const normalizedTags = computed(() => {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
line-height: 1; line-height: 1;
transition: background-color 0.18s ease, border-color 0.18s ease, color 0.18s ease; transition:
background-color 0.18s ease,
border-color 0.18s ease,
color 0.18s ease;
cursor: pointer; cursor: pointer;
appearance: none; appearance: none;
background: var(--main-50); background: var(--main-50);
color: var(--main-700); color: var(--main-700);
&:hover, &:focus { &:hover,
&:focus {
outline: none; outline: none;
border-color: var(--main-200); border-color: var(--main-200);
background: var(--main-50); background: var(--main-50);
@ -299,7 +331,8 @@ const normalizedTags = computed(() => {
background: var(--color-error-50); background: var(--color-error-50);
color: var(--color-error-700); color: var(--color-error-700);
&:hover, &:focus { &:hover,
&:focus {
border-color: var(--color-error-200); border-color: var(--color-error-200);
background: var(--color-error-50); background: var(--color-error-50);
color: var(--color-error-800); color: var(--color-error-800);
@ -319,7 +352,8 @@ const normalizedTags = computed(() => {
font-size: 11px; font-size: 11px;
font-weight: 600; font-weight: 600;
&--success, &--on { &--success,
&--on {
background: var(--color-success-50); background: var(--color-success-50);
color: var(--color-success-700); color: var(--color-success-700);
} }

View File

@ -1,5 +1,8 @@
<template> <template>
<div class="extension-card-grid" :style="{ gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))` }"> <div
class="extension-card-grid"
:style="{ gridTemplateColumns: `repeat(auto-fill, minmax(${minWidth}px, 1fr))` }"
>
<slot /> <slot />
<div v-if="!$slots.default && items.length === 0" class="extension-card-grid-empty"> <div v-if="!$slots.default && items.length === 0" class="extension-card-grid-empty">
<a-empty :image="false" :description="emptyText" /> <a-empty :image="false" :description="emptyText" />

View File

@ -28,4 +28,4 @@ defineProps({
max-width: 720px; max-width: 720px;
} }
} }
</style> </style>

View File

@ -76,4 +76,4 @@ defineProps({
background-color: transparent; background-color: transparent;
} }
} }
</style> </style>

View File

@ -14,8 +14,14 @@
</template> </template>
</PageShoulder> </PageShoulder>
<div v-if="filteredEnabledServers.length === 0 && filteredDisabledServers.length === 0" class="extension-card-grid-empty-state"> <div
<a-empty :image="false" :description="searchQuery ? '无匹配 MCP' : '暂无 MCP点击上方按钮添加'" /> v-if="filteredEnabledServers.length === 0 && filteredDisabledServers.length === 0"
class="extension-card-grid-empty-state"
>
<a-empty
:image="false"
:description="searchQuery ? '无匹配 MCP' : '暂无 MCP点击上方按钮添加'"
/>
</div> </div>
<template v-else> <template v-else>
@ -89,7 +95,10 @@ const formModalVisible = ref(false)
const filteredServers = computed(() => { const filteredServers = computed(() => {
const sorted = [...servers.value].sort((a, b) => const sorted = [...servers.value].sort((a, b) =>
String(a.name || '').localeCompare(String(b.name || ''), 'zh-Hans-CN', { sensitivity: 'base', numeric: true }) String(a.name || '').localeCompare(String(b.name || ''), 'zh-Hans-CN', {
sensitivity: 'base',
numeric: true
})
) )
if (!searchQuery.value) return sorted if (!searchQuery.value) return sorted
const q = searchQuery.value.toLowerCase() const q = searchQuery.value.toLowerCase()
@ -98,8 +107,12 @@ const filteredServers = computed(() => {
) )
}) })
const filteredEnabledServers = computed(() => filteredServers.value.filter((item) => !!item.enabled)) const filteredEnabledServers = computed(() =>
const filteredDisabledServers = computed(() => filteredServers.value.filter((item) => !item.enabled)) filteredServers.value.filter((item) => !!item.enabled)
)
const filteredDisabledServers = computed(() =>
filteredServers.value.filter((item) => !item.enabled)
)
const getTransportColor = (transport) => { const getTransportColor = (transport) => {
const colors = { sse: 'orange', stdio: 'green', streamable_http: 'blue' } const colors = { sse: 'orange', stdio: 'green', streamable_http: 'blue' }
@ -109,7 +122,8 @@ const getTransportColor = (transport) => {
const mcpTags = (server) => { const mcpTags = (server) => {
const tags = [] const tags = []
if (server.created_by === 'system') tags.push({ name: '内置' }) if (server.created_by === 'system') tags.push({ name: '内置' })
if (server.transport) tags.push({ name: server.transport, color: getTransportColor(server.transport) }) if (server.transport)
tags.push({ name: server.transport, color: getTransportColor(server.transport) })
return tags return tags
} }
@ -168,4 +182,4 @@ defineExpose({ fetchServers, loading })
font-size: 18px; font-size: 18px;
line-height: 1; line-height: 1;
} }
</style> </style>

View File

@ -40,7 +40,9 @@
:class="[ :class="[
'lucide-icon-btn', 'lucide-icon-btn',
'extension-panel-action', 'extension-panel-action',
server?.enabled === false ? 'extension-panel-action-primary' : 'extension-panel-action-danger' server?.enabled === false
? 'extension-panel-action-primary'
: 'extension-panel-action-danger'
]" ]"
> >
<Plus v-if="server?.enabled === false" :size="14" /> <Plus v-if="server?.enabled === false" :size="14" />
@ -68,21 +70,31 @@
<div class="info-item"> <div class="info-item">
<label>传输类型</label> <label>传输类型</label>
<span> <span>
<a-tag :color="getTransportColor(server.transport)">{{ server.transport }}</a-tag> <a-tag :color="getTransportColor(server.transport)">{{
server.transport
}}</a-tag>
</span> </span>
</div> </div>
<div class="info-item" v-if="Array.isArray(server.tags) && server.tags.length > 0"> <div
class="info-item"
v-if="Array.isArray(server.tags) && server.tags.length > 0"
>
<label>标签</label> <label>标签</label>
<span> <span>
<a-tag v-for="tag in server.tags" :key="tag">{{ tag }}</a-tag> <a-tag v-for="tag in server.tags" :key="tag">{{ tag }}</a-tag>
</span> </span>
</div> </div>
<template v-if="server.transport === 'streamable_http' || server.transport === 'sse'"> <template
v-if="server.transport === 'streamable_http' || server.transport === 'sse'"
>
<div class="info-item" v-if="server.url"> <div class="info-item" v-if="server.url">
<label>MCP URL</label> <label>MCP URL</label>
<span class="code-inline text-break-all">{{ server.url }}</span> <span class="code-inline text-break-all">{{ server.url }}</span>
</div> </div>
<div class="info-item" v-if="server.headers && Object.keys(server.headers).length > 0"> <div
class="info-item"
v-if="server.headers && Object.keys(server.headers).length > 0"
>
<label>请求头</label> <label>请求头</label>
<pre class="code-pre">{{ JSON.stringify(server.headers, null, 2) }}</pre> <pre class="code-pre">{{ JSON.stringify(server.headers, null, 2) }}</pre>
</div> </div>
@ -103,7 +115,9 @@
<div class="info-item" v-if="server.args && server.args.length > 0"> <div class="info-item" v-if="server.args && server.args.length > 0">
<label>参数</label> <label>参数</label>
<span> <span>
<a-tag v-for="(arg, index) in server.args" :key="index" size="small">{{ arg }}</a-tag> <a-tag v-for="(arg, index) in server.args" :key="index" size="small">{{
arg
}}</a-tag>
</span> </span>
</div> </div>
<div class="info-item" v-if="server.env && Object.keys(server.env).length > 0"> <div class="info-item" v-if="server.env && Object.keys(server.env).length > 0">
@ -184,7 +198,10 @@
<div class="tool-description" v-if="tool.description"> <div class="tool-description" v-if="tool.description">
{{ tool.description }} {{ tool.description }}
</div> </div>
<a-collapse v-if="tool.parameters && Object.keys(tool.parameters).length > 0" ghost> <a-collapse
v-if="tool.parameters && Object.keys(tool.parameters).length > 0"
ghost
>
<a-collapse-panel key="params" header="参数"> <a-collapse-panel key="params" header="参数">
<div class="params-list"> <div class="params-list">
<div <div
@ -194,10 +211,16 @@
> >
<div class="param-header"> <div class="param-header">
<span class="param-name">{{ paramName }}</span> <span class="param-name">{{ paramName }}</span>
<span class="param-required" v-if="tool.required?.includes(paramName)">必填</span> <span
class="param-required"
v-if="tool.required?.includes(paramName)"
>必填</span
>
<span class="param-type">{{ param.type || 'any' }}</span> <span class="param-type">{{ param.type || 'any' }}</span>
</div> </div>
<div class="param-desc" v-if="param.description">{{ param.description }}</div> <div class="param-desc" v-if="param.description">
{{ param.description }}
</div>
</div> </div>
</div> </div>
</a-collapse-panel> </a-collapse-panel>
@ -272,7 +295,9 @@ const filteredTools = computed(() => {
if (!toolSearchText.value) return tools.value if (!toolSearchText.value) return tools.value
const search = toolSearchText.value.toLowerCase() const search = toolSearchText.value.toLowerCase()
return tools.value.filter( return tools.value.filter(
(t) => t.name.toLowerCase().includes(search) || (t.description && t.description.toLowerCase().includes(search)) (t) =>
t.name.toLowerCase().includes(search) ||
(t.description && t.description.toLowerCase().includes(search))
) )
}) })
@ -458,4 +483,4 @@ onMounted(() => {
padding: 16px var(--page-padding); padding: 16px var(--page-padding);
} }
} }
</style> </style>

View File

@ -18,7 +18,11 @@
<a-form v-if="formMode === 'form'" layout="vertical" class="extension-form"> <a-form v-if="formMode === 'form'" layout="vertical" class="extension-form">
<a-form-item label="MCP 名称" required class="form-item"> <a-form-item label="MCP 名称" required class="form-item">
<a-input v-model:value="form.name" placeholder="请输入 MCP 名称(唯一标识)" :disabled="editMode" /> <a-input
v-model:value="form.name"
placeholder="请输入 MCP 名称(唯一标识)"
:disabled="editMode"
/>
</a-form-item> </a-form-item>
<a-form-item label="描述" class="form-item"> <a-form-item label="描述" class="form-item">
<a-input v-model:value="form.description" placeholder="请输入 MCP 描述" /> <a-input v-model:value="form.description" placeholder="请输入 MCP 描述" />
@ -44,17 +48,31 @@
<a-input v-model:value="form.url" placeholder="https://example.com/mcp" /> <a-input v-model:value="form.url" placeholder="https://example.com/mcp" />
</a-form-item> </a-form-item>
<a-form-item label="HTTP 请求头" class="form-item"> <a-form-item label="HTTP 请求头" class="form-item">
<a-textarea v-model:value="form.headersText" placeholder='JSON 格式,如:{"Authorization": "Bearer xxx"}' :rows="3" /> <a-textarea
v-model:value="form.headersText"
placeholder='JSON 格式,如:{"Authorization": "Bearer xxx"}'
:rows="3"
/>
</a-form-item> </a-form-item>
<a-row :gutter="16"> <a-row :gutter="16">
<a-col :span="12"> <a-col :span="12">
<a-form-item label="HTTP 超时(秒)" class="form-item"> <a-form-item label="HTTP 超时(秒)" class="form-item">
<a-input-number v-model:value="form.timeout" :min="1" :max="300" style="width: 100%" /> <a-input-number
v-model:value="form.timeout"
:min="1"
:max="300"
style="width: 100%"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-form-item label="SSE 读取超时(秒)" class="form-item"> <a-form-item label="SSE 读取超时(秒)" class="form-item">
<a-input-number v-model:value="form.sse_read_timeout" :min="1" :max="300" style="width: 100%" /> <a-input-number
v-model:value="form.sse_read_timeout"
:min="1"
:max="300"
style="width: 100%"
/>
</a-form-item> </a-form-item>
</a-col> </a-col>
</a-row> </a-row>
@ -64,14 +82,24 @@
<a-input v-model:value="form.command" placeholder="例如npx 或 /path/to/server" /> <a-input v-model:value="form.command" placeholder="例如npx 或 /path/to/server" />
</a-form-item> </a-form-item>
<a-form-item label="参数" class="form-item"> <a-form-item label="参数" class="form-item">
<a-select v-model:value="form.args" mode="tags" placeholder="输入参数后回车添加,如:-m" style="width: 100%" /> <a-select
v-model:value="form.args"
mode="tags"
placeholder="输入参数后回车添加,如:-m"
style="width: 100%"
/>
</a-form-item> </a-form-item>
<a-form-item label="环境变量" class="form-item"> <a-form-item label="环境变量" class="form-item">
<McpEnvEditor v-model="form.env" /> <McpEnvEditor v-model="form.env" />
</a-form-item> </a-form-item>
</template> </template>
<a-form-item label="标签" class="form-item"> <a-form-item label="标签" class="form-item">
<a-select v-model:value="form.tags" mode="tags" placeholder="输入标签后回车添加" style="width: 100%" /> <a-select
v-model:value="form.tags"
mode="tags"
placeholder="输入标签后回车添加"
style="width: 100%"
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -79,7 +107,7 @@
<a-textarea <a-textarea
v-model:value="jsonContent" v-model:value="jsonContent"
:rows="15" :rows="15"
placeholder='请输入 JSON 配置' placeholder="请输入 JSON 配置"
class="json-textarea" class="json-textarea"
/> />
<div class="json-actions"> <div class="json-actions">
@ -129,7 +157,10 @@ const form = reactive({
}) })
const isStdioTransport = computed( const isStdioTransport = computed(
() => String(form.transport || '').trim().toLowerCase() === 'stdio' () =>
String(form.transport || '')
.trim()
.toLowerCase() === 'stdio'
) )
watch( watch(
@ -310,4 +341,4 @@ const handleFormSubmit = async () => {
gap: 8px; gap: 8px;
} }
} }
</style> </style>

View File

@ -2,7 +2,11 @@
<div class="skill-cards-page extension-page-root"> <div class="skill-cards-page extension-page-root">
<PageShoulder search-placeholder="搜索技能..." v-model:search="searchQuery"> <PageShoulder search-placeholder="搜索技能..." v-model:search="searchQuery">
<template #actions> <template #actions>
<a-button @click="handleOpenRemoteInstall" :disabled="loading || importing" class="lucide-icon-btn"> <a-button
@click="handleOpenRemoteInstall"
:disabled="loading || importing"
class="lucide-icon-btn"
>
<Computer :size="14" /> <Computer :size="14" />
<span>远程安装</span> <span>远程安装</span>
</a-button> </a-button>
@ -26,12 +30,17 @@
</template> </template>
</PageShoulder> </PageShoulder>
<div v-if="filteredInstalledSkills.length === 0 && filteredUninstalledBuiltinSkills.length === 0" class="extension-card-grid-empty-state"> <div
v-if="filteredInstalledSkills.length === 0 && filteredUninstalledBuiltinSkills.length === 0"
class="extension-card-grid-empty-state"
>
<a-empty :image="false" description="无匹配技能" /> <a-empty :image="false" description="无匹配技能" />
</div> </div>
<template v-else> <template v-else>
<div v-if="filteredInstalledSkills.length" class="extension-section-header">已添加 Skills</div> <div v-if="filteredInstalledSkills.length" class="extension-section-header">
已添加 Skills
</div>
<ExtensionCardGrid> <ExtensionCardGrid>
<ExtensionCard <ExtensionCard
v-for="skill in filteredInstalledSkills" v-for="skill in filteredInstalledSkills"
@ -46,7 +55,9 @@
</ExtensionCard> </ExtensionCard>
</ExtensionCardGrid> </ExtensionCardGrid>
<div v-if="filteredUninstalledBuiltinSkills.length" class="extension-section-header">可添加 Skills</div> <div v-if="filteredUninstalledBuiltinSkills.length" class="extension-section-header">
可添加 Skills
</div>
<ExtensionCardGrid v-if="filteredUninstalledBuiltinSkills.length"> <ExtensionCardGrid v-if="filteredUninstalledBuiltinSkills.length">
<ExtensionCard <ExtensionCard
v-for="skill in filteredUninstalledBuiltinSkills" v-for="skill in filteredUninstalledBuiltinSkills"
@ -102,13 +113,24 @@
/> />
</a-form-item> </a-form-item>
<div class="remote-install-actions"> <div class="remote-install-actions">
<a-button :loading="listingRemoteSkills" :disabled="installingRemoteSkill" @click="handleListRemoteSkills"> <a-button
:loading="listingRemoteSkills"
:disabled="installingRemoteSkill"
@click="handleListRemoteSkills"
>
查看可安装 Skills 查看可安装 Skills
</a-button> </a-button>
<a-button type="primary" :loading="installingRemoteSkill" :disabled="listingRemoteSkills" @click="handleInstallRemoteSkill"> <a-button
type="primary"
:loading="installingRemoteSkill"
:disabled="listingRemoteSkills"
@click="handleInstallRemoteSkill"
>
安装 安装
</a-button> </a-button>
<span v-if="remoteInstallStatusText" class="remote-install-status">{{ remoteInstallStatusText }}</span> <span v-if="remoteInstallStatusText" class="remote-install-status">{{
remoteInstallStatusText
}}</span>
</div> </div>
<div v-if="remoteSkillOptions.length" class="remote-skill-summary"> <div v-if="remoteSkillOptions.length" class="remote-skill-summary">
共发现 {{ remoteSkillOptions.length }} skills可按输入内容筛选候选项 共发现 {{ remoteSkillOptions.length }} skills可按输入内容筛选候选项
@ -174,9 +196,10 @@ const installedSkillCards = computed(() => {
...skill, ...skill,
sourceType: 'builtin', sourceType: 'builtin',
sourceLabel: '内置', sourceLabel: '内置',
status: skill.status === 'update_available' status:
? { label: '更新可用', level: 'warning' } skill.status === 'update_available'
: { label: '已安装', level: 'success' } ? { label: '更新可用', level: 'warning' }
: { label: '已安装', level: 'success' }
} }
]) ])
) )
@ -194,7 +217,9 @@ const installedSkillCards = computed(() => {
const filteredInstalledSkills = computed(() => installedSkillCards.value.filter(matchesSearch)) const filteredInstalledSkills = computed(() => installedSkillCards.value.filter(matchesSearch))
const filteredUninstalledBuiltinSkills = computed(() => { const filteredUninstalledBuiltinSkills = computed(() => {
return (builtinSkills.value || []).filter((skill) => skill.status === 'not_installed' && matchesSearch(skill)) return (builtinSkills.value || []).filter(
(skill) => skill.status === 'not_installed' && matchesSearch(skill)
)
}) })
const filteredRemoteSkillOptions = computed(() => const filteredRemoteSkillOptions = computed(() =>
@ -208,7 +233,8 @@ const remoteInstallStatusText = computed(() => {
if (!remoteInstallProgress.visible || !remoteInstallProgress.total) return '' if (!remoteInstallProgress.visible || !remoteInstallProgress.total) return ''
const progressText = `[${remoteInstallProgress.completed}/${remoteInstallProgress.total}]` const progressText = `[${remoteInstallProgress.completed}/${remoteInstallProgress.total}]`
const currentSkill = remoteInstallProgress.currentSkill || '' const currentSkill = remoteInstallProgress.currentSkill || ''
const failedText = remoteInstallProgress.failed > 0 ? `, ${remoteInstallProgress.failed} failed` : '' const failedText =
remoteInstallProgress.failed > 0 ? `, ${remoteInstallProgress.failed} failed` : ''
return `${progressText} ${currentSkill}${failedText}`.trim() return `${progressText} ${currentSkill}${failedText}`.trim()
}) })
@ -401,12 +427,16 @@ onMounted(() => {
fetchSkills() fetchSkills()
}) })
defineExpose({ fetchSkills, handleImportUpload, openRemoteInstallModal: handleOpenRemoteInstall, loading }) defineExpose({
fetchSkills,
handleImportUpload,
openRemoteInstallModal: handleOpenRemoteInstall,
loading
})
</script> </script>
<style lang="less" scoped> <style lang="less" scoped>
@import '@/assets/css/extensions.less'; @import '@/assets/css/extensions.less';
</style> </style>
<style lang="less" scoped> <style lang="less" scoped>
@ -469,4 +499,4 @@ defineExpose({ fetchSkills, handleImportUpload, openRemoteInstallModal: handleOp
color: var(--gray-500); color: var(--gray-500);
} }
} }
</style> </style>

View File

@ -19,7 +19,11 @@
</div> </div>
<div class="detail-actions"> <div class="detail-actions">
<a-space :size="8"> <a-space :size="8">
<span v-if="currentSkillStatusLabel" class="panel-status-chip" :class="{ warning: currentSkillStatusTone === 'warning' }"> <span
v-if="currentSkillStatusLabel"
class="panel-status-chip"
:class="{ warning: currentSkillStatusTone === 'warning' }"
>
{{ currentSkillStatusLabel }} {{ currentSkillStatusLabel }}
</span> </span>
<button <button
@ -66,7 +70,9 @@
<div v-if="!isInstalledSkill" class="builtin-uninstalled-state"> <div v-if="!isInstalledSkill" class="builtin-uninstalled-state">
<h3>{{ currentSkill.description }}</h3> <h3>{{ currentSkill.description }}</h3>
<p>版本 {{ currentSkill.version }}</p> <p>版本 {{ currentSkill.version }}</p>
<a-button type="primary" @click="handleInstallBuiltin(currentSkill)">安装内置 Skill</a-button> <a-button type="primary" @click="handleInstallBuiltin(currentSkill)"
>安装内置 Skill</a-button
>
</div> </div>
<a-tabs v-else v-model:activeKey="activeTab" class="minimal-tabs"> <a-tabs v-else v-model:activeKey="activeTab" class="minimal-tabs">
@ -79,9 +85,15 @@
<div class="tree-header"> <div class="tree-header">
<span class="label">项目结构</span> <span class="label">项目结构</span>
<div class="tree-actions"> <div class="tree-actions">
<a-tooltip v-if="!isBuiltinInstalledSkill" title="新建文件"><button @click="openCreateModal(false)"><FilePlus :size="14" /></button></a-tooltip> <a-tooltip v-if="!isBuiltinInstalledSkill" title="新建文件"
<a-tooltip v-if="!isBuiltinInstalledSkill" title="新建目录"><button @click="openCreateModal(true)"><FolderPlus :size="14" /></button></a-tooltip> ><button @click="openCreateModal(false)"><FilePlus :size="14" /></button
<a-tooltip title="刷新"><button @click="reloadTree"><RotateCw :size="14" /></button></a-tooltip> ></a-tooltip>
<a-tooltip v-if="!isBuiltinInstalledSkill" title="新建目录"
><button @click="openCreateModal(true)"><FolderPlus :size="14" /></button
></a-tooltip>
<a-tooltip title="刷新"
><button @click="reloadTree"><RotateCw :size="14" /></button
></a-tooltip>
</div> </div>
</div> </div>
<div class="tree-content"> <div class="tree-content">
@ -127,7 +139,11 @@
</div> </div>
</div> </div>
<div class="editor-main"> <div class="editor-main">
<a-empty v-if="!selectedPath || selectedIsDir" description="选择文件以开始编辑" class="mt-40" /> <a-empty
v-if="!selectedPath || selectedIsDir"
description="选择文件以开始编辑"
class="mt-40"
/>
<template v-else> <template v-else>
<MdPreview <MdPreview
v-if="viewMode === 'preview'" v-if="viewMode === 'preview'"
@ -172,13 +188,34 @@
<div class="config-form"> <div class="config-form">
<a-form layout="vertical"> <a-form layout="vertical">
<a-form-item label="工具依赖 (Tools)"> <a-form-item label="工具依赖 (Tools)">
<a-select v-model:value="dependencyForm.tool_dependencies" mode="multiple" :options="toolDependencyOptions" placeholder="选择工具..." allow-clear show-search /> <a-select
v-model:value="dependencyForm.tool_dependencies"
mode="multiple"
:options="toolDependencyOptions"
placeholder="选择工具..."
allow-clear
show-search
/>
</a-form-item> </a-form-item>
<a-form-item label="MCP 依赖 (Model Context Protocol)"> <a-form-item label="MCP 依赖 (Model Context Protocol)">
<a-select v-model:value="dependencyForm.mcp_dependencies" mode="multiple" :options="mcpDependencyOptions" placeholder="选择 MCP 服务..." allow-clear show-search /> <a-select
v-model:value="dependencyForm.mcp_dependencies"
mode="multiple"
:options="mcpDependencyOptions"
placeholder="选择 MCP 服务..."
allow-clear
show-search
/>
</a-form-item> </a-form-item>
<a-form-item label="Skill 依赖"> <a-form-item label="Skill 依赖">
<a-select v-model:value="dependencyForm.skill_dependencies" mode="multiple" :options="skillDependencyOptions" placeholder="选择 Skill..." allow-clear show-search /> <a-select
v-model:value="dependencyForm.skill_dependencies"
mode="multiple"
:options="skillDependencyOptions"
placeholder="选择 Skill..."
allow-clear
show-search
/>
</a-form-item> </a-form-item>
</a-form> </a-form>
</div> </div>
@ -270,11 +307,17 @@ const dependencyForm = reactive({
}) })
const isInstalledSkill = computed(() => { const isInstalledSkill = computed(() => {
return !!(currentSkill.value && (currentSkill.value.installed_record || currentSkill.value.dir_path)) return !!(
currentSkill.value &&
(currentSkill.value.installed_record || currentSkill.value.dir_path)
)
}) })
const isBuiltinInstalledSkill = computed(() => { const isBuiltinInstalledSkill = computed(() => {
return !!(isInstalledSkill.value && (currentSkill.value?.is_builtin || currentSkill.value?.installed_record)) return !!(
isInstalledSkill.value &&
(currentSkill.value?.is_builtin || currentSkill.value?.installed_record)
)
}) })
const currentSkillStatusLabel = computed(() => { const currentSkillStatusLabel = computed(() => {
@ -312,7 +355,9 @@ const mcpDependencyOptions = computed(() =>
(dependencyOptions.mcps || []).map((i) => ({ label: i, value: i })) (dependencyOptions.mcps || []).map((i) => ({ label: i, value: i }))
) )
const skillDependencyOptions = computed(() => const skillDependencyOptions = computed(() =>
(dependencyOptions.skills || []).filter((s) => s !== currentSkill.value?.slug).map((i) => ({ label: i, value: i })) (dependencyOptions.skills || [])
.filter((s) => s !== currentSkill.value?.slug)
.map((i) => ({ label: i, value: i }))
) )
const goBack = () => { const goBack = () => {
@ -451,7 +496,13 @@ const handleTreeSelect = async (keys, info) => {
} }
const saveCurrentFile = async () => { const saveCurrentFile = async () => {
if (!currentSkill.value || !selectedPath.value || selectedIsDir.value || isBuiltinInstalledSkill.value) return if (
!currentSkill.value ||
!selectedPath.value ||
selectedIsDir.value ||
isBuiltinInstalledSkill.value
)
return
savingFile.value = true savingFile.value = true
try { try {
await skillApi.updateSkillFile(currentSkill.value.slug, { await skillApi.updateSkillFile(currentSkill.value.slug, {
@ -521,7 +572,11 @@ const handleUpdateBuiltin = async (record) => {
const confirmDeleteSkill = () => { const confirmDeleteSkill = () => {
const target = currentSkill.value const target = currentSkill.value
if (!target) return if (!target) return
const isBuiltinTarget = !!(target?.is_builtin || target?.installed_record || target?.sourceType === 'builtin') const isBuiltinTarget = !!(
target?.is_builtin ||
target?.installed_record ||
target?.sourceType === 'builtin'
)
const actionText = isBuiltinTarget ? '卸载' : '删除' const actionText = isBuiltinTarget ? '卸载' : '删除'
Modal.confirm({ Modal.confirm({
title: `确认${actionText}技能「${target.slug}」?`, title: `确认${actionText}技能「${target.slug}」?`,
@ -840,4 +895,4 @@ onMounted(() => {
.pt-12 { .pt-12 {
padding-top: 12px; padding-top: 12px;
} }
</style> </style>

View File

@ -14,7 +14,10 @@
</template> </template>
</PageShoulder> </PageShoulder>
<div v-if="filteredEnabledSubAgents.length === 0 && filteredDisabledSubAgents.length === 0" class="extension-card-grid-empty-state"> <div
v-if="filteredEnabledSubAgents.length === 0 && filteredDisabledSubAgents.length === 0"
class="extension-card-grid-empty-state"
>
<a-empty :image="false" :description="searchQuery ? '无匹配 SubAgent' : '暂无 SubAgent'" /> <a-empty :image="false" :description="searchQuery ? '无匹配 SubAgent' : '暂无 SubAgent'" />
</div> </div>
@ -27,7 +30,7 @@
:title="agent.name" :title="agent.name"
:description="agent.description || '暂无描述'" :description="agent.description || '暂无描述'"
:default-icon="BotIcon" :default-icon="BotIcon"
:tags="agent.is_builtin ? [{ name: '内置' }] : [{name: '添加'}]" :tags="agent.is_builtin ? [{ name: '内置' }] : [{ name: '添加' }]"
:status="{ label: '已添加', level: 'success' }" :status="{ label: '已添加', level: 'success' }"
@click="navigateToDetail(agent)" @click="navigateToDetail(agent)"
> >
@ -42,7 +45,7 @@
:title="agent.name" :title="agent.name"
:description="agent.description || '暂无描述'" :description="agent.description || '暂无描述'"
:default-icon="BotIcon" :default-icon="BotIcon"
:tags="agent.is_builtin ? [{ name: '内置' }] : [{name: '添加'}]" :tags="agent.is_builtin ? [{ name: '内置' }] : [{ name: '添加' }]"
action-label="添加" action-label="添加"
@click="navigateToDetail(agent)" @click="navigateToDetail(agent)"
@action-click="handleSetAgentEnabled(agent, true)" @action-click="handleSetAgentEnabled(agent, true)"
@ -88,7 +91,9 @@
class="model-selector-full" class="model-selector-full"
@select-model="handleModelSelect" @select-model="handleModelSelect"
/> />
<a-button v-if="form.model" type="link" size="small" @click="form.model = ''">清空</a-button> <a-button v-if="form.model" type="link" size="small" @click="form.model = ''"
>清空</a-button
>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -259,4 +264,4 @@ defineExpose({ fetchSubAgents, loading })
width: 100%; width: 100%;
} }
} }
</style> </style>

View File

@ -140,7 +140,11 @@
> >
<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.name" placeholder="请输入 SubAgent 名称(唯一标识)" disabled /> <a-input
v-model:value="form.name"
placeholder="请输入 SubAgent 名称(唯一标识)"
disabled
/>
</a-form-item> </a-form-item>
<a-form-item label="描述" class="form-item"> <a-form-item label="描述" class="form-item">
<a-input v-model:value="form.description" placeholder="请输入 SubAgent 描述" /> <a-input v-model:value="form.description" placeholder="请输入 SubAgent 描述" />
@ -166,7 +170,9 @@
class="model-selector-full" class="model-selector-full"
@select-model="handleModelSelect" @select-model="handleModelSelect"
/> />
<a-button v-if="form.model" type="link" size="small" @click="form.model = ''">清空</a-button> <a-button v-if="form.model" type="link" size="small" @click="form.model = ''"
>清空</a-button
>
</div> </div>
</a-form-item> </a-form-item>
</a-form> </a-form>
@ -353,4 +359,4 @@ onMounted(() => {
width: 100%; width: 100%;
} }
} }
</style> </style>

View File

@ -219,4 +219,4 @@ defineExpose({ fetchTools, loading })
.config-guide { .config-guide {
white-space: pre-line; white-space: pre-line;
} }
</style> </style>

View File

@ -114,7 +114,9 @@ function emitChange(item) {
line-height: 1; line-height: 1;
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
transition: background-color 0.2s ease, color 0.2s ease; transition:
background-color 0.2s ease,
color 0.2s ease;
&:hover { &:hover {
color: var(--gray-900); color: var(--gray-900);

View File

@ -23,7 +23,8 @@ export function useModelStatus() {
const getStatusTooltip = (key) => { const getStatusTooltip = (key) => {
const status = statusMap[key] const status = statusMap[key]
if (!status) return '状态未知' if (!status) return '状态未知'
const text = { available: '可用', unavailable: '不可用', error: '错误' }[status.status] || '未知' const text =
{ available: '可用', unavailable: '不可用', error: '错误' }[status.status] || '未知'
return `${text}: ${status.message || '无详细信息'}` return `${text}: ${status.message || '无详细信息'}`
} }
@ -44,5 +45,12 @@ export function useModelStatus() {
} }
} }
return { statusMap, getStatusIcon, getStatusClass, getStatusTooltip, checkV2Status, checkV2Statuses } return {
} statusMap,
getStatusIcon,
getStatusClass,
getStatusTooltip,
checkV2Status,
checkV2Statuses
}
}

View File

@ -29,4 +29,4 @@ export const modelIcons = {
zhipu: `${ICON_BASE}/zhipu-color.svg`, zhipu: `${ICON_BASE}/zhipu-color.svg`,
zhipuai: `${ICON_BASE}/zhipu-color.svg`, zhipuai: `${ICON_BASE}/zhipu-color.svg`,
'zhipuai-coding-plan': `${ICON_BASE}/zhipu-color.svg` 'zhipuai-coding-plan': `${ICON_BASE}/zhipu-color.svg`
} }

View File

@ -53,9 +53,11 @@ const extensionTabs = [
] ]
const isDetailPage = computed(() => { const isDetailPage = computed(() => {
return route.path.startsWith('/extensions/mcp/') || return (
route.path.startsWith('/extensions/mcp/') ||
route.path.startsWith('/extensions/subagent/') || route.path.startsWith('/extensions/subagent/') ||
route.path.startsWith('/extensions/skill/') route.path.startsWith('/extensions/skill/')
)
}) })
const activeChildLoading = computed(() => { const activeChildLoading = computed(() => {
@ -91,4 +93,4 @@ watch(
} }
} }
} }
</style> </style>

View File

@ -256,7 +256,6 @@ function getProviderInfo(provider) {
] ]
} }
function getProviderStatus(provider) { function getProviderStatus(provider) {
if (!provider.is_enabled) return { label: '未启用', level: 'info' } if (!provider.is_enabled) return { label: '未启用', level: 'info' }
if (provider.credential_status === 'warning') return { label: '凭证缺失', level: 'warning' } if (provider.credential_status === 'warning') return { label: '凭证缺失', level: 'warning' }