diff --git a/.env.example b/.env.example
index 6b94f84..1ea3ffc 100644
--- a/.env.example
+++ b/.env.example
@@ -1,28 +1,39 @@
-# AI 写作工坊 - 模型服务商配置
-# 复制此文件为 .env 并填入您的 API Key
+# AI 写作工坊 - 服务端模型与数据库配置
+# 复制此文件为 .env 并填入服务端 API Key
+
+# ========== 服务端 API ==========
+API_PORT=3001
# ========== DeepSeek (默认) ==========
-VITE_DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
-VITE_DEEPSEEK_API_KEY=your_deepseek_api_key_here
-VITE_DEEPSEEK_MODEL=deepseek-chat
+DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
+DEEPSEEK_API_KEY=your_deepseek_api_key_here
+DEEPSEEK_MODEL=deepseek-chat
# ========== 千帆大模型 (百度文心一言) ==========
-# VITE_QIANFAN_API_URL=https://qianfan.baidubce.com/v2/chat/completions
-# VITE_QIANFAN_API_KEY=your_bearer_token_here
-# VITE_QIANFAN_APP_ID=your_app_id_here
-# VITE_QIANFAN_MODEL=ernie-4.0-8k
+# QIANFAN_API_URL=https://qianfan.baidubce.com/v2/chat/completions
+# QIANFAN_API_KEY=your_bearer_token_here
+# QIANFAN_APP_ID=your_app_id_here
+# QIANFAN_MODEL=ernie-4.0-8k
# ========== OpenAI ==========
-# VITE_OPENAI_API_URL=https://api.openai.com/v1/chat/completions
-# VITE_OPENAI_API_KEY=your_openai_api_key_here
-# VITE_OPENAI_MODEL=gpt-4o
+# OPENAI_API_URL=https://api.openai.com/v1/chat/completions
+# OPENAI_API_KEY=your_openai_api_key_here
+# OPENAI_MODEL=gpt-4o
# ========== Claude (Anthropic) ==========
-# VITE_CLAUDE_API_URL=https://api.anthropic.com/v1/messages
-# VITE_CLAUDE_API_KEY=your_claude_api_key_here
-# VITE_CLAUDE_MODEL=claude-3-5-sonnet
+# CLAUDE_API_URL=https://api.anthropic.com/v1/messages
+# CLAUDE_API_KEY=your_claude_api_key_here
+# CLAUDE_MODEL=claude-3-5-sonnet
# ========== 自定义 API 中继 ==========
-# VITE_CUSTOM_API_URL=https://your-api-gateway.com/v1/chat/completions
-# VITE_CUSTOM_API_KEY=your_custom_api_key_here
+# CUSTOM_API_URL=https://your-api-gateway.com/v1/chat/completions
+# CUSTOM_API_KEY=your_custom_api_key_here
+# CUSTOM_MODEL=your_model_name
+
+# ========== 前端配置(仅展示,不含密钥) ==========
+VITE_API_BASE=http://localhost:3001/api
+# VITE_DEEPSEEK_MODEL=deepseek-chat
+# VITE_QIANFAN_MODEL=ernie-4.0-8k
+# VITE_OPENAI_MODEL=gpt-4o
+# VITE_CLAUDE_MODEL=claude-3-5-sonnet
# VITE_CUSTOM_MODEL=your_model_name
diff --git a/README.md b/README.md
index 13ed01a..360ef64 100644
--- a/README.md
+++ b/README.md
@@ -44,6 +44,12 @@ npm run dev
# 访问 http://localhost:3001
```
+如需启用服务端 LLM 代理与 SQLite,请先配置 `.env`,并使用:
+
+```bash
+npm run start
+```
+
### 构建生产版本
```bash
@@ -66,8 +72,7 @@ npm run preview
- Marked (Markdown 解析)
数据存储:
- - SQL.js (浏览器端数据库)
- - LocalStorage (范式配置)
+ - SQLite (服务端数据库)
工具库:
- Axios (HTTP 客户端)
@@ -81,7 +86,7 @@ npm run preview
- IconLibrary (SVG 图标组件库)
AI 集成:
- - DeepSeek API (可扩展)
+ - 服务端 LLM 代理(DeepSeek 等,可扩展)
```
---
@@ -137,7 +142,7 @@ src/
│ └── deepseek.js # DeepSeek API 封装
│
├── db/ # 数据库
-│ └── index.js # SQL.js 封装
+│ └── index.js # 服务端数据库 API 客户端
│
├── styles/ # 样式文件 ⭐
│ ├── design-tokens.css # 设计变量
diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md
index 7aafa14..4d56b19 100644
--- a/docs/DEPLOYMENT.md
+++ b/docs/DEPLOYMENT.md
@@ -2,14 +2,14 @@
## 🏗️ 项目架构
-本项目是一个纯前端 Vue 3 应用,使用 Vite 构建,数据存储在浏览器 IndexedDB 中。
+本项目为 **前端 + Node/Express 服务端** 架构:前端使用 Vite 构建,数据存储在服务端 SQLite,LLM 调用通过服务端代理。
### 技术栈
- **前端框架**: Vue 3 + Composition API
- **构建工具**: Vite
- **状态管理**: Pinia
-- **数据存储**: IndexedDB (sql.js)
-- **API 调用**: 直接调用各 LLM 服务商 API
+- **数据存储**: SQLite(服务端)
+- **API 调用**: 服务端 LLM 代理
## 📦 构建生产版本
@@ -24,7 +24,7 @@ npm run build
npm run preview
```
-构建产物位于 `dist/` 目录,是纯静态文件。
+构建产物位于 `dist/` 目录,前端可静态托管,但仍需单独部署服务端 API。
---
@@ -39,12 +39,8 @@ npm run preview
- 导入 Git 仓库
2. **配置环境变量**
- 在 Vercel 控制台设置:
- ```
- VITE_DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
- VITE_DEEPSEEK_API_KEY=sk-xxx
- VITE_DEEPSEEK_MODEL=deepseek-chat
- ```
+ - 前端:仅需设置 API 基地址(指向自建服务端)
+ - 服务端:配置 LLM 的 API Key 与模型
3. **部署**
- 每次 push 到 main 分支自动部署
@@ -56,7 +52,7 @@ npm run preview
1. 在 Netlify 导入仓库
2. 构建命令: `npm run build`
3. 发布目录: `dist`
-4. 在 Site settings > Environment 设置环境变量
+4. 在 Site settings > Environment 设置前端环境变量(如 `VITE_API_BASE`)
### 方案三:自托管 Nginx
@@ -106,7 +102,7 @@ npm run preview
适合容器化部署。
-1. **Dockerfile**
+1. **Dockerfile(前端静态站点)**
```dockerfile
# 构建阶段
FROM node:20-alpine as builder
@@ -114,8 +110,7 @@ npm run preview
COPY package*.json ./
RUN npm ci
COPY . .
- ARG VITE_DEEPSEEK_API_KEY
- ARG VITE_DEEPSEEK_API_URL
+ ARG VITE_API_BASE
RUN npm run build
# 运行阶段
@@ -128,12 +123,12 @@ npm run preview
2. **构建并运行**
```bash
- docker build \
- --build-arg VITE_DEEPSEEK_API_KEY=sk-xxx \
- --build-arg VITE_DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions \
+ docker build \\
+ --build-arg VITE_API_BASE=http://your-api-host:3001/api \\
-t ai-writer .
docker run -d -p 8080:80 ai-writer
+\n+> ⚠️ 注意:服务端 API 需单独部署(Node/Express + SQLite),并配置 `.env` 中的 LLM 密钥。
```
---
@@ -142,25 +137,17 @@ npm run preview
### API Key 安全
-由于这是纯前端应用,API Key 会被打包到 JS 文件中。有以下解决方案:
-
-1. **内部使用**:如果只在内网使用,风险可控
-
-2. **API 中继**(推荐):
- - 部署一个后端代理服务
- - 前端调用代理,代理转发到 LLM API
- - API Key 只存在于后端
-
-3. **IP 白名单**:
- - 在 LLM 服务商配置 API Key 的 IP 白名单
- - 限制只能从部署服务器的 IP 调用
+当前版本默认通过服务端代理调用 LLM,API Key 不再暴露在前端。
### 环境变量最佳实践
```bash
-# 生产环境 .env.production
-VITE_DEEPSEEK_API_URL=https://your-proxy.com/v1/chat/completions
-VITE_DEEPSEEK_API_KEY=xxx
+# 服务端 .env
+DEEPSEEK_API_URL=https://api.deepseek.com/chat/completions
+DEEPSEEK_API_KEY=xxx
+
+# 前端 .env.production
+VITE_API_BASE=https://your-api-host/api
```
---
@@ -179,8 +166,8 @@ VITE_DEEPSEEK_API_KEY=xxx
┌────────────┴────────────┐
│ │
┌──────▼──────┐ ┌──────▼──────┐
- │ 静态资源 │ │ API 中继 │ (可选)
- │ (dist/) │ │ (后端代理) │
+ │ 静态资源 │ │ Node API │
+ │ (dist/) │ │ + SQLite │
└─────────────┘ └──────┬──────┘
│
┌──────▼──────┐
@@ -217,8 +204,7 @@ jobs:
npm ci
npm run build
env:
- VITE_DEEPSEEK_API_KEY: ${{ secrets.VITE_DEEPSEEK_API_KEY }}
- VITE_DEEPSEEK_API_URL: ${{ secrets.VITE_DEEPSEEK_API_URL }}
+ VITE_API_BASE: ${{ secrets.VITE_API_BASE }}
- name: Deploy to Server
# 根据您的部署目标配置
diff --git a/package.json b/package.json
index ab1c5bf..3467359 100644
--- a/package.json
+++ b/package.json
@@ -20,11 +20,11 @@
"cors": "^2.8.5",
"diff-match-patch": "^1.0.5",
"docx": "^9.5.1",
+ "dotenv": "^16.3.1",
"express": "^4.18.2",
"file-saver": "^2.0.5",
"marked": "^9.1.0",
"pinia": "^2.1.0",
- "sql.js": "^1.13.0",
"vue": "^3.4.0"
},
"devDependencies": {
@@ -32,10 +32,9 @@
"@vitejs/plugin-vue": "^4.5.0",
"autoprefixer": "^10.4.23",
"concurrently": "^8.2.0",
- "dotenv": "^16.3.1",
"playwright": "^1.57.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"vite": "^5.0.0"
}
-}
\ No newline at end of file
+}
diff --git a/server/db.js b/server/db.js
index 79cb315..1d3d5fb 100644
--- a/server/db.js
+++ b/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;
diff --git a/server/index.js b/server/index.js
index 130cf7f..d43e47b 100644
--- a/server/index.js
+++ b/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');
});
diff --git a/server/llm.js b/server/llm.js
new file mode 100644
index 0000000..bd179d9
--- /dev/null
+++ b/server/llm.js
@@ -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();
+ });
+};
diff --git a/src/App.vue b/src/App.vue
index c83e02b..617cd47 100644
--- a/src/App.vue
+++ b/src/App.vue
@@ -18,6 +18,11 @@
+
+
+
+
+
@@ -31,8 +36,8 @@
-
-
+
+
appStore.currentPage)
diff --git a/src/api/deepseek.js b/src/api/deepseek.js
index 3f96750..3cae262 100644
--- a/src/api/deepseek.js
+++ b/src/api/deepseek.js
@@ -1,52 +1,38 @@
-import { GHOSTWRITER_SYSTEM_PROMPT, buildSystemPrompt } from '../utils/promptBuilder.js'
+import { buildSystemPrompt } from '../utils/promptBuilder.js'
+
+const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:3001/api'
class DeepSeekAPI {
constructor(config) {
- this.baseURL = config.url
- this.apiKey = config.key
- this.model = config.model || 'deepseek-chat'
- this.appId = config.appId || '' // 千帆大模型需要
- console.log('DeepSeekAPI 已初始化:', { baseURL: this.baseURL, model: this.model })
+ this.providerId = config.providerId
+ this.model = config.model
+ this.appId = config.appId || ''
+ this.baseURL = `${API_BASE}/llm/stream`
+ console.log('LLM 代理已初始化:', { providerId: this.providerId, model: this.model })
}
async _streamRequest(messages, options = {}, onContent) {
- const authHeader = this.apiKey.trim().startsWith('Bearer ')
- ? this.apiKey.trim()
- : `Bearer ${this.apiKey.trim()}`;
-
- // 构建 headers
- const headers = {
- 'Content-Type': 'application/json',
- 'Authorization': authHeader
- }
-
- // 千帆大模型需要 appid header
- if (this.appId) {
- headers['appid'] = this.appId
- }
-
- console.log('DeepSeekAPI: Starting stream request...', { messagesLength: messages.length })
+ console.log('LLM 代理: Starting stream request...', { messagesLength: messages.length })
try {
const response = await fetch(this.baseURL, {
method: 'POST',
- headers,
+ headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
+ providerId: this.providerId,
model: this.model,
+ appId: this.appId,
messages,
- stream: true,
- ...options
+ options
})
})
if (!response.ok) {
const errorText = await response.text()
- console.error('DeepSeekAPI: HTTP Error', response.status, errorText)
+ console.error('LLM 代理: HTTP Error', response.status, errorText)
throw new Error(`API请求失败: ${response.status} ${errorText}`)
}
- console.log('DeepSeekAPI: Stream connection established.')
-
const reader = response.body.getReader()
const decoder = new TextDecoder()
let buffer = ''
@@ -54,54 +40,48 @@ class DeepSeekAPI {
while (true) {
const { done, value } = await reader.read()
if (done) {
- console.log('DeepSeekAPI: Stream finished by server.')
+ console.log('LLM 代理: Stream finished by server.')
break
}
buffer += decoder.decode(value, { stream: true })
-
- // Split by newline
- let lines = buffer.split('\n')
- // Keep the last part (potential incomplete line) in buffer
+
+ const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
const trimmedLine = line.trim()
if (!trimmedLine) continue
-
+
if (trimmedLine.startsWith('data:')) {
- const data = trimmedLine.substring(5).trim() // Remove 'data:' prefix
-
+ const data = trimmedLine.substring(5).trim()
+
if (data === '[DONE]') {
- console.log('DeepSeekAPI: Received [DONE] signal.')
+ console.log('LLM 代理: Received [DONE] signal.')
return
}
try {
const parsed = JSON.parse(data)
const content = parsed.choices?.[0]?.delta?.content || ''
- if (content) {
- // console.log('DeepSeekAPI: Received content chunk:', content.length) // Too verbose for large text
- if (onContent) {
- onContent(content)
- }
+ if (content && onContent) {
+ onContent(content)
}
} catch (e) {
- console.warn('DeepSeekAPI: JSON parse error for line:', trimmedLine, e)
+ console.warn('LLM 代理: JSON parse error for line:', trimmedLine, e)
}
}
}
}
} catch (err) {
- console.error('DeepSeekAPI: Stream processing error:', err);
- throw err;
+ console.error('LLM 代理: Stream processing error:', err)
+ throw err
}
}
async generateContent(prompt, onContent, options = {}) {
- // 构建系统提示词(支持范式专用 Prompt)
const systemPrompt = buildSystemPrompt(options.paradigm)
-
+
return this._streamRequest([
{
role: 'system',
@@ -135,7 +115,7 @@ class DeepSeekAPI {
},
{
role: 'user',
- content: `请分析以下文本的风格:\n\n${text.substring(0, 2000)}` // Limit context length
+ content: `请分析以下文本的风格:\n\n${text.substring(0, 2000)}`
}
], { temperature: 0.2 }, onContent)
}
diff --git a/src/components/AnalysisPanel.vue b/src/components/AnalysisPanel.vue
index 9c3bb9f..ca65b10 100644
--- a/src/components/AnalysisPanel.vue
+++ b/src/components/AnalysisPanel.vue
@@ -175,7 +175,6 @@ import { storeToRefs } from 'pinia'
import { useAppStore } from '../stores/app'
import { useDatabaseStore } from '../stores/database.js'
import { useParadigmStore } from '../stores/paradigm.js'
-import DeepSeekAPI from '../api/deepseek.js'
import { getParadigmList } from '../config/paradigms.js'
import RequirementParserPanel from './RequirementParserPanel.vue'
import IconLibrary from './icons/IconLibrary.vue'
@@ -303,6 +302,18 @@ const openEditModal = (paradigm) => {
form.tagsInput = (paradigm.tags || []).join(', ')
form.tagClass = paradigm.tagClass || 'bg-blue-900/30 text-blue-300'
form.specializedPrompt = paradigm.specializedPrompt || ''
+ // 加载章节大纲
+ form.sections = (paradigm.sections || []).map(s => ({
+ title: s.title || '',
+ description: s.description || '',
+ weight: s.weight || 20
+ }))
+ // 加载检查规范
+ form.expertGuidelines = (paradigm.expertGuidelines || []).map(g =>
+ typeof g === 'object'
+ ? { title: g.title || '', description: g.description || '', scope: g.scope || 'document' }
+ : { title: '', description: g, scope: 'document' }
+ )
}
diff --git a/src/components/ArticleRewritePanel.vue b/src/components/ArticleRewritePanel.vue
index 8795733..2f1bfde 100644
--- a/src/components/ArticleRewritePanel.vue
+++ b/src/components/ArticleRewritePanel.vue
@@ -835,10 +835,10 @@ const saveToDocument = async () => {
// 保存版本历史
const changeNote = `范式润色:修改了 ${changedCount} 处`
- saveDocumentVersion(sourceDocId.value, articleContent.value, changeNote)
+ await saveDocumentVersion(sourceDocId.value, articleContent.value, changeNote)
// 更新文稿内容
- updateDocument(sourceDocId.value, {
+ await updateDocument(sourceDocId.value, {
content: articleContent.value
})
diff --git a/src/components/ComparePanel.vue b/src/components/ComparePanel.vue
index f318945..9b11d06 100644
--- a/src/components/ComparePanel.vue
+++ b/src/components/ComparePanel.vue
@@ -1084,10 +1084,10 @@ const saveRightContent = async () => {
if (stats.removed > 0) changeNote += `,删除了 ${stats.removed} 处`
// 保存新版本
- const versionNumber = saveDocumentVersion(rightSourceDocId.value, rightContent.value, changeNote)
+ const versionNumber = await saveDocumentVersion(rightSourceDocId.value, rightContent.value, changeNote)
// 同时更新文稿主内容
- updateDocument(rightSourceDocId.value, { content: rightContent.value })
+ await updateDocument(rightSourceDocId.value, { content: rightContent.value })
// 更新原始内容为当前内容(标记为已保存)
rightOriginalContent.value = rightContent.value
@@ -1127,10 +1127,10 @@ const saveLeftContent = async () => {
if (stats.removed > 0) changeNote += `,删除了 ${stats.removed} 处`
// 保存新版本
- const versionNumber = saveDocumentVersion(leftSourceDocId.value, leftContent.value, changeNote)
+ const versionNumber = await saveDocumentVersion(leftSourceDocId.value, leftContent.value, changeNote)
// 同时更新文稿主内容
- updateDocument(leftSourceDocId.value, { content: leftContent.value })
+ await updateDocument(leftSourceDocId.value, { content: leftContent.value })
// 更新原始内容为当前内容(标记为已保存)
leftOriginalContent.value = leftContent.value
diff --git a/src/components/DocumentSelectorModal.vue b/src/components/DocumentSelectorModal.vue
index f2472d8..e795474 100644
--- a/src/components/DocumentSelectorModal.vue
+++ b/src/components/DocumentSelectorModal.vue
@@ -115,7 +115,7 @@ const filteredDocuments = computed(() => {
const loadDocuments = async () => {
try {
const { getAllDocuments } = await import('../db/index.js')
- documents.value = getAllDocuments()
+ documents.value = await getAllDocuments()
} catch (error) {
console.error('加载文稿失败:', error)
}
diff --git a/src/components/DocumentVersionPanel.vue b/src/components/DocumentVersionPanel.vue
index aa16b22..299656b 100644
--- a/src/components/DocumentVersionPanel.vue
+++ b/src/components/DocumentVersionPanel.vue
@@ -170,7 +170,7 @@
-
diff --git a/src/components/DocumentsPanel.vue b/src/components/DocumentsPanel.vue
index 6d6013f..53f6d2d 100644
--- a/src/components/DocumentsPanel.vue
+++ b/src/components/DocumentsPanel.vue
@@ -179,7 +179,7 @@ const getCountByStatus = (status) => {
const loadDocuments = async () => {
try {
const { getAllDocuments } = await import('../db/index.js')
- documents.value = getAllDocuments()
+ documents.value = await getAllDocuments()
} catch (error) {
console.error('加载文稿失败:', error)
}
@@ -207,7 +207,7 @@ const toggleVersionPanel = () => {
const createNewDocument = async () => {
try {
const { createDocument } = await import('../db/index.js')
- const id = createDocument({
+ const id = await createDocument({
title: '未命名文稿',
content: '',
status: 'draft'
@@ -237,7 +237,7 @@ const duplicateDocument = async () => {
try {
const { createDocument } = await import('../db/index.js')
- createDocument({
+ await createDocument({
title: doc.title + ' (副本)',
content: doc.content,
paradigmId: doc.paradigm_id,
@@ -262,7 +262,7 @@ const deleteSelectedDocument = async () => {
try {
const { deleteDocument } = await import('../db/index.js')
- deleteDocument(selectedDocId.value)
+ await deleteDocument(selectedDocId.value)
selectedDocId.value = null
showDeleteConfirm.value = false
await loadDocuments()
diff --git a/src/components/GlobalSidebar.vue b/src/components/GlobalSidebar.vue
index 61f6102..d90b9fd 100644
--- a/src/components/GlobalSidebar.vue
+++ b/src/components/GlobalSidebar.vue
@@ -64,6 +64,7 @@ const navItems = [
{ id: 'mimicWriter', label: '以稿写稿', icon: 'copy' },
{ id: 'analysis', label: '范式库', icon: 'analysis' },
{ id: 'paradigmWriter', label: '范式写作', icon: 'article' },
+ { id: 'outlineWriter', label: '提纲写作', icon: 'folder' },
{ 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 5e759ef..a88979d 100644
--- a/src/components/HomePage.vue
+++ b/src/components/HomePage.vue
@@ -91,7 +91,7 @@
diff --git a/src/components/MainContent.vue b/src/components/MainContent.vue
index a782c97..c5a1d16 100644
--- a/src/components/MainContent.vue
+++ b/src/components/MainContent.vue
@@ -374,15 +374,110 @@
-
-
- 提示:这是 AI 调用时的核心指令。完整的 Prompt 应包含角色、目标、步骤和输出格式。
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 提示:章节大纲用于"范式分段写作",检查规范用于自动验证生成内容是否合规。
+
@@ -599,6 +694,54 @@ const colorOptions = [
{ label: '青色', class: 'bg-cyan-900/30 text-cyan-300' }
]
+// 拖拽状态
+const dragState = ref({
+ type: null, // 'section' | 'guideline'
+ dragIndex: null,
+ overIndex: null
+})
+
+// 拖拽开始
+const handleDragStart = (event, index, type) => {
+ dragState.value.type = type
+ dragState.value.dragIndex = index
+ event.dataTransfer.effectAllowed = 'move'
+}
+
+// 拖拽经过
+const handleDragOver = (event, index, type) => {
+ if (dragState.value.type === type) {
+ dragState.value.overIndex = index
+ }
+}
+
+// 放置
+const handleDrop = (event, targetIndex, type) => {
+ if (dragState.value.type !== type) return
+
+ const sourceIndex = dragState.value.dragIndex
+ if (sourceIndex === targetIndex) return
+
+ const list = type === 'section'
+ ? paradigmEditState.value.editForm.sections
+ : paradigmEditState.value.editForm.expertGuidelines
+
+ if (!list) return
+
+ // 移动元素
+ const [movedItem] = list.splice(sourceIndex, 1)
+ list.splice(targetIndex, 0, movedItem)
+
+ handleDragEnd()
+}
+
+// 拖拽结束
+const handleDragEnd = () => {
+ dragState.value.type = null
+ dragState.value.dragIndex = null
+ dragState.value.overIndex = null
+}
+
// 关闭编辑
const closeParadigmEdit = () => {
paradigmEditState.value.isEditing = false
@@ -606,6 +749,40 @@ const closeParadigmEdit = () => {
paradigmEditState.value.editingParadigmId = null
}
+// 添加章节
+const addSection = () => {
+ if (!paradigmEditState.value.editForm.sections) {
+ paradigmEditState.value.editForm.sections = []
+ }
+ paradigmEditState.value.editForm.sections.push({
+ title: '',
+ description: '',
+ weight: 20
+ })
+}
+
+// 删除章节
+const removeSection = (index) => {
+ paradigmEditState.value.editForm.sections.splice(index, 1)
+}
+
+// 添加检查规范
+const addGuideline = () => {
+ if (!paradigmEditState.value.editForm.expertGuidelines) {
+ paradigmEditState.value.editForm.expertGuidelines = []
+ }
+ paradigmEditState.value.editForm.expertGuidelines.push({
+ title: '',
+ description: '',
+ scope: 'document'
+ })
+}
+
+// 删除检查规范
+const removeGuideline = (index) => {
+ paradigmEditState.value.editForm.expertGuidelines.splice(index, 1)
+}
+
// 保存范式编辑
const saveParadigmEdit = async () => {
const form = paradigmEditState.value.editForm
@@ -621,6 +798,8 @@ const saveParadigmEdit = async () => {
tags: form.tagsInput.split(',').map(t => t.trim()).filter(t => t),
tagClass: form.tagClass,
specializedPrompt: form.specializedPrompt,
+ sections: (form.sections || []).filter(s => s.title), // 过滤空标题
+ expertGuidelines: (form.expertGuidelines || []).filter(g => g.title || g.description),
isCustom: true,
createdAt: paradigmEditState.value.isAddMode ? new Date().toISOString() : undefined
}
@@ -669,7 +848,7 @@ const saveDocument = async () => {
try {
const { updateDocument } = await import('../db/index.js')
- updateDocument(currentDocument.value.id, {
+ await updateDocument(currentDocument.value.id, {
title: documentTitle.value,
content: documentContent.value
})
@@ -693,7 +872,7 @@ const changeDocStatus = async (status) => {
try {
const { updateDocument } = await import('../db/index.js')
- updateDocument(currentDocument.value.id, { status })
+ await updateDocument(currentDocument.value.id, { status })
currentDocument.value.status = status
} catch (error) {
console.error('修改状态失败:', error)
diff --git a/src/components/MaterialsPanel.vue b/src/components/MaterialsPanel.vue
index 4ffc9a9..a52abdf 100644
--- a/src/components/MaterialsPanel.vue
+++ b/src/components/MaterialsPanel.vue
@@ -371,7 +371,7 @@ const getTypeIcon = (type) => {
const loadMaterials = async () => {
try {
const { getAllReferences } = await import('../db/index.js')
- materials.value = getAllReferences()
+ materials.value = await getAllReferences()
} catch (error) {
console.error('加载素材失败:', error)
}
@@ -454,9 +454,9 @@ const saveMaterial = async () => {
}
if (isAddMode.value) {
- addReference(materialData)
+ await addReference(materialData)
} else {
- updateReference(editForm.id, materialData)
+ await updateReference(editForm.id, materialData)
}
await loadMaterials()
@@ -484,7 +484,7 @@ const deleteMaterial = async () => {
try {
const { deleteReference } = await import('../db/index.js')
- deleteReference(selectedId.value)
+ await deleteReference(selectedId.value)
selectedId.value = null
showDeleteConfirm.value = false
await loadMaterials()
diff --git a/src/components/OutlineResultPanel.vue b/src/components/OutlineResultPanel.vue
new file mode 100644
index 0000000..9851d55
--- /dev/null
+++ b/src/components/OutlineResultPanel.vue
@@ -0,0 +1,302 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
输入大纲并点击生成
+
生成的内容将在这里实时预览
+
+
+
+
+
+
+ {{ getHeadingPrefix(section.level) }}{{ section.title }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/components/OutlineWriterPanel.vue b/src/components/OutlineWriterPanel.vue
new file mode 100644
index 0000000..a9b0425
--- /dev/null
+++ b/src/components/OutlineWriterPanel.vue
@@ -0,0 +1,845 @@
+
+
+
+
+
+
+
diff --git a/src/components/RequirementParserPanel.vue b/src/components/RequirementParserPanel.vue
index 7bfe77c..6b9df3c 100644
--- a/src/components/RequirementParserPanel.vue
+++ b/src/components/RequirementParserPanel.vue
@@ -275,10 +275,11 @@
diff --git a/src/db/index.js b/src/db/index.js
index ceaff69..0dafa46 100644
--- a/src/db/index.js
+++ b/src/db/index.js
@@ -1,546 +1,85 @@
// ============================================
-// SQLite 数据库服务层
-// ============================================
-// 使用 sql.js 在浏览器端运行 SQLite
-// 数据持久化到 IndexedDB
-
-// 数据库实例
-let db = null
-let SQL = null
-
-// IndexedDB 配置
-const DB_NAME = 'AIWriterWorkshop'
-const DB_STORE = 'database'
-const DB_KEY = 'sqlite_db'
-
-// ============================================
-// 数据库初始化
+// 服务端 SQLite API 客户端
// ============================================
-/**
- * 从 CDN 加载 sql.js
- */
-const loadSqlJs = () => {
- return new Promise((resolve, reject) => {
- // 如果已经加载过
- if (window.initSqlJs) {
- resolve(window.initSqlJs)
- return
- }
+const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:3001/api'
- // 动态加载脚本
- const script = document.createElement('script')
- script.src = 'https://sql.js.org/dist/sql-wasm.js'
- script.async = true
-
- script.onload = () => {
- if (window.initSqlJs) {
- resolve(window.initSqlJs)
- } else {
- reject(new Error('sql.js 加载失败'))
- }
- }
-
- script.onerror = () => reject(new Error('sql.js 脚本加载失败'))
- document.head.appendChild(script)
+const requestJson = async (path, options = {}) => {
+ const response = await fetch(`${API_BASE}${path}`, {
+ headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
+ ...options
})
+
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(errorText || `请求失败: ${response.status}`)
+ }
+
+ const result = await response.json()
+ if (result && result.success === false) {
+ throw new Error(result.error || '请求失败')
+ }
+
+ return result?.data
}
export const initDatabase = async () => {
- if (db) return db
-
- try {
- // 从 CDN 加载 sql.js
- const initSqlJs = await loadSqlJs()
-
- // 加载 sql.js WASM
- SQL = await initSqlJs({
- locateFile: file => `https://sql.js.org/dist/${file}`
- })
-
- // 尝试从 IndexedDB 加载现有数据库
- const savedData = await loadFromIndexedDB()
-
- if (savedData) {
- db = new SQL.Database(savedData)
- console.log('📦 从 IndexedDB 加载数据库成功')
- // 对已有数据库执行迁移以添加新字段
- migrateDatabase()
- } else {
- db = new SQL.Database()
- console.log('🆕 创建新数据库')
- // 初始化表结构
- await initTables()
- // 导入默认数据
- await importDefaultData()
- }
-
- return db
- } catch (error) {
- console.error('❌ 数据库初始化失败:', error)
- throw error
- }
+ await requestJson('/health')
+ return true
}
-/**
- * 初始化数据表结构
- */
-const initTables = async () => {
- // 素材表
- db.run(`
- 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
- )
- `)
+export const saveToIndexedDB = async () => true
- // 素材摘录表
- db.run(`
- 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
- )
- `)
-
- // 范式表
- db.run(`
- 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
- )
- `)
-
- // 维度集表
- db.run(`
- 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
- )
- `)
-
- // 维度表
- db.run(`
- 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
- )
- `)
-
- // 用户配置表
- db.run(`
- CREATE TABLE IF NOT EXISTS user_config (
- key TEXT PRIMARY KEY,
- value TEXT,
- updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
- )
- `)
-
- // 分析历史表
- db.run(`
- 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
- )
- `)
-
- // 文稿记录表
- db.run(`
- 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
- )
- `)
-
- // 文稿版本历史表
- db.run(`
- 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
- )
- `)
-
- console.log('✅ 数据表初始化完成')
-
- // 运行数据库迁移
- migrateDatabase()
+export const query = () => {
+ throw new Error('服务端模式不支持直接 SQL 查询')
}
-/**
- * 数据库迁移:添加范式表的新字段
- */
-const migrateDatabase = () => {
- try {
- // 检查是否已存在 specialized_prompt 列
- const columns = query(`
- SELECT name FROM pragma_table_info('paradigms')
- `).map(row => row.name)
-
- const neededColumns = ['specialized_prompt', 'expert_guidelines', 'outline_template', 'recommended_tags']
- const missingColumns = neededColumns.filter(col => !columns.includes(col))
-
- if (missingColumns.length > 0) {
- console.log('🔄 检测到数据库需要迁移,添加新字段...')
-
- if (missingColumns.includes('specialized_prompt')) {
- execute('ALTER TABLE paradigms ADD COLUMN specialized_prompt TEXT')
- }
- if (missingColumns.includes('expert_guidelines')) {
- execute('ALTER TABLE paradigms ADD COLUMN expert_guidelines TEXT')
- }
- if (missingColumns.includes('outline_template')) {
- execute('ALTER TABLE paradigms ADD COLUMN outline_template TEXT')
- }
- if (missingColumns.includes('recommended_tags')) {
- execute('ALTER TABLE paradigms ADD COLUMN recommended_tags TEXT')
- }
-
- console.log('✅ 数据库迁移完成')
- }
- } catch (error) {
- console.error('❌ 数据库迁移失败:', error)
- }
+export const queryOne = () => {
+ throw new Error('服务端模式不支持直接 SQL 查询')
}
-// ============================================
-// IndexedDB 持久化
-// ============================================
-
-/**
- * 保存数据库到 IndexedDB
- */
-export const saveToIndexedDB = async () => {
- if (!db) return
-
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(DB_NAME, 1)
-
- request.onerror = () => reject(request.error)
-
- request.onupgradeneeded = (event) => {
- const idb = event.target.result
- if (!idb.objectStoreNames.contains(DB_STORE)) {
- idb.createObjectStore(DB_STORE)
- }
- }
-
- request.onsuccess = (event) => {
- const idb = event.target.result
- const transaction = idb.transaction([DB_STORE], 'readwrite')
- const store = transaction.objectStore(DB_STORE)
-
- const data = db.export()
- const buffer = new Uint8Array(data)
-
- const putRequest = store.put(buffer, DB_KEY)
- putRequest.onsuccess = () => {
- console.log('💾 数据库已保存到 IndexedDB')
- resolve()
- }
- putRequest.onerror = () => reject(putRequest.error)
- }
- })
+export const execute = () => {
+ throw new Error('服务端模式不支持直接 SQL 执行')
}
-/**
- * 从 IndexedDB 加载数据库
- */
-const loadFromIndexedDB = async () => {
- return new Promise((resolve, reject) => {
- const request = indexedDB.open(DB_NAME, 1)
-
- request.onerror = () => reject(request.error)
-
- request.onupgradeneeded = (event) => {
- const idb = event.target.result
- if (!idb.objectStoreNames.contains(DB_STORE)) {
- idb.createObjectStore(DB_STORE)
- }
- }
-
- request.onsuccess = (event) => {
- const idb = event.target.result
- const transaction = idb.transaction([DB_STORE], 'readonly')
- const store = transaction.objectStore(DB_STORE)
-
- const getRequest = store.get(DB_KEY)
- getRequest.onsuccess = () => {
- resolve(getRequest.result || null)
- }
- getRequest.onerror = () => resolve(null)
- }
- })
-}
-
-// ============================================
-// 通用 CRUD 操作
-// ============================================
-
-/**
- * 执行查询并返回结果
- */
-export const query = (sql, params = []) => {
- if (!db) throw new Error('数据库未初始化')
-
- try {
- const stmt = db.prepare(sql)
- stmt.bind(params)
-
- const results = []
- while (stmt.step()) {
- results.push(stmt.getAsObject())
- }
- stmt.free()
-
- return results
- } catch (error) {
- console.error('查询失败:', sql, error)
- throw error
- }
-}
-
-/**
- * 执行单条查询
- */
-export const queryOne = (sql, params = []) => {
- const results = query(sql, params)
- return results.length > 0 ? results[0] : null
-}
-
-/**
- * 执行更新/插入操作
- */
-export const execute = (sql, params = []) => {
- if (!db) throw new Error('数据库未初始化')
-
- try {
- db.run(sql, params)
- // 自动保存到 IndexedDB
- saveToIndexedDB()
- return true
- } catch (error) {
- console.error('执行失败:', sql, error)
- throw error
- }
-}
-
-/**
- * 批量执行
- */
-export const executeBatch = (statements) => {
- if (!db) throw new Error('数据库未初始化')
-
- try {
- statements.forEach(({ sql, params = [] }) => {
- db.run(sql, params)
- })
- saveToIndexedDB()
- return true
- } catch (error) {
- console.error('批量执行失败:', error)
- throw error
- }
+export const executeBatch = () => {
+ throw new Error('服务端模式不支持直接 SQL 批量执行')
}
// ============================================
// 素材库 CRUD
// ============================================
-/**
- * 获取所有素材
- */
-export const getAllReferences = () => {
- const refs = query('SELECT * FROM materials ORDER BY created_at DESC')
-
- return refs.map(ref => ({
- ...ref,
- tags: ref.tags ? JSON.parse(ref.tags) : [],
- relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [],
- excerpts: getExcerptsByReferenceId(ref.id)
- }))
+export const getAllReferences = async () => {
+ return requestJson('/references')
}
-/**
- * 根据ID获取素材
- */
-export const getReferenceById = (id) => {
- const ref = queryOne('SELECT * FROM materials WHERE id = ?', [id])
- if (!ref) return null
-
- return {
- ...ref,
- tags: ref.tags ? JSON.parse(ref.tags) : [],
- relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [],
- excerpts: getExcerptsByReferenceId(ref.id)
- }
+export const getReferenceById = async (id) => {
+ return requestJson(`/references/${id}`)
}
-/**
- * 根据类型获取素材
- */
-export const getReferencesByType = (type) => {
- const refs = query('SELECT * FROM materials WHERE type = ? ORDER BY created_at DESC', [type])
-
- return refs.map(ref => ({
- ...ref,
- tags: ref.tags ? JSON.parse(ref.tags) : [],
- relatedDimensionSets: ref.related_dimension_sets ? JSON.parse(ref.related_dimension_sets) : [],
- excerpts: getExcerptsByReferenceId(ref.id)
- }))
+export const getReferencesByType = async (type) => {
+ return requestJson(`/references?type=${encodeURIComponent(type)}`)
}
-/**
- * 获取素材的摘录
- */
-const getExcerptsByReferenceId = (referenceId) => {
- const excerpts = query('SELECT * FROM reference_excerpts WHERE reference_id = ?', [referenceId])
-
- return excerpts.map(e => ({
- ...e,
- applicableDimensions: e.applicable_dimensions ? JSON.parse(e.applicable_dimensions) : []
- }))
+export const addReference = async (reference) => {
+ const data = await requestJson('/references', {
+ method: 'POST',
+ body: JSON.stringify(reference)
+ })
+ return data?.id
}
-/**
- * 添加素材
- */
-export const addReference = (reference) => {
- const id = reference.id || `ref-${Date.now()}`
-
- execute(`
- INSERT INTO materials (id, type, title, source, date, tags, related_dimension_sets, is_default)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
- `, [
- 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) {
- reference.excerpts.forEach((excerpt, index) => {
- execute(`
- INSERT INTO reference_excerpts (id, reference_id, topic, content, applicable_dimensions, use_for)
- VALUES (?, ?, ?, ?, ?, ?)
- `, [
- excerpt.id || `${id}-excerpt-${index}`,
- id,
- excerpt.topic,
- excerpt.content,
- JSON.stringify(excerpt.applicableDimensions || []),
- excerpt.useFor || null
- ])
- })
- }
-
- return id
-}
-
-/**
- * 更新素材
- */
-export const 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)
-
- execute(`UPDATE materials SET ${setClauses.join(', ')} WHERE id = ?`, params)
-
+export const updateReference = async (id, updates) => {
+ await requestJson(`/references/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(updates)
+ })
return true
}
-/**
- * 删除素材
- */
-export const deleteReference = (id) => {
- execute('DELETE FROM reference_excerpts WHERE reference_id = ?', [id])
- execute('DELETE FROM materials WHERE id = ?', [id])
+export const deleteReference = async (id) => {
+ await requestJson(`/references/${id}`, { method: 'DELETE' })
return true
}
@@ -548,131 +87,32 @@ export const deleteReference = (id) => {
// 范式 CRUD
// ============================================
-/**
- * 获取所有范式
- */
-export const getAllParadigms = () => {
- const paradigms = query('SELECT * FROM paradigms ORDER BY is_custom ASC, created_at DESC')
-
- return paradigms.map(p => ({
- ...p,
- tags: p.tags ? JSON.parse(p.tags) : [],
- systemConstraints: p.system_constraints ? JSON.parse(p.system_constraints) : [],
- customDimensions: p.custom_dimensions ? JSON.parse(p.custom_dimensions) : null,
- logicParadigms: p.logic_paradigms ? JSON.parse(p.logic_paradigms) : null,
- selectedRefs: p.selected_refs ? JSON.parse(p.selected_refs) : [],
- specializedPrompt: p.specialized_prompt || null,
- expertGuidelines: p.expert_guidelines ? JSON.parse(p.expert_guidelines) : null,
- outlineTemplate: p.outline_template || null,
- recommendedTags: p.recommended_tags ? JSON.parse(p.recommended_tags) : [],
- isCustom: p.is_custom === 1,
- autoMatchRefs: p.auto_match_refs === 1
- }))
+export const getAllParadigms = async () => {
+ return requestJson('/paradigms')
}
-/**
- * 添加范式
- */
-export const addParadigm = (paradigm) => {
- const id = paradigm.id || `paradigm-${Date.now()}`
-
- execute(`
- 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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
- `, [
- 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
- ])
-
- return id
+export const getParadigmById = async (id) => {
+ return requestJson(`/paradigms/${id}`)
}
-/**
- * 更新范式
- */
-export const updateParadigm = (id, updates) => {
- const setClauses = []
- const params = []
-
- const fieldMap = {
- name: 'name',
- icon: 'icon',
- description: 'description',
- tagClass: 'tag_class',
- dimensionSetId: 'dimension_set_id',
- autoMatchRefs: 'auto_match_refs'
- }
-
- Object.entries(updates).forEach(([key, value]) => {
- if (fieldMap[key]) {
- setClauses.push(`${fieldMap[key]} = ?`)
- params.push(key === 'autoMatchRefs' ? (value ? 1 : 0) : value)
- }
+export const addParadigm = async (paradigm) => {
+ const data = await requestJson('/paradigms', {
+ method: 'POST',
+ body: JSON.stringify(paradigm)
})
+ return data?.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.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)
-
- execute(`UPDATE paradigms SET ${setClauses.join(', ')} WHERE id = ?`, params)
-
+export const updateParadigm = async (id, updates) => {
+ await requestJson(`/paradigms/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(updates)
+ })
return true
}
-/**
- * 删除范式
- */
-export const deleteParadigm = (id) => {
- execute('DELETE FROM paradigms WHERE id = ?', [id])
+export const deleteParadigm = async (id) => {
+ await requestJson(`/paradigms/${id}`, { method: 'DELETE' })
return true
}
@@ -680,32 +120,16 @@ export const deleteParadigm = (id) => {
// 用户配置 CRUD
// ============================================
-/**
- * 获取配置
- */
-export const getConfig = (key, defaultValue = null) => {
- const result = queryOne('SELECT value FROM user_config WHERE key = ?', [key])
- if (!result) return defaultValue
-
- try {
- return JSON.parse(result.value)
- } catch {
- return result.value
- }
+export const getConfig = async (key, defaultValue = null) => {
+ const data = await requestJson(`/config/${encodeURIComponent(key)}`)
+ return data ?? defaultValue
}
-/**
- * 设置配置
- */
-export const setConfig = (key, value) => {
- const valueStr = typeof value === 'string' ? value : JSON.stringify(value)
-
- execute(`
- INSERT INTO user_config (key, value, updated_at)
- VALUES (?, ?, CURRENT_TIMESTAMP)
- ON CONFLICT(key) DO UPDATE SET value = ?, updated_at = CURRENT_TIMESTAMP
- `, [key, valueStr, valueStr])
-
+export const setConfig = async (key, value) => {
+ await requestJson(`/config/${encodeURIComponent(key)}`, {
+ method: 'PUT',
+ body: JSON.stringify({ value })
+ })
return true
}
@@ -713,217 +137,104 @@ export const setConfig = (key, value) => {
// 文稿 CRUD
// ============================================
-/**
- * 获取所有文稿
- */
-export const getAllDocuments = () => {
- const docs = query('SELECT * FROM documents ORDER BY updated_at DESC')
-
- return docs.map(doc => ({
- ...doc,
- tags: doc.tags ? JSON.parse(doc.tags) : [],
- selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : []
- }))
+export const getAllDocuments = async () => {
+ return requestJson('/documents')
}
-/**
- * 根据ID获取文稿
- */
-export const getDocumentById = (id) => {
- const doc = queryOne('SELECT * FROM documents WHERE id = ?', [id])
- if (!doc) return null
-
- return {
- ...doc,
- tags: doc.tags ? JSON.parse(doc.tags) : [],
- selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : [],
- versions: getDocumentVersions(id)
- }
+export const getDocumentById = async (id) => {
+ return requestJson(`/documents/${id}`)
}
-/**
- * 获取文稿版本历史
- */
-export const getDocumentVersions = (documentId) => {
- return query('SELECT * FROM document_versions WHERE document_id = ? ORDER BY version_number DESC', [documentId])
+export const getDocumentVersions = async (documentId) => {
+ return requestJson(`/documents/${documentId}/versions`)
}
-/**
- * 创建文稿
- */
-export const createDocument = (document) => {
- const id = document.id || `doc-${Date.now()}`
-
- execute(`
- INSERT INTO documents (id, title, content, paradigm_id, dimension_set_id, selected_refs, status, word_count, tags)
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
- `, [
- 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 const createDocument = async (document) => {
+ const data = await requestJson('/documents', {
+ method: 'POST',
+ body: JSON.stringify(document)
+ })
+ return data?.id
}
-/**
- * 更新文稿
- */
-export const 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.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)
-
- execute(`UPDATE documents SET ${setClauses.join(', ')} WHERE id = ?`, params)
-
+export const updateDocument = async (id, updates) => {
+ await requestJson(`/documents/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(updates)
+ })
return true
}
-/**
- * 保存文稿版本
- */
-export const saveDocumentVersion = (documentId, content, changeNote = '') => {
- // 获取当前最大版本号
- const result = queryOne('SELECT MAX(version_number) as max_version FROM document_versions WHERE document_id = ?', [documentId])
- const nextVersion = (result?.max_version || 0) + 1
-
- execute(`
- INSERT INTO document_versions (document_id, content, version_number, change_note)
- VALUES (?, ?, ?, ?)
- `, [documentId, content, nextVersion, changeNote])
-
- return nextVersion
+export const saveDocumentVersion = async (documentId, content, changeNote = '') => {
+ const data = await requestJson(`/documents/${documentId}/versions`, {
+ method: 'POST',
+ body: JSON.stringify({ content, changeNote })
+ })
+ return data?.versionNumber
}
-/**
- * 删除文稿
- */
-export const deleteDocument = (id) => {
- execute('DELETE FROM document_versions WHERE document_id = ?', [id])
- execute('DELETE FROM documents WHERE id = ?', [id])
+export const deleteDocument = async (id) => {
+ await requestJson(`/documents/${id}`, { method: 'DELETE' })
return true
}
-/**
- * 根据状态筛选文稿
- */
-export const getDocumentsByStatus = (status) => {
- const docs = query('SELECT * FROM documents WHERE status = ? ORDER BY updated_at DESC', [status])
+export const clearDocuments = async () => {
+ await requestJson('/documents/clear', { method: 'POST' })
+ return true
+}
- return docs.map(doc => ({
- ...doc,
- tags: doc.tags ? JSON.parse(doc.tags) : [],
- selectedRefs: doc.selected_refs ? JSON.parse(doc.selected_refs) : []
- }))
+export const getDocumentsByStatus = async (status) => {
+ return requestJson(`/documents?status=${encodeURIComponent(status)}`)
}
// ============================================
// 数据导入/导出
// ============================================
-/**
- * 导出数据库为二进制
- */
-export const exportDatabase = () => {
- if (!db) throw new Error('数据库未初始化')
- return db.export()
-}
-
-/**
- * 导入数据库
- */
-export const importDatabase = async (data) => {
- if (!SQL) throw new Error('SQL.js 未初始化')
-
- db = new SQL.Database(new Uint8Array(data))
- await saveToIndexedDB()
-
- return true
-}
-
-/**
- * 导出为 JSON
- */
-export const exportAsJSON = () => {
- return {
- references: getAllReferences(),
- paradigms: getAllParadigms(),
- documents: getAllDocuments(),
- config: query('SELECT * FROM user_config'),
- exportedAt: new Date().toISOString()
+export const exportDatabase = async () => {
+ const response = await fetch(`${API_BASE}/db/export`)
+ if (!response.ok) {
+ const errorText = await response.text()
+ throw new Error(errorText || '导出失败')
}
+ const buffer = await response.arrayBuffer()
+ return new Uint8Array(buffer)
+}
+
+export const exportAsJSON = async () => {
+ return requestJson('/db/export-json')
}
-/**
- * 重置数据库
- */
export const resetDatabase = async () => {
- if (!SQL) throw new Error('SQL.js 未初始化')
-
- db = new SQL.Database()
- await initTables()
- await importDefaultData()
- await saveToIndexedDB()
-
+ await requestJson('/db/reset', { method: 'POST' })
return true
}
// ============================================
-// 默认数据导入
+// 提纲写作素材 CRUD
// ============================================
-/**
- * 导入默认数据(从静态配置文件)
- */
-const importDefaultData = async () => {
- // 导入默认素材
- const { REFERENCES } = await import('../config/references.js')
-
- Object.values(REFERENCES).forEach(ref => {
- addReference({
- ...ref,
- isDefault: true
- })
- })
-
- console.log('✅ 默认素材导入完成')
+export const getAllOutlineMaterials = async () => {
+ return requestJson('/outline-materials')
}
-// 导出数据库实例(用于调试)
-export const getDb = () => db
+export const addOutlineMaterial = async (material) => {
+ const data = await requestJson('/outline-materials', {
+ method: 'POST',
+ body: JSON.stringify(material)
+ })
+ return data?.id
+}
+
+export const updateOutlineMaterial = async (id, updates) => {
+ await requestJson(`/outline-materials/${id}`, {
+ method: 'PUT',
+ body: JSON.stringify(updates)
+ })
+ return true
+}
+
+export const deleteOutlineMaterial = async (id) => {
+ await requestJson(`/outline-materials/${id}`, { method: 'DELETE' })
+ return true
+}
diff --git a/src/stores/app.js b/src/stores/app.js
index cf0bd1a..222505f 100644
--- a/src/stores/app.js
+++ b/src/stores/app.js
@@ -1,6 +1,6 @@
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
-import { config, modelProviders, getConfiguredProviders, getDefaultProvider } from '../utils/config.js'
+import { modelProviders, getDefaultProvider } from '../utils/config.js'
import DeepSeekAPI from '../api/deepseek.js'
import { buildPrompt, createStreamParser, parseGhostwriterOutput } from '../utils/promptBuilder.js'
import { PARADIGMS, getParadigmById, buildParadigmConstraints } from '../config/paradigms.js'
@@ -21,9 +21,14 @@ export const useAppStore = defineStore('app', () => {
return modelProviders[selectedProviderId.value] || getDefaultProvider()
})
- // API 配置(基于选择的服务商)
- const apiUrl = computed(() => currentProvider.value.apiUrl)
- const apiKey = computed(() => currentProvider.value.apiKey)
+ // LLM 客户端(服务端代理)
+ const createLlmClient = () => {
+ return new DeepSeekAPI({
+ providerId: selectedProviderId.value,
+ model: currentProvider.value.model,
+ appId: currentProvider.value.appId
+ })
+ }
// 写作相关
const inputTask = ref('请帮我写一篇关于"AI 辅助编程如何改变软件开发流程"的博客文章,面向中级程序员。')
@@ -123,6 +128,20 @@ export const useAppStore = defineStore('app', () => {
fusionResult: ''
})
+ // 提纲写作相关 (Outline Writer)
+ const outlineWriterState = ref({
+ rawOutline: '', // 原始大纲文本
+ parsedSections: [], // 解析后的章节树
+ writingContext: '', // 全局写作背景
+ currentSectionIndex: -1, // 当前选中的章节索引
+ isGenerating: false, // 是否正在生成
+ shouldAbort: false, // 是否应该中断生成
+ // 素材管理
+ materials: [], // 素材池 [{ id, name, content }]
+ showMaterialModal: false, // 素材编辑弹窗
+ editingMaterial: null // 当前编辑的素材对象
+ })
+
// UI状态
const showPromptDebug = ref(false)
const showRefInput = ref(false)
@@ -146,22 +165,12 @@ export const useAppStore = defineStore('app', () => {
const ref = references.value[index]
if (!ref || !ref.content) return
- if (!apiUrl.value || !apiKey.value || apiKey.value === 'YOUR_KEY') {
- console.warn('Store: Missing API Key for style analysis')
- return
- }
-
// 如果已经有标签,跳过(或者强制刷新?这里假设自动触发仅一次)
if (ref.styleTags && ref.styleTags.length > 0) return
ref.isAnalyzing = true
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
let fullTags = ''
console.log(`Store: Analyzing style for reference [${index}]...`)
@@ -182,10 +191,6 @@ export const useAppStore = defineStore('app', () => {
// 生成内容动作
const generateContentAction = async () => {
- if (!apiUrl.value || !apiKey.value || apiKey.value === 'YOUR_KEY') {
- throw new Error('请先配置 API 地址和 API Key')
- }
-
console.log('Store: generateContentAction 启动', { mode: isDeepMode.value ? 'Deep' : 'Standard' })
isGenerating.value = true
generatedContent.value = ''
@@ -194,12 +199,7 @@ export const useAppStore = defineStore('app', () => {
generationStage.value = 'thinking'
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
const streamParser = createStreamParser()
// 构建 Prompt(XML 结构化数据)
@@ -379,12 +379,7 @@ ${draft}
}
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
let fullContent = ''
console.log('Store: 调用 API 分析文章...')
@@ -505,12 +500,7 @@ ${draft}
// 通用 API 调用方法
const callApi = async (prompt, onContent, options = {}) => {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
return api.generateContent(prompt, onContent, options)
}
@@ -579,7 +569,25 @@ ${draft}
return sections
}
- // 自定义范式没有 outlineTemplate:根据 expertGuidelines 生成基础章节
+ // 自定义范式:优先使用 sections 字段(如存在)
+ if (paradigm.sections && paradigm.sections.length > 0) {
+ const sections = paradigm.sections.map((s, index) => ({
+ title: s.title,
+ type: 'h2',
+ userInput: '',
+ inputType: 'idea',
+ corePoint: '',
+ materialData: '',
+ supplementNote: '',
+ placeholder: s.description || '',
+ weight: s.weight || Math.round(100 / paradigm.sections.length),
+ generatedContent: '',
+ isGenerating: false
+ }))
+ return sections
+ }
+
+ // 兼容旧数据:没有 sections 字段,使用默认章节
const sections = []
// 添加默认的开篇章节
@@ -596,64 +604,19 @@ ${draft}
isGenerating: false
})
- // 如果有 expertGuidelines,根据其内容生成章节
- if (paradigm.expertGuidelines && paradigm.expertGuidelines.length > 0) {
- // 分析 expertGuidelines 中是否有结构性建议
- const guidelines = paradigm.expertGuidelines
-
- // 按 scope 分组,document 级别的可能包含结构要求
- const docLevelGuidelines = guidelines.filter(g =>
- (typeof g === 'object' && g.scope === 'document') ||
- (typeof g === 'string' && /章节|结构|部分|篇幅/.test(g))
- )
-
- if (docLevelGuidelines.length > 0) {
- // 有文档级别指令,尝试提取章节建议
- docLevelGuidelines.forEach((g, i) => {
- const title = typeof g === 'object' ? g.title : `核心内容 ${i + 1}`
- sections.push({
- title: title,
- type: 'h2',
- userInput: '',
- inputType: 'idea',
- corePoint: '',
- materialData: '',
- supplementNote: '',
- placeholder: typeof g === 'object' ? g.description : g,
- generatedContent: '',
- isGenerating: false
- })
- })
- } else {
- // 没有明确的结构要求,生成通用章节
- sections.push({
- title: '核心内容',
- type: 'h2',
- userInput: '',
- inputType: 'idea',
- corePoint: '',
- materialData: '',
- supplementNote: '',
- placeholder: '请输入主要论点、分析或内容...',
- generatedContent: '',
- isGenerating: false
- })
- }
- } else {
- // 没有 expertGuidelines,生成最基础的章节
- sections.push({
- title: '主体内容',
- type: 'h2',
- userInput: '',
- inputType: 'idea',
- corePoint: '',
- materialData: '',
- supplementNote: '',
- placeholder: '请输入文章的主要内容...',
- generatedContent: '',
- isGenerating: false
- })
- }
+ // 添加核心内容章节
+ sections.push({
+ title: '核心内容',
+ type: 'h2',
+ userInput: '',
+ inputType: 'idea',
+ corePoint: '',
+ materialData: '',
+ supplementNote: '',
+ placeholder: '请输入主要论点、分析或内容...',
+ generatedContent: '',
+ isGenerating: false
+ })
// 添加默认的结尾章节
sections.push({
@@ -702,12 +665,7 @@ ${draft}
section.generatedContent = ''
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
const streamParser = createStreamParser()
// 构建上文内容,保持连贯性
@@ -797,6 +755,89 @@ ${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的
section.generatedContent = `[生成失败: ${error.message}]`
} finally {
section.isGenerating = false
+
+ // 生成完成后自动检查规范
+ if (section.generatedContent && !section.generatedContent.startsWith('[生成失败')) {
+ checkGuidelinesForSection(index)
+ }
+ }
+ }
+
+ /**
+ * 检查单个章节是否符合专家规范
+ */
+ const checkGuidelinesForSection = async (sectionIndex) => {
+ const section = paradigmWriterState.value.sections[sectionIndex]
+ if (!section || !section.generatedContent) return
+
+ const paradigm = getParadigmById(paradigmWriterState.value.selectedParadigmId, paradigmStore.customParadigms)
+ if (!paradigm || !paradigm.expertGuidelines || paradigm.expertGuidelines.length === 0) return
+
+ // 初始化检查结果
+ section.guidelineChecks = paradigm.expertGuidelines.map(g => ({
+ title: typeof g === 'object' ? g.title : g,
+ description: typeof g === 'object' ? g.description : g,
+ scope: typeof g === 'object' ? g.scope : 'document',
+ status: 'pending' // pending | pass | fail
+ }))
+
+ try {
+ const api = createLlmClient()
+
+ // 只检查与该章节相关的规范(sentence/paragraph 级别)
+ const relevantChecks = section.guidelineChecks.filter(c =>
+ c.scope === 'sentence' || c.scope === 'paragraph'
+ )
+
+ if (relevantChecks.length === 0) return
+
+ const checkPrompt = `请检查以下内容是否符合给定的写作规范。
+
+【待检查内容】
+${section.generatedContent}
+
+【检查规范】
+${relevantChecks.map((c, i) => `${i + 1}. ${c.title}:${c.description}`).join('\n')}
+
+请按以下 JSON 格式输出检查结果,每项返回 pass(符合)或 fail(不符合):
+\`\`\`json
+[
+ {"index": 0, "result": "pass"},
+ {"index": 1, "result": "fail", "reason": "不符合原因"}
+]
+\`\`\`
+`
+
+ let responseText = ''
+ await api._streamRequest([
+ { role: 'system', content: '你是一名专业的文稿审核专家,请严格按照规范进行检查。' },
+ { role: 'user', content: checkPrompt }
+ ], { temperature: 0.2, max_tokens: 1000 }, (chunk) => {
+ responseText += chunk
+ })
+
+ // 解析检查结果
+ const jsonMatch = responseText.match(/```json\n?([\s\S]*?)\n?```/)
+ if (jsonMatch) {
+ try {
+ const results = JSON.parse(jsonMatch[1])
+ results.forEach(r => {
+ if (relevantChecks[r.index]) {
+ const check = section.guidelineChecks.find(c => c.title === relevantChecks[r.index].title)
+ if (check) {
+ check.status = r.result
+ if (r.reason) check.reason = r.reason
+ }
+ }
+ })
+ } catch (e) {
+ console.error('解析检查结果失败:', e)
+ }
+ }
+
+ console.log(`✅ 章节 "${section.title}" 规范检查完成`)
+ } catch (error) {
+ console.error('规范检查失败:', error)
}
}
@@ -817,8 +858,6 @@ ${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的
return {
// 状态
currentPage,
- apiUrl,
- apiKey,
inputTask,
inputType,
outlinePoints,
@@ -865,12 +904,7 @@ ${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的
mimicWriterState.value.styleAnalysis = ''
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
await api.analyzeContent(mimicWriterState.value.sourceArticle, (content) => {
mimicWriterState.value.styleAnalysis += content
@@ -893,12 +927,7 @@ ${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的
mimicWriterState.value.thinkingContent = ''
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
const streamParser = createStreamParser()
@@ -949,12 +978,7 @@ ${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的
mimicWriterState.value.isGenerating = true
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
const streamParser = createStreamParser()
@@ -1033,12 +1057,7 @@ ${isMaterialType && materialDataText ? '4. ⚠️ 特别注意:用户提供的
mimicWriterState.value.isAnalyzing = true
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
const splitPrompt = `请分析以下文章的结构,将其拆分成语义完整的段落。
@@ -1117,12 +1136,7 @@ ${text}
mimicWriterState.value.generatedContent = ''
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
const { buildParagraphMimicPrompt } = await import('../utils/promptBuilder.js')
@@ -1187,12 +1201,7 @@ ${text}
articleFusionState.value.fusionResult = ''
try {
- const api = new DeepSeekAPI({
- url: apiUrl.value,
- key: apiKey.value,
- model: currentProvider.value.model,
- appId: currentProvider.value.appId
- })
+ const api = createLlmClient()
// 第一阶段:分析优劣
const analysisPrompt = `你是一位专业的文章分析专家。请分析以下两篇关于相同主题的文章,比较它们的优缺点。
@@ -1299,6 +1308,324 @@ ${modeInstruction}
await this.startArticleFusionAction({ titleA, titleB, articleA, articleB, fusionMode })
},
+ // 提纲写作
+ outlineWriterState,
+ parseOutlineAction: () => {
+ const raw = outlineWriterState.value.rawOutline
+ if (!raw.trim()) return
+
+ const lines = raw.split('\n').filter(l => l.trim())
+ const sections = []
+ let currentStack = [{ children: sections }]
+
+ // 智能检测大纲格式并解析
+ const parseLevel = (line) => {
+ const trimmed = line.trimStart()
+ const indent = line.length - trimmed.length
+
+ // 格式1: Markdown 标题 (# ## ###)
+ const mdMatch = trimmed.match(/^(#{1,3})\s*(.+)$/)
+ if (mdMatch) {
+ return { level: mdMatch[1].length, title: mdMatch[2].trim() }
+ }
+
+ // 格式2: 中文数字一级标题 (一、 二、 三、)
+ const zhL1Match = trimmed.match(/^[一二三四五六七八九十]+[、..]\s*(.+)$/)
+ if (zhL1Match) {
+ return { level: 1, title: trimmed }
+ }
+
+ // 格式3: 括号中文数字二级标题 ((一) (二) (一) (二))
+ const zhParenMatch = trimmed.match(/^[((][一二三四五六七八九十]+[))]\s*(.+)$/)
+ if (zhParenMatch) {
+ return { level: 2, title: trimmed }
+ }
+
+ // 格式4: 简单阿拉伯数字三级标题 (1. 2. 3. 4.)
+ // 注意:这是公文中常见的三级格式
+ const simpleNumMatch = trimmed.match(/^(\d+)[.、.]\s*(.+)$/)
+ if (simpleNumMatch && !trimmed.match(/^\d+\.\d+/)) {
+ // 纯数字如 1. 2. 3. 是三级标题
+ return { level: 3, title: trimmed }
+ }
+
+ // 格式5: 阿拉伯数字多级 (1.1 1.1.1)
+ const numMatch = trimmed.match(/^(\d+(?:\.\d+)+)[.、.]?\s*(.+)$/)
+ if (numMatch) {
+ const dotCount = (numMatch[1].match(/\./g) || []).length
+ return { level: Math.min(dotCount + 2, 3), title: trimmed } // 1.1 = level 3
+ }
+
+ // 格式6: 缩进层级(Tab 或空格)
+ if (indent > 0 && trimmed.length > 0) {
+ // 每 2-4 个空格或 1 个 Tab 为一个层级
+ const tabCount = (line.match(/\t/g) || []).length
+ const spaceLevel = Math.floor((indent - tabCount) / 2)
+ const level = Math.min(tabCount + spaceLevel + 1, 3)
+ return { level, title: trimmed }
+ }
+
+ // 默认作为一级标题
+ if (trimmed.length > 0) {
+ return { level: 1, title: trimmed }
+ }
+
+ return null
+ }
+
+ // 检测是否为标题行(返回 true 表示是标题)
+ const isHeadingLine = (line) => {
+ const trimmed = line.trimStart()
+ // Markdown 标题
+ if (/^#{1,3}\s+.+$/.test(trimmed)) return true
+ // 中文数字一级 (一、 二、)
+ if (/^[一二三四五六七八九十]+[、..]\s*.+$/.test(trimmed)) return true
+ // 括号中文数字 ((一) (二))
+ if (/^[((][一二三四五六七八九十]+[))]\s*.+$/.test(trimmed)) return true
+ // 阿拉伯数字 (1. 1.1)
+ if (/^\d+[.、.]\s*.+$/.test(trimmed)) return true
+ if (/^\d+(\.\d+)+[.、.]?\s*.+$/.test(trimmed)) return true
+ return false
+ }
+
+ let lastSection = null
+
+ lines.forEach(line => {
+ const trimmed = line.trim()
+ if (!trimmed) return
+
+ // 判断是否为标题行
+ if (isHeadingLine(line)) {
+ const parsed = parseLevel(line)
+ if (!parsed) return
+
+ const section = {
+ level: parsed.level,
+ title: parsed.title,
+ hint: '',
+ content: '',
+ isGenerating: false,
+ linkedMaterials: [], // 关联的素材 ID
+ children: []
+ }
+
+ // 找到正确的父级
+ while (currentStack.length > parsed.level) {
+ currentStack.pop()
+ }
+
+ currentStack[currentStack.length - 1].children.push(section)
+ currentStack.push(section)
+ lastSection = section
+ } else {
+ // 非标题行 → 作为上一个章节的 hint
+ if (lastSection) {
+ // 累加到 hint(支持多行说明)
+ if (lastSection.hint) {
+ lastSection.hint += '\n' + trimmed
+ } else {
+ lastSection.hint = trimmed
+ }
+ }
+ }
+ })
+
+ outlineWriterState.value.parsedSections = sections
+ outlineWriterState.value.currentSectionIndex = -1
+ console.log('✅ 解析大纲完成:', sections.length, '个顶级章节')
+ },
+
+ generateOutlineSectionAction: async (flatIndex) => {
+ // 获取扁平化的章节列表
+ const flatSections = []
+ const flatten = (sections) => {
+ sections.forEach(s => {
+ flatSections.push(s)
+ if (s.children && s.children.length > 0) {
+ flatten(s.children)
+ }
+ })
+ }
+ flatten(outlineWriterState.value.parsedSections)
+
+ const section = flatSections[flatIndex]
+ if (!section) return
+
+ section.isGenerating = true
+ outlineWriterState.value.isGenerating = true
+
+ try {
+ const api = createLlmClient()
+
+ // 收集已生成的前文作为上下文
+ const previousContent = flatSections
+ .slice(0, flatIndex)
+ .filter(s => s.content)
+ .map(s => `## ${s.title}\n${s.content}`)
+ .join('\n\n')
+ .slice(-2000) // 限制上下文长度
+
+ // 获取关联的素材内容
+ const linkedMaterials = (section.linkedMaterials || [])
+ .map(matId => outlineWriterState.value.materials.find(m => m.id === matId))
+ .filter(m => m)
+
+ const linkedMaterialsContent = linkedMaterials
+ .map(m => `【${m.name}】\n${m.content}`)
+ .join('\n\n---\n\n')
+
+ // 调试日志
+ console.log('📎 章节:', section.title)
+ console.log('📎 关联素材 IDs:', section.linkedMaterials)
+ console.log('📎 素材池:', outlineWriterState.value.materials.map(m => ({ id: m.id, name: m.name, hasContent: !!m.content, contentLength: m.content?.length })))
+ console.log('📎 匹配到的素材:', linkedMaterials.map(m => ({ name: m.name, contentLength: m.content?.length })))
+ console.log('📎 素材内容前100字:', linkedMaterialsContent.slice(0, 100))
+
+ // 构建强调素材引用的提示词
+ const materialInstruction = linkedMaterialsContent
+ ? `\n\n【重要】请务必在撰写时:
+1. 直接引用和融入上述【参考素材】中的具体内容
+2. 可以直接使用素材中的数据、案例、表述
+3. 如果素材中有相关的政策要求、工作成果,必须体现在文中`
+ : ''
+
+ const prompt = `你正在撰写一篇文章。
+
+${outlineWriterState.value.writingContext ? `【写作背景】\n${outlineWriterState.value.writingContext}\n\n` : ''}${linkedMaterialsContent ? `【参考素材 - 必须引用】\n${linkedMaterialsContent}\n\n` : ''}${previousContent ? `【已完成的前文】\n${previousContent}\n\n---\n\n` : ''}【当前任务】
+请撰写章节:${section.title}
+层级:H${section.level}
+${section.hint ? `\n【写作提示】\n${section.hint}` : ''}${materialInstruction}
+
+【要求】
+1. ${linkedMaterialsContent ? '必须充分引用【参考素材】中的具体内容、数据和表述' : '根据主题展开论述'}
+2. 内容要与前文衔接自然,避免重复
+3. 不要输出标题,只输出该章节的正文内容
+4. 语言流畅,论述清晰
+5. 根据层级控制篇幅(H1 较长,H3 较短)
+
+开始撰写:`
+
+ let responseText = ''
+ outlineWriterState.value.shouldAbort = false
+
+ await api._streamRequest([
+ { role: 'system', content: '你是一位专业的写作助手,擅长根据大纲展开撰写高质量的内容。' },
+ { role: 'user', content: prompt }
+ ], { temperature: 0.7, max_tokens: 2000 }, (chunk) => {
+ // 检查是否应该中断
+ if (outlineWriterState.value.shouldAbort) {
+ throw new Error('用户已中断生成')
+ }
+ responseText += chunk
+ section.content = responseText.trim()
+ })
+
+ console.log(`✅ 章节 "${section.title}" 生成完成`)
+ } catch (error) {
+ if (error.message === '用户已中断生成') {
+ console.log(`⏹️ 章节 "${section.title}" 生成已中断`)
+ section.content = section.content || '[已中断]'
+ } else {
+ console.error('生成失败:', error)
+ section.content = `[生成失败: ${error.message}]`
+ }
+ } finally {
+ section.isGenerating = false
+ outlineWriterState.value.isGenerating = false
+ outlineWriterState.value.shouldAbort = false
+ }
+ },
+
+ generateAllOutlineSectionsAction: async () => {
+ const flatSections = []
+ const flatten = (sections) => {
+ sections.forEach(s => {
+ flatSections.push(s)
+ if (s.children && s.children.length > 0) {
+ flatten(s.children)
+ }
+ })
+ }
+ flatten(outlineWriterState.value.parsedSections)
+
+ if (flatSections.length === 0) return
+
+ outlineWriterState.value.isGenerating = true
+ outlineWriterState.value.shouldAbort = false
+
+ try {
+ for (let i = 0; i < flatSections.length; i++) {
+ // 检查是否应该中断
+ if (outlineWriterState.value.shouldAbort) {
+ console.log('⏹️ 全文生成已中断')
+ break
+ }
+
+ outlineWriterState.value.currentSectionIndex = i
+ // 调用单节生成(需要使用 appStore 的方法)
+ const section = flatSections[i]
+ section.isGenerating = true
+
+ try {
+ const api = createLlmClient()
+
+ const previousContent = flatSections
+ .slice(0, i)
+ .filter(s => s.content)
+ .map(s => `## ${s.title}\n${s.content}`)
+ .join('\n\n')
+ .slice(-2000)
+
+ const prompt = `你正在撰写一篇文章。
+
+${outlineWriterState.value.writingContext ? `【写作背景】\n${outlineWriterState.value.writingContext}\n\n` : ''}${previousContent ? `【已完成的前文】\n${previousContent}\n\n---\n\n` : ''}【当前任务】
+请撰写章节:${section.title}
+层级:H${section.level}
+${section.hint ? `\n【写作提示】\n${section.hint}` : ''}
+
+【要求】
+1. 内容要与前文衔接自然,避免重复
+2. 不要输出标题,只输出该章节的正文内容
+3. 语言流畅,论述清晰
+
+开始撰写:`
+
+ let responseText = ''
+ await api._streamRequest([
+ { role: 'system', content: '你是一位专业的写作助手。' },
+ { role: 'user', content: prompt }
+ ], { temperature: 0.7, max_tokens: 2000 }, (chunk) => {
+ // 检查是否应该中断
+ if (outlineWriterState.value.shouldAbort) {
+ throw new Error('用户已中断生成')
+ }
+ responseText += chunk
+ section.content = responseText.trim()
+ })
+ } catch (error) {
+ if (error.message === '用户已中断生成') {
+ section.content = section.content || '[已中断]'
+ break
+ }
+ section.content = `[生成失败: ${error.message}]`
+ } finally {
+ section.isGenerating = false
+ }
+ }
+ console.log('✅ 全文生成完成')
+ } finally {
+ outlineWriterState.value.isGenerating = false
+ outlineWriterState.value.currentSectionIndex = -1
+ outlineWriterState.value.shouldAbort = false
+ }
+ },
+
+ // 中断生成
+ abortOutlineGenerationAction: () => {
+ outlineWriterState.value.shouldAbort = true
+ console.log('🛑 请求中断生成...')
+ },
+
// 方法
switchPage,
setCurrentPage,
diff --git a/src/stores/database.js b/src/stores/database.js
index c5bc295..558e2ac 100644
--- a/src/stores/database.js
+++ b/src/stores/database.js
@@ -7,8 +7,6 @@ import { ref, computed } from 'vue'
import {
initDatabase,
getAllReferences,
- getReferenceById as dbGetReferenceById,
- getReferencesByType as dbGetReferencesByType,
addReference as dbAddReference,
updateReference as dbUpdateReference,
deleteReference as dbDeleteReference,
@@ -72,11 +70,11 @@ export const useDatabaseStore = defineStore('database', () => {
// ============================================
const refreshReferences = async () => {
- references.value = getAllReferences()
+ references.value = await getAllReferences()
}
const getReferenceById = (id) => {
- return references.value.find(r => r.id === id) || dbGetReferenceById(id)
+ return references.value.find(r => r.id === id) || null
}
const getReferencesByType = (type) => {
@@ -105,18 +103,18 @@ export const useDatabaseStore = defineStore('database', () => {
}
const addReference = async (reference) => {
- const id = dbAddReference(reference)
+ const id = await dbAddReference(reference)
await refreshReferences()
return id
}
const updateReference = async (id, updates) => {
- dbUpdateReference(id, updates)
+ await dbUpdateReference(id, updates)
await refreshReferences()
}
const deleteReference = async (id) => {
- dbDeleteReference(id)
+ await dbDeleteReference(id)
await refreshReferences()
}
@@ -125,7 +123,7 @@ export const useDatabaseStore = defineStore('database', () => {
// ============================================
const refreshParadigms = async () => {
- paradigms.value = getAllParadigms()
+ paradigms.value = await getAllParadigms()
}
const getParadigmById = (id) => {
@@ -133,18 +131,18 @@ export const useDatabaseStore = defineStore('database', () => {
}
const addParadigm = async (paradigm) => {
- const id = dbAddParadigm(paradigm)
+ const id = await dbAddParadigm(paradigm)
await refreshParadigms()
return id
}
const updateParadigm = async (id, updates) => {
- dbUpdateParadigm(id, updates)
+ await dbUpdateParadigm(id, updates)
await refreshParadigms()
}
const deleteParadigm = async (id) => {
- dbDeleteParadigm(id)
+ await dbDeleteParadigm(id)
await refreshParadigms()
}
@@ -152,11 +150,11 @@ export const useDatabaseStore = defineStore('database', () => {
// 配置操作
// ============================================
- const getUserConfig = (key, defaultValue = null) => {
+ const getUserConfig = async (key, defaultValue = null) => {
return getConfig(key, defaultValue)
}
- const setUserConfig = (key, value) => {
+ const setUserConfig = async (key, value) => {
return setConfig(key, value)
}
@@ -164,7 +162,7 @@ export const useDatabaseStore = defineStore('database', () => {
// 数据导入导出
// ============================================
- const exportData = () => {
+ const exportData = async () => {
return exportAsJSON()
}
diff --git a/src/stores/paradigm.js b/src/stores/paradigm.js
index c579881..a133eb1 100644
--- a/src/stores/paradigm.js
+++ b/src/stores/paradigm.js
@@ -2,7 +2,7 @@ import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
// API 基础 URL
-const API_BASE = 'http://localhost:3001/api'
+const API_BASE = import.meta.env.VITE_API_BASE || 'http://localhost:3001/api'
/**
* 自定义范式管理 Store
diff --git a/src/utils/config.js b/src/utils/config.js
index 88c54ea..3291484 100644
--- a/src/utils/config.js
+++ b/src/utils/config.js
@@ -1,52 +1,45 @@
-// 环境变量配置
-// 模型服务商配置列表
+// 模型服务商配置列表(服务端托管)
export const modelProviders = {
deepseek: {
id: 'deepseek',
name: 'DeepSeek',
description: '深度求索',
- apiUrl: import.meta.env.VITE_DEEPSEEK_API_URL || 'https://api.deepseek.com/chat/completions',
- apiKey: import.meta.env.VITE_DEEPSEEK_API_KEY || '',
- model: import.meta.env.VITE_DEEPSEEK_MODEL || 'deepseek-chat'
+ model: import.meta.env.VITE_DEEPSEEK_MODEL || 'deepseek-chat',
+ serverManaged: true
},
qianfan: {
id: 'qianfan',
name: '千帆大模型',
description: '百度文心一言',
- apiUrl: import.meta.env.VITE_QIANFAN_API_URL || 'https://qianfan.baidubce.com/v2/chat/completions',
- apiKey: import.meta.env.VITE_QIANFAN_API_KEY || '',
- appId: import.meta.env.VITE_QIANFAN_APP_ID || '',
- model: import.meta.env.VITE_QIANFAN_MODEL || 'ernie-4.0-8k'
+ model: import.meta.env.VITE_QIANFAN_MODEL || 'ernie-4.0-8k',
+ serverManaged: true
},
openai: {
id: 'openai',
name: 'OpenAI',
description: 'GPT 系列',
- apiUrl: import.meta.env.VITE_OPENAI_API_URL || 'https://api.openai.com/v1/chat/completions',
- apiKey: import.meta.env.VITE_OPENAI_API_KEY || '',
- model: import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o'
+ model: import.meta.env.VITE_OPENAI_MODEL || 'gpt-4o',
+ serverManaged: true
},
claude: {
id: 'claude',
name: 'Claude',
description: 'Anthropic Claude',
- apiUrl: import.meta.env.VITE_CLAUDE_API_URL || 'https://api.anthropic.com/v1/messages',
- apiKey: import.meta.env.VITE_CLAUDE_API_KEY || '',
- model: import.meta.env.VITE_CLAUDE_MODEL || 'claude-3-5-sonnet'
+ model: import.meta.env.VITE_CLAUDE_MODEL || 'claude-3-5-sonnet',
+ serverManaged: true
},
custom: {
id: 'custom',
name: '自定义',
description: '自定义 API 端点',
- apiUrl: import.meta.env.VITE_CUSTOM_API_URL || '',
- apiKey: import.meta.env.VITE_CUSTOM_API_KEY || '',
- model: import.meta.env.VITE_CUSTOM_MODEL || ''
+ model: import.meta.env.VITE_CUSTOM_MODEL || '',
+ serverManaged: true
}
}
-// 获取已配置的服务商列表(有 API Key 的)
+// 服务端托管模式下,前端仅展示所有服务商
export const getConfiguredProviders = () => {
- return Object.values(modelProviders).filter(p => p.apiKey && p.apiKey.length > 0)
+ return Object.values(modelProviders)
}
// 获取默认服务商
@@ -55,32 +48,16 @@ export const getDefaultProvider = () => {
return configured.length > 0 ? configured[0] : modelProviders.deepseek
}
-// 兼容旧版配置
+// 应用配置
export const config = {
- // 向后兼容:使用第一个已配置的服务商
- get apiUrl() {
- return getDefaultProvider().apiUrl
- },
- get apiKey() {
- return getDefaultProvider().apiKey
- },
-
- // 应用配置
appVersion: '1.0.0',
isDev: import.meta.env.DEV,
mode: import.meta.env.MODE
}
-// 验证必需的环境变量
+// 验证配置(服务端托管时仅返回空错误)
export const validateConfig = () => {
- const errors = []
- const configured = getConfiguredProviders()
-
- if (configured.length === 0) {
- errors.push('未配置任何模型服务商的 API Key')
- }
-
- return errors
+ return []
}
// 获取配置摘要(用于调试)
diff --git a/src/utils/requirementParser.js b/src/utils/requirementParser.js
index 5264e83..d9dad49 100644
--- a/src/utils/requirementParser.js
+++ b/src/utils/requirementParser.js
@@ -10,32 +10,37 @@ export function buildRequirementParserPrompt(requirementText) {
return `你是一位专业的文档分析专家,擅长提取文档中的核心要求并将其转化为结构化的写作范式配置。
【任务】
-分析以下需求文档,提取关键要求并生成"范式配置",用于指导AI检查和润色文稿。
+分析以下需求文档,提取关键要求并生成"范式配置",用于指导AI写作和检查文稿。
【需求文档】
${requirementText}
【输出要求】
-请生成以下三个部分,使用JSON格式输出:
+请生成以下四个部分,使用JSON格式输出:
1. **specializedPrompt** (string): 系统提示词,包含:
- 会议主题/文档目标
- 核心要求(列表形式)
- 论述规范(结构要求、术语规范、语气要求等)
- - 维度要求(如五维度溯源等)
长度:300-500字
-2. **expertGuidelines** (array): 专家检查指令,每条指令应该:
- - 针对需求文档中的具体要求
- - 可直接用于检查文稿是否符合标准
- - 清晰、可执行
- - **包含 scope 字段**(指定检查粒度):
- * "sentence" - 句子级检查(如术语规范、语气分寸、表达方式)
- * "paragraph" - 段落级检查(如逻辑结构、递进关系、论述层次)
- * "document" - 全文级检查(如章节完整性、篇幅占比、结构要求)
- 数量:8-12条
+2. **sections** (array): 文章章节大纲,每个章节是实际需要撰写的内容部分:
+ - title (string): 章节标题(如"开篇引言"、"问题查摆"、"整改措施")
+ - description (string): 该章节应包含的内容说明
+ - weight (number): 该章节在全文中的建议篇幅占比(0-100,所有章节总和=100)
+ 数量:3-6个章节
-3. **metadata** (object): 元数据,包含:
+3. **expertGuidelines** (array): 专家检查指令(用于检验生成内容是否合规,不是章节):
+ - title (string): 检查项名称(如"五维度覆盖"、"术语规范")
+ - description (string): 具体检查标准
+ - scope (string): 检查粒度 - "sentence" | "paragraph" | "document"
+ 数量:8-12条
+
+ **重要**:expertGuidelines 是检查规范,不是章节标题!
+ - 正确示例:"五维度覆盖" → 检查文稿是否涵盖五个维度
+ - 错误示例:把"问题查摆"作为 expertGuideline(这应该放在 sections)
+
+4. **metadata** (object): 元数据,包含:
- name (string): 范式名称(简短,10字以内)
- description (string): 范式描述(30字以内)
- keyRequirements (array): 核心要求关键词(3-5个)
@@ -44,11 +49,15 @@ ${requirementText}
\`\`\`json
{
"specializedPrompt": "你是一位资深的...",
+ "sections": [
+ {"title": "开篇引言", "description": "说明会议背景、主题和目的", "weight": 15},
+ {"title": "问题查摆", "description": "从五个维度进行深入查摆分析", "weight": 60},
+ {"title": "整改措施", "description": "针对问题提出具体整改方向", "weight": 25}
+ ],
"expertGuidelines": [
- {"title": "党内术语规范", "description": "检查是否使用...", "scope": "sentence"},
- {"title": "递进式结构", "description": "检查段落是否符合...", "scope": "paragraph"},
- {"title": "章节完整性", "description": "检查是否包含会前准备...", "scope": "document"},
- ...
+ {"title": "五维度覆盖", "description": "检查问题查摆是否涵盖政治忠诚、党性修养等五个维度", "scope": "document"},
+ {"title": "核心篇幅占比", "description": "检查问题查摆部分是否占全文60%以上", "scope": "document"},
+ {"title": "党内术语规范", "description": "检查是否正确使用中央文件表述", "scope": "sentence"}
],
"metadata": {
"name": "范式名称",
@@ -59,14 +68,12 @@ ${requirementText}
\`\`\`
【注意事项】
-1. specializedPrompt 应该完整、系统,涵盖所有关键要求
-2. expertGuidelines 应该具体、可操作,每条针对一个检查点
+1. **sections 与 expertGuidelines 的区别**:
+ - sections = 需要撰写的章节(如"开篇引言"、"问题分析")
+ - expertGuidelines = 检验标准(如"篇幅占比"、"术语规范")
+2. specializedPrompt 应该完整、系统,涵盖所有关键要求
3. 保留原文中的专业术语和标准表述
-4. 如果需求文档提到篇幅要求、格式要求等,务必在 specializedPrompt 中明确体现
-5. **scope 分配原则**:
- - sentence 级:适用于任何句子片段(1-5句)
- - paragraph 级:需要段落上下文(6-20句)
- - document 级:需要完整文档或大段落(21+句)
+4. scope 分配原则:sentence(1-5句)、paragraph(6-20句)、document(21+句)
请开始分析并生成范式配置。`;
}
@@ -100,15 +107,29 @@ export function parseParadigmConfig(aiResponse) {
const config = JSON.parse(jsonText);
// 验证必需字段
- if (!config.specializedPrompt || !Array.isArray(config.expertGuidelines) || !config.metadata) {
+ if (!config.specializedPrompt || !config.metadata) {
console.error('配置缺少必需字段:', {
hasPrompt: !!config.specializedPrompt,
- hasGuidelines: Array.isArray(config.expertGuidelines),
hasMetadata: !!config.metadata
});
throw new Error('缺少必需字段');
}
+ // sections 可选,如果没有则生成默认章节
+ if (!Array.isArray(config.sections) || config.sections.length === 0) {
+ console.warn('配置中缺少 sections,将使用默认章节');
+ config.sections = [
+ { title: '开篇引言', description: '文章背景与目的', weight: 20 },
+ { title: '核心内容', description: '主要论述', weight: 60 },
+ { title: '总结与展望', description: '总结与下一步', weight: 20 }
+ ];
+ }
+
+ // expertGuidelines 可选
+ if (!Array.isArray(config.expertGuidelines)) {
+ config.expertGuidelines = [];
+ }
+
return config;
} catch (error) {
console.error('解析范式配置失败:', error);
@@ -142,6 +163,7 @@ export function buildParadigmObject(parsedConfig, sourceDocPath = null) {
tags: parsedConfig.metadata.keyRequirements || [], // 使用关键要求作为标签
specializedPrompt: parsedConfig.specializedPrompt,
+ sections: parsedConfig.sections || [], // 章节大纲
expertGuidelines: parsedConfig.expertGuidelines,
// 可选:继承默认的逻辑范式和维度集