Skip to content

[ISSUE #15345] Return skill front matter in list response - #15508

Open
jay666mnj wants to merge 4 commits into
alibaba:developfrom
jay666mnj:fix-skill-list-frontmatter
Open

jay666mnj wants to merge 4 commits into
alibaba:developfrom
jay666mnj:fix-skill-list-frontmatter

Conversation

@jay666mnj

Copy link
Copy Markdown
Contributor

What is the purpose of the change

Fixes #15345

Support returning front matter parsed from SKILL.md in the skill list response, so the console can display metadata such as alias and other custom fields.

Brief changelog

  • Add frontMatter to SkillSummary.
  • Load and parse SKILL.md front matter for the display version in skill list API.
  • Add unit test coverage for list response front matter.

Verifying this change

  • git diff --check
  • mvn -pl ai -am "-Dtest=SkillOperationServiceImplTest#testListSkillsSuccessfully" "-Dsurefire.failIfNoSpecifiedTests=false" test

Result: Tests run: 1, Failures: 0, Errors: 0, Skipped: 0.

@github-actions

Copy link
Copy Markdown

Thanks for your this PR. 🙏
Please check again for your PR changes whether contains any usage/api/configuration change such as Add new API , Add new configuration, Change default value of configuration.
If so, please add or update documents(markdown type) in docs/next/ for repository nacos-group/nacos-group.github.io


感谢您提交的PR。 🙏
请再次查看您的PR内容,确认是否包含任何使用方式/API/配置参数的变更,如:新增API新增配置参数修改默认配置等操作。
如果是,请确保在提交之前,在仓库nacos-group/nacos-group.github.io中的docs/next/目录下添加或更新文档(markdown格式)。

@jay666mnj

jay666mnj commented Jul 14, 2026 via email

Copy link
Copy Markdown
Contributor Author

@KomachiSion KomachiSion left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think current implementation is good design.

If we query for a big page, the original list query only query the resource table and handle it.

But after the change, each skill will query two more: one version and query all skill data to get the frontmatter. which cost so many and get a small frontmatter.

I suggest to redesign for this before you do coding.

@jay666mnj
jay666mnj force-pushed the fix-skill-list-frontmatter branch from be14f5a to 94ae27a Compare July 15, 2026 14:43
@jay666mnj

jay666mnj commented Jul 15, 2026 via email

Copy link
Copy Markdown
Contributor Author

@jay666mnj
jay666mnj requested a review from KomachiSion July 15, 2026 15:15
@KomachiSion

Copy link
Copy Markdown
Collaborator

Thanks for redesigning the implementation. Reading front matter directly from ai_resource.ext avoids the N+1 version/storage queries on the list path, which is the right performance direction.

However, the cached data lifecycle is not complete yet:

  1. ext.frontMatter is overwritten when a draft is created or updated, while the display-version logic previously preferred latest. If v1 is online and v2 is being edited, the list may expose v2's unpublished front matter. Deleting the draft also does not restore the cache, so stale metadata may remain.

  2. How will existing skills be updated with this design? Their ai_resource.ext does not contain frontMatter, so the list API may continue returning frontMatter = null for them. Please clarify the migration or backfill strategy, and avoid lazy per-item database/storage queries in the list path.

  3. bumpMetaDescriptionAndExt ignores the final CAS result. If retries are exhausted, the content update succeeds but the list cache remains stale. Since the list no longer has a read-through fallback, this cache update should be reliable or repairable.

I suggest storing the corresponding frontMatterVersion in ext and updating the cached display metadata at lifecycle transitions such as publish, draft deletion, redraft, and bootstrap. The list path should continue to use only the single paginated ai_resource query.

Please also add lifecycle and large-page tests that verify:

  • latest plus editing draft returns the intended version's front matter;
  • deleting/publishing a draft updates the cache;
  • legacy and bootstrap skills are covered;
  • listing multiple skills performs no version-table or storage queries.

Additionally, the current PR fails spotless:check in SkillSummary.java, and the Admin/Console OpenAPI IT scenarios and Skill spec should be updated for the new response contract.

@jay666mnj

Copy link
Copy Markdown
Contributor Author

Thanks for the detailed review. I have updated the implementation according to your suggestions.

The list API still reads frontMatter only from ai_resource.ext and does not perform per-item version-table or storage queries. I added frontMatterVersion into ext and refreshed the cached display metadata during lifecycle transitions, including publish, force publish, draft deletion, redraft, and bootstrap.

Draft create/update with an existing latest version no longer exposes unpublished front matter. For legacy skills without cached frontMatter, the list API may return frontMatter = null until a later lifecycle transition or bootstrap repair, and it will not do lazy per-item backfill in the list path.

I also changed bumpMetaDescriptionAndExt to fail explicitly when CAS retries are exhausted, added lifecycle/large-page/legacy/bootstrap tests, updated the skill spec and Admin/Console OpenAPI IT scenarios, and fixed the SkillSummary spotless issue.

@KomachiSion

Copy link
Copy Markdown
Collaborator

Thanks for the update. The list hot path now avoids per-item version/storage queries, and the display-version lifecycle is improved. However, there are still several blockers:

  1. Version-level online/offline transitions are missing from the cache lifecycle. toggleVersionOnlineStatus may change or remove the latest label, but changeOnlineStatus does not refresh the cached front matter. Taking the current latest version offline, or bringing a newer version online, can therefore leave the list response with stale metadata.

  2. The cache update is not race-safe. refreshDisplayMetadataCache resolves the display version from one meta snapshot, then fetches the meta again before updating it. If the display version changes between those reads, the new versionInfo may be combined with front matter from the old version. The CAS conflict callback also refreshes versionInfo without recomputing ext. Although frontMatterVersion is stored, it is never validated or read.

Please restart the whole display-version calculation on CAS conflict, or update the lifecycle state and corresponding cache atomically. The list path can also compare frontMatterVersion with the resolved display version and return null on mismatch without introducing extra queries.

  1. Existing skills are still not actually backfilled. SkillDataBootstrapInitializer.buildBootstrapPlan only imports missing packages and skips existing built-in skills before bootstrapSkillFromZip is called. Non-built-in legacy skills also have no migration path. The new legacy test only confirms that they continue returning null. Please provide a bounded asynchronous/offline repair strategy rather than lazy list-time queries.

  2. Draft updates now introduce repeated version/storage work. When latest=v1 and editing=v2, every draft update calls refreshDisplayMetadataCache, queries v1, loads all files from storage, and performs another meta CAS update even when frontMatterVersion is already v1. Please use frontMatterVersion to short-circuit when the cache is current, and read only SKILL.md when a rebuild is actually required.

I also ran the complete related unit tests:

./mvnw -pl api,ai -am -Dtest=SkillSummaryTest,SkillOperationServiceImplTest -Dsurefire.failIfNoSpecifiedTests=false -DskipITs test

SkillSummaryTest passed, but SkillOperationServiceImplTest had 2 failures and 4 errors out of 79 tests. spotless:check passes.

Please also update the Admin/Console API scenario matrices and coverage registry for the changed response contract, and clean up the unrelated whitespace churn and corrupted 鈫? Javadoc characters.

So I prefer to discuss full solution before you do PR. We can discuss in issue first.

Can you submit one design plan spec into issue first, we disscuss it first.

@nacos-community

Copy link
Copy Markdown
Collaborator

⚠️ Merge conflict detected

This PR has conflicts with the develop branch and cannot be merged in its current state. Please rebase or merge develop into your branch and resolve the conflicts:

git fetch origin
git checkout fix-skill-list-frontmatter
git rebase origin/develop
# resolve conflicts, then:
git push --force-with-lease

This is a one-time reminder. Feel free to @mention me for a re-review after conflicts are resolved.


Automated notification by github-manager-bot

@nacos-community

Copy link
Copy Markdown
Collaborator

Status check on this PR:

  • The last commit is from 2026-07-16. The maintainer review on 2026-07-17 raised several unresolved blockers (version online/offline transitions missing from the cache lifecycle, race safety of the cache update, backfill for existing skills, and repeated version/storage work on draft updates), and asked for a design plan to be submitted to 针对技能查询列表支持返回SKILL.md中frontmatter #15345 for discussion before further code changes.
  • There has been no response since then. To move this forward, please either submit the requested design plan spec in 针对技能查询列表支持返回SKILL.md中frontmatter #15345 or reply to the maintainer's comments.
  • The PR also still has a merge conflict with develop (see the earlier notification); it will need a rebase once work resumes.

Automated notification by github-manager-bot

@nacos-community nacos-community left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Summary

Adds frontMatter to the skill list response by parsing SKILL.md front matter at write time (bootstrap / draft create-update / publish / redraft / delete-draft) and caching it into ai_resource.ext, so the list path performs no per-row version or storage queries. This addresses the N+1 concern raised in the earlier review round — the new unit tests explicitly verify findVersion / storage.get are never called during list pagination. Specs (specs/en|zh-cn/ai/skill-spec.md) and openapi-test cases are updated accordingly, and SkillSummary (api module) remains Java 8 compatible. Overall the redesigned implementation looks solid; a few non-blocking suggestions below.

Findings

  • [Warning] ai/src/main/java/com/alibaba/nacos/ai/service/skills/SkillOperationServiceImpl.java:1911 — no size cap on cached front matter; long YAML scalars would bloat ext and every list response row
  • [Warning] ai/src/main/java/com/alibaba/nacos/ai/service/skills/SkillOperationServiceImpl.java:1888refreshDisplayMetadataCache failures after an already-persisted lifecycle transition can surface as 5xx and mask a successful publish
  • [Info] test/openapi-test/src/test/java/com/alibaba/nacos/test/adminapi/ai/skill/SkillAdminApiOpenApiITCase.java:246frontMatter assertion can NPE instead of failing with a readable message
  • [Info] test/openapi-test/src/test/java/com/alibaba/nacos/test/consoleapi/ai/skill/SkillConsoleApiOpenApiITCase.java:246 — same NPE risk as the admin-API counterpart
  • [Info] ai/src/main/java/com/alibaba/nacos/ai/service/skills/SkillOperationServiceImpl.java:1925parseFrontMatterFromExt silently stringifies non-string scalars exposed via public Map<String, String>
  • [Info] ai/src/main/java/com/alibaba/nacos/ai/service/resource/AiResourceManager.java:309 — mojibake (鈫?) corrupted the arrows in Javadoc (also near line 1254)

Suggestions

  1. Cap the cached front matter (e.g. ~500 chars per scalar value and/or ~4 KB total serialized), or keep only an allow-list of display-relevant keys (name, description, alias, version) and drop the rest.
  2. Wrap refreshDisplayMetadataCache in try/catch + WARN log so a cache-refresh failure does not mask an already-successful lifecycle operation; the cache self-repairs on the next lifecycle transition, which is the contract already documented for legacy rows.
  3. In the openapi ITs, add assertNotNull(found.get("frontMatter"), found.toString()) before dereferencing, matching the existing labels pattern one line above.
  4. Restore the corrupted characters in AiResourceManager.java Javadoc (encoding artifact unrelated to this feature) before merge.

Automated review by github-manager-bot


private static Map<String, String> parseFrontMatterFromExt(String extJson) {
Map<String, Object> ext = parseExt(extJson);
Object frontMatter = ext.get(EXT_FRONT_MATTER_KEY);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

buildSkillMetaExt stores the entire parsed front matter map into ai_resource.ext with no per-value length cap and no total-size cap. Since ext is echoed verbatim in every list response row and is also read back by every lifecycle transition, a SKILL.md whose YAML scalars are very long (e.g. an inline multi-KB description, a giant metadata.* block, or an accidentally embedded blob) would bloat every paginated list row and the meta row itself. The upload zip is capped at ~50 MB uncompressed, but that is far too generous for metadata echoed in list responses. Suggest: reject or truncate individual front-matter scalar values above a small limit (e.g. 500 chars) and/or cap the serialised frontMatter payload (e.g. 4 KB); alternatively, store only an allow-list of display-relevant keys (name, description, alias, version) and drop the rest.

if (meta == null) {
return false;
}
ResourceVersionInfo info = AiResourceManager.parseVersionInfo(meta.getVersionInfo());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Every call site of refreshDisplayMetadataCache in this diff runs the refresh AFTER the primary lifecycle transition (doPublish, doForcePublish, doRedraft, doDeleteDraft, publishApprovedBySystem, overwriteEditingDraft, bootstrapSkillFromZip's early-return branch) has already mutated persisted state. Inside the refresh, loadSkillFromStorage reads storage bytes for the display version; if that read throws (storage plugin error, transient IO, concurrent repair job, or a corrupt storage row), the exception currently propagates out of the lifecycle method. Concretely in publish(...) the publish itself has already succeeded — the version row is online and the manifest may not have been updated yet — but the caller receives a 5xx. This masks a successful state change as a failure and leaves the ext cache stale. Suggest: wrap the refresh in a try/catch that logs at WARN and lets the primary operation's success propagate; the cache will be repaired on the next lifecycle transition (or by a future bootstrap repair), which is the same contract already documented in the spec for legacy rows.

assertFalse(found.isMissingNode(), page.toString());
assertEquals(skillName, found.get("name").asText(), found.toString());
assertNotNull(found.get("labels"), found.toString());
assertEquals(skillName, found.get("frontMatter").get("name").asText(), found.toString());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

found.get("frontMatter").get("name").asText() does not null-check frontMatter. Per the updated skill-spec, list responses MAY include frontMatter; legacy rows (or rows whose front matter failed to parse) legitimately return frontMatter = null. If a regression ever causes the cache to be missing for the test skill, this assertion NPEs with a generic stack trace instead of a clear assertion message, hiding the root cause. Suggest: assertNotNull(found.get("frontMatter"), found.toString()); before the assertEquals, matching the existing pattern used for labels one line above.

assertFalse(found.isMissingNode(), page.toString());
assertEquals(skillName, found.get("name").asText(), found.toString());
assertNotNull(found.get("labels"), found.toString());
assertEquals(skillName, found.get("frontMatter").get("name").asText(), found.toString());

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as the admin-API counterpart: found.get("frontMatter").get("name").asText() will NPE if the cache is missing instead of producing a readable assertion failure. Add an explicit assertNotNull on frontMatter to keep the test robust against legitimate frontMatter = null rows.

}

@SuppressWarnings("unchecked")
private static Map<String, Object> parseExt(String extJson) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parseFrontMatterFromExt silently coerces every cached value to String.valueOf(value), including non-string YAML scalars (numbers, booleans) and — more importantly — values that were already non-string in the stored ext JSON (Jackson deserialises unquoted numbers as Integer/Long, true/false as Boolean). The display layer will see "true" or "42" rather than typed values, and any future caller that relies on typed front-matter values will get surprises. Since SkillSummary.frontMatter is declared Map<String, String> in the public api module, please either (a) document this stringification in the Javadoc on SkillSummary.getFrontMatter() so downstream consumers know values are always stringified, or (b) change the API model to Map<String, Object> before it ships (breaking change later).

});
}

/**

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The Javadoc comment on doCasLoop was corrupted by this diff: the arrow (newValue, latestMeta) → refresh non-target fields became (newValue, latestMeta) 鈫?refresh non-target fields (mojibake). The same corruption appears at line 1254. This is unrelated to the feature and appears to be an encoding artefact from the diff/edit tooling, but it will ship in source and rendered Javadoc. Please restore the original character (or rewrite as ->) before merge.

@Zhengcy05

Copy link
Copy Markdown
Contributor

@jay666mnj @KomachiSion
Thanks for the clarification. As discussed, I’d like to take over this work and follow up on #15508 with a smaller scope.

The current PR already reads frontMatter from ai_resource.ext, avoiding per-item version and storage queries during listing. I propose keeping that approach with these boundaries:

  • Populate frontmatter metadata when Skill content is created or updated. Existing content without this metadata may return frontMatter: null; historical migration, bootstrap repair, and list-time backfill are out of scope.
  • Preserve the previously discussed display-version semantics: prefer latest, falling back to the editing or reviewing version when appropriate. Draft edits should not replace an online version’s displayed frontmatter.
  • Keep the cached frontmatter and its version consistent through relevant lifecycle changes, including publish, draft deletion, redraft, and version online/offline transitions. Missing or mismatched metadata should return null rather than another version’s content.
  • Recalculate against the current state on CAS conflicts, reuse valid cached metadata, and avoid loading the entire Skill package just to obtain frontmatter.
  • Update the Skill specs, Admin/Console API scenario matrices and coverage registry, with regression tests for lifecycle consistency, concurrent updates, and listing without per-item storage reads.

Frontmatter search will remain outside this change. I plan to submit a follow-up PR from a fresh branch based on current develop, referencing the original contribution.

Does this scope match your intended simplification? 😄

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

针对技能查询列表支持返回SKILL.md中frontmatter

4 participants