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 skills/*.md}(文件名用 ASCII——部署链路里 unzip war 会把中文文件名搞乱),
* 首行 = 技能名,次行 = 一句话用途(进 system prompt 索引),其余 = 技能正文。
* 默认从 classpath 读取(随包发布、缓存一次);配置 {@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(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(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 content) {
if (content == null || content.isBlank()) {
return null;
}
String[] parts = content.split("\n", 3);
if (parts.length < 3) {
return null;
}
String name = parts[0].trim();
String brief = parts[1].trim();
String body = parts[2].trim();
if (name.isEmpty() || body.isEmpty()) {
return null;
}
return new Skill(name, brief, body);
}
}