feat: 添加大纲写作功能与服务器端改进

- 新增 OutlineWriterPanel 和 OutlineResultPanel 组件
- 重构服务器端数据库接口 (server/db.js)
- 添加 LLM 服务模块 (server/llm.js)
- 更新配置和设置面板
- 优化文档选择器和素材面板
- 更新部署文档和环境变量示例
This commit is contained in:
empty
2026-01-21 17:23:48 +08:00
parent d7f1664766
commit 94301c81a6
29 changed files with 3430 additions and 1373 deletions

View File

@@ -7,124 +7,805 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const DATA_DIR = path.join(__dirname, '../data');
const DB_PATH = path.join(DATA_DIR, 'paradigms.db');
// 确保数据目录存在
if (!fs.existsSync(DATA_DIR)) {
fs.mkdirSync(DATA_DIR, { recursive: true });
fs.mkdirSync(DATA_DIR, { recursive: true });
}
// 创建数据库连接
const db = new Database(DB_PATH);
// 初始化表结构
db.pragma('journal_mode = WAL');
db.exec(`
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
);
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
);
CREATE TABLE IF NOT EXISTS paradigms (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
icon TEXT,
description TEXT,
icon TEXT DEFAULT 'sparkles',
tag_class TEXT DEFAULT 'bg-purple-900/30 text-purple-300',
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,
specialized_prompt TEXT,
expert_guidelines TEXT,
is_custom INTEGER DEFAULT 1,
created_at TEXT,
updated_at TEXT
)
outline_template TEXT,
recommended_tags TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_custom INTEGER DEFAULT 0
);
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
);
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
);
CREATE TABLE IF NOT EXISTS user_config (
key TEXT PRIMARY KEY,
value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
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
);
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
);
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
);
CREATE TABLE IF NOT EXISTS outline_materials (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
content TEXT NOT NULL,
word_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
const ensureColumn = (table, column, definition) => {
const columns = db.prepare(`PRAGMA table_info(${table})`).all();
const exists = columns.some(col => col.name === column);
if (!exists) {
try {
db.exec(`ALTER TABLE ${table} ADD COLUMN ${column} ${definition}`);
} catch (error) {
console.warn(`⚠️ 字段迁移失败: ${table}.${column}`, error.message);
}
}
};
const ensureSchema = () => {
ensureColumn('paradigms', 'system_constraints', 'TEXT');
ensureColumn('paradigms', 'dimension_set_id', 'TEXT');
ensureColumn('paradigms', 'custom_dimensions', 'TEXT');
ensureColumn('paradigms', 'logic_paradigms', 'TEXT');
ensureColumn('paradigms', 'auto_match_refs', 'INTEGER DEFAULT 1');
ensureColumn('paradigms', 'selected_refs', 'TEXT');
ensureColumn('paradigms', 'outline_template', 'TEXT');
ensureColumn('paradigms', 'recommended_tags', 'TEXT');
ensureColumn('paradigms', 'tag_class', "TEXT");
ensureColumn('paradigms', 'tags', 'TEXT');
ensureColumn('paradigms', 'specialized_prompt', 'TEXT');
ensureColumn('paradigms', 'expert_guidelines', 'TEXT');
ensureColumn('paradigms', 'is_custom', 'INTEGER DEFAULT 0');
ensureColumn('paradigms', 'created_at', 'TEXT');
ensureColumn('paradigms', 'updated_at', 'TEXT');
};
ensureSchema();
const parseJson = (value, fallback) => {
if (!value) return fallback;
try {
return JSON.parse(value);
} catch {
return fallback;
}
};
const getExcerptsByReferenceId = (referenceId) => {
const rows = db.prepare('SELECT * FROM reference_excerpts WHERE reference_id = ?').all(referenceId);
return rows.map(row => ({
...row,
applicableDimensions: parseJson(row.applicable_dimensions, []),
useFor: row.use_for || null
}));
};
const seedDefaultReferences = async () => {
const count = db.prepare('SELECT COUNT(*) as count FROM materials').get().count;
if (count > 0) return;
try {
const { REFERENCES } = await import('../src/config/references.js');
const insertMaterial = db.prepare(`
INSERT INTO materials (id, type, title, source, date, tags, related_dimension_sets, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`);
const insertExcerpt = db.prepare(`
INSERT INTO reference_excerpts (id, reference_id, topic, content, applicable_dimensions, use_for)
VALUES (?, ?, ?, ?, ?, ?)
`);
const insertMany = db.transaction((items) => {
items.forEach((ref) => {
insertMaterial.run(
ref.id,
ref.type,
ref.title,
ref.source || null,
ref.date || null,
JSON.stringify(ref.tags || []),
JSON.stringify(ref.relatedDimensionSets || []),
1
);
if (ref.excerpts?.length) {
ref.excerpts.forEach((excerpt, index) => {
insertExcerpt.run(
excerpt.id || `${ref.id}-excerpt-${index}`,
ref.id,
excerpt.topic,
excerpt.content,
JSON.stringify(excerpt.applicableDimensions || []),
excerpt.useFor || null
);
});
}
});
});
insertMany(Object.values(REFERENCES));
console.log('✅ 默认素材导入完成');
} catch (error) {
console.warn('⚠️ 默认素材导入失败:', error.message);
}
};
await seedDefaultReferences();
console.log('📦 SQLite 数据库初始化完成:', DB_PATH);
// CRUD 方法
export function getAllReferences() {
const refs = db.prepare('SELECT * FROM materials ORDER BY created_at DESC').all();
return refs.map(ref => ({
...ref,
tags: parseJson(ref.tags, []),
relatedDimensionSets: parseJson(ref.related_dimension_sets, []),
excerpts: getExcerptsByReferenceId(ref.id)
}));
}
export function getReferenceById(id) {
const ref = db.prepare('SELECT * FROM materials WHERE id = ?').get(id);
if (!ref) return null;
return {
...ref,
tags: parseJson(ref.tags, []),
relatedDimensionSets: parseJson(ref.related_dimension_sets, []),
excerpts: getExcerptsByReferenceId(ref.id)
};
}
export function getReferencesByType(type) {
const refs = db.prepare('SELECT * FROM materials WHERE type = ? ORDER BY created_at DESC').all(type);
return refs.map(ref => ({
...ref,
tags: parseJson(ref.tags, []),
relatedDimensionSets: parseJson(ref.related_dimension_sets, []),
excerpts: getExcerptsByReferenceId(ref.id)
}));
}
export function addReference(reference) {
const id = reference.id || `ref-${Date.now()}`;
db.prepare(`
INSERT INTO materials (id, type, title, source, date, tags, related_dimension_sets, is_default)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
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) {
const insertExcerpt = db.prepare(`
INSERT INTO reference_excerpts (id, reference_id, topic, content, applicable_dimensions, use_for)
VALUES (?, ?, ?, ?, ?, ?)
`);
reference.excerpts.forEach((excerpt, index) => {
insertExcerpt.run(
excerpt.id || `${id}-excerpt-${index}`,
id,
excerpt.topic,
excerpt.content,
JSON.stringify(excerpt.applicableDimensions || []),
excerpt.useFor || null
);
});
}
return id;
}
export function 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);
db.prepare(`UPDATE materials SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
return true;
}
export function deleteReference(id) {
db.prepare('DELETE FROM reference_excerpts WHERE reference_id = ?').run(id);
db.prepare('DELETE FROM materials WHERE id = ?').run(id);
return true;
}
export function getAllParadigms() {
const rows = db.prepare('SELECT * FROM paradigms ORDER BY created_at DESC').all();
return rows.map(row => ({
id: row.id,
name: row.name,
description: row.description,
icon: row.icon,
tagClass: row.tag_class,
tags: row.tags ? JSON.parse(row.tags) : [],
specializedPrompt: row.specialized_prompt,
expertGuidelines: row.expert_guidelines ? JSON.parse(row.expert_guidelines) : [],
isCustom: Boolean(row.is_custom),
createdAt: row.created_at,
updatedAt: row.updated_at
}));
const paradigms = db.prepare('SELECT * FROM paradigms ORDER BY is_custom ASC, created_at DESC').all();
return paradigms.map(p => ({
...p,
tagClass: p.tag_class || 'bg-blue-900/30 text-blue-300',
tags: parseJson(p.tags, []),
systemConstraints: parseJson(p.system_constraints, []),
customDimensions: parseJson(p.custom_dimensions, null),
logicParadigms: parseJson(p.logic_paradigms, null),
selectedRefs: parseJson(p.selected_refs, []),
specializedPrompt: p.specialized_prompt || null,
expertGuidelines: parseJson(p.expert_guidelines, null),
outlineTemplate: p.outline_template || null,
recommendedTags: parseJson(p.recommended_tags, []),
isCustom: p.is_custom === 1,
autoMatchRefs: p.auto_match_refs === 1
}));
}
export function getParadigmById(id) {
const row = db.prepare('SELECT * FROM paradigms WHERE id = ?').get(id);
if (!row) return null;
return {
id: row.id,
name: row.name,
description: row.description,
icon: row.icon,
tagClass: row.tag_class,
tags: row.tags ? JSON.parse(row.tags) : [],
specializedPrompt: row.specialized_prompt,
expertGuidelines: row.expert_guidelines ? JSON.parse(row.expert_guidelines) : [],
isCustom: Boolean(row.is_custom),
createdAt: row.created_at,
updatedAt: row.updated_at
};
const p = db.prepare('SELECT * FROM paradigms WHERE id = ?').get(id);
if (!p) return null;
return {
...p,
tagClass: p.tag_class || 'bg-blue-900/30 text-blue-300',
tags: parseJson(p.tags, []),
systemConstraints: parseJson(p.system_constraints, []),
customDimensions: parseJson(p.custom_dimensions, null),
logicParadigms: parseJson(p.logic_paradigms, null),
selectedRefs: parseJson(p.selected_refs, []),
specializedPrompt: p.specialized_prompt || null,
expertGuidelines: parseJson(p.expert_guidelines, null),
outlineTemplate: p.outline_template || null,
recommendedTags: parseJson(p.recommended_tags, []),
isCustom: p.is_custom === 1,
autoMatchRefs: p.auto_match_refs === 1
};
}
export function createParadigm(paradigm) {
const stmt = db.prepare(`
INSERT INTO paradigms (id, name, description, icon, tag_class, tags, specialized_prompt, expert_guidelines, is_custom, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`);
export function addParadigm(paradigm) {
const id = paradigm.id || `paradigm-${Date.now()}`;
db.prepare(`
INSERT INTO paradigms (id, name, icon, description, tags, tag_class, system_constraints,
dimension_set_id, custom_dimensions, logic_paradigms, auto_match_refs, selected_refs,
specialized_prompt, expert_guidelines, outline_template, recommended_tags, is_custom)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
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.specializedPrompt || null,
paradigm.expertGuidelines ? JSON.stringify(paradigm.expertGuidelines) : null,
paradigm.outlineTemplate || null,
paradigm.recommendedTags ? JSON.stringify(paradigm.recommendedTags || []) : null,
paradigm.isCustom ? 1 : 0
);
const now = new Date().toISOString();
stmt.run(
paradigm.id,
paradigm.name,
paradigm.description || '',
paradigm.icon || 'sparkles',
paradigm.tagClass || 'bg-purple-900/30 text-purple-300',
JSON.stringify(paradigm.tags || []),
paradigm.specializedPrompt || '',
JSON.stringify(paradigm.expertGuidelines || []),
paradigm.isCustom ? 1 : 0,
paradigm.createdAt || now,
now
);
return getParadigmById(paradigm.id);
return id;
}
export function updateParadigm(id, updates) {
const existing = getParadigmById(id);
if (!existing) return null;
const setClauses = [];
const params = [];
const merged = { ...existing, ...updates };
const stmt = db.prepare(`
UPDATE paradigms
SET name = ?, description = ?, icon = ?, tag_class = ?, tags = ?,
specialized_prompt = ?, expert_guidelines = ?, updated_at = ?
WHERE id = ?
`);
const fieldMap = {
name: 'name',
icon: 'icon',
description: 'description',
tagClass: 'tag_class',
dimensionSetId: 'dimension_set_id',
autoMatchRefs: 'auto_match_refs'
};
stmt.run(
merged.name,
merged.description,
merged.icon,
merged.tagClass,
JSON.stringify(merged.tags || []),
merged.specializedPrompt,
JSON.stringify(merged.expertGuidelines || []),
new Date().toISOString(),
id
);
Object.entries(updates).forEach(([key, value]) => {
if (fieldMap[key]) {
setClauses.push(`${fieldMap[key]} = ?`);
params.push(key === 'autoMatchRefs' ? (value ? 1 : 0) : value);
}
});
return getParadigmById(id);
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.logicParadigms !== undefined) {
setClauses.push('logic_paradigms = ?');
params.push(updates.logicParadigms ? JSON.stringify(updates.logicParadigms) : null);
}
if (updates.selectedRefs !== undefined) {
setClauses.push('selected_refs = ?');
params.push(JSON.stringify(updates.selectedRefs));
}
if (updates.specializedPrompt !== undefined) {
setClauses.push('specialized_prompt = ?');
params.push(updates.specializedPrompt);
}
if (updates.expertGuidelines !== undefined) {
setClauses.push('expert_guidelines = ?');
params.push(updates.expertGuidelines ? JSON.stringify(updates.expertGuidelines) : null);
}
if (updates.outlineTemplate !== undefined) {
setClauses.push('outline_template = ?');
params.push(updates.outlineTemplate);
}
if (updates.recommendedTags !== undefined) {
setClauses.push('recommended_tags = ?');
params.push(updates.recommendedTags ? JSON.stringify(updates.recommendedTags) : null);
}
setClauses.push('updated_at = CURRENT_TIMESTAMP');
params.push(id);
db.prepare(`UPDATE paradigms SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
return true;
}
export function deleteParadigm(id) {
const stmt = db.prepare('DELETE FROM paradigms WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
db.prepare('DELETE FROM paradigms WHERE id = ?').run(id);
return true;
}
export function getConfig(key, defaultValue = null) {
const result = db.prepare('SELECT value FROM user_config WHERE key = ?').get(key);
if (!result) return defaultValue;
return parseJson(result.value, result.value);
}
export function setConfig(key, value) {
const valueStr = typeof value === 'string' ? value : JSON.stringify(value);
db.prepare(`
INSERT INTO user_config (key, value, updated_at)
VALUES (?, ?, CURRENT_TIMESTAMP)
ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
`).run(key, valueStr, valueStr);
return true;
}
export function getAllDocuments() {
const docs = db.prepare('SELECT * FROM documents ORDER BY updated_at DESC').all();
return docs.map(doc => ({
...doc,
tags: parseJson(doc.tags, []),
selectedRefs: parseJson(doc.selected_refs, [])
}));
}
export function getDocumentById(id) {
const doc = db.prepare('SELECT * FROM documents WHERE id = ?').get(id);
if (!doc) return null;
return {
...doc,
tags: parseJson(doc.tags, []),
selectedRefs: parseJson(doc.selected_refs, []),
versions: getDocumentVersions(id)
};
}
export function getDocumentVersions(documentId) {
return db.prepare('SELECT * FROM document_versions WHERE document_id = ? ORDER BY version_number DESC').all(documentId);
}
export function createDocument(document) {
const id = document.id || `doc-${Date.now()}`;
db.prepare(`
INSERT INTO documents (id, title, content, paradigm_id, dimension_set_id, selected_refs, status, word_count, tags)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
`).run(
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 function 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.dimensionSetId !== undefined) {
setClauses.push('dimension_set_id = ?');
params.push(updates.dimensionSetId);
}
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);
db.prepare(`UPDATE documents SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
return true;
}
export function saveDocumentVersion(documentId, content, changeNote = '') {
const result = db.prepare('SELECT MAX(version_number) as max_version FROM document_versions WHERE document_id = ?').get(documentId);
const nextVersion = (result?.max_version || 0) + 1;
db.prepare(`
INSERT INTO document_versions (document_id, content, version_number, change_note)
VALUES (?, ?, ?, ?)
`).run(documentId, content, nextVersion, changeNote);
return nextVersion;
}
export function deleteDocument(id) {
db.prepare('DELETE FROM document_versions WHERE document_id = ?').run(id);
db.prepare('DELETE FROM documents WHERE id = ?').run(id);
return true;
}
export function clearDocuments() {
db.prepare('DELETE FROM document_versions').run();
db.prepare('DELETE FROM documents').run();
return true;
}
export function getDocumentsByStatus(status) {
const docs = db.prepare('SELECT * FROM documents WHERE status = ? ORDER BY updated_at DESC').all(status);
return docs.map(doc => ({
...doc,
tags: parseJson(doc.tags, []),
selectedRefs: parseJson(doc.selected_refs, [])
}));
}
export function getAllOutlineMaterials() {
return db.prepare('SELECT * FROM outline_materials ORDER BY created_at DESC').all();
}
export function addOutlineMaterial(material) {
const id = material.id || `omat-${Date.now()}`;
db.prepare(`
INSERT INTO outline_materials (id, name, content, word_count)
VALUES (?, ?, ?, ?)
`).run(
id,
material.name,
material.content,
material.content?.length || 0
);
return id;
}
export function updateOutlineMaterial(id, updates) {
const setClauses = [];
const params = [];
if (updates.name !== undefined) {
setClauses.push('name = ?');
params.push(updates.name);
}
if (updates.content !== undefined) {
setClauses.push('content = ?');
params.push(updates.content);
setClauses.push('word_count = ?');
params.push(updates.content?.length || 0);
}
setClauses.push('updated_at = CURRENT_TIMESTAMP');
params.push(id);
db.prepare(`UPDATE outline_materials SET ${setClauses.join(', ')} WHERE id = ?`).run(...params);
return true;
}
export function deleteOutlineMaterial(id) {
db.prepare('DELETE FROM outline_materials WHERE id = ?').run(id);
return true;
}
export function exportDatabase() {
return fs.readFileSync(DB_PATH);
}
export function exportAsJSON() {
return {
references: getAllReferences(),
paradigms: getAllParadigms(),
documents: getAllDocuments(),
config: db.prepare('SELECT * FROM user_config').all(),
exportedAt: new Date().toISOString()
};
}
export async function resetDatabase() {
db.exec(`
DROP TABLE IF EXISTS reference_excerpts;
DROP TABLE IF EXISTS materials;
DROP TABLE IF EXISTS paradigms;
DROP TABLE IF EXISTS dimension_sets;
DROP TABLE IF EXISTS dimensions;
DROP TABLE IF EXISTS user_config;
DROP TABLE IF EXISTS analysis_history;
DROP TABLE IF EXISTS documents;
DROP TABLE IF EXISTS document_versions;
DROP TABLE IF EXISTS outline_materials;
`);
db.exec(`
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
);
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
);
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,
specialized_prompt TEXT,
expert_guidelines TEXT,
outline_template TEXT,
recommended_tags TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
is_custom INTEGER DEFAULT 0
);
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
);
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
);
CREATE TABLE IF NOT EXISTS user_config (
key TEXT PRIMARY KEY,
value TEXT,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
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
);
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
);
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
);
CREATE TABLE IF NOT EXISTS outline_materials (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
content TEXT NOT NULL,
word_count INTEGER DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
`);
await seedDefaultReferences();
console.log('🔄 数据库已重置并恢复默认数据');
return true;
}
export default db;

View File

@@ -1,93 +1,344 @@
import 'dotenv/config';
import express from 'express';
import cors from 'cors';
import { getAllParadigms, getParadigmById, createParadigm, updateParadigm, deleteParadigm } from './db.js';
import {
getAllParadigms,
getParadigmById,
updateParadigm,
deleteParadigm,
addParadigm,
getAllReferences,
getReferenceById,
getReferencesByType,
addReference,
updateReference,
deleteReference,
getConfig,
setConfig,
getAllDocuments,
getDocumentById,
getDocumentVersions,
createDocument,
updateDocument,
saveDocumentVersion,
deleteDocument,
clearDocuments,
getDocumentsByStatus,
getAllOutlineMaterials,
addOutlineMaterial,
updateOutlineMaterial,
deleteOutlineMaterial,
exportDatabase,
exportAsJSON,
resetDatabase
} from './db.js';
import { getProviderSummary, streamChat } from './llm.js';
const app = express();
const PORT = process.env.API_PORT || 3001;
// 中间件
app.use(cors());
app.use(express.json());
app.use(express.json({ limit: '2mb' }));
// API 路由
// 获取所有范式
app.get('/api/paradigms', (req, res) => {
try {
const paradigms = getAllParadigms();
res.json({ success: true, data: paradigms });
} catch (error) {
console.error('获取范式列表失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 获取单个范式
app.get('/api/paradigms/:id', (req, res) => {
try {
const paradigm = getParadigmById(req.params.id);
if (!paradigm) {
return res.status(404).json({ success: false, error: '范式不存在' });
}
res.json({ success: true, data: paradigm });
} catch (error) {
console.error('获取范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 创建范式
app.post('/api/paradigms', (req, res) => {
try {
const paradigm = createParadigm(req.body);
console.log('✅ 创建范式:', paradigm.name);
res.status(201).json({ success: true, data: paradigm });
} catch (error) {
console.error('创建范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 更新范式
app.put('/api/paradigms/:id', (req, res) => {
try {
const paradigm = updateParadigm(req.params.id, req.body);
if (!paradigm) {
return res.status(404).json({ success: false, error: '范式不存在' });
}
console.log('✅ 更新范式:', paradigm.name);
res.json({ success: true, data: paradigm });
} catch (error) {
console.error('更新范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 删除范式
app.delete('/api/paradigms/:id', (req, res) => {
try {
const deleted = deleteParadigm(req.params.id);
if (!deleted) {
return res.status(404).json({ success: false, error: '范式不存在' });
}
console.log('✅ 删除范式:', req.params.id);
res.json({ success: true });
} catch (error) {
console.error('删除范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 健康检查
app.get('/api/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// 启动服务器
app.listen(PORT, () => {
console.log(`🚀 API 服务器已启动: http://localhost:${PORT}`);
console.log(` - GET /api/paradigms`);
console.log(` - GET /api/paradigms/:id`);
console.log(` - POST /api/paradigms`);
console.log(` - PUT /api/paradigms/:id`);
console.log(` - DELETE /api/paradigms/:id`);
// LLM 代理
app.get('/api/llm/providers', (req, res) => {
res.json({ success: true, data: getProviderSummary() });
});
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)) {
res.status(400).json({ success: false, error: '参数不完整' });
return;
}
await streamChat({
providerId,
model,
appId,
messages,
options,
res,
signal: controller.signal
});
} catch (error) {
if (!res.headersSent) {
res.status(500).json({ success: false, error: error.message });
}
}
});
// 素材库
app.get('/api/references', (req, res) => {
try {
const { type } = req.query;
const data = type ? getReferencesByType(type) : getAllReferences();
res.json({ success: true, data });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/references/:id', (req, res) => {
try {
const data = getReferenceById(req.params.id);
if (!data) return res.status(404).json({ success: false, error: '素材不存在' });
res.json({ success: true, data });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/references', (req, res) => {
try {
const id = addReference(req.body);
res.status(201).json({ success: true, data: { id } });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.put('/api/references/:id', (req, res) => {
try {
const ok = updateReference(req.params.id, req.body || {});
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.delete('/api/references/:id', (req, res) => {
try {
const ok = deleteReference(req.params.id);
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 范式
app.get('/api/paradigms', (req, res) => {
try {
const paradigms = getAllParadigms();
res.json({ success: true, data: paradigms });
} catch (error) {
console.error('获取范式列表失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/paradigms/:id', (req, res) => {
try {
const paradigm = getParadigmById(req.params.id);
if (!paradigm) return res.status(404).json({ success: false, error: '范式不存在' });
res.json({ success: true, data: paradigm });
} catch (error) {
console.error('获取范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/paradigms', (req, res) => {
try {
const id = addParadigm(req.body);
res.status(201).json({ success: true, data: { id } });
} catch (error) {
console.error('创建范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
app.put('/api/paradigms/:id', (req, res) => {
try {
const ok = updateParadigm(req.params.id, req.body || {});
res.json({ success: true, data: ok });
} catch (error) {
console.error('更新范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
app.delete('/api/paradigms/:id', (req, res) => {
try {
const ok = deleteParadigm(req.params.id);
res.json({ success: true, data: ok });
} catch (error) {
console.error('删除范式失败:', error);
res.status(500).json({ success: false, error: error.message });
}
});
// 用户配置
app.get('/api/config/:key', (req, res) => {
try {
const value = getConfig(req.params.key, null);
res.json({ success: true, data: value });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.put('/api/config/:key', (req, res) => {
try {
const ok = setConfig(req.params.key, req.body?.value);
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 文稿
app.get('/api/documents', (req, res) => {
try {
const { status } = req.query;
const docs = status ? getDocumentsByStatus(status) : getAllDocuments();
res.json({ success: true, data: docs });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/documents/:id', (req, res) => {
try {
const doc = getDocumentById(req.params.id);
if (!doc) return res.status(404).json({ success: false, error: '文稿不存在' });
res.json({ success: true, data: doc });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/documents/:id/versions', (req, res) => {
try {
const versions = getDocumentVersions(req.params.id);
res.json({ success: true, data: versions });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/documents', (req, res) => {
try {
const id = createDocument(req.body);
res.status(201).json({ success: true, data: { id } });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/documents/:id/versions', (req, res) => {
try {
const { content, changeNote } = req.body || {};
const versionNumber = saveDocumentVersion(req.params.id, content || '', changeNote || '');
res.json({ success: true, data: { versionNumber } });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.put('/api/documents/:id', (req, res) => {
try {
const ok = updateDocument(req.params.id, req.body || {});
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.delete('/api/documents/:id', (req, res) => {
try {
const ok = deleteDocument(req.params.id);
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/documents/clear', (req, res) => {
try {
const ok = clearDocuments();
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 提纲素材
app.get('/api/outline-materials', (req, res) => {
try {
const data = getAllOutlineMaterials();
res.json({ success: true, data });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/outline-materials', (req, res) => {
try {
const id = addOutlineMaterial(req.body);
res.status(201).json({ success: true, data: { id } });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.put('/api/outline-materials/:id', (req, res) => {
try {
const ok = updateOutlineMaterial(req.params.id, req.body || {});
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.delete('/api/outline-materials/:id', (req, res) => {
try {
const ok = deleteOutlineMaterial(req.params.id);
res.json({ success: true, data: ok });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
// 数据导出/重置
app.get('/api/db/export', (req, res) => {
try {
const data = exportDatabase();
res.setHeader('Content-Type', 'application/octet-stream');
res.send(data);
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.get('/api/db/export-json', (req, res) => {
try {
const data = exportAsJSON();
res.json({ success: true, data });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.post('/api/db/reset', async (req, res) => {
try {
await resetDatabase();
res.json({ success: true });
} catch (error) {
res.status(500).json({ success: false, error: error.message });
}
});
app.listen(PORT, () => {
console.log(`🚀 API 服务器已启动: http://localhost:${PORT}`);
console.log(' - LLM 代理: POST /api/llm/stream');
console.log(' - 数据接口: /api/references /api/paradigms /api/documents');
});

140
server/llm.js Normal file
View File

@@ -0,0 +1,140 @@
import { Readable } from 'stream';
const providers = {
deepseek: {
id: 'deepseek',
name: 'DeepSeek',
description: '深度求索',
apiUrl: process.env.DEEPSEEK_API_URL || 'https://api.deepseek.com/chat/completions',
apiKey: process.env.DEEPSEEK_API_KEY || '',
model: process.env.DEEPSEEK_MODEL || 'deepseek-chat'
},
qianfan: {
id: 'qianfan',
name: '千帆大模型',
description: '百度文心一言',
apiUrl: process.env.QIANFAN_API_URL || 'https://qianfan.baidubce.com/v2/chat/completions',
apiKey: process.env.QIANFAN_API_KEY || '',
appId: process.env.QIANFAN_APP_ID || '',
model: process.env.QIANFAN_MODEL || 'ernie-4.0-8k'
},
openai: {
id: 'openai',
name: 'OpenAI',
description: 'GPT 系列',
apiUrl: process.env.OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
apiKey: process.env.OPENAI_API_KEY || '',
model: process.env.OPENAI_MODEL || 'gpt-4o'
},
claude: {
id: 'claude',
name: 'Claude',
description: 'Anthropic Claude',
apiUrl: process.env.CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
apiKey: process.env.CLAUDE_API_KEY || '',
model: process.env.CLAUDE_MODEL || 'claude-3-5-sonnet'
},
custom: {
id: 'custom',
name: '自定义',
description: '自定义 API 端点',
apiUrl: process.env.CUSTOM_API_URL || '',
apiKey: process.env.CUSTOM_API_KEY || '',
model: process.env.CUSTOM_MODEL || ''
}
};
export const getProviderSummary = () => {
return Object.values(providers).map(provider => ({
id: provider.id,
name: provider.name,
description: provider.description,
model: provider.model,
enabled: Boolean(provider.apiKey)
}));
};
const buildHeaders = (provider) => {
const headers = {
'Content-Type': 'application/json'
};
if (provider.apiKey) {
headers['Authorization'] = provider.apiKey.trim().startsWith('Bearer ')
? provider.apiKey.trim()
: `Bearer ${provider.apiKey.trim()}`;
}
if (provider.appId) {
headers['appid'] = provider.appId;
}
return headers;
};
export const streamChat = async ({
providerId,
model,
appId,
messages,
options,
res,
signal
}) => {
const provider = providers[providerId];
if (!provider) {
res.status(400).json({ success: false, error: '未知模型服务商' });
return;
}
if (!provider.apiKey) {
res.status(400).json({ success: false, error: '模型服务商未配置 API Key' });
return;
}
if (!provider.apiUrl) {
res.status(400).json({ success: false, error: '模型服务商未配置 API 地址' });
return;
}
const payload = {
model: model || provider.model,
messages,
...options,
stream: true
};
const headers = buildHeaders({ ...provider, appId: appId || provider.appId });
const response = await fetch(provider.apiUrl, {
method: 'POST',
headers,
body: JSON.stringify(payload),
signal
});
if (!response.ok) {
const errorText = await response.text();
res.status(500).json({ success: false, error: errorText || `上游请求失败: ${response.status}` });
return;
}
res.status(200);
res.setHeader('Content-Type', 'text/event-stream; charset=utf-8');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
const readable = Readable.fromWeb(response.body);
readable.on('data', (chunk) => {
res.write(chunk);
});
readable.on('end', () => {
res.end();
});
readable.on('error', (error) => {
console.error('LLM 流式代理错误:', error);
res.end();
});
};