From 87286986ea034b3cbc2d8ef0d2af9fe22d9e3c6e Mon Sep 17 00:00:00 2001 From: hubian <908234780@qq.com> Date: Sat, 23 May 2026 00:51:42 +0800 Subject: [PATCH] =?UTF-8?q?v2.0:=20=E6=B7=BB=E5=8A=A0=E5=88=86=E7=B1=BB?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=92=8C=E6=A0=87=E7=AD=BE=E7=B3=BB=E7=BB=9F?= =?UTF-8?q?=EF=BC=8C=E6=94=AF=E6=8C=81=E5=A4=9A=E7=B1=BB=E5=88=AB=E5=A4=9A?= =?UTF-8?q?=E6=A0=87=E7=AD=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 52 ++++-- app.py | 218 ++++++++++++++++++++++- templates/index.html | 401 ++++++++++++++++++++++++++++++++++++++----- templates/topic.html | 184 +++++++++++++++++--- 4 files changed, 770 insertions(+), 85 deletions(-) diff --git a/README.md b/README.md index c68f633..5cd78fa 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,21 @@ # Article Writer - 文章编写系统 -基于 Flask + LLM 的智能文章编写系统,支持主题管理、素材收集和大模型生成文章。 +基于 Flask + LLM 的智能文章编写系统,支持主题管理、素材收集、分类标签和大模型生成文章。 ## 功能 - 主题管理:创建、编辑、删除文章主题 -- 素材收集:支持文本素材和图片素材 -- Prompt编辑:每个主题可自定义生成提示词 +- 分类系统:多级分类,每个主题可归属多个分类 +- 标签系统:灵活标签,每个主题支持多个标签,支持新建标签 +- 素材收集:支持文本素材和图片素材(含Ctrl+V粘贴截图) +- Prompt编辑:每个主题可自定义生成提示词,支持历史记录 - 大模型生成:调用 LLM 根据素材自动生成文章 -- 文章管理:查看、编辑、删除生成的文章 +- 文章管理:查看、编辑、删除、复制、下载生成的文章 ## 技术栈 - 后端:Python 3 + Flask + SQLite -- 前端:HTML/CSS/JS(原生) +- 前端:HTML/CSS/JS(Bootstrap 5 暗色主题) - LLM:兼容 OpenAI API 格式的大模型接口 ## 快速开始 @@ -24,7 +26,7 @@ pip install flask requests # 配置环境变量 export LLM_URL="https://your-llm-api/v1/chat/completions" -export LLM_API_KEY="your-api-key" +export LLM_API_KEY="***" export LLM_MODEL="qwen3.6-plus" # 启动服务 @@ -35,7 +37,7 @@ python app.py ## 配置 -通过环境变量配置 LLM 接口: +通过环境变量或页面配置 LLM 接口: | 环境变量 | 说明 | 默认值 | |---------|------|--------| @@ -43,21 +45,49 @@ python app.py | LLM_API_KEY | API 密钥 | - | | LLM_MODEL | 模型名称 | qwen3.6-plus | +## 数据结构 + +- **topics** - 文章主题 +- **categories** - 分类(支持多对多关联) +- **tags** - 标签(支持多对多关联,自动清理孤立标签) +- **topic_categories** - 主题-分类关联表 +- **topic_tags** - 主题-标签关联表 +- **materials** - 素材(文本/图片) +- **articles** - 生成的文章 +- **llm_config** - LLM接口配置 +- **prompt_history** - Prompt历史记录 + ## 项目结构 ``` article-writer/ ├── app.py # 主应用(Flask后端) ├── templates/ -│ ├── index.html # 首页(主题列表) -│ └── topic.html # 主题详情页 +│ ├── index.html # 首页(主题列表 + 分类/标签筛选) +│ └── topic.html # 主题详情页(素材 + 文章生成 + 分类标签编辑) ├── static/ -│ ├── css/ -│ ├── js/ │ └── uploads/ # 图片上传目录 +├── .env.example # 环境变量示例 └── .gitignore ``` +## API + +### 分类 +- `GET /api/categories` - 分类列表(含主题计数) +- `POST /api/categories` - 创建分类 +- `PUT /api/categories/` - 更新分类 +- `DELETE /api/categories/` - 删除分类 + +### 标签 +- `GET /api/tags` - 标签列表(含主题计数) +- `POST /api/tags` - 创建标签 +- `DELETE /api/tags/` - 删除标签 + +### 主题 +- `POST /api/topics` - 创建主题(支持 category_ids, tag_ids, new_tags) +- `PUT /api/topics/` - 更新主题(支持更新分类和标签关联) + ## License MIT diff --git a/app.py b/app.py index 8e1afb5..cfce41e 100644 --- a/app.py +++ b/app.py @@ -1,7 +1,7 @@ #!/usr/bin/env python3 """ 文章编写系统 - Article Writer System -功能:主题管理、素材收集、Prompt编辑、大模型生成文章 +功能:主题管理、素材收集、Prompt编辑、大模型生成文章、分类与标签 """ import os @@ -82,6 +82,31 @@ def init_db(): created_at TEXT DEFAULT (datetime('now','localtime')), FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE ); + CREATE TABLE IF NOT EXISTS categories ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + sort_order INTEGER DEFAULT 0, + created_at TEXT DEFAULT (datetime('now','localtime')) + ); + CREATE TABLE IF NOT EXISTS tags ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL UNIQUE, + created_at TEXT DEFAULT (datetime('now','localtime')) + ); + CREATE TABLE IF NOT EXISTS topic_categories ( + topic_id TEXT NOT NULL, + category_id TEXT NOT NULL, + PRIMARY KEY (topic_id, category_id), + 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 topic_tags ( + topic_id TEXT NOT NULL, + tag_id TEXT NOT NULL, + PRIMARY KEY (topic_id, tag_id), + FOREIGN KEY (topic_id) REFERENCES topics(id) ON DELETE CASCADE, + FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE + ); ''') # 初始化默认配置 conn.execute('INSERT OR IGNORE INTO llm_config (id, url, api_key, model) VALUES (1, ?, ?, ?)', @@ -104,6 +129,22 @@ def allowed_file(filename): return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS +def _topic_with_tags(conn, topic_row): + """给 topic row 附上分类和标签信息""" + t = dict(topic_row) + cats = conn.execute( + 'SELECT c.* FROM categories c JOIN topic_categories tc ON c.id=tc.category_id WHERE tc.topic_id=? ORDER BY c.sort_order', + (t['id'],) + ).fetchall() + t['categories'] = [dict(c) for c in cats] + tags = conn.execute( + 'SELECT tg.* FROM tags tg JOIN topic_tags tt ON tg.id=tt.tag_id WHERE tt.topic_id=? ORDER BY tg.name', + (t['id'],) + ).fetchall() + t['tags'] = [dict(tg) for tg in tags] + return t + + # ==================== 页面路由 ==================== @app.route('/') @@ -113,8 +154,14 @@ def index(): 'SELECT t.*, (SELECT COUNT(*) FROM materials WHERE topic_id=t.id) as mat_count ' 'FROM topics t ORDER BY updated_at DESC' ).fetchall() + categories = conn.execute('SELECT * FROM categories ORDER BY sort_order, name').fetchall() + tags = conn.execute('SELECT * FROM tags ORDER BY name').fetchall() + # 附上分类和标签 + topic_list = [] + for t in topics: + topic_list.append(_topic_with_tags(conn, t)) conn.close() - return render_template('index.html', topics=topics) + return render_template('index.html', topics=topic_list, categories=categories, tags=tags) @app.route('/topic/') @@ -124,6 +171,7 @@ def topic_detail(topic_id): if not topic: conn.close() return redirect(url_for('index')) + topic = _topic_with_tags(conn, topic) materials = conn.execute( 'SELECT * FROM materials WHERE topic_id=? ORDER BY sort_order, created_at', (topic_id,) @@ -132,8 +180,11 @@ def topic_detail(topic_id): 'SELECT * FROM articles WHERE topic_id=? ORDER BY created_at DESC', (topic_id,) ).fetchall() + categories = conn.execute('SELECT * FROM categories ORDER BY sort_order, name').fetchall() + tags = conn.execute('SELECT * FROM tags ORDER BY name').fetchall() conn.close() - return render_template('topic.html', topic=topic, materials=materials, articles=articles) + return render_template('topic.html', topic=topic, materials=materials, articles=articles, + categories=categories, tags=tags) # ==================== 主题API ==================== @@ -151,10 +202,29 @@ def create_topic(): 'INSERT INTO topics (id, name, description, prompt, created_at, updated_at) VALUES (?,?,?,?,?,?)', (tid, name, data.get('description', ''), data.get('prompt', ''), now, now) ) + # 关联分类 + for cat_id in data.get('category_ids', []): + conn.execute('INSERT OR IGNORE INTO topic_categories (topic_id, category_id) VALUES (?,?)', (tid, cat_id)) + # 关联标签(如果传了已有tag id) + for tag_id in data.get('tag_ids', []): + conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (tid, tag_id)) + # 处理新标签(按名称创建) + for tag_name in data.get('new_tags', []): + tag_name = tag_name.strip() + if not tag_name: + continue + existing = conn.execute('SELECT id FROM tags WHERE name=?', (tag_name,)).fetchone() + if existing: + conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (tid, existing['id'])) + else: + tag_id = str(uuid.uuid4())[:8] + conn.execute('INSERT INTO tags (id, name, created_at) VALUES (?,?,?)', (tag_id, tag_name, now)) + conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (tid, tag_id)) conn.commit() topic = conn.execute('SELECT * FROM topics WHERE id=?', (tid,)).fetchone() + result = _topic_with_tags(conn, topic) conn.close() - return jsonify(dict(topic)) + return jsonify(result) @app.route('/api/topics/', methods=['PUT']) @@ -174,9 +244,37 @@ def update_topic(topic_id): values.append(topic_id) conn.execute(f'UPDATE topics SET {",".join(fields)} WHERE id=?', values) conn.commit() + + # 更新分类关联 + if 'category_ids' in data: + conn.execute('DELETE FROM topic_categories WHERE topic_id=?', (topic_id,)) + for cat_id in data['category_ids']: + conn.execute('INSERT OR IGNORE INTO topic_categories (topic_id, category_id) VALUES (?,?)', (topic_id, cat_id)) + + # 更新标签关联 + if 'tag_ids' in data or 'new_tags' in data: + conn.execute('DELETE FROM topic_tags WHERE topic_id=?', (topic_id,)) + for tag_id in data.get('tag_ids', []): + conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (topic_id, tag_id)) + for tag_name in data.get('new_tags', []): + tag_name = tag_name.strip() + if not tag_name: + continue + existing = conn.execute('SELECT id FROM tags WHERE name=?', (tag_name,)).fetchone() + if existing: + conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (topic_id, existing['id'])) + else: + tag_id = str(uuid.uuid4())[:8] + conn.execute('INSERT INTO tags (id, name, created_at) VALUES (?,?,?)', (tag_id, tag_name, now)) + conn.execute('INSERT OR IGNORE INTO topic_tags (topic_id, tag_id) VALUES (?,?)', (topic_id, tag_id)) + # 清理无引用的孤立标签 + conn.execute('DELETE FROM tags WHERE id NOT IN (SELECT tag_id FROM topic_tags)') + + conn.commit() topic = conn.execute('SELECT * FROM topics WHERE id=?', (topic_id,)).fetchone() + result = _topic_with_tags(conn, topic) conn.close() - return jsonify(dict(topic)) + return jsonify(result) @app.route('/api/topics/', methods=['DELETE']) @@ -190,6 +288,112 @@ def delete_topic(topic_id): if os.path.exists(fp): os.remove(fp) conn.execute('DELETE FROM topics WHERE id=?', (topic_id,)) + # 清理孤立标签 + conn.execute('DELETE FROM tags WHERE id NOT IN (SELECT tag_id FROM topic_tags)') + conn.commit() + conn.close() + return jsonify({'ok': True}) + + +# ==================== 分类API ==================== + +@app.route('/api/categories', methods=['GET']) +def list_categories(): + conn = get_db() + rows = conn.execute( + 'SELECT c.*, (SELECT COUNT(*) FROM topic_categories WHERE category_id=c.id) as topic_count ' + 'FROM categories c ORDER BY c.sort_order, c.name' + ).fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + + +@app.route('/api/categories', methods=['POST']) +def create_category(): + data = request.get_json(force=True) + name = data.get('name', '').strip() + if not name: + return jsonify({'error': '分类名不能为空'}), 400 + conn = get_db() + existing = conn.execute('SELECT * FROM categories WHERE name=?', (name,)).fetchone() + if existing: + conn.close() + return jsonify({'error': '分类已存在'}), 400 + cid = str(uuid.uuid4())[:8] + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + conn.execute('INSERT INTO categories (id, name, sort_order, created_at) VALUES (?,?,?,?)', + (cid, name, data.get('sort_order', 0), now)) + conn.commit() + cat = conn.execute('SELECT * FROM categories WHERE id=?', (cid,)).fetchone() + conn.close() + return jsonify(dict(cat)) + + +@app.route('/api/categories/', methods=['PUT']) +def update_category(category_id): + data = request.get_json(force=True) + conn = get_db() + fields = [] + values = [] + for key in ['name', 'sort_order']: + if key in data: + fields.append(f'{key}=?') + values.append(data[key]) + if fields: + values.append(category_id) + conn.execute(f'UPDATE categories SET {",".join(fields)} WHERE id=?', values) + 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 + + +@app.route('/api/categories/', methods=['DELETE']) +def delete_category(category_id): + conn = get_db() + conn.execute('DELETE FROM categories WHERE id=?', (category_id,)) + conn.commit() + conn.close() + return jsonify({'ok': True}) + + +# ==================== 标签API ==================== + +@app.route('/api/tags', methods=['GET']) +def list_tags(): + conn = get_db() + rows = conn.execute( + 'SELECT tg.*, (SELECT COUNT(*) FROM topic_tags WHERE tag_id=tg.id) as topic_count ' + 'FROM tags tg ORDER BY tg.name' + ).fetchall() + conn.close() + return jsonify([dict(r) for r in rows]) + + +@app.route('/api/tags', methods=['POST']) +def create_tag(): + data = request.get_json(force=True) + name = data.get('name', '').strip() + if not name: + return jsonify({'error': '标签名不能为空'}), 400 + conn = get_db() + existing = conn.execute('SELECT * FROM tags WHERE name=?', (name,)).fetchone() + if existing: + conn.close() + return jsonify(dict(existing)) # 已存在直接返回 + tid = str(uuid.uuid4())[:8] + now = datetime.now().strftime('%Y-%m-%d %H:%M:%S') + conn.execute('INSERT INTO tags (id, name, created_at) VALUES (?,?,?)', (tid, name, now)) + conn.commit() + tag = conn.execute('SELECT * FROM tags WHERE id=?', (tid,)).fetchone() + conn.close() + return jsonify(dict(tag)) + + +@app.route('/api/tags/', methods=['DELETE']) +def delete_tag(tag_id): + conn = get_db() + conn.execute('DELETE FROM tags WHERE id=?', (tag_id,)) conn.commit() conn.close() return jsonify({'ok': True}) @@ -304,8 +508,8 @@ def update_llm_config(): conn.execute(''' INSERT INTO llm_config (id, url, api_key, model, updated_at) VALUES (1, ?, ?, ?, ?) - ON CONFLICT(id) DO UPDATE SET url=?, api_key=?, model=?, updated_at=? - ''', (1, url, api_key, model, now, url, api_key, model, now)) + ON CONFLICT(id) DO UPDATE SET url=excluded.url, api_key=excluded.api_key, model=excluded.model, updated_at=excluded.updated_at + ''', (url, api_key, model, now)) conn.commit() conn.close() return jsonify({'ok': True}) diff --git a/templates/index.html b/templates/index.html index 35ae2ce..c410028 100644 --- a/templates/index.html +++ b/templates/index.html @@ -7,7 +7,7 @@ @@ -51,6 +76,9 @@ + @@ -59,33 +87,90 @@
- {% if topics %}
- {% for t in topics %} -
-
-
-
{{t.name}}
- + +
+
+
+
分类
+
- {% if t.description %}
{{t.description}}
{% endif %} -
- {{t.mat_count}} 素材 - {{t.updated_at}} +
+ 全部 +
+ {% for c in categories %} +
+ {{c.name}} + +
+ {% endfor %} +
+ +
+
+
标签
+ +
+
+ {% for t in tags %} + {{t.name}} + {% endfor %} + {% if not tags %} + 暂无标签 + {% endif %}
- {% endfor %} + + +
+ {% if topics %} +
+ {% for t in topics %} +
+
+
+
{{t.name}}
+ +
+ {% if t.description %}
{{t.description}}
{% endif %} + + {% if t.categories %} +
+ {% for c in t.categories %} + {{c.name}} + {% endfor %} +
+ {% endif %} + + {% if t.tags %} +
+ {% for tg in t.tags %} + {{tg.name}} + {% endfor %} +
+ {% endif %} +
+ {{t.mat_count}} 素材 + {{t.updated_at}} +
+
+
+ {% endfor %} +
+ {% else %} +
+ +
还没有主题
+

点击右上角「新建主题」开始吧

+
+ {% endif %} +
- {% else %} -
- -
还没有主题
-

点击右上角「新建主题」开始吧

-
- {% endif %}
@@ -105,6 +190,26 @@
+
+ +
+ +
+
点击选择所属分类
+
+
+ +
+ +
+
+ + +
+
+ +
+
@@ -118,6 +223,27 @@
+ + +