# AI 文章润色功能技术实施文档 **与其它 AI 功能**:润色始终使用 `DashScopeProperties.model`,与《AI助手模型管理功能需求》中的 `ai_assistant_model` **无关**;实施或联调助手模型管理时 **无需改动** 本模块。参见润色需求文档 §8。 ## 前提条件 **数据安全规则**: - 联调和测试过程中如需创建测试数据,测试完成后必须删除 - **数据库中存在一篇名为 "hello" 的文章,此文章不允许任何修改或删除操作** --- ## 1. 功能概述 在博客后台编辑器中增加"AI 润色"按钮,用户选中文本后调用大模型优化语法、表达和标点,返回结果以对比视图展示,支持逐段采纳或全部采纳。 ## 2. 核心实现思路 ### 2.1 复用现有 AI 能力 项目已有 `AiSummaryServiceImpl` 调用阿里云 DashScope 模型的能力,润色功能将**复用**该服务的模型调用链路: - 复用 `DashScopeProperties` 配置(API Key、Base URL、模型、超时、限流) - 复用 `AiSummaryServiceImpl.callModel()` 方法(HTTP 请求、认证、错误处理) - 复用 Redis Rate Limit 限流机制 ### 2.2 差异化实现 与摘要生成、标签推荐的主要差异: | 特性 | 摘要生成 | 标签推荐 | 文章润色 | |------|----------|----------|----------| | System Prompt | 生成摘要 | 提取标签 | 润色文本 | | User Prompt | 标题 + 正文 | 标题 + 正文 | 纯文本段落 | | 输入 | 全文(≤8000字符) | 全文(≤8000字符) | 选中文本(≤6000字符) | | 响应解析 | 截取摘要文本 | 解析逗号分隔标签 | 段落拆分 + 对比 | | 返回格式 | SummaryResultVO | TagRecommendResultVO | PolishResultVO(含 diffSegments) | | 前端交互 | 回填输入框 | 勾选标签确认 | 对比视图逐段采纳 | ### 2.3 实现方案 **后端**:新建 `TextPolishService` 服务类,调用 `AiSummaryServiceImpl.callModel()`,对返回结果做段落对齐拆分,返回 `diffSegments` 数组。 **前端**:在富文本工具栏增加"AI 润色"按钮,新建 `PolishDiffPanel.vue` 对比面板组件,实现逐段采纳交互。 --- ## 3. 改动项清单 ### 3.1 后端改动 #### 3.1.1 新增 DTO | 文件 | 说明 | |------|------| | `dto/PolishTextDTO.java` | 请求 DTO,包含 `text` 字段(@NotBlank,最长10000) | ```java // dto/PolishTextDTO.java @Data public class PolishTextDTO { @NotBlank(message = "文本不能为空") @Size(max = 10000, message = "文本长度不能超过10000") private String text; } ``` #### 3.1.2 新增 VO | 文件 | 说明 | |------|------| | `dto/DiffSegmentVO.java` | 段落对比 VO,包含 `index`、`original`、`polished` | | `dto/PolishResultVO.java` | 返回 VO,包含 `original`、`polished`、`diffSegments`、`model`、`costMillis` 等 | ```java // dto/DiffSegmentVO.java @Data @Builder public class DiffSegmentVO { private int index; private String original; private String polished; } ``` ```java // dto/PolishResultVO.java @Data @Builder public class PolishResultVO { private String original; private String polished; private List diffSegments; private String model; private Integer promptTokens; private Integer completionTokens; private long costMillis; } ``` #### 3.1.3 新增服务接口与实现 | 文件 | 说明 | |------|------| | `service/TextPolishService.java` | 润色服务接口 | | `service/impl/TextPolishServiceImpl.java` | 调用 AiSummaryServiceImpl.callModel(),实现润色 + 段落对齐拆分 | #### 3.1.4 修改 Controller | 文件 | 变更说明 | |------|----------| | `controller/ArticleController.java` | 新增 `POST /api/app/article/polish` 接口,注入 `TextPolishService` | #### 3.1.5 错误码 复用现有错误码,无需新增: | 错误码 | 常量名 | 触发场景 | |--------|--------|----------| | 5004 | AI_CONTENT_TOO_SHORT | text < 20 字符 | | 5005 | AI_API_KEY_MISSING | api-key 未配置 | | 5001 | AI_SERVICE_UNAVAILABLE | DashScope 返回异常 | | 5002 | AI_SERVICE_TIMEOUT | 调用超时 | | 429 | RATE_LIMITED | 超出频率限制 | --- ### 3.2 前端改动 #### 3.2.1 新增 API 方法 | 文件 | 说明 | |------|------| | `src/api/article.js` | 新增 `polishArticleText(data)` 方法 | ```js export function polishArticleText(data) { return request.post('/api/app/article/polish', data, { timeout: 35000 }) } ``` #### 3.2.2 修改文章编辑器 | 文件 | 说明 | |------|------| | `src/views/app/ArticleEditor.vue` | 工具栏新增"AI 润色"按钮、引入对比面板、选中文本获取逻辑 | #### 3.2.3 新增对比面板组件 | 文件 | 说明 | |------|------| | `src/components/PolishDiffPanel.vue` | 原文 vs 润色后对比面板,支持逐段采纳 / 全部采纳 / 关闭 | --- ## 4. 核心逻辑说明 ### 4.1 后端核心逻辑 ```java // TextPolishServiceImpl.java // 专用提示词 private static final String SYSTEM_PROMPT = "你是一名专业的文字编辑,请润色用户提供的中文文本。" + "修正语法错误、优化表达方式、规范标点符号,保持原意不变,使文本更流畅专业。" + "直接输出润色后的文本,不要添加任何解释或标记。"; private static final int POLISH_MIN_LENGTH = 20; private static final int POLISH_MAX_INPUT = 6000; private static final String RATE_LIMIT_KEY_PREFIX = "text_polish:rate:"; // 1. 构建请求 Map requestBody = Map.of( "model", properties.getModel(), "messages", List.of( Map.of("role", "system", "content", SYSTEM_PROMPT), Map.of("role", "user", "content", "请润色下面这段中文文本:\n" + plainText) ), "temperature", 0.3 // 低温度保证输出稳定 ); // 2. 调用模型(复用 AiSummaryServiceImpl.callModel) Map response = aiSummaryService.callModel(requestBody); // 3. 段落对齐:按原文换行符拆分,将润色结果对齐到对应段落 List segments = alignParagraphs(originalText, polishedText); // 4. 返回对比结果 return PolishResultVO.builder() .original(originalText) .polished(polishedText) .diffSegments(segments) .model(model) .costMillis(costMillis) .build(); ``` ### 4.2 段落对齐逻辑 模型可能合并或拆分段落,需要做对齐处理: ``` 输入:原文按 \n\n 拆分为段落数组 [P1, P2, P3] 输出:润色文本按 \n\n 拆分 对齐策略: - 如果段落数相等 → 一一对应 - 如果润色段落多 → 按顺序匹配,多余段落合并到最后一段 - 如果润色段落少 → 原文多余段落合并,原文和润色文本保持相同的 diffSegments 长度 - 段数差异过大(>2倍) → 降级为全文对比(单个 diffSegment) ``` ### 4.3 前端核心逻辑 #### 4.3.1 编辑器工具栏新增按钮 ```vue
✨ AI 润色
``` #### 4.3.2 获取选中文本 ```js // 通过 Tiptap editor 获取选中文本 const handlePolishText = () => { const { from, to } = editor.state.selection const selectedText = editor.state.doc.textBetween(from, to, '\n') if (!selectedText || selectedText.trim().length < 20) { ElMessage.warning('请先选中至少 20 个字符的文本') return } polishing.value = true polishArticleText({ text: selectedText }) .then(res => { polishResult.value = res.data.data showDiffPanel.value = true }) .catch(err => { ElMessage.error(err.response?.data?.message || '润色失败') }) .finally(() => { polishing.value = false }) } ``` #### 4.3.3 对比面板组件 ```vue ``` #### 4.3.4 采纳替换逻辑 ```js // ArticleEditor.vue 中处理采纳 const handleAcceptSegment = (index, polishedText) => { // 通过 Tiptap editor 替换对应段落 const { from, to } = polishSelection.value // 保存的选中范围 // 按段落拆分原文,定位到第 index 段在文档中的位置 // 使用 editor.chain().setTextSelection(...).insertContent(polishedText).run() } const handleAcceptAll = () => { // 用完整润色文本替换整个选中区域 editor.chain() .setTextSelection(polishSelection.value) .insertContent(polishResult.value.polished) .run() showDiffPanel.value = false ElMessage.success('已全部采纳') } ``` --- ## 5. 控件说明 ### 5.1 编辑器选中文本处理 由于编辑器使用 Tiptap(富文本),选中的内容可能是带格式的 HTML。润色处理流程: ``` 1. editor.state.selection → 获取选中范围 { from, to } 2. editor.state.doc.textBetween(from, to, '\n') → 提取纯文本(保留段落分隔) 3. 提交纯文本到后端 → 后端调用 AI 润色 4. 前端接收 diffSegments → 渲染对比视图 5. 用户采纳时 → 用润色后纯文本替换选中区域(保留原格式较复杂,首版用纯文本替换) ``` ### 5.2 对比面板布局 ``` ┌───────────────────────────────────────────────────────────┐ │ AI 润色结果对比 [全部采纳] [✕] │ ├───────────────────────────────────────────────────────────┤ │ ┌──────────────┬──────────────┬──────┐ │ │ │ 原文 │ 润色后 │ 操作 │ │ │ ├──────────────┼──────────────┼──────┤ │ │ │ 原文段落1 │ 润色段落1 │ 采纳 │ │ │ ├──────────────┼──────────────┼──────┤ │ │ │ 原文段落2 │ 润色段落2 │ 已采纳│ │ │ └──────────────┴──────────────┴──────┘ │ └───────────────────────────────────────────────────────────┘ ``` --- ## 6. 待办事项 | 序号 | 待办项 | 状态 | |------|--------|------| | 1 | 后端:创建 PolishTextDTO.java | ⬜ 待开始 | | 2 | 后端:创建 DiffSegmentVO.java | ⬜ 待开始 | | 3 | 后端:创建 PolishResultVO.java | ⬜ 待开始 | | 4 | 后端:创建 TextPolishService 接口 | ⬜ 待开始 | | 5 | 后端:创建 TextPolishServiceImpl 实现类 | ⬜ 待开始 | | 6 | 后端:修改 AiSummaryServiceImpl,提升 callModel 为 public | ⬜ 待开始 | | 7 | 后端:在 ArticleController 添加 POST /api/app/article/polish | ⬜ 待开始 | | 8 | 后端:单元测试(TextPolishServiceImplTest) | ⬜ 待开始 | | 9 | 前端:在 article.js 添加 polishArticleText API 方法 | ⬜ 待开始 | | 10 | 前端:创建 PolishDiffPanel.vue 对比面板组件 | ⬜ 待开始 | | 11 | 前端:在 ArticleEditor.vue 添加"AI 润色"按钮和交互 | ⬜ 待开始 | | 12 | 前后端联调 + 功能验证 | ⬜ 待开始 | --- ## 7. 实施顺序建议 1. **后端子步骤**:调整 `AiSummaryServiceImpl.callModel` 可见性(private → public)→ PolishTextDTO → DiffSegmentVO → PolishResultVO → TextPolishService 接口 → TextPolishServiceImpl → Controller 接口 → 单元测试 2. **前端子步骤**:API 方法 → PolishDiffPanel 组件 → ArticleEditor 按钮和交互 3. **最后联调**:前后端对接、bug 修复、验收 --- ## 8. 风险与注意事项 1. **callModel 可见性**:`AiSummaryServiceImpl.callModel()` 当前为 `protected`,需改为 `public` 以便 `TextPolishServiceImpl` 调用;或改为通过 `AiSummaryService` 接口暴露(推荐后者以保持封装) 2. **段落对齐准确性**:模型可能重组段落结构,对齐逻辑需覆盖段数不匹配的降级场景,建议首版实现简单策略(按 \n\n 拆分 + 全文降级) 3. **选中文本格式丢失**:采纳润色结果时,原选中区域的富文本格式(加粗、斜体等)会丢失,替换为纯文本。后续版本可考虑保留格式 4. **限流共享**:润色与摘要、标签推荐共享同一 Rate Limit 配额(每分钟 5 次/用户),高频使用场景需评估是否独立配额 5. **模型温度设置**:润色功能使用 `temperature=0.3`(与摘要一致),保证输出稳定;若模型输出过于保守可适当调高 6. **超长文本**:后端截断至 6000 字符,前端应在选中文本过长时给出提示