代码片段管理工具:rofi前端+Java守护进程

This commit is contained in:
2025-10-05 00:30:37 +08:00
commit a6b2905ad2
49 changed files with 3058 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
package work.slhaf.snippet;
import cn.hutool.core.bean.BeanUtil;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.entity.db.Index;
import work.slhaf.snippet.entity.file.RebuildEntity;
import work.slhaf.snippet.gateway.CodeSnippetSocketServer;
import work.slhaf.snippet.service.IndexManager;
import work.slhaf.snippet.service.SnippetManager;
import java.io.File;
import java.io.IOException;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Slf4j
public class App{
void main() throws IOException, SQLException {
beforeLaunch();
CodeSnippetSocketServer server = new CodeSnippetSocketServer();
server.launch();
}
private void beforeLaunch() throws SQLException, IOException {
log.info("启动前检查环境");
//检查对应目录是否存在并创建
checkEnv();
checkDB();
}
private void checkDB() throws SQLException, IOException {
log.info("检查索引数据库");
//获取当前数据路径下的文件目录查看文件路径、sha值与索引数据库是否一致如果不一致需要重建
IndexManager indexManager = IndexManager.getInstance();
SnippetManager snippetManager = new SnippetManager();
File data = new File(System.getenv(Constant.Property.DIR));
File[] files = data.listFiles();
if (files == null || files.length == 0) {
if (!indexManager.isEmpty()) {
log.info("数据库为空,但文件目录不为空,删除数据库");
indexManager.resetIndex();
}
return;
}
HashMap<String, String> sha2PathDB = indexManager.getIndexStatus();
HashMap<String, String> sha2PathMD = snippetManager.getFileStatus();
if (!sha2PathMD.equals(sha2PathDB)) {
log.info("数据库与文件目录不一致,重建索引数据库");
List<RebuildEntity> snippets = snippetManager.listAll();
Set<Index> indexSet = snippets.stream().map(entity -> {
Index index = new Index();
BeanUtil.copyProperties(entity, index);
return index;
})
.collect(Collectors.toSet());
indexManager.rebuildIndex(indexSet);
}
log.info("索引数据库检查通过");
}
private void checkEnv() {
log.info("检查环境变量");
getEnvOrThrow(Constant.Property.PORT);
getEnvOrThrow(Constant.Property.API_KEY);
getEnvOrThrow(Constant.Property.BASE_URL);
getEnvOrThrow(Constant.Property.MODEL);
checkDir(Constant.Property.DIR);
checkDir(Constant.Property.CONF);
log.info("环境变量检查通过");
}
private void checkDir(String dir) {
File file = new File(getEnvOrThrow(dir));
if (file.exists()) return;
boolean ok = file.mkdirs();
if (!ok) {
throw new RuntimeException("创建目录失败: " + dir);
} else {
log.info("创建目录成功: {}", dir);
}
}
private String getEnvOrThrow(String key) {
String value = System.getenv(key);
if (value == null) throw new RuntimeException("未找到环境变量: " + key);
return value;
}
}

View File

@@ -0,0 +1,22 @@
package work.slhaf.snippet.common;
public class Constant {
public static class Property {
private static final String BASE = "CODE_SNIPPET_";
public static final String DIR = BASE + "DIR";
public static final String CONF = BASE + "CONF";
public static final String PORT = BASE + "PORT";
public static final String API_KEY = BASE + "API_KEY";
public static final String BASE_URL = BASE + "BASE_URL";
public static final String MODEL = BASE + "MODEL";
}
public enum Action {
LIST, EDIT, ADD, DELETE
}
public enum Status {
SUCCESS, FAILED
}
}

View File

@@ -0,0 +1,13 @@
package work.slhaf.snippet.common;
public class SnippetUtil {
public static String extractJson(String jsonStr) {
jsonStr = jsonStr.replace("", "\"").replace("", "\"");
int start = jsonStr.indexOf("{");
int end = jsonStr.lastIndexOf("}");
if (start != -1 && end != -1 && start < end) {
return jsonStr.substring(start, end + 1);
}
return jsonStr;
}
}

View File

@@ -0,0 +1,70 @@
package work.slhaf.snippet.common.chat;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import cn.hutool.json.JSONUtil;
import lombok.Data;
import lombok.NoArgsConstructor;
import work.slhaf.snippet.common.chat.constant.ChatConstant;
import work.slhaf.snippet.common.chat.pojo.ChatBody;
import work.slhaf.snippet.common.chat.pojo.ChatResponse;
import work.slhaf.snippet.common.chat.pojo.Message;
import work.slhaf.snippet.common.chat.pojo.PrimaryChatResponse;
import java.util.List;
@Data
@NoArgsConstructor
public class ChatClient {
private String clientId;
private String url;
private String apikey;
private String model;
private double top_p;
private double temperature;
private int max_tokens;
public ChatClient(String url, String apikey, String model) {
this.url = url;
this.apikey = apikey;
this.model = model;
}
public ChatResponse runChat(List<Message> messages) {
HttpRequest request = HttpRequest.post(url);
request.header("Content-Type", "application/json");
request.header("Authorization", "Bearer " + apikey);
ChatBody body;
if (top_p > 0) {
body = ChatBody.builder()
.model(model)
.messages(messages)
.top_p(top_p)
.temperature(temperature)
.max_tokens(max_tokens)
.build();
} else {
body = ChatBody.builder()
.model(model)
.messages(messages)
.build();
}
HttpResponse response = request.body(JSONUtil.toJsonStr(body)).execute();
ChatResponse finalResponse;
PrimaryChatResponse primaryChatResponse = JSONUtil.toBean(response.body(), PrimaryChatResponse.class);
finalResponse = ChatResponse.builder()
.type(ChatConstant.Response.SUCCESS)
.message(primaryChatResponse.getChoices().get(0).getMessage().getContent())
.usageBean(primaryChatResponse.getUsage())
.build();
response.close();
return finalResponse;
}
}

View File

@@ -0,0 +1,15 @@
package work.slhaf.snippet.common.chat.constant;
public class ChatConstant {
public static class Character {
public static final String USER = "user";
public static final String SYSTEM = "system";
public static final String ASSISTANT = "assistant";
}
public static class Response {
public static final String SUCCESS = "success";
public static final String ERROR = "error";
}
}

View File

@@ -0,0 +1,25 @@
package work.slhaf.snippet.common.chat.pojo;
import lombok.*;
import java.util.List;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ChatBody {
@NonNull
private String model;
@NonNull
private List<Message> messages;
@Builder.Default
private double temperature = 1;
@Builder.Default
private double top_p = 1;
private boolean stream;
@Builder.Default
private int max_tokens = 1024;
private int presence_penalty;
private int frequency_penalty;
}

View File

@@ -0,0 +1,16 @@
package work.slhaf.snippet.common.chat.pojo;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class ChatResponse {
private String type;
private String message;
private PrimaryChatResponse.UsageBean usageBean;
}

View File

@@ -0,0 +1,15 @@
package work.slhaf.snippet.common.chat.pojo;
import lombok.*;
@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Message {
@NonNull
private String role;
@NonNull
private String content;
}

View File

@@ -0,0 +1,111 @@
package work.slhaf.snippet.common.chat.pojo;
import lombok.Getter;
import lombok.Setter;
import java.util.List;
@Getter
@Setter
public class PrimaryChatResponse {
/**
* id
*/
private String id;
/**
* object
*/
private String object;
/**
* created
*/
private int created;
/**
* model
*/
private String model;
/**
* choices
*/
private List<ChoicesBean> choices;
/**
* usage
*/
private UsageBean usage;
/**
* system_fingerprint
*/
private String system_fingerprint;
@Setter
@Getter
public static class UsageBean {
/**
* prompt_tokens
*/
private int prompt_tokens;
/**
* completion_tokens
*/
private int completion_tokens;
/**
* total_tokens
*/
private int total_tokens;
/**
* prompt_cache_hit_tokens
*/
private int prompt_cache_hit_tokens;
/**
* prompt_cache_miss_tokens
*/
private int prompt_cache_miss_tokens;
@Override
public String toString() {
return "UsageBean{" +
"prompt_tokens=" + prompt_tokens +
", completion_tokens=" + completion_tokens +
", total_tokens=" + total_tokens +
", prompt_cache_hit_tokens=" + prompt_cache_hit_tokens +
", prompt_cache_miss_tokens=" + prompt_cache_miss_tokens +
'}';
}
}
@Setter
@Getter
public static class ChoicesBean {
/**
* index
*/
private int index;
/**
* message
*/
private MessageBean message;
/**
* logprobs
*/
private Object logprobs;
/**
* finish_reason
*/
private String finish_reason;
@Setter
@Getter
public static class MessageBean {
/**
* role
*/
private String role;
/**
* content
*/
private String content;
}
}
}

View File

@@ -0,0 +1,42 @@
package work.slhaf.snippet.entity;
import lombok.Data;
/**
* 片段完整数据存储至完整的markdown文件中
* 而数据库条目只需要元信息即可
*/
@Data
public class Snippet {
private String language;
private String[] tags;
private String description;
private String content;
public String toMarkdown() {
StringBuilder sb = new StringBuilder();
// Add Snippet content
sb.append("## Snippet\n");
sb.append("```").append(language).append("\n");
sb.append(content).append("\n");
sb.append("```\n\n");
// Add MetaData section
sb.append("## MetaData\n");
sb.append("- Language\n");
sb.append(" - ").append(language).append("\n");
sb.append("- Tags\n");
for (String tag : tags) {
sb.append(" - ").append(tag).append("\n");
}
sb.append("- Description\n");
sb.append(" - ").append(description).append("\n");
return sb.toString();
}
}

View File

@@ -0,0 +1,10 @@
package work.slhaf.snippet.entity;
import lombok.Data;
import work.slhaf.snippet.common.Constant;
@Data
public class SocketInputData {
private Constant.Action action;
private String data;
}

View File

@@ -0,0 +1,12 @@
package work.slhaf.snippet.entity;
import lombok.AllArgsConstructor;
import lombok.Data;
import work.slhaf.snippet.common.Constant;
@Data
@AllArgsConstructor
public class SocketOutputData {
private Constant.Status status;
private String data;
}

View File

@@ -0,0 +1,14 @@
package work.slhaf.snippet.entity.db;
import lombok.Data;
@Data
public class Index {
private int id;
private String name;
private String language;
private String[] tags;
private String sha;
private String description;
private String path;
}

View File

@@ -0,0 +1,14 @@
package work.slhaf.snippet.entity.file;
import lombok.Data;
@Data
public class AddEntity {
private String name;
private String language;
private String content;
public boolean checkEmpty(){
return name.isEmpty() || language.isEmpty() || content.isEmpty();
}
}

View File

@@ -0,0 +1,20 @@
package work.slhaf.snippet.entity.file;
import lombok.Data;
@Data
public class EditEntity {
private String id;
private String path;
private String[] tags;
private String description;
private String content;
public boolean checkEmpty() {
return id.isEmpty() ||
path.isEmpty() ||
content.isEmpty();
}
}

View File

@@ -0,0 +1,16 @@
package work.slhaf.snippet.entity.file;
import lombok.Data;
@Data
public class ListEntity implements Comparable<ListEntity> {
private String id;
private String path;
private String name;
private int score;
@Override
public int compareTo(ListEntity listEntity) {
return Integer.compare(listEntity.getScore(), this.score);
}
}

View File

@@ -0,0 +1,9 @@
package work.slhaf.snippet.entity.file;
import lombok.Data;
@Data
public class MetaDataEntity {
private String description;
private String[] tags;
}

View File

@@ -0,0 +1,15 @@
package work.slhaf.snippet.entity.file;
import lombok.Data;
@Data
public class RebuildEntity {
private String name;
private String path;
private String sha;
private String language;
private String[] tags;
private String description;
}

View File

@@ -0,0 +1,54 @@
package work.slhaf.snippet.gateway;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.entity.SocketInputData;
import work.slhaf.snippet.entity.SocketOutputData;
import work.slhaf.snippet.service.ActionHandler;
import java.io.*;
import java.net.Socket;
@Slf4j
public record ClientSocket(Socket socket, ActionHandler handler) implements Runnable {
@Override
public void run() {
System.out.println("建立连接: " + socket.getRemoteSocketAddress());
try (BufferedReader reader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = reader.readLine()) != null) {
sb.append(line);
}
SocketInputData inputData = JSONUtil.toBean(sb.toString(), SocketInputData.class);
SocketOutputData outputData = handler.handle(inputData);
writer.write(JSONUtil.toJsonStr(outputData));
writer.newLine();
writer.flush();
} catch (Exception e) {
SocketOutputData outputData = new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage());
try {
BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
writer.write(JSONUtil.toJsonStr(outputData));
writer.newLine();
writer.flush();
} catch (IOException ex) {
log.error(ex.getLocalizedMessage());
}
} finally {
try {
socket.close();
} catch (IOException e) {
log.error(e.getLocalizedMessage());
}
}
}
}

View File

@@ -0,0 +1,34 @@
package work.slhaf.snippet.gateway;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.service.ActionHandler;
import java.io.IOException;
import java.net.ServerSocket;
import java.sql.SQLException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
@Slf4j
public class CodeSnippetSocketServer {
public void launch() throws IOException, SQLException {
int port = Integer.parseInt(System.getenv(Constant.Property.PORT));
ActionHandler handler = new ActionHandler();
try (ServerSocket socket = new ServerSocket(port);
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
socket.setReuseAddress(true);
while (true) {
Future<?> future = executor.submit(new ClientSocket(socket.accept(), handler));
try{
future.get();
}catch (Exception e){
log.error(e.getLocalizedMessage());
}
}
}
}
}

View File

@@ -0,0 +1,120 @@
package work.slhaf.snippet.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.json.JSONUtil;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.entity.Snippet;
import work.slhaf.snippet.entity.SocketInputData;
import work.slhaf.snippet.entity.SocketOutputData;
import work.slhaf.snippet.entity.db.Index;
import work.slhaf.snippet.entity.file.AddEntity;
import work.slhaf.snippet.entity.file.EditEntity;
import work.slhaf.snippet.entity.file.ListEntity;
import work.slhaf.snippet.entity.file.MetaDataEntity;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.SQLException;
import java.util.List;
@Slf4j
public class ActionHandler {
private final SnippetManager snippetManager = new SnippetManager();
private final IndexManager indexManager = IndexManager.getInstance();
private final MetaDataExtractor metaDataExtractor = new MetaDataExtractor();
public ActionHandler() throws SQLException {
}
public SocketOutputData handle(SocketInputData inputData) {
log.info("收到请求: {}", inputData.getAction());
String data = inputData.getData();
try {
return switch (inputData.getAction()) {
case Constant.Action.ADD -> handleAdd(JSONUtil.toBean(data, AddEntity.class));
case Constant.Action.EDIT -> handleEdit(JSONUtil.toBean(data, EditEntity.class));
case Constant.Action.DELETE -> handleDelete(data);
case Constant.Action.LIST -> handleList(data);
};
} catch (Exception e) {
log.error("处理请求失败: {}", e.getLocalizedMessage());
return new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage());
}
}
private SocketOutputData handleList(String key) throws SQLException {
List<ListEntity> result = indexManager.list(key);
return new SocketOutputData(Constant.Status.SUCCESS, JSONUtil.toJsonStr(result));
}
private SocketOutputData handleDelete(String path) throws SQLException, IOException {
if (path.isEmpty()) {
return new SocketOutputData(Constant.Status.FAILED, "Path不能为空!");
}
indexManager.delete(path);
snippetManager.delete(path);
return new SocketOutputData(Constant.Status.SUCCESS, "删除成功: " + path);
}
private SocketOutputData handleEdit(EditEntity entity) throws IOException {
if (entity.checkEmpty()) {
return new SocketOutputData(Constant.Status.FAILED, "Id、Path、代码内容均不能为空!");
}
try {
snippetManager.update(entity, SnippetManager.UpdateAction.EDIT);
Index index = new Index();
BeanUtil.copyProperties(entity, index);
String sha = snippetManager.sha(entity.getPath());
index.setSha(sha);
indexManager.update(index);
snippetManager.update(entity, SnippetManager.UpdateAction.CONFIRM);
return new SocketOutputData(Constant.Status.SUCCESS, "文件编辑成功: " + entity.getPath());
} catch (Exception e) {
snippetManager.update(entity, SnippetManager.UpdateAction.FALLBACK);
log.error("文件编辑失败, 已回滚: {}", e.getLocalizedMessage());
return new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage());
}
}
private SocketOutputData handleAdd(AddEntity entity) throws IOException {
if (entity.checkEmpty()) {
return new SocketOutputData(Constant.Status.FAILED, "Language、Name、代码片段内容均不能为空!");
}
Path path = Path.of(System.getenv(Constant.Property.DIR), entity.getLanguage().toLowerCase(), entity.getName() + ".md");
try {
Snippet snippet = fixSnippet(entity);
snippetManager.add(snippet, path);
Index index = fixIndexEntity(snippet, path);
index.setName(entity.getName());
indexManager.add(index);
return new SocketOutputData(Constant.Status.SUCCESS, "代码片段已添加, 路径: " + path);
} catch (Exception e) {
Files.deleteIfExists(path);
log.error("文件添加失败: {}", e.getLocalizedMessage());
return new SocketOutputData(Constant.Status.FAILED, e.getLocalizedMessage());
}
}
private Index fixIndexEntity(Snippet snippet, Path path) {
Index index = new Index();
BeanUtil.copyProperties(snippet, index);
index.setPath(path.toString());
index.setSha(snippetManager.sha(index.getPath()));
return index;
}
private Snippet fixSnippet(AddEntity entity) {
MetaDataEntity metaData = metaDataExtractor.extract(entity);
Snippet snippet = new Snippet();
snippet.setLanguage(entity.getLanguage().toLowerCase());
snippet.setContent(entity.getContent());
snippet.setDescription(metaData.getDescription());
snippet.setTags(metaData.getTags());
return snippet;
}
}

View File

@@ -0,0 +1,178 @@
package work.slhaf.snippet.service;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.entity.db.Index;
import work.slhaf.snippet.entity.file.ListEntity;
import java.sql.*;
import java.util.*;
@Slf4j
public class IndexManager {
private static volatile IndexManager instance;
private final Connection connection;
public static IndexManager getInstance() throws SQLException {
if (instance == null) {
synchronized (IndexManager.class) {
if (instance == null) {
instance = new IndexManager();
}
}
}
return instance;
}
private IndexManager() throws SQLException {
connection = DriverManager.getConnection("jdbc:sqlite:" + System.getenv(Constant.Property.CONF) + "/index.db");
checkTable(connection);
}
private void checkTable(Connection connection) throws SQLException {
Statement statement = connection.createStatement();
statement.execute("""
create table if not exists code_snippet_index(
id integer primary key,
name varchar(255) not null,
language varchar(60) not null,
tags varchar(300) not null,
sha varchar(100) not null,
description varchar(300) not null,
path varchar(300) not null
)
""");
statement.close();
}
public void rebuildIndex(Set<Index> indexSet) throws SQLException {
log.info("重建索引数据库");
resetIndex();
for (Index index : indexSet) {
add(index);
}
}
public void add(Index index) throws SQLException {
PreparedStatement statement = connection.prepareStatement("""
insert into code_snippet_index(name,language, tags, sha, description, path)
values (?,?,?,?,?,?)
""");
statement.setString(1, index.getName());
statement.setString(2, index.getLanguage());
statement.setString(3, Arrays.toString(index.getTags()));
statement.setString(4, index.getSha());
statement.setString(5, index.getDescription());
statement.setString(6, index.getPath());
statement.execute();
statement.close();
}
public void update(Index index) throws SQLException {
PreparedStatement statement = connection.prepareStatement("""
update code_snippet_index
set tags = ?, description = ?, sha = ?
where id = ?
""");
statement.setString(1, Arrays.toString(index.getTags()));
statement.setString(2, index.getDescription());
statement.setString(3, index.getSha());
statement.executeUpdate();
statement.close();
}
public void delete(String path) throws SQLException {
PreparedStatement statement = connection.prepareStatement("delete from code_snippet_index where path = ?");
statement.setString(1, path);
statement.executeUpdate();
}
public List<ListEntity> list(String input) throws SQLException {
String[] keywords = input.trim().toLowerCase().split("\\s+");
// 构建 SQL 占位符字符串
StringBuilder sb = new StringBuilder("SELECT * FROM code_snippet_index WHERE ");
List<String> params = new ArrayList<>();
for (int k = 0; k < keywords.length; k++) {
if (k > 0) sb.append(" OR ");
sb.append("(name LIKE ? OR description LIKE ? OR tags LIKE ? OR language LIKE ?)");
for (int i = 0; i < 4; i++) params.add("%" + keywords[k] + "%");
}
PreparedStatement statement = connection.prepareStatement(sb.toString());
for (int i = 0; i < params.size(); i++) {
statement.setString(i + 1, params.get(i));
}
ResultSet rs = statement.executeQuery();
List<ListEntity> list = new ArrayList<>();
while (rs.next()) {
ListEntity entity = new ListEntity();
entity.setId(rs.getString("id"));
entity.setPath(rs.getString("path"));
entity.setName(rs.getString("name"));
entity.setScore(calculateScore(rs, keywords)); // 多关键字打分
list.add(entity);
}
// 按分数降序排序
list.sort(Comparator.comparingInt(ListEntity::getScore).reversed());
return list;
}
// 多关键字加权打分
private int calculateScore(ResultSet rs, String[] keywords) throws SQLException {
int score = 0;
String name = rs.getString("name").toLowerCase();
String description = rs.getString("description").toLowerCase();
String language = rs.getString("language").toLowerCase();
String tagsRaw = rs.getString("tags");
List<String> tagList = Arrays.stream(tagsRaw.substring(1, tagsRaw.length() - 1)
.split(","))
.map(String::toLowerCase)
.toList();
for (String key : keywords) {
if (name.contains(key)) score += 5;
if (tagList.stream().anyMatch(t -> t.contains(key))) score += 4;
if (description.contains(key)) score += 3;
if (language.contains(key)) score += 2;
}
return score;
}
public boolean isEmpty() throws SQLException {
Statement statement = connection.createStatement();
//判断表是否为空
ResultSet rs = statement.executeQuery("select count(*) from code_snippet_index");
rs.next();
int count = rs.getInt(1);
statement.close();
return count == 0;
}
public void resetIndex() throws SQLException {
log.info("重置索引");
//删除表中全部索引条目
Statement statement = connection.createStatement();
statement.execute("delete from code_snippet_index");
statement.close();
}
public HashMap<String, String> getIndexStatus() throws SQLException {
Statement statement = connection.createStatement();
ResultSet rs = statement.executeQuery("""
select sha,path from code_snippet_index
""");
HashMap<String, String> map = new HashMap<>();
while (rs.next()) {
map.put(rs.getString("path"), rs.getString("sha"));
}
return map;
}
}

View File

@@ -0,0 +1,55 @@
package work.slhaf.snippet.service;
import cn.hutool.json.JSONUtil;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.common.chat.ChatClient;
import work.slhaf.snippet.common.chat.constant.ChatConstant;
import work.slhaf.snippet.common.chat.pojo.ChatResponse;
import work.slhaf.snippet.common.chat.pojo.Message;
import work.slhaf.snippet.entity.file.AddEntity;
import work.slhaf.snippet.entity.file.MetaDataEntity;
import java.util.ArrayList;
import java.util.List;
import static work.slhaf.snippet.common.SnippetUtil.extractJson;
public class MetaDataExtractor {
private final ChatClient chatClient = new ChatClient(
System.getenv(Constant.Property.BASE_URL),
System.getenv(Constant.Property.API_KEY),
System.getenv(Constant.Property.MODEL)
);
private final List<Message> messages = List.of(
new Message(ChatConstant.Character.SYSTEM, """
你需要对接下来发送的代码相关内容进行提取,提取出‘描述’、‘标签’等内容。
输入格式示例:
```json
{
"name": "ByteBuddy用法示例", //代码片段名称
"language": "Java", //代码片段所属语言
"content": "Class<? extends Module> clazz = module.getClazz();\\nClass<? extends Module> proxyClass = new ByteBuddy()\\n .subclass(clazz)\\n .method(ElementMatchers.isOverriddenFrom(overrideSource))\\n .intercept(MethodDelegation.to(new ModuleProxyInterceptor(record.post, record.pre)))\\n .make()\\n .load(ModuleProxyFactory.class.getClassLoader())\\n .getLoaded();" //代码片段内容
}
```
输出格式示例
```json
{
"tags": [
"ByteBuddy",
"动态代理"
],
"description": "通过ByteBuddy创建动态代理类的示例"
}
```
""")
);
public MetaDataEntity extract(AddEntity entity) {
List<Message> temp = new ArrayList<>(messages);
temp.add(new Message(ChatConstant.Character.USER, JSONUtil.toJsonStr(entity)));
ChatResponse response = chatClient.runChat(temp);
return JSONUtil.toBean(extractJson(response.getMessage()), MetaDataEntity.class);
}
}

View File

@@ -0,0 +1,144 @@
package work.slhaf.snippet.service;
import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.io.file.FileNameUtil;
import cn.hutool.crypto.digest.DigestUtil;
import lombok.extern.slf4j.Slf4j;
import work.slhaf.snippet.common.Constant;
import work.slhaf.snippet.entity.Snippet;
import work.slhaf.snippet.entity.file.EditEntity;
import work.slhaf.snippet.entity.file.RebuildEntity;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
@Slf4j
public class SnippetManager {
private final SnippetReader snippetReader = new SnippetReader();
public HashMap<String, String> getFileStatus() {
File file = new File(System.getenv(Constant.Property.DIR));
HashMap<String, String> map = new HashMap<>();
listFileStatus(file, map);
return map;
}
private void listFileStatus(File file, HashMap<String, String> map) {
File[] files = file.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
listFileStatus(f, map);
} else {
String sha = DigestUtil.sha256Hex(f);
map.put(f.getAbsolutePath(), sha);
}
}
}
public void add(Snippet snippet, Path path) throws IOException {
String markdown = snippet.toMarkdown();
File file = path.toFile();
if (file.exists()) {
throw new RuntimeException("文件已存在: " + file.getAbsolutePath());
}
file.getParentFile().mkdirs();
Files.writeString(file.toPath(), markdown, StandardCharsets.UTF_8);
}
public void update(EditEntity entity, UpdateAction action) throws IOException {
log.info("编辑操作: {}, {}", entity.getPath(), action);
switch (action) {
case EDIT -> edit(entity);
case CONFIRM -> confirm(entity);
case FALLBACK -> fallback(entity);
}
}
private void fallback(EditEntity entity) throws IOException {
String path = entity.getPath();
String tempPath = getTempPath(path);
Files.move(Paths.get(tempPath), Paths.get(path), StandardCopyOption.REPLACE_EXISTING);
}
private void confirm(EditEntity entity) throws IOException {
String tempPath = getTempPath(entity.getPath());
Files.deleteIfExists(Paths.get(tempPath));
}
private void edit(EditEntity entity) throws IOException {
String path = entity.getPath();
String tempPath = getTempPath(path);
Path p = Paths.get(path);
Snippet snippet = new Snippet();
BeanUtil.copyProperties(entity, snippet);
Files.move(p, Paths.get(tempPath), StandardCopyOption.REPLACE_EXISTING);
Files.writeString(p, snippet.toMarkdown(), StandardCharsets.UTF_8);
}
private String getTempPath(String path) {
return path + ".tmp";
}
public void delete(String path) throws IOException {
Files.deleteIfExists(Paths.get(path));
}
public String sha(String filePath) {
File file = new File(filePath);
if (!file.exists()) {
throw new RuntimeException("文件不存在: " + filePath);
}
return DigestUtil.sha256Hex(file);
}
public List<RebuildEntity> listAll() throws IOException {
log.info("获取数据目录[{}]文件信息", System.getenv(Constant.Property.DIR));
List<RebuildEntity> list = new ArrayList<>();
File file = new File(System.getenv(Constant.Property.DIR));
listAll(file, list);
return list;
}
private void listAll(File file, List<RebuildEntity> list) throws IOException {
File[] files = file.listFiles();
if (files == null) {
return;
}
for (File f : files) {
if (f.isDirectory()) {
listAll(f, list);
} else {
FileReader reader = new FileReader(f, StandardCharsets.UTF_8);
String s = reader.readAllAsString();
Snippet snippet = snippetReader.visit(s);
RebuildEntity rebuildEntity = new RebuildEntity();
BeanUtil.copyProperties(snippet, rebuildEntity);
rebuildEntity.setPath(f.getAbsolutePath());
rebuildEntity.setSha(DigestUtil.sha256Hex(f));
rebuildEntity.setName(FileNameUtil.mainName(f));
list.add(rebuildEntity);
reader.close();
}
}
}
public enum UpdateAction {
EDIT, CONFIRM, FALLBACK
}
}

View File

@@ -0,0 +1,134 @@
package work.slhaf.snippet.service;
import org.commonmark.node.*;
import org.commonmark.parser.Parser;
import work.slhaf.snippet.entity.Snippet;
public class SnippetReader {
public Snippet visit(String text){
Snippet snippet = new Snippet();
Node document = Parser.builder().build().parse(text);
document.accept(new SnippetVisitor(snippet));
return snippet;
}
private static class SnippetVisitor extends AbstractVisitor {
private final Snippet snippet;
private String currentSection = "";
private String lastMetadataKey = "";
public SnippetVisitor(Snippet snippet) {
this.snippet = snippet;
}
private String extractText(Node node) {
StringBuilder text = new StringBuilder();
Node child = node.getFirstChild();
while (child != null) {
if (child instanceof Text) {
text.append(((Text) child).getLiteral());
} else if (child instanceof Paragraph) {
text.append(extractText(child));
}
child = child.getNext();
}
return text.toString();
}
@Override
public void visit(FencedCodeBlock fencedCodeBlock) {
// Extract the code content from the fenced code block
String codeContent = fencedCodeBlock.getLiteral();
if (codeContent != null) {
snippet.setContent(codeContent);
}
// Extract the language from the info string
String info = fencedCodeBlock.getInfo();
if (info != null && !info.isEmpty()) {
snippet.setLanguage(info);
}
super.visit(fencedCodeBlock);
}
@Override
public void visit(Heading heading) {
// Extract text content from the heading to identify the section
StringBuilder headingText = new StringBuilder();
Node child = heading.getFirstChild();
while (child != null) {
if (child instanceof Text) {
headingText.append(((Text) child).getLiteral());
}
child = child.getNext();
}
// Set the current section based on the heading text
currentSection = headingText.toString();
super.visit(heading);
}
private void addTag(String tag) {
String[] existingTags = snippet.getTags();
if (existingTags == null) {
snippet.setTags(new String[]{tag});
} else {
String[] newTags = new String[existingTags.length + 1];
System.arraycopy(existingTags, 0, newTags, 0, existingTags.length);
newTags[existingTags.length] = tag;
snippet.setTags(newTags);
}
}
@Override
public void visit(BulletList bulletList) {
super.visit(bulletList);
}
@Override
public void visit(ListItem listItem) {
// Extract text content from the list item
String itemText = extractText(listItem);
// Remove leading/trailing whitespace
itemText = itemText.trim();
// Process metadata based on the current section and item text
if ("MetaData".equals(currentSection)) {
if (itemText.equals("Language")) {
lastMetadataKey = "Language";
} else if (itemText.equals("Tags")) {
lastMetadataKey = "Tags";
} else if (itemText.equals("Description")) {
lastMetadataKey = "description";
} else {
// This is a value for the previous metadata key
if ("Language".equals(lastMetadataKey)) {
snippet.setLanguage(itemText);
} else if ("description".equals(lastMetadataKey)) {
snippet.setDescription(itemText);
} else if ("Tags".equals(lastMetadataKey)) {
// Collect tags
addTag(itemText);
}
}
}
super.visit(listItem);
}
@Override
public void visit(Paragraph paragraph) {
super.visit(paragraph);
}
@Override
public void visit(Text text) {
super.visit(text);
}
}
}

View File

@@ -0,0 +1,11 @@
import work.slhaf.snippet.entity.Snippet;
import work.slhaf.snippet.service.SnippetReader;
void main() throws IOException {
// Read the test file
FileReader reader = new FileReader("../test/test.md", StandardCharsets.UTF_8);
String s = reader.readAllAsString();
SnippetReader snippetReader = new SnippetReader();
Snippet visit = snippetReader.visit(s);
System.out.println(visit);
}

View File

@@ -0,0 +1,29 @@
import work.slhaf.snippet.entity.Snippet;
import work.slhaf.snippet.service.SnippetReader;
void main() throws IOException {
// Read the test file
FileReader reader = new FileReader("../test/test.md", StandardCharsets.UTF_8);
SnippetReader snippetReader = new SnippetReader();
StringBuilder content = new StringBuilder();
int ch;
while ((ch = reader.read()) != -1) {
content.append((char) ch);
}
reader.close();
// Parse the content using SnippetReader
Snippet snippet = snippetReader.visit(content.toString());
// Print the extracted information
System.out.println("Language: " + snippet.getLanguage());
System.out.println("Description: " + snippet.getDescription());
System.out.println("Tags: ");
if (snippet.getTags() != null) {
for (String tag : snippet.getTags()) {
System.out.println(" - " + tag);
}
}
System.out.println("Content: ");
System.out.println(snippet.getContent());
}