package com.xly.service;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
/**
* skill 存储 —— 业务流程知识做成文本技能,模型用 useSkill 载入后照步骤执行。
*
*
文件格式:{@code <名称>.md},首行 = 一句话用途(进 system prompt 索引),其余 = 技能正文。
* 默认从 classpath {@code skills/*.md} 读取(随包发布、缓存一次);配置
* {@code xly.skills.dir} 指向文件系统目录后改从该目录**每次现读**——改文件即热更,不用重启。
*/
@Service
public class SkillService {
private static final Logger log = LoggerFactory.getLogger(SkillService.class);
public record Skill(String name, String brief, String body) { }
@Value("${xly.skills.dir:}")
private String skillsDir;
private volatile List classpathCache;
public List all() {
if (skillsDir != null && !skillsDir.isBlank() && Files.isDirectory(Path.of(skillsDir))) {
return loadFromDir(Path.of(skillsDir));
}
List c = classpathCache;
if (c == null) {
c = loadFromClasspath();
classpathCache = c;
}
return c;
}
/** 按名精确匹配,其次互相包含(「新建报价单」→「新建报价」)。找不到返回 null。 */
public Skill find(String name) {
if (name == null || name.isBlank()) {
return null;
}
String n = name.trim();
List skills = all();
for (Skill s : skills) {
if (s.name().equals(n)) {
return s;
}
}
for (Skill s : skills) {
if (n.contains(s.name()) || s.name().contains(n)) {
return s;
}
}
return null;
}
/** system prompt 的技能索引(一行一个:名称:用途)。 */
public String indexLines() {
StringBuilder sb = new StringBuilder();
for (Skill s : all()) {
sb.append("- ").append(s.name()).append(":").append(s.brief).append('\n');
}
return sb.toString();
}
private List loadFromDir(Path dir) {
List out = new ArrayList<>();
try (Stream files = Files.list(dir)) {
files.filter(p -> p.getFileName().toString().endsWith(".md")).sorted().forEach(p -> {
try {
Skill s = parse(p.getFileName().toString(), Files.readString(p, StandardCharsets.UTF_8));
if (s != null) {
out.add(s);
}
} catch (IOException e) {
log.warn("skill 读取失败 {}: {}", p, e.getMessage());
}
});
} catch (IOException e) {
log.warn("skill 目录读取失败 {}: {}", dir, e.getMessage());
}
return out;
}
private List loadFromClasspath() {
List out = new ArrayList<>();
try {
Resource[] resources = new PathMatchingResourcePatternResolver()
.getResources("classpath:skills/*.md");
for (Resource r : resources) {
try {
Skill s = parse(r.getFilename(), new String(
r.getInputStream().readAllBytes(), StandardCharsets.UTF_8));
if (s != null) {
out.add(s);
}
} catch (IOException e) {
log.warn("skill 读取失败 {}: {}", r.getFilename(), e.getMessage());
}
}
} catch (IOException e) {
log.warn("classpath skills 加载失败: {}", e.getMessage());
}
out.sort((a, b) -> a.name().compareTo(b.name()));
return out;
}
private static Skill parse(String filename, String content) {
if (filename == null || content == null || content.isBlank()) {
return null;
}
String name = filename.endsWith(".md") ? filename.substring(0, filename.length() - 3) : filename;
int nl = content.indexOf('\n');
String brief = (nl < 0 ? content : content.substring(0, nl)).trim();
String body = nl < 0 ? "" : content.substring(nl + 1).trim();
return new Skill(name, brief, body);
}
}