"""保单智能客服系统 — 独立后端入口 独立部署时使用的 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)