Elevate MiroFish/CrowdSight from single-container dev to a SaaS foundation: - Local memory backend (Zep-compatible): memory services/models, local graph builder + updater, AgentActivity seam, import-boundary isolation; Zep stays default, local is opt-in behind MEMORY_BACKEND. Semantic parity not yet proven. - Durable product persistence: projects/simulations/reports schema (migration 0007) + tenant/owner-scoped ProductRepository + dual-write + scoped_project read-first + ArtifactStore abstraction; durable JobQueue + worker.py. - SaaS hardening: durable RateLimiter (wired to login), UsageService (LLM accounting), redacted AuditService, idempotency, CORS allowlist, safe API errors, single-use PasswordResetService + endpoints (covers invite-pending). - Exactly 3 roles (super_admin/admin/user) with tenant authz policy. - Admin UI: GET/POST/PATCH /api/admin/users + GET/PUT /api/admin/settings (super-admin only, encrypted/masked); AdminView.vue + SettingsView.vue with admin/super-admin route guards, th/en i18n. - Production deploy topology: multi-stage Dockerfile (frontend build + gunicorn wsgi + nginx SPA-proxy + supervisord worker), backend/wsgi.py, gunicorn dep. Backend 197 passed; frontend 10 tests + build green. ruff unavailable (gap). No commit of credentials; secrets handled via env/.env.example. Deferred: Zep semantic A/B parity, object storage cutover, mobile QA, EasyPanel container build of deploy topology.
123 lines
4.6 KiB
Python
123 lines
4.6 KiB
Python
"""
|
||
CrowdSight Backend - Flask应用工厂
|
||
"""
|
||
|
||
import os
|
||
import warnings
|
||
|
||
# 抑制 multiprocessing resource_tracker 的警告(来自第三方库如 transformers)
|
||
# 需要在所有其他导入之前设置
|
||
warnings.filterwarnings("ignore", message=".*resource_tracker.*")
|
||
|
||
from flask import Flask, jsonify, request
|
||
from flask_cors import CORS
|
||
from werkzeug.exceptions import HTTPException
|
||
|
||
from .config import Config
|
||
from .db import create_database_engine, create_session_factory
|
||
from .utils.api_errors import ApiError, internal_error_payload
|
||
from .utils.locale import t
|
||
from .utils.logger import setup_logger, get_logger
|
||
|
||
|
||
def create_app(config_class=Config):
|
||
"""Flask应用工厂函数"""
|
||
app = Flask(__name__)
|
||
app.config.from_object(config_class)
|
||
if "*" in app.config.get("CORS_ALLOWED_ORIGINS", []):
|
||
raise RuntimeError("wildcard_cors_not_allowed")
|
||
|
||
database_engine = create_database_engine(os.environ.get("DATABASE_URL"))
|
||
app.extensions["crowdsight_database_engine"] = database_engine
|
||
app.extensions["crowdsight_session_factory"] = create_session_factory(database_engine)
|
||
|
||
# JSON config: keep Unicode characters readable in API responses.
|
||
# Flask >= 2.3 使用 app.json.ensure_ascii,旧版本使用 JSON_AS_ASCII 配置
|
||
if hasattr(app, 'json') and hasattr(app.json, 'ensure_ascii'):
|
||
app.json.ensure_ascii = False
|
||
|
||
# Configure server-side logging before registering error handlers.
|
||
logger = setup_logger('crowdsight')
|
||
|
||
@app.errorhandler(ApiError)
|
||
def handle_api_error(error: ApiError):
|
||
return jsonify(error.to_payload(t)), error.status_code
|
||
|
||
@app.errorhandler(HTTPException)
|
||
def handle_http_error(error: HTTPException):
|
||
logger.warning("HTTP request failed: status=%s", error.code)
|
||
api_error = ApiError(
|
||
code=f"http_{error.code or 500}",
|
||
status_code=error.code or 500,
|
||
message_key="api.requestError",
|
||
)
|
||
return jsonify(api_error.to_payload(t)), error.code or 500
|
||
|
||
@app.errorhandler(Exception)
|
||
def handle_unexpected_error(error: Exception):
|
||
# Keep exception details in server logs only; never serialize them.
|
||
logger.exception("Unhandled request error: %s", type(error).__name__)
|
||
return jsonify(internal_error_payload(t)), 500
|
||
|
||
# Only startup state is logged; request bodies are intentionally excluded.
|
||
is_reloader_process = os.environ.get('WERKZEUG_RUN_MAIN') == 'true'
|
||
debug_mode = app.config.get('DEBUG', False)
|
||
should_log_startup = not debug_mode or is_reloader_process
|
||
|
||
if should_log_startup:
|
||
logger.info("=" * 50)
|
||
logger.info("CrowdSight Backend 启动中...")
|
||
logger.info("=" * 50)
|
||
|
||
# Explicit allowlist only; wildcard CORS is incompatible with auth cookies.
|
||
CORS(
|
||
app,
|
||
resources={r"/api/*": {"origins": app.config.get("CORS_ALLOWED_ORIGINS", [])}},
|
||
supports_credentials=True,
|
||
)
|
||
|
||
# 注册模拟进程清理函数(确保服务器关闭时终止所有模拟进程)
|
||
from .services.simulation_runner import SimulationRunner
|
||
SimulationRunner.register_cleanup()
|
||
if should_log_startup:
|
||
logger.info("已注册模拟进程清理函数")
|
||
|
||
# 请求日志中间件
|
||
@app.before_request
|
||
def log_request():
|
||
logger = get_logger('crowdsight.request')
|
||
logger.debug(f"请求: {request.method} {request.path}")
|
||
if request.content_type and 'json' in request.content_type:
|
||
logger.debug("JSON request received: path=%s", request.path)
|
||
|
||
@app.after_request
|
||
def log_response(response):
|
||
logger = get_logger('crowdsight.request')
|
||
logger.debug(f"响应: {response.status_code}")
|
||
return response
|
||
|
||
# 注册蓝图
|
||
from .api import graph_bp, simulation_bp, report_bp
|
||
from .api.admin import admin_bp
|
||
from .api.auth import auth_bp
|
||
from .api.template import template_bp
|
||
from .api.agent_group import agent_group_bp
|
||
app.register_blueprint(graph_bp, url_prefix='/api/graph')
|
||
app.register_blueprint(auth_bp, url_prefix='/api/auth')
|
||
app.register_blueprint(admin_bp, url_prefix='/api/admin')
|
||
app.register_blueprint(simulation_bp, url_prefix='/api/simulation')
|
||
app.register_blueprint(report_bp, url_prefix='/api/report')
|
||
app.register_blueprint(template_bp, url_prefix='/api/template')
|
||
app.register_blueprint(agent_group_bp, url_prefix='/api/agent-group')
|
||
|
||
# 健康检查
|
||
@app.route('/health')
|
||
def health():
|
||
return {'status': 'ok', 'service': 'CrowdSight Backend'}
|
||
|
||
if should_log_startup:
|
||
logger.info("CrowdSight Backend 启动完成")
|
||
|
||
return app
|
||
|