io.agentscope
diff --git a/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/pom.xml b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/pom.xml
new file mode 100644
index 000000000..92cb72aa1
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/pom.xml
@@ -0,0 +1,47 @@
+
+
+
+
+ 4.0.0
+
+ io.agentscope
+ agentscope-extensions-nacos
+ ${revision}
+ ../pom.xml
+
+
+ agentscope-extensions-nacos-skill
+ AgentScope Java - Nacos Skill Repository
+ agentscope-extensions-nacos-skill
+
+
+
+
+ io.agentscope
+ agentscope-core
+
+
+
+ com.alibaba.nacos
+ nacos-client
+
+
+
+
+
diff --git a/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/main/java/io/agentscope/core/nacos/skill/NacosSkillRepository.java b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/main/java/io/agentscope/core/nacos/skill/NacosSkillRepository.java
new file mode 100644
index 000000000..468894619
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/main/java/io/agentscope/core/nacos/skill/NacosSkillRepository.java
@@ -0,0 +1,155 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.nacos.skill;
+
+import com.alibaba.nacos.api.ai.AiService;
+import com.alibaba.nacos.api.ai.model.skills.Skill;
+import com.alibaba.nacos.api.exception.NacosException;
+import com.alibaba.nacos.common.utils.StringUtils;
+import io.agentscope.core.skill.AgentSkill;
+import io.agentscope.core.skill.repository.AgentSkillRepository;
+import io.agentscope.core.skill.repository.AgentSkillRepositoryInfo;
+import java.util.Collections;
+import java.util.List;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Nacos-based implementation of {@link AgentSkillRepository}.
+ *
+ * Reads skills from Nacos Config via {@code AiService.loadSkill(String)}. This implementation
+ * supports read operations: {@link #getSkill(String)}, {@link #skillExists(String)}, {@link
+ * #getRepositoryInfo()}, {@link #getSource()}, and {@link #isWriteable()}. List and write
+ * operations ({@link #getAllSkillNames()}, {@link #getAllSkills()}, {@link #save(List, boolean)},
+ * {@link #delete(String)})
+ * are implemented as read-only no-ops: they return empty list or {@code false} and log a warning.
+ */
+public class NacosSkillRepository implements AgentSkillRepository {
+
+ private static final Logger log = LoggerFactory.getLogger(NacosSkillRepository.class);
+
+ private static final String REPO_TYPE = "nacos";
+ private static final String LOCATION_PREFIX = "namespace:";
+
+ private final AiService aiService;
+ private final String namespaceId;
+ private final String source;
+ private final String location;
+
+ /**
+ * Creates a Nacos skill repository.
+ *
+ * @param aiService the Nacos AI service (must not be null)
+ * @param namespaceId the Nacos namespace ID (null or blank is treated as default namespace)
+ */
+ public NacosSkillRepository(AiService aiService, String namespaceId) {
+ if (aiService == null) {
+ throw new IllegalArgumentException("AiService cannot be null");
+ }
+ this.aiService = aiService;
+ this.namespaceId = StringUtils.isBlank(namespaceId) ? "public" : namespaceId.trim();
+ this.source = REPO_TYPE + ":" + this.namespaceId;
+ this.location = LOCATION_PREFIX + this.namespaceId;
+ log.info("NacosSkillRepository initialized for namespace: {}", this.namespaceId);
+ }
+
+ @Override
+ public AgentSkill getSkill(String name) {
+ if (name == null || name.isBlank()) {
+ throw new IllegalArgumentException("Skill name cannot be null or empty");
+ }
+ try {
+ Skill nacosSkill = aiService.loadSkill(name.trim());
+ if (nacosSkill == null) {
+ throw new IllegalArgumentException("Skill not found: " + name);
+ }
+ return NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, getSource());
+ } catch (NacosException e) {
+ if (e.getErrCode() == NacosException.NOT_FOUND) {
+ throw new IllegalArgumentException("Skill not found: " + name, e);
+ }
+ throw new RuntimeException("Failed to load skill from Nacos: " + name, e);
+ }
+ }
+
+ @Override
+ public boolean skillExists(String skillName) {
+ if (skillName == null || skillName.isBlank()) {
+ return false;
+ }
+ try {
+ Skill skill = aiService.loadSkill(skillName.trim());
+ return skill != null;
+ } catch (NacosException e) {
+ if (e.getErrCode() == NacosException.NOT_FOUND) {
+ return false;
+ }
+ log.warn("Error checking skill existence for {}: {}", skillName, e.getMessage());
+ return false;
+ }
+ }
+
+ @Override
+ public AgentSkillRepositoryInfo getRepositoryInfo() {
+ return new AgentSkillRepositoryInfo(REPO_TYPE, location, false);
+ }
+
+ @Override
+ public String getSource() {
+ return source;
+ }
+
+ @Override
+ public void setWriteable(boolean writeable) {
+ log.warn("NacosSkillRepository is read-only, set writeable operation ignored");
+ }
+
+ @Override
+ public boolean isWriteable() {
+ return false;
+ }
+
+ // ---------- Read-only no-op operations (list and write) ----------
+
+ @Override
+ public List getAllSkillNames() {
+ log.warn("NacosSkillRepository is read-only, getAllSkillNames returns empty list");
+ return Collections.emptyList();
+ }
+
+ @Override
+ public List getAllSkills() {
+ log.warn("NacosSkillRepository is read-only, getAllSkills returns empty list");
+ return Collections.emptyList();
+ }
+
+ @Override
+ public boolean save(List skills, boolean force) {
+ log.warn("NacosSkillRepository is read-only, save operation ignored");
+ return false;
+ }
+
+ @Override
+ public boolean delete(String skillName) {
+ log.warn("NacosSkillRepository is read-only, delete operation ignored");
+ return false;
+ }
+
+ @Override
+ public void close() {
+ // AiService lifecycle is managed by the caller; nothing to release here
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/main/java/io/agentscope/core/nacos/skill/NacosSkillToAgentSkillConverter.java b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/main/java/io/agentscope/core/nacos/skill/NacosSkillToAgentSkillConverter.java
new file mode 100644
index 000000000..bf3c1dd56
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/main/java/io/agentscope/core/nacos/skill/NacosSkillToAgentSkillConverter.java
@@ -0,0 +1,69 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package io.agentscope.core.nacos.skill;
+
+import com.alibaba.nacos.api.ai.model.skills.Skill;
+import com.alibaba.nacos.api.ai.model.skills.SkillResource;
+import io.agentscope.core.skill.AgentSkill;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Converts Nacos AI {@link Skill} to AgentScope {@link AgentSkill}.
+ */
+public final class NacosSkillToAgentSkillConverter {
+
+ private static final String NO_DESCRIPTION = "(no description)";
+ private static final String NO_INSTRUCTION = "(no instruction)";
+
+ private NacosSkillToAgentSkillConverter() {}
+
+ /**
+ * Converts a Nacos Skill to an AgentSkill.
+ *
+ * @param nacosSkill the Nacos Skill (must not be null)
+ * @param source the source identifier for the resulting AgentSkill (e.g. "nacos:public")
+ * @return the converted AgentSkill
+ */
+ public static AgentSkill toAgentSkill(Skill nacosSkill, String source) {
+ if (nacosSkill == null) {
+ throw new IllegalArgumentException("Nacos Skill cannot be null");
+ }
+ String name = blankToDefault(nacosSkill.getName(), "unknown");
+ String description = blankToDefault(nacosSkill.getDescription(), NO_DESCRIPTION);
+ String skillContent = blankToDefault(nacosSkill.getInstruction(), NO_INSTRUCTION);
+ Map resources = toResourceMap(nacosSkill.getResource());
+ return new AgentSkill(name, description, skillContent, resources, source);
+ }
+
+ private static String blankToDefault(String value, String defaultValue) {
+ return (value != null && !value.isBlank()) ? value.trim() : defaultValue;
+ }
+
+ private static Map toResourceMap(Map resourceMap) {
+ if (resourceMap == null || resourceMap.isEmpty()) {
+ return new HashMap<>();
+ }
+ Map result = new HashMap<>(resourceMap.size());
+ for (Map.Entry e : resourceMap.entrySet()) {
+ String key = e.getKey() != null ? e.getKey() : "resource";
+ SkillResource res = e.getValue();
+ String content = (res != null && res.getContent() != null) ? res.getContent() : "";
+ result.put(key, content);
+ }
+ return result;
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/test/java/io/agentscope/core/nacos/skill/NacosSkillRepositoryTest.java b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/test/java/io/agentscope/core/nacos/skill/NacosSkillRepositoryTest.java
new file mode 100644
index 000000000..e0cf8923c
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/test/java/io/agentscope/core/nacos/skill/NacosSkillRepositoryTest.java
@@ -0,0 +1,255 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agentscope.core.nacos.skill;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import com.alibaba.nacos.api.ai.AiService;
+import com.alibaba.nacos.api.ai.model.skills.Skill;
+import com.alibaba.nacos.api.exception.NacosException;
+import io.agentscope.core.skill.AgentSkill;
+import io.agentscope.core.skill.repository.AgentSkillRepositoryInfo;
+import java.util.List;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Unit tests for {@link NacosSkillRepository}.
+ */
+@ExtendWith(MockitoExtension.class)
+class NacosSkillRepositoryTest {
+
+ @Mock private AiService aiService;
+
+ private NacosSkillRepository repository;
+
+ @BeforeEach
+ void setUp() {
+ repository = new NacosSkillRepository(aiService, "public");
+ }
+
+ @Test
+ @DisplayName("Should throw when AiService is null")
+ void testConstructorWithNullAiService() {
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> new NacosSkillRepository(null, "public"));
+ assertEquals("AiService cannot be null", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw when getSkill is called with null name")
+ void testGetSkillWithNullName() {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> repository.getSkill(null));
+ assertEquals("Skill name cannot be null or empty", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw when getSkill is called with blank name")
+ void testGetSkillWithBlankName() {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> repository.getSkill(" "));
+ assertEquals("Skill name cannot be null or empty", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw when getSkill is called with empty name")
+ void testGetSkillWithEmptyName() {
+ IllegalArgumentException e =
+ assertThrows(IllegalArgumentException.class, () -> repository.getSkill(""));
+ assertEquals("Skill name cannot be null or empty", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw when skill not found (loadSkill returns null)")
+ void testGetSkillWhenLoadSkillReturnsNull() throws NacosException {
+ when(aiService.loadSkill("missing-skill")).thenReturn(null);
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class, () -> repository.getSkill("missing-skill"));
+ assertEquals("Skill not found: missing-skill", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should throw IllegalArgumentException when NacosException is NOT_FOUND")
+ void testGetSkillWhenNacosExceptionNotFound() throws NacosException {
+ NacosException nacosEx = new NacosException(NacosException.NOT_FOUND, "not found");
+ when(aiService.loadSkill("missing-skill")).thenThrow(nacosEx);
+
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class, () -> repository.getSkill("missing-skill"));
+ assertEquals("Skill not found: missing-skill", e.getMessage());
+ assertEquals(nacosEx, e.getCause());
+ }
+
+ @Test
+ @DisplayName("Should throw RuntimeException for other NacosException")
+ void testGetSkillWhenOtherNacosException() throws NacosException {
+ NacosException nacosEx = new NacosException(500, "server error");
+ when(aiService.loadSkill("my-skill")).thenThrow(nacosEx);
+
+ RuntimeException e =
+ assertThrows(RuntimeException.class, () -> repository.getSkill("my-skill"));
+ assertEquals("Failed to load skill from Nacos: my-skill", e.getMessage());
+ assertEquals(nacosEx, e.getCause());
+ }
+
+ @Test
+ @DisplayName("Should return AgentSkill when skill is found")
+ void testGetSkillSuccess() throws NacosException {
+ Skill nacosSkill = mockNacosSkill("test-skill", "A test skill", "Do something");
+ when(aiService.loadSkill("test-skill")).thenReturn(nacosSkill);
+
+ AgentSkill result = repository.getSkill("test-skill");
+
+ assertNotNull(result);
+ assertEquals("test-skill", result.getName());
+ assertEquals("A test skill", result.getDescription());
+ assertEquals("Do something", result.getSkillContent());
+ assertEquals("nacos:public", result.getSource());
+ }
+
+ @Test
+ @DisplayName("Should trim skill name when calling loadSkill")
+ void testGetSkillTrimsName() throws NacosException {
+ Skill nacosSkill = mockNacosSkill("trimmed", "Desc", "Content");
+ when(aiService.loadSkill("trimmed")).thenReturn(nacosSkill);
+
+ repository.getSkill(" trimmed ");
+
+ verify(aiService).loadSkill("trimmed");
+ }
+
+ @Test
+ @DisplayName("Should return false for skillExists with null")
+ void testSkillExistsWithNull() {
+ assertFalse(repository.skillExists(null));
+ }
+
+ @Test
+ @DisplayName("Should return false for skillExists with blank")
+ void testSkillExistsWithBlank() {
+ assertFalse(repository.skillExists(" "));
+ }
+
+ @Test
+ @DisplayName("Should return true when skill exists")
+ void testSkillExistsWhenFound() throws NacosException {
+ when(aiService.loadSkill("exists")).thenReturn(mock(Skill.class));
+
+ assertTrue(repository.skillExists("exists"));
+ }
+
+ @Test
+ @DisplayName("Should return false when skill not found")
+ void testSkillExistsWhenNotFound() throws NacosException {
+ when(aiService.loadSkill("missing")).thenReturn(null);
+
+ assertFalse(repository.skillExists("missing"));
+ }
+
+ @Test
+ @DisplayName("Should return false when NacosException NOT_FOUND")
+ void testSkillExistsWhenNacosNotFound() throws NacosException {
+ when(aiService.loadSkill("missing"))
+ .thenThrow(new NacosException(NacosException.NOT_FOUND, "not found"));
+
+ assertFalse(repository.skillExists("missing"));
+ }
+
+ @Test
+ @DisplayName("Should return correct repository info")
+ void testGetRepositoryInfo() {
+ AgentSkillRepositoryInfo info = repository.getRepositoryInfo();
+
+ assertNotNull(info);
+ assertEquals("nacos", info.getType());
+ assertEquals("namespace:public", info.getLocation());
+ assertFalse(info.isWritable());
+ }
+
+ @Test
+ @DisplayName("Should return correct source")
+ void testGetSource() {
+ assertEquals("nacos:public", repository.getSource());
+ }
+
+ @Test
+ @DisplayName("Should use default namespace when namespaceId is blank")
+ void testDefaultNamespace() {
+ try (NacosSkillRepository repo = new NacosSkillRepository(aiService, null)) {
+ assertEquals("nacos:public", repo.getSource());
+ assertEquals("namespace:public", repo.getRepositoryInfo().getLocation());
+ }
+ }
+
+ @Test
+ @DisplayName("Should always return false for isWriteable")
+ void testIsWriteable() {
+ assertFalse(repository.isWriteable());
+ repository.setWriteable(true);
+ assertFalse(repository.isWriteable());
+ }
+
+ @Test
+ @DisplayName("Should return empty list for getAllSkillNames")
+ void testGetAllSkillNames() {
+ assertTrue(repository.getAllSkillNames().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should return empty list for getAllSkills")
+ void testGetAllSkills() {
+ assertTrue(repository.getAllSkills().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should return false for save")
+ void testSave() {
+ assertFalse(repository.save(List.of(), false));
+ }
+
+ @Test
+ @DisplayName("Should return false for delete")
+ void testDelete() {
+ assertFalse(repository.delete("any-skill"));
+ }
+
+ private static Skill mockNacosSkill(String name, String description, String instruction) {
+ Skill skill = mock(Skill.class);
+ when(skill.getName()).thenReturn(name);
+ when(skill.getDescription()).thenReturn(description);
+ when(skill.getInstruction()).thenReturn(instruction);
+ when(skill.getResource()).thenReturn(null);
+ return skill;
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/test/java/io/agentscope/core/nacos/skill/NacosSkillToAgentSkillConverterTest.java b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/test/java/io/agentscope/core/nacos/skill/NacosSkillToAgentSkillConverterTest.java
new file mode 100644
index 000000000..1cdd6af16
--- /dev/null
+++ b/agentscope-extensions/agentscope-extensions-nacos/agentscope-extensions-nacos-skill/src/test/java/io/agentscope/core/nacos/skill/NacosSkillToAgentSkillConverterTest.java
@@ -0,0 +1,276 @@
+/*
+ * Copyright 2024-2026 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package io.agentscope.core.nacos.skill;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import com.alibaba.nacos.api.ai.model.skills.Skill;
+import com.alibaba.nacos.api.ai.model.skills.SkillResource;
+import io.agentscope.core.skill.AgentSkill;
+import java.util.HashMap;
+import java.util.Map;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.Mock;
+import org.mockito.junit.jupiter.MockitoExtension;
+
+/**
+ * Unit tests for {@link NacosSkillToAgentSkillConverter}.
+ */
+@ExtendWith(MockitoExtension.class)
+class NacosSkillToAgentSkillConverterTest {
+
+ @Mock private Skill nacosSkill;
+
+ @Test
+ @DisplayName("Should throw when Nacos Skill is null")
+ void testToAgentSkillWithNullSkill() {
+ IllegalArgumentException e =
+ assertThrows(
+ IllegalArgumentException.class,
+ () -> NacosSkillToAgentSkillConverter.toAgentSkill(null, "nacos:public"));
+ assertEquals("Nacos Skill cannot be null", e.getMessage());
+ }
+
+ @Test
+ @DisplayName("Should use default 'unknown' when name is null")
+ void testDefaultNameWhenNull() {
+ when(nacosSkill.getName()).thenReturn(null);
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("unknown", result.getName());
+ }
+
+ @Test
+ @DisplayName("Should use default 'unknown' when name is blank")
+ void testDefaultNameWhenBlank() {
+ when(nacosSkill.getName()).thenReturn(" ");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("unknown", result.getName());
+ }
+
+ @Test
+ @DisplayName("Should use default '(no description)' when description is null")
+ void testDefaultDescriptionWhenNull() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn(null);
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("(no description)", result.getDescription());
+ }
+
+ @Test
+ @DisplayName("Should use default '(no description)' when description is blank")
+ void testDefaultDescriptionWhenBlank() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("(no description)", result.getDescription());
+ }
+
+ @Test
+ @DisplayName("Should use default '(no instruction)' when instruction is null")
+ void testDefaultInstructionWhenNull() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn(null);
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("(no instruction)", result.getSkillContent());
+ }
+
+ @Test
+ @DisplayName("Should use default '(no instruction)' when instruction is blank")
+ void testDefaultInstructionWhenBlank() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn(" ");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("(no instruction)", result.getSkillContent());
+ }
+
+ @Test
+ @DisplayName("Should trim name, description, and instruction")
+ void testTrimsFields() {
+ when(nacosSkill.getName()).thenReturn(" my-skill ");
+ when(nacosSkill.getDescription()).thenReturn(" desc ");
+ when(nacosSkill.getInstruction()).thenReturn(" content ");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("my-skill", result.getName());
+ assertEquals("desc", result.getDescription());
+ assertEquals("content", result.getSkillContent());
+ }
+
+ @Test
+ @DisplayName("Should return empty resources when resource map is null")
+ void testNullResourceMap() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertNotNull(result.getResources());
+ assertTrue(result.getResources().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should return empty resources when resource map is empty")
+ void testEmptyResourceMap() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(new HashMap<>());
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertTrue(result.getResources().isEmpty());
+ }
+
+ @Test
+ @DisplayName("Should map resources correctly")
+ void testResourceMapping() {
+ SkillResource res1 = mock(SkillResource.class);
+ when(res1.getContent()).thenReturn("content1");
+ SkillResource res2 = mock(SkillResource.class);
+ when(res2.getContent()).thenReturn("content2");
+
+ Map resourceMap = new HashMap<>();
+ resourceMap.put("ref/guide.md", res1);
+ resourceMap.put("examples/sample.txt", res2);
+
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(resourceMap);
+
+ AgentSkill result = NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:ns1");
+
+ assertEquals(2, result.getResources().size());
+ assertEquals("content1", result.getResource("ref/guide.md"));
+ assertEquals("content2", result.getResource("examples/sample.txt"));
+ }
+
+ @Test
+ @DisplayName("Should use 'resource' as key when resource key is null")
+ void testNullResourceKey() {
+ SkillResource res = mock(SkillResource.class);
+ when(res.getContent()).thenReturn("content");
+ Map resourceMap = new HashMap<>();
+ resourceMap.put(null, res);
+
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(resourceMap);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("content", result.getResource("resource"));
+ }
+
+ @Test
+ @DisplayName("Should use empty string when SkillResource content is null")
+ void testNullResourceContent() {
+ SkillResource res = mock(SkillResource.class);
+ when(res.getContent()).thenReturn(null);
+ Map resourceMap = new HashMap<>();
+ resourceMap.put("key", res);
+
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(resourceMap);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("", result.getResource("key"));
+ }
+
+ @Test
+ @DisplayName("Should use empty string when SkillResource is null")
+ void testNullSkillResource() {
+ Map resourceMap = new HashMap<>();
+ resourceMap.put("key", null);
+
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(resourceMap);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:public");
+
+ assertEquals("", result.getResource("key"));
+ }
+
+ @Test
+ @DisplayName("Should pass source to AgentSkill")
+ void testSourcePassedToAgentSkill() {
+ when(nacosSkill.getName()).thenReturn("my-skill");
+ when(nacosSkill.getDescription()).thenReturn("desc");
+ when(nacosSkill.getInstruction()).thenReturn("instruction");
+ when(nacosSkill.getResource()).thenReturn(null);
+
+ AgentSkill result =
+ NacosSkillToAgentSkillConverter.toAgentSkill(nacosSkill, "nacos:custom-ns");
+
+ assertEquals("nacos:custom-ns", result.getSource());
+ }
+}
diff --git a/agentscope-extensions/agentscope-extensions-nacos/pom.xml b/agentscope-extensions/agentscope-extensions-nacos/pom.xml
index 8ba5f51a7..6d218e575 100644
--- a/agentscope-extensions/agentscope-extensions-nacos/pom.xml
+++ b/agentscope-extensions/agentscope-extensions-nacos/pom.xml
@@ -34,6 +34,7 @@
agentscope-extensions-nacos-a2a
agentscope-extensions-nacos-prompt
+ agentscope-extensions-nacos-skill
diff --git a/docs/en/task/agent-skill.md b/docs/en/task/agent-skill.md
index f09536af1..4f7436fec 100644
--- a/docs/en/task/agent-skill.md
+++ b/docs/en/task/agent-skill.md
@@ -301,6 +301,20 @@ Resource structure: Place multiple skill subdirectories under `src/main/resource
> Note: `JarSkillRepositoryAdapter` is deprecated. Use `ClasspathSkillRepository` instead.
+#### Nacos Repository (Read-Only)
+
+Pulls or subscribes to Skills from Nacos via a pre-built `AiService` (or Nacos connection config). The Agent fetches Skills from Nacos at runtime in real time, with support for change subscription and automatic awareness. Suitable for online scenarios that need to stay in sync with Nacos.
+
+```java
+// Create Nacos skill repository with a pre-built AiService
+try (NacosSkillRepository repository = new NacosSkillRepository(aiService, "namespace")) {
+ AgentSkill skill = repository.getSkill("data-analysis");
+ boolean exists = repository.skillExists("data-analysis");
+} catch //...
+```
+
+> Note: Add the `agentscope-extensions-nacos-skill` dependency.
+
### Performance Optimization Recommendations
1. **Control SKILL.md Size**: Keep under 5k tokens, recommended 1.5-2k tokens
diff --git a/docs/zh/task/agent-skill.md b/docs/zh/task/agent-skill.md
index 7dfdc85d2..2f876aad3 100644
--- a/docs/zh/task/agent-skill.md
+++ b/docs/zh/task/agent-skill.md
@@ -295,6 +295,20 @@ try (ClasspathSkillRepository repository = new ClasspathSkillRepository("skills"
> 注意: `JarSkillRepositoryAdapter` 已废弃,请使用 `ClasspathSkillRepository`。
+#### Nacos 仓库 (只读)
+
+通过已构建的 `AiService`(或 Nacos 连接配置)从 Nacos 拉取或订阅 Skill,Agent 运行时从 Nacos 实时获取,支持变更订阅与自动感知,适合需要与 Nacos 保持同步的在线场景。
+
+```java
+// 使用已构建的 AiService 创建 Nacos 技能仓库
+try (NacosSkillRepository repository = new NacosSkillRepository(aiService, "namespace")) {
+ AgentSkill skill = repository.getSkill("data-analysis");
+ boolean exists = repository.skillExists("data-analysis");
+} catch //...
+```
+
+> 注意: 需引入 `agentscope-extensions-nacos-skill` 依赖
+
### 性能优化建议
1. **控制 SKILL.md 大小**: 保持在 5k tokens 以内,建议 1.5-2k tokens