feat: 主题核心思路功能 - 每个主题可设定核心思路,AI分析和生成文章时自动引入

This commit is contained in:
2026-05-23 23:07:40 +08:00
parent 2e8bfea262
commit 8899b21aea
3 changed files with 737 additions and 14 deletions

242
app.py
View File

@@ -43,6 +43,7 @@ def init_db():
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
description TEXT DEFAULT '',
core_idea TEXT DEFAULT '',
prompt TEXT DEFAULT '',
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime'))
@@ -53,6 +54,7 @@ def init_db():
type TEXT NOT NULL DEFAULT 'text',
content TEXT DEFAULT '',
file_path TEXT DEFAULT '',
analysis TEXT DEFAULT '',
sort_order INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now','localtime')),
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE
@@ -100,6 +102,14 @@ def init_db():
FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE,
FOREIGN KEY (category_id) REFERENCES categories(id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS prompt_templates (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
prompt TEXT NOT NULL DEFAULT '',
is_default INTEGER DEFAULT 0,
created_at TEXT DEFAULT (datetime('now','localtime')),
updated_at TEXT DEFAULT (datetime('now','localtime'))
);
CREATE TABLE IF NOT EXISTS topic_tags (
topic_id TEXT NOT NULL,
tag_id TEXT NOT NULL,
@@ -108,10 +118,35 @@ def init_db():
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
);
''')
# 初始化默认Prompt模板
default_templates = [
('通用写作', '请根据以下素材撰写一篇结构清晰、内容丰富的文章:'),
('深度分析', '请根据以下素材撰写一篇深度分析文章,要求:\n1. 深入挖掘素材背后的逻辑和趋势\n2. 提供独到的观点和见解\n3. 论证充分,数据支撑\n4. 结构严谨,层次分明'),
('新闻报道', '请根据以下素材撰写一篇新闻报道风格的文章,要求:\n1. 标题醒目,吸引读者\n2. 导语概括核心信息\n3. 正文详实客观\n4. 语言简洁流畅'),
('技术解读', '请根据以下素材撰写一篇技术解读文章,要求:\n1. 技术概念清晰解释\n2. 结合实际应用场景\n3. 对比分析优劣势\n4. 展望未来发展趋势'),
('自媒体爆款', '请根据以下素材撰写一篇适合自媒体发布的爆款文章,要求:\n1. 标题吸睛,引发好奇\n2. 开头设置悬念或冲突\n3. 内容有干货,有情绪共鸣\n4. 结尾引导互动,促转发'),
]
for tname, tprompt in default_templates:
conn.execute('INSERT OR IGNORE INTO prompt_templates (id, name, prompt, is_default) VALUES (?, ?, ?, 1)',
(str(uuid.uuid4())[:8], tname, tprompt))
# 初始化默认配置
conn.execute('INSERT OR IGNORE INTO llm_config (id, url, api_key, model) VALUES (1, ?, ?, ?)',
(LLM_URL, LLM_API_KEY, LLM_MODEL))
conn.commit()
# 兼容旧库:如 topics 缺少 core_idea 列则补上
try:
conn.execute('ALTER TABLE topics ADD COLUMN core_idea TEXT DEFAULT ""')
except Exception:
pass
# 兼容旧库:如 materials 缺少 analysis/selected 列则补上
try:
conn.execute('ALTER TABLE materials ADD COLUMN analysis TEXT DEFAULT ""')
except Exception:
pass
try:
conn.execute('ALTER TABLE materials ADD COLUMN selected INTEGER DEFAULT 1')
except Exception:
pass
conn.close()
@@ -199,8 +234,8 @@ def create_topic():
tid = str(uuid.uuid4())[:8]
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn.execute(
'INSERT INTO topics (id, name, description, prompt, created_at, updated_at) VALUES (?,?,?,?,?,?)',
(tid, name, data.get('description', ''), data.get('prompt', ''), now, now)
'INSERT INTO topics (id, name, description, core_idea, prompt, created_at, updated_at) VALUES (?,?,?,?,?,?,?)',
(tid, name, data.get('description', ''), data.get('core_idea', ''), data.get('prompt', ''), now, now)
)
# 关联分类
for cat_id in data.get('category_ids', []):
@@ -234,7 +269,7 @@ def update_topic(topic_id):
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
fields = []
values = []
for key in ['name', 'description', 'prompt']:
for key in ['name', 'description', 'core_idea', 'prompt']:
if key in data:
fields.append(f'{key}=?')
values.append(data[key])
@@ -295,6 +330,69 @@ def delete_topic(topic_id):
return jsonify({'ok': True})
# ==================== Prompt模板API ====================
@app.route('/api/prompt-templates', methods=['GET'])
def list_prompt_templates():
conn = get_db()
rows = conn.execute('SELECT * FROM prompt_templates ORDER BY is_default DESC, name').fetchall()
conn.close()
return jsonify([dict(r) for r in rows])
@app.route('/api/prompt-templates', methods=['POST'])
def create_prompt_template():
data = request.get_json(force=True)
name = data.get('name', '').strip()
prompt = data.get('prompt', '').strip()
if not name:
return jsonify({'error': '模板名称不能为空'}), 400
if not prompt:
return jsonify({'error': '模板内容不能为空'}), 400
conn = get_db()
tid = str(uuid.uuid4())[:8]
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
conn.execute('INSERT INTO prompt_templates (id, name, prompt, is_default, created_at, updated_at) VALUES (?,?,?,?,?,?)',
(tid, name, prompt, 0, now, now))
conn.commit()
tmpl = conn.execute('SELECT * FROM prompt_templates WHERE id=?', (tid,)).fetchone()
conn.close()
return jsonify(dict(tmpl))
@app.route('/api/prompt-templates/<template_id>', methods=['PUT'])
def update_prompt_template(template_id):
data = request.get_json(force=True)
conn = get_db()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
fields = []
values = []
for key in ['name', 'prompt']:
if key in data:
fields.append(f'{key}=?')
values.append(data[key])
if fields:
fields.append('updated_at=?')
values.append(now)
values.append(template_id)
conn.execute(f'UPDATE prompt_templates SET {",".join(fields)} WHERE id=?', values)
conn.commit()
tmpl = conn.execute('SELECT * FROM prompt_templates WHERE id=?', (template_id,)).fetchone()
conn.close()
if tmpl:
return jsonify(dict(tmpl))
return jsonify({'error': '未找到'}), 404
@app.route('/api/prompt-templates/<template_id>', methods=['DELETE'])
def delete_prompt_template(template_id):
conn = get_db()
conn.execute('DELETE FROM prompt_templates WHERE id=?', (template_id,))
conn.commit()
conn.close()
return jsonify({'ok': True})
# ==================== 分类API ====================
@app.route('/api/categories', methods=['GET'])
@@ -345,7 +443,9 @@ def update_category(category_id):
conn.commit()
cat = conn.execute('SELECT * FROM categories WHERE id=?', (category_id,)).fetchone()
conn.close()
return jsonify(dict(cat)) if cat else jsonify({'error': '未找到'}), 404
if cat:
return jsonify(dict(cat))
return jsonify({'error': '未找到'}), 404
@app.route('/api/categories/<category_id>', methods=['DELETE'])
@@ -473,7 +573,7 @@ def update_material(material_id):
conn = get_db()
fields = []
values = []
for key in ['content', 'sort_order']:
for key in ['content', 'sort_order', 'analysis', 'selected']:
if key in data:
fields.append(f'{key}=?')
values.append(data[key])
@@ -483,7 +583,9 @@ def update_material(material_id):
conn.commit()
material = conn.execute('SELECT * FROM materials WHERE id=?', (material_id,)).fetchone()
conn.close()
return jsonify(dict(material)) if material else jsonify({'error': '未找到'}), 404
if material:
return jsonify(dict(material))
return jsonify({'error': '未找到'}), 404
@app.route('/api/materials/<material_id>', methods=['DELETE'])
@@ -500,6 +602,128 @@ def delete_material(material_id):
return jsonify({'ok': True})
@app.route('/api/topics/<topic_id>/materials/select', methods=['POST'])
def batch_update_material_selection(topic_id):
"""批量更新素材选中状态"""
data = request.get_json(force=True)
conn = get_db()
now = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# data 格式: {"materials": [{"id": "xxx", "selected": 1/0}, ...]}
# 或: {"select_all": true/false}
if 'select_all' in data:
selected = 1 if data['select_all'] else 0
conn.execute('UPDATE materials SET selected=? WHERE topic_id=?', (selected, topic_id))
elif 'materials' in data:
for item in data['materials']:
conn.execute('UPDATE materials SET selected=? WHERE id=? AND topic_id=?',
(1 if item.get('selected') else 0, item['id'], topic_id))
conn.execute('UPDATE topics SET updated_at=? WHERE id=?', (now, topic_id))
conn.commit()
conn.close()
return jsonify({'ok': True})
@app.route('/api/materials/<material_id>/analyze', methods=['POST'])
def analyze_material(material_id):
"""大模型分析单个素材,返回分析结果并保存"""
conn = get_db()
mat = conn.execute('SELECT * FROM materials WHERE id=?', (material_id,)).fetchone()
if not mat:
conn.close()
return jsonify({'error': '素材不存在'}), 404
mat_dict = dict(mat)
# 获取主题核心思路
topic = conn.execute('SELECT core_idea FROM topics WHERE id=?', (mat_dict['topic_id'],)).fetchone()
core_idea = dict(topic)['core_idea'] if topic else ''
conn.close()
llm_config = get_llm_config()
if not llm_config['api_key']:
return jsonify({'error': '请先配置LLM API Key'}), 400
# 构建核心思路引导语
core_idea_hint = ''
if core_idea:
core_idea_hint = f'\n\n=== 主题核心思路 ===\n{core_idea}\n请围绕上述核心思路进行分析,确保分析结果与文章整体方向一致。'
# 构建分析prompt
if mat_dict['type'] == 'text':
user_content = f"""请对以下文本素材进行深度分析,要求:
1. 概括核心观点和关键信息
2. 分析文本的逻辑结构和论证方式
3. 识别潜在的写作价值和可延伸方向
4. 指出可能的不足或需要补充的信息
{core_idea_hint}
=== 文本素材 ===
{mat_dict['content']}
请直接给出分析结果,不要输出思考过程。"""
else:
# 图片素材:发送图片给多模态模型
note = mat_dict['content'] or '无备注'
img_path = os.path.join(BASE_DIR, 'static', mat_dict['file_path']) if mat_dict['file_path'] else None
# 尝试读取图片并编码为base64
image_content = None
if img_path and os.path.exists(img_path):
import base64
with open(img_path, 'rb') as f:
img_data = base64.b64encode(f.read()).decode('utf-8')
ext = mat_dict['file_path'].rsplit('.', 1)[-1].lower()
mime_map = {'png': 'image/png', 'jpg': 'image/jpeg', 'jpeg': 'image/jpeg',
'gif': 'image/gif', 'webp': 'image/webp', 'svg': 'image/svg+xml', 'bmp': 'image/bmp'}
mime = mime_map.get(ext, 'image/png')
image_content = f'data:{mime};base64,{img_data}'
if image_content:
user_content = [
{'type': 'text', 'text': f"""请对以下图片素材进行深度分析,要求:
1. 详细描述图片内容(场景、人物、物体、文字等)
2. 分析图片的核心信息和表达意图
3. 识别图片在文章写作中的潜在用途
4. 如有备注,结合备注分析:{note}
{core_idea_hint}
请直接给出分析结果,不要输出思考过程。"""},
{'type': 'image_url', 'image_url': {'url': image_content}}
]
else:
user_content = f"""请对以下图片素材备注进行分析(图片文件不可读,仅基于备注):
备注:{note}
{core_idea_hint}
请尝试基于备注信息给出分析,并建议补充更多图片细节。"""
try:
req_body = {
'model': llm_config['model'],
'messages': [{'role': 'user', 'content': user_content}],
'temperature': 0.5,
'max_tokens': 2048
}
resp = requests.post(
llm_config['url'],
headers={'Authorization': f"Bearer {llm_config['api_key']}", 'Content-Type': 'application/json'},
json=req_body,
timeout=60
)
resp.raise_for_status()
result = resp.json()
analysis_text = result['choices'][0]['message']['content']
except Exception as e:
return jsonify({'error': f'分析失败: {str(e)}'}), 500
# 保存分析结果
conn = get_db()
conn.execute('UPDATE materials SET analysis=? WHERE id=?', (analysis_text, material_id))
conn.commit()
conn.close()
return jsonify({'analysis': analysis_text})
# ==================== LLM配置API ====================
@app.route('/api/llm-config', methods=['GET'])
@@ -563,7 +787,7 @@ def generate_article(topic_id):
return jsonify({'error': '主题不存在'}), 404
materials = conn.execute(
'SELECT * FROM materials WHERE topic_id=? ORDER BY sort_order, created_at',
'SELECT * FROM materials WHERE topic_id=? AND selected=1 ORDER BY sort_order, created_at',
(topic_id,)
).fetchall()
conn.close()
@@ -595,6 +819,7 @@ def generate_article(topic_id):
主题:{topic['name']}
{f'主题说明:{topic["description"]}' if topic['description'] else ''}
{f'核心思路:{topic["core_idea"]}' if topic['core_idea'] else ''}
=== 素材 ===
{materials_text}
@@ -603,7 +828,8 @@ def generate_article(topic_id):
1. 充分利用提供的素材
2. 文章结构完整,层次分明
3. 语言流畅,逻辑清晰
4. 不要输出思考过程,直接给出文章内容"""
4. 紧扣核心思路进行创作,保持文章方向与风格一致
5. 不要输出思考过程,直接给出文章内容"""
# 调用大模型
try: