diff --git a/server/db.js b/server/db.js index 1d3d5fb..f722b6c 100644 --- a/server/db.js +++ b/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; } diff --git a/server/index.js b/server/index.js index d43e47b..755f86d 100644 --- a/server/index.js +++ b/server/index.js @@ -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 }); } diff --git a/server/llm.js b/server/llm.js index bd179d9..6af809e 100644 --- a/server/llm.js +++ b/server/llm.js @@ -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(); }); }; + diff --git a/src/App.vue b/src/App.vue index 617cd47..6a50ec1 100644 --- a/src/App.vue +++ b/src/App.vue @@ -28,6 +28,8 @@ + + - - + + appStore.currentPage) diff --git a/src/components/ArticleEditorPanel.vue b/src/components/ArticleEditorPanel.vue new file mode 100644 index 0000000..4330976 --- /dev/null +++ b/src/components/ArticleEditorPanel.vue @@ -0,0 +1,1248 @@ + + + + + + + + + + + 修改设置 + + + + + + + + 关联素材 + + + 添加 + + + + + {{ mat.title }} + + + + + + 点击"添加"选择参考素材 + + + + + 修改提示词 + + + + + + 快捷操作 + + {{ prompt.label }} + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/GlobalSidebar.vue b/src/components/GlobalSidebar.vue index d90b9fd..db9aa65 100644 --- a/src/components/GlobalSidebar.vue +++ b/src/components/GlobalSidebar.vue @@ -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' }, diff --git a/src/components/HomePage.vue b/src/components/HomePage.vue index a88979d..940d0d6 100644 --- a/src/components/HomePage.vue +++ b/src/components/HomePage.vue @@ -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' }, diff --git a/src/components/MaterialSelectorModal.vue b/src/components/MaterialSelectorModal.vue index 192c8b5..e05e4d8 100644 --- a/src/components/MaterialSelectorModal.vue +++ b/src/components/MaterialSelectorModal.vue @@ -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(() => {
点击"添加"选择参考素材