feat: 添加大纲写作功能与服务器端改进
- 新增 OutlineWriterPanel 和 OutlineResultPanel 组件 - 重构服务器端数据库接口 (server/db.js) - 添加 LLM 服务模块 (server/llm.js) - 更新配置和设置面板 - 优化文档选择器和素材面板 - 更新部署文档和环境变量示例
This commit is contained in:
851
server/db.js
851
server/db.js
@@ -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;
|
||||
|
||||
413
server/index.js
413
server/index.js
@@ -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
140
server/llm.js
Normal 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();
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user