feat: 更新内置技能版本,增强技能更新逻辑,优化技能管理界面
This commit is contained in:
parent
8041c5ef4d
commit
6078518577
@ -25,7 +25,7 @@ BUILTIN_SKILLS: list[BuiltinSkillSpec] = [
|
|||||||
slug="reporter",
|
slug="reporter",
|
||||||
source_dir=_SKILLS_ROOT / "reporter",
|
source_dir=_SKILLS_ROOT / "reporter",
|
||||||
description="生成 SQL 查询报表并生成可视化图表。",
|
description="生成 SQL 查询报表并生成可视化图表。",
|
||||||
version="1.0.0",
|
version="2026.03.28",
|
||||||
tool_dependencies=[t["name"] for t in get_tool_info(get_mysql_tools())],
|
tool_dependencies=[t["name"] for t in get_tool_info(get_mysql_tools())],
|
||||||
mcp_dependencies=("mcp-server-chart",),
|
mcp_dependencies=("mcp-server-chart",),
|
||||||
),
|
),
|
||||||
|
|||||||
@ -838,7 +838,7 @@ async def update_builtin_skill(
|
|||||||
item = await repo.get_by_slug(slug)
|
item = await repo.get_by_slug(slug)
|
||||||
if not item:
|
if not item:
|
||||||
raise ValueError(f"内置 skill '{slug}' 未安装")
|
raise ValueError(f"内置 skill '{slug}' 未安装")
|
||||||
if not item.is_builtin:
|
if not item.is_builtin and not _is_builtin_managed(item, slug):
|
||||||
raise ValueError(f"技能 '{slug}' 不是内置 skill")
|
raise ValueError(f"技能 '{slug}' 不是内置 skill")
|
||||||
|
|
||||||
if item.content_hash != spec["content_hash"] and not force:
|
if item.content_hash != spec["content_hash"] and not force:
|
||||||
|
|||||||
@ -677,6 +677,110 @@ async def test_update_builtin_skill_needs_confirm_when_hash_mismatch(
|
|||||||
assert exc_info.value.needs_confirm is True
|
assert exc_info.value.needs_confirm is True
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_update_builtin_skill_accepts_legacy_managed_record(
|
||||||
|
tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||||
|
):
|
||||||
|
monkeypatch.setattr(svc.sys_config, "save_dir", str(tmp_path))
|
||||||
|
|
||||||
|
source_dir = tmp_path / "builtin" / "reporter"
|
||||||
|
source_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
(source_dir / "SKILL.md").write_text(
|
||||||
|
"---\nname: reporter\ndescription: builtin\n---\n# SQL Reporter\n",
|
||||||
|
encoding="utf-8",
|
||||||
|
)
|
||||||
|
|
||||||
|
monkeypatch.setattr(
|
||||||
|
svc,
|
||||||
|
"list_builtin_skill_specs",
|
||||||
|
lambda: [
|
||||||
|
{
|
||||||
|
"slug": "reporter",
|
||||||
|
"name": "reporter",
|
||||||
|
"description": "builtin",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"tool_dependencies": ["mysql_query"],
|
||||||
|
"mcp_dependencies": ["charts"],
|
||||||
|
"skill_dependencies": [],
|
||||||
|
"content_hash": "hash-v2",
|
||||||
|
"source_dir": source_dir,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
monkeypatch.setattr(
|
||||||
|
svc,
|
||||||
|
"get_builtin_skill_specs",
|
||||||
|
lambda: [SimpleNamespace(slug="reporter", source_dir=source_dir)],
|
||||||
|
)
|
||||||
|
|
||||||
|
installed = Skill(
|
||||||
|
slug="reporter",
|
||||||
|
name="reporter",
|
||||||
|
description="old",
|
||||||
|
dir_path="skills/reporter",
|
||||||
|
created_by="system",
|
||||||
|
updated_by="system",
|
||||||
|
version=None,
|
||||||
|
content_hash=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
captured: dict[str, object] = {}
|
||||||
|
|
||||||
|
class FakeRepo:
|
||||||
|
def __init__(self, _db):
|
||||||
|
pass
|
||||||
|
|
||||||
|
async def get_by_slug(self, slug: str):
|
||||||
|
return installed
|
||||||
|
|
||||||
|
async def update_metadata(self, item: Skill, *, name: str, description: str, updated_by: str | None):
|
||||||
|
item.name = name
|
||||||
|
item.description = description
|
||||||
|
captured["metadata_updated_by"] = updated_by
|
||||||
|
return item
|
||||||
|
|
||||||
|
async def update_dependencies(
|
||||||
|
self,
|
||||||
|
item: Skill,
|
||||||
|
*,
|
||||||
|
tool_dependencies: list[str],
|
||||||
|
mcp_dependencies: list[str],
|
||||||
|
skill_dependencies: list[str],
|
||||||
|
updated_by: str | None,
|
||||||
|
):
|
||||||
|
item.tool_dependencies = tool_dependencies
|
||||||
|
item.mcp_dependencies = mcp_dependencies
|
||||||
|
item.skill_dependencies = skill_dependencies
|
||||||
|
captured["deps_updated_by"] = updated_by
|
||||||
|
return item
|
||||||
|
|
||||||
|
async def update_builtin_install(
|
||||||
|
self,
|
||||||
|
item: Skill,
|
||||||
|
*,
|
||||||
|
version: str,
|
||||||
|
content_hash: str,
|
||||||
|
updated_by: str | None,
|
||||||
|
):
|
||||||
|
item.version = version
|
||||||
|
item.content_hash = content_hash
|
||||||
|
item.is_builtin = True
|
||||||
|
item.updated_by = updated_by
|
||||||
|
captured["version"] = version
|
||||||
|
captured["content_hash"] = content_hash
|
||||||
|
captured["updated_by"] = updated_by
|
||||||
|
return item
|
||||||
|
|
||||||
|
monkeypatch.setattr(svc, "SkillRepository", FakeRepo)
|
||||||
|
|
||||||
|
item = await svc.update_builtin_skill(None, "reporter", force=True, updated_by="root")
|
||||||
|
|
||||||
|
assert item.is_builtin is True
|
||||||
|
assert item.version == "1.0.1"
|
||||||
|
assert item.content_hash == "hash-v2"
|
||||||
|
assert captured["updated_by"] == "root"
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_update_builtin_skill_force_overwrites(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
async def test_update_builtin_skill_force_overwrites(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
|
||||||
monkeypatch.setattr(svc.sys_config, "save_dir", str(tmp_path))
|
monkeypatch.setattr(svc.sys_config, "save_dir", str(tmp_path))
|
||||||
|
|||||||
@ -50,6 +50,7 @@
|
|||||||
|
|
||||||
### 修复
|
### 修复
|
||||||
|
|
||||||
|
- 兼容旧版已安装的内置 `reporter` 技能记录:`update_builtin_skill` 现在会识别由 `system` 或 `builtin-system` 管理的历史记录,避免更新时误报“技能 `reporter` 不是内置 skill”
|
||||||
- 调整沙盒 user-data 目录隔离策略:`workspace` 改为全局共享目录 `saves/user-data/workspace`,`uploads/outputs` 继续保持 thread 级隔离;同时更新 thread artifact 权限校验、viewer 文件系统列举逻辑,以及对应的 router/E2E 测试
|
- 调整沙盒 user-data 目录隔离策略:`workspace` 改为全局共享目录 `saves/user-data/workspace`,`uploads/outputs` 继续保持 thread 级隔离;同时更新 thread artifact 权限校验、viewer 文件系统列举逻辑,以及对应的 router/E2E 测试
|
||||||
- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试
|
- 重构聊天接口请求模型:流式与非流式聊天统一使用 `query + agent_config_id` 请求体,并移除路径中的 `agent_id`;同时修复非流式接口实际误走流式执行链路的问题,改为调用 `invoke_messages` 一次性执行,并补充对应测试
|
||||||
- 修复对话线程与 Agent 配置错位的问题:发送消息时将当前 `agent_config_id` 绑定到 thread 的 `extra_metadata`,线程列表接口返回该绑定值,前端切换历史 thread 时会自动恢复对应配置
|
- 修复对话线程与 Agent 配置错位的问题:发送消息时将当前 `agent_config_id` 绑定到 thread 的 `extra_metadata`,线程列表接口返回该绑定值,前端切换历史 thread 时会自动恢复对应配置
|
||||||
|
|||||||
@ -93,6 +93,9 @@
|
|||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
border: 1px solid var(--gray-100);
|
border: 1px solid var(--gray-100);
|
||||||
background-color: var(--gray-0);
|
background-color: var(--gray-0);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
border-color: @border-color;
|
border-color: @border-color;
|
||||||
@ -136,7 +139,7 @@
|
|||||||
|
|
||||||
.item-details {
|
.item-details {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
// flex-direction: column;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -18,76 +18,47 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="list-container">
|
<div class="list-container">
|
||||||
<div v-if="filteredBuiltinSkills.length === 0 && filteredSkills.length === 0" class="empty-text">
|
<div
|
||||||
|
v-if="filteredInstalledSkills.length === 0 && filteredUninstalledBuiltinSkills.length === 0"
|
||||||
|
class="empty-text"
|
||||||
|
>
|
||||||
<a-empty :image="false" description="无匹配技能" />
|
<a-empty :image="false" description="无匹配技能" />
|
||||||
</div>
|
</div>
|
||||||
<div v-if="filteredBuiltinSkills.length" class="list-section-title">内置 Skills</div>
|
<div v-if="filteredInstalledSkills.length" class="list-section-title">已安装 Skills</div>
|
||||||
<template v-for="(skill, index) in filteredBuiltinSkills" :key="`builtin-${skill.slug}`">
|
<template v-for="(skill, index) in filteredInstalledSkills" :key="`installed-${skill.slug}`">
|
||||||
<div
|
<div
|
||||||
class="list-item"
|
class="list-item skill-list-item"
|
||||||
:class="{ active: currentSkill?.slug === skill.slug && currentSkill?.is_builtin_spec }"
|
:class="{ active: currentSkill?.slug === skill.slug }"
|
||||||
@click="selectSkill(skill)"
|
@click="selectSkill(skill)"
|
||||||
>
|
>
|
||||||
<div class="item-header">
|
<div class="item-main-row">
|
||||||
<BookMarked :size="16" class="item-icon" />
|
<div class="item-header">
|
||||||
<span class="item-name">{{ skill.name }}</span>
|
<BookMarked :size="16" class="item-icon" />
|
||||||
<span class="builtin-badge">内置</span>
|
<span class="item-name">{{ skill.name }}</span>
|
||||||
</div>
|
|
||||||
<div class="item-details item-details-inline">
|
|
||||||
<span class="item-slug item-meta mono-text">{{ skill.slug }}</span>
|
|
||||||
<span v-if="skill.status === 'update_available'" class="status-text warning">可更新</span>
|
|
||||||
<span v-else-if="skill.status === 'installed'" class="status-text">已安装</span>
|
|
||||||
<span v-else class="status-text">未安装</span>
|
|
||||||
<div class="item-badges">
|
|
||||||
<span
|
|
||||||
v-if="skill.installed_record?.tool_dependencies?.length"
|
|
||||||
class="dot-badge blue"
|
|
||||||
title="工具依赖"
|
|
||||||
></span>
|
|
||||||
<span
|
|
||||||
v-if="skill.installed_record?.mcp_dependencies?.length"
|
|
||||||
class="dot-badge green"
|
|
||||||
title="MCP依赖"
|
|
||||||
></span>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="item-inline-actions">
|
<div class="item-status">
|
||||||
<a-button
|
<a-button
|
||||||
v-if="skill.status === 'not_installed'"
|
v-if="skill.status === 'update_available'"
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
@click.stop="handleInstallBuiltin(skill)"
|
|
||||||
>
|
|
||||||
安装
|
|
||||||
</a-button>
|
|
||||||
<a-button
|
|
||||||
v-else-if="skill.status === 'update_available'"
|
|
||||||
size="small"
|
size="small"
|
||||||
@click.stop="handleUpdateBuiltin(skill)"
|
@click.stop="handleUpdateBuiltin(skill)"
|
||||||
>
|
>
|
||||||
更新
|
更新
|
||||||
</a-button>
|
</a-button>
|
||||||
|
<span
|
||||||
|
v-else-if="skill.statusLabel"
|
||||||
|
class="status-chip"
|
||||||
|
:class="{ warning: skill.statusTone === 'warning' }"
|
||||||
|
>
|
||||||
|
{{ skill.statusLabel }}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
<div class="item-details">
|
||||||
<div
|
<div class="item-tags">
|
||||||
v-if="index < filteredBuiltinSkills.length - 1 || filteredSkills.length > 0"
|
<span class="source-tag" :class="{ builtin: skill.sourceType === 'builtin' }">
|
||||||
class="list-separator"
|
{{ skill.sourceLabel }}
|
||||||
></div>
|
</span>
|
||||||
</template>
|
</div>
|
||||||
|
|
||||||
<div v-if="filteredSkills.length" class="list-section-title">已安装 Skills</div>
|
|
||||||
<template v-for="(skill, index) in filteredSkills" :key="skill.slug">
|
|
||||||
<div
|
|
||||||
class="list-item"
|
|
||||||
:class="{ active: currentSkill?.slug === skill.slug && !currentSkill?.is_builtin_spec }"
|
|
||||||
@click="selectSkill(skill)"
|
|
||||||
>
|
|
||||||
<div class="item-header">
|
|
||||||
<BookMarked :size="16" class="item-icon" />
|
|
||||||
<span class="item-name">{{ skill.name }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="item-details item-details-inline">
|
|
||||||
<span class="item-slug item-meta mono-text">{{ skill.slug }}</span>
|
|
||||||
<div class="item-badges">
|
<div class="item-badges">
|
||||||
<span
|
<span
|
||||||
v-if="skill.tool_dependencies?.length"
|
v-if="skill.tool_dependencies?.length"
|
||||||
@ -102,7 +73,49 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div v-if="index < filteredSkills.length - 1" class="list-separator"></div>
|
<div
|
||||||
|
v-if="index < filteredInstalledSkills.length - 1 || filteredUninstalledBuiltinSkills.length > 0"
|
||||||
|
class="list-separator"
|
||||||
|
></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div v-if="filteredUninstalledBuiltinSkills.length" class="list-section-title">未安装 Skills</div>
|
||||||
|
<template v-for="(skill, index) in filteredUninstalledBuiltinSkills" :key="`builtin-${skill.slug}`">
|
||||||
|
<div
|
||||||
|
class="list-item skill-list-item"
|
||||||
|
:class="{ active: currentSkill?.slug === skill.slug }"
|
||||||
|
@click="selectSkill(skill)"
|
||||||
|
>
|
||||||
|
<div class="item-main-row">
|
||||||
|
<div class="item-header">
|
||||||
|
<BookMarked :size="16" class="item-icon" />
|
||||||
|
<span class="item-name">{{ skill.name }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-status">
|
||||||
|
<a-button size="small" type="primary" @click.stop="handleInstallBuiltin(skill)">
|
||||||
|
安装
|
||||||
|
</a-button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="item-details item-details-inline">
|
||||||
|
<div class="item-tags">
|
||||||
|
<span class="source-tag builtin">内置</span>
|
||||||
|
</div>
|
||||||
|
<div class="item-badges">
|
||||||
|
<span
|
||||||
|
v-if="skill.installed_record?.tool_dependencies?.length || skill.tool_dependencies?.length"
|
||||||
|
class="dot-badge blue"
|
||||||
|
title="工具依赖"
|
||||||
|
></span>
|
||||||
|
<span
|
||||||
|
v-if="skill.installed_record?.mcp_dependencies?.length || skill.mcp_dependencies?.length"
|
||||||
|
class="dot-badge green"
|
||||||
|
title="MCP依赖"
|
||||||
|
></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div v-if="index < filteredUninstalledBuiltinSkills.length - 1" class="list-separator"></div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -124,6 +137,13 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="panel-actions">
|
<div class="panel-actions">
|
||||||
<a-space :size="8">
|
<a-space :size="8">
|
||||||
|
<span
|
||||||
|
v-if="currentSkillStatusLabel"
|
||||||
|
class="panel-status-chip"
|
||||||
|
:class="{ warning: currentSkillStatusTone === 'warning' }"
|
||||||
|
>
|
||||||
|
{{ currentSkillStatusLabel }}
|
||||||
|
</span>
|
||||||
<a-button
|
<a-button
|
||||||
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'not_installed'"
|
v-if="currentSkill.is_builtin_spec && currentSkill.status === 'not_installed'"
|
||||||
type="primary"
|
type="primary"
|
||||||
@ -407,21 +427,45 @@ const dependencyForm = reactive({
|
|||||||
skill_dependencies: []
|
skill_dependencies: []
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredSkills = computed(() => {
|
const matchesSearch = (skill) => {
|
||||||
const installedSkills = (skills.value || []).filter((skill) => !skill.is_builtin)
|
if (!searchQuery.value) return true
|
||||||
if (!searchQuery.value) return installedSkills
|
|
||||||
const q = searchQuery.value.toLowerCase()
|
const q = searchQuery.value.toLowerCase()
|
||||||
return installedSkills.filter(
|
return skill.name.toLowerCase().includes(q) || skill.slug.toLowerCase().includes(q)
|
||||||
(s) => s.name.toLowerCase().includes(q) || s.slug.toLowerCase().includes(q)
|
}
|
||||||
|
|
||||||
|
const installedSkillCards = computed(() => {
|
||||||
|
const builtinInstalledMap = new Map(
|
||||||
|
(builtinSkills.value || [])
|
||||||
|
.filter((skill) => skill.status !== 'not_installed')
|
||||||
|
.map((skill) => [
|
||||||
|
skill.slug,
|
||||||
|
{
|
||||||
|
...skill,
|
||||||
|
sourceType: 'builtin',
|
||||||
|
sourceLabel: '内置',
|
||||||
|
statusLabel: skill.status === 'update_available' ? '更新可用' : '已安装',
|
||||||
|
statusTone: skill.status === 'update_available' ? 'warning' : 'default'
|
||||||
|
}
|
||||||
|
])
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const importedInstalled = (skills.value || [])
|
||||||
|
.filter((skill) => !builtinInstalledMap.has(skill.slug))
|
||||||
|
.map((skill) => ({
|
||||||
|
...skill,
|
||||||
|
sourceType: 'imported',
|
||||||
|
sourceLabel: '导入',
|
||||||
|
statusLabel: '已上传',
|
||||||
|
statusTone: 'default'
|
||||||
|
}))
|
||||||
|
|
||||||
|
return [...builtinInstalledMap.values(), ...importedInstalled]
|
||||||
})
|
})
|
||||||
|
|
||||||
const filteredBuiltinSkills = computed(() => {
|
const filteredInstalledSkills = computed(() => installedSkillCards.value.filter(matchesSearch))
|
||||||
if (!searchQuery.value) return builtinSkills.value
|
|
||||||
const q = searchQuery.value.toLowerCase()
|
const filteredUninstalledBuiltinSkills = computed(() => {
|
||||||
return builtinSkills.value.filter(
|
return (builtinSkills.value || []).filter((skill) => skill.status === 'not_installed' && matchesSearch(skill))
|
||||||
(s) => s.name.toLowerCase().includes(q) || s.slug.toLowerCase().includes(q)
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
const isInstalledSkill = computed(() => {
|
const isInstalledSkill = computed(() => {
|
||||||
@ -432,6 +476,22 @@ 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 skill = currentSkill.value
|
||||||
|
if (!skill) return ''
|
||||||
|
if (skill.is_builtin_spec) {
|
||||||
|
if (skill.status === 'not_installed') return '未安装'
|
||||||
|
if (skill.status === 'update_available') return '更新可用'
|
||||||
|
return '已安装'
|
||||||
|
}
|
||||||
|
if (skill.is_builtin) return '已安装'
|
||||||
|
return '已上传'
|
||||||
|
})
|
||||||
|
|
||||||
|
const currentSkillStatusTone = computed(() => {
|
||||||
|
return currentSkill.value?.status === 'update_available' ? 'warning' : 'default'
|
||||||
|
})
|
||||||
|
|
||||||
const canSave = computed(() => {
|
const canSave = computed(() => {
|
||||||
if (!selectedPath.value || selectedIsDir.value) return false
|
if (!selectedPath.value || selectedIsDir.value) return false
|
||||||
return fileContent.value !== originalFileContent.value
|
return fileContent.value !== originalFileContent.value
|
||||||
@ -501,7 +561,9 @@ const fetchSkills = async () => {
|
|||||||
}))
|
}))
|
||||||
|
|
||||||
// 默认选中第一个技能并加载 SKILL.md
|
// 默认选中第一个技能并加载 SKILL.md
|
||||||
const preferredList = builtinSkills.value.length ? builtinSkills.value : filteredSkills.value
|
const preferredList = filteredInstalledSkills.value.length
|
||||||
|
? filteredInstalledSkills.value
|
||||||
|
: filteredUninstalledBuiltinSkills.value
|
||||||
if (!currentSkill.value && preferredList.length > 0) {
|
if (!currentSkill.value && preferredList.length > 0) {
|
||||||
await selectSkill(preferredList[0])
|
await selectSkill(preferredList[0])
|
||||||
} else if (currentSkill.value) {
|
} else if (currentSkill.value) {
|
||||||
@ -816,53 +878,124 @@ defineExpose({
|
|||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-item {
|
.skill-list-item {
|
||||||
.item-header {
|
|
||||||
gap: 8px;
|
.item-main-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-start;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.builtin-badge {
|
.item-header {
|
||||||
padding: 1px 6px;
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
min-width: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
.item-name {
|
||||||
|
min-width: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.item-status {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 24px;
|
||||||
|
padding: 0 9px;
|
||||||
border-radius: 999px;
|
border-radius: 999px;
|
||||||
background: var(--color-primary-50);
|
background: var(--gray-100);
|
||||||
color: var(--color-primary-700);
|
color: var(--gray-600);
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
line-height: 18px;
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: var(--color-warning-50);
|
||||||
|
color: var(--color-warning-900);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-details {
|
.item-details {
|
||||||
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
|
||||||
.status-text {
|
.item-tags {
|
||||||
color: var(--gray-500);
|
display: flex;
|
||||||
font-size: 12px;
|
align-items: center;
|
||||||
&.warning {
|
gap: 8px;
|
||||||
color: #d97706;
|
min-width: 0;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-inline-actions {
|
.source-tag {
|
||||||
margin-left: auto;
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 0 8px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--color-info-50);
|
||||||
|
color: var(--color-info-700);
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
&.builtin {
|
||||||
|
background: var(--color-primary-50);
|
||||||
|
color: var(--color-primary-700);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.item-badges {
|
.item-badges {
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 4px;
|
gap: 4px;
|
||||||
|
flex-shrink: 0;
|
||||||
.dot-badge {
|
.dot-badge {
|
||||||
width: 6px;
|
width: 6px;
|
||||||
height: 6px;
|
height: 6px;
|
||||||
border-radius: 50%;
|
border-radius: 50%;
|
||||||
&.blue {
|
&.blue {
|
||||||
background-color: #3b82f6;
|
background-color: var(--color-info-500);
|
||||||
}
|
}
|
||||||
&.green {
|
&.green {
|
||||||
background-color: #22c55e;
|
background-color: var(--color-success-500);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel-status-chip {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: var(--gray-100);
|
||||||
|
color: var(--gray-600);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
line-height: 1;
|
||||||
|
|
||||||
|
&.warning {
|
||||||
|
background: var(--color-warning-50);
|
||||||
|
color: var(--color-warning-900);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.builtin-uninstalled-state {
|
.builtin-uninstalled-state {
|
||||||
padding: 24px;
|
padding: 24px;
|
||||||
h3 {
|
h3 {
|
||||||
@ -956,7 +1089,7 @@ defineExpose({
|
|||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
color: var(--gray-500);
|
color: var(--gray-500);
|
||||||
.save-hint {
|
.save-hint {
|
||||||
color: #f59e0b;
|
color: var(--color-warning-500);
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
margin-left: 4px;
|
margin-left: 4px;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
>
|
>
|
||||||
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
<a-button type="primary" :loading="skillsImporting" class="lucide-icon-btn">
|
||||||
<Upload :size="14" />
|
<Upload :size="14" />
|
||||||
<span>导入 ZIP</span>
|
<span>上传 ZIP</span>
|
||||||
</a-button>
|
</a-button>
|
||||||
</a-upload>
|
</a-upload>
|
||||||
<a-button @click="handleSkillsRefresh" :disabled="skillsLoading" class="lucide-icon-btn">
|
<a-button @click="handleSkillsRefresh" :disabled="skillsLoading" class="lucide-icon-btn">
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user