From 2b87c6c5ea06f82482016fd4129fadb8de5ad35f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=8D=83=E4=B9=98=E5=A6=8D=20=28Xiaoyaner=29?= Date: Fri, 30 Oct 2026 22:45:21 +0800 Subject: [PATCH] feat: add skill presentation fields MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: 千乘妍 (Xiaoyaner) --- docs/07-skill-protocol.md | 5 +- .../add-skill-presentation-fields/design.md | 7 +++ .../add-skill-presentation-fields/proposal.md | 10 ++++ .../specs/skill-presentation/spec.md | 23 +++++++++ .../add-skill-presentation-fields/tasks.md | 10 ++++ .../portal/SkillPublishController.java | 6 ++- .../dto/SkillPackagePreviewResponse.java | 5 +- .../service/SkillPackagePreviewService.java | 3 +- .../service/SkillPublishAppService.java | 12 +++-- .../skillhub/domain/review/ReviewService.java | 2 +- .../domain/skill/metadata/SkillMetadata.java | 47 ++++++++++++++++++- .../skill/metadata/SkillMetadataParser.java | 10 +++- .../skill/service/SkillGovernanceService.java | 2 +- .../skill/service/SkillPublishService.java | 29 +++++++++++- .../metadata/SkillMetadataParserTest.java | 16 +++++++ .../PostgresSearchRebuildService.java | 1 + web/src/i18n/locales/en.json | 2 + web/src/i18n/locales/zh.json | 2 + web/src/pages/dashboard/publish.tsx | 25 ++++++++++ web/src/pages/skill-detail.tsx | 5 +- web/src/shared/hooks/use-skill-queries.ts | 7 ++- web/src/shared/lib/version-presentation.ts | 9 +++- 22 files changed, 215 insertions(+), 23 deletions(-) create mode 100644 openspec/changes/add-skill-presentation-fields/design.md create mode 100644 openspec/changes/add-skill-presentation-fields/proposal.md create mode 100644 openspec/changes/add-skill-presentation-fields/specs/skill-presentation/spec.md create mode 100644 openspec/changes/add-skill-presentation-fields/tasks.md diff --git a/docs/07-skill-protocol.md b/docs/07-skill-protocol.md index ef2a17f9a..76b06f0c0 100644 --- a/docs/07-skill-protocol.md +++ b/docs/07-skill-protocol.md @@ -44,8 +44,9 @@ description: When to use # 必需,1-2 句话 - 网页发布 multipart 可选字段 `displayName` 在 trim 后非空时优先于包内展示名,最长 200 字符;空白回退包内字段。该覆盖值保存在版本元数据快照,原始 frontmatter 和文件内容不改写;下载 ZIP 沿用现有重新打包机制,压缩包二进制可能不同。CLI 不填写此字段时继续使用包内规则。 - 网页上传后通过鉴权只读 `POST /api/web/skills/package-preview`(也提供 `/api/v1/skills/package-preview`)读取 `name/displayName/description/version`;此步骤不保存包、不创建技能或启动扫描,正式提交仍执行完整校验。 - 上述类型和长度限制在新上传解析时检查。读取历史版本快照时,非字符串或超长的旧 `display-name` 回退到该版本的 `name`,原始 frontmatter 保留,避免阻塞审核、提升或版本撤回。 -- `description` 映射为 `skill.summary` -- frontmatter 完整解析结果存入 `skill_version.parsed_metadata_json` +- `metadata.short-description` 可选,作为卡片、列表和搜索摘要,去除首尾空白后最多 200 个字符;缺失时回退到原始 `description`。`metadata.overview` 可选,为详情页 Markdown 概览,最多 20,000 个字符;缺失时详情页按 README.md → SKILL.md 回退。 +- 网页发布 multipart 可选字段 `shortDescription`、`overview` 和 `resetPresentation`;网页显式值优先于包内元数据,`resetPresentation=true` 清除自定义覆盖。值随版本快照保存,原始包文件不变。 +- 有效 short description 映射为 `skill.summary`;原始 `description` 仍加入搜索文本。frontmatter 完整解析结果存入 `skill_version.parsed_metadata_json`。 平台扩展字段(可选,`x-astron-` 前缀): diff --git a/openspec/changes/add-skill-presentation-fields/design.md b/openspec/changes/add-skill-presentation-fields/design.md new file mode 100644 index 000000000..7c5ca64ba --- /dev/null +++ b/openspec/changes/add-skill-presentation-fields/design.md @@ -0,0 +1,7 @@ +# Design + +- Parse `metadata.short-description` and `metadata.overview` from SKILL.md, with limits of 200 and 20,000 characters. +- Accept multipart `shortDescription`, `overview`, and `resetPresentation`; web values override package metadata and are stored in `parsedMetadataJson` as `shortDescriptionOverride`, `overviewOverride`, and `presentationInheritedFrom`. +- Keep the existing display-name/version snapshot model and publish/review/rollback lifecycle. Pending versions never update the Skill container summary. +- The effective short description feeds `skill.summary`; search also retains the original package description in `search_text`. Overview is detail-only and falls back to README then SKILL.md in the existing documentation reader. +- Web package preview supplies defaults without editing the uploaded archive. Generated API types remain authoritative. diff --git a/openspec/changes/add-skill-presentation-fields/proposal.md b/openspec/changes/add-skill-presentation-fields/proposal.md new file mode 100644 index 000000000..594ac348d --- /dev/null +++ b/openspec/changes/add-skill-presentation-fields/proposal.md @@ -0,0 +1,10 @@ +# Add skill presentation fields + +## Why +Skill packages currently expose only one description and cannot provide a concise card summary plus a richer detail overview without modifying package files. + +## Scope +Add optional short description and Markdown overview to web and CLI-compatible publish metadata. Values are version snapshots, preserve package files, and use package metadata/default documentation when omitted. Public content changes only when the version publishes. + +## Non-goals +No upload-free editing, database migration, i18n variants, or dedicated presentation file. diff --git a/openspec/changes/add-skill-presentation-fields/specs/skill-presentation/spec.md b/openspec/changes/add-skill-presentation-fields/specs/skill-presentation/spec.md new file mode 100644 index 000000000..bbf6fec84 --- /dev/null +++ b/openspec/changes/add-skill-presentation-fields/specs/skill-presentation/spec.md @@ -0,0 +1,23 @@ +# Requirements + +## Presentation metadata + +The system MUST accept optional `metadata.short-description` and `metadata.overview` in SKILL.md. The short description MUST be at most 200 characters and the overview at most 20,000 characters. + +## Web publish precedence + +A web-provided value MUST override the package value for that submitted version. The archive MUST remain unchanged. Reset MUST remove the custom override so the package/default resolution applies. + +## Lifecycle isolation + +A pending version MUST NOT replace the currently published title, summary, or overview. Published metadata MUST be restored by rollback/yank using the selected published version snapshot. + +## Search and detail + +The effective short description MUST be used as the public summary. The original package description MUST remain searchable. The detailed overview MUST NOT be indexed and MUST fall back to README.md then SKILL.md when no explicit/package overview exists. + +## Scenarios + +- Given package metadata and web values, when previewing, the form is prefilled from package defaults and explicit edits win. +- Given a pending update, when it is submitted, public summary remains unchanged until publication. +- Given reset, when the version publishes, it uses package defaults and later default versions read their own package defaults. diff --git a/openspec/changes/add-skill-presentation-fields/tasks.md b/openspec/changes/add-skill-presentation-fields/tasks.md new file mode 100644 index 000000000..0e135035e --- /dev/null +++ b/openspec/changes/add-skill-presentation-fields/tasks.md @@ -0,0 +1,10 @@ +# Tasks + +- [x] Add SKILL.md metadata parsing and validation. +- [x] Add multipart publish fields and package-preview defaults. +- [x] Snapshot presentation fields in parsed metadata and update effective published summary. +- [x] Preserve original description in search text and keep pending updates isolated. +- [x] Add web short-description and Markdown-overview inputs. +- [ ] Add full inheritance/restore-default/rollback acceptance coverage. +- [ ] Regenerate OpenAPI types and update CLI usage documentation. +- [ ] Add detail-page overview projection and View SKILL.md entry. diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java index 7e2141310..7579bd803 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/controller/portal/SkillPublishController.java @@ -83,6 +83,9 @@ public class SkillPublishController extends BaseApiController { @RequestParam(value = "avatarUploadId", required = false) String avatarUploadId, @RequestParam(value = "expectedSlug", required = false) String expectedSlug, @RequestParam(value = "displayName", required = false) String displayName, + @RequestParam(value = "shortDescription", required = false) String shortDescription, + @RequestParam(value = "overview", required = false) String overview, + @RequestParam(value = "resetPresentation", defaultValue = "false") boolean resetPresentation, @AuthenticationPrincipal PlatformPrincipal principal, HttpServletRequest request) throws IOException { SkillVisibility skillVisibility = SkillVisibility.valueOf(visibility.toUpperCase()); @@ -110,7 +113,8 @@ public class SkillPublishController extends BaseApiController { principal.userId(), skillVisibility, principal.platformRoles(), - confirmWarnings, avatarAction, avatarUploadId, expectedSlug, displayName, AuditRequestContext.from(request) + confirmWarnings, avatarAction, avatarUploadId, expectedSlug, displayName, + shortDescription, overview, resetPresentation, AuditRequestContext.from(request) ); PublishResponse response = new PublishResponse( diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillPackagePreviewResponse.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillPackagePreviewResponse.java index e0867b2b5..411d7dc1a 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillPackagePreviewResponse.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/dto/SkillPackagePreviewResponse.java @@ -4,10 +4,11 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.iflytek.skillhub.domain.skill.validation.SkillExamplesSummary.Status; /** Package defaults only: preview never creates a skill or changes its files. */ -public record SkillPackagePreviewResponse(String name, String displayName, String description, String version, +public record SkillPackagePreviewResponse(String name, String displayName, String description, String shortDescription, + String overview, String version, @JsonInclude(JsonInclude.Include.NON_NULL) Status examplesStatus, @JsonInclude(JsonInclude.Include.NON_NULL) Integer examplesCount) { public SkillPackagePreviewResponse(String name, String displayName, String description, String version) { - this(name, displayName, description, version, null, null); + this(name, displayName, description, description, null, version, null, null); } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPackagePreviewService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPackagePreviewService.java index 8ca0db72b..252c176bb 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPackagePreviewService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPackagePreviewService.java @@ -31,7 +31,8 @@ public class SkillPackagePreviewService { .orElseThrow(() -> new DomainBadRequestException("error.skill.publish.skillMd.notFound")); SkillMetadata metadata = parser.parse(new String(skillMd.content(), StandardCharsets.UTF_8)); SkillExamplesSummary examples = SkillExamplesSummary.recognize(entries); - return new SkillPackagePreviewResponse(metadata.name(), metadata.displayName(), metadata.description(), metadata.version(), + return new SkillPackagePreviewResponse(metadata.name(), metadata.displayName(), metadata.description(), + metadata.shortDescription(), metadata.overview(), metadata.version(), examples.status(), examples.count()); } } diff --git a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPublishAppService.java b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPublishAppService.java index ad49260fc..b1dd854eb 100644 --- a/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPublishAppService.java +++ b/server/skillhub-app/src/main/java/com/iflytek/skillhub/service/SkillPublishAppService.java @@ -42,13 +42,14 @@ public class SkillPublishAppService { SkillVisibility visibility, Set roles, boolean confirmWarnings, AvatarAction action, String uploadId, String expectedSlug, AuditRequestContext context) { return publish(namespace, entries, actor, visibility, roles, confirmWarnings, action, - uploadId, expectedSlug, null, context); + uploadId, expectedSlug, null, null, null, false, context); } @Transactional public SkillPublishService.PublishResult publish(String namespace, List entries, String actor, SkillVisibility visibility, Set roles, boolean confirmWarnings, AvatarAction action, - String uploadId, String expectedSlug, String displayName, AuditRequestContext context) { + String uploadId, String expectedSlug, String displayName, String shortDescription, + String overview, boolean resetPresentation, AuditRequestContext context) { if (action == AvatarAction.REPLACE && (uploadId == null || uploadId.isBlank()) || action != AvatarAction.REPLACE && uploadId != null && !uploadId.isBlank()) { throw new DomainBadRequestException("error.skill.avatar.invalidUpload"); @@ -64,9 +65,12 @@ public class SkillPublishAppService { uploads.validateReference(namespace, targetSlug, actor, uploadId); } } - SkillPublishService.PublishResult result = displayName == null || displayName.isBlank() + SkillPublishService.PublishResult result = (displayName == null || displayName.isBlank()) + && (shortDescription == null || shortDescription.isBlank()) + && (overview == null || overview.isBlank()) && !resetPresentation ? publications.publishFromEntries(namespace, entries, actor, visibility, roles, confirmWarnings) - : publications.publishFromEntries(namespace, entries, actor, visibility, roles, confirmWarnings, displayName); + : publications.publishFromEntries(namespace, entries, actor, visibility, roles, confirmWarnings, displayName, + shortDescription, overview, resetPresentation); var review = reviews.findBySkillVersionIdAndStatus( result.version().getId(), ReviewTaskStatus.PENDING).orElse(null); if (action == AvatarAction.REPLACE) { diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java index c18159ff2..1341dcdaa 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/review/ReviewService.java @@ -467,7 +467,7 @@ public class ReviewService { try { SkillMetadata metadata = objectMapper.readValue(metadataJson, SkillMetadata.class); skill.setDisplayName(metadata.displayName()); - skill.setSummary(metadata.description()); + skill.setSummary(metadata.shortDescription()); } catch (Exception e) { throw new IllegalStateException("Failed to deserialize skill metadata", e); } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java index 1c1a65e4a..c4a546e06 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadata.java @@ -9,10 +9,35 @@ public record SkillMetadata( String version, String body, Map frontmatter, - String displayNameOverride + String displayNameOverride, + String shortDescriptionOverride, + String overviewOverride, + String presentationInheritedFrom ) { public SkillMetadata(String name, String description, String version, String body, Map frontmatter) { - this(name, description, version, body, frontmatter, null); + this(name, description, version, body, frontmatter, null, null, null, null); + } + + public SkillMetadata(String name, String description, String version, String body, Map frontmatter, + String displayNameOverride) { + this(name, description, version, body, frontmatter, displayNameOverride, null, null, null); + } + + public SkillMetadata withPresentation(String shortDescription, String overview, String inheritedFrom) { + String shortValue = normalize(shortDescription); + String overviewValue = normalize(overview); + if (shortValue != null && shortValue.length() > 200) { + throw new DomainBadRequestException("error.skill.shortDescription.tooLong", 200); + } + if (overviewValue != null && overviewValue.length() > 20000) { + throw new DomainBadRequestException("error.skill.overview.tooLong", 20000); + } + return new SkillMetadata(name, description, version, body, frontmatter, displayNameOverride, + shortValue, overviewValue, inheritedFrom); + } + + private static String normalize(String value) { + return value == null || value.strip().isEmpty() ? null : value.strip(); } /** Explicit portal input is validated before it becomes part of the version snapshot. */ @@ -26,6 +51,24 @@ public record SkillMetadata( override == null || override.isEmpty() ? null : override); } + public String shortDescription() { + return shortDescriptionOverride != null ? shortDescriptionOverride + : nestedText(frontmatter, "short-description", description); + } + + public String overview() { + String packageOverview = nestedText(frontmatter, "overview", null); + return overviewOverride != null ? overviewOverride : (packageOverview != null ? packageOverview : body); + } + + private static String nestedText(Map values, String key, String fallback) { + if (values != null && values.get("metadata") instanceof Map metadata + && metadata.get(key) instanceof String text && !text.strip().isEmpty()) { + return text.strip(); + } + return fallback; + } + /** Human-readable label; unsupported legacy snapshot values fall back to the package name. */ public String displayName() { if (displayNameOverride != null && !displayNameOverride.isBlank() diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java index af7959dbe..5097d542f 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParser.java @@ -60,7 +60,15 @@ public class SkillMetadataParser { } validateDisplayName(frontmatter); - return new SkillMetadata(name, description, version, body, frontmatter); + String shortDescription = extractNestedOptionalField(frontmatter, "metadata", "short-description"); + String overview = extractNestedOptionalField(frontmatter, "metadata", "overview"); + if (shortDescription != null && shortDescription.length() > 200) { + throw new DomainBadRequestException("error.skill.metadata.shortDescription.tooLong", 200); + } + if (overview != null && overview.length() > 20000) { + throw new DomainBadRequestException("error.skill.metadata.overview.tooLong", 20000); + } + return new SkillMetadata(name, description, version, body, frontmatter, null, shortDescription, overview, null); } private void validateDisplayName(Map frontmatter) { diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java index 82536db4c..78e222c3d 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillGovernanceService.java @@ -388,7 +388,7 @@ public class SkillGovernanceService { try { SkillMetadata metadata = objectMapper.readValue(metadataJson, SkillMetadata.class); skill.setDisplayName(metadata.displayName()); - skill.setSummary(metadata.description()); + skill.setSummary(metadata.shortDescription()); } catch (Exception e) { throw new IllegalStateException("Failed to deserialize skill metadata", e); } diff --git a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java index d6043acf0..fe4a87db7 100644 --- a/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java +++ b/server/skillhub-domain/src/main/java/com/iflytek/skillhub/domain/skill/service/SkillPublishService.java @@ -336,8 +336,17 @@ public class SkillPublishService { String namespaceSlug, List entries, String publisherId, SkillVisibility visibility, Set platformRoles, boolean confirmWarnings, String displayName) { + return publishFromEntries(namespaceSlug, entries, publisherId, visibility, platformRoles, confirmWarnings, + displayName, null, null, false); + } + + @Transactional + public PublishResult publishFromEntries( + String namespaceSlug, List entries, String publisherId, + SkillVisibility visibility, Set platformRoles, boolean confirmWarnings, + String displayName, String shortDescription, String overview, boolean resetPresentation) { return publishFromEntriesInternal(namespaceSlug, entries, publisherId, visibility, platformRoles, - confirmWarnings, false, false, null, null, displayName); + confirmWarnings, false, false, null, null, displayName, shortDescription, overview, resetPresentation); } /** Publishes a validated GitHub package into the dedicated sync review queue. */ @@ -499,6 +508,17 @@ public class SkillPublishService { SkillVisibility visibility, Set platformRoles, boolean confirmWarnings, boolean forceAutoPublish, boolean bypassMembershipCheck, BundlePublicationTarget bundleTarget, GithubSyncPublicationTarget syncTarget, String displayName) { + return publishFromEntriesInternal(namespaceSlug, entries, publisherId, visibility, platformRoles, + confirmWarnings, forceAutoPublish, bypassMembershipCheck, bundleTarget, syncTarget, displayName, + null, null, false); + } + + private PublishResult publishFromEntriesInternal( + String namespaceSlug, List entries, String publisherId, + SkillVisibility visibility, Set platformRoles, boolean confirmWarnings, + boolean forceAutoPublish, boolean bypassMembershipCheck, + BundlePublicationTarget bundleTarget, GithubSyncPublicationTarget syncTarget, String displayName, + String shortDescription, String overview, boolean resetPresentation) { // 1. Find namespace by slug Namespace namespace = namespaceRepository.findBySlug(namespaceSlug) @@ -538,6 +558,8 @@ public class SkillPublishService { String skillMdContent = new String(skillMd.content()); SkillMetadata metadata = skillMetadataParser.parse(skillMdContent).withDisplayNameOverride(displayName); + metadata = metadata.withPresentation(resetPresentation ? null : shortDescription, + resetPresentation ? null : overview, null); String upstreamVersion = syncTarget == null ? null : metadata.version(); if (syncTarget != null) { // A reverted upstream package is a new review target after an intervening update. @@ -886,7 +908,7 @@ public class SkillPublishService { // A pending update must not replace the currently published title or summary. if (autoPublish || visibility == SkillVisibility.PRIVATE || skill.getLatestVersionId() == null) { skill.setDisplayName(metadata.displayName()); - skill.setSummary(metadata.description()); + skill.setSummary(metadata.shortDescription()); } if (syncTarget != null) { skill.assignGithubSyncOwnership(publisherId); @@ -1150,6 +1172,9 @@ public class SkillPublishService { parsedMetadata.put("body", metadata.body()); parsedMetadata.put("frontmatter", metadata.frontmatter()); parsedMetadata.put("displayNameOverride", metadata.displayNameOverride()); + parsedMetadata.put("shortDescriptionOverride", metadata.shortDescriptionOverride()); + parsedMetadata.put("overviewOverride", metadata.overviewOverride()); + parsedMetadata.put("presentationInheritedFrom", metadata.presentationInheritedFrom()); if (syncTarget != null) { parsedMetadata.put("upstreamVersion", upstreamVersion); parsedMetadata.put("githubContentSha256", syncTarget.contentSha256()); diff --git a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java index 5b03de325..bb4954bec 100644 --- a/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java +++ b/server/skillhub-domain/src/test/java/com/iflytek/skillhub/domain/skill/metadata/SkillMetadataParserTest.java @@ -11,6 +11,22 @@ class SkillMetadataParserTest { private final SkillMetadataParser parser = new SkillMetadataParser(); + @Test + void presentationOverridesAndPackageDefaultsRemainDistinct() { + SkillMetadata source = parser.parse("---\nname: my-skill\ndescription: Original\nmetadata:\n short-description: Package short\n overview: Package overview\n---\nBody"); + assertEquals("Package short", source.shortDescription()); + assertEquals("Package overview", source.overview()); + SkillMetadata custom = source.withPresentation(" Web short ", " # Web overview ", null); + assertEquals("Web short", custom.shortDescription()); + assertEquals("# Web overview", custom.overview()); + assertEquals(source.frontmatter(), custom.frontmatter()); + assertThrows(DomainBadRequestException.class, + () -> source.withPresentation("x".repeat(201), null, null)); + assertThrows(DomainBadRequestException.class, + () -> source.withPresentation(null, "x".repeat(20001), null)); + } + + @Test void platformOverrideIsVersionedWithoutChangingFrontmatter() throws Exception { SkillMetadata source = parser.parse("---\nname: my-skill\ndescription: Test\nmetadata:\n display-name: 包内标题\n---\nBody"); diff --git a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java index 20d1ecf9c..509b8a33c 100644 --- a/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java +++ b/server/skillhub-search/src/main/java/com/iflytek/skillhub/search/postgres/PostgresSearchRebuildService.java @@ -129,6 +129,7 @@ public class PostgresSearchRebuildService implements SearchRebuildService { resolveLatestVersion(skill) .map(this::extractParsedMetadata) .ifPresent(metadata -> { + addPart(searchParts, String.valueOf(metadata.get("description"))); appendFrontmatter(asMap(metadata.get("frontmatter")), keywords, searchParts); appendComplianceSnapshot( asMap(metadata.get(ComplianceMetadataService.SNAPSHOT_FIELD_NAME)), diff --git a/web/src/i18n/locales/en.json b/web/src/i18n/locales/en.json index 550f8db9f..a0c8eace9 100644 --- a/web/src/i18n/locales/en.json +++ b/web/src/i18n/locales/en.json @@ -2235,6 +2235,8 @@ "displayName": "Display name", "displayNamePlaceholder": "For example: Meeting notes assistant", "displayNameHelp": "Up to 200 characters. Defaults to the package title. Changes follow this version’s review and do not alter the identifier or package files. Leave blank to use the package title.", + "shortDescription": "Short description", + "overview": "Detailed overview (Markdown)", "readingPackage": "Reading package metadata…", "examplesReady": "Recognized {{count}} task examples across all languages.", "examplesNone": "No task examples provided. Publishing and use remain available.", diff --git a/web/src/i18n/locales/zh.json b/web/src/i18n/locales/zh.json index cfda81fe1..03dd9bc7a 100644 --- a/web/src/i18n/locales/zh.json +++ b/web/src/i18n/locales/zh.json @@ -2234,6 +2234,8 @@ "displayName": "显示名称", "displayNamePlaceholder": "例如:会议纪要助手", "displayNameHelp": "支持中文,最多 200 个字符。默认读取包内名称;修改后随本次版本审核,不改变技能标识或包内文件。留空则使用包内名称。", + "shortDescription": "简短描述", + "overview": "详细概览(Markdown)", "readingPackage": "正在读取技能包信息…", "examplesReady": "已识别 {{count}} 条使用案例(包含全部语种)。", "examplesNone": "未提供使用案例,可正常发布和使用。", diff --git a/web/src/pages/dashboard/publish.tsx b/web/src/pages/dashboard/publish.tsx index 98686f228..95ce76224 100644 --- a/web/src/pages/dashboard/publish.tsx +++ b/web/src/pages/dashboard/publish.tsx @@ -22,6 +22,7 @@ import { normalizeSelectValue, } from '@/shared/ui/select' import { Input } from '@/shared/ui/input' +import { Textarea } from '@/shared/ui/textarea' import { Label } from '@/shared/ui/label' import { Card } from '@/shared/ui/card' import { SkillAvatarEditor, KEEP_AVATAR, type AvatarEdit } from '@/features/skill/skill-avatar-editor' @@ -52,7 +53,11 @@ export function PublishPage() { const previewId = useId() const [fileRevision, setFileRevision] = useState(0) const [displayName, setDisplayName] = useState('') + const [shortDescription, setShortDescription] = useState('') + const [overview, setOverview] = useState('') const displayNameEdited = useRef(false) + const shortDescriptionEdited = useRef(false) + const overviewEdited = useRef(false) const [selectedFile, setSelectedFile] = useState(null) const [namespaceSlug, setNamespaceSlug] = useState(prefill.namespace) const [visibility, setVisibility] = useState(prefill.visibility) @@ -75,6 +80,12 @@ export function PublishPage() { if (packagePreview.data && !displayNameEdited.current) { setDisplayName(packagePreview.data.displayName ?? packagePreview.data.name ?? '') } + if (packagePreview.data && !shortDescriptionEdited.current) { + setShortDescription(packagePreview.data.shortDescription ?? packagePreview.data.description ?? '') + } + if (packagePreview.data && !overviewEdited.current) { + setOverview(packagePreview.data.overview ?? '') + } }, [packagePreview.data]) const avatarTarget = useQuery({ queryKey: ['skill-avatar', namespaceSlug, updateSlug], queryFn: () => skillAvatarApi.status(namespaceSlug, updateSlug), enabled: !!namespaceSlug && !!updateSlug }) const avatarUnavailable = !!updateSlug && (avatarTarget.isLoading || avatarTarget.isError) @@ -148,6 +159,9 @@ export function PublishPage() { avatarUploadId: avatarEdit.uploadId, expectedSlug: updateSlug || undefined, displayName, + shortDescription, + overview, + resetPresentation: false, }) setPrecheckWarnings([]) setWarningDialogOpen(false) @@ -348,6 +362,17 @@ export function PublishPage() { {selectedFile && packagePreview.data?.examplesStatus === 'invalid' &&

{t('publish.examplesInvalid')}

} +
+ + { shortDescriptionEdited.current = true; setShortDescription(event.target.value) }} /> +
+
+ +