From 1dfe3756a9a9a7207bfb341b8af129d0e5f56206 Mon Sep 17 00:00:00 2001 From: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> Date: Mon, 10 Aug 2026 10:10:01 +0800 Subject: [PATCH] feat(search): refine compliance discovery interactions Signed-off-by: XiaoSeS <87064762+XiaoSeS@users.noreply.github.com> --- docs/07-skill-protocol.md | 3 +- docs/24-compliance-metadata-design.md | 4 + .../skill/compliance-snapshot-panel.test.tsx | 63 ++++++++-- .../skill/compliance-snapshot-panel.tsx | 111 ++++++++++++------ web/src/i18n/locales/en.json | 9 +- web/src/i18n/locales/zh.json | 9 +- web/src/pages/search.test.tsx | 19 --- web/src/pages/search.tsx | 21 ---- 8 files changed, 143 insertions(+), 96 deletions(-) diff --git a/docs/07-skill-protocol.md b/docs/07-skill-protocol.md index c247e748..d98ca793 100644 --- a/docs/07-skill-protocol.md +++ b/docs/07-skill-protocol.md @@ -63,7 +63,8 @@ x-astron-compliance: # 可选,平台私有合规元数据 ``` > 合规元数据先按 SkillHub/Astron 私有扩展实现,字段名采用 `x-astron-compliance`。 -> 当前第一阶段支持发布校验和版本级 `complianceSnapshot` 固化;详情展示、审核 diff、搜索 facet +> 当前第一阶段支持发布校验和版本级 `complianceSnapshot` 固化;这些信息表示“技能作者声明的合规映射”, +> SkillHub 校验证据引用的格式和可访问性,但不等同于第三方认证或平台背书。详情展示、审核 diff、搜索投影 > 和 Runtime trace 集成按后续阶段推进。设计边界、分阶段实现和 Runtime 职责划分见 > [24-compliance-metadata-design.md](24-compliance-metadata-design.md)。 diff --git a/docs/24-compliance-metadata-design.md b/docs/24-compliance-metadata-design.md index 1d060b9b..a7ea9c05 100644 --- a/docs/24-compliance-metadata-design.md +++ b/docs/24-compliance-metadata-design.md @@ -20,6 +20,10 @@ Issue #556 提出的方向是让 SkillHub 支持“可标准映射、可审计 > SkillHub 负责“这个技能版本声明了什么合规能力”;Agent Runtime 负责“这次执行实际用了哪个技能版本”。两者通过 `skillVersionId + complianceSnapshotDigest` 关联。 +这里的 compliance 是作者随技能包提交的声明型元数据。SkillHub 第一阶段只验证字段结构、取值格式、 +包内证据文件是否存在、外部证据 URL 是否是合法 HTTP(S) URL,并生成不可变快照摘要;它不验证外部标准内容是否真实适用, +也不代表第三方审计、认证通过或平台背书。 + ## 2. 职责边界 ### 2.1 SkillHub 职责 diff --git a/web/src/features/skill/compliance-snapshot-panel.test.tsx b/web/src/features/skill/compliance-snapshot-panel.test.tsx index 5070ebb5..9bf957c3 100644 --- a/web/src/features/skill/compliance-snapshot-panel.test.tsx +++ b/web/src/features/skill/compliance-snapshot-panel.test.tsx @@ -1,5 +1,8 @@ +/** @vitest-environment jsdom */ + +import { cleanup, fireEvent, render, screen } from '@testing-library/react' import { renderToStaticMarkup } from 'react-dom/server' -import { describe, expect, it, vi } from 'vitest' +import { afterEach, describe, expect, it, vi } from 'vitest' import { ComplianceSnapshotPanel } from './compliance-snapshot-panel' vi.mock('react-i18next', async () => { @@ -8,14 +11,32 @@ vi.mock('react-i18next', async () => { ...actual, useTranslation: () => ({ t: (key: string, values?: Record) => - key === 'compliance.mappingCount' ? `${values?.count} mappings` : key, + key === 'compliance.mappingCount' + ? `${values?.count} mappings` + : key === 'common.expand' + ? '展开详情' + : key === 'common.collapse' + ? '收起详情' + : key === 'compliance.title' + ? '合规声明' + : key, }), } }) describe('ComplianceSnapshotPanel', () => { - it('renders compliance mappings and evidence', () => { - const html = renderToStaticMarkup( + afterEach(() => { + cleanup() + }) + + it('renders nothing when there are no compliance mappings', () => { + const html = renderToStaticMarkup() + + expect(html).toBe('') + }) + + it('renders a compact summary by default and expands on demand', () => { + render( { title: 'Command and Scripting Interpreter', evidence: [{ type: 'packaged-file', path: 'references/standards.md', sha256: 'abc' }], }, + { + standard: 'nist-csf', + version: '2.0', + controlId: 'PR.DS-01', + title: 'Data-at-rest protection', + evidence: [], + }, + { + standard: 'soc2', + version: '2023', + controlId: 'CC6.1', + title: 'Logical Access Security', + evidence: [], + }, ], }} />, ) - expect(html).toContain('compliance.title') - expect(html).toContain('1 mappings') - expect(html).toContain('mitre-attack') - expect(html).toContain('T1059') - expect(html).toContain('references/standards.md') - }) + const toggle = screen.getByRole('button', { name: '展开详情' }) + expect(toggle).toBeTruthy() + expect(toggle.getAttribute('aria-expanded')).toBe('false') + expect(screen.getByText('mitre-attack · T1059')).toBeTruthy() + expect(screen.getByText('+1')).toBeTruthy() + expect(screen.queryByText('references/standards.md')).toBeNull() - it('renders nothing when there are no compliance mappings', () => { - const html = renderToStaticMarkup() + fireEvent.click(toggle) - expect(html).toBe('') + const expandedToggle = screen.getByRole('button', { name: '收起详情' }) + expect(expandedToggle.getAttribute('aria-expanded')).toBe('true') + expect(screen.getByText('references/standards.md')).toBeTruthy() + expect(screen.getByText('soc2')).toBeTruthy() }) }) diff --git a/web/src/features/skill/compliance-snapshot-panel.tsx b/web/src/features/skill/compliance-snapshot-panel.tsx index b35460c1..a08d50cd 100644 --- a/web/src/features/skill/compliance-snapshot-panel.tsx +++ b/web/src/features/skill/compliance-snapshot-panel.tsx @@ -1,4 +1,5 @@ -import { ExternalLink, ShieldCheck } from 'lucide-react' +import { useState } from 'react' +import { ChevronDown, ChevronUp, ExternalLink, ShieldCheck } from 'lucide-react' import { useTranslation } from 'react-i18next' import type { ComplianceSnapshot } from '@/api/types' import { cn } from '@/shared/lib/utils' @@ -6,6 +7,7 @@ import { cn } from '@/shared/lib/utils' interface ComplianceSnapshotPanelProps { snapshot?: ComplianceSnapshot | null className?: string + defaultExpanded?: boolean } function shortDigest(digest?: string) { @@ -18,61 +20,98 @@ function shortDigest(digest?: string) { return `${digest.slice(0, 17)}…` } -export function ComplianceSnapshotPanel({ snapshot, className }: ComplianceSnapshotPanelProps) { +export function ComplianceSnapshotPanel({ snapshot, className, defaultExpanded = false }: ComplianceSnapshotPanelProps) { const { t } = useTranslation() const items = snapshot?.items?.filter((item) => item.standard || item.controlId) ?? [] + const [isExpanded, setIsExpanded] = useState(defaultExpanded) if (items.length === 0) { return null } return ( -
+
-
+
{t('compliance.title')} {t('compliance.mappingCount', { count: items.length })} + {snapshot?.schemaVersion ? ( + + {snapshot.schemaVersion} + + ) : null}
-
- {shortDigest(snapshot?.digest)} +
+
+ {shortDigest(snapshot?.digest)} +
+
-
- {items.map((item, index) => ( -
-
- - {item.standard ?? t('compliance.unknownStandard')} - - {item.version ? ( - {item.version} - ) : null} - {item.controlId ?? '—'} -
- {item.title ? ( -
{item.title}
- ) : null} - {item.evidence && item.evidence.length > 0 ? ( -
- {item.evidence.map((evidence, evidenceIndex) => ( - - {evidence.url ? : null} - {evidence.path ?? evidence.url ?? evidence.type ?? t('compliance.evidence')} - - ))} -
- ) : null} -
+
+ {items.slice(0, isExpanded ? items.length : 2).map((item, index) => ( + + + {[item.standard, item.controlId].filter(Boolean).join(' · ')} + ))} + {!isExpanded && items.length > 2 ? ( + + +{items.length - 2} + + ) : null}
+ + {isExpanded ? ( +
+ {items.map((item, index) => ( +
+
+ + {item.standard ?? t('compliance.unknownStandard')} + + {item.version ? ( + {item.version} + ) : null} + {item.controlId ?? '—'} +
+ {item.title ? ( +
{item.title}
+ ) : null} + {item.evidence && item.evidence.length > 0 ? ( +
+ {item.evidence.map((evidence, evidenceIndex) => ( + + {evidence.url ? : null} + {evidence.path ?? evidence.url ?? evidence.type ?? t('compliance.evidence')} + + ))} +
+ ) : null} +
+ ))} +
+ ) : null}
) } diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 06868fe6..65a1e265 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -1,10 +1,14 @@ { "compliance": { - "title": "Compliance mappings", + "title": "Compliance claims", "mappingCount": "{{count}} items", "unknownStandard": "Unknown standard", "evidence": "Evidence" }, + "common": { + "expand": "Expand details", + "collapse": "Collapse details" + }, "nav": { "landing": "Home", "home": "Skill Center", @@ -199,8 +203,7 @@ "enterKeyword": "Please enter a search keyword", "results": "{{count}} skills found", "resultCount": "Found <1>{{count}} results", - "loadingMore": "Updating search results...", - "complianceSuggestions": "Compliance:" + "loadingMore": "Updating search results..." }, "searchBar": { "placeholder": "Search skills...", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index 348b9dcd..cf1b92b8 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -1,10 +1,14 @@ { "compliance": { - "title": "合规映射", + "title": "合规声明", "mappingCount": "{{count}} 项", "unknownStandard": "未知标准", "evidence": "证据" }, + "common": { + "expand": "展开详情", + "collapse": "收起详情" + }, "nav": { "landing": "首页", "home": "技能中心", @@ -199,8 +203,7 @@ "enterKeyword": "请输入搜索关键词", "results": "找到 {{count}} 个技能", "resultCount": "找到 <1>{{count}} 个结果", - "loadingMore": "正在更新搜索结果...", - "complianceSuggestions": "合规检索:" + "loadingMore": "正在更新搜索结果..." }, "searchBar": { "placeholder": "搜索技能...", diff --git a/web/src/pages/search.test.tsx b/web/src/pages/search.test.tsx index 3c83bef8..4db0210c 100644 --- a/web/src/pages/search.test.tsx +++ b/web/src/pages/search.test.tsx @@ -200,25 +200,6 @@ describe('SearchPage', () => { }) }) - it('offers compliance search suggestions that update the query', () => { - renderToStaticMarkup() - - findButton('MITRE T1059').onClick?.() - - expect(navigateMock).toHaveBeenCalledWith({ - to: '/search', - search: { - q: 'MITRE T1059', - namespace: 'team-ai', - label: 'code-generation', - sort: 'downloads', - page: 0, - starredOnly: false, - }, - replace: true, - }) - }) - it('preserves the active label when paging and when toggling starred-only', () => { renderToStaticMarkup() diff --git a/web/src/pages/search.tsx b/web/src/pages/search.tsx index dbfec8ce..a9426b55 100644 --- a/web/src/pages/search.tsx +++ b/web/src/pages/search.tsx @@ -18,7 +18,6 @@ import { Button } from '@/shared/ui/button' import { APP_SHELL_PAGE_CLASS_NAME } from '@/app/page-shell-style' const PAGE_SIZE = 12 -const COMPLIANCE_SEARCH_SUGGESTIONS = ['MITRE T1059', 'NIST CSF', 'SOC2', 'GDPR'] function blurActiveElement() { if (typeof document === 'undefined' || typeof HTMLElement === 'undefined') { @@ -166,13 +165,6 @@ export function SearchPage() { }) } - const handleComplianceSuggestion = (query: string) => { - setQueryInput(formatNamespaceSearchInput(namespace, query)) - startTransition(() => { - navigate({ to: '/search', search: { q: query, namespace, label: selectedLabel, sort, page: 0, starredOnly }, replace: true }) - }) - } - const handleSortChange = (newSort: string) => { navigate({ to: '/search', search: { q, namespace, label: selectedLabel, sort: newSort, page: 0, starredOnly } }) } @@ -313,19 +305,6 @@ export function SearchPage() { ) : null}
-
- {t('search.complianceSuggestions')} - {COMPLIANCE_SEARCH_SUGGESTIONS.map((suggestion) => ( - - ))} -
{/* Results */}