feat: 彻底重构后台 UI 与对话上下文引用模块,全面优化移动端适配 (v1.0.4) #6

Merged
bbb-lsy07 merged 11 commits from main into main 2026-06-21 12:20:25 +08:00
7 changed files with 636 additions and 2427 deletions
+7
View File
@@ -63,6 +63,7 @@ lerna-debug.log*
*.ctxt
### Package Files
*.jar
*.war
*.nar
*.ear
@@ -70,6 +71,12 @@ lerna-debug.log*
*.tar.gz
*.rar
### UI build output
ui/dist/
ui/dist-ssr/
ui/*.local
ui/.eslintcache
### Local file
application-local.yml
application-local.yaml
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
}
group 'top.nxxy335.commentaiautopilot'
version '1.0.0'
version '1.0.4'
repositories {
mavenCentral()
@@ -291,26 +291,51 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
var commentTime = String.valueOf(comment.getMetadata().getCreationTimestamp());
var isCommentAi = isAiOwner(comment.getSpec().getOwner());
// 首条评论没有引用对象
var commentMsg = new ConversationMessage(
"comment", commentOwner, commentContent, commentTime, isCommentAi
"comment", commentOwner, commentContent, commentTime, isCommentAi, null, null
);
return client.list(Reply.class,
reply -> commentName.equals(reply.getSpec().getCommentName()),
null)
.sort(Comparator.comparing(r -> r.getMetadata().getCreationTimestamp()))
.map(reply -> {
var replyOwner = extractOwnerName(reply.getSpec().getOwner());
var replyContent = extractContent(reply.getSpec().getRaw(), reply.getSpec().getContent());
var replyTime = String.valueOf(reply.getMetadata().getCreationTimestamp());
var isAi = isAiOwner(reply.getSpec().getOwner());
return new ConversationMessage("reply", replyOwner, replyContent, replyTime, isAi);
})
.collectList()
.collectList() // 收集为List以便统一处理引用映射
.map(replyList -> {
List<ConversationMessage> messages = new ArrayList<>();
messages.add(commentMsg);
messages.addAll(replyList);
// 构建 Reply 的映射字典,方便查找引用关系
Map<String, Reply> replyMap = new HashMap<>();
for (Reply r : replyList) {
replyMap.put(r.getMetadata().getName(), r);
}
for (Reply reply : replyList) {
var replyOwner = extractOwnerName(reply.getSpec().getOwner());
var replyContent = extractContent(reply.getSpec().getRaw(), reply.getSpec().getContent());
var replyTime = String.valueOf(reply.getMetadata().getCreationTimestamp());
var isAi = isAiOwner(reply.getSpec().getOwner());
String quoteOwner = null;
String quoteContent = null;
// 获取引用的 Reply 名称 (Halo中如果为空,代表直接回复顶级 Comment)
String quoteReplyName = reply.getSpec().getQuoteReply();
if (quoteReplyName != null && !quoteReplyName.isBlank()) {
Reply quotedReply = replyMap.get(quoteReplyName);
if (quotedReply != null) {
quoteOwner = extractOwnerName(quotedReply.getSpec().getOwner());
quoteContent = extractContent(quotedReply.getSpec().getRaw(), quotedReply.getSpec().getContent());
}
} else {
// 没有 quoteReply 表示直接回复首条评论
quoteOwner = commentOwner;
quoteContent = commentContent;
}
messages.add(new ConversationMessage("reply", replyOwner, replyContent, replyTime, isAi, quoteOwner, quoteContent));
}
return messages;
});
})
@@ -712,7 +737,9 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
String owner,
String content,
String time,
boolean isAi
boolean isAi,
String quoteOwner,
String quoteContent
) {}
public record CommenterInfo(
@@ -80,63 +80,63 @@ public class CommentReplyPublisher {
private Mono<Reply> doPublish(String parentCommentName, String replyContent,
String postName, String quoteReplyName, boolean autoPublish,
String personaName) {
return resolvePersona(personaName).flatMap(persona -> {
String displayName = persona.displayName();
String email = persona.email();
Reply reply = new Reply();
reply.setMetadata(new Metadata());
reply.getMetadata().setName(generateReplyName());
reply.setSpec(new Reply.ReplySpec());
// 解析 AI 角色并直接发布纯净的回复内容
return resolvePersona(personaName)
.flatMap(persona -> {
String displayName = persona.displayName();
String email = persona.email();
var spec = reply.getSpec();
spec.setCommentName(parentCommentName);
spec.setRaw(replyContent);
spec.setContent(replyContent);
spec.setApproved(autoPublish);
if (autoPublish) {
spec.setApprovedTime(Instant.now());
}
spec.setPriority(0);
spec.setTop(false);
spec.setAllowNotification(false);
spec.setHidden(false);
Reply reply = new Reply();
reply.setMetadata(new Metadata());
reply.getMetadata().setName(generateReplyName());
reply.setSpec(new Reply.ReplySpec());
if (quoteReplyName != null && !quoteReplyName.isBlank()) {
spec.setQuoteReply(quoteReplyName);
}
var spec = reply.getSpec();
spec.setCommentName(parentCommentName);
var owner = new Comment.CommentOwner();
owner.setKind(Comment.CommentOwner.KIND_EMAIL);
if (email != null && !email.isBlank()) {
owner.setName(email);
} else {
owner.setName(AI_PERSONA_OWNER_PREFIX + displayName);
}
owner.setDisplayName(displayName + " AI");
// 直接存入纯净的 AI 回复内容,不加任何 Markdown 前缀
spec.setRaw(replyContent);
spec.setContent(replyContent);
Map<String, String> ownerAnnotations = new HashMap<>();
ownerAnnotations.put("comment-ai-autopilot.nxxy335.top/is-ai", "true");
// 使用Gravatar邮箱头像
if (email != null && !email.isBlank()) {
String gravatarUrl = GravatarUtil.generateUrl(email);
ownerAnnotations.put(Comment.CommentOwner.AVATAR_ANNO, gravatarUrl);
}
owner.setAnnotations(ownerAnnotations);
spec.setOwner(owner);
spec.setApproved(autoPublish);
if (autoPublish) {
spec.setApprovedTime(Instant.now());
}
spec.setPriority(0);
spec.setTop(false);
spec.setAllowNotification(false);
spec.setHidden(false);
log.info("[Publisher] Creating reply for comment: {}, owner: kind={}, name={}, displayName={}, annotations={}",
parentCommentName, owner.getKind(), owner.getName(), owner.getDisplayName(), ownerAnnotations);
// Halo 原生评论组件正是靠这个字段来渲染 "回复 @某人" 的
if (quoteReplyName != null && !quoteReplyName.isBlank()) {
spec.setQuoteReply(quoteReplyName);
}
return client.create(reply)
.doOnSuccess(created -> {
var createdOwner = created.getSpec().getOwner();
log.info("[Publisher] AI Persona '{}' reply published for comment: {}, quoteReply: {}, owner annotations after create: {}",
displayName, parentCommentName, quoteReplyName,
createdOwner != null ? createdOwner.getAnnotations() : "null");
})
.doOnError(e -> log.error("[Publisher] Failed to publish AI reply: {}", e.getMessage()));
});
var owner = new Comment.CommentOwner();
owner.setKind(Comment.CommentOwner.KIND_EMAIL);
if (email != null && !email.isBlank()) {
owner.setName(email);
} else {
owner.setName(AI_PERSONA_OWNER_PREFIX + displayName);
}
owner.setDisplayName(displayName + " AI");
Map<String, String> ownerAnnotations = new HashMap<>();
ownerAnnotations.put("comment-ai-autopilot.nxxy335.top/is-ai", "true");
if (email != null && !email.isBlank()) {
String gravatarUrl = GravatarUtil.generateUrl(email);
ownerAnnotations.put(Comment.CommentOwner.AVATAR_ANNO, gravatarUrl);
}
owner.setAnnotations(ownerAnnotations);
spec.setOwner(owner);
log.info("[Publisher] Creating reply for comment: {}, content length: {}", parentCommentName, replyContent.length());
return client.create(reply)
.doOnSuccess(created -> log.info("[Publisher] AI Persona '{}' reply published for comment: {}", displayName, parentCommentName))
.doOnError(e -> log.error("[Publisher] Failed to publish AI reply: {}", e.getMessage()));
});
}
/**
+1 -1
View File
@@ -30,4 +30,4 @@ spec:
url: "https://github.com/sunny-335/plugin-comment-ai-autopilot/blob/main/LICENSE"
settingName: "comment-ai-autopilot-settings"
configMapName: "comment-ai-autopilot-configmap"
version: "1.0.0"
version: "1.0.4"
+202 -869
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff