From 1cd273494dc0d6cef354f2196b31fb5dc6f35e2f Mon Sep 17 00:00:00 2001 From: sunny-335 Date: Thu, 25 Jun 2026 07:56:16 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AF=AF=E6=8A=A5=E5=8F=8D=E9=A6=88?= =?UTF-8?q?=E3=80=81=E4=B8=8A=E4=B8=8B=E6=96=87=E4=BC=98=E5=85=88=E5=88=A4?= =?UTF-8?q?=E6=96=AD=E3=80=81FALSE=5FPOSITIVE=E7=8A=B6=E6=80=81=20(v1.1.3)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + docs/CHANGELOG.md | 40 ++++++ docs/guide/faq.md | 9 ++ docs/guide/introduction.md | 3 +- docs/guide/settings.md | 4 +- gradle.properties | 2 +- .../endpoint/CommentAiAutopilotEndpoint.java | 124 ++++++++++++++++++ .../service/AiFoundationDelegate.java | 7 +- .../service/AiReplyOrchestrator.java | 78 +++++------ .../service/CommentPreFilterService.java | 33 ++++- .../service/FilterService.java | 13 ++ src/main/resources/plugin.yaml | 2 +- ui/src/views/LogsView.vue | 93 ++++++++++++- 13 files changed, 357 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index 0470a97..bbd78a0 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ - **多语言适配** — 根据评论语言自动用对应语言回复 - **情感分析** — 分析评论情感倾向(非常正面/正面/中性/负面/非常负面),根据情感调整回复语气 - **前置过滤(合规检测)** — AI 回复前对评论进行合规性分类,自动拦截广告/辱骂攻击/敏感内容/乱码,违规评论停止生成 AI 回复以节省 Token,可选自动将违规评论设为待审核状态 +- **误报反馈** — 被误拦截的评论可进行误报反馈,支持"AI回复"和"仅通过"两种处理方式,"仅通过"后可随时补触发 AI 回复 - **草稿模式** — AI 回复先存为草稿,管理员审核后再发布,支持批量操作 - **失败重试** — AI 生成失败时自动重试,指数退避策略 - **对话轮次限制** — 同一评论线程中限制 AI 最多回复轮次,防止无限对话 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 73b7972..f379722 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -1,5 +1,34 @@ # 更新日志 +## v1.1.3 + +> 2026-06-25 + +### 新增 + +- **误报反馈功能** — 被拦截的评论可进行误报反馈,支持两种处理方式: + - **AI 回复**:标记为通过 + 触发 AI 生成回复 + - **仅通过**:仅标记为通过,不生成回复 +- **误报通过状态** — 新增 `FALSE_POSITIVE` 状态,"仅通过"的记录显示为"误报通过",不显示"通过/拒绝"按钮 +- **触发 AI 回复按钮** — "误报通过"状态的记录可随时点击"触发AI回复"按钮补生成 AI 回复 +- **上下文优先判断原则** — 前置过滤 AI 提示词重写,遵循五条核心原则:上下文优先、口语化宽容、恶意导向判定、宁放勿杀、闲聊不算无意义 + +### Bug 修复 + +- **修复误报反馈"AI 回复"被前置过滤再次拦截** — `processComment()` 始终调用 `preFilterService.check()`,用户已确认为误报的评论会被再次拦截。新增 `processFalsePositive()` 方法跳过前置过滤和去重检查 +- **修复误报反馈"AI 回复"被去重检查拦截** — `hasExistingReply()` 找到已有的 FILTERED→PENDING 记录导致 AI 回复无法生成。`processFalsePositive()` 复用已有记录,不经过去重检查 +- **修复误报反馈"AI 回复"导致全站崩溃** — `processComment()` 同步等待 AI 生成完成,HTTP 请求长时间不返回。改为 `.subscribe()` 异步执行,API 立即返回 +- **修复误报反馈"仅通过"后显示通过/拒绝按钮** — "仅通过"将记录设为 `status=PASS, published=false, reply=""`,导致显示"通过/拒绝"按钮且内容为空。改为 `status=FALSE_POSITIVE` +- **修复 `extractChoice` 无匹配时返回原始文本** — AI 返回非预期文本时被误判为违规类别。改为返回空字符串触发安全拦截 +- **修复 `approveOriginalComment` 缺少乐观锁重试** — 并发更新 Comment/Reply 时可能静默失败。添加 `Retry.backoff(3, 100ms)` 重试 + +### 改进 + +- **消除 `checkBlockedCommenters` 重复代码** — `FilterService` 新增 `isCommenterBlocked(commentName)` 公共方法,`AiReplyOrchestrator` 改为调用它 +- **前端批量操作防重复提交** — 批量通过/拒绝/删除按钮添加 `batchLoading` 状态,操作期间禁用按钮 + +--- + ## v1.1.2 > 2026-06-24 @@ -16,6 +45,17 @@ --- +## v1.1.1 + +> 2026-06-24 + +### 改进 + +- **"无意义"分类范围收窄** — 与文章主题无关的闲聊、灌水、打招呼不再被判为"无意义",仅纯乱码和无意义字符堆砌(如随机符号、键盘乱敲)才归类为"无意义" +- **AI 分类降级方案** — 当 `OutputSpec.choice` 结构化输出不被模型支持时,自动退回到普通 chat 调用并从响应文本中提取分类值(`classifyWithChat` fallback) + +--- + ## v1.1.0 > 2026-06-23 diff --git a/docs/guide/faq.md b/docs/guide/faq.md index 44f261f..1162a19 100644 --- a/docs/guide/faq.md +++ b/docs/guide/faq.md @@ -112,3 +112,12 @@ 1. **停止生成 AI 回复** — 不会消耗后续 Token 2. **创建拦截记录** — 在日志页显示为"已拦截"状态,标注分类标签(如"辱骂攻击")和详细原因(含评论内容摘要) 3. **自动设为待审核** — 原评论的 `approved` 会被置为 `false`,前端不再展示该评论,需人工判断后审核通过 + +## 被误拦截的评论怎么处理? + +在日志页的"已拦截"记录右侧,点击 **误报反馈** 按钮,可选择: + +- **AI 回复** — 标记为误报 + 自动通过评论 + 触发 AI 生成回复 +- **仅通过** — 仅标记为误报 + 自动通过评论,不生成 AI 回复 + +选择"仅通过"后,记录状态变为"误报通过",可随时点击 **触发AI回复** 按钮补生成 AI 回复。 diff --git a/docs/guide/introduction.md b/docs/guide/introduction.md index 34d276b..f8ceae5 100644 --- a/docs/guide/introduction.md +++ b/docs/guide/introduction.md @@ -15,7 +15,8 @@ AI回评(Comment AI Autopilot)是一个 Halo 博客系统的插件,能够 - **批量操作** — 草稿模式下支持批量通过/拒绝/删除 - **文章/页面级开关** — 在文章编辑器中直接控制是否启用AI回复,文章默认开启,页面默认关闭 - **评论者黑名单** — 屏蔽指定评论者,不触发AI回复,支持名称、邮箱和正则表达式 -- **前置过滤(合规检测)** — AI回复前对评论进行合规性分类,自动拦截广告/辱骂/敏感/无意义内容,节省Token;可选将违规评论设为待审核状态 +- **前置过滤(合规检测)** — AI回复前对评论进行合规性分类,自动拦截广告/辱骂/敏感/乱码内容,节省Token;可选将违规评论设为待审核状态 +- **误报反馈** — 被误拦截的评论可进行误报反馈,支持"AI回复"和"仅通过"两种处理方式,"仅通过"后可随时补触发 AI 回复 - **手动触发** — 在评论管理页面对历史评论手动触发AI回复 - **安全审核** — AI生成的内容经过两阶段安全审核(安全检查 + 质量评分),不合规内容自动拒绝 - **Prompt 预设** — 内置友好型、专业型、幽默型、简洁型预设风格,可多选组合 diff --git a/docs/guide/settings.md b/docs/guide/settings.md index 5b7f80b..5da7833 100644 --- a/docs/guide/settings.md +++ b/docs/guide/settings.md @@ -39,7 +39,7 @@ - **广告**:包含推广链接、产品推销、引流信息等 - **辱骂攻击**:包含辱骂、人身攻击、恶意挑衅、歧视性言论等 - **敏感内容**:涉及政治敏感、违法违规、色情暴力等 -- **无意义**:纯乱码、无意义字符堆砌、与文章完全无关的废话 +- **无意义**:纯乱码、无意义字符堆砌(如随机符号、键盘乱敲) 对于非"正常"类别的评论,插件会: @@ -47,6 +47,8 @@ 2. 创建一条 `FILTERED` 状态的日志记录(可在日志页通过"已拦截"状态筛选查看) 3. 若启用"违规评论设为待审核",会自动将原评论的 `approved` 置为 `false`,使其进入待审核队列,需人工判断后审核通过 +被误拦截的评论可在日志页点击 **误报反馈** 按钮处理,支持"AI 回复"和"仅通过"两种方式。选择"仅通过"后记录变为"误报通过"状态,可随时点击"触发AI回复"按钮补生成回复。 + ::: warning 前置过滤依赖 AI Foundation 插件进行分类判断,会额外消耗少量 Token。若 AI 服务不可用或分类失败,为安全起见将拦截评论而非放行,防止违规内容漏网。 ::: diff --git a/gradle.properties b/gradle.properties index b0c9890..b078b51 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -version=1.1.2 +version=1.1.3 # Fix Windows Gradle Worker Daemon exit code 268435659 when running pnpm via Exec tasks org.gradle.daemon=false diff --git a/src/main/java/top/nxxy335/commentaiautopilot/endpoint/CommentAiAutopilotEndpoint.java b/src/main/java/top/nxxy335/commentaiautopilot/endpoint/CommentAiAutopilotEndpoint.java index 135a523..5246dbb 100644 --- a/src/main/java/top/nxxy335/commentaiautopilot/endpoint/CommentAiAutopilotEndpoint.java +++ b/src/main/java/top/nxxy335/commentaiautopilot/endpoint/CommentAiAutopilotEndpoint.java @@ -101,6 +101,8 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint { .POST("/import", this::importConfig) // 更新草稿回复内容(同时更新 AiCommentReply 和 Reply 扩展) .PUT("/replies/{name}/content", this::updateReplyContent) + // 误报反馈:将拦截记录标记为误报,可选触发AI回复 + .POST("/replies/{name}/false-positive", this::falsePositive) .build(); } @@ -1035,4 +1037,126 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint { .switchIfEmpty(ServerResponse.notFound().build()); }); } + + /** + * 误报反馈:将被拦截的评论标记为误报(正常),并可选触发 AI 回复。 + * + * 请求体:{ "action": "aiReply" | "approveOnly" } + * - aiReply: 将评论审核状态设为已通过 + 触发 AI 生成回复 + * - approveOnly: 仅将评论审核状态设为已通过,不触发 AI 回复 + */ + private Mono falsePositive(ServerRequest request) { + var name = request.pathVariable("name"); + return request.bodyToMono(String.class) + .flatMap(body -> { + String actionStr; + try { + JsonNode node = objectMapper.readTree(body); + actionStr = node.has("action") ? node.get("action").asText("approveOnly") : "approveOnly"; + } catch (Exception e) { + actionStr = "approveOnly"; + } + final String action = actionStr; + + return client.fetch(AiCommentReply.class, name) + .flatMap(record -> { + if (!"FILTERED".equals(record.getSpec().getStatus()) + && !"FALSE_POSITIVE".equals(record.getSpec().getStatus())) { + return ServerResponse.badRequest() + .bodyValue(Map.of("message", "仅已拦截或误报通过状态的记录可进行误报反馈")); + } + + String commentName = record.getSpec().getCommentId(); + String replyName = record.getSpec().getReplyTo(); + + // 1. 将原评论/回复的审核状态设为已通过 + Mono approveMono = approveOriginalComment(commentName, replyName); + + // 2. 更新 AiCommentReply 记录状态 + Mono updateRecordMono = Mono.defer(() -> client.fetch(AiCommentReply.class, name) + .flatMap(latest -> { + latest.getSpec().setFilterCategory("误报"); + latest.getSpec().setFilterReason("用户确认为误报,已通过"); + if ("aiReply".equals(action)) { + latest.getSpec().setStatus("PENDING"); + latest.getSpec().setReply(""); + } else { + // 仅通过:使用 FALSE_POSITIVE 状态,区别于 PASS + // 避免前端显示"通过/拒绝"按钮和"未发布"标签 + latest.getSpec().setStatus("FALSE_POSITIVE"); + latest.getSpec().setPublished(false); + } + return client.update(latest); + }) + .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) + .filter(e -> e instanceof OptimisticLockingFailureException)) + .then()); + + // 3. 异步触发 AI 回复(不阻塞 HTTP 响应) + // 使用 processFalsePositive 跳过前置过滤和去重检查 + if ("aiReply".equals(action)) { + boolean isConversation = Boolean.TRUE.equals(record.getSpec().getIsAiConversation()); + String recordName = record.getMetadata().getName(); + personaResolver.getPersonaNameFromComment(commentName) + .flatMap(personaName -> + orchestrator.processFalsePositive(commentName, replyName, isConversation, personaName, recordName) + ) + .subscribe( + null, + err -> log.warn("[FalsePositive] AI reply trigger failed for {}: {}", commentName, err.getMessage()), + () -> log.info("[FalsePositive] AI reply trigger completed for {}", commentName) + ); + } + + return approveMono + .then(updateRecordMono) + .then(ServerResponse.ok().bodyValue(Map.of( + "message", "aiReply".equals(action) ? "已标记为误报,AI回复正在后台生成" : "已标记为误报并通过" + ))); + }) + .switchIfEmpty(ServerResponse.notFound().build()); + }); + } + + /** + * 将被拦截评论的原 Comment 或 Reply 审核状态设为已通过。 + */ + private Mono approveOriginalComment(String commentName, String replyName) { + // 优先处理 Reply(AI 对话场景下违规内容来自 Reply) + if (replyName != null && !replyName.isBlank()) { + return client.fetch(Reply.class, replyName) + .flatMap(reply -> { + var spec = reply.getSpec(); + if (spec != null && !Boolean.TRUE.equals(spec.getApproved())) { + spec.setApproved(true); + spec.setApprovedTime(Instant.now()); + return client.update(reply) + .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) + .filter(e -> e instanceof OptimisticLockingFailureException)) + .doOnSuccess(r -> log.info("[FalsePositive] Reply {} approved", replyName)) + .then(); + } + return Mono.empty(); + }) + .switchIfEmpty(Mono.defer(() -> approveComment(commentName))); + } + return approveComment(commentName); + } + + private Mono approveComment(String commentName) { + return client.fetch(Comment.class, commentName) + .flatMap(comment -> { + var spec = comment.getSpec(); + if (spec != null && !Boolean.TRUE.equals(spec.getApproved())) { + spec.setApproved(true); + spec.setApprovedTime(Instant.now()); + return client.update(comment) + .retryWhen(Retry.backoff(3, Duration.ofMillis(100)) + .filter(e -> e instanceof OptimisticLockingFailureException)) + .doOnSuccess(c -> log.info("[FalsePositive] Comment {} approved", commentName)) + .then(); + } + return Mono.empty(); + }); + } } diff --git a/src/main/java/top/nxxy335/commentaiautopilot/service/AiFoundationDelegate.java b/src/main/java/top/nxxy335/commentaiautopilot/service/AiFoundationDelegate.java index d33276c..7831134 100644 --- a/src/main/java/top/nxxy335/commentaiautopilot/service/AiFoundationDelegate.java +++ b/src/main/java/top/nxxy335/commentaiautopilot/service/AiFoundationDelegate.java @@ -127,6 +127,7 @@ class AiFoundationDelegate { /** * 从 chat 响应文本中提取匹配的分类值。 * 优先精确匹配,其次包含匹配。 + * 无匹配时返回空字符串(触发 defaultIfEmpty 安全拦截),避免原始文本被误判为违规类别。 */ static String extractChoice(String text, List choices) { if (text == null || text.isBlank()) return ""; @@ -139,9 +140,9 @@ class AiFoundationDelegate { for (String choice : choices) { if (trimmed.contains(choice)) return choice; } - // 无匹配,返回原始文本(让调用方处理) - log.warn("[Delegate] No matching choice found in response: {}", trimmed); - return trimmed; + // 无匹配,返回空字符串触发安全拦截 + log.warn("[Delegate] No matching choice found in response: '{}', returning empty for safety", trimmed); + return ""; } static Mono isAvailable(ExtensionGetter extensionGetter) { diff --git a/src/main/java/top/nxxy335/commentaiautopilot/service/AiReplyOrchestrator.java b/src/main/java/top/nxxy335/commentaiautopilot/service/AiReplyOrchestrator.java index 283e6e5..d7c5b33 100644 --- a/src/main/java/top/nxxy335/commentaiautopilot/service/AiReplyOrchestrator.java +++ b/src/main/java/top/nxxy335/commentaiautopilot/service/AiReplyOrchestrator.java @@ -105,7 +105,7 @@ public class AiReplyOrchestrator { } // Wake word triggered: skip page-level annotation check if (wakeWordTriggered) { - return checkBlockedCommenters(commentName) + return filterService.isCommenterBlocked(commentName) .flatMap(blocked -> { if (blocked) { log.info("[Orchestrator] Commenter blocked, skipping wake word: {}", commentName); @@ -133,6 +133,45 @@ public class AiReplyOrchestrator { .then(); } + /** + * 误报反馈专用:跳过前置过滤和去重检查,直接为已确认误报的评论生成 AI 回复。 + * + *

与 {@link #processComment} 不同,此方法: + *

    + *
  • 跳过前置过滤(用户已确认评论合规)
  • + *
  • 跳过去重检查(已有 FILTERED 记录,需复用)
  • + *
  • 跳过速率限制和黑名单检查(管理员主动操作)
  • + *
+ * + * @param commentName the parent Comment name + * @param replyName the Reply name (null for top-level comments) + * @param isAiConversation true when this is a conversation continuation + * @param personaName the persona name to use + * @param recordName the existing AiCommentReply record name to update + */ + public Mono processFalsePositive(String commentName, String replyName, + boolean isAiConversation, String personaName, + String recordName) { + log.info("[Orchestrator] Processing false-positive: comment={}, record={}", commentName, recordName); + + return getModelName().flatMap(modelName -> + contextExtractor.extract(commentName, replyName, isAiConversation) + .flatMap(context -> { + // Fetch the existing record (was FILTERED, now PENDING) + return client.fetch(AiCommentReply.class, recordName) + .flatMap(replyRecord -> + sentimentService.analyzeSentiment(context.commentContent(), modelName) + .flatMap(sentimentResult -> + promptBuilder.buildPrompt(context, sentimentResult.sentiment(), personaName) + .flatMap(prompt -> generateAndPublish(prompt, context, replyRecord, modelName, personaName)) + ) + ); + }) + ) + .doOnError(e -> log.error("[Orchestrator] Error processing false-positive {}: {}", commentName, e.getMessage(), e)) + .then(); + } + /** * Proceed with processing after all checks have passed. * Handles dedup checks and conversation round limits. @@ -171,43 +210,6 @@ public class AiReplyOrchestrator { ); } - /** - * Check if the commenter is in the blocked list. - */ - private Mono checkBlockedCommenters(String commentName) { - return client.fetch(run.halo.app.core.extension.content.Comment.class, commentName) - .flatMap(comment -> { - var owner = comment.getSpec().getOwner(); - if (owner == null) return Mono.just(false); - String displayName = owner.getDisplayName(); - String email = run.halo.app.core.extension.content.Comment.CommentOwner.KIND_EMAIL.equals(owner.getKind()) - ? owner.getName() : ""; - return client.fetch(ConfigMap.class, CONFIG_MAP_NAME) - .mapNotNull(cm -> { - var data = cm.getData(); - if (data == null) return false; - String basicJson = data.get("basic"); - if (basicJson == null || basicJson.isBlank()) return false; - try { - JsonNode node = objectMapper.readTree(basicJson); - String blockedStr = node.has("blockedCommenters") ? node.get("blockedCommenters").asText("") : ""; - if (blockedStr.isBlank()) return false; - for (String item : blockedStr.split(",")) { - String trimmed = item.trim(); - if (!trimmed.isEmpty() && (trimmed.equalsIgnoreCase(displayName) || trimmed.equalsIgnoreCase(email))) { - return true; - } - } - return false; - } catch (Exception e) { - return false; - } - }) - .defaultIfEmpty(false); - }) - .defaultIfEmpty(false); - } - private Mono doProcess(String commentName, String replyName, boolean isAiConversation, String personaName) { return getModelName().flatMap(modelName -> diff --git a/src/main/java/top/nxxy335/commentaiautopilot/service/CommentPreFilterService.java b/src/main/java/top/nxxy335/commentaiautopilot/service/CommentPreFilterService.java index eae7ebe..7366e6a 100644 --- a/src/main/java/top/nxxy335/commentaiautopilot/service/CommentPreFilterService.java +++ b/src/main/java/top/nxxy335/commentaiautopilot/service/CommentPreFilterService.java @@ -49,12 +49,43 @@ public class CommentPreFilterService { private static final String CLASSIFY_SYSTEM_PROMPT = """ 你是评论内容合规检测员。请判断以下评论属于哪个类别: + + 类别定义: - 正常:正常的评论、提问、讨论、赞美、闲聊等,即使与文章主题无关也算正常 - 广告:包含推广链接、产品推销、引流信息等 - 辱骂攻击:包含辱骂、人身攻击、恶意挑衅、歧视性言论等 - 敏感内容:涉及政治敏感、违法违规、色情暴力等 - 无意义:纯乱码、无意义字符堆砌(如随机符号、键盘乱敲) - 注意:与文章主题无关的闲聊、灌水、打招呼等属于"正常",不要误判为"无意义"。 + + ═══════════════════════════════════════ + 核心判断原则(必须严格遵守): + ═══════════════════════════════════════ + + 【原则一:上下文优先】 + 绝对禁止仅凭单个词汇进行机械拦截。必须结合整句话的语境、语气和前后文逻辑进行综合判断。一个词是否违规,取决于它在句子中的功能,而非词汇本身。 + + 【原则二:口语化宽容】 + 中文互联网存在大量口语化简写、谐音和省略表达。如果某个词在特定语境下明显是中性词或亲属称谓的口语化表达,且整句无攻击性、无恶意,必须判定为"正常"。 + 常见口语化中性用法示例: + - "他妈" → 可能是"他妈妈"的简称,如"小轩是他妈的朋友"=小轩是他妈妈的朋友 → 正常 + - "你妹" → 可能是"你妹妹"的简称,如"你妹在哪上学"=你妹妹在哪上学 → 正常 + - "卧槽" → 可能是语气词表示惊讶,如"卧槽这也太强了"=哇塞这也太厉害了 → 正常 + - "牛逼" → 口语化赞美,如"这文章写得牛逼" → 正常 + - "靠" → 语气词表示无奈或惊讶,如"靠又忘了" → 正常 + + 【原则三:恶意导向判定】 + 只有当词汇被明确用作辱骂、人身攻击、引战或带有较强负面情绪时,才判定为"辱骂攻击"。 + 恶意用法示例(这些才应判为"辱骂攻击"): + - "你他妈的" → 直接对他人进行辱骂 → 辱骂攻击 + - "你妹的" → 带有攻击性的语气词 → 辱骂攻击 + - "傻逼" → 直接辱骂他人 → 辱骂攻击 + + 【原则四:宁放勿杀】 + 当你无法确定评论是否违规时,应判定为"正常"而非"辱骂攻击"。误杀正常评论比漏判违规评论的负面影响更大。 + + 【原则五:闲聊不算无意义】 + 与文章主题无关的闲聊、灌水、打招呼等属于"正常",不要误判为"无意义"。 + 只返回类别名称,不要返回其他内容。"""; public CommentPreFilterService(ReactiveExtensionClient client, diff --git a/src/main/java/top/nxxy335/commentaiautopilot/service/FilterService.java b/src/main/java/top/nxxy335/commentaiautopilot/service/FilterService.java index c6c7cb2..bf058c7 100644 --- a/src/main/java/top/nxxy335/commentaiautopilot/service/FilterService.java +++ b/src/main/java/top/nxxy335/commentaiautopilot/service/FilterService.java @@ -58,6 +58,19 @@ public class FilterService { }); } + /** + * 检查评论者是否在黑名单中(按 commentName 查询)。 + */ + public Mono isCommenterBlocked(String commentName) { + return client.fetch(Comment.class, commentName) + .flatMap(this::checkBlockedCommenters) + .defaultIfEmpty(false) + .onErrorResume(e -> { + log.warn("[Filter] Error checking blocked commenter: {}", e.getMessage()); + return Mono.just(false); + }); + } + private Mono checkBlockedCommenters(Comment comment) { return client.fetch(ConfigMap.class, CONFIG_MAP_NAME) .mapNotNull(cm -> { diff --git a/src/main/resources/plugin.yaml b/src/main/resources/plugin.yaml index a178dba..5da6442 100644 --- a/src/main/resources/plugin.yaml +++ b/src/main/resources/plugin.yaml @@ -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.1.2" + version: "1.1.3" diff --git a/ui/src/views/LogsView.vue b/ui/src/views/LogsView.vue index 16d4972..f0ae677 100644 --- a/ui/src/views/LogsView.vue +++ b/ui/src/views/LogsView.vue @@ -27,6 +27,7 @@ +