feat: v1.0.0-beta.1 - AI Foundation reflection integration and draft mode fix

This commit is contained in:
sunny-335
2026-06-16 23:20:51 +08:00
parent 72726da9b8
commit d737cbc8f1
26 changed files with 410 additions and 601 deletions
@@ -1,7 +1,6 @@
package top.nxxy335.commentaiautopilot.endpoint;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
@@ -54,17 +53,17 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
private final ReactiveExtensionClient client;
private final AiReplyOrchestrator orchestrator;
private final AiReplyCleanupService cleanupService;
private final ObjectProvider<AiFoundationClient> aiFoundationClientProvider;
private final AiFoundationClient aiFoundationClient;
private final CommentReplyPublisher commentReplyPublisher;
private final ObjectMapper objectMapper;
private static final String CONFIG_MAP_NAME = "comment-ai-autopilot-configmap";
public CommentAiAutopilotEndpoint(ReactiveExtensionClient client, AiReplyOrchestrator orchestrator, AiReplyCleanupService cleanupService, ObjectProvider<AiFoundationClient> aiFoundationClientProvider, CommentReplyPublisher commentReplyPublisher) {
public CommentAiAutopilotEndpoint(ReactiveExtensionClient client, AiReplyOrchestrator orchestrator, AiReplyCleanupService cleanupService, AiFoundationClient aiFoundationClient, CommentReplyPublisher commentReplyPublisher) {
this.client = client;
this.orchestrator = orchestrator;
this.cleanupService = cleanupService;
this.aiFoundationClientProvider = aiFoundationClientProvider;
this.aiFoundationClient = aiFoundationClient;
this.commentReplyPublisher = commentReplyPublisher;
this.objectMapper = new ObjectMapper();
}
@@ -134,8 +133,9 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
final Instant finalStartInstant = startInstant;
final Instant finalEndInstant = endInstant;
// Check if we need in-memory filtering (keyword or date range)
boolean needsMemoryFilter = !keywordFilter.isBlank() || finalStartInstant != null || finalEndInstant != null;
// Check if we need in-memory filtering (keyword, date range, status, or sentiment)
boolean needsMemoryFilter = !keywordFilter.isBlank() || finalStartInstant != null || finalEndInstant != null
|| !statusFilter.isBlank() || !sentimentFilter.isBlank();
if (needsMemoryFilter) {
// Fall back to listAll + in-memory filter for complex queries
@@ -184,30 +184,18 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
.flatMap(result -> ServerResponse.ok().bodyValue(result));
}
// Simple filters only - use server-side pagination
// No filters - use server-side pagination directly
Sort sort = "asc".equalsIgnoreCase(sortOrder)
? Sort.by(Sort.Order.asc("metadata.creationTimestamp"))
: Sort.by(Sort.Order.desc("metadata.creationTimestamp"));
var listOptions = ListOptions.builder().build();
// Note: Halo's ListOptions fieldSelector support may be limited
// For status and sentiment, we'll still filter in memory but with paginated data
return client.listBy(AiCommentReply.class, listOptions,
PageRequestImpl.of(page - 1, size, sort))
.map(listResult -> {
var items = listResult.getItems();
// Apply status/sentiment filter in memory on the current page
var filtered = items.stream()
.filter(r -> {
if (!statusFilter.isBlank() && !statusFilter.equals(r.getSpec().getStatus())) return false;
if (!sentimentFilter.isBlank() && !sentimentFilter.equals(r.getSpec().getSentiment())) return false;
return true;
})
.toList();
Map<String, Object> result = new HashMap<>();
result.put("items", filtered);
result.put("items", listResult.getItems());
result.put("total", listResult.getTotal());
result.put("page", page);
result.put("size", size);
@@ -438,31 +426,50 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
.flatMap(record -> {
String replyName = record.getSpec().getReplyName();
if (replyName == null || replyName.isBlank()) {
// Draft mode: no Reply extension exists, create one with approved=true
return commentReplyPublisher.publishReply(
record.getSpec().getCommentId(),
record.getSpec().getReply(),
record.getSpec().getPostId(),
record.getSpec().getReplyTo(),
true,
record.getSpec().getPersonaName()
)
.switchIfEmpty(Mono.defer(() -> {
log.warn("[Endpoint] publishReply returned empty for draft approval of {}, AI reply may already exist", name);
return Mono.error(new IllegalStateException("AI回复已存在,无法重复发布"));
}))
.flatMap(publishedReply -> {
String newReplyName = publishedReply.getMetadata().getName();
return client.fetch(AiCommentReply.class, name)
.flatMap(latest -> {
latest.getSpec().setReplyName(newReplyName);
latest.getSpec().setPublished(true);
return client.update(latest);
})
// Draft mode: no Reply extension exists yet.
// First check if a Reply already exists (e.g. from a previous autoPublish=true run)
return findReplyForRecord(record)
.flatMap(existingReply -> {
// Reply already exists, just approve it
existingReply.getSpec().setApproved(true);
existingReply.getSpec().setApprovedTime(Instant.now());
return client.update(existingReply)
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof OptimisticLockingFailureException));
.filter(e -> e instanceof OptimisticLockingFailureException))
.then(Mono.defer(() -> client.fetch(AiCommentReply.class, name)
.flatMap(latest -> {
latest.getSpec().setReplyName(existingReply.getMetadata().getName());
latest.getSpec().setPublished(true);
return client.update(latest);
})
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof OptimisticLockingFailureException))
))
.then(ServerResponse.ok().bodyValue(Map.of("message", "approved")));
})
.then(ServerResponse.ok().bodyValue(Map.of("message", "approved")));
.switchIfEmpty(Mono.defer(() -> {
// No existing Reply found, create a new approved one
return commentReplyPublisher.publishReply(
record.getSpec().getCommentId(),
record.getSpec().getReply(),
record.getSpec().getPostId(),
record.getSpec().getReplyTo(),
true,
record.getSpec().getPersonaName()
)
.flatMap(publishedReply -> {
String newReplyName = publishedReply.getMetadata().getName();
return client.fetch(AiCommentReply.class, name)
.flatMap(latest -> {
latest.getSpec().setReplyName(newReplyName);
latest.getSpec().setPublished(true);
return client.update(latest);
})
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof OptimisticLockingFailureException));
})
.then(ServerResponse.ok().bodyValue(Map.of("message", "approved")));
}));
} else {
// Reply extension already exists, set approved=true
return client.fetch(Reply.class, replyName)
@@ -536,27 +543,45 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
.flatMap(record -> {
String replyName = record.getSpec().getReplyName();
if (replyName == null || replyName.isBlank()) {
// Draft mode: no Reply extension exists, create one with approved=true
return commentReplyPublisher.publishReply(
record.getSpec().getCommentId(),
record.getSpec().getReply(),
record.getSpec().getPostId(),
record.getSpec().getReplyTo(),
true,
record.getSpec().getPersonaName()
)
.switchIfEmpty(Mono.error(new IllegalStateException("AI回复已存在,无法重复发布")))
.flatMap(publishedReply -> {
String newReplyName = publishedReply.getMetadata().getName();
return client.fetch(AiCommentReply.class, name)
.flatMap(latest -> {
latest.getSpec().setReplyName(newReplyName);
latest.getSpec().setPublished(true);
return client.update(latest);
})
// Draft mode: check if Reply already exists first
return findReplyForRecord(record)
.flatMap(existingReply -> {
existingReply.getSpec().setApproved(true);
existingReply.getSpec().setApprovedTime(Instant.now());
return client.update(existingReply)
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof OptimisticLockingFailureException));
});
.filter(e -> e instanceof OptimisticLockingFailureException))
.then(Mono.defer(() -> client.fetch(AiCommentReply.class, name)
.flatMap(latest -> {
latest.getSpec().setReplyName(existingReply.getMetadata().getName());
latest.getSpec().setPublished(true);
return client.update(latest);
})
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof OptimisticLockingFailureException))
));
})
.switchIfEmpty(Mono.defer(() ->
commentReplyPublisher.publishReply(
record.getSpec().getCommentId(),
record.getSpec().getReply(),
record.getSpec().getPostId(),
record.getSpec().getReplyTo(),
true,
record.getSpec().getPersonaName()
)
.flatMap(publishedReply -> {
String newReplyName = publishedReply.getMetadata().getName();
return client.fetch(AiCommentReply.class, name)
.flatMap(latest -> {
latest.getSpec().setReplyName(newReplyName);
latest.getSpec().setPublished(true);
return client.update(latest);
})
.retryWhen(Retry.backoff(3, Duration.ofMillis(100))
.filter(e -> e instanceof OptimisticLockingFailureException));
})
));
} else {
// Reply extension already exists, set approved=true
return client.fetch(Reply.class, replyName)
@@ -860,21 +885,10 @@ public class CommentAiAutopilotEndpoint implements CustomEndpoint {
}
private Mono<ServerResponse> health(ServerRequest request) {
AiFoundationClient aiClient = aiFoundationClientProvider.getIfAvailable();
boolean aiFoundationInstalled = aiClient != null;
if (!aiFoundationInstalled) {
return ServerResponse.ok().bodyValue(
new HealthResponse(false, false, false, "", "unhealthy"));
}
// AI Foundation is installed, check if it's enabled and model is available
return aiClient.chat("ping", null)
.map(response -> (HealthResponse) new HealthResponse(true, true, true, "default", "healthy"))
.onErrorResume(e -> {
log.debug("Health check: AI Foundation call failed: {}", e.getMessage());
return Mono.just(new HealthResponse(true, true, false, "", "degraded"));
})
// AiFoundationClient is always created; availability is checked at runtime
return aiFoundationClient.isAvailable()
.map(available -> (HealthResponse) new HealthResponse(available, available, available, "", available ? "healthy" : "degraded"))
.defaultIfEmpty(new HealthResponse(false, false, false, "", "unhealthy"))
.flatMap(health -> ServerResponse.ok().bodyValue(health));
}
@@ -60,7 +60,7 @@ public class AiCommentReply extends AbstractExtension {
@Schema(description = "使用的AI角色名称")
private String personaName;
@Schema(description = "关联的Reply扩展名称,草稿模式下为空")
@Schema(description = "已发布的回复名称")
private String replyName;
}
}
@@ -38,7 +38,7 @@ public class AiPersona extends AbstractExtension {
@JsonProperty("isDefault")
private Boolean isDefault;
@Schema(description = "排序优先级,数值越小越靠前")
@Schema(description = "角色优先级,数值越小优先级越高")
private Integer priority;
}
}
@@ -72,6 +72,8 @@ public class ReplyReconciler implements Reconciler<Reconciler.Request> {
String parentCommentName = reply.getSpec().getCommentName();
if (parentCommentName == null || parentCommentName.isBlank()) {
markProcessed(reply);
client.update(reply);
return;
}
@@ -83,6 +85,8 @@ public class ReplyReconciler implements Reconciler<Reconciler.Request> {
// No quoteReply - this is a direct reply to the top-level comment,
// NOT a reply to AI. Skip it (CommentReconciler handles top-level comments).
log.debug("[ReplyReconciler] Reply {} has no quoteReply, skipping (not a reply to AI)", name);
markProcessed(reply);
client.update(reply);
return;
}
@@ -92,6 +96,8 @@ public class ReplyReconciler implements Reconciler<Reconciler.Request> {
if (!isReplyToAi) {
log.debug("[ReplyReconciler] Not a reply to AI, skipping: {}", name);
markProcessed(reply);
client.update(reply);
return;
}
@@ -1,32 +1,43 @@
package top.nxxy335.commentaiautopilot.service;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import run.halo.app.core.extension.Plugin;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
import run.halo.aifoundation.AiModelService;
import run.halo.aifoundation.chat.LanguageModel;
import run.halo.aifoundation.chat.GenerateTextResult;
import java.lang.reflect.Method;
import java.util.Map;
/**
* AI Foundation client that calls the AI Foundation plugin's AiModelService.
* Only instantiated when AI Foundation classes are available (via @ConditionalOnClass).
* AI Foundation client that uses runtime class loading and reflection
* to call the AI Foundation plugin's AiModelService.
* <p>
* This approach avoids classloader identity issues by loading AiModelService
* from ai-foundation's own classloader, so that Spring's getBeansOfType()
* can correctly match the implementation bean.
* <p>
* No @ConditionalOnClass or pluginDependencies needed.
* Always registered as a bean; availability is checked at runtime.
*/
@Slf4j
@RequiredArgsConstructor
@Component
public class AiFoundationClient {
private static final String AI_FOUNDATION_PLUGIN_NAME = "ai-foundation";
private static final String AI_MODEL_SERVICE_CLASS = "run.halo.aifoundation.AiModelService";
private final ExtensionGetter extensionGetter;
private final ReactiveExtensionClient client;
private final ApplicationContext applicationContext;
public AiFoundationClient(ReactiveExtensionClient client, ApplicationContext applicationContext) {
this.client = client;
this.applicationContext = applicationContext;
}
/**
* Call AI Foundation to generate a chat response using the specified model.
* Checks at runtime whether the ai-foundation plugin is installed and enabled
* before attempting to use it.
*
* @param prompt the prompt text
* @param modelName the AiModel metadata.name, null or blank to use default model
@@ -44,8 +55,16 @@ public class AiFoundationClient {
}
/**
* Check if the ai-foundation plugin is installed and enabled at runtime.
* Check if AI Foundation is available: plugin installed, enabled, and AiModelService bean found.
*/
public Mono<Boolean> isAvailable() {
return isAiFoundationEnabled()
.flatMap(enabled -> {
if (!enabled) return Mono.just(false);
return findAiModelService().hasElement();
});
}
private Mono<Boolean> isAiFoundationEnabled() {
return client.fetch(Plugin.class, AI_FOUNDATION_PLUGIN_NAME)
.map(plugin -> plugin.getSpec().getEnabled())
@@ -57,24 +76,144 @@ public class AiFoundationClient {
}
private Mono<String> doChat(String prompt, String modelName) {
return extensionGetter.getEnabledExtension(AiModelService.class)
.flatMap(service -> {
Mono<LanguageModel> modelMono;
if (modelName != null && !modelName.isBlank()) {
modelMono = service.languageModel(modelName);
} else {
modelMono = service.languageModel();
}
return modelMono.flatMap(model -> model.generateText(prompt)
.map(GenerateTextResult::getText)
.doOnNext(text -> log.debug("AI generated reply ({} chars) using model '{}'",
text.length(), modelName != null ? modelName : "default"))
);
})
return findAiModelService()
.flatMap(service -> invokeLanguageModel(service, modelName)
.flatMap(model -> invokeGenerateText(model, prompt))
)
.doOnError(e -> log.error("AI Foundation call failed: {}", e.getMessage()))
.onErrorResume(e -> {
log.warn("AI Foundation not available: {}", e.getMessage());
return Mono.empty();
});
}
/**
* Get PluginManager via the pluginWrapper bean registered in our plugin context.
* Halo's DefaultPluginApplicationContextFactory registers pluginWrapper as a singleton:
* beanFactory.registerSingleton("pluginWrapper", pluginWrapper);
* Then PluginWrapper.getPluginManager() gives us the PluginManager instance.
*/
private Object findPluginManager() {
try {
Object pluginWrapper = applicationContext.getBean("pluginWrapper");
Method getPluginManagerMethod = pluginWrapper.getClass().getMethod("getPluginManager");
getPluginManagerMethod.setAccessible(true);
Object pm = getPluginManagerMethod.invoke(pluginWrapper);
if (pm != null) {
log.info("Found PluginManager via pluginWrapper bean: {}", pm.getClass().getName());
}
return pm;
} catch (NoSuchMethodException e) {
log.warn("pluginWrapper does not have getPluginManager() method: {}", e.getMessage());
} catch (Exception e) {
log.warn("Failed to get PluginManager via pluginWrapper: {}", e.getMessage());
}
log.warn("PluginManager not found");
return null;
}
/**
* Find the AiModelService bean from ai-foundation's PluginApplicationContext.
* Uses PluginManager.getPlugin() to get the plugin wrapper, then reflection
* to get the plugin's ApplicationContext.
*/
private Mono<Object> findAiModelService() {
return Mono.fromCallable(() -> {
Object pm = findPluginManager();
if (pm == null) return null;
// Call pm.getPlugin("ai-foundation") via reflection
Method getPluginMethod = pm.getClass().getMethod("getPlugin", String.class);
getPluginMethod.setAccessible(true);
Object pluginWrapper = getPluginMethod.invoke(pm, AI_FOUNDATION_PLUGIN_NAME);
if (pluginWrapper == null) {
log.debug("ai-foundation plugin not found in PluginManager");
return null;
}
// Call pluginWrapper.getPlugin() to get the plugin instance
Method getPluginInstanceMethod = pluginWrapper.getClass().getMethod("getPlugin");
getPluginInstanceMethod.setAccessible(true);
Object pluginInstance = getPluginInstanceMethod.invoke(pluginWrapper);
if (pluginInstance == null) {
log.debug("ai-foundation plugin instance is null");
return null;
}
// Get the plugin's ApplicationContext via reflection on SpringPlugin
// DefaultSpringPlugin is package-private, so we need setAccessible
Method getCtxMethod = pluginInstance.getClass().getMethod("getApplicationContext");
getCtxMethod.setAccessible(true);
ApplicationContext pluginAppContext = (ApplicationContext) getCtxMethod.invoke(pluginInstance);
// Get the plugin classloader
Method getClassLoaderMethod = pluginWrapper.getClass().getMethod("getPluginClassLoader");
getClassLoaderMethod.setAccessible(true);
ClassLoader pluginClassLoader = (ClassLoader) getClassLoaderMethod.invoke(pluginWrapper);
// Load AiModelService from ai-foundation's classloader
Class<?> aiModelServiceClass = pluginClassLoader.loadClass(AI_MODEL_SERVICE_CLASS);
// Find the AiModelService bean in ai-foundation's ApplicationContext
Map<String, ?> beans = pluginAppContext.getBeansOfType(aiModelServiceClass);
if (beans.isEmpty()) {
log.debug("AiModelService bean not found in ai-foundation's ApplicationContext");
return null;
}
log.info("Found AiModelService bean in ai-foundation's ApplicationContext");
Object result = beans.values().iterator().next();
return (Object) result;
}).doOnError(e -> log.error("Failed to find AiModelService: {}", e.getMessage()));
}
/**
* Call service.languageModel(modelName) or service.languageModel() via reflection.
* Returns Mono&lt;LanguageModel&gt; from ai-foundation's classloader.
*/
private Mono<Object> invokeLanguageModel(Object service, String modelName) {
return Mono.fromCallable(() -> {
Method method;
if (modelName != null && !modelName.isBlank()) {
method = service.getClass().getMethod("languageModel", String.class);
method.setAccessible(true);
return method.invoke(service, modelName);
} else {
method = service.getClass().getMethod("languageModel");
method.setAccessible(true);
return method.invoke(service);
}
}).flatMap(result -> {
if (result instanceof Mono<?> mono) return mono;
return Mono.justOrEmpty(result);
});
}
/**
* Call model.generateText(prompt) via reflection, then extract text from result.
* Returns the generated text string.
*/
private Mono<String> invokeGenerateText(Object model, String prompt) {
return Mono.fromCallable(() -> {
Method method = model.getClass().getMethod("generateText", String.class);
method.setAccessible(true);
return method.invoke(model, prompt);
}).flatMap(result -> {
if (result instanceof Mono<?> mono) {
return mono.map(this::extractText);
}
return Mono.justOrEmpty(extractText(result));
});
}
private String extractText(Object result) {
if (result == null) return null;
try {
Method getText = result.getClass().getMethod("getText");
getText.setAccessible(true);
return (String) getText.invoke(result);
} catch (Exception e) {
throw new RuntimeException("Failed to call getText() on GenerateTextResult: " + e.getMessage(), e);
}
}
}
@@ -1,23 +0,0 @@
package top.nxxy335.commentaiautopilot.service;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import run.halo.app.extension.ReactiveExtensionClient;
import run.halo.app.plugin.extensionpoint.ExtensionGetter;
/**
* Configuration that registers AiFoundationClient only when
* AI Foundation plugin classes are available in the classloader.
* When AI Foundation is not installed, this entire configuration is skipped.
*/
@Configuration
@ConditionalOnClass(name = "run.halo.aifoundation.AiModelService")
public class AiFoundationConfiguration {
@Bean
public AiFoundationClient aiFoundationClient(ExtensionGetter extensionGetter,
ReactiveExtensionClient client) {
return new AiFoundationClient(extensionGetter, client);
}
}
@@ -1,7 +1,6 @@
package top.nxxy335.commentaiautopilot.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@@ -9,10 +8,10 @@ import reactor.core.publisher.Mono;
@Slf4j
public class AiReplyService {
private final ObjectProvider<AiFoundationClient> aiFoundationClientProvider;
private final AiFoundationClient aiFoundationClient;
public AiReplyService(ObjectProvider<AiFoundationClient> aiFoundationClientProvider) {
this.aiFoundationClientProvider = aiFoundationClientProvider;
public AiReplyService(AiFoundationClient aiFoundationClient) {
this.aiFoundationClient = aiFoundationClient;
}
/**
@@ -22,12 +21,7 @@ public class AiReplyService {
* @param modelName the model name (null for default)
*/
public Mono<String> generateReply(String prompt, String modelName) {
AiFoundationClient client = aiFoundationClientProvider.getIfAvailable();
if (client == null) {
log.warn("AI Foundation plugin is not installed, cannot generate reply");
return Mono.empty();
}
return client.chat(prompt, modelName)
return aiFoundationClient.chat(prompt, modelName)
.doOnError(e -> log.error("AI reply generation failed: {}", e.getMessage()))
.onErrorResume(e -> {
log.warn("AI Foundation not available: {}", e.getMessage());
@@ -39,18 +39,6 @@ public class PromptBuilder {
private static final String PRESET_CONCISE = """
【简洁型预设】你的回复应该非常简洁,一两句话即可。不要展开讨论,直接回应评论的核心内容。
""";
private static final String PRESET_TECHNICAL = """
【技术解答型预设】你的回复应该侧重于技术解答,提供准确的技术信息和解决方案。使用专业术语但要解释清楚,必要时提供代码示例或步骤说明。保持逻辑清晰,分点阐述。
""";
private static final String PRESET_ENCOURAGING = """
【鼓励型预设】你的回复应该充满鼓励和正能量,认可评论者的观点和想法。多用肯定性语言,表达对评论者思考的赞赏。即使评论有不足,也要以建设性的方式指出,给予信心和动力。
""";
private static final String PRESET_EDUCATIONAL = """
【知识科普型预设】你的回复应该以科普的方式展开,将复杂概念用通俗易懂的语言解释。适当引用相关知识点,帮助评论者拓宽视野。使用类比和举例让内容更易理解,但避免过于学术化。
""";
private static final Map<String, String> PRESET_MAP = new LinkedHashMap<>();
@@ -59,9 +47,6 @@ public class PromptBuilder {
PRESET_MAP.put("professional", PRESET_PROFESSIONAL);
PRESET_MAP.put("humorous", PRESET_HUMOROUS);
PRESET_MAP.put("concise", PRESET_CONCISE);
PRESET_MAP.put("technical", PRESET_TECHNICAL);
PRESET_MAP.put("encouraging", PRESET_ENCOURAGING);
PRESET_MAP.put("educational", PRESET_EDUCATIONAL);
}
private static final String SAFETY_PROMPT = """
@@ -76,12 +61,7 @@ public class PromptBuilder {
{{safety_prompt}}
【语言要求】你必须使用与评论相同的语言回复。检测评论的语言特征:
- 如果评论包含中文字符(汉字),请用中文回复
- 如果评论包含日文假名(平假名/片假名),请用日文回复
- 如果评论包含韩文字符,请用韩文回复
- 如果评论主要是拉丁字母,请根据其语言特征(如英语、法语、西班牙语等)用相同语言回复
- 绝对不要用与评论不同的语言回复
【语言要求】请用评论所使用的语言回复。如果评论是英文,请用英文回复;如果是中文,请用中文回复;如果是日文,请用日文回复;以此类推。
请回复以下评论。注意:
- 回复长度应与评论长度匹配,简短问候简短回复
@@ -1,7 +1,6 @@
package top.nxxy335.commentaiautopilot.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@@ -9,10 +8,10 @@ import reactor.core.publisher.Mono;
@Slf4j
public class ReviewService {
private final ObjectProvider<AiFoundationClient> aiFoundationClientProvider;
private final AiFoundationClient aiFoundationClient;
public ReviewService(ObjectProvider<AiFoundationClient> aiFoundationClientProvider) {
this.aiFoundationClientProvider = aiFoundationClientProvider;
public ReviewService(AiFoundationClient aiFoundationClient) {
this.aiFoundationClient = aiFoundationClient;
}
private static final String REVIEW_PROMPT_TEMPLATE = """
@@ -37,18 +36,12 @@ public class ReviewService {
public Mono<ReviewResult> review(String articleContent, String commentContent, String aiReply,
String modelName) {
AiFoundationClient client = aiFoundationClientProvider.getIfAvailable();
if (client == null) {
log.warn("AI Foundation plugin is not installed, skipping review (auto-pass)");
return Mono.just(new ReviewResult(100, "PASS", "AI Foundation 未安装,自动通过"));
}
String reviewPrompt = String.format(REVIEW_PROMPT_TEMPLATE,
truncate(articleContent, 2000),
truncate(commentContent, 500),
truncate(aiReply, 500));
return client.chat(reviewPrompt, modelName)
return aiFoundationClient.chat(reviewPrompt, modelName)
.map(this::parseSafetyResult)
.defaultIfEmpty(new ReviewResult(100, "PASS", "审核无响应,自动通过"))
.onErrorResume(e -> {
@@ -1,7 +1,6 @@
package top.nxxy335.commentaiautopilot.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@@ -9,10 +8,10 @@ import reactor.core.publisher.Mono;
@Slf4j
public class SentimentService {
private final ObjectProvider<AiFoundationClient> aiFoundationClientProvider;
private final AiFoundationClient aiFoundationClient;
public SentimentService(ObjectProvider<AiFoundationClient> aiFoundationClientProvider) {
this.aiFoundationClientProvider = aiFoundationClientProvider;
public SentimentService(AiFoundationClient aiFoundationClient) {
this.aiFoundationClient = aiFoundationClient;
}
public record SentimentResult(String sentiment, double confidence) {
@@ -22,15 +21,9 @@ public class SentimentService {
}
public Mono<SentimentResult> analyzeSentiment(String commentContent, String modelName) {
AiFoundationClient client = aiFoundationClientProvider.getIfAvailable();
if (client == null) {
log.warn("[Sentiment] AI Foundation plugin is not installed, defaulting to NEUTRAL");
return Mono.just(new SentimentResult(SentimentResult.NEUTRAL, 0.0));
}
String prompt = buildSentimentPrompt(commentContent);
return client.chat(prompt, modelName)
return aiFoundationClient.chat(prompt, modelName)
.map(response -> {
String sentiment = parseSentiment(response);
return new SentimentResult(sentiment, 1.0);
+2 -4
View File
@@ -7,7 +7,7 @@ metadata:
name: comment-ai-autopilot
spec:
enabled: true
requires: ">=2.23.0"
requires: ">=2.25.0"
author:
name: 暖心向阳335
website: https://nxxy335.top
@@ -22,6 +22,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: "0.0.0-ygkszvd"
pluginDependencies:
ai-foundation: "*"
version: "1.0.0-beta.1"