feat: 添加文章修改保存功能及首页入口
This commit is contained in:
78
server/db.js
78
server/db.js
@@ -233,6 +233,29 @@ const seedDefaultReferences = async () => {
|
||||
|
||||
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);
|
||||
|
||||
export function getAllReferences() {
|
||||
@@ -618,6 +641,32 @@ export function getAllOutlineMaterials() {
|
||||
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) {
|
||||
const id = material.id || `omat-${Date.now()}`;
|
||||
db.prepare(`
|
||||
@@ -629,6 +678,14 @@ export function addOutlineMaterial(material) {
|
||||
material.content,
|
||||
material.content?.length || 0
|
||||
);
|
||||
|
||||
// 同步到通用素材库
|
||||
try {
|
||||
syncToReferences(id, material.name, material.content);
|
||||
} catch (err) {
|
||||
console.error('同步素材到通用库失败:', err);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
@@ -651,11 +708,32 @@ export function updateOutlineMaterial(id, updates) {
|
||||
params.push(id);
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
export function deleteOutlineMaterial(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;
|
||||
}
|
||||
|
||||
|
||||
@@ -50,9 +50,6 @@ app.get('/api/llm/providers', (req, res) => {
|
||||
});
|
||||
|
||||
app.post('/api/llm/stream', async (req, res) => {
|
||||
const controller = new AbortController();
|
||||
req.on('close', () => controller.abort());
|
||||
|
||||
try {
|
||||
const { providerId, model, appId, messages, options } = req.body || {};
|
||||
if (!providerId || !Array.isArray(messages)) {
|
||||
@@ -66,10 +63,10 @@ app.post('/api/llm/stream', async (req, res) => {
|
||||
appId,
|
||||
messages,
|
||||
options,
|
||||
res,
|
||||
signal: controller.signal
|
||||
res
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('LLM 代理请求错误:', error);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, error: error.message });
|
||||
}
|
||||
|
||||
@@ -78,8 +78,7 @@ export const streamChat = async ({
|
||||
appId,
|
||||
messages,
|
||||
options,
|
||||
res,
|
||||
signal
|
||||
res
|
||||
}) => {
|
||||
const provider = providers[providerId];
|
||||
|
||||
@@ -107,19 +106,31 @@ export const streamChat = async ({
|
||||
|
||||
const headers = buildHeaders({ ...provider, appId: appId || provider.appId });
|
||||
|
||||
const response = await fetch(provider.apiUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload),
|
||||
signal
|
||||
});
|
||||
console.log('LLM 代理: 发起请求', { providerId, model: payload.model, apiUrl: provider.apiUrl });
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(provider.apiUrl, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(payload)
|
||||
});
|
||||
} catch (fetchError) {
|
||||
console.error('LLM 代理: Fetch 错误', fetchError);
|
||||
if (!res.headersSent) {
|
||||
res.status(500).json({ success: false, error: fetchError.message || '网络请求失败' });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
console.error('LLM 代理: 上游响应错误', response.status, errorText);
|
||||
res.status(500).json({ success: false, error: errorText || `上游请求失败: ${response.status}` });
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('LLM 代理: 开始流式响应');
|
||||
res.status(200);
|
||||
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
|
||||
res.setHeader('Cache-Control', 'no-cache');
|
||||
@@ -131,6 +142,7 @@ export const streamChat = async ({
|
||||
res.write(chunk);
|
||||
});
|
||||
readable.on('end', () => {
|
||||
console.log('LLM 代理: 流式响应完成');
|
||||
res.end();
|
||||
});
|
||||
readable.on('error', (error) => {
|
||||
@@ -138,3 +150,4 @@ export const streamChat = async ({
|
||||
res.end();
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -28,6 +28,8 @@
|
||||
<ArticleFusionPanel />
|
||||
<FusionResultPanel />
|
||||
</template>
|
||||
<!-- 文章修改页面 -->
|
||||
<ArticleEditorPanel v-else-if="currentPage === 'articleEditor'" />
|
||||
<DocumentsPanel
|
||||
v-else-if="currentPage === 'documents'"
|
||||
@toggle-version-panel="toggleVersionPanel"
|
||||
@@ -36,8 +38,8 @@
|
||||
<MaterialsPanel v-else-if="currentPage === 'materials'" />
|
||||
<SettingsPanel v-else-if="currentPage === 'settings'" />
|
||||
|
||||
<!-- 右侧核心内容区(compare、rewrite、diffAnnotation、articleFusion 和 outlineWriter 页面使用自己的内部布局) -->
|
||||
<MainContent v-if="currentPage !== 'compare' && currentPage !== 'rewrite' && currentPage !== 'diffAnnotation' && currentPage !== 'articleFusion' && currentPage !== 'outlineWriter'" />
|
||||
<!-- 右侧核心内容区(compare、rewrite、diffAnnotation、articleFusion、outlineWriter 和 articleEditor 页面使用自己的内部布局) -->
|
||||
<MainContent v-if="currentPage !== 'compare' && currentPage !== 'rewrite' && currentPage !== 'diffAnnotation' && currentPage !== 'articleFusion' && currentPage !== 'outlineWriter' && currentPage !== 'articleEditor'" />
|
||||
|
||||
<!-- 侧滑浮层面板 (仅文稿页) -->
|
||||
<DocumentVersionPanel
|
||||
@@ -74,6 +76,7 @@ import ArticleFusionPanel from './components/ArticleFusionPanel.vue'
|
||||
import FusionResultPanel from './components/FusionResultPanel.vue'
|
||||
import OutlineWriterPanel from './components/OutlineWriterPanel.vue'
|
||||
import OutlineResultPanel from './components/OutlineResultPanel.vue'
|
||||
import ArticleEditorPanel from './components/ArticleEditorPanel.vue'
|
||||
|
||||
const appStore = useAppStore()
|
||||
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: 'paradigmWriter', label: '范式写作', icon: 'article' },
|
||||
{ id: 'outlineWriter', label: '提纲写作', icon: 'folder' },
|
||||
{ id: 'articleEditor', label: '文章修改', icon: 'edit' },
|
||||
{ id: 'articleFusion', label: '文章融合', icon: 'sparkles' },
|
||||
{ id: 'documents', label: '文稿库', icon: 'folder' },
|
||||
{ id: 'materials', label: '素材库', icon: 'chart' },
|
||||
|
||||
@@ -159,6 +159,9 @@ const features = [
|
||||
{ id: 'mimicWriter', name: '以稿写稿', icon: 'copy' },
|
||||
{ id: 'analysis', name: '范式库', icon: 'analysis' },
|
||||
{ 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: 'materials', name: '素材库', icon: 'chart' },
|
||||
{ id: 'rewrite', name: '范式润色', icon: 'sparkles' },
|
||||
|
||||
@@ -89,7 +89,8 @@ const materialTypes = [
|
||||
{ id: 'policy', label: '政策文件' },
|
||||
{ id: 'speech', label: '领导讲话' },
|
||||
{ id: 'case', label: '典型案例' },
|
||||
{ id: 'reference', label: '参考范文' }
|
||||
{ id: 'reference', label: '参考范文' },
|
||||
{ id: 'outline', label: '提纲素材' }
|
||||
]
|
||||
|
||||
const filteredMaterials = computed(() => {
|
||||
|
||||
Reference in New Issue
Block a user