diff --git a/README.md b/README.md index a393e1a4c..9180f72e1 100644 --- a/README.md +++ b/README.md @@ -55,6 +55,8 @@ OpenIsle 基于 Spring Boot 构建,提供社区后台常见的注册、登录 - `CAPTCHA_LOGIN_ENABLED`:登录是否需要验证码 - `CAPTCHA_POST_ENABLED`:发帖是否需要验证码 - `CAPTCHA_COMMENT_ENABLED`:评论是否需要验证码 + - `OPENAI_API_KEY`:OpenAI 接口密钥 + - `OPENAI_MODEL`:调用的模型名称,默认为 `gpt-3.5-turbo` 2. 启动项目: ```bash diff --git a/open-isle-cli/src/components/PostEditor.vue b/open-isle-cli/src/components/PostEditor.vue index c8019f2fd..164748978 100644 --- a/open-isle-cli/src/components/PostEditor.vue +++ b/open-isle-cli/src/components/PostEditor.vue @@ -1,6 +1,9 @@ @@ -22,11 +25,24 @@ export default { editorId: { type: String, default: () => 'post-editor-' + Math.random().toString(36).slice(2) + }, + loading: { + type: Boolean, + default: false } }, setup(props, { emit }) { const vditorInstance = ref(null) + watch( + () => props.loading, + val => { + if (vditorInstance.value && typeof vditorInstance.value.disabled === 'function') { + vditorInstance.value.disabled(val) + } + } + ) + watch( () => props.modelValue, val => { @@ -109,5 +125,19 @@ export default { diff --git a/open-isle-cli/src/views/NewPostPageView.vue b/open-isle-cli/src/views/NewPostPageView.vue index fb349cf17..d27fef06d 100644 --- a/open-isle-cli/src/views/NewPostPageView.vue +++ b/open-isle-cli/src/views/NewPostPageView.vue @@ -3,7 +3,7 @@
- +
@@ -47,6 +47,7 @@ export default { const selectedCategory = ref('') const selectedTags = ref([]) const isWaitingPosting = ref(false) + const isAiLoading = ref(false) const loadDraft = async () => { const token = getToken() @@ -154,6 +155,31 @@ export default { } } } + + const aiGenerate = async () => { + if (!content.value.trim()) { + toast.error('内容为空,无法优化') + return + } + isAiLoading.value = true + try { + const res = await fetch(`${API_BASE_URL}/api/ai/format`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ text: content.value }) + }) + if (res.ok) { + const data = await res.json() + content.value = data.content || '' + } else { + toast.error('AI 优化失败') + } + } catch (e) { + toast.error('AI 优化失败') + } finally { + isAiLoading.value = false + } + } const submitPost = async () => { if (!title.value.trim()) { toast.error('标题不能为空') @@ -203,7 +229,7 @@ export default { isWaitingPosting.value = false } } - return { title, content, selectedCategory, selectedTags, submitPost, saveDraft, clearPost, isWaitingPosting } + return { title, content, selectedCategory, selectedTags, submitPost, saveDraft, clearPost, isWaitingPosting, aiGenerate, isAiLoading } } } diff --git a/src/main/java/com/openisle/controller/AiController.java b/src/main/java/com/openisle/controller/AiController.java new file mode 100644 index 000000000..ddffc8d35 --- /dev/null +++ b/src/main/java/com/openisle/controller/AiController.java @@ -0,0 +1,30 @@ +package com.openisle.controller; + +import com.openisle.service.OpenAiService; +import lombok.RequiredArgsConstructor; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.Map; + +@RestController +@RequestMapping("/api/ai") +@RequiredArgsConstructor +public class AiController { + + private final OpenAiService openAiService; + + @PostMapping("/format") + public ResponseEntity> format(@RequestBody Map req) { + String text = req.get("text"); + if (text == null) { + return ResponseEntity.badRequest().build(); + } + return openAiService.formatMarkdown(text) + .map(t -> ResponseEntity.ok(Map.of("content", t))) + .orElse(ResponseEntity.status(500).build()); + } +} diff --git a/src/main/java/com/openisle/service/OpenAiService.java b/src/main/java/com/openisle/service/OpenAiService.java new file mode 100644 index 000000000..33346677f --- /dev/null +++ b/src/main/java/com/openisle/service/OpenAiService.java @@ -0,0 +1,65 @@ +package com.openisle.service; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import java.util.*; + +@Service +public class OpenAiService { + + @Value("${openai.api-key:}") + private String apiKey; + + @Value("${openai.model:gpt-3.5-turbo}") + private String model; + + private final RestTemplate restTemplate = new RestTemplate(); + + public Optional formatMarkdown(String text) { + if (apiKey == null || apiKey.isBlank()) { + return Optional.empty(); + } + String url = "https://api.openai.com/v1/chat/completions"; + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + headers.set("Authorization", "Bearer " + apiKey); + + Map body = new HashMap<>(); + body.put("model", model); + List> messages = new ArrayList<>(); + messages.add(Map.of("role", "system", "content", "请优化以下 Markdown 文本的格式,不改变其内容。")); + messages.add(Map.of("role", "user", "content", text)); + body.put("messages", messages); + + HttpEntity> entity = new HttpEntity<>(body, headers); + try { + ResponseEntity resp = restTemplate.exchange(url, HttpMethod.POST, entity, Map.class); + Map respBody = resp.getBody(); + if (respBody != null) { + Object choicesObj = respBody.get("choices"); + if (choicesObj instanceof List choices && !choices.isEmpty()) { + Object first = choices.get(0); + if (first instanceof Map firstMap) { + Object messageObj = firstMap.get("message"); + if (messageObj instanceof Map message) { + Object content = message.get("content"); + if (content instanceof String str) { + return Optional.of(str.trim()); + } + } + } + } + } + } catch (Exception ignored) { + } + return Optional.empty(); + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index bb941f3c3..83982c3aa 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -46,3 +46,6 @@ cos.bucket-name=${COS_BUCKET_NAME:} # Google OAuth configuration google.client-id=${GOOGLE_CLIENT_ID:} +# OpenAI configuration +openai.api-key=${OPENAI_API_KEY:} +openai.model=${OPENAI_MODEL:gpt-3.5-turbo}