feat: 添加文章修改保存功能及首页入口
This commit is contained in:
78
server/db.js
78
server/db.js
@@ -233,6 +233,29 @@ const seedDefaultReferences = async () => {
|
|||||||
|
|
||||||
await seedDefaultReferences();
|
await seedDefaultReferences();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 同步现有的提纲素材到通用素材库
|
||||||
|
*/
|
||||||
|
const syncExistingOutlineMaterials = () => {
|
||||||
|
try {
|
||||||
|
const materials = db.prepare('SELECT * FROM outline_materials').all();
|
||||||
|
if (materials.length === 0) return;
|
||||||
|
|
||||||
|
console.log(`🔍 正在同步 ${materials.length} 个存量提纲素材到通用素材库...`);
|
||||||
|
db.transaction(() => {
|
||||||
|
materials.forEach(mat => {
|
||||||
|
syncToReferences(mat.id, mat.name, mat.content);
|
||||||
|
});
|
||||||
|
})();
|
||||||
|
|
||||||
|
console.log(`✅ 存量提纲素材同步完成`);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('⚠️ 同步存量素材失败:', err.message);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
syncExistingOutlineMaterials();
|
||||||
|
|
||||||
console.log('📦 SQLite 数据库初始化完成:', DB_PATH);
|
console.log('📦 SQLite 数据库初始化完成:', DB_PATH);
|
||||||
|
|
||||||
export function getAllReferences() {
|
export function getAllReferences() {
|
||||||
@@ -618,6 +641,32 @@ export function getAllOutlineMaterials() {
|
|||||||
return db.prepare('SELECT * FROM outline_materials ORDER BY created_at DESC').all();
|
return db.prepare('SELECT * FROM outline_materials ORDER BY created_at DESC').all();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将提纲素材同步到通用素材库 (materials/excerpts)
|
||||||
|
* 提纲素材作为 'outline' 类型存储,便于在通用素材库中管理
|
||||||
|
*/
|
||||||
|
const syncToReferences = (id, name, content) => {
|
||||||
|
const refId = `ref-${id}`; // 使用固定前缀关联
|
||||||
|
|
||||||
|
// 1. 更新或插入到 materials 表
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO materials (id, type, title, source, is_default, created_at, updated_at)
|
||||||
|
VALUES (?, 'outline', ?, '提纲写作', 0, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
title = EXCLUDED.title,
|
||||||
|
updated_at = CURRENT_TIMESTAMP
|
||||||
|
`).run(refId, name);
|
||||||
|
|
||||||
|
// 2. 更新或插入到 reference_excerpts 表
|
||||||
|
const excerptId = `${refId}-content`;
|
||||||
|
db.prepare(`
|
||||||
|
INSERT INTO reference_excerpts (id, reference_id, topic, content)
|
||||||
|
VALUES (?, ?, '正文', ?)
|
||||||
|
ON CONFLICT(id) DO UPDATE SET
|
||||||
|
content = EXCLUDED.content
|
||||||
|
`).run(excerptId, refId, content);
|
||||||
|
};
|
||||||
|
|
||||||
export function addOutlineMaterial(material) {
|
export function addOutlineMaterial(material) {
|
||||||
const id = material.id || `omat-${Date.now()}`;
|
const id = material.id || `omat-${Date.now()}`;
|
||||||
db.prepare(`
|
db.prepare(`
|
||||||
@@ -629,6 +678,14 @@ export function addOutlineMaterial(material) {
|
|||||||
material.content,
|
material.content,
|
||||||
material.content?.length || 0
|
material.content?.length || 0
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 同步到通用素材库
|
||||||
|
try {
|
||||||
|
syncToReferences(id, material.name, material.content);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('同步素材到通用库失败:', err);
|
||||||
|
}
|
||||||
|
|
||||||
return id;
|
return id;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -651,11 +708,32 @@ export function updateOutlineMaterial(id, updates) {
|
|||||||
params.push(id);
|
params.push(id);
|
||||||
|
|
||||||
db.prepare(`UPDATE outline_materials SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
|
db.prepare(`UPDATE outline_materials SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
|
||||||
|
|
||||||
|
// 如果更新了名称或内容,需要同步
|
||||||
|
try {
|
||||||
|
const current = db.prepare('SELECT * FROM outline_materials WHERE id = ?').get(id);
|
||||||
|
if (current) {
|
||||||
|
syncToReferences(id, current.name, current.content);
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error('更新素材同步失败:', err);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteOutlineMaterial(id) {
|
export function deleteOutlineMaterial(id) {
|
||||||
db.prepare('DELETE FROM outline_materials WHERE id = ?').run(id);
|
db.prepare('DELETE FROM outline_materials WHERE id = ?').run(id);
|
||||||
|
|
||||||
|
// 同时从通用库删除 (如果存在)
|
||||||
|
try {
|
||||||
|
const refId = `ref-${id}`;
|
||||||
|
db.prepare('DELETE FROM reference_excerpts WHERE reference_id = ?').run(refId);
|
||||||
|
db.prepare('DELETE FROM materials WHERE id = ?').run(refId);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('删除素材同步失败:', err);
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,9 +50,6 @@ app.get('/api/llm/providers', (req, res) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
app.post('/api/llm/stream', async (req, res) => {
|
app.post('/api/llm/stream', async (req, res) => {
|
||||||
const controller = new AbortController();
|
|
||||||
req.on('close', () => controller.abort());
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { providerId, model, appId, messages, options } = req.body || {};
|
const { providerId, model, appId, messages, options } = req.body || {};
|
||||||
if (!providerId || !Array.isArray(messages)) {
|
if (!providerId || !Array.isArray(messages)) {
|
||||||
@@ -66,10 +63,10 @@ app.post('/api/llm/stream', async (req, res) => {
|
|||||||
appId,
|
appId,
|
||||||
messages,
|
messages,
|
||||||
options,
|
options,
|
||||||
res,
|
res
|
||||||
signal: controller.signal
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('LLM 代理请求错误:', error);
|
||||||
if (!res.headersSent) {
|
if (!res.headersSent) {
|
||||||
res.status(500).json({ success: false, error: error.message });
|
res.status(500).json({ success: false, error: error.message });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -78,8 +78,7 @@ export const streamChat = async ({
|
|||||||
appId,
|
appId,
|
||||||
messages,
|
messages,
|
||||||
options,
|
options,
|
||||||
res,
|
res
|
||||||
signal
|
|
||||||
}) => {
|
}) => {
|
||||||
const provider = providers[providerId];
|
const provider = providers[providerId];
|
||||||
|
|
||||||
@@ -107,19 +106,31 @@ export const streamChat = async ({
|
|||||||
|
|
||||||
const headers = buildHeaders({ ...provider, appId: appId || provider.appId });
|
const headers = buildHeaders({ ...provider, appId: appId || provider.appId });
|
||||||
|
|
||||||
const response = await fetch(provider.apiUrl, {
|
console.log('LLM 代理: 发起请求', { providerId, model: payload.model, apiUrl: provider.apiUrl });
|
||||||
|
|
||||||
|
let response;
|
||||||
|
try {
|
||||||
|
response = await fetch(provider.apiUrl, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers,
|
headers,
|
||||||
body: JSON.stringify(payload),
|
body: JSON.stringify(payload)
|
||||||
signal
|
|
||||||
});
|
});
|
||||||
|
} catch (fetchError) {
|
||||||
|
console.error('LLM 代理: Fetch 错误', fetchError);
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.status(500).json({ success: false, error: fetchError.message || '网络请求失败' });
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const errorText = await response.text();
|
const errorText = await response.text();
|
||||||
|
console.error('LLM 代理: 上游响应错误', response.status, errorText);
|
||||||
res.status(500).json({ success: false, error: errorText || `上游请求失败: ${response.status}` });
|
res.status(500).json({ success: false, error: errorText || `上游请求失败: ${response.status}` });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log('LLM 代理: 开始流式响应');
|
||||||
res.status(200);
|
res.status(200);
|
||||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
||||||
res.setHeader('Cache-Control', 'no-cache');
|
res.setHeader('Cache-Control', 'no-cache');
|
||||||
@@ -131,6 +142,7 @@ export const streamChat = async ({
|
|||||||
res.write(chunk);
|
res.write(chunk);
|
||||||
});
|
});
|
||||||
readable.on('end', () => {
|
readable.on('end', () => {
|
||||||
|
console.log('LLM 代理: 流式响应完成');
|
||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
readable.on('error', (error) => {
|
readable.on('error', (error) => {
|
||||||
@@ -138,3 +150,4 @@ export const streamChat = async ({
|
|||||||
res.end();
|
res.end();
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
<ArticleFusionPanel />
|
<ArticleFusionPanel />
|
||||||
<FusionResultPanel />
|
<FusionResultPanel />
|
||||||
</template>
|
</template>
|
||||||
|
<!-- 文章修改页面 -->
|
||||||
|
<ArticleEditorPanel v-else-if="currentPage === 'articleEditor'" />
|
||||||
<DocumentsPanel
|
<DocumentsPanel
|
||||||
v-else-if="currentPage === 'documents'"
|
v-else-if="currentPage === 'documents'"
|
||||||
@toggle-version-panel="toggleVersionPanel"
|
@toggle-version-panel="toggleVersionPanel"
|
||||||
@@ -36,8 +38,8 @@
|
|||||||
<MaterialsPanel v-else-if="currentPage === 'materials'" />
|
<MaterialsPanel v-else-if="currentPage === 'materials'" />
|
||||||
<SettingsPanel v-else-if="currentPage === 'settings'" />
|
<SettingsPanel v-else-if="currentPage === 'settings'" />
|
||||||
|
|
||||||
<!-- 右侧核心内容区(compare、rewrite、diffAnnotation、articleFusion 和 outlineWriter 页面使用自己的内部布局) -->
|
<!-- 右侧核心内容区(compare、rewrite、diffAnnotation、articleFusion、outlineWriter 和 articleEditor 页面使用自己的内部布局) -->
|
||||||
<MainContent v-if="currentPage !== 'compare' && currentPage !== 'rewrite' && currentPage !== 'diffAnnotation' && currentPage !== 'articleFusion' && currentPage !== 'outlineWriter'" />
|
<MainContent v-if="currentPage !== 'compare' && currentPage !== 'rewrite' && currentPage !== 'diffAnnotation' && currentPage !== 'articleFusion' && currentPage !== 'outlineWriter' && currentPage !== 'articleEditor'" />
|
||||||
|
|
||||||
<!-- 侧滑浮层面板 (仅文稿页) -->
|
<!-- 侧滑浮层面板 (仅文稿页) -->
|
||||||
<DocumentVersionPanel
|
<DocumentVersionPanel
|
||||||
@@ -74,6 +76,7 @@ import ArticleFusionPanel from './components/ArticleFusionPanel.vue'
|
|||||||
import FusionResultPanel from './components/FusionResultPanel.vue'
|
import FusionResultPanel from './components/FusionResultPanel.vue'
|
||||||
import OutlineWriterPanel from './components/OutlineWriterPanel.vue'
|
import OutlineWriterPanel from './components/OutlineWriterPanel.vue'
|
||||||
import OutlineResultPanel from './components/OutlineResultPanel.vue'
|
import OutlineResultPanel from './components/OutlineResultPanel.vue'
|
||||||
|
import ArticleEditorPanel from './components/ArticleEditorPanel.vue'
|
||||||
|
|
||||||
const appStore = useAppStore()
|
const appStore = useAppStore()
|
||||||
const currentPage = computed(() => appStore.currentPage)
|
const currentPage = computed(() => appStore.currentPage)
|
||||||
|
|||||||
1248
src/components/ArticleEditorPanel.vue
Normal file
1248
src/components/ArticleEditorPanel.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -65,6 +65,7 @@ const navItems = [
|
|||||||
{ id: 'analysis', label: '范式库', icon: 'analysis' },
|
{ id: 'analysis', label: '范式库', icon: 'analysis' },
|
||||||
{ id: 'paradigmWriter', label: '范式写作', icon: 'article' },
|
{ id: 'paradigmWriter', label: '范式写作', icon: 'article' },
|
||||||
{ id: 'outlineWriter', label: '提纲写作', icon: 'folder' },
|
{ id: 'outlineWriter', label: '提纲写作', icon: 'folder' },
|
||||||
|
{ id: 'articleEditor', label: '文章修改', icon: 'edit' },
|
||||||
{ id: 'articleFusion', label: '文章融合', icon: 'sparkles' },
|
{ id: 'articleFusion', label: '文章融合', icon: 'sparkles' },
|
||||||
{ id: 'documents', label: '文稿库', icon: 'folder' },
|
{ id: 'documents', label: '文稿库', icon: 'folder' },
|
||||||
{ id: 'materials', label: '素材库', icon: 'chart' },
|
{ id: 'materials', label: '素材库', icon: 'chart' },
|
||||||
|
|||||||
@@ -159,6 +159,9 @@ const features = [
|
|||||||
{ id: 'mimicWriter', name: '以稿写稿', icon: 'copy' },
|
{ id: 'mimicWriter', name: '以稿写稿', icon: 'copy' },
|
||||||
{ id: 'analysis', name: '范式库', icon: 'analysis' },
|
{ id: 'analysis', name: '范式库', icon: 'analysis' },
|
||||||
{ id: 'paradigmWriter', name: '范式写作', icon: 'article' },
|
{ id: 'paradigmWriter', name: '范式写作', icon: 'article' },
|
||||||
|
{ id: 'outlineWriter', name: '提纲写作', icon: 'folder' },
|
||||||
|
{ id: 'articleEditor', name: '文章修改', icon: 'edit' },
|
||||||
|
{ id: 'articleFusion', name: '文章融合', icon: 'sparkles' },
|
||||||
{ id: 'documents', name: '文稿库', icon: 'folder' },
|
{ id: 'documents', name: '文稿库', icon: 'folder' },
|
||||||
{ id: 'materials', name: '素材库', icon: 'chart' },
|
{ id: 'materials', name: '素材库', icon: 'chart' },
|
||||||
{ id: 'rewrite', name: '范式润色', icon: 'sparkles' },
|
{ id: 'rewrite', name: '范式润色', icon: 'sparkles' },
|
||||||
|
|||||||
@@ -89,7 +89,8 @@ const materialTypes = [
|
|||||||
{ id: 'policy', label: '政策文件' },
|
{ id: 'policy', label: '政策文件' },
|
||||||
{ id: 'speech', label: '领导讲话' },
|
{ id: 'speech', label: '领导讲话' },
|
||||||
{ id: 'case', label: '典型案例' },
|
{ id: 'case', label: '典型案例' },
|
||||||
{ id: 'reference', label: '参考范文' }
|
{ id: 'reference', label: '参考范文' },
|
||||||
|
{ id: 'outline', label: '提纲素材' }
|
||||||
]
|
]
|
||||||
|
|
||||||
const filteredMaterials = computed(() => {
|
const filteredMaterials = computed(() => {
|
||||||
|
|||||||
Reference in New Issue
Block a user