SkillService.java
4.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
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 载入后照步骤执行。
*
* <p>文件格式:{@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<Skill> classpathCache;
public List<Skill> all() {
if (skillsDir != null && !skillsDir.isBlank() && Files.isDirectory(Path.of(skillsDir))) {
return loadFromDir(Path.of(skillsDir));
}
List<Skill> 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<Skill> 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<Skill> loadFromDir(Path dir) {
List<Skill> out = new ArrayList<>();
try (Stream<Path> 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<Skill> loadFromClasspath() {
List<Skill> 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);
}
}