Also resolve .env.local and .env paths relative to project root instead of current working directory.
184 lines
6.2 KiB
Python
184 lines
6.2 KiB
Python
"""保单智能客服系统 — 独立后端入口
|
||
|
||
独立部署时使用的 Flask 应用,不依赖 Dify 扩展。
|
||
"""
|
||
import os
|
||
import sys
|
||
import logging
|
||
from logging.handlers import RotatingFileHandler
|
||
|
||
# 将 api/ 目录加入搜索路径,使 `from insurance.xxx` 可从任意位置运行
|
||
_this_dir = os.path.dirname(os.path.abspath(__file__))
|
||
_api_dir = os.path.dirname(_this_dir)
|
||
if _api_dir not in sys.path:
|
||
sys.path.insert(0, _api_dir)
|
||
|
||
from flask import Flask
|
||
from flask_cors import CORS
|
||
from dotenv import load_dotenv
|
||
|
||
# 加载环境变量,.env.local 优先级最高(本地开发用,不影响 Docker 部署)
|
||
_project_root = os.path.dirname(_api_dir) # baodanagent/
|
||
load_dotenv(os.path.join(_project_root, '.env.local'), override=True)
|
||
load_dotenv(os.path.join(_project_root, '.env'))
|
||
load_dotenv()
|
||
|
||
|
||
def setup_logging(app: Flask) -> None:
|
||
"""配置统一的日志系统。"""
|
||
log_level = app.config.get('LOG_LEVEL', 'INFO')
|
||
log_file = app.config.get('LOG_FILE', 'logs/insurance.log')
|
||
|
||
# 创建日志目录
|
||
log_dir = os.path.dirname(log_file)
|
||
if log_dir and not os.path.exists(log_dir):
|
||
os.makedirs(log_dir, exist_ok=True)
|
||
|
||
# 配置根日志记录器
|
||
root_logger = logging.getLogger()
|
||
root_logger.setLevel(getattr(logging, log_level.upper(), logging.INFO))
|
||
|
||
# 控制台输出
|
||
console_handler = logging.StreamHandler()
|
||
console_handler.setLevel(logging.INFO)
|
||
console_format = logging.Formatter(
|
||
'%(asctime)s [%(levelname)s] %(name)s: %(message)s',
|
||
datefmt='%Y-%m-%d %H:%M:%S'
|
||
)
|
||
console_handler.setFormatter(console_format)
|
||
root_logger.addHandler(console_handler)
|
||
|
||
# 文件输出(带轮转)
|
||
try:
|
||
file_handler = RotatingFileHandler(
|
||
log_file,
|
||
maxBytes=10 * 1024 * 1024, # 10MB
|
||
backupCount=5,
|
||
encoding='utf-8'
|
||
)
|
||
file_handler.setLevel(logging.DEBUG)
|
||
file_format = logging.Formatter(
|
||
'%(asctime)s [%(levelname)s] %(name)s [%(filename)s:%(lineno)d]: %(message)s',
|
||
datefmt='%Y-%m-%d %H:%M:%S'
|
||
)
|
||
file_handler.setFormatter(file_format)
|
||
root_logger.addHandler(file_handler)
|
||
except Exception as e:
|
||
print(f"[logging] 文件日志配置失败: {e}")
|
||
|
||
# 将 print 替换为 logging
|
||
import builtins
|
||
original_print = builtins.print
|
||
def logging_print(*args, **kwargs):
|
||
msg = ' '.join(str(arg) for arg in args)
|
||
logging.info(msg)
|
||
builtins.print = logging_print
|
||
|
||
|
||
def create_app() -> Flask:
|
||
"""创建 Flask 应用。"""
|
||
app = Flask(__name__)
|
||
|
||
# 配置
|
||
app.config['SECRET_KEY'] = os.getenv('JWT_SECRET', 'baodan-jwt-secret-2026')
|
||
app.config['JWT_SECRET'] = os.getenv('JWT_SECRET', 'baodan-jwt-secret-2026')
|
||
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
|
||
app.config['REDIS_URL'] = os.getenv('REDIS_URL')
|
||
|
||
# 配置日志
|
||
setup_logging(app)
|
||
|
||
# 数据库:优先 DATABASE_URL,其次 DB_* 变量构建 PostgreSQL URL,最后回退到 SQLite
|
||
db_url = os.getenv('DATABASE_URL', '')
|
||
if not db_url:
|
||
# 从 DB_* 环境变量构建 PostgreSQL URL
|
||
db_host = os.getenv('DB_HOST', '')
|
||
db_port = os.getenv('DB_PORT', '5432')
|
||
db_user = os.getenv('DB_USERNAME', '')
|
||
db_pass = os.getenv('DB_PASSWORD', '')
|
||
db_name = os.getenv('DB_DATABASE', '')
|
||
if db_host and db_user and db_name:
|
||
db_url = f"postgresql://{db_user}:{db_pass}@{db_host}:{db_port}/{db_name}"
|
||
if db_url and 'postgresql' in db_url:
|
||
# 检查 psycopg2 是否可用
|
||
try:
|
||
import psycopg2
|
||
app.config['SQLALCHEMY_DATABASE_URI'] = db_url
|
||
except ImportError:
|
||
print("[warning] psycopg2 未安装,回退到 SQLite")
|
||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db'
|
||
else:
|
||
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///baodan.db'
|
||
app.config['MIGRATION_ENABLED'] = True
|
||
|
||
# Dify 配置
|
||
app.config['BAODAN_API_URL'] = os.getenv('DIFY_BASE_URL', 'http://localhost:5001')
|
||
app.config['BAODAN_CHAT_API_KEY'] = os.getenv('DIFY_CHAT_APP_API_KEY', '')
|
||
app.config['BAODAN_WORKFLOW_API_KEY'] = os.getenv('DIFY_WORKFLOW_APP_API_KEY', '')
|
||
|
||
# 企业微信配置
|
||
app.config['WECOM_CORP_ID'] = os.getenv('WECOM_CORP_ID', '')
|
||
app.config['WECOM_SECRET'] = os.getenv('WECOM_SECRET', '')
|
||
app.config['WECOM_TOKEN'] = os.getenv('WECOM_TOKEN', '')
|
||
app.config['WECOM_AES_KEY'] = os.getenv('WECOM_AES_KEY', '')
|
||
app.config['WECOM_WEBHOOK_URL'] = os.getenv('WECOM_WEBHOOK_URL', '')
|
||
|
||
# 访客模式
|
||
app.config['GUEST_MODE'] = os.getenv('GUEST_MODE', 'true').lower() == 'true'
|
||
|
||
# 域名(分享链接用)
|
||
app.config['DOMAIN'] = os.getenv('DOMAIN', 'localhost')
|
||
|
||
# CORS
|
||
CORS(app, resources={r"/*": {"origins": "*"}})
|
||
|
||
# 初始化数据库
|
||
from insurance.db.database import init_db
|
||
init_db(app)
|
||
|
||
# 初始化 Redis
|
||
from insurance.db.redis import init_redis
|
||
init_redis(app)
|
||
|
||
# 注册路由
|
||
from insurance.routes import register_insurance_routes
|
||
register_insurance_routes(app)
|
||
|
||
# 注册错误处理器
|
||
from insurance.utils.error_handler import register_error_handlers
|
||
register_error_handlers(app)
|
||
|
||
# 健康检查(检查实际依赖)
|
||
@app.route('/health')
|
||
def health():
|
||
checks = {'status': 'ok', 'service': 'baodanagent-api'}
|
||
try:
|
||
from insurance.db.compat import db
|
||
db.session.execute(db.text('SELECT 1'))
|
||
checks['database'] = 'ok'
|
||
except Exception as e:
|
||
checks['database'] = f'error: {str(e)[:100]}'
|
||
checks['status'] = 'degraded'
|
||
try:
|
||
from insurance.db.compat import redis_client
|
||
if redis_client:
|
||
redis_client.ping()
|
||
checks['redis'] = 'ok'
|
||
else:
|
||
checks['redis'] = 'not configured'
|
||
except Exception as e:
|
||
checks['redis'] = f'error: {str(e)[:100]}'
|
||
checks['status'] = 'degraded'
|
||
return checks
|
||
|
||
return app
|
||
|
||
|
||
# 创建应用实例
|
||
app = create_app()
|
||
|
||
if __name__ == '__main__':
|
||
port = int(os.getenv('PORT', 5001))
|
||
debug = os.getenv('FLASK_DEBUG', 'false').lower() == 'true'
|
||
app.run(host='0.0.0.0', port=port, debug=debug)
|