From 1a1d7dabdf463d31708b54e99000d84d0ebed857 Mon Sep 17 00:00:00 2001 From: empty Date: Fri, 9 Jan 2026 00:21:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E6=96=87=E7=A8=BF?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E3=80=81=E7=B4=A0=E6=9D=90=E5=BA=93=E3=80=81?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E9=A1=B5=E9=9D=A2=E5=8F=8A=E5=AF=B9=E7=85=A7?= =?UTF-8?q?=E6=A3=80=E6=9F=A5=E9=87=8D=E5=86=99=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 DocumentsPanel.vue 文稿管理页面 - 新增 MaterialsPanel.vue 素材库管理页面 - 新增 SettingsPanel.vue 设置页面 - 新增 DocumentSelectorModal.vue 文稿选择弹窗 - 新增 MaterialSelectorModal.vue 素材选择弹窗 - 集成 SQLite 数据库持久化 (sql.js) - 对照检查页面支持从文稿库选取内容 - 对照检查页面新增一键重写及差异对比功能 - 修复对照检查页面布局问题 - MainContent 支持文稿编辑功能 --- package-lock.json | 21 + package.json | 11 +- src/App.vue | 6 + src/components/AnalysisPanel.vue | 677 +++++++++++++++++- src/components/ComparePanel.vue | 847 +++++++++++++++++++++- src/components/DocumentSelectorModal.vue | 153 ++++ src/components/DocumentsPanel.vue | 273 +++++++ src/components/MainContent.vue | 158 ++++- src/components/MaterialSelectorModal.vue | 117 +++ src/components/MaterialsPanel.vue | 506 +++++++++++++ src/components/SettingsPanel.vue | 317 +++++++++ src/components/WriterPanel.vue | 18 + src/config/dimensionSets.js | 265 +++++++ src/config/logicParadigms.js | 286 ++++++++ src/config/paradigms.js | 318 ++++++++- src/config/references.js | 390 ++++++++++ src/config/sectionTypes.js | 83 +++ src/db/index.js | 859 +++++++++++++++++++++++ src/main.js | 12 + src/stores/app.js | 20 + src/stores/database.js | 246 +++++++ src/utils/textDiff.js | 286 ++++++++ vite.config.js | 3 + 23 files changed, 5808 insertions(+), 64 deletions(-) create mode 100644 src/components/DocumentSelectorModal.vue create mode 100644 src/components/DocumentsPanel.vue create mode 100644 src/components/MaterialSelectorModal.vue create mode 100644 src/components/MaterialsPanel.vue create mode 100644 src/components/SettingsPanel.vue create mode 100644 src/config/dimensionSets.js create mode 100644 src/config/logicParadigms.js create mode 100644 src/config/references.js create mode 100644 src/config/sectionTypes.js create mode 100644 src/db/index.js create mode 100644 src/stores/database.js create mode 100644 src/utils/textDiff.js diff --git a/package-lock.json b/package-lock.json index 85d2fe0..0089fcc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,10 +11,12 @@ "axios": "^1.6.0", "marked": "^9.1.0", "pinia": "^2.1.0", + "sql.js": "^1.13.0", "vue": "^3.4.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.0", + "dotenv": "^16.3.1", "vite": "^5.0.0" } }, @@ -995,6 +997,19 @@ "node": ">=0.4.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -1444,6 +1459,12 @@ "node": ">=0.10.0" } }, + "node_modules/sql.js": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/sql.js/-/sql.js-1.13.0.tgz", + "integrity": "sha512-RJbVP1HRDlUUXahJ7VMTcu9Rm1Nzw+EBpoPr94vnbD4LwR715F3CcxE2G2k45PewcaZ57pjetYa+LoSJLAASgA==", + "license": "MIT" + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", diff --git a/package.json b/package.json index 1ba70d0..83172f9 100644 --- a/package.json +++ b/package.json @@ -8,14 +8,15 @@ "preview": "vite preview" }, "dependencies": { - "vue": "^3.4.0", - "pinia": "^2.1.0", "axios": "^1.6.0", - "marked": "^9.1.0" + "marked": "^9.1.0", + "pinia": "^2.1.0", + "sql.js": "^1.13.0", + "vue": "^3.4.0" }, "devDependencies": { "@vitejs/plugin-vue": "^4.5.0", - "vite": "^5.0.0", - "dotenv": "^16.3.1" + "dotenv": "^16.3.1", + "vite": "^5.0.0" } } diff --git a/src/App.vue b/src/App.vue index 54bd0ba..38b54d3 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,6 +8,9 @@ + + + @@ -20,6 +23,9 @@ import { computed } from 'vue' import { useAppStore } from './stores/app' import WriterPanel from './components/WriterPanel.vue' import AnalysisPanel from './components/AnalysisPanel.vue' +import DocumentsPanel from './components/DocumentsPanel.vue' +import MaterialsPanel from './components/MaterialsPanel.vue' +import SettingsPanel from './components/SettingsPanel.vue' import MainContent from './components/MainContent.vue' import ComparePanel from './components/ComparePanel.vue' diff --git a/src/components/AnalysisPanel.vue b/src/components/AnalysisPanel.vue index 50d7599..87294ed 100644 --- a/src/components/AnalysisPanel.vue +++ b/src/components/AnalysisPanel.vue @@ -20,7 +20,15 @@
-

📚 写作范式库

+
+

📚 写作范式库

+ +
NEW + + 自定义 + - +
+ + + +

{{ paradigm.description }}

@@ -105,27 +133,628 @@
+ + +
+
+
+

{{ isAddMode ? '新增写作范式' : '编辑写作范式' }}

+ +
+ +
+ +
+ +
+ +
+
+ + +
+ + +
+ + +
+ + +
+ + +
+ + +
+ + {{ tag }} + +
+
+ + +
+ +
+ +
+
+ + +
+ + +
+ + +
+
+ + +
+ +
+ +
+ + +
+ + +
+ +
+
+
+ + +
+ +
+
+ +
+ + +
+

选择一个维度集模板,或手动添加维度

+ +
+
+
+ + +
+
+ + +
+ +
+ +
+ 自动匹配相关素材 + +
+ + +
+ +
+ +
+
+ + +
+
+
+ {{ ref.title }} + +
+
{{ ref.excerptCount }} 条可引用内容
+
+
+ + +
+ 已选择 {{ editForm.selectedRefs.length }} 个素材 +
+
+
+
+ +
+ + +
+
+
+ + diff --git a/src/components/DocumentsPanel.vue b/src/components/DocumentsPanel.vue new file mode 100644 index 0000000..2cc8d17 --- /dev/null +++ b/src/components/DocumentsPanel.vue @@ -0,0 +1,273 @@ + + + diff --git a/src/components/MainContent.vue b/src/components/MainContent.vue index 4a36781..a870ee1 100644 --- a/src/components/MainContent.vue +++ b/src/components/MainContent.vue @@ -17,8 +17,9 @@
- {{ currentPage === 'writer' ? '输出预览' : '范式分析结果' }} + {{ headerTitle }} +
字数: {{ generatedContent.length }} {{ isGenerating ? '(生成中...)' : '' }} @@ -32,6 +33,20 @@
+ +
+ + 字数: {{ documentContent.length }} + +
+ + +
+
@@ -142,6 +157,62 @@ + +
+
+ 📄 +

在左侧选择文稿进行编辑

+
+ +
+ +
+ +
+ + +
+ +
+ + +
+
+ 状态: {{ currentDocument.status === 'draft' ? '草稿' : currentDocument.status === 'published' ? '已发布' : '已归档' }} + 创建: {{ formatDate(currentDocument.created_at) }} +
+
+ + + +
+
+
+
+
@@ -218,7 +289,7 @@ diff --git a/src/components/MaterialsPanel.vue b/src/components/MaterialsPanel.vue new file mode 100644 index 0000000..058a6bf --- /dev/null +++ b/src/components/MaterialsPanel.vue @@ -0,0 +1,506 @@ + + + diff --git a/src/components/SettingsPanel.vue b/src/components/SettingsPanel.vue new file mode 100644 index 0000000..aabf682 --- /dev/null +++ b/src/components/SettingsPanel.vue @@ -0,0 +1,317 @@ + + + diff --git a/src/components/WriterPanel.vue b/src/components/WriterPanel.vue index eaa59da..40ab62c 100644 --- a/src/components/WriterPanel.vue +++ b/src/components/WriterPanel.vue @@ -12,12 +12,30 @@ > 写作范式 + + +
Pro版 diff --git a/src/config/dimensionSets.js b/src/config/dimensionSets.js new file mode 100644 index 0000000..8d4a780 --- /dev/null +++ b/src/config/dimensionSets.js @@ -0,0 +1,265 @@ +// ============================================ +// 维度配置库 - 可变的维度插槽 (The Model Layer) +// ============================================ +// 这些是"靶子",可以根据不同的政策要求动态切换 + +export const DIMENSION_SETS = { + // 传统五维度(思想、政治、作风、能力、纪律) + 'five-roots': { + id: 'five-roots', + name: '传统五维度', + description: '思想、政治、作风、能力、纪律五个根本层面', + applicableFor: ['原因剖析', '自我检视'], + dimensions: [ + { + id: 'thought', + name: '思想层面', + focus: '理想信念、宗旨意识、理论武装、世界观人生观价值观', + keywords: ['理论武装不够深入', '理想信念有所动摇', '宗旨意识不够牢固', '总开关拧得不紧'], + // 反向关键词:常见问题表述 + negativeKeywords: ['理论学习碎片化', '学用脱节', '浮光掠影', '走马观花', '学而不思'], + // 正向对标:应达到的标准 + positiveBenchmark: '学思用贯通、知信行统一,筑牢信仰之基、补足精神之钙、把稳思想之舵' + }, + { + id: 'political', + name: '政治层面', + focus: '政治站位、两个维护、政治敏锐性、政治判断力、政治执行力', + keywords: ['政治站位不够高', '政治敏锐性不强', '政治三力有待提升'], + negativeKeywords: ['上有政策下有对策', '选择性执行', '本位主义', '表态多调门少'], + positiveBenchmark: '始终同党中央保持高度一致,做到令行禁止、不打折扣' + }, + { + id: 'style', + name: '作风层面', + focus: '形式主义、官僚主义、实干精神、群众观念、调查研究', + keywords: ['形式主义官僚主义', '脱离群众', '调研不深入', '作风不够扎实'], + negativeKeywords: ['重留痕轻实绩', '以会议落实会议', '以文件落实文件', '指尖上的政绩'], + positiveBenchmark: '真抓实干、勇于担当,深入群众、沈到一线' + }, + { + id: 'capability', + name: '能力层面', + focus: '本领恐慌、新发展理念、攻坚克难、专业能力、创新思维', + keywords: ['本领恐慌', '能力不足', '知识更新不够', '创新意识不强'], + negativeKeywords: ['穿新鞋走老路', '老办法不管用、新办法不会用', '经验主义', '等靠要'], + positiveBenchmark: '勇于攻坚克难、善于研究新情况解决新问题' + }, + { + id: 'discipline', + name: '纪律层面', + focus: '底线思维、廉洁自律、规矩意识、纪法意识', + keywords: ['规矩意识不强', '底线意识有待加强', '自我要求不够严格'], + negativeKeywords: ['打擊边球', '红线意识不强', '侵占群众利益', '纪律观念淡薄'], + positiveBenchmark: '廉洁自律、奉公守法,筑牢拒腐防变的思想防线' + } + ] + }, + + // 五个带头(2025组织生活会版) + 'five-leads': { + id: 'five-leads', + name: '五个带头', + description: '基于二十届四中全会精神的五个带头维度', + applicableFor: ['对照检查', '问题查摆'], + dimensions: [ + { + id: 'political-loyalty', + name: '带头强化政治忠诚、提高政治能力', + focus: '对党忠诚、贯彻落实党中央决策、两个确立、政治三力、战略定力', + keywords: ['政治忠诚', '两个确立', '政治三力', '战略定力'], + negativeKeywords: ['上有政策下有对策', '选择性执行', '本位主义', '政治站位不高', '表态多调门少'], + positiveBenchmark: '始终同党中央保持高度一致,坚决做到“两个维护”' + }, + { + id: 'party-nature', + name: '带头固本培元、增强党性', + focus: '世界观人生观价值观“总开关”、党章党规、党性修养', + keywords: ['总开关', '党性锤炼', '固本培元', '初心使命'], + negativeKeywords: ['理论学习碎片化', '学用脱节', '党性锻炼不经常', '精神懈怠'], + positiveBenchmark: '学思用贯通、知信行统一,筑牢信仰之基' + }, + { + id: 'reverence', + name: '带头敬畏人民、敬畏组织、敬畏法纪', + focus: '“四下基层”、民主集中制、特权思想、群众路线', + keywords: ['敬畏之心', '四下基层', '民主集中制', '群众路线'], + negativeKeywords: ['高高在上', '脱离群众', '容8气十足', '特权思想', '家长制作风'], + positiveBenchmark: '始终心怀敬畏、严守底线,走好新时代群众路线' + }, + { + id: 'responsibility', + name: '带头干事创业、担当作为', + focus: '政绩观、新发展理念、“十五五”开局、防风险、斗争精神', + keywords: ['担当作为', '政绩观', '斗争精神', '攻坚克难'], + negativeKeywords: ['求稳怕错', '不敢攻坚', '等靠要', '观望心态', '拈轻怕重'], + positiveBenchmark: '敢于较真碰硬、勇于担当作为,以斜斗精神攻坚克难' + }, + { + id: 'party-governance', + name: '带头坚决抗起管党治党责任', + focus: '一岗双责、形式主义减负、公文抄袭整治、选人用人', + keywords: ['管党治党', '一岗双责', '从严治党', '选人用人'], + negativeKeywords: ['好人主义', '下不为例', '影子工程', '带病提拔', '管理松懈'], + positiveBenchmark: '层层压实责任、从严从实抵党,营造风清气正的政治生态' + } + ] + }, + + // 四个对照(民主生活会版) + 'four-contrasts': { + id: 'four-contrasts', + name: '四个对照', + description: '对照党章党规、对照初心使命、对照先进典型、对照群众期盼', + applicableFor: ['对照检查', '自我批评'], + dimensions: [ + { + id: 'party-rules', + name: '对照党章党规', + focus: '党员义务、组织纪律、廉洁纪律、工作纪律', + keywords: ['党章党规', '党员义务', '纪律规矩'] + }, + { + id: 'original-mission', + name: '对照初心使命', + focus: '为人民服务、群众利益、宗旨意识', + keywords: ['初心使命', '为民情怀', '宗旨意识'] + }, + { + id: 'role-models', + name: '对照先进典型', + focus: '榜样力量、先进事迹、标杆作用', + keywords: ['先进典型', '榜样力量', '对标对表'] + }, + { + id: 'public-expectations', + name: '对照群众期盼', + focus: '群众满意度、民生问题、服务质量', + keywords: ['群众期盼', '民生福祉', '群众满意'] + } + ] + }, + + // 六大领域(综合性述职报告版) + 'six-domains': { + id: 'six-domains', + name: '六大领域', + description: '政治建设、思想建设、组织建设、作风建设、纪律建设、制度建设', + applicableFor: ['述职报告', '工作总结'], + dimensions: [ + { + id: 'political-construction', + name: '政治建设', + focus: '政治站位、贯彻落实、意识形态', + keywords: ['政治建设', '意识形态', '政治站位'] + }, + { + id: 'ideological-construction', + name: '思想建设', + focus: '理论学习、思想教育、理想信念', + keywords: ['思想建设', '理论武装', '理想信念'] + }, + { + id: 'organizational-construction', + name: '组织建设', + focus: '组织力、战斗力、党员管理', + keywords: ['组织建设', '战斗堡垒', '党员管理'] + }, + { + id: 'style-construction', + name: '作风建设', + focus: '八项规定、形式主义官僚主义、群众路线', + keywords: ['作风建设', '八项规定', '群众路线'] + }, + { + id: 'discipline-construction', + name: '纪律建设', + focus: '纪律教育、监督执纪、警示教育', + keywords: ['纪律建设', '监督执纪', '警示教育'] + }, + { + id: 'system-construction', + name: '制度建设', + focus: '制度执行、规范化建设、长效机制', + keywords: ['制度建设', '制度执行', '长效机制'] + } + ] + } +} + +// 获取所有维度集列表 +export const getDimensionSetList = () => { + return Object.values(DIMENSION_SETS) +} + +// 根据ID获取维度集 +export const getDimensionSetById = (id) => { + return DIMENSION_SETS[id] || null +} + +// 根据适用场景筛选维度集 +export const getDimensionSetsByApplicable = (scene) => { + return Object.values(DIMENSION_SETS).filter(ds => + ds.applicableFor.includes(scene) + ) +} + +// 合并默认维度集与自定义维度 +export const mergeDimensions = (dimensionSetId, customDimensions = []) => { + const defaultSet = DIMENSION_SETS[dimensionSetId] + if (!defaultSet) return customDimensions + + // 如果没有自定义维度,返回默认维度 + if (!customDimensions || customDimensions.length === 0) { + return defaultSet.dimensions + } + + // 合并:自定义维度覆盖同ID的默认维度,新增的追加到末尾 + const result = [...defaultSet.dimensions] + + customDimensions.forEach(custom => { + const existingIndex = result.findIndex(d => d.id === custom.id) + if (existingIndex !== -1) { + // 覆盖已有维度 + result[existingIndex] = { ...result[existingIndex], ...custom } + } else { + // 新增维度 + result.push(custom) + } + }) + + return result +} + +// 格式化维度为Prompt文本 +export const formatDimensionsForPrompt = (dimensions) => { + return dimensions.map((d, i) => { + let text = `${i + 1}. **${d.name}**` + if (d.focus) { + text += `\n - 关注重点:${d.focus}` + } + if (d.keywords?.length) { + text += `\n - 关键词库:${d.keywords.join('、')}` + } + return text + }).join('\n') +} + +// 从本地存储加载自定义维度集 +export const loadCustomDimensionSets = () => { + const saved = localStorage.getItem('customDimensionSets') + return saved ? JSON.parse(saved) : {} +} + +// 保存自定义维度集到本地存储 +export const saveCustomDimensionSet = (id, dimensionSet) => { + const existing = loadCustomDimensionSets() + existing[id] = dimensionSet + localStorage.setItem('customDimensionSets', JSON.stringify(existing)) +} + +// 获取所有维度集(包含自定义) +export const getAllDimensionSets = () => { + const customSets = loadCustomDimensionSets() + return { ...DIMENSION_SETS, ...customSets } +} diff --git a/src/config/logicParadigms.js b/src/config/logicParadigms.js new file mode 100644 index 0000000..f1f0b77 --- /dev/null +++ b/src/config/logicParadigms.js @@ -0,0 +1,286 @@ +// ============================================ +// 逻辑范式库 - 不变的写作逻辑内核 (The Controller Layer) +// ============================================ +// 这些是"射击姿势",无论靶子(维度)如何变化,逻辑保持不变 + +export const LOGIC_PARADIGMS = { + // 深度归因分析法 - 用于原因剖析 + 'deep-attribution': { + id: 'deep-attribution', + name: '深度归因分析法', + description: '从"现象描述"到"本质探究"的跨越,采取归因分析法进行深度溯源', + applicableSection: '原因剖析', + + // 四层递归逻辑 (Universal Kernel) - 含标准化句式模板 + layers: [ + { + id: 'phenomenon', + name: '现象锚定', + question: '这种问题具体表现为什么行为?', + examples: ['拖延推诶', '不敢担责', '只喊口号不落实'], + // 标准化句式模板(可直接嵌入Prompt) + sentenceTemplate: '在{维度名称}方面,存在{具体行为偏差}的问题,具体表现为{1-2个具象案例/场景};' + }, + { + id: 'causality', + name: '归因溯源', + question: '为什么会有这种行为?', + examples: ['怕出事', '没学透', '私心杂念'], + sentenceTemplate: '究其根源,是对{维度核心要求}的认识存在偏差,本质上是{思想/认知/态度层面的问题};' + }, + { + id: 'mapping', + name: '价值映射', + question: '这反映了什么深层缺失?(连接到世界观、人生观、价值观)', + examples: ['党性不纯', '宗旨意识淡薄', '理想信念动摇'], + sentenceTemplate: '这反映出世界观、人生观、价值观这个“总开关”拧得不紧,{党性/宗旨/纪律}意识树得不牢;' + }, + { + id: 'closing', + name: '闭环回扣', + question: '这种缺失导致了什么后果?', + examples: ['阻碍了发展', '损害了形象', '贻误了工作'], + sentenceTemplate: '最终导致{工作成效/政治效果/群众口碑}方面出现{具体负面影响},与{上级要求/政策导向}存在差距。' + } + ], + + // 结构公式 + structureFormula: '[根源定位] + [具体阐释]', + + // 写作原则(常量) + principles: [ + { + title: '拒绝表面化', + description: '不要只停留在"做了什么"或"没做什么",必须挖掘"大脑里在想什么"' + }, + { + title: '触及总开关', + description: '所有的不足,最终都要追溯到"世界观、人生观、价值观"的偏差,或者"党性修养"的弱化' + }, + { + title: '因果闭环', + description: '每条原因必须与问题形成严密的因果对应关系' + } + ], + + // 语言风格规范 + languageStyle: { + sentencePattern: '采用"定性判断 + 具体表现 + 深层归因"的结构', + vocabulary: ['总开关拧得不紧', '思想防线不牢', '政绩观偏差', '党性锤炼不够严格', '理论武装不够深入'], + logic: '体现"从现象到本质"的跨越' + } + }, + + // 递进式问题查摆法 - 用于存在问题部分 + 'progressive-problem': { + id: 'progressive-problem', + name: '递进式问题查摆法', + description: '采用三段递进结构进行问题查摆,不可省略任何一环', + applicableSection: '存在问题', + + // 三段递进结构 + layers: [ + { + id: 'qualification', + name: '定性判断', + question: '用一句话概括问题的本质', + examples: ['理论学习的深度不够', '担当意识不够强', '调查研究不够深入'] + }, + { + id: 'manifestation', + name: '具体表现', + question: '这个问题具体表现在哪些方面?', + examples: ['满足于读通报、看标题,缺乏系统钻研', '遇到矛盾绕着走'] + }, + { + id: 'consequence', + name: '后果/例证', + question: '导致了什么后果或有什么具体例证?', + examples: ['导致在指导具体工作时,出现"本领恐慌"', '使得群众反映的问题久拖不决'] + } + ], + + structureFormula: '[定性判断] + [具体表现] + [后果/例证分析]', + + principles: [ + { + title: '画像精准', + description: '问题查摆真正做到"个人对照",紧密结合分管领域和日常工作' + }, + { + title: '避免空泛', + description: '避免"集体病"表述,必须有具体的岗位、职责、事件描述' + }, + { + title: '不可省略', + description: '三段结构不可省略任何一环,每一环都是必要的' + } + ], + + languageStyle: { + sentencePattern: '采用"定性判断 + 具体表现 + 后果/例证"的递进结构', + vocabulary: ['学用脱节', '上接天线、下接地气', '好人主义', '本领恐慌', '红脸出汗', '刀刃向内'], + moderators: ['有待', '不够', '有所'], // 批评用语谦抑 + connectors: ['导致', '造成', '使得', '以至于', '从而'] // 后果连接词 + } + }, + + // 整改措施法 - 用于整改部分 + 'remediation': { + id: 'remediation', + name: '整改措施法', + description: '采用问题-措施对应结构,包含可量化指标,语气坚定', + applicableSection: '整改措施', + + layers: [ + { + id: 'target', + name: '针对问题', + question: '明确指出针对哪个问题?' + }, + { + id: 'action', + name: '具体措施', + question: '采取什么具体行动?' + }, + { + id: 'indicator', + name: '量化指标', + question: '如何衡量整改效果?' + } + ], + + structureFormula: '[针对问题] + [具体措施] + [量化指标]', + + principles: [ + { + title: '一一对应', + description: '每条措施必须与前文的问题一一对应,形成完整闭环' + }, + { + title: '可量化', + description: '包含可量化指标(如:每月精读X篇、每季度调研X次)' + }, + { + title: '语气坚定', + description: '使用坚定动词:持续用力、筑牢、压实、锤炼' + } + ], + + languageStyle: { + vocabulary: ['持续用力', '筑牢', '压实', '锤炼', '久久为功', '一以贯之'], + quantifiers: ['每月', '每季度', '每年', '至少X次', '不少于X篇'] + } + }, + + // 典型案例剖析法 - 用于案例分析 + 'case-study': { + id: 'case-study', + name: '典型案例剖析法', + description: '采用"以案说德、以案说纪、以案说法、以案说责"四维剖析', + applicableSection: '典型案例', + + layers: [ + { + id: 'case-virtue', + name: '以案说德', + question: '这个案例在道德层面反映了什么问题?' + }, + { + id: 'case-discipline', + name: '以案说纪', + question: '这个案例在纪律层面违反了哪些规定?' + }, + { + id: 'case-law', + name: '以案说法', + question: '这个案例在法规层面触犯了什么条例?' + }, + { + id: 'case-responsibility', + name: '以案说责', + question: '这个案例在责任层面涉及哪些失职?' + } + ], + + structureFormula: '[案例概述] + [四维剖析] + [举一反三]', + + principles: [ + { + title: '四维完整', + description: '德、纪、法、责四个维度必须全覆盖' + }, + { + title: '举一反三', + description: '必须联系自身查摆类似风险点' + } + ] + } +} + +// 获取逻辑范式列表 +export const getLogicParadigmList = () => { + return Object.values(LOGIC_PARADIGMS) +} + +// 根据ID获取逻辑范式 +export const getLogicParadigmById = (id) => { + return LOGIC_PARADIGMS[id] || null +} + +// 生成逻辑范式的Prompt片段 +export const buildLogicPrompt = (logicId, dimensions = []) => { + const logic = LOGIC_PARADIGMS[logicId] + if (!logic) return '' + + let prompt = `\n## ${logic.name}\n` + prompt += `${logic.description}\n\n` + + // 添加结构公式 + prompt += `### 结构公式\n` + prompt += `${logic.structureFormula}\n\n` + + // 添加层次说明 + prompt += `### 分析层次\n` + logic.layers.forEach((layer, i) => { + prompt += `${i + 1}. **${layer.name}**:${layer.question}\n` + if (layer.examples?.length) { + prompt += ` 示例:${layer.examples.join('、')}\n` + } + }) + prompt += '\n' + + // 添加原则 + prompt += `### 必须遵循的原则\n` + logic.principles.forEach((p, i) => { + prompt += `${i + 1}. **${p.title}**:${p.description}\n` + }) + prompt += '\n' + + // 添加语言规范 + if (logic.languageStyle) { + prompt += `### 语言规范\n` + if (logic.languageStyle.sentencePattern) { + prompt += `- 句式:${logic.languageStyle.sentencePattern}\n` + } + if (logic.languageStyle.vocabulary?.length) { + prompt += `- 术语库:${logic.languageStyle.vocabulary.join('、')}\n` + } + if (logic.languageStyle.moderators?.length) { + prompt += `- 程度词(批评用语谦抑):${logic.languageStyle.moderators.join('、')}\n` + } + if (logic.languageStyle.connectors?.length) { + prompt += `- 后果连接词:${logic.languageStyle.connectors.join('、')}\n` + } + } + + // 如果提供了维度,添加维度配置 + if (dimensions.length > 0) { + prompt += `\n### 本次任务的维度配置\n` + dimensions.forEach((d, i) => { + prompt += `${i + 1}. **${d.name}**:${d.focus || d.description || ''}\n` + }) + } + + return prompt +} diff --git a/src/config/paradigms.js b/src/config/paradigms.js index d037062..7054825 100644 --- a/src/config/paradigms.js +++ b/src/config/paradigms.js @@ -1,6 +1,13 @@ // ============================================ // 写作范式配置库 - 包含专家指令和评价量表 // ============================================ +// 重构说明:范式现在采用"组合模式",引用逻辑范式和维度集 +// - 逻辑范式 (logicParadigms.js): 不变的写作逻辑内核 +// - 维度集 (dimensionSets.js): 可变的维度插槽 + +import { getLogicParadigmById, buildLogicPrompt } from './logicParadigms.js' +import { getDimensionSetById, mergeDimensions, formatDimensionsForPrompt } from './dimensionSets.js' +import { autoMatchReferences, formatReferencesForPrompt, getReferenceById } from './references.js' export const PARADIGMS = { // 民主生活会对照检查材料 @@ -12,6 +19,18 @@ export const PARADIGMS = { tags: ['开篇引言', '对照查摆', '根源剖析', '整改措施', '表态'], tagClass: 'bg-red-900/30 text-red-300', + // ===== 新架构:组合模式配置 ===== + // 各章节使用的逻辑范式 + logicParadigms: { + problemSection: 'progressive-problem', // 存在问题 → 递进式问题查摆法 + analysisSection: 'deep-attribution', // 原因剖析 → 深度归因分析法 + remediationSection: 'remediation' // 整改措施 → 整改措施法 + }, + // 默认使用的维度集 + dimensionSetId: 'five-roots', // 默认:思想/政治/作风/能力/纪律 + // 用户自定义维度覆盖(可选) + customDimensions: null, + // 默认参考范文路径 defaultReference: { title: '高质量对照检查材料范本', @@ -179,6 +198,16 @@ export const PARADIGMS = { tagClass: 'bg-red-900/30 text-red-300', isNew: true, // 标记为最新版本 + // ===== 新架构:组合模式配置 ===== + logicParadigms: { + problemSection: 'progressive-problem', // 存在问题 → 递进式问题查摆法 + analysisSection: 'deep-attribution', // 原因剖析 → 深度归因分析法 + caseSection: 'case-study', // 典型案例 → 典型案例剖析法 + remediationSection: 'remediation' // 整改措施 → 整改措施法 + }, + dimensionSetId: 'five-leads', // 默认:五个带头 + customDimensions: null, + // 默认参考范文 defaultReference: { title: '2025组织生活会高质量范本', @@ -429,6 +458,13 @@ export const PARADIGMS = { tags: ['问题引入', '解决方案', '代码示例', '总结'], tagClass: 'bg-blue-900/30 text-blue-300', + // ===== 新架构:组合模式配置 ===== + logicParadigms: { + problemSection: 'progressive-problem' + }, + dimensionSetId: null, // 技术博客不使用固定维度集 + customDimensions: null, + defaultReference: { title: '技术博客风格范本', content: '本文深入探讨了...(此处省略2000字,这是为了让AI模仿这种干练的技术风格)...' @@ -471,6 +507,11 @@ export const PARADIGMS = { tags: ['背景介绍', '数据支撑', '趋势分析', '建议'], tagClass: 'bg-green-900/30 text-green-300', + // ===== 新架构:组合模式配置 ===== + logicParadigms: {}, + dimensionSetId: null, + customDimensions: null, + defaultReference: null, expertGuidelines: [ @@ -510,6 +551,11 @@ export const PARADIGMS = { tags: ['痛点切入', '价值主张', '功能亮点', '行动号召'], tagClass: 'bg-purple-900/30 text-purple-300', + // ===== 新架构:组合模式配置 ===== + logicParadigms: {}, + dimensionSetId: null, + customDimensions: null, + defaultReference: null, expertGuidelines: [ @@ -549,6 +595,11 @@ export const PARADIGMS = { tags: ['摘要', '引言', '文献综述', '方法论', '结果'], tagClass: 'bg-orange-900/30 text-orange-300', + // ===== 新架构:组合模式配置 ===== + logicParadigms: {}, + dimensionSetId: null, + customDimensions: null, + defaultReference: null, expertGuidelines: [ @@ -588,6 +639,14 @@ export const PARADIGMS = { tags: ['工作回顾', '成绩总结', '问题分析', '下步计划'], tagClass: 'bg-cyan-900/30 text-cyan-300', + // ===== 新架构:组合模式配置 ===== + logicParadigms: { + problemSection: 'progressive-problem', + analysisSection: 'deep-attribution' + }, + dimensionSetId: 'six-domains', // 六大领域 + customDimensions: null, + defaultReference: null, expertGuidelines: [ @@ -637,7 +696,7 @@ export const buildParadigmConstraints = (paradigmId) => { let constraints = `\n# 专家级写作标准 (${paradigm.name})\n` constraints += `请严格遵循以下专家评价标准:\n\n` - paradigm.expertGuidelines.forEach((g, idx) => { + paradigm.expertGuidelines?.forEach((g, idx) => { constraints += `${idx + 1}. 【${g.title}】${g.description}\n` }) @@ -650,3 +709,260 @@ export const buildParadigmConstraints = (paradigmId) => { return constraints } + +// ============================================ +// 新架构:动态维度注入引擎 +// ============================================ + +/** + * 构建完整的动态 Prompt(核心函数) + * 将逻辑范式与维度配置组合,生成最终的 System Prompt + * + * @param {string} paradigmId - 范式ID + * @param {string} section - 章节类型 ('problem'|'analysis'|'remediation'|'case') + * @param {Array} customDimensions - 用户自定义维度(可选,覆盖默认维度) + * @returns {string} 生成的 Prompt 片段 + */ +export const buildDynamicPrompt = (paradigmId, section = 'analysis', customDimensions = null) => { + const paradigm = PARADIGMS[paradigmId] + if (!paradigm) return '' + + // 1. 确定使用的逻辑范式 + const sectionMap = { + 'problem': 'problemSection', + 'analysis': 'analysisSection', + 'remediation': 'remediationSection', + 'case': 'caseSection' + } + const logicId = paradigm.logicParadigms?.[sectionMap[section]] + if (!logicId) { + // 如果没有配置逻辑范式,回退到传统方式 + return buildParadigmConstraints(paradigmId) + } + + // 2. 获取逻辑范式 + const logic = getLogicParadigmById(logicId) + if (!logic) return '' + + // 3. 获取维度配置 + let dimensions = [] + if (customDimensions && customDimensions.length > 0) { + // 使用用户自定义维度 + dimensions = customDimensions + } else if (paradigm.dimensionSetId) { + // 使用默认维度集,可能与用户自定义合并 + dimensions = mergeDimensions(paradigm.dimensionSetId, paradigm.customDimensions) + } + + // 4. 构建动态 Prompt + let prompt = `\n# ${logic.name} - 写作逻辑指南\n` + prompt += `${logic.description}\n\n` + + // 添加结构公式 + prompt += `## 结构公式\n` + prompt += `**${logic.structureFormula}**\n\n` + + // 添加层次说明 + prompt += `## 分析层次(必须遵循)\n` + logic.layers.forEach((layer, i) => { + prompt += `${i + 1}. **${layer.name}**:${layer.question}\n` + if (layer.examples?.length) { + prompt += ` - 示例:${layer.examples.join('、')}\n` + } + }) + prompt += '\n' + + // 添加原则 + prompt += `## 必须遵循的原则\n` + logic.principles.forEach((p, i) => { + prompt += `${i + 1}. **${p.title}**:${p.description}\n` + }) + prompt += '\n' + + // 添加语言规范 + if (logic.languageStyle) { + prompt += `## 语言规范\n` + if (logic.languageStyle.sentencePattern) { + prompt += `- **句式**:${logic.languageStyle.sentencePattern}\n` + } + if (logic.languageStyle.vocabulary?.length) { + prompt += `- **术语库**:${logic.languageStyle.vocabulary.join('、')}\n` + } + if (logic.languageStyle.moderators?.length) { + prompt += `- **批评用语**(谦抑):${logic.languageStyle.moderators.join('、')}\n` + } + if (logic.languageStyle.connectors?.length) { + prompt += `- **后果连接词**:${logic.languageStyle.connectors.join('、')}\n` + } + prompt += '\n' + } + + // 添加标准化句式模板(如果有) + if (logic.layers?.some(l => l.sentenceTemplate)) { + prompt += `## 标准化句式模板\n` + prompt += `请按照以下句式结构组织语言:\n\n` + logic.layers.forEach((layer, i) => { + if (layer.sentenceTemplate) { + prompt += `${i + 1}. **${layer.name}**:\`${layer.sentenceTemplate}\`\n` + } + }) + prompt += '\n' + } + + // 添加维度配置(增强版:含反向关键词和正向对标) + if (dimensions.length > 0) { + prompt += `## 本次任务的维度配置\n` + prompt += `请严格按照以下维度进行分析:\n\n` + + dimensions.forEach((d, i) => { + prompt += `### ${i + 1}. ${d.name}\n` + if (d.focus) { + prompt += `- **关注重点**:${d.focus}\n` + } + if (d.negativeKeywords?.length) { + prompt += `- **常见问题表述**(可引用):${d.negativeKeywords.join('、')}\n` + } + if (d.positiveBenchmark) { + prompt += `- **正向对标**:${d.positiveBenchmark}\n` + } + prompt += '\n' + }) + + prompt += `**注意**:每个维度都必须覆盖,不可遗漏。\n\n` + } + + // 添加政治校准与风格统一规则(强制) + prompt += `## 政治校准与风格统一(强制规则)\n` + prompt += `1. **政治校准**:所有剖析内容必须紧扣最新政策表述(如“两个确立”“两个维护”“新发展理念”),禁止使用与党内规范表述相悖的词汇;\n` + prompt += `2. **风格统一**:整体语言风格为“自我批评式”,语气诚恳、不回避问题,避免“轻描淡写”或“过度上纲上线”,保持“有血有肉、不浮于表面”的尺度;\n` + prompt += `3. **禁止套话**:不得输出空泛的“集体病”表述,必须结合具体岗位、职责、事件进行剖析。\n\n` + + return prompt +} + +/** + * 构建带素材引用的完整 Prompt + * 在基础 buildDynamicPrompt 之上添加素材引用 + * + * @param {string} paradigmId - 范式ID + * @param {string} section - 章节类型 + * @param {Object} options - 配置选项 + * @param {Array} options.customDimensions - 自定义维度 + * @param {Array} options.referenceIds - 指定的素材ID数组 + * @param {boolean} options.autoMatchRefs - 是否自动匹配素材 + * @param {number} options.maxRefsPerDimension - 每个维度最多匹配素材数 + * @returns {string} 完整的 Prompt + */ +export const buildPromptWithReferences = (paradigmId, section = 'analysis', options = {}) => { + const { + customDimensions = null, + referenceIds = [], + autoMatchRefs = true, + maxRefsPerDimension = 2 + } = options + + // 1. 获取基础 Prompt + let prompt = buildDynamicPrompt(paradigmId, section, customDimensions) + if (!prompt) return '' + + // 2. 获取维度配置(用于自动匹配) + const paradigm = PARADIGMS[paradigmId] + let dimensions = [] + if (customDimensions && customDimensions.length > 0) { + dimensions = customDimensions + } else if (paradigm?.dimensionSetId) { + dimensions = mergeDimensions(paradigm.dimensionSetId, paradigm.customDimensions) + } + + // 3. 处理素材引用 + let referencesToUse = [] + + // 3.1 指定的素材 + if (referenceIds.length > 0) { + referencesToUse = referenceIds.map(id => getReferenceById(id)).filter(Boolean) + } + + // 3.2 自动匹配素材 + if (autoMatchRefs && dimensions.length > 0) { + const matchedRefs = autoMatchReferences(dimensions, maxRefsPerDimension) + if (matchedRefs.length > 0) { + prompt += `\n## 智能匹配的参考素材\n` + prompt += `以下素材与本次写作维度相关,可适当引用:\n\n` + + matchedRefs.forEach(ref => { + prompt += `### ${ref.topic}(关联维度:${ref.dimensionName})\n` + prompt += `> 来源:${ref.referenceTitle}\n` + prompt += `> “${ref.content}”\n` + if (ref.useFor) { + prompt += `> 【${ref.useFor === 'positive' ? '正面案例' : '反面案例'}】\n` + } + prompt += '\n' + }) + } + } + + // 3.3 添加指定的素材 + if (referencesToUse.length > 0) { + prompt += formatReferencesForPrompt(referencesToUse, { + includeSource: true, + maxExcerpts: 5 + }) + } + + return prompt +} + +/** + * 获取范式的维度配置(用于UI展示和编辑) + * + * @param {string} paradigmId - 范式ID + * @returns {Object} 包含维度集信息和维度列表 + */ +export const getParadigmDimensions = (paradigmId) => { + const paradigm = PARADIGMS[paradigmId] + if (!paradigm) return { dimensionSet: null, dimensions: [] } + + const dimensionSet = paradigm.dimensionSetId + ? getDimensionSetById(paradigm.dimensionSetId) + : null + + const dimensions = paradigm.dimensionSetId + ? mergeDimensions(paradigm.dimensionSetId, paradigm.customDimensions) + : paradigm.customDimensions || [] + + return { + dimensionSetId: paradigm.dimensionSetId, + dimensionSet, + dimensions, + customDimensions: paradigm.customDimensions + } +} + +/** + * 更新范式的自定义维度(运行时修改,不持久化到配置文件) + * + * @param {string} paradigmId - 范式ID + * @param {Array} newDimensions - 新的维度列表 + */ +export const updateParadigmDimensions = (paradigmId, newDimensions) => { + const paradigm = PARADIGMS[paradigmId] + if (!paradigm) return false + + paradigm.customDimensions = newDimensions + return true +} + +/** + * 切换范式使用的维度集 + * + * @param {string} paradigmId - 范式ID + * @param {string} newDimensionSetId - 新的维度集ID + */ +export const switchParadigmDimensionSet = (paradigmId, newDimensionSetId) => { + const paradigm = PARADIGMS[paradigmId] + if (!paradigm) return false + + paradigm.dimensionSetId = newDimensionSetId + paradigm.customDimensions = null // 切换维度集时清空自定义维度 + return true +} diff --git a/src/config/references.js b/src/config/references.js new file mode 100644 index 0000000..7f083e0 --- /dev/null +++ b/src/config/references.js @@ -0,0 +1,390 @@ +// ============================================ +// 素材库 - 可引用的政策文件、讲话、案例等 +// ============================================ +// 这些是"弹药",可以根据写作需要动态注入到Prompt中 + +// 素材类型枚举 +export const REFERENCE_TYPES = { + POLICY: 'policy', // 政策文件 + SPEECH: 'speech', // 领导讲话 + CASE: 'case', // 典型案例 + DATA: 'data', // 数据统计 + QUOTE: 'quote', // 金句警句 + REGULATION: 'regulation' // 党规党纪 +} + +// 素材库 +export const REFERENCES = { + // ==================== 政策文件 ==================== + 'policy-20th-4th': { + id: 'policy-20th-4th', + type: REFERENCE_TYPES.POLICY, + title: '二十届四中全会精神', + source: '中国共产党第二十届中央委员会第四次全体会议公报', + date: '2024-07', + tags: ['全面深化改革', '中国式现代化', '制度建设'], + // 可引用的关键段落 + excerpts: [ + { + id: 'excerpt-1', + topic: '改革总目标', + content: '进一步全面深化改革的总目标是继续完善和发展中国特色社会主义制度,推进国家治理体系和治理能力现代化。', + applicableDimensions: ['political-loyalty', 'thought'] + }, + { + id: 'excerpt-2', + topic: '党的领导', + content: '坚持党中央对进一步全面深化改革的集中统一领导,深刻领悟"两个确立"的决定性意义,增强"四个意识"、坚定"四个自信"、做到"两个维护"。', + applicableDimensions: ['political-loyalty', 'party-nature'] + } + ], + // 关联的维度集 + relatedDimensionSets: ['five-leads', 'five-roots'] + }, + + 'policy-discipline-education': { + id: 'policy-discipline-education', + type: REFERENCE_TYPES.POLICY, + title: '党纪学习教育', + source: '中共中央办公厅关于在全党开展党纪学习教育的通知', + date: '2024-04', + tags: ['党纪学习', '纪律建设', '从严治党'], + excerpts: [ + { + id: 'excerpt-1', + topic: '学习重点', + content: '以学习贯彻《中国共产党纪律处分条例》为重点,教育引导党员干部学纪、知纪、明纪、守纪。', + applicableDimensions: ['discipline', 'party-governance'] + }, + { + id: 'excerpt-2', + topic: '目标要求', + content: '搞清楚党的纪律规矩是什么,弄明白能干什么、不能干什么,把遵规守纪刻印在心,内化为言行准则。', + applicableDimensions: ['discipline', 'reverence'] + } + ], + relatedDimensionSets: ['five-leads', 'five-roots'] + }, + + // ==================== 领导讲话 ==================== + 'speech-strictparty': { + id: 'speech-strictparty', + type: REFERENCE_TYPES.SPEECH, + title: '全面从严治党重要论述', + source: '习近平总书记系列重要讲话', + tags: ['从严治党', '党的建设', '自我革命'], + excerpts: [ + { + id: 'excerpt-1', + topic: '自我革命', + content: '勇于自我革命,是我们党最鲜明的品格,也是我们党最大的优势。', + applicableDimensions: ['party-nature', 'party-governance'] + }, + { + id: 'excerpt-2', + topic: '作风建设', + content: '作风问题本质上是党性问题,抓作风建设,就要返璞归真、固本培元,重点突出坚定理想信念、践行根本宗旨、加强道德修养。', + applicableDimensions: ['style', 'party-nature'] + }, + { + id: 'excerpt-3', + topic: '政绩观', + content: '树立和践行正确政绩观,起决定性作用的是党性。只有党性坚强、摒弃私心杂念,才能保证政绩观不出偏差。', + applicableDimensions: ['responsibility', 'party-nature'] + } + ], + relatedDimensionSets: ['five-leads', 'five-roots'] + }, + + 'speech-investigation': { + id: 'speech-investigation', + type: REFERENCE_TYPES.SPEECH, + title: '调查研究重要论述', + source: '习近平总书记关于调查研究的重要讲话', + tags: ['调查研究', '作风建设', '实事求是'], + excerpts: [ + { + id: 'excerpt-1', + topic: '调研方法', + content: '调查研究是谋事之基、成事之道,没有调查就没有发言权,没有调查就没有决策权。', + applicableDimensions: ['style', 'capability'] + }, + { + id: 'excerpt-2', + topic: '四下基层', + content: '要大力弘扬"四下基层"优良传统,推动各级干部深入基层、深入实际、深入群众。', + applicableDimensions: ['reverence', 'style'] + } + ], + relatedDimensionSets: ['five-leads'] + }, + + // ==================== 典型案例 ==================== + 'case-formalism': { + id: 'case-formalism', + type: REFERENCE_TYPES.CASE, + title: '形式主义典型案例', + source: '中央纪委国家监委通报', + tags: ['形式主义', '官僚主义', '作风问题'], + excerpts: [ + { + id: 'excerpt-1', + topic: '文山会海', + content: '某地区层层加码开会发文,基层干部疲于应付,大量时间消耗在文来文往、会来会往中,真正用于服务群众的时间被严重挤压。', + applicableDimensions: ['style', 'party-governance'], + useFor: 'negative' // 反面案例 + }, + { + id: 'excerpt-2', + topic: '痕迹管理', + content: '某单位过度强调工作留痕,要求拍照打卡、层层签字,基层干部戏称"工作没干完,材料摞成山",重"痕"不重"绩"。', + applicableDimensions: ['style'], + useFor: 'negative' + } + ], + relatedDimensionSets: ['five-roots'] + }, + + 'case-responsibility': { + id: 'case-responsibility', + type: REFERENCE_TYPES.CASE, + title: '担当作为典型案例', + source: '先进典型事迹汇编', + tags: ['担当作为', '攻坚克难', '先进典型'], + excerpts: [ + { + id: 'excerpt-1', + topic: '攻坚克难', + content: '某同志面对历史遗留的信访积案,不推诿、不绕道,主动担责、迎难而上,经过三个月的艰苦努力,成功化解了长达十年的矛盾纠纷。', + applicableDimensions: ['responsibility'], + useFor: 'positive' // 正面案例 + } + ], + relatedDimensionSets: ['five-leads'] + }, + + // ==================== 金句警句 ==================== + 'quote-party-spirit': { + id: 'quote-party-spirit', + type: REFERENCE_TYPES.QUOTE, + title: '党性修养金句', + tags: ['党性', '修养', '自省'], + excerpts: [ + { + id: 'excerpt-1', + topic: '总开关', + content: '世界观、人生观、价值观是"总开关",拧紧了这个"总开关",才能把准政治方向。', + applicableDimensions: ['thought', 'party-nature'] + }, + { + id: 'excerpt-2', + topic: '初心使命', + content: '走得再远、走到再光辉的未来,也不能忘记走过的过去,不能忘记为什么出发。', + applicableDimensions: ['party-nature', 'original-mission'] + }, + { + id: 'excerpt-3', + topic: '敬畏之心', + content: '心有所畏,方能言有所戒、行有所止。', + applicableDimensions: ['reverence', 'discipline'] + } + ], + relatedDimensionSets: ['five-leads', 'five-roots', 'four-contrasts'] + }, + + // ==================== 党规党纪 ==================== + 'regulation-discipline': { + id: 'regulation-discipline', + type: REFERENCE_TYPES.REGULATION, + title: '《中国共产党纪律处分条例》要点', + source: '2024年修订版', + date: '2024-01', + tags: ['纪律处分', '六大纪律', '底线红线'], + excerpts: [ + { + id: 'excerpt-1', + topic: '政治纪律', + content: '对党不忠诚不老实,表里不一,阳奉阴违,欺上瞒下,搞两面派,做两面人的,给予警告或者严重警告处分。', + applicableDimensions: ['political-loyalty', 'discipline'] + }, + { + id: 'excerpt-2', + topic: '工作纪律', + content: '工作中不负责任或者疏于管理,贯彻执行、检查督促落实上级决策部署不力的,给予警告或者严重警告处分。', + applicableDimensions: ['responsibility', 'party-governance'] + } + ], + relatedDimensionSets: ['five-roots', 'five-leads'] + } +} + +// ============================================ +// 素材库工具函数 +// ============================================ + +/** + * 获取所有素材列表 + */ +export const getReferenceList = () => { + return Object.values(REFERENCES).map(ref => ({ + id: ref.id, + type: ref.type, + title: ref.title, + source: ref.source, + tags: ref.tags, + excerptCount: ref.excerpts?.length || 0 + })) +} + +/** + * 根据ID获取素材 + */ +export const getReferenceById = (id) => { + return REFERENCES[id] || null +} + +/** + * 根据类型筛选素材 + */ +export const getReferencesByType = (type) => { + return Object.values(REFERENCES).filter(ref => ref.type === type) +} + +/** + * 根据标签筛选素材 + */ +export const getReferencesByTag = (tag) => { + return Object.values(REFERENCES).filter(ref => ref.tags?.includes(tag)) +} + +/** + * 根据维度ID获取相关素材 + * @param {string} dimensionId - 维度ID + * @returns {Array} 包含相关素材段落的数组 + */ +export const getReferencesByDimension = (dimensionId) => { + const results = [] + + Object.values(REFERENCES).forEach(ref => { + ref.excerpts?.forEach(excerpt => { + if (excerpt.applicableDimensions?.includes(dimensionId)) { + results.push({ + referenceId: ref.id, + referenceTitle: ref.title, + referenceType: ref.type, + source: ref.source, + ...excerpt + }) + } + }) + }) + + return results +} + +/** + * 根据维度集获取所有相关素材 + * @param {string} dimensionSetId - 维度集ID + * @returns {Array} 素材列表 + */ +export const getReferencesByDimensionSet = (dimensionSetId) => { + return Object.values(REFERENCES).filter(ref => + ref.relatedDimensionSets?.includes(dimensionSetId) + ) +} + +/** + * 格式化素材为Prompt文本 + * @param {Array} references - 素材ID数组或素材对象数组 + * @param {Object} options - 配置选项 + * @returns {string} 格式化后的Prompt片段 + */ +export const formatReferencesForPrompt = (references, options = {}) => { + const { + includeSource = true, + groupByType = false, + maxExcerpts = 5 + } = options + + let prompt = '' + let excerptCount = 0 + + // 处理输入:支持ID数组或对象数组 + const refObjects = references.map(ref => + typeof ref === 'string' ? getReferenceById(ref) : ref + ).filter(Boolean) + + if (refObjects.length === 0) return '' + + prompt += `\n## 可引用素材\n` + prompt += `以下素材可在写作中自然引用或化用:\n\n` + + refObjects.forEach(ref => { + if (excerptCount >= maxExcerpts) return + + ref.excerpts?.forEach(excerpt => { + if (excerptCount >= maxExcerpts) return + + prompt += `### ${excerpt.topic}\n` + if (includeSource) { + prompt += `> 来源:${ref.title}(${ref.source || ''})\n` + } + prompt += `> "${excerpt.content}"\n` + if (excerpt.useFor) { + prompt += `> 【${excerpt.useFor === 'positive' ? '正面案例' : '反面案例'}】\n` + } + prompt += '\n' + excerptCount++ + }) + }) + + prompt += `**引用要求**:自然融入行文,不可生硬堆砌;可适当改写,保留核心要义。\n\n` + + return prompt +} + +/** + * 智能匹配素材(根据维度自动推荐) + * @param {Array} dimensions - 维度配置数组 + * @param {number} maxPerDimension - 每个维度最多推荐数量 + * @returns {Array} 推荐的素材段落 + */ +export const autoMatchReferences = (dimensions, maxPerDimension = 2) => { + const results = [] + const usedExcerptIds = new Set() + + dimensions.forEach(dim => { + const dimRefs = getReferencesByDimension(dim.id) + let count = 0 + + dimRefs.forEach(ref => { + if (count >= maxPerDimension) return + if (usedExcerptIds.has(ref.id)) return + + results.push({ + dimensionId: dim.id, + dimensionName: dim.name, + ...ref + }) + usedExcerptIds.add(ref.id) + count++ + }) + }) + + return results +} + +/** + * 获取素材类型的中文名称 + */ +export const getReferenceTypeName = (type) => { + const names = { + [REFERENCE_TYPES.POLICY]: '政策文件', + [REFERENCE_TYPES.SPEECH]: '领导讲话', + [REFERENCE_TYPES.CASE]: '典型案例', + [REFERENCE_TYPES.DATA]: '数据统计', + [REFERENCE_TYPES.QUOTE]: '金句警句', + [REFERENCE_TYPES.REGULATION]: '党规党纪' + } + return names[type] || '其他' +} diff --git a/src/config/sectionTypes.js b/src/config/sectionTypes.js new file mode 100644 index 0000000..09df892 --- /dev/null +++ b/src/config/sectionTypes.js @@ -0,0 +1,83 @@ +// ============================================ +// 章节类型配置 - 用于段落级范式匹配 +// ============================================ + +export const SECTION_TYPES = { + problem: { + id: 'problem', + label: '存在问题', + color: 'amber', + bgClass: 'bg-amber-600/50', + textClass: 'text-amber-200', + logicKey: 'problemSection', + keywords: ['在...方面', '不够', '有待', '存在', '问题', '不足'], + description: '问题查摆、不足之处,通常采用"定性判断+具体表现+后果"结构' + }, + analysis: { + id: 'analysis', + label: '原因剖析', + color: 'purple', + bgClass: 'bg-purple-600/50', + textClass: 'text-purple-200', + logicKey: 'analysisSection', + keywords: ['究其根源', '根本原因', '深层原因', '思想上', '政治上', '作风上'], + description: '原因分析、根源探究,通常从思想/政治/作风/能力/纪律等维度溯源' + }, + remediation: { + id: 'remediation', + label: '整改措施', + color: 'green', + bgClass: 'bg-green-600/50', + textClass: 'text-green-200', + logicKey: 'remediationSection', + keywords: ['针对', '整改', '措施', '持续', '加强', '提升', '下一步'], + description: '整改方案、改进措施,通常包含具体行动和量化指标' + }, + case: { + id: 'case', + label: '典型案例', + color: 'red', + bgClass: 'bg-red-600/50', + textClass: 'text-red-200', + logicKey: 'caseSection', + keywords: ['案例', '以案', '典型', '警示', '教训'], + description: '案例剖析,通常采用"以案说德/纪/法/责"结构' + }, + intro: { + id: 'intro', + label: '开篇引言', + color: 'slate', + bgClass: 'bg-slate-600/50', + textClass: 'text-slate-200', + logicKey: null, + keywords: ['根据', '按照', '为了', '会议', '要求'], + description: '背景介绍、会议说明等' + }, + conclusion: { + id: 'conclusion', + label: '结尾表态', + color: 'blue', + bgClass: 'bg-blue-600/50', + textClass: 'text-blue-200', + logicKey: null, + keywords: ['决心', '表态', '今后', '将以', '努力'], + description: '表态发言、决心表述等' + } +} + +// 获取所有章节类型列表 +export const getSectionTypeList = () => { + return Object.values(SECTION_TYPES) +} + +// 根据 ID 获取章节类型 +export const getSectionTypeById = (id) => { + return SECTION_TYPES[id] || null +} + +// 获取章节类型的样式类 +export const getSectionTypeClasses = (id) => { + const type = SECTION_TYPES[id] + if (!type) return 'bg-slate-600/50 text-slate-200' + return `${type.bgClass} ${type.textClass}` +} diff --git a/src/db/index.js b/src/db/index.js new file mode 100644 index 0000000..7fcfc99 --- /dev/null +++ b/src/db/index.js @@ -0,0 +1,859 @@ +// ============================================ +// SQLite 数据库服务层 +// ============================================ +// 使用 sql.js 在浏览器端运行 SQLite +// 数据持久化到 IndexedDB + +// 数据库实例 +let db = null +let SQL = null + +// IndexedDB 配置 +const DB_NAME = 'AIWriterWorkshop' +const DB_STORE = 'database' +const DB_KEY = 'sqlite_db' + +// ============================================ +// 数据库初始化 +// ============================================ + +/** + * 从 CDN 加载 sql.js + */ +const loadSqlJs = () => { + return new Promise((resolve, reject) => { + // 如果已经加载过 + if (window.initSqlJs) { + resolve(window.initSqlJs) + return + } + + // 动态加载脚本 + const script = document.createElement('script') + script.src = 'https://sql.js.org/dist/sql-wasm.js' + script.async = true + + script.onload = () => { + if (window.initSqlJs) { + resolve(window.initSqlJs) + } else { + reject(new Error('sql.js 加载失败')) + } + } + + script.onerror = () => reject(new Error('sql.js 脚本加载失败')) + document.head.appendChild(script) + }) +} + +export const initDatabase = async () => { + if (db) return db + + try { + // 从 CDN 加载 sql.js + const initSqlJs = await loadSqlJs() + + // 加载 sql.js WASM + SQL = await initSqlJs({ + locateFile: file => `https://sql.js.org/dist/${file}` + }) + + // 尝试从 IndexedDB 加载现有数据库 + const savedData = await loadFromIndexedDB() + + if (savedData) { + db = new SQL.Database(savedData) + console.log('📦 从 IndexedDB 加载数据库成功') + } else { + db = new SQL.Database() + console.log('🆕 创建新数据库') + // 初始化表结构 + await initTables() + // 导入默认数据 + await importDefaultData() + } + + return db + } catch (error) { + console.error('❌ 数据库初始化失败:', error) + throw error + } +} + +/** + * 初始化数据表结构 + */ +const initTables = async () => { + // 素材表 + db.run(` + CREATE TABLE IF NOT EXISTS materials ( + id TEXT PRIMARY KEY, + type TEXT NOT NULL, + title TEXT NOT NULL, + source TEXT, + date TEXT, + tags TEXT, + related_dimension_sets TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + is_default INTEGER DEFAULT 0 + ) + `) + + // 素材摘录表 + db.run(` + CREATE TABLE IF NOT EXISTS reference_excerpts ( + id TEXT PRIMARY KEY, + reference_id TEXT NOT NULL, + topic TEXT NOT NULL, + content TEXT NOT NULL, + applicable_dimensions TEXT, + use_for TEXT, + FOREIGN KEY (reference_id) REFERENCES materials(id) ON DELETE CASCADE + ) + `) + + // 范式表 + db.run(` + CREATE TABLE IF NOT EXISTS paradigms ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + icon TEXT, + description TEXT, + tags TEXT, + tag_class TEXT, + system_constraints TEXT, + dimension_set_id TEXT, + custom_dimensions TEXT, + logic_paradigms TEXT, + auto_match_refs INTEGER DEFAULT 1, + selected_refs TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + is_custom INTEGER DEFAULT 0 + ) + `) + + // 维度集表 + db.run(` + CREATE TABLE IF NOT EXISTS dimension_sets ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + applicable_for TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP, + is_custom INTEGER DEFAULT 0 + ) + `) + + // 维度表 + db.run(` + CREATE TABLE IF NOT EXISTS dimensions ( + id TEXT PRIMARY KEY, + dimension_set_id TEXT NOT NULL, + name TEXT NOT NULL, + focus TEXT, + keywords TEXT, + negative_keywords TEXT, + positive_benchmark TEXT, + sort_order INTEGER DEFAULT 0, + FOREIGN KEY (dimension_set_id) REFERENCES dimension_sets(id) ON DELETE CASCADE + ) + `) + + // 用户配置表 + db.run(` + CREATE TABLE IF NOT EXISTS user_config ( + key TEXT PRIMARY KEY, + value TEXT, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + + // 分析历史表 + db.run(` + CREATE TABLE IF NOT EXISTS analysis_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + paradigm_id TEXT, + input_text TEXT, + result TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + + // 文稿记录表 + db.run(` + CREATE TABLE IF NOT EXISTS documents ( + id TEXT PRIMARY KEY, + title TEXT NOT NULL, + content TEXT, + paradigm_id TEXT, + dimension_set_id TEXT, + selected_refs TEXT, + status TEXT DEFAULT 'draft', + word_count INTEGER DEFAULT 0, + tags TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP + ) + `) + + // 文稿版本历史表 + db.run(` + CREATE TABLE IF NOT EXISTS document_versions ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + document_id TEXT NOT NULL, + content TEXT, + version_number INTEGER, + change_note TEXT, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (document_id) REFERENCES documents(id) ON DELETE CASCADE + ) + `) + + console.log('✅ 数据表初始化完成') +} + +// ============================================ +// IndexedDB 持久化 +// ============================================ + +/** + * 保存数据库到 IndexedDB + */ +export const saveToIndexedDB = async () => { + if (!db) return + + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1) + + request.onerror = () => reject(request.error) + + request.onupgradeneeded = (event) => { + const idb = event.target.result + if (!idb.objectStoreNames.contains(DB_STORE)) { + idb.createObjectStore(DB_STORE) + } + } + + request.onsuccess = (event) => { + const idb = event.target.result + const transaction = idb.transaction([DB_STORE], 'readwrite') + const store = transaction.objectStore(DB_STORE) + + const data = db.export() + const buffer = new Uint8Array(data) + + const putRequest = store.put(buffer, DB_KEY) + putRequest.onsuccess = () => { + console.log('💾 数据库已保存到 IndexedDB') + resolve() + } + putRequest.onerror = () => reject(putRequest.error) + } + }) +} + +/** + * 从 IndexedDB 加载数据库 + */ +const loadFromIndexedDB = async () => { + return new Promise((resolve, reject) => { + const request = indexedDB.open(DB_NAME, 1) + + request.onerror = () => reject(request.error) + + request.onupgradeneeded = (event) => { + const idb = event.target.result + if (!idb.objectStoreNames.contains(DB_STORE)) { + idb.createObjectStore(DB_STORE) + } + } + + request.onsuccess = (event) => { + const idb = event.target.result + const transaction = idb.transaction([DB_STORE], 'readonly') + const store = transaction.objectStore(DB_STORE) + + const getRequest = store.get(DB_KEY) + getRequest.onsuccess = () => { + resolve(getRequest.result || null) + } + getRequest.onerror = () => resolve(null) + } + }) +} + +// ============================================ +// 通用 CRUD 操作 +// ============================================ + +/** + * 执行查询并返回结果 + */ +export const query = (sql, params = []) => { + if (!db) throw new Error('数据库未初始化') + + try { + const stmt = db.prepare(sql) + stmt.bind(params) + + const results = [] + while (stmt.step()) { + results.push(stmt.getAsObject()) + } + stmt.free() + + return results + } catch (error) { + console.error('查询失败:', sql, error) + throw error + } +} + +/** + * 执行单条查询 + */ +export const queryOne = (sql, params = []) => { + const results = query(sql, params) + return results.length > 0 ? results[0] : null +} + +/** + * 执行更新/插入操作 + */ +export const execute = (sql, params = []) => { + if (!db) throw new Error('数据库未初始化') + + try { + db.run(sql, params) + // 自动保存到 IndexedDB + saveToIndexedDB() + return true + } catch (error) { + console.error('执行失败:', sql, error) + throw error + } +} + +/** + * 批量执行 + */ +export const executeBatch = (statements) => { + if (!db) throw new Error('数据库未初始化') + + try { + statements.forEach(({ sql, params = [] }) => { + db.run(sql, params) + }) + saveToIndexedDB() + return true + } catch (error) { + console.error('批量执行失败:', error) + throw error + } +} + +// ============================================ +// 素材库 CRUD +// ============================================ + +/** + * 获取所有素材 + */ +export const getAllReferences = () => { + const refs = query('SELECT * FROM materials ORDER BY created_at DESC') + + return refs.map(ref => ({ + ...ref, + tags: ref.tags ? JSON.parse(ref.tags) : [], + relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [], + excerpts: getExcerptsByReferenceId(ref.id) + })) +} + +/** + * 根据ID获取素材 + */ +export const getReferenceById = (id) => { + const ref = queryOne('SELECT * FROM materials WHERE id = ?', [id]) + if (!ref) return null + + return { + ...ref, + tags: ref.tags ? JSON.parse(ref.tags) : [], + relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [], + excerpts: getExcerptsByReferenceId(ref.id) + } +} + +/** + * 根据类型获取素材 + */ +export const getReferencesByType = (type) => { + const refs = query('SELECT * FROM materials WHERE type = ? ORDER BY created_at DESC', [type]) + + return refs.map(ref => ({ + ...ref, + tags: ref.tags ? JSON.parse(ref.tags) : [], + relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [], + excerpts: getExcerptsByReferenceId(ref.id) + })) +} + +/** + * 获取素材的摘录 + */ +const getExcerptsByReferenceId = (referenceId) => { + const excerpts = query('SELECT * FROM reference_excerpts WHERE reference_id = ?', [referenceId]) + + return excerpts.map(e => ({ + ...e, + applicableDimensions: e.applicable_dimensions ? JSON.parse(e.applicable_dimensions) : [] + })) +} + +/** + * 添加素材 + */ +export const addReference = (reference) => { + const id = reference.id || `ref-${Date.now()}` + + execute(` + INSERT INTO materials (id, type, title, source, date, tags, related_dimension_sets, is_default) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + `, [ + id, + reference.type, + reference.title, + reference.source || null, + reference.date || null, + JSON.stringify(reference.tags || []), + JSON.stringify(reference.relatedDimensionSets || []), + reference.isDefault ? 1 : 0 + ]) + + // 添加摘录 + if (reference.excerpts?.length) { + reference.excerpts.forEach((excerpt, index) => { + execute(` + INSERT INTO reference_excerpts (id, reference_id, topic, content, applicable_dimensions, use_for) + VALUES (?, ?, ?, ?, ?, ?) + `, [ + excerpt.id || `${id}-excerpt-${index}`, + id, + excerpt.topic, + excerpt.content, + JSON.stringify(excerpt.applicableDimensions || []), + excerpt.useFor || null + ]) + }) + } + + return id +} + +/** + * 更新素材 + */ +export const updateReference = (id, updates) => { + const setClauses = [] + const params = [] + + if (updates.type !== undefined) { + setClauses.push('type = ?') + params.push(updates.type) + } + if (updates.title !== undefined) { + setClauses.push('title = ?') + params.push(updates.title) + } + if (updates.source !== undefined) { + setClauses.push('source = ?') + params.push(updates.source) + } + if (updates.tags !== undefined) { + setClauses.push('tags = ?') + params.push(JSON.stringify(updates.tags)) + } + if (updates.relatedDimensionSets !== undefined) { + setClauses.push('related_dimension_sets = ?') + params.push(JSON.stringify(updates.relatedDimensionSets)) + } + + setClauses.push('updated_at = CURRENT_TIMESTAMP') + params.push(id) + + execute(`UPDATE materials SET ${setClauses.join(', ')} WHERE id = ?`, params) + + return true +} + +/** + * 删除素材 + */ +export const deleteReference = (id) => { + execute('DELETE FROM reference_excerpts WHERE reference_id = ?', [id]) + execute('DELETE FROM materials WHERE id = ?', [id]) + return true +} + +// ============================================ +// 范式 CRUD +// ============================================ + +/** + * 获取所有范式 + */ +export const getAllParadigms = () => { + const paradigms = query('SELECT * FROM paradigms ORDER BY is_custom ASC, created_at DESC') + + return paradigms.map(p => ({ + ...p, + tags: p.tags ? JSON.parse(p.tags) : [], + systemConstraints: p.system_constraints ? JSON.parse(p.system_constraints) : [], + customDimensions: p.custom_dimensions ? JSON.parse(p.custom_dimensions) : null, + logicParadigms: p.logic_paradigms ? JSON.parse(p.logic_paradigms) : null, + selectedRefs: p.selected_refs ? JSON.parse(p.selected_refs) : [], + isCustom: p.is_custom === 1, + autoMatchRefs: p.auto_match_refs === 1 + })) +} + +/** + * 添加范式 + */ +export const addParadigm = (paradigm) => { + const id = paradigm.id || `paradigm-${Date.now()}` + + execute(` + INSERT INTO paradigms (id, name, icon, description, tags, tag_class, system_constraints, + dimension_set_id, custom_dimensions, logic_paradigms, auto_match_refs, selected_refs, is_custom) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + id, + paradigm.name, + paradigm.icon || '📝', + paradigm.description || null, + JSON.stringify(paradigm.tags || []), + paradigm.tagClass || 'bg-blue-900/30 text-blue-300', + JSON.stringify(paradigm.systemConstraints || []), + paradigm.dimensionSetId || null, + paradigm.customDimensions ? JSON.stringify(paradigm.customDimensions) : null, + paradigm.logicParadigms ? JSON.stringify(paradigm.logicParadigms) : null, + paradigm.autoMatchRefs !== false ? 1 : 0, + JSON.stringify(paradigm.selectedRefs || []), + paradigm.isCustom ? 1 : 0 + ]) + + return id +} + +/** + * 更新范式 + */ +export const updateParadigm = (id, updates) => { + const setClauses = [] + const params = [] + + const fieldMap = { + name: 'name', + icon: 'icon', + description: 'description', + tagClass: 'tag_class', + dimensionSetId: 'dimension_set_id', + autoMatchRefs: 'auto_match_refs' + } + + Object.entries(updates).forEach(([key, value]) => { + if (fieldMap[key]) { + setClauses.push(`${fieldMap[key]} = ?`) + params.push(key === 'autoMatchRefs' ? (value ? 1 : 0) : value) + } + }) + + if (updates.tags !== undefined) { + setClauses.push('tags = ?') + params.push(JSON.stringify(updates.tags)) + } + if (updates.systemConstraints !== undefined) { + setClauses.push('system_constraints = ?') + params.push(JSON.stringify(updates.systemConstraints)) + } + if (updates.customDimensions !== undefined) { + setClauses.push('custom_dimensions = ?') + params.push(updates.customDimensions ? JSON.stringify(updates.customDimensions) : null) + } + if (updates.selectedRefs !== undefined) { + setClauses.push('selected_refs = ?') + params.push(JSON.stringify(updates.selectedRefs)) + } + + setClauses.push('updated_at = CURRENT_TIMESTAMP') + params.push(id) + + execute(`UPDATE paradigms SET ${setClauses.join(', ')} WHERE id = ?`, params) + + return true +} + +/** + * 删除范式 + */ +export const deleteParadigm = (id) => { + execute('DELETE FROM paradigms WHERE id = ?', [id]) + return true +} + +// ============================================ +// 用户配置 CRUD +// ============================================ + +/** + * 获取配置 + */ +export const getConfig = (key, defaultValue = null) => { + const result = queryOne('SELECT value FROM user_config WHERE key = ?', [key]) + if (!result) return defaultValue + + try { + return JSON.parse(result.value) + } catch { + return result.value + } +} + +/** + * 设置配置 + */ +export const setConfig = (key, value) => { + const valueStr = typeof value === 'string' ? value : JSON.stringify(value) + + execute(` + INSERT INTO user_config (key, value, updated_at) + VALUES (?, ?, CURRENT_TIMESTAMP) + ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP + `, [key, valueStr, valueStr]) + + return true +} + +// ============================================ +// 文稿 CRUD +// ============================================ + +/** + * 获取所有文稿 + */ +export const getAllDocuments = () => { + const docs = query('SELECT * FROM documents ORDER BY updated_at DESC') + + return docs.map(doc => ({ + ...doc, + tags: doc.tags ? JSON.parse(doc.tags) : [], + selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : [] + })) +} + +/** + * 根据ID获取文稿 + */ +export const getDocumentById = (id) => { + const doc = queryOne('SELECT * FROM documents WHERE id = ?', [id]) + if (!doc) return null + + return { + ...doc, + tags: doc.tags ? JSON.parse(doc.tags) : [], + selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : [], + versions: getDocumentVersions(id) + } +} + +/** + * 获取文稿版本历史 + */ +export const getDocumentVersions = (documentId) => { + return query('SELECT * FROM document_versions WHERE document_id = ? ORDER BY version_number DESC', [documentId]) +} + +/** + * 创建文稿 + */ +export const createDocument = (document) => { + const id = document.id || `doc-${Date.now()}` + + execute(` + INSERT INTO documents (id, title, content, paradigm_id, dimension_set_id, selected_refs, status, word_count, tags) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + `, [ + id, + document.title || '未命名文稿', + document.content || '', + document.paradigmId || null, + document.dimensionSetId || null, + JSON.stringify(document.selectedRefs || []), + document.status || 'draft', + document.wordCount || 0, + JSON.stringify(document.tags || []) + ]) + + return id +} + +/** + * 更新文稿 + */ +export const updateDocument = (id, updates) => { + const setClauses = [] + const params = [] + + if (updates.title !== undefined) { + setClauses.push('title = ?') + params.push(updates.title) + } + if (updates.content !== undefined) { + setClauses.push('content = ?') + params.push(updates.content) + // 自动计算字数 + setClauses.push('word_count = ?') + params.push(updates.content.length) + } + if (updates.paradigmId !== undefined) { + setClauses.push('paradigm_id = ?') + params.push(updates.paradigmId) + } + if (updates.status !== undefined) { + setClauses.push('status = ?') + params.push(updates.status) + } + if (updates.tags !== undefined) { + setClauses.push('tags = ?') + params.push(JSON.stringify(updates.tags)) + } + if (updates.selectedRefs !== undefined) { + setClauses.push('selected_refs = ?') + params.push(JSON.stringify(updates.selectedRefs)) + } + + setClauses.push('updated_at = CURRENT_TIMESTAMP') + params.push(id) + + execute(`UPDATE documents SET ${setClauses.join(', ')} WHERE id = ?`, params) + + return true +} + +/** + * 保存文稿版本 + */ +export const saveDocumentVersion = (documentId, content, changeNote = '') => { + // 获取当前最大版本号 + const result = queryOne('SELECT MAX(version_number) as max_version FROM document_versions WHERE document_id = ?', [documentId]) + const nextVersion = (result?.max_version || 0) + 1 + + execute(` + INSERT INTO document_versions (document_id, content, version_number, change_note) + VALUES (?, ?, ?, ?) + `, [documentId, content, nextVersion, changeNote]) + + return nextVersion +} + +/** + * 删除文稿 + */ +export const deleteDocument = (id) => { + execute('DELETE FROM document_versions WHERE document_id = ?', [id]) + execute('DELETE FROM documents WHERE id = ?', [id]) + return true +} + +/** + * 根据状态筛选文稿 + */ +export const getDocumentsByStatus = (status) => { + const docs = query('SELECT * FROM documents WHERE status = ? ORDER BY updated_at DESC', [status]) + + return docs.map(doc => ({ + ...doc, + tags: doc.tags ? JSON.parse(doc.tags) : [], + selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : [] + })) +} + +// ============================================ +// 数据导入/导出 +// ============================================ + +/** + * 导出数据库为二进制 + */ +export const exportDatabase = () => { + if (!db) throw new Error('数据库未初始化') + return db.export() +} + +/** + * 导入数据库 + */ +export const importDatabase = async (data) => { + if (!SQL) throw new Error('SQL.js 未初始化') + + db = new SQL.Database(new Uint8Array(data)) + await saveToIndexedDB() + + return true +} + +/** + * 导出为 JSON + */ +export const exportAsJSON = () => { + return { + references: getAllReferences(), + paradigms: getAllParadigms(), + documents: getAllDocuments(), + config: query('SELECT * FROM user_config'), + exportedAt: new Date().toISOString() + } +} + +/** + * 重置数据库 + */ +export const resetDatabase = async () => { + if (!SQL) throw new Error('SQL.js 未初始化') + + db = new SQL.Database() + await initTables() + await importDefaultData() + await saveToIndexedDB() + + return true +} + +// ============================================ +// 默认数据导入 +// ============================================ + +/** + * 导入默认数据(从静态配置文件) + */ +const importDefaultData = async () => { + // 导入默认素材 + const { REFERENCES } = await import('../config/references.js') + + Object.values(REFERENCES).forEach(ref => { + addReference({ + ...ref, + isDefault: true + }) + }) + + console.log('✅ 默认素材导入完成') +} + +// 导出数据库实例(用于调试) +export const getDb = () => db diff --git a/src/main.js b/src/main.js index 764d958..4b79f79 100644 --- a/src/main.js +++ b/src/main.js @@ -1,9 +1,21 @@ import { createApp } from 'vue' import { createPinia } from 'pinia' import App from './App.vue' +import { useDatabaseStore } from './stores/database.js' const app = createApp(App) const pinia = createPinia() app.use(pinia) + +// 初始化数据库 +const initApp = async () => { + const dbStore = useDatabaseStore() + await dbStore.initialize() + console.log('🚀 应用初始化完成') +} + app.mount('#app') + +// 应用挂载后初始化数据库 +initApp() diff --git a/src/stores/app.js b/src/stores/app.js index 43fa38e..f238461 100644 --- a/src/stores/app.js +++ b/src/stores/app.js @@ -35,6 +35,9 @@ export const useAppStore = defineStore('app', () => { const activeParadigm = ref(null) // 当前激活的范式配置 const expertGuidelines = ref([]) // 专家指令列表 + // 当前编辑的文稿 + const currentDocument = ref(null) + // 质检报告(深度模式) const qualityReport = ref(null) // { checks: [{key, title, status, message}], overall: 'pass'|'warning'|'fail' } @@ -333,6 +336,20 @@ ${draft} const switchPage = (page) => { currentPage.value = page } + + // 设置当前页面(别名) + const setCurrentPage = (page) => { + currentPage.value = page + } + + // 设置当前文稿(用于从文稿列表打开编辑) + const setCurrentDocument = (doc) => { + if (doc) { + inputTask.value = doc.content || '' + generatedContent.value = doc.content || '' + // 可以扩展更多字段 + } + } // 加载范式预设 const loadParadigmPreset = (paradigmId) => { @@ -423,6 +440,7 @@ ${draft} analysisResult, isAnalyzing, styleAnalysis, + currentDocument, showPromptDebug, showRefInput, newRefTitle, @@ -430,6 +448,8 @@ ${draft} // 方法 switchPage, + setCurrentPage, + setCurrentDocument, addReferenceFromAnalysis, generateContentAction, analyzeArticleAction, diff --git a/src/stores/database.js b/src/stores/database.js new file mode 100644 index 0000000..c5bc295 --- /dev/null +++ b/src/stores/database.js @@ -0,0 +1,246 @@ +// ============================================ +// 数据库状态管理 Store +// ============================================ + +import { defineStore } from 'pinia' +import { ref, computed } from 'vue' +import { + initDatabase, + getAllReferences, + getReferenceById as dbGetReferenceById, + getReferencesByType as dbGetReferencesByType, + addReference as dbAddReference, + updateReference as dbUpdateReference, + deleteReference as dbDeleteReference, + getAllParadigms, + addParadigm as dbAddParadigm, + updateParadigm as dbUpdateParadigm, + deleteParadigm as dbDeleteParadigm, + getConfig, + setConfig, + exportAsJSON, + resetDatabase, + saveToIndexedDB +} from '../db/index.js' + +export const useDatabaseStore = defineStore('database', () => { + // ============================================ + // 状态 + // ============================================ + + const isInitialized = ref(false) + const isLoading = ref(false) + const error = ref(null) + + // 素材数据 + const references = ref([]) + + // 范式数据 + const paradigms = ref([]) + + // ============================================ + // 初始化 + // ============================================ + + const initialize = async () => { + if (isInitialized.value) return true + + isLoading.value = true + error.value = null + + try { + await initDatabase() + + // 加载所有数据 + await refreshReferences() + await refreshParadigms() + + isInitialized.value = true + console.log('✅ 数据库 Store 初始化完成') + return true + } catch (err) { + error.value = err.message + console.error('❌ 数据库初始化失败:', err) + return false + } finally { + isLoading.value = false + } + } + + // ============================================ + // 素材库操作 + // ============================================ + + const refreshReferences = async () => { + references.value = getAllReferences() + } + + const getReferenceById = (id) => { + return references.value.find(r => r.id === id) || dbGetReferenceById(id) + } + + const getReferencesByType = (type) => { + if (type === 'all') return references.value + return references.value.filter(r => r.type === type) + } + + const getReferencesByDimension = (dimensionId) => { + const results = [] + + references.value.forEach(ref => { + ref.excerpts?.forEach(excerpt => { + if (excerpt.applicableDimensions?.includes(dimensionId)) { + results.push({ + referenceId: ref.id, + referenceTitle: ref.title, + referenceType: ref.type, + source: ref.source, + ...excerpt + }) + } + }) + }) + + return results + } + + const addReference = async (reference) => { + const id = dbAddReference(reference) + await refreshReferences() + return id + } + + const updateReference = async (id, updates) => { + dbUpdateReference(id, updates) + await refreshReferences() + } + + const deleteReference = async (id) => { + dbDeleteReference(id) + await refreshReferences() + } + + // ============================================ + // 范式操作 + // ============================================ + + const refreshParadigms = async () => { + paradigms.value = getAllParadigms() + } + + const getParadigmById = (id) => { + return paradigms.value.find(p => p.id === id) + } + + const addParadigm = async (paradigm) => { + const id = dbAddParadigm(paradigm) + await refreshParadigms() + return id + } + + const updateParadigm = async (id, updates) => { + dbUpdateParadigm(id, updates) + await refreshParadigms() + } + + const deleteParadigm = async (id) => { + dbDeleteParadigm(id) + await refreshParadigms() + } + + // ============================================ + // 配置操作 + // ============================================ + + const getUserConfig = (key, defaultValue = null) => { + return getConfig(key, defaultValue) + } + + const setUserConfig = (key, value) => { + return setConfig(key, value) + } + + // ============================================ + // 数据导入导出 + // ============================================ + + const exportData = () => { + return exportAsJSON() + } + + const reset = async () => { + await resetDatabase() + await refreshReferences() + await refreshParadigms() + } + + const save = async () => { + await saveToIndexedDB() + } + + // ============================================ + // 计算属性 + // ============================================ + + const referenceCount = computed(() => references.value.length) + const paradigmCount = computed(() => paradigms.value.length) + const customParadigmCount = computed(() => paradigms.value.filter(p => p.isCustom).length) + + const referencesByType = computed(() => { + const grouped = {} + references.value.forEach(ref => { + if (!grouped[ref.type]) { + grouped[ref.type] = [] + } + grouped[ref.type].push(ref) + }) + return grouped + }) + + // ============================================ + // 返回 + // ============================================ + + return { + // 状态 + isInitialized, + isLoading, + error, + references, + paradigms, + + // 初始化 + initialize, + + // 素材操作 + refreshReferences, + getReferenceById, + getReferencesByType, + getReferencesByDimension, + addReference, + updateReference, + deleteReference, + + // 范式操作 + refreshParadigms, + getParadigmById, + addParadigm, + updateParadigm, + deleteParadigm, + + // 配置操作 + getUserConfig, + setUserConfig, + + // 导入导出 + exportData, + reset, + save, + + // 计算属性 + referenceCount, + paradigmCount, + customParadigmCount, + referencesByType + } +}) diff --git a/src/utils/textDiff.js b/src/utils/textDiff.js new file mode 100644 index 0000000..e47869e --- /dev/null +++ b/src/utils/textDiff.js @@ -0,0 +1,286 @@ +/** + * 文本差异计算工具 + * 用于对比原文和重写内容,生成差异片段 + */ + +/** + * 将文本按句子拆分,并记录每个句子的位置信息 + * @param {string} text - 输入文本 + * @returns {Array<{text: string, start: number, end: number}>} - 句子数组(含位置) + */ +export const splitIntoSentencesWithPosition = (text) => { + if (!text) return [] + + const sentences = [] + const regex = /[^。!?;\n]+[。!?;\n]*/g + let match + + while ((match = regex.exec(text)) !== null) { + const sentence = match[0].trim() + if (sentence) { + sentences.push({ + text: sentence, + start: match.index, + end: match.index + match[0].length + }) + } + } + + return sentences +} + +/** + * 将文本按句子拆分(简单版本,不含位置) + * @param {string} text - 输入文本 + * @returns {string[]} - 句子数组 + */ +export const splitIntoSentences = (text) => { + return splitIntoSentencesWithPosition(text).map(s => s.text) +} + +/** + * 计算两个字符串的相似度(基于 Levenshtein 距离) + * @param {string} s1 - 字符串1 + * @param {string} s2 - 字符串2 + * @returns {number} - 相似度 0-1 + */ +export const similarity = (s1, s2) => { + if (!s1 && !s2) return 1 + if (!s1 || !s2) return 0 + + const longer = s1.length > s2.length ? s1 : s2 + const shorter = s1.length > s2.length ? s2 : s1 + + if (longer.length === 0) return 1 + + // 简化版:使用字符重叠率 + const set1 = new Set(s1) + const set2 = new Set(s2) + const intersection = [...set1].filter(c => set2.has(c)).length + const union = new Set([...set1, ...set2]).size + + return intersection / union +} + +/** + * 计算两段文本的差异(含位置信息) + * @param {string} original - 原文 + * @param {string} rewritten - 重写后的文本 + * @returns {Array<{type: 'unchanged'|'modified'|'added'|'removed', original: string, rewritten: string, idx: number, originalStart?: number, originalEnd?: number}>} + */ +export const computeDiff = (original, rewritten) => { + const originalSentences = splitIntoSentencesWithPosition(original) + const rewrittenSentences = splitIntoSentencesWithPosition(rewritten) + + const diff = [] + let oIdx = 0 + let rIdx = 0 + let diffIdx = 0 + + // 使用简单的贪心匹配算法 + while (oIdx < originalSentences.length || rIdx < rewrittenSentences.length) { + const oItem = originalSentences[oIdx] + const rItem = rewrittenSentences[rIdx] + const oSentence = oItem?.text + const rSentence = rItem?.text + + if (oIdx >= originalSentences.length) { + // 原文已结束,剩余都是新增 + diff.push({ + type: 'added', + original: '', + rewritten: rSentence, + idx: diffIdx++, + // 新增内容插入到最后一个原文句子之后 + insertAfterIdx: originalSentences.length > 0 ? originalSentences[originalSentences.length - 1].end : 0 + }) + rIdx++ + continue + } + + if (rIdx >= rewrittenSentences.length) { + // 重写已结束,剩余都是删除 + diff.push({ + type: 'removed', + original: oSentence, + rewritten: '', + idx: diffIdx++, + originalStart: oItem.start, + originalEnd: oItem.end + }) + oIdx++ + continue + } + + // 计算相似度 + const sim = similarity(oSentence, rSentence) + + if (sim > 0.8) { + // 高度相似,视为未修改 + diff.push({ + type: 'unchanged', + original: oSentence, + rewritten: rSentence, + idx: diffIdx++, + originalStart: oItem.start, + originalEnd: oItem.end + }) + oIdx++ + rIdx++ + } else if (sim > 0.4) { + // 中等相似,视为修改 + diff.push({ + type: 'modified', + original: oSentence, + rewritten: rSentence, + idx: diffIdx++, + originalStart: oItem.start, + originalEnd: oItem.end + }) + oIdx++ + rIdx++ + } else { + // 低相似度,尝试向前查找匹配 + let foundMatch = false + + // 在重写文本中向前查找原句的匹配 + for (let i = rIdx + 1; i < Math.min(rIdx + 3, rewrittenSentences.length); i++) { + if (similarity(oSentence, rewrittenSentences[i].text) > 0.6) { + // 找到匹配,中间的都是新增 + for (let j = rIdx; j < i; j++) { + diff.push({ + type: 'added', + original: '', + rewritten: rewrittenSentences[j].text, + idx: diffIdx++, + insertAfterIdx: oItem.start + }) + } + rIdx = i + foundMatch = true + break + } + } + + if (!foundMatch) { + // 在原文中向前查找重写句的匹配 + for (let i = oIdx + 1; i < Math.min(oIdx + 3, originalSentences.length); i++) { + if (similarity(originalSentences[i].text, rSentence) > 0.6) { + // 找到匹配,中间的都是删除 + for (let j = oIdx; j < i; j++) { + diff.push({ + type: 'removed', + original: originalSentences[j].text, + rewritten: '', + idx: diffIdx++, + originalStart: originalSentences[j].start, + originalEnd: originalSentences[j].end + }) + } + oIdx = i + foundMatch = true + break + } + } + } + + if (!foundMatch) { + // 没找到匹配,视为修改 + diff.push({ + type: 'modified', + original: oSentence, + rewritten: rSentence, + idx: diffIdx++, + originalStart: oItem.start, + originalEnd: oItem.end + }) + oIdx++ + rIdx++ + } + } + } + + return diff +} + +/** + * 根据选中的修改应用差异,在原文中精确替换 + * @param {string} original - 原文 + * @param {Array} diffSegments - 差异片段(含位置信息) + * @param {Set} acceptedChanges - 接受的修改索引集合 + * @returns {string} - 精确替换后的文本 + */ +export const applySelectedChanges = (original, diffSegments, acceptedChanges) => { + // 收集所有需要执行的替换操作 + const operations = [] + + for (const segment of diffSegments) { + const isAccepted = acceptedChanges.has(segment.idx) + + if (segment.type === 'modified' && isAccepted) { + // 修改:替换原文中的对应位置 + if (segment.originalStart !== undefined && segment.originalEnd !== undefined) { + operations.push({ + type: 'replace', + start: segment.originalStart, + end: segment.originalEnd, + content: segment.rewritten + }) + } + } else if (segment.type === 'removed' && isAccepted) { + // 删除:移除原文中的对应位置 + if (segment.originalStart !== undefined && segment.originalEnd !== undefined) { + operations.push({ + type: 'delete', + start: segment.originalStart, + end: segment.originalEnd + }) + } + } else if (segment.type === 'added' && isAccepted) { + // 新增:在指定位置插入 + if (segment.insertAfterIdx !== undefined) { + operations.push({ + type: 'insert', + position: segment.insertAfterIdx, + content: segment.rewritten + }) + } + } + } + + // 按位置从后往前排序(避免位置偏移) + operations.sort((a, b) => { + const posA = a.start !== undefined ? a.start : a.position + const posB = b.start !== undefined ? b.start : b.position + return posB - posA + }) + + // 执行替换操作 + let result = original + for (const op of operations) { + if (op.type === 'replace') { + result = result.slice(0, op.start) + op.content + result.slice(op.end) + } else if (op.type === 'delete') { + result = result.slice(0, op.start) + result.slice(op.end) + } else if (op.type === 'insert') { + result = result.slice(0, op.position) + op.content + result.slice(op.position) + } + } + + return result +} + +/** + * 获取差异统计 + * @param {Array} diffSegments - 差异片段 + * @returns {{total: number, modified: number, added: number, removed: number}} + */ +export const getDiffStats = (diffSegments) => { + return { + total: diffSegments.length, + modified: diffSegments.filter(s => s.type === 'modified').length, + added: diffSegments.filter(s => s.type === 'added').length, + removed: diffSegments.filter(s => s.type === 'removed').length, + unchanged: diffSegments.filter(s => s.type === 'unchanged').length + } +} diff --git a/vite.config.js b/vite.config.js index 4915aa6..171bd53 100644 --- a/vite.config.js +++ b/vite.config.js @@ -14,6 +14,9 @@ export default defineConfig(({ mode }) => { define: { // 将环境变量暴露给客户端 __APP_ENV__: JSON.stringify(env) + }, + build: { + target: 'esnext' } } })