From 29bb7e2e87024b9050f281849321db87dd119dd7 Mon Sep 17 00:00:00 2001 From: empty Date: Sun, 11 Jan 2026 13:51:40 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=8C=83=E5=BC=8F=E6=A3=80=E6=9F=A5?= =?UTF-8?q?=E5=92=8C=E5=A4=8D=E6=A3=80=E5=8A=9F=E8=83=BD=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=99=BA=E8=83=BD=E7=B2=92=E5=BA=A6=E8=BF=87=E6=BB=A4=E5=92=8C?= =?UTF-8?q?=E6=89=B9=E9=87=8F=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 核心功能 - 范式检查和复检支持自动分批处理(15句/批) - 智能粒度过滤:根据选中句子数量自动调整检查粒度 * 1-5句:仅句子级检查(术语、语气、表达) * 6-20句:句子级+段落级检查(逻辑结构、递进关系) * 21+句:全文级检查(章节完整性、篇幅占比等) ## 新增文件 - src/stores/paradigm.js:自定义范式管理 store * inferScope:智能推断检查项粒度 * ensureGuidelinesHasScope:为旧范式补充 scope 字段 - src/utils/requirementParser.js:需求文档解析工具 * buildRequirementParserPrompt:生成解析 prompt * validateParadigm:范式验证(兼容字符串/对象格式) - src/components/RequirementParserPanel.vue:需求解析面板 * 支持粘贴文本或选择文件 * AI 自动生成范式配置(specializedPrompt + expertGuidelines) * 可视化编辑检查项(title/description/scope) ## 主要改进 - ArticleRewritePanel.vue: * 范式检查支持粒度过滤和批量处理 * 复检功能支持粒度过滤和批量处理 * 集成 paradigm store 自动补充 scope 字段 * 添加批次进度显示 - RequirementParserPanel.vue: * 修复对象格式 expertGuidelines 显示问题 * 支持编辑 title/description/scope 三个字段 * 添加粒度下拉选择器(句子级/段落级/全文级) ## 技术细节 - 向后兼容:支持字符串和对象两种 expertGuidelines 格式 - 自动转换:旧范式字符串格式自动转换为对象格式并推断 scope - 错误处理:JSON 解析失败时提供降级方案 - 控制台日志:[复检] 前缀标识复检相关日志 Co-Authored-By: Claude Sonnet 4.5 --- src/components/ArticleRewritePanel.vue | 414 +++++++++++++++------ src/components/RequirementParserPanel.vue | 431 ++++++++++++++++++++++ src/stores/paradigm.js | 267 ++++++++++++++ src/utils/requirementParser.js | 199 ++++++++++ 4 files changed, 1206 insertions(+), 105 deletions(-) create mode 100644 src/components/RequirementParserPanel.vue create mode 100644 src/stores/paradigm.js create mode 100644 src/utils/requirementParser.js diff --git a/src/components/ArticleRewritePanel.vue b/src/components/ArticleRewritePanel.vue index a996ba3..a28552e 100644 --- a/src/components/ArticleRewritePanel.vue +++ b/src/components/ArticleRewritePanel.vue @@ -141,9 +141,14 @@ -
- - AI 正在检查中... +
+
+ + AI 正在检查中... +
+
+ {{ checkProgress }} +
AI 正在重写中...
+
+ {{ rewriteProgress }} +

{{ rewriteStreamContent }}

@@ -295,6 +303,7 @@ :visible="showParadigmSelector" @close="showParadigmSelector = false" @select="handleParadigmSelect" + @create-custom="handleCreateCustomParadigm" /> @@ -303,18 +312,28 @@ @close="showDocSelector = false" @select="handleDocSelect" /> + + +
diff --git a/src/stores/paradigm.js b/src/stores/paradigm.js new file mode 100644 index 0000000..05aee0e --- /dev/null +++ b/src/stores/paradigm.js @@ -0,0 +1,267 @@ +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' + +/** + * 自定义范式管理 Store + * 用于管理用户通过需求文档生成的自定义范式 + */ +export const useParadigmStore = defineStore('paradigm', () => { + // 自定义范式列表(存储在 localStorage) + const customParadigms = ref([]) + + // 当前正在编辑的范式 + const editingParadigm = ref(null) + + // 是否正在解析需求文档 + const isParsing = ref(false) + + // 解析进度信息 + const parsingProgress = ref('') + + /** + * 从 localStorage 加载自定义范式 + */ + function loadCustomParadigms() { + try { + const stored = localStorage.getItem('customParadigms') + if (stored) { + customParadigms.value = JSON.parse(stored) + } + } catch (error) { + console.error('加载自定义范式失败:', error) + customParadigms.value = [] + } + } + + /** + * 保存自定义范式到 localStorage + */ + function saveCustomParadigms() { + try { + localStorage.setItem('customParadigms', JSON.stringify(customParadigms.value)) + } catch (error) { + console.error('保存自定义范式失败:', error) + } + } + + /** + * 添加自定义范式 + * @param {Object} paradigm - 范式对象 + */ + function addCustomParadigm(paradigm) { + // 检查是否已存在相同ID + const index = customParadigms.value.findIndex(p => p.id === paradigm.id) + if (index >= 0) { + // 更新现有范式 + customParadigms.value[index] = paradigm + } else { + // 添加新范式 + customParadigms.value.push(paradigm) + } + saveCustomParadigms() + } + + /** + * 删除自定义范式 + * @param {string} paradigmId - 范式ID + */ + function deleteCustomParadigm(paradigmId) { + customParadigms.value = customParadigms.value.filter(p => p.id !== paradigmId) + saveCustomParadigms() + } + + /** + * 根据ID获取自定义范式 + * @param {string} paradigmId - 范式ID + * @returns {Object|null} + */ + function getCustomParadigmById(paradigmId) { + return customParadigms.value.find(p => p.id === paradigmId) || null + } + + /** + * 更新范式的某个字段 + * @param {string} paradigmId - 范式ID + * @param {string} field - 字段名 + * @param {any} value - 新值 + */ + function updateParadigmField(paradigmId, field, value) { + const paradigm = getCustomParadigmById(paradigmId) + if (paradigm) { + paradigm[field] = value + saveCustomParadigms() + } + } + + /** + * 获取所有范式(包括内置和自定义) + * @param {Object} builtInParadigms - 内置范式对象 + * @returns {Array} + */ + function getAllParadigms(builtInParadigms = {}) { + const builtIn = Object.values(builtInParadigms).map(p => ({ + ...p, + type: 'builtin' + })) + + return [ + ...builtIn, + ...customParadigms.value + ] + } + + /** + * 计算属性:自定义范式数量 + */ + const customParadigmCount = computed(() => customParadigms.value.length) + + /** + * 计算属性:最近使用的范式(按创建时间排序) + */ + const recentCustomParadigms = computed(() => { + return [...customParadigms.value] + .sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)) + .slice(0, 5) + }) + + /** + * 导出范式为JSON + * @param {string} paradigmId - 范式ID + * @returns {string|null} JSON字符串 + */ + function exportParadigm(paradigmId) { + const paradigm = getCustomParadigmById(paradigmId) + if (!paradigm) return null + + try { + return JSON.stringify(paradigm, null, 2) + } catch (error) { + console.error('导出范式失败:', error) + return null + } + } + + /** + * 从JSON导入范式 + * @param {string} jsonString - JSON字符串 + * @returns {boolean} 是否成功 + */ + function importParadigm(jsonString) { + try { + const paradigm = JSON.parse(jsonString) + + // 基本验证 + if (!paradigm.id || !paradigm.name || !paradigm.specializedPrompt) { + throw new Error('范式格式不正确') + } + + // 生成新ID避免冲突 + paradigm.id = `custom-imported-${Date.now()}` + paradigm.createdAt = new Date().toISOString() + + addCustomParadigm(paradigm) + return true + } catch (error) { + console.error('导入范式失败:', error) + return false + } + } + + /** + * 清空所有自定义范式(慎用) + */ + function clearAllCustomParadigms() { + customParadigms.value = [] + saveCustomParadigms() + } + + /** + * 智能推断 guideline 的 scope + * @param {string|Object} guideline - 检查指令 + * @returns {string} scope ('sentence' | 'paragraph' | 'document') + */ + function inferScope(guideline) { + const text = typeof guideline === 'string' ? guideline : (guideline.description || guideline.title || '') + + // 全文级关键词 + if (/篇幅|章节|完整性|结构.*完整|会前准备|典型案例|巡视|巡察/.test(text)) { + return 'document' + } + + // 段落级关键词 + if (/递进|逻辑|段落|承接|前后.*呼应|层次|论述|因果/.test(text)) { + return 'paragraph' + } + + // 默认句子级 + return 'sentence' + } + + /** + * 为现有范式补充 scope 字段(返回新对象,不修改原对象) + * @param {Object} paradigm - 范式对象 + * @returns {Object} 更新后的范式(新对象) + */ + function ensureGuidelinesHasScope(paradigm) { + if (!paradigm.expertGuidelines || !Array.isArray(paradigm.expertGuidelines)) { + return paradigm + } + + // 创建深拷贝,避免修改原对象 + const updatedParadigm = { + ...paradigm, + expertGuidelines: paradigm.expertGuidelines.map(g => { + if (typeof g === 'string') { + // 字符串格式:转换为对象格式并推断 scope + // 提取前面的关键词作为标题(最多15个字符) + const title = g.length > 15 ? g.substring(0, 15) : g + return { + title: title, + description: g, + scope: inferScope(g) + } + } else if (!g.scope) { + // 对象格式但缺少 scope:推断并补充 + return { + ...g, + scope: inferScope(g) + } + } + return g + }) + } + + return updatedParadigm + } + + // 初始化时加载 + loadCustomParadigms() + + return { + // 状态 + customParadigms, + editingParadigm, + isParsing, + parsingProgress, + + // 计算属性 + customParadigmCount, + recentCustomParadigms, + + // 方法 + loadCustomParadigms, + saveCustomParadigms, + addCustomParadigm, + deleteCustomParadigm, + getCustomParadigmById, + updateParadigmField, + getAllParadigms, + exportParadigm, + importParadigm, + clearAllCustomParadigms, + + // Scope 工具函数 + inferScope, + ensureGuidelinesHasScope + } +}) diff --git a/src/utils/requirementParser.js b/src/utils/requirementParser.js new file mode 100644 index 0000000..7078e71 --- /dev/null +++ b/src/utils/requirementParser.js @@ -0,0 +1,199 @@ +/** + * 需求文档解析工具 + * 将需求文档转换为范式配置 + */ + +/** + * 构建需求解析的 Prompt + */ +export function buildRequirementParserPrompt(requirementText) { + return `你是一位专业的文档分析专家,擅长提取文档中的核心要求并将其转化为结构化的写作范式配置。 + +【任务】 +分析以下需求文档,提取关键要求并生成"范式配置",用于指导AI检查和润色文稿。 + +【需求文档】 +${requirementText} + +【输出要求】 +请生成以下三个部分,使用JSON格式输出: + +1. **specializedPrompt** (string): 系统提示词,包含: + - 会议主题/文档目标 + - 核心要求(列表形式) + - 论述规范(结构要求、术语规范、语气要求等) + - 维度要求(如五维度溯源等) + 长度:300-500字 + +2. **expertGuidelines** (array): 专家检查指令,每条指令应该: + - 针对需求文档中的具体要求 + - 可直接用于检查文稿是否符合标准 + - 清晰、可执行 + - **包含 scope 字段**(指定检查粒度): + * "sentence" - 句子级检查(如术语规范、语气分寸、表达方式) + * "paragraph" - 段落级检查(如逻辑结构、递进关系、论述层次) + * "document" - 全文级检查(如章节完整性、篇幅占比、结构要求) + 数量:8-12条 + +3. **metadata** (object): 元数据,包含: + - name (string): 范式名称(简短,10字以内) + - description (string): 范式描述(30字以内) + - keyRequirements (array): 核心要求关键词(3-5个) + +【输出格式】 +\`\`\`json +{ + "specializedPrompt": "你是一位资深的...", + "expertGuidelines": [ + {"title": "党内术语规范", "description": "检查是否使用...", "scope": "sentence"}, + {"title": "递进式结构", "description": "检查段落是否符合...", "scope": "paragraph"}, + {"title": "章节完整性", "description": "检查是否包含会前准备...", "scope": "document"}, + ... + ], + "metadata": { + "name": "范式名称", + "description": "范式描述", + "keyRequirements": ["要求1", "要求2", "要求3"] + } +} +\`\`\` + +【注意事项】 +1. specializedPrompt 应该完整、系统,涵盖所有关键要求 +2. expertGuidelines 应该具体、可操作,每条针对一个检查点 +3. 保留原文中的专业术语和标准表述 +4. 如果需求文档提到篇幅要求、格式要求等,务必在 specializedPrompt 中明确体现 +5. **scope 分配原则**: + - sentence 级:适用于任何句子片段(1-5句) + - paragraph 级:需要段落上下文(6-20句) + - document 级:需要完整文档或大段落(21+句) + +请开始分析并生成范式配置。`; +} + +/** + * 解析AI返回的范式配置 + * @param {string} aiResponse - AI返回的文本 + * @returns {Object|null} 解析后的范式配置,失败返回null + */ +export function parseParadigmConfig(aiResponse) { + try { + // 调试:输出原始响应 + console.log('AI 原始响应长度:', aiResponse.length); + console.log('AI 原始响应内容(前500字符):', aiResponse.substring(0, 500)); + + // 检查响应是否为空 + if (!aiResponse || aiResponse.trim().length === 0) { + console.error('AI 返回内容为空'); + return null; + } + + // 尝试提取 JSON 代码块 + const jsonMatch = aiResponse.match(/```json\s*([\s\S]*?)\s*```/); + let jsonText = jsonMatch ? jsonMatch[1] : aiResponse; + + console.log('提取的 JSON 文本(前200字符):', jsonText.substring(0, 200)); + + // 移除可能的注释 + jsonText = jsonText.replace(/\/\*[\s\S]*?\*\//g, '').replace(/\/\/.*/g, ''); + + const config = JSON.parse(jsonText); + + // 验证必需字段 + if (!config.specializedPrompt || !Array.isArray(config.expertGuidelines) || !config.metadata) { + console.error('配置缺少必需字段:', { + hasPrompt: !!config.specializedPrompt, + hasGuidelines: Array.isArray(config.expertGuidelines), + hasMetadata: !!config.metadata + }); + throw new Error('缺少必需字段'); + } + + return config; + } catch (error) { + console.error('解析范式配置失败:', error); + console.error('完整的 AI 响应:', aiResponse); + return null; + } +} + +/** + * 将解析的配置转换为完整的范式对象 + * @param {Object} parsedConfig - 解析后的配置 + * @param {string} sourceDocPath - 源需求文档路径(可选) + * @returns {Object} 完整的范式对象 + */ +export function buildParadigmObject(parsedConfig, sourceDocPath = null) { + const timestamp = Date.now(); + const id = `custom-${timestamp}`; + + return { + id, + name: parsedConfig.metadata.name, + description: parsedConfig.metadata.description, + type: 'custom', // 标记为自定义范式 + createdAt: new Date().toISOString(), + sourceDoc: sourceDocPath, + + specializedPrompt: parsedConfig.specializedPrompt, + expertGuidelines: parsedConfig.expertGuidelines, + + // 可选:继承默认的逻辑范式和维度集 + logicParadigms: null, // 由使用者决定是否继承 + dimensionSetId: null, + defaultReference: null, + + // 元数据 + metadata: { + ...parsedConfig.metadata, + customGenerated: true + } + }; +} + +/** + * 验证范式配置的完整性 + * @param {Object} paradigm - 范式对象 + * @returns {Object} 验证结果 { valid: boolean, errors: string[] } + */ +export function validateParadigm(paradigm) { + const errors = []; + + if (!paradigm.name || paradigm.name.trim().length === 0) { + errors.push('范式名称不能为空'); + } + + if (!paradigm.specializedPrompt || paradigm.specializedPrompt.length < 100) { + errors.push('specializedPrompt 内容过短,建议至少300字'); + } + + if (!Array.isArray(paradigm.expertGuidelines) || paradigm.expertGuidelines.length < 5) { + errors.push('expertGuidelines 至少需要5条指令'); + } + + if (paradigm.expertGuidelines) { + paradigm.expertGuidelines.forEach((guideline, index) => { + // 兼容字符串和对象格式 + if (typeof guideline === 'string') { + if (guideline.trim().length === 0) { + errors.push(`第${index + 1}条指令为空`); + } + } else if (typeof guideline === 'object') { + // 对象格式:检查 description 字段 + if (!guideline.description || guideline.description.trim().length === 0) { + errors.push(`第${index + 1}条指令的描述为空`); + } + if (!guideline.scope || !['sentence', 'paragraph', 'document'].includes(guideline.scope)) { + errors.push(`第${index + 1}条指令的 scope 字段无效`); + } + } else { + errors.push(`第${index + 1}条指令格式错误`); + } + }); + } + + return { + valid: errors.length === 0, + errors + }; +}