mistpe commited on
Commit
44cae7c
·
verified ·
1 Parent(s): d82405d

Upload 6 files

Browse files
Files changed (6) hide show
  1. app/__init__.py +28 -0
  2. app/ai_service.py +37 -0
  3. app/extensions.py +3 -0
  4. app/models.py +37 -0
  5. app/routes.py +186 -0
  6. app/utils.py +45 -0
app/__init__.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask
2
+ from flask_cors import CORS
3
+ from config import Config
4
+ from app.utils import markdown_to_html
5
+ from app.extensions import db
6
+
7
+ def create_app():
8
+ app = Flask(__name__)
9
+ app.config.from_object(Config)
10
+
11
+ # Initialize extensions
12
+ CORS(app)
13
+ db.init_app(app)
14
+
15
+ # Register Markdown filter
16
+ app.jinja_env.filters['markdown'] = markdown_to_html
17
+
18
+ # Register blueprints
19
+ from app.routes import main, admin, api
20
+ app.register_blueprint(main)
21
+ app.register_blueprint(admin, url_prefix='/admin')
22
+ app.register_blueprint(api, url_prefix='/api')
23
+
24
+ # Create database tables
25
+ with app.app_context():
26
+ db.create_all()
27
+
28
+ return app
app/ai_service.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from openai import OpenAI
2
+ from flask import current_app
3
+
4
+ def create_ai_client():
5
+ """Create AI client with configuration from environment"""
6
+ return OpenAI(
7
+ api_key=current_app.config['AI_API_KEY'],
8
+ base_url=current_app.config['AI_BASE_URL']
9
+ )
10
+
11
+ def generate_summary(content):
12
+ """Generate a summary of the article content"""
13
+ client = create_ai_client()
14
+ prompt = f"请为以下文章生成一个简洁的摘要:\n\n{content}"
15
+
16
+ try:
17
+ response = client.chat.completions.create(
18
+ model="deepseek-chat",
19
+ messages=[{"role": "user", "content": prompt}]
20
+ )
21
+ return response.choices[0].message.content
22
+ except Exception as e:
23
+ print(f"Error generating summary: {e}")
24
+ return None
25
+
26
+ def chat_with_ai(messages):
27
+ """Chat with AI about the article content"""
28
+ client = create_ai_client()
29
+ try:
30
+ response = client.chat.completions.create(
31
+ model="deepseek-chat",
32
+ messages=messages
33
+ )
34
+ return response.choices[0].message.content
35
+ except Exception as e:
36
+ print(f"Error in chat: {e}")
37
+ return None
app/extensions.py ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ from flask_sqlalchemy import SQLAlchemy
2
+
3
+ db = SQLAlchemy()
app/models.py ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from datetime import datetime
2
+ from app.extensions import db
3
+ from slugify import slugify
4
+ import random
5
+ import string
6
+
7
+ class Article(db.Model):
8
+ id = db.Column(db.Integer, primary_key=True)
9
+ title = db.Column(db.String(200), nullable=False)
10
+ content = db.Column(db.Text, nullable=False)
11
+ summary = db.Column(db.Text)
12
+ slug = db.Column(db.String(200), unique=True, nullable=False)
13
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
14
+ updated_at = db.Column(db.DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
15
+
16
+ def __init__(self, *args, **kwargs):
17
+ if not kwargs.get('slug') and kwargs.get('title'):
18
+ base_slug = slugify(kwargs.get('title'))
19
+ slug = base_slug
20
+ counter = 1
21
+
22
+ # Keep trying until we find a unique slug
23
+ while Article.query.filter_by(slug=slug).first() is not None:
24
+ # Add random string for uniqueness
25
+ random_string = ''.join(random.choices(string.ascii_lowercase + string.digits, k=4))
26
+ slug = f"{base_slug}-{random_string}"
27
+
28
+ kwargs['slug'] = slug
29
+
30
+ super().__init__(*args, **kwargs)
31
+
32
+ class Image(db.Model):
33
+ id = db.Column(db.Integer, primary_key=True)
34
+ filename = db.Column(db.String(200), nullable=False)
35
+ data = db.Column(db.LargeBinary, nullable=False) # Store actual image data
36
+ mime_type = db.Column(db.String(100), nullable=False) # Store MIME type
37
+ created_at = db.Column(db.DateTime, default=datetime.utcnow)
app/routes.py ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Blueprint, render_template, request, jsonify, redirect, url_for, session, flash, send_file, current_app
2
+ import os
3
+ from app.models import db, Article, Image
4
+ from app.utils import handle_image_upload, login_required, check_auth
5
+ from app.ai_service import generate_summary, chat_with_ai
6
+ import io
7
+ import json
8
+
9
+ # Blueprints
10
+ main = Blueprint('main', __name__)
11
+ admin = Blueprint('admin', __name__)
12
+ api = Blueprint('api', __name__)
13
+
14
+ # Main routes
15
+ @main.route('/')
16
+ def index():
17
+ articles = Article.query.order_by(Article.created_at.desc()).all()
18
+ return render_template('index.html', articles=articles)
19
+
20
+ @main.route('/article/<slug>')
21
+ def article(slug):
22
+ article = Article.query.filter_by(slug=slug).first_or_404()
23
+ return render_template('article.html', article=article)
24
+
25
+ # Admin routes
26
+ # @admin.route('/login', methods=['GET', 'POST'])
27
+ # def login():
28
+ # if request.method == 'POST':
29
+ # if check_auth(request.form['username'], request.form['password']):
30
+ # session['logged_in'] = True
31
+ # return redirect(url_for('admin.dashboard'))
32
+ # flash('Invalid credentials')
33
+ # return render_template('admin/login.html')
34
+ @admin.route('/login', methods=['GET', 'POST'])
35
+ def login():
36
+ if request.method == 'POST':
37
+ if session.get('logged_in'):
38
+ session.pop('logged_in', None)
39
+ flash('已成功退出登录')
40
+ return redirect(url_for('main.index'))
41
+
42
+ if check_auth(request.form['username'], request.form['password']):
43
+ session['logged_in'] = True
44
+ return redirect(url_for('admin.dashboard'))
45
+ flash('用户名或密码错误')
46
+ return render_template('admin/login.html')
47
+ @api.route('/search', methods=['GET'])
48
+ def search():
49
+ query = request.args.get('q', '')
50
+ if not query:
51
+ return jsonify({'articles': []})
52
+
53
+ # 简单的模糊搜索:标题或内容包含搜索词
54
+ articles = Article.query.filter(
55
+ db.or_(
56
+ Article.title.ilike(f'%{query}%'),
57
+ Article.content.ilike(f'%{query}%')
58
+ )
59
+ ).order_by(Article.created_at.desc()).all()
60
+
61
+ # 转换为JSON格式
62
+ articles_data = [{
63
+ 'title': article.title,
64
+ 'summary': article.summary,
65
+ 'slug': article.slug,
66
+ 'created_at': article.created_at.strftime('%Y-%m-%d')
67
+ } for article in articles]
68
+
69
+ return jsonify({'articles': articles_data})
70
+ @admin.route('/dashboard')
71
+ @login_required
72
+ def dashboard():
73
+ articles = Article.query.order_by(Article.created_at.desc()).all()
74
+ return render_template('admin/dashboard.html', articles=articles)
75
+
76
+ @admin.route('/editor', defaults={'slug': None})
77
+ @admin.route('/editor/<slug>')
78
+ @login_required
79
+ def editor(slug):
80
+ article = Article.query.filter_by(slug=slug).first() if slug else None
81
+ return render_template('editor.html', article=article)
82
+
83
+ # API routes
84
+ @api.route('/articles', methods=['POST'])
85
+ @login_required
86
+ def create_article():
87
+ try:
88
+ data = request.get_json()
89
+ if not data or 'title' not in data or 'content' not in data:
90
+ return jsonify({'error': '标题和内容不能为空'}), 400
91
+
92
+ article = Article(
93
+ title=data['title'],
94
+ content=data['content']
95
+ )
96
+ article.summary = generate_summary(data['content'])
97
+ db.session.add(article)
98
+ db.session.commit()
99
+ return jsonify({'slug': article.slug})
100
+ except Exception as e:
101
+ db.session.rollback()
102
+ return jsonify({'error': str(e)}), 500
103
+
104
+ @api.errorhandler(500)
105
+ def handle_500(error):
106
+ return jsonify({'error': '服务器内部错误'}), 500
107
+
108
+ @api.route('/articles/<slug>', methods=['PUT'])
109
+ @login_required
110
+ def update_article(slug):
111
+ article = Article.query.filter_by(slug=slug).first_or_404()
112
+ data = request.get_json()
113
+ article.title = data['title']
114
+ article.content = data['content']
115
+ article.summary = generate_summary(data['content'])
116
+ db.session.commit()
117
+ return jsonify({'success': True})
118
+
119
+ @api.route('/articles/<slug>', methods=['DELETE'])
120
+ @login_required
121
+ def delete_article(slug):
122
+ article = Article.query.filter_by(slug=slug).first_or_404()
123
+ db.session.delete(article)
124
+ db.session.commit()
125
+ return jsonify({'success': True})
126
+
127
+ @api.route('/upload', methods=['POST'])
128
+ @login_required
129
+ def upload():
130
+ if 'file' not in request.files:
131
+ return jsonify({'error': 'No file provided'}), 400
132
+
133
+ file = request.files['file']
134
+ path = handle_image_upload(file)
135
+
136
+ if path:
137
+ return jsonify({'url': path})
138
+
139
+ return jsonify({'error': 'Invalid file'}), 400
140
+
141
+ @api.route('/images/<int:image_id>')
142
+ def get_image(image_id):
143
+ image = Image.query.get_or_404(image_id)
144
+ return send_file(
145
+ io.BytesIO(image.data),
146
+ mimetype=image.mime_type,
147
+ as_attachment=False
148
+ )
149
+
150
+ @api.route('/chat', methods=['POST'])
151
+ def chat():
152
+ data = request.get_json()
153
+ response = chat_with_ai(data['messages'])
154
+ return jsonify({'response': response})
155
+
156
+ @api.route('/export', methods=['GET'])
157
+ @login_required
158
+ def export_data():
159
+ db_path = os.path.join(current_app.root_path, '..', 'instance', 'blog.db')
160
+ return send_file(
161
+ db_path,
162
+ as_attachment=True,
163
+ download_name='blog-backup.db',
164
+ mimetype='application/x-sqlite3'
165
+ )
166
+
167
+ @api.route('/import', methods=['POST'])
168
+ @login_required
169
+ def import_data():
170
+ if 'file' not in request.files:
171
+ return jsonify({'error': 'No file provided'}), 400
172
+
173
+ file = request.files['file']
174
+ if file.filename == '':
175
+ return jsonify({'error': 'No file selected'}), 400
176
+
177
+ if not file.filename.endswith('.db'):
178
+ return jsonify({'error': 'Invalid file type'}), 400
179
+
180
+ try:
181
+ db_path = os.path.join(current_app.root_path, '..', 'instance', 'blog.db')
182
+ file.save(db_path)
183
+ db.session.remove() # Close any existing connections
184
+ return jsonify({'success': True})
185
+ except Exception as e:
186
+ return jsonify({'error': str(e)}), 500
app/utils.py ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from werkzeug.utils import secure_filename
3
+ from functools import wraps
4
+ from flask import current_app, request, redirect, url_for, session, send_file
5
+ import markdown
6
+ from markupsafe import Markup
7
+ from app.extensions import db
8
+ from app.models import Image
9
+ import io
10
+
11
+ def markdown_to_html(text):
12
+ return Markup(markdown.markdown(text, extensions=['fenced_code', 'tables']))
13
+
14
+ def allowed_file(filename):
15
+ """Check if uploaded file has allowed extension"""
16
+ return '.' in filename and \
17
+ filename.rsplit('.', 1)[1].lower() in current_app.config['ALLOWED_EXTENSIONS']
18
+
19
+ def handle_image_upload(file):
20
+ """Handle image upload and store in database"""
21
+ if file and allowed_file(file.filename):
22
+ filename = secure_filename(file.filename)
23
+ image = Image(
24
+ filename=filename,
25
+ data=file.read(),
26
+ mime_type=file.content_type
27
+ )
28
+ db.session.add(image)
29
+ db.session.commit()
30
+ return f'/api/images/{image.id}'
31
+ return None
32
+
33
+ def login_required(f):
34
+ """Decorator to require login for admin routes"""
35
+ @wraps(f)
36
+ def decorated_function(*args, **kwargs):
37
+ if not session.get('logged_in'):
38
+ return redirect(url_for('admin.login'))
39
+ return f(*args, **kwargs)
40
+ return decorated_function
41
+
42
+ def check_auth(username, password):
43
+ """Check if username and password match environment variables"""
44
+ return (username == current_app.config['ADMIN_USERNAME'] and
45
+ password == current_app.config['ADMIN_PASSWORD'])