feat: 主题核心思路功能 - 每个主题可设定核心思路,AI分析和生成文章时自动引入
This commit is contained in:
242
app.py
242
app.py
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user