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 *.ctxt
### Package Files ### Package Files
*.jar
*.war *.war
*.nar *.nar
*.ear *.ear
@@ -70,6 +71,12 @@ lerna-debug.log*
*.tar.gz *.tar.gz
*.rar *.rar
### UI build output
ui/dist/
ui/dist-ssr/
ui/*.local
ui/.eslintcache
### Local file ### Local file
application-local.yml application-local.yml
application-local.yaml application-local.yaml
+1 -1
View File
@@ -5,7 +5,7 @@ plugins {
} }
group 'top.nxxy335.commentaiautopilot' group 'top.nxxy335.commentaiautopilot'
version '1.0.0' version '1.0.4'
repositories { repositories {
mavenCentral() mavenCentral()
@@ -291,26 +291,51 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
var commentTime = String.valueOf(comment.getMetadata().getCreationTimestamp()); var commentTime = String.valueOf(comment.getMetadata().getCreationTimestamp());
var isCommentAi = isAiOwner(comment.getSpec().getOwner()); var isCommentAi = isAiOwner(comment.getSpec().getOwner());
// 首条评论没有引用对象
var commentMsg = new ConversationMessage( var commentMsg = new ConversationMessage(
"comment", commentOwner, commentContent, commentTime, isCommentAi "comment", commentOwner, commentContent, commentTime, isCommentAi, null, null
); );
return client.list(Reply.class, return client.list(Reply.class,
reply -> commentName.equals(reply.getSpec().getCommentName()), reply -> commentName.equals(reply.getSpec().getCommentName()),
null) null)
.sort(Comparator.comparing(r -> r.getMetadata().getCreationTimestamp())) .sort(Comparator.comparing(r -> r.getMetadata().getCreationTimestamp()))
.map(reply -> { .collectList() // 收集为List以便统一处理引用映射
.map(replyList -> {
List<ConversationMessage> messages = new ArrayList<>();
messages.add(commentMsg);
// 构建 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 replyOwner = extractOwnerName(reply.getSpec().getOwner());
var replyContent = extractContent(reply.getSpec().getRaw(), reply.getSpec().getContent()); var replyContent = extractContent(reply.getSpec().getRaw(), reply.getSpec().getContent());
var replyTime = String.valueOf(reply.getMetadata().getCreationTimestamp()); var replyTime = String.valueOf(reply.getMetadata().getCreationTimestamp());
var isAi = isAiOwner(reply.getSpec().getOwner()); var isAi = isAiOwner(reply.getSpec().getOwner());
return new ConversationMessage("reply", replyOwner, replyContent, replyTime, isAi);
}) String quoteOwner = null;
.collectList() String quoteContent = null;
.map(replyList -> {
List<ConversationMessage> messages = new ArrayList<>(); // 获取引用的 Reply 名称 (Halo中如果为空,代表直接回复顶级 Comment)
messages.add(commentMsg); String quoteReplyName = reply.getSpec().getQuoteReply();
messages.addAll(replyList); 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; return messages;
}); });
}) })
@@ -712,7 +737,9 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
String owner, String owner,
String content, String content,
String time, String time,
boolean isAi boolean isAi,
String quoteOwner,
String quoteContent
) {} ) {}
public record CommenterInfo( public record CommenterInfo(
@@ -80,7 +80,10 @@ public class CommentReplyPublisher {
private Mono<Reply> doPublish(String parentCommentName, String replyContent, private Mono<Reply> doPublish(String parentCommentName, String replyContent,
String postName, String quoteReplyName, boolean autoPublish, String postName, String quoteReplyName, boolean autoPublish,
String personaName) { String personaName) {
return resolvePersona(personaName).flatMap(persona -> {
// 解析 AI 角色并直接发布纯净的回复内容
return resolvePersona(personaName)
.flatMap(persona -> {
String displayName = persona.displayName(); String displayName = persona.displayName();
String email = persona.email(); String email = persona.email();
@@ -91,8 +94,11 @@ public class CommentReplyPublisher {
var spec = reply.getSpec(); var spec = reply.getSpec();
spec.setCommentName(parentCommentName); spec.setCommentName(parentCommentName);
// 直接存入纯净的 AI 回复内容,不加任何 Markdown 前缀
spec.setRaw(replyContent); spec.setRaw(replyContent);
spec.setContent(replyContent); spec.setContent(replyContent);
spec.setApproved(autoPublish); spec.setApproved(autoPublish);
if (autoPublish) { if (autoPublish) {
spec.setApprovedTime(Instant.now()); spec.setApprovedTime(Instant.now());
@@ -102,6 +108,7 @@ public class CommentReplyPublisher {
spec.setAllowNotification(false); spec.setAllowNotification(false);
spec.setHidden(false); spec.setHidden(false);
// Halo 原生评论组件正是靠这个字段来渲染 "回复 @某人" 的
if (quoteReplyName != null && !quoteReplyName.isBlank()) { if (quoteReplyName != null && !quoteReplyName.isBlank()) {
spec.setQuoteReply(quoteReplyName); spec.setQuoteReply(quoteReplyName);
} }
@@ -117,7 +124,6 @@ public class CommentReplyPublisher {
Map<String, String> ownerAnnotations = new HashMap<>(); Map<String, String> ownerAnnotations = new HashMap<>();
ownerAnnotations.put("comment-ai-autopilot.nxxy335.top/is-ai", "true"); ownerAnnotations.put("comment-ai-autopilot.nxxy335.top/is-ai", "true");
// 使用Gravatar邮箱头像
if (email != null && !email.isBlank()) { if (email != null && !email.isBlank()) {
String gravatarUrl = GravatarUtil.generateUrl(email); String gravatarUrl = GravatarUtil.generateUrl(email);
ownerAnnotations.put(Comment.CommentOwner.AVATAR_ANNO, gravatarUrl); ownerAnnotations.put(Comment.CommentOwner.AVATAR_ANNO, gravatarUrl);
@@ -125,16 +131,10 @@ public class CommentReplyPublisher {
owner.setAnnotations(ownerAnnotations); owner.setAnnotations(ownerAnnotations);
spec.setOwner(owner); spec.setOwner(owner);
log.info("[Publisher] Creating reply for comment: {}, owner: kind={}, name={}, displayName={}, annotations={}", log.info("[Publisher] Creating reply for comment: {}, content length: {}", parentCommentName, replyContent.length());
parentCommentName, owner.getKind(), owner.getName(), owner.getDisplayName(), ownerAnnotations);
return client.create(reply) return client.create(reply)
.doOnSuccess(created -> { .doOnSuccess(created -> log.info("[Publisher] AI Persona '{}' reply published for comment: {}", displayName, parentCommentName))
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())); .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" url: "https://github.com/sunny-335/plugin-comment-ai-autopilot/blob/main/LICENSE"
settingName: "comment-ai-autopilot-settings" settingName: "comment-ai-autopilot-settings"
configMapName: "comment-ai-autopilot-configmap" configMapName: "comment-ai-autopilot-configmap"
version: "1.0.0" version: "1.0.4"
+201 -868
View File
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff